555. Parkinsons Freezing of Gait Prediction | tlvmc-parkinsons-freezing-gait-prediction
感谢Kaggle和比赛主办方举办这场比赛,也祝贺其他获奖团队!这是我第一次全身心投入的Kaggle比赛,这是一次非常棒的经历,我真正学到了很多。
对我而言表现最好的模型是多层GRU模型的一个变体,在GRU层之间添加了一些残差连接和全连接层:
以下是对应此模型的PyTorch类:
class ResidualBiGRU(nn.Module):
def __init__(self, hidden_size, n_layers=1, bidir=True):
super(ResidualBiGRU, self).__init__()
self.hidden_size = hidden_size
self.n_layers = n_layers
self.gru = nn.GRU(
hidden_size,
hidden_size,
n_layers,
batch_first=True,
bidirectional=bidir,
)
dir_factor = 2 if bidir else 1
self.fc1 = nn.Linear(
hidden_size * dir_factor, hidden_size * dir_factor * 2
)
self.ln1 = nn.LayerNorm(hidden_size * dir_factor * 2)
self.fc2 = nn.Linear(hidden_size * dir_factor * 2, hidden_size)
self.ln2 = nn.LayerNorm(hidden_size)
def forward(self, x, h=None):
res, new_h = self.gru(x, h)
# res.shape = (batch_size, sequence_size, 2*hidden_size)
res = self.fc1(res)
res = self.ln1(res)
res = nn.functional.relu(res)
res = self.fc2(res)
res = self.ln2(res)
res = nn.functional.relu(res)
# 残差连接
res = res + x
return res, new_h
class MultiResidualBiGRU(nn.Module):
def __init__(self, input_size, hidden_size, out_size, n_layers, bidir=True):
super(MultiResidualBiGRU, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.out_size = out_size
self.n_layers = n_layers
self.fc_in = nn.Linear(input_size, hidden_size)
self.ln = nn.LayerNorm(hidden_size)
self.res_bigrus = nn.ModuleList(
[
ResidualBiGRU(hidden_size, n_layers=1, bidir=bidir)
for _ in range(n_layers)
]
)
self.fc_out = nn.Linear(hidden_size, out_size)
def forward(self, x, h=None):
# 如果位于序列开头(无隐藏状态)
if h is None:
# (重新)初始化隐藏状态
h = [None for _ in range(self.n_layers)]
x = self.fc_in(x)
x = self.ln(x)
x = nn.functional.relu(x)
new_h = []
for i, res_bigru in enumerate(self.res_bigrus):
x, new_hi = res_bigru(x, h[i])
new_h.append(new_hi)
x = self.fc_out(x)
return x, new_h # log概率 + 隐藏状态
请注意,代码可以简化:对于我的最佳模型(私有排行榜得分0.417),"h"实际上总是用None初始化的。
在数据方面,我为模型所做的选择非常简单:只考虑加速度计数据(AccV、AccML、AccAP),将tdcsfog和defog的数据合并在一起,并训练一个单一模型。我的预处理流程主要步骤如下:
除了来自defog的无标签时间步外,我没有使用任何其他无标签数据。
对于另一个我没时间完成的模型原型,我也开始考虑一些与产生序列的人相关的特征,特别是使用了"Visit"、"Age"、"Sex"、"YearsSinceDx"、"UPDRSIII_On"和"NFOGQ"。这个原型大致遵循与我最佳模型相同的架构;主要思想是在使用一些全连接层将这些特征投影到隐藏状态的维度后,用它们来初始化GRU的隐藏状态。这个原型还在将加速度计数据传递给GRU层之前使用1D卷积来提取特征,并且我还考虑添加dropout。我认为如果我有更多时间来调整它,它本可以超越我当前的最好模型。第一个版本在私有排行榜上取得了0.398的分数。
我的最佳模型——在私有排行榜上取得0.417分数的模型——是在没有任何交叉验证的情况下训练的,只使用了80%训练集和20%验证集的分割。说实话,这个模型出现在我早期实验的原型阶段,之后我才考虑分层交叉验证。
在这个解决方案中,我将每个完整下采样(50Hz)的序列逐一输入模型,即批量大小为1。请注意,如果不进行下采样,我将无法将某些序列输入模型。我尝试了此架构的多种不同方法,但在增加批量大小时未能获得更好的分数。我尝试了多种序列窗口大小;然而,由于我对时间序列还比较陌生,并且参赛时间也比较晚,我没有实现任何形式的重叠,想到这一点时为时已晚。这可能是一个关键因素。此外,当增加批量大小时,批归一化似乎明显优于层归一化。
对于损失函数,我使用了简单的交叉熵。由于类别非常不平衡(特别是我的第4个人工类别),我也考虑过使用加权交叉熵,以每个类别的逆频率作为权重。我也考虑过尝试Focal Loss;但这些初步测试似乎在我的情况下表现不如交叉熵。尽管这些实验未获成功,我仍然认为以比我更好的方式处理问题的不平衡特性是很重要的。
在优化器方面,我使用了Ranger。我也试过Adam和AdamW,说实话我不认为这个选择太重要。使用Ranger时,我采用1e-3的学习率,训练20个epoch,从第15个epoch开始使用余弦退火调度。
请注意,我还使用了混合精度训练和梯度裁剪。
我找到的模型架构最佳参数是:
后来,我还尝试了分层k折交叉验证,通过简单平均的方式堆叠k个模型的预测结果。每个fold的架构和训练细节与我0.417分数的模型相同,这种堆叠过程产生了我的第二佳模型,在私有排行榜上取得了0.415的分数(k=5)。我也尝试在整个数据集上重新训练模型,但这种方法并未提高我的排行榜分数。
以下是其他一些我也尝试过但未能提高分数的架构(顺序不限):