С момента последней публикации о проекте 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, но это тема платная... может придется перепродавать чужие токены или что-то еще придумывать
С уважением, адвокат Антон Лебедев [реклама удалена мод.]
