С момента последней публикации о проекте Fingers прошло много времени... И я во многом продвинулся.
Вводная часть
Ранее я анонсировал проект Fingers2 по распознаванию алфавита глухонемых (РЖЯ). При этом первая версия конструировалась по принципу обнаруженя объекта (yolo), обработка обнаруженного объекта сверточными слоями (conv2D), однако, данная гепориза показала свою не эффективность и сложность. Поэтому Fingers2 был построен на спайке mediapipe и полносвязных (dense) слоев, что показало скорость и высокую обучаемость (>99% на 34 класса). Однако, в fingers2 были использваны не все буквы. Такие буквы как Е – Ё, И – Й и Ш-Щ отличаются только движением руки, но не формой и с учетом конструкции модели, которая обрабатывает слепок кисти руки невозможно было их различить. Перфекционист во мне не мог с этим смириться и вот что получилось…
Теория
Для динамических букв нужно записать их значения в динамике, значит длинна различных образцов одного класса. Более того, такие данные обрабатывает слой LSTM, а значит данные должны быть трехмерные. Ранее разработанный датасет для Fingers2 был двумерный. При этом как разместить последовательности разной размерности в трехмерной структуре типа кубик-рубика? Добавлять четвертое измерение? Решение было найдено – Padding!
Писать длинный текст вручную, я, естественно, привлек LLM и вот что оно пишет:
Padding:
Padding и Masking в LSTM слоях (Keras/TensorFlow)
В рекуррентных нейронных сетях (например, LSTM) входные данные обычно представляют собой последовательности разной длины. Однако нейронные сети требуют фиксированного размера входных данных, поэтому применяются методы padding (дополнение) и masking (маскирование).
Padding (Дополнение последовательностей)
Зачем нужно?
- Последовательности в наборе данных часто имеют разную длину (например, предложения в NLP или временные ряды).
- Чтобы подавать их в LSTM слои, все последовательности приводят к одинаковой длине с помощью дополнения (padding).
Как работает?
- pad_sequences
в Keras дополняет последовательности до заданной длины (
maxlen`).- Дополнение обычно добавляется в начало (
padding='pre'
) или в конец (padding='post'
).- По умолчанию используется нулевое заполнение (
value=0
).
Пример:
from tensorflow.keras.preprocessing.sequence import pad_sequences
sequences = [
[1, 2, 3],
[4, 5],
[6, 7, 8, 9]
]
padded = pad_sequences(sequences, maxlen=4, padding='post')
print(padded)
Вывод:
[[1 2 3 0] # дополнено нулём в конце
[4 5 0 0] # дополнено двумя нулями
[6 7 8 9]] # не изменено (maxlen=4)
Разумеется я решил использовать post-padding, к тому времени я уже запилил простой скрипт, который должен был писать последовательности координат с камеры от нажатия space до нажатия space. Однако, меня мучал вопрос скорости работы моей нейронки. Решение пришло тоже с помощью LLM – masking. Оказывается, умные люди уже обо всем подумали и включили в пакет Keras слой Masking.
Masking (Маскирование дополненных значений)
Зачем нужно?
- После padding в данных появляются "искусственные" нули, которые не являются реальными данными .
- LSTM должен игнорировать эти нули при обучении, чтобы они не влияли на градиенты.
- Для этого используется маскирование (masking) .
Как работает?
- В Keras можно добавить слой Masking перед LSTM.
- Маскирование автоматически пропускает нули (или другие указанные значения).
Пример:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Masking
model = Sequential([
Masking(mask_value=0, input_shape=(4, 1)), # маскирует нули
LSTM(32)
])
- Теперь LSTM будет игнорировать нули, добавленные при padding.
Таким образом, добавляя в датасет синтетические данные, возможно их игнорировать при обработке. Нули, конечно, не сильно нагружают систему, но их количество вполне может сыграть злую шутку. При этом mediapipe, по всей видимости создавался с расчетом на обработку данных нейронными сетями и выдает значения приближенные к нулю, это позволяет задействовать меньше ресурсов для матричных вычислений, т.к. машина считает быстрее, но ей, как и человеку, требуется больше ресурсов для вычислений больших цифр.
При этом можно использовать две гипотезы с Masking и без по результатам исполнения которых просто замерить скорость обработки. Что меня вполне устраивало. Поэтому я принялся за дело…
Сама LLM предложила по запросу полноценный код для загрузки нового «динамического датасета», однако предложенный ей код загрузки старого датасета сокращал после чистки датасет с 74 000 до 4 000, что не могло меня устраивать, поэтому я взял ранее написанный код от Fingers2.
# функция загрузки файлов букв из директории
# принимает: путь к файлам
# возвращает: очищеный dataFrame
import os
import re
def loadDatasetF2(dir ="/content/drive/MyDrive/Data/fingers/fingers-csv/"):
#listFiles=os.listdir(path=dir)
listFiles = [f for f in os.listdir(dir)]
listFiles.sort()
dataset = pd.DataFrame()
print(listFiles)
print(len(listFiles))
result = pd.DataFrame() # columns=titles
for i in listFiles:
dataFile =
pd.read
_csv(dir+i, sep=",", float_precision='round_trip', encoding='latin1',on_bad_lines='skip', header=[0]) # header=None
# on bad lines для исключения обшибки токенизации
dataFile.dropna(inplace=True)
print("File:", i, "size:",dataFile.shape)
Как видно pd.read_csv грузит датасет с большим количетвом дополнительных параметров, но это все позволяет получить на выходе наиболее чистый dataframe, однако и его пришлось обрабатывать для lstmслоев, при этом полносвязные (dense) слои принимали его и без дополнительной обработки. Выйти на путь дополнительной обработки помогла опять же таки LLM, которая похоже хранит большое количество exception и их решений
Пока модель Fingers3 выглядит так:
# Создание модели LSTM
model = Sequential([
# Первый LSTM слой с возвратом последовательности для stacked LSTM
LSTM(128, return_sequences=True, input_shape=(21, 63)),
Dropout(0.2),
# Второй LSTM слой
LSTM(64, return_sequences=False),
Dropout(0.2),
# Полносвязные слои
Dense(64, activation='relu'),
Dropout(0.2),
# Выходной слой (35 нейронов, по количеству выходных признаков)
Dense(35, activation='softmax') # или 'sigmoid' в зависимости от задачи
])
С гипер-параметрами я пока не игрался, возможно поручу это дело генетическуому алгоритму, т.к. он переберет большее количество и быстрее. Без всякого колдовства на 10 эпохах получил "accuracy: 0.9831 - loss: 0.0621 - val_accuracy: 0.9888 - val_loss: 0.0365", что прямо прекрасно исходя из того что эпоха обучает 1.5 минуты.
При все при этом Fingers2 доступен на ginhub (https://github.com/LEbEdEV79/Fingers) На его базе я даже сделал приложение (kivy) – FingersGame , однако, пока не получается собрать полноценный пакет для телефона. Буду благодарен за помощь, если есть специалисты по этому вопросу.

По сюжету игры злая Барыня обижает Герасима - владельца собаки и ставит ему задачу из серии: "Ты сумей-ка мне добыть, то чего не может быть"

Поручает Герасиму обучить Му-му разоговаривать или прикажет ее утопить. Герасим, как мы помним глухонемой, начинает ее учить жестовому языку.

Все картинки, естественно, генерированные, т.к. мне еще рисовать только начать не хватает для полного счастья ;-)
В принципе та подготовка, которая проведена позволяет добавить сканирование второй руки, лица и тела что даст возможность обучать модель полноценному языку, а не только буквам. Самый известный датасет жестового языка уже попал в мои цепкие руки, но думаю без носителя жестового языка мало чего хорошего получится, потому как даже с алфавитом есть акценты. Скорее всего далее я займусь полноценным РЖЯ и получится переводчик, а если он залезет в телефон - можно будет понимать глухонемого через телефон напрямую. Думаю, для финально обработки придется использовать LLM по api, но это тема платная... может придется перепродавать чужие токены или что-то еще придумывать
С уважением, адвокат Антон Лебедев [реклама удалена мод.]