Распознавание языка жестов, LSTM и ragged tf.Dataset

Какое-то время назад на Kaggle проходило соревнование по распознаванию языка жестов: https://www.kaggle.com/competitions/asl-signs

Собственно были размеченные данные со скелетом человека, лицом и скелетом кисти. Например, для кисти выглядит так:

__results___9_0

Были различного рода решения, в том числе особо популярные на основе Трансформеров, но здесь мы рассмотрим решение на основе LSTM и ragged tf.Dataset. Пример решения здесь:

https://www.kaggle.com/code/jvthunder/lstm-baseline-for-starters-sign-language/notebook

Однако здесь мы его рассмотрим с изменениями, которые улучшают качество решения на 10%. А пример создания ragged tf.Dataset здесь:

https://www.kaggle.com/code/aapokossi/how-to-save-parquet-data-as-ragged-tf-dataset?scriptVersionId=121106452

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()

Вот ее структура:

2023-05-29_17-35-32

Тренировка модели:

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

В остальном с задачей и способами ее решения можете ознакомиться на сайте соревнования, указанного в начале статьи