570. Google - American Sign Language Fingerspelling Recognition | asl-fingerspelling
首先,我要向我的队友致以最诚挚的感谢:
他们的巨大努力对这次竞赛的成功至关重要。
同时,我也感谢以下笔记本的作者。没有这些 notebook 中的见解和技术,我们的成功是不可能实现的。非常感谢你们。
@irohith 出色的预处理和训练 notebook:
@hoyso48 上一届竞赛的冠军解决方案:
@greysnow 的TPU上CTC notebook:
@royalacecat 关于更多量化模块的 notebook:
我们的解决方案基于CTC开源 notebook。以下是我们引入的关键改进(所有分数均为公开LB):
参考 @irohith 的预处理 notebook,我们通过添加更多面部和姿态索引进行了改进。我们根据方差选择新的索引——方差越高表示信息越多。
最初的索引为:
LIP = [
61, 185, 40, 39, 37, 0, 267, 269, 270, 409,
291, 146, 91, 181, 84, 17, 314, 405, 321, 375,
78, 191, 80, 81, 82, 13, 312, 311, 310, 415,
95, 88, 178, 87, 14, 317, 402, 318, 324, 308,
]
LPOSE = [13, 15, 17, 19, 21]
RPOSE = [14, 16, 18, 20, 22]
POSE = LPOSE + RPOSE
我们将其更新为:
LIP = [
61, 185, 40, 39, 37, 0, 267, 269, 270, 409,
291, 146, 91, 181, 84, 17, 314, 405, 321, 375,
78, 191, 80, 81, 82, 13, 312, 311, 310, 415,
95, 88, 178, 87, 14, 317, 402, 318, 324, 308,
]
# 新的面部ID索引
face_id = [454,356,323,361,389,288,251,264,447,366,368,
401,397,435,284,301,372,345,383,367,365,352,433,
376,298,265,93,234,300,132,340,353,127]
# 如果新索引不存在于LIP中,则追加
for k in face_id:
if k not in LIP:
LIP.append(k)
# 修剪LIP列表
l = len(LIP)
LIP = LIP[:int(l - l/4)]
# 更新后的POSE索引
LPOSE = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31]
RPOSE = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
POSE = LPOSE + RPOSE
过滤完全NaN的帧
原始 notebook:
hand = tf.concat([rhand, lhand], axis=1)
hand = tf.where(tf.math.is_nan(hand), 0.0, hand)
mask = tf.math.not_equal(tf.reduce_sum(hand, axis=[1, 2]), 0.0)
更新为:
hand = tf.concat([rhand, lhand,lip,rpose,lpose], axis=1)
hand = tf.where(tf.math.is_nan(hand), 0.0, hand)
mask = tf.math.not_equal(tf.reduce_sum(hand, axis=[1, 2]), 0.0)
这些新索引加上256帧长度,给我们带来了约0.02 LB的提升!
详见 1st Place Solution Training
为了提高模型训练的稳定性,我们实现了带正则化组件的CTC损失。通过将CTC损失与Kullback-Leibler (KL) 散度结合,我们引入了标签平滑。平滑权重=0.7对我们的模型效果最佳。
def smooth_ctc_loss(labels, logits, num_classes = 60 , blank=0, weight=0.7):
# 计算CTC损失
label_length = tf.reduce_sum(tf.cast(labels != pad_token_idx, tf.int32), axis=-1)
logit_length = tf.ones(tf.shape(logits)[0], dtype=tf.int32) * tf.shape(logits)[1]
ctc_loss = tf.nn.ctc_loss(
labels=labels,
logits=logits,
label_length=label_length,
logit_length=logit_length,
blank_index=pad_token_idx,
logits_time_major=False
)
ctc_loss = tf.reduce_mean(ctc_loss)
# 计算KL散度损失
kl_inp = tf.nn.softmax(logits)
# 创建目标分布
kl_tar = tf.fill(tf.shape(logits), 1. / num_classes)
# 计算KL散度
kldiv_loss = (tf.keras.losses.KLDivergence(tf.keras.losses.Reduction.NONE)(kl_tar, kl_inp)
+ tf.keras.losses.KLDivergence(tf.keras.losses.Reduction.NONE)(kl_inp, kl_tar))/2.0
kldiv_loss = tf.reduce_mean(kldiv_loss)
# 组合损失
loss = (1. - weight) * ctc_loss + weight * kldiv_loss
return loss
在实验过程中,我们发现了一个重要洞察:仅使用补充数据集训练获得了0.367的LB分数。这凸显了补充数据中蕴含的巨大价值。
我们决定先在补充数据集上预训练模型,然后加载这些预训练权重来在主数据集上进一步训练。我们测试了40、60和80个预训练轮数。80轮效果最佳,为LB分数带来了0.013的提升。
数据增强:
在第二名的解决方案中,@hoyso48提到某些增强在短期训练中并未改善模型,但在更长时间的训练中会产生效果。这可能也适用于我们的情况,但我们尚未验证。
切换到GIC-CTC:
训练 notebook 专为Google Colab设计。首先使用gcs-path notebook检查训练 notebook 中的路径。先将USE_SUPPLY设为True训练补充集,然后将USE_VAL设为True加载预训练权重并训练主数据集。
要在Kaggle TPU上使用,请将文件地址更改为对应的Kaggle输入地址,并且需要修改CTC损失函数。参考 ASLFR CTC on TPU