Datawhale AI 夏令营 -- Conv3D

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一样,直接把时序信息舍弃,暴力卷积堆,也可以取得一定成果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值