Conv3d里面输入数据维度为(B, C, T, W, H)
- B -- batch_size
- C -- channels,特征数,也就是数据集中的24对应的维度
- T -- time,时间帧,时序信息部分
这么看下来,Conv3d正好和本次比赛的数据集完美对应上了,那就开始编写代码
数据集准备
这次数据量实在有点大,6个7G大小的压缩包,本地跑不了,于是放到kaggle上。首先确定训练集以及测试集的路径
然后就是修改一下路径变量,直接带入之前写好的Dataset, Dataloader里面就好了
有个小注意事项--数据集默认文件名是"年份+id",比如2021.1,而GT加载的时候不能包含id,如果直接运行会报错。做一点小修改——
self.gt_paths = [os.path.join(self.path, f'{year[:-2]}.nc') for year in self.years]
定义模型
要使用Conv3d,除了关注通道数之外,对于W,H的限定也很重要。根据卷积的计算公式,如果需要保持高宽不变的话,可以将k设置为3,padding设置为1。这个主要用在block中(按照一半模型设定,会有一个重复的基准块),头层和尾层网络控制输入输出的shape与题目限制一致。
class Conv3D(nn.Module):
# 24 -> 1
def __init__(self, input_channels, output_channels):
super(Conv3D, self).__init__()
self.feedforward = nn.Sequential(
nn.Conv3d(input_channels, 32, kernel_size=(3, 3, 3), padding=(1, 1, 1)),
nn.ReLU(),
)
self.block = nn.Sequential(
nn.Conv3d(32, 32, kernel_size=(3, 3, 3), padding=(1, 1, 1)),
nn.SiLU(),
nn.BatchNorm3d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
)
self.output = nn.Sequential(
nn.Conv3d(32, output_channels, kernel_size=(3, 3, 3), padding=(1, 1, 1)),
nn.ReLU(),
)
def forward(self, x, num_blocks=5):
x = x.permute(0, 2, 1, 3, 4) # 将72的时序信息与24的通道 位置互换
x = self.feedforward(x)
for i in range(num_blocks):
x = self.block(x)
x = self.output(x)
return x # (1, 1, 72, W, H)
这里的模型是很不完善的,首先基础的残差连接就没有使用(大部分卷积网络都使用了残差思想,对模型的拟合效果显著增强),但这里模型只是个演示用,暂且忽略。这里我们每一层Conv3d都是使用了K=3, P=1, S=1的设定,有瑕疵。
注意:
刚刚文章开头提到了Conv3d每一维度的意义,我们这里需要使用permute进行调整,时间帧维度和特征通道维度是对应不上的
思考:这里能不能用池化层?
池化层具有平移不变性的特点,对于图像而言,可以简单理解成切割掉一部分像素,只保留最显著的特征。这里我们使用的是气象数据,个人理解,每个数据都具有价值,加上池化层可能会导致loss增加。(个人理解
训练模型
model = Conv3D(24, 1).to(device)
epochs = 100
criterion = nn.MSELoss().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-5)
loss_list = []
val_loss_list = []
for epoch in range(epochs):
sum_loss = 0.0
model.train()
for index, (ft_item, gt_item) in enumerate(train_loader):
optimizer.zero_grad()
if np.isnan(ft_item).any():
np.nan_to_num(ft_item, np.nanmean(ft_item))
ft_item, gt_item = ft_item.to(device), gt_item.to(device)
output = model(ft_item)
loss = criterion(output, gt_item)
loss.backward()
optimizer.step()
sum_loss += loss.item()
loss_list.append(sum_loss / len(train_loader))
torch.save(model, f'./model_{epoch}.pth')
model.eval()
val_loss_sum = 0.0
with torch.no_grad():
for index, (ft_item, gt_item) in enumerate(val_loader):
if np.isnan(ft_item).any():
np.nan_to_num(ft_item, np.nanmean(ft_item))
ft_item, gt_item = ft_item.to(device), gt_item.to(device)
output = model(ft_item)
val_loss = criterion(output, gt_item)
val_loss_sum += val_loss.item()
val_loss_sum /= len(val_loader)
val_loss_list.append(val_loss_sum)
print(f"[Epoch {epoch+1}/{epochs}], Validation Loss: {val_loss_sum:.6f}")
这部分主要是有一个步骤——缺失值处理。
本来是想在dataset中使用xarray读取数据时就使用Dataset.interpolate_na,进行线性插值。然而这里坐标使用的是datetime.datetime,使用线性插值或者样本插值等,需要先进行排序,代码会报错该特征不具有单调性,当然可以处理,但本文直接使用了均值填充
正常来说,每一个epoch训练完后,进行eval,然后判断——如果loss降低,则保存新模型并覆盖旧模型,本文将每一轮的模型都保存了下来,主要是想提交多次结果,看看分数上提升了多少。
差错
问题出在loss曲线上:
橙色代表train_loss,蓝色代表val_loss,这个图像就很奇怪。可能是模型的结构问题,激活函数、正则化、池化、注意力等,具体原因未知。最后尝试了使用第40个epoch的模型test提交结果,分数自然很低很低.......
总结
虽然结果不尽人意,但说明了Conv3d其实是可行的。不过参数量增加了许多,训练时间较长——10G的训练模型,100个epoch,一张P100跑了15小时
Conv3d可以在时序上有一定效果,但是综合下来性能还是不够的。可以考虑使用MIM、SimVP等,或者时空间卷积网络(没找到讲解透彻的文章)。或者干脆像baseline一样,直接把时序信息舍弃,暴力卷积堆,也可以取得一定成果。