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

С уважением, адвокат Антон Лебедев [реклама удалена мод.]