546. Google - Isolated Sign Language Recognition | asl-signs
首先,我要感谢谷歌主办这场精彩的竞赛。我一直以来都是谷歌服务、框架和平台(Colab、GCP、TensorFlow等)的忠实粉丝。如果没有谷歌提供的这些优秀资源,我不可能赢得这场比赛。同时,我也感谢所有分享想法的参赛者们,特别是@hengck23的见解让我受益匪浅。
我的解决方案结合了1D CNN和Transformer模型,仅使用比赛数据从头训练,并通过4个不同种子的模型集成进行提交。最初使用PyTorch+GPU开发,后来切换到TensorFlow+Colab TPU(tpuv2-8)以确保与TensorFlow Lite的兼容性。
我的假设是:在序列数据建模中,如果存在强烈的帧间相关性,1D CNN会比Transformer更高效。
实验证明,纯1D CNN模型轻松超越了Transformer,仅使用1D CNN就在Public LB上达到了0.80的分数。
然而,Transformer仍然有其价值,它可以作为1D CNN的上层结构(我们可以将1D CNN视为一种可训练的tokenizer)。
def get_model(max_len=64, dropout_step=0, dim=192):
inp = tf.keras.Input((max_len,CHANNELS))
x = tf.keras.layers.Masking(mask_value=PAD,input_shape=(max_len,CHANNELS))(inp)
ksize = 17
x = tf.keras.layers.Dense(dim, use_bias=False,name='stem_conv')(x)
x = tf.keras.layers.BatchNormalization(momentum=0.95,name='stem_bn')(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = TransformerBlock(dim,expand=2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = TransformerBlock(dim,expand=2)(x)
if dim == 384: #for the 4x sized model
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = TransformerBlock(dim,expand=2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
x = TransformerBlock(dim,expand=2)(x)
x = tf.keras.layers.Dense(dim*2,activation=None,name='top_conv')(x)
x = tf.keras.layers.GlobalAveragePooling1D()(x)
x = LateDropout(0.8, start_step=dropout_step)(x)
x = tf.keras.layers.Dense(NUM_CLASSES,name='classifier')(x)
return tf.keras.Model(inp, x)
CNN与Transformer的结合是近年来前沿模型的常见设计思路(如coatnet、conformer、Maxvit、nextvit等)。我从192维8层1D CNN开始,后来切换到192维(3+1)×2的conv-transformer结构,使CV和LB分数提升了+0.01。
1D CNN模型采用深度可分离卷积和因果填充。Transformer使用BatchNorm + Swish替代常规的LayerNorm + GELU,在保持相同精度的同时略微减轻了推理负担。单个模型约有185万参数。
正确处理可变长度输入对确保训练-测试一致性和高效推理至关重要,因为短视频不一定需要填充。训练时使用max_len=384并进行填充和截断,推理时仅应用截断。这种方法提供了足够的推理速度,并允许使用相当大的模型。为了准确地将掩码应用于1D CNN,我使用因果填充来保持掩码索引。在TensorFlow中,只需在模型开头使用tf.keras.layers.Masking即可轻松实现掩码。此外,必须确保掩码被准确应用于批归一化和全局平均池化等可能受掩码影响的操作。
由于需要从头训练模型,正则化技术发挥了关键作用。我使用了drop_path(0.2,应用于每个块之后)、dropout(0.8,应用于GAP之后)和AWP(对抗性权重扰动,lambda=0.2),其中AWP和dropout在第15个epoch后应用。所有这些方法对于防止超过300个epoch的长周期训练过拟合都至关重要。这三种方法对CV和排行榜分数都有显著影响,移除其中任意一个都会导致性能明显下降。
class Preprocess(tf.keras.layers.Layer):
def __init__(self, max_len=MAX_LEN, point_landmarks=POINT_LANDMARKS, **kwargs):
super().__init__(**kwargs)
self.max_len = max_len
self.point_landmarks = point_landmarks
def call(self, inputs):
if tf.rank(inputs) == 3:
x = inputs[None,...]
else:
x = inputs
mean = tf_nan_mean(tf.gather(x, [17], axis=2), axis=[1,2], keepdims=True)
mean = tf.where(tf.math.is_nan(mean), tf.constant(0.5,x.dtype), mean)
x = tf.gather(x, self.point_landmarks, axis=2) #N,T,P,C
std = tf_nan_std(x, center=mean, axis=[1,2], keepdims=True)
x = (x - mean)/std
if self.max_len is not None:
x = x[:,:self.max_len]
length = tf.shape(x)[1]
x = x[...,:2]
dx = tf.cond(tf.shape(x)[1]>1,lambda:tf.pad(x[:,1:] - x[:,:-1], [[0,0],[0,1],[0,0],[0,0]]),lambda:tf.zeros_like(x))
dx2 = tf.cond(tf.shape(x)[1]>2,lambda:tf.pad(x[:,2:] - x[:,:-2], [[0,0],[0,2],[0,0],[0,0]]),lambda:tf.zeros_like(x))
x = tf.concat([
tf.reshape(x, (-1,length,2*len(self.point_landmarks))),
tf.reshape(dx, (-1,length,2*len(self.point_landmarks))),
tf.reshape(dx2, (-1,length,2*len(self.point_landmarks))),
], axis = -1)
x = tf.where(tf.math.is_nan(x),tf.constant(0.,x.dtype),x)
return x
我使用了左右手、眼睛、鼻子和嘴唇的 landmarks。归一化时使用位于鼻子的第17个 landmark 作为参考点,因为它通常位于中心位置([0.5, 0.5])。我还使用了滞后1阶(x[:1] - x[1:])和滞后2阶(x[:2] - x[2:])的运动特征(滞后超过2阶帮助不大)。
时间增强
空间增强
最终单模型结果
CV(参与者5折分割):0.80
Public LB:0.80
Private LB:0.88
使用Colab TPUv2-8训练大约需要4小时。单模型在参与者5折分割的CV上最终达到约0.80。我集成了4个不同种子的模型(训练配置有细微差异)得到最终LB 0.81。另外,使用相同设置提交4倍大小(384维,16层)的单模型分数略低,我认为通过更好的配置可以达到或超越该分数。
我总是惊叹于这样一个事实:我们尝试彼此的方法,却得到了不同的结果。我也尝试了2D-CNN(类似@kolyaforrat的精彩方案)和纯Transformer方法,但由于初始性能不佳以及对我自己假设的信心,我没有深入挖掘。看到其他团队用我遇到困难的方法通过他们的技能和努力取得成功,真的令人鼓舞。我从这场比赛中学到了很多,感谢这次经历!谢谢大家!!! :)
编辑:我已公开代码,查看https://www.kaggle.com/competitions/asl-signs/discussion/406978