返回列表

🥈18th Place Solution🥈

546. Google - Isolated Sign Language Recognition | asl-signs

开始: 2023-02-23 结束: 2023-05-01 音视频处理 数据算法赛
作者:siwooyong (Kaggle Master)
排名:第18名
发布时间:2023年5月2日

🥈18th Place Solution🥈

TL;DR

根据我的实验,对性能提升最显著的因素是正则化特征处理嵌入层。最终的集成模型包含了多种输入特征和模型,包括Transformer、MLP和GRU。

数据处理

在提供的原始数据中,共有543个关键点,其中使用了115个关键点,分别对应手部、姿态和嘴唇。输入特征通过拼接xy(z)坐标、运动信息和距离信息构建,最终输入特征维度为1196。最初仅使用了手部数据的距离特征,但通过添加姿态和嘴唇数据的距离特征,实现了显著的性能提升(交叉验证得分+0.05)。

feature = tf.concat([
        tf.reshape(xyz_hand[:, :21, :3], [-1, 21 * 3]), 
        tf.reshape(xyz_pose[:, 21:46, :2], [-1, 25 * 2]), 
        tf.reshape(xyz_lip[:, 46:66, :2], [-1, 20 * 2]), 
        tf.reshape(motion_hand[:, :21, :3], [-1, 21 * 3]), 
        tf.reshape(motion_pose[:, 21:46, :2], [-1, 25 * 2]), 
        tf.reshape(motion_lip[:, 46:66, :2], [-1, 20 * 2]), 
        tf.reshape(distance_hand, [-1, 210]),
        tf.reshape(distance_pose, [-1, 300]),
        tf.reshape(distance_outlip, [-1, 190]),
        tf.reshape(distance_inlip, [-1, 190]),
    ], axis=-1)

此外,我们选择使用NaN值较少的那只手,并从姿态关键点中移除了腿部关键点。基于此,我们创建了两个版本的输入数据:第一个版本仅使用包含非NaN手部数据的帧,最大长度为100;第二个版本包含存在NaN手部数据的帧,最大长度为200。

cond = lefth_sum > righth_sum
h_x = tf.where(cond, lefth_x, righth_x)
xfeat = tf.where(cond, tf.concat([lefth_x, pose_x, lip_x], axis = 1), tf.concat([righth_x, pose_x, lip_x], axis = 1))

数据增强

我们选择使用NaN值较少的那只手,并将两只手都翻转为右手输入模型,这种方法实际带来了约0.01的交叉验证得分提升。

# x轴镜像
cond = lefth_sum > righth_sum
xfeat_xcoordi = xfeat[:, :, 0]
xfeat_else = xfeat[:, :, 1:]
xfeat_xcoordi = tf.where(cond, -xfeat_xcoordi, xfeat_xcoordi)
xfeat = tf.concat([xfeat_xcoordi[:, :, tf.newaxis], xfeat_else], axis = -1)

我并未观察到翻转、旋转、mixup等数据增强技术对交叉验证得分有提升作用,因此我通过集成上述两个版本输入数据的模型来弥补这一点。

模型架构

在作为Transformer模型输入之前,输入特征(xy(z)坐标、运动信息和距离信息)各自通过独立的嵌入层进行处理。与不独立处理特征相比,这种独立处理方式带来了0.01的交叉验证得分提升。

# 嵌入层
xy = xy_embeddings(xy)
motion = motion_embeddings(motion)
distance_hand = distance_hand_embeddings(distance_hand)
distance_pose = distance_pose_embeddings(distance_pose)
distance_outlip = distance_outlip_embeddings(distance_outlip)
distance_inlip = distance_inlip_embeddings(distance_inlip)

x = tf.concat([xy, motion, distance_hand, distance_pose, distance_outlip, distance_inlip], axis=-1)
x = relu(x)
x = fc_layer(x)
x = TransformerModel(input_ids = None, inputs_embeds=x, attention_mask=x_mask).last_hidden_state

对于Transformer模型,我使用了Hugging Face的RoBERTa-PreLayerNorm、DeBERTaV2和GPT2。输入数据分别对xyz坐标、运动信息和距离信息进行独立处理,然后拼接形成300维的Transformer输入。Transformer的输出通过均值、最大值和標準差池化后拼接得到最终输出。

# 池化函数
def get_pool(self, x, x_mask):
    x = x * tf.expand_dims(x_mask, axis=-1)  # 应用掩码
    nonzero_count = tf.reduce_sum(x_mask, axis=1, keepdims=True)  # 统计非零元素数量
    max_discount = (1-x_mask)*1e10

    apool = tf.reduce_sum(x, axis=1) / nonzero_count
    mpool = tf.reduce_max(x - tf.expand_dims(max_discount, axis=-1), axis=1)
    spool = tf.sqrt((tf.reduce_sum(((x - tf.expand_dims(apool, axis=1)) ** 2) * tf.expand_dims(x_mask, axis=-1), axis=1) / nonzero_count) + 1e-9)
    return tf.concat([apool, mpool, spool], axis=-1)

除了Transformer模型外,简单的线性模型和GRU模型也取得了与Transformer模型相似的性能,因此我将这三类模型进行了集成。

训练配置

  • 学习率调度器: lr_warmup_cosine_decay
  • 预热比例: 0.2
  • 优化器: AdamW
  • 权重衰减: 0.01
  • 训练轮数: 40
  • 学习率: 1e-3
  • 损失函数: CrossEntropyLoss
  • 标签平滑值: 0.65 ~ 0.75

正则化技术

在模型训练过程中,有三种主要的正则化技术对提升收敛速度和最终性能做出了显著贡献:

  • 在最终线性层应用权重归一化论文

    final_layer = torch.nn.utils.weight_norm(nn.Linear(hidden_size, 250))
  • 在最终线性层前应用批归一化

    # 头部网络
    x = fc_layer(x)
    x = batchnorm1d(x)
    x = relu(x)
    x = dropout(x)
    x = final_layer(x)
  • 配合AdamW使用较高的权重衰减值

TFLite转换

在竞赛初期,我使用PyTorch进行开发,但在转换为TFLite时遇到了大量错误,最终无法处理动态输入形状。在竞赛后期,我转用Keras,TFLite转换过程中的错误显著减少。

未奏效的方法

  • 添加距离特征有助于性能提升,但添加角度和方向特征并无帮助
  • 增加Transformer层数并未带来性能提升
  • 我尝试使用Transformer或GAT对关键点之间的关系进行建模,但模型推理速度变慢,性能反而下降
  • 针对XYZ坐标的类BERT预训练(MLM)在竞赛提供的数据上并未提升性能

心得与体会

我目前正在韩国服兵役。在军队训练期间,我利用间隙时间进行编程学习,收获颇丰。Kaggle社区分享各种想法并进行讨论的文化让我深受震撼。通过这种社区协作,能够诞生出更伟大、更优秀的作品,这非常棒。我也希望未来能更积极地参与其中。感谢阅读我的分享。

同比赛其他方案