Распознавание языка жестов, LSTM и ragged tf.Dataset
Какое-то время назад на Kaggle проходило соревнование по распознаванию языка жестов: https://www.kaggle.com/competitions/asl-signs
Собственно были размеченные данные со скелетом человека, лицом и скелетом кисти. Например, для кисти выглядит так:
Были различного рода решения, в том числе особо популярные на основе Трансформеров, но здесь мы рассмотрим решение на основе LSTM и ragged tf.Dataset. Пример решения здесь:
https://www.kaggle.com/code/jvthunder/lstm-baseline-for-starters-sign-language/notebook
Однако здесь мы его рассмотрим с изменениями, которые улучшают качество решения на 10%. А пример создания ragged tf.Dataset здесь:
LSTM
LSTM расшифровывается, как долгая краткосрочная память. LSTM были специально разработаны для преодоления проблемы долговременной зависимости. Способность запоминать информацию на длительные периоды времени является для них стандартным поведением, а не чем-то, что им приходится трудно обучать.
Структура сети была придумана в 1997 году, поэтому нет нужды описывать её здесь.
Основная идея долгой краткосрочной памяти (LSTM) заключается в том, чтобы позволить модели рекуррентных нейронных сетей сохранять и использовать информацию на длительные периоды времени. Она достигается за счет использования специальных блоков памяти, которые позволяют удалять, добавлять и сохранять информацию в зависимости от необходимости.
Блок LSTM состоит из нескольких взаимодействующих между собой слоев, которые позволяют модели управлять потоком информации, сохранять ее в память и забывать ее при необходимости. В частности, блок LSTM содержит специальные ячейки памяти, которые могут сохранять информацию на долгие периоды времени, а также контроллеры, которые позволяют управлять этими ячейками и регулировать поток информации через блок LSTM.
Таким образом, благодаря использованию блоков памяти и контроллеров, LSTM позволяют модели рекуррентных нейронных сетей лучше улавливать долговременные зависимости в данных и использовать эту информацию для более точного прогнозирования и генерации текста.
ragged tf.Dataset
tf.RaggedTensor или рваные тензоры — это эквивалент TensorFlow вложенных списков переменной длины. Они упрощают хранение и обработку данных неоднородной формы.
С помощью регулярного tf.Dataset и RaggedTensor можно эффективно обрабатывать данные, содержащие последовательности переменной длины, такие как тексты различной длины или аудиофайлы с разной продолжительностью. Ragged tf.Dataset позволяет создавать датасеты, где каждый элемент может иметь разную длину вложенных последовательностей, что очень удобно для обработки различных типов данных.
В задаче распознавания языка жестов, данные - это:
- последовательность кадров произвольной длины;
- в каждом кадре присутствует фиксированный набор точек скелета, кистей и лица в формате X,Y,Z.
Код сохранения данных в ragged Dataset здесь:
import tensorflow as tf
import pandas as pd
import numpy as np
import json
from tqdm import tqdm
# Загрузка словаря знаков языка жестов
with open('/kaggle/input/asl-signs/sign_to_prediction_index_map.json') as f:
sign_ids = json.load(f)
print(sign_ids)
DATA_COLUMNS = ['x', 'y', 'z']
ROWS_PER_FRAME = 543 # Общее количество точек скелета, кистей и лица
NUM_SHARDS = 2
SAVE_PATH = '/tmp/GoogleISLDataset'
BATCH_SIZE = 256
# Функции загрузки
def load_relevant_data_subset(pq_path):
data = pd.read_parquet('/kaggle/input/asl-signs/'+pq_path, columns=DATA_COLUMNS)
n_frames = int(len(data) / ROWS_PER_FRAME)
data = data.values.astype(np.float32)
return data.reshape(n_frames, ROWS_PER_FRAME, len(DATA_COLUMNS))
def tf_get_features(ftensor):
def feat_wrapper(ftensor):
return load_relevant_data_subset(ftensor.numpy().decode('utf-8'))
return tf.py_function(
feat_wrapper,
[ftensor],
Tout=tf.float32
)
def set_shape(x):
# None dimensions can be of any length
return tf.ensure_shape(x, (None, ROWS_PER_FRAME, len(DATA_COLUMNS))
# Генерация датасета
train_df = pd.read_csv('/kaggle/input/asl-signs/train.csv')
X_ds = tf.data.Dataset.from_tensor_slices(
train_df.path.values # start with a dataset of the parquet paths
).map(
tf_get_features # load individual sequences
).map(
set_shape # set and enforce element shape
).apply(
tf.data.experimental.dense_to_ragged_batch(batch_size=BATCH_SIZE) # apply batching function
)
# load and batch the labels
y_ds = tf.data.Dataset.from_tensor_slices(
train_df.sign.map(sign_ids).values.reshape(-1,1)
).batch(BATCH_SIZE)
# zip the features and labels
train_ds = tf.data.Dataset.zip((X_ds, y_ds))
# Сохранение на диск
def shard_func(*_):
return tf.random.uniform(shape=[, maxval=NUM_SHARDS, dtype=tf.int64)
train_ds.prefetch(tf.data.AUTOTUNE).save(SAVE_PATH, shard_func=shard_func)
Обучение LSTM модели
Сначала загрузка ragged Dataset с диска:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers, optimizers
LANDMARK_FILES_DIR = "/kaggle/input/asl-signs/train_landmark_files"
TRAIN_FILE = "/kaggle/input/asl-signs/train.csv"
# Set constants and pick important landmarks
lip_marks = [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291, 78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308, 95, 88, 178, 87, 14, 317, 402, 318, 324, 146, 91, 181, 84, 17, 314, 405, 321, 375]
LEFT_HAND_OFFSET = 468
POSE_OFFSET = LEFT_HAND_OFFSET+21
RIGHT_HAND_OFFSET = POSE_OFFSET+33
LANDMARK_IDX = lip_marks + list(range(LEFT_HAND_OFFSET,POSE_OFFSET))+ list(range(RIGHT_HAND_OFFSET,543))
DATA_PATH = "/kaggle/input/saved-tfdataset-of-google-isl-recognition-data/GoogleISLDatasetBatched"
DS_CARDINALITY = 185
VAL_SIZE = 20
N_SIGNS = 250
ROWS_PER_FRAME = 543
def preprocess(ragged_batch, labels):
ragged_batch = tf.gather(ragged_batch, LANDMARK_IDX, axis=2)
ragged_batch = tf.where(tf.math.is_nan(ragged_batch), tf.zeros_like(ragged_batch), ragged_batch)
return tf.concat([ragged_batch[...,i] for i in range(3)],-1), labels
dataset = tf.data.Dataset.load(DATA_PATH)
dataset = dataset.map(preprocess)
train_ds = dataset.take(DS_CARDINALITY-VAL_SIZE).cache().shuffle(20).prefetch(tf.data.AUTOTUNE)
val_ds = dataset.skip(DS_CARDINALITY-VAL_SIZE).cache().prefetch(tf.data.AUTOTUNE)
В функции preprocess выбираются только нужные точки, которые используются в обучении.
Создание модели на основе LSTM:
def get_callbacks():
return [
tf.keras.callbacks.EarlyStopping(
monitor="val_accuracy",
patience=10,
restore_best_weights=True
),
tf.keras.callbacks.ReduceLROnPlateau(
monitor = "val_accuracy",
factor = 0.5,
patience = 3
),
]
def dense_block(units, name):
fc = layers.Dense(units)
norm = layers.LayerNormalization()
act = layers.Activation("gelu")
return lambda x: act(norm(fc(x)))
def classifier(lstm_units):
lstm = layers.LSTM(lstm_units)
drop = tf.keras.layers.Dropout(0.4)
out = layers.Dense(256)
return lambda x: out(drop(lstm(x)))
encoder_units = [512, 256]
lstm_units = 256
#define the inputs (ragged batches of time series of landmark coordinates)
inputs = tf.keras.Input(shape=(None,3*len(LANDMARK_IDX)), ragged=True)
# dense encoder model
x = inputs
for i, n in enumerate(encoder_units):
x = dense_block(n, f"encoder_{i}")(x)
x = tf.keras.layers.Dropout(0.4)(x)
# classifier model
out = classifier(lstm_units)(x)
out = layers.Dense(N_SIGNS, activation="softmax")(out)
model = tf.keras.Model(inputs=inputs, outputs=out)
model.summary()
Вот ее структура:
Тренировка модели:
steps_per_epoch = DS_CARDINALITY - VAL_SIZE
boundaries = [steps_per_epoch * n for n in [30,50,70]]
values = [1e-3,1e-4,1e-5,1e-6]
lr_sched = optimizers.schedules.PiecewiseConstantDecay(boundaries, values)
optimizer = optimizers.Adam(lr_sched)
model.compile(optimizer=optimizer,
loss="sparse_categorical_crossentropy",
metrics=["accuracy","sparse_top_k_categorical_accuracy"])
model.fit(train_ds,
validation_data = val_ds,
callbacks = get_callbacks(),
epochs = 100)
Конечными показателями модели были:
loss: 0.6966 - accuracy: 0.8141 - sparse_top_k_categorical_accuracy: 0.9385 - val_loss: 1.0505 - val_accuracy: 0.7445 - val_sparse_top_k_categorical_accuracy: 0.9061 - lr: 1.0000e-06
В остальном с задачей и способами ее решения можете ознакомиться на сайте соревнования, указанного в начале статьи