在之前的Task1:赛事初探——降水预测模型搭建过程概述(http://t.csdnimg.cn/Y7z2J)和Task2:抽丝剥茧——降水预测baseline详解(http://t.csdnimg.cn/TWyR3)任务中,我们对这次赛事的赛题进行了全面的分析,并对基础的baseline进行了详细的解读。接下的Task3任务,我们追求进一步的上分,并对进阶的新baseline进行解读。
Task3: 进阶上分——赛题解析与新baseline解读
Part1 精读赛题——确定隐含条件和提交数据格式
1.赛题背景:在全球变暖背景下,极端降水发生频次更多、范围更广、强度更强,其时空尺度上也呈现出非典型的特征。2023年夏季,我国共发生 17次暴雨过程,引发严重洪涝灾害,对人民生活和社会经济造成极大影响。2022年以来基于数据驱动的大模型开始逐步超过基于物理的气象模型预报能力。本次大赛考察选手基于业界领先的伏羲气象大模型输出,进一步优化和提升未来三天的极端降水预测。
2.输入数据是伏羲气象大模型的输出,共有24个特征,大致可以分为高空变量,地面变量,位置变量(经纬度)三个大类,目标数据是欧洲中期天气预报中心(ECMWF)的再分析数据ERA5的子集,给出的tp(累积降水)值。我们需要根据伏羲大模型的输出对tp值进行订正。
3. 赛题提供方更新:数据为原始数据的标准分数Zscore,Zscore = (x - μ)/ σ 。ERA5降水, μ: 0.19359495,σ:0.6458292; FuXi降水, μ: 0.13014507,σ: 0.4056911。
4.数据格式:数据格式是.nc格式的文件,我们需要使用xarray库进行数据的操作,xarray库是是一个 Python 库,用于处理和分析多维数组数据。xarray库提供了两种主要的数据结构:DataArray和DataSet。DataArray类似于 NumPy 数组,但带有坐标和元数据。DataSet则是一组带有共享坐标和属性的 DataArray对象的集合;xarray库允许通过标签而不是索引位置来访问数据,这在地理空间数据中特别有用;支持 matplotlib 以及其他绘图库,方便进行数据可视化。
5.数据集存在缺失等问题,需要进行清洗,数据的预处理很重要,数据决定了算法的上限。对数据进行进一步的挖掘是上分的有效手段之一。
Part2 速通新baseline——与原始baseline对比发现实验提升细节
原始baseline已经讲过的,就不再赘述。这里主要比较一下两者的不同,进阶baseline都提供了哪些优化的方向和可能提升性能的细节。当然不是说加了这些细节就一定有效果,可能有时候还不如原始的baseline。这都是正常的,我们无法保证复杂的模型就一定会比简单的的效果好。
1.模型角度:原始baseline只是使用了单层的卷积神经网络,进阶baseline对模型架构进行了加深处理,使用了4个卷积层,并使用了BatchNormalization,和Relu激活函数。
从模型的角度来说,我们还可以选择更适合时序任务的RNN,Transformer等架构,这也是很多竞赛,高分选手采用的方案。
import torch
import torch.nn as nn
# 实验1 加深模型
class EnhancedModel(nn.Module):
def __init__(self, num_in_ch, num_out_ch):
super(EnhancedModel, self).__init__()
self.conv1 = nn.Conv2d(num_in_ch, 64, kernel_size=3, padding=1)
self.batchnorm = nn.BatchNorm2d(64)
self.activation = nn.ReLU()
self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.conv4 = nn.Conv2d(64, num_out_ch, kernel_size=3, padding=1)
def forward(self, x):
B, S, C, W, H = tuple(x.shape)
x = x.reshape(B, -1, W, H)
out = self.conv1(x)
out = self.activation(out)
out = self.conv2(out)
out = self.activation(out)
out = self.conv3(out)
out = self.activation(out)
out = self.conv4(out)
out = self.activation(out)
out = out.reshape(B, S, W, H)
return out
# define model
in_varibales = 24
in_times = len(fcst_steps)
out_varibales = 1
out_times = len(fcst_steps)
input_size = in_times * in_varibales
output_size = out_times * out_varibales
model = EnhancedModel(input_size, output_size).cuda()
2.划分训练集和验证集:赛事的官方网站上我们每天可以提交三次数据,这对我们调参来说是远远不够的的,所以我们需要划分验证集,在验证集上对我们的每一次调参结果进行评估,择优提交到赛事官方。我们可以按照8:2的比例来划分我们的训练集和验证集,训练集用于拟合模型,使模型能够从数据中学习特征并建立预测函数。验证集帮助我们在训练过程中监测模型的表现,防止过拟合。我们可以使用验证集上的表现来决定何时停止训练(早停策略),或者选择最佳的模型架构。训练集和验证集中的样本应该是完全不重叠的,以确保评估过程的公正性。
import torch.utils
from torch.utils.data import Dataset, DataLoader, random_split
# Split the dataset into training and validation sets
train_size = int(0.8 * len(my_data))
val_size = len(my_data) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(my_data,[train_size, val_size])
# Create data loaders for training and validation sets
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=2, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=2, shuffle=False)
3. 异常处理:如果使用全部数据集来训练的话,train的时候是会报错KeyError的,在进阶baseline里面对这块进行了改进,使用try...except...来进行了异常捕捉。
# 构建Dataset部分
class mydataset(Dataset):
def __init__(self):
self.ft = Feature()
self.gt = GT()
self.features_paths_dict = self.ft.features_paths_dict
self.init_times = list(self.features_paths_dict.keys())
def __getitem__(self, index):
init_time = self.init_times[index]
try:
ft_item = self.ft.get_fts(init_time).to_array().isel(variable=0).values
gt_item = self.gt.get_gts(init_time).to_array().isel(variable=0).values
except KeyError as e:
print(e)
print(f'init_time: {init_time} not found')
# return None, None
return self.__getitem__(index - 1) #如果数据缺失,则使用前一个节点的数据
return ft_item, gt_item
def __len__(self):
return len(list(self.init_times))
4. 损失函数:进阶baseline提供了另一种损失函数SmoothL1Loss,我们可以对比其和MSELoss的区别。
SmoothL1Loss是 PyTorch 中的一个损失函数,它结合了 L1 loss(绝对误差)和 L2 loss(均方误差)的优点。SmoothL1Loss也被称为 Huber loss,在计算机视觉和机器学习任务中经常被用作回归任务的损失函数。
loss_func = nn.SmoothL1Loss()
# loss_func = nn.MSELoss()
SmoothL1Loss 的特点
- 平滑处理:当误差较小时(小于 β),损失函数采用 L2 loss 的形式;当误差较大时,损失函数采用 L1 loss 的形式。这样的设计既保留了 L2 loss 的平滑性,又避免了 L2 loss 在误差较大时的敏感性。
- 鲁棒性:由于在大误差时采用了 L1 loss 形式,SmoothL1Loss 对异常值(outliers)更加鲁棒。
- 可微性:与 L1 loss 相比,SmoothL1Loss 在误差为零时也是可微的,这有利于优化过程。
SmoothL1Loss 的应用场景
- 物体检测:在物体检测任务中,边界框回归通常使用 SmoothL1Loss,因为它对边界框的坐标回归更加稳定。
- 图像回归任务:在图像中的关键点定位等回归任务中,SmoothL1Loss 也经常被采用。
SmoothL1Loss和MSELoss的区别:
-
平滑性与鲁棒性:
- SmoothL1Loss 在误差较小时采用 L2 loss 的形式,保证了平滑性;而在误差较大时采用 L1 loss 的形式,增加了鲁棒性。
- MSELoss 整体上是平滑的,但对较大的误差更为敏感。
-
异常值处理:
- SmoothL1Loss 更能抵抗异常值的影响,因为它在误差较大时采用 L1 loss 的形式。
- MSELoss 对异常值更为敏感,因为它是误差的平方。
-
应用场景:
- SmoothL1Loss 更适合需要平衡模型对小误差和平滑性的需求的情况,例如物体检测中的边界框回归。
- MSELoss 更适合强调模型精确度的回归任务。
5.模型参数初始化:模型参数初始化是机器学习和深度学习中一个重要的步骤,它决定了模型开始训练时的起始点。良好的参数初始化可以帮助模型更快地收敛,并且避免一些常见的训练问题,如梯度消失或梯度爆炸。
# 模型初始化
import torch.nn.init as init
def init_weights(m):
if isinstance(m, nn.Conv2d):
init.xavier_uniform_(m.weight)
if m.bias is not None:
init.constant_(m.bias, 0)
model.apply(init_weights)
初始化方法的选择
- 对于线性模型,通常使用随机初始化。
- 对于使用 ReLU 激活函数的模型,He 初始化通常是一个好的选择。
- 对于使用 tanh 或 sigmoid 激活函数的模型,Xavier 初始化通常更为合适。
- 对于复杂的模型,预训练初始化或自定义初始化策略可能会带来更好的效果。
Xavier 初始化是一种有效的参数初始化方法,它可以帮助神经网络更好地收敛。通过保持每一层的激活函数的方差大致相等,可以避免梯度消失或梯度爆炸的问题。
5.其他:进阶baseline相比原始baseline,还有一些的改进,这主要体现在训练的周期epochs,进阶baseline训练了多个epochs,并打印了每个epoch验证集上的Validation Loss,我们可以查看并选择最优的模型;weight_decay(权重衰减),我们在实例化优化器时直接通过weight_decay指定weight decay超参数。权重衰减是一种简单而有效的技术,可以用来防止模型过拟合,并提高模型的泛化能力。通过在损失函数中加入一个正则化项,权重衰减有助于控制模型权重的大小,从而简化模型。
import numpy as np
import torch
# from tqdm import tqdm
# Train the model
num_epochs = 1
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001, weight_decay=1e-6)
# for epoch in tqdm(range(num_epochs)):
os.makedirs('./model', exist_ok=True)
for epoch in range(num_epochs):
model.train()
loss = 0.0
for index, (ft_item, gt_item) in enumerate(train_loader):
ft_item = ft_item.cuda().float()
gt_item = gt_item.cuda().float()
# print("gt", gt_item.max(), gt_item.min())
# Backward and optimize
optimizer.zero_grad()
# Forward pass
output_item = model(ft_item)
# print(output_item.max(), output_item.min())
loss = loss_func(output_item, gt_item)
loss.backward()
optimizer.step()
loss += loss.item()
# Print the loss for every 10 steps
if (index+1) % 20 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Step [{index+1}/{len(train_loader)}], Loss: {loss.item():.6f}")
loss = 0.0
# Save the model weights
torch.save(model.state_dict(), f'./model/model_weights_{epoch}.pth')
model.eval()
val_loss = 0.0
with torch.no_grad():
for index, (ft_item, gt_item) in enumerate(val_loader):
ft_item = ft_item.cuda().float()
gt_item = gt_item.cuda().float()
output_item = model(ft_item)
val_loss = loss_func(output_item.max(), gt_item.max())
val_loss += val_loss.item()
val_loss /= len(val_loader)
print(f"[Epoch {epoch+1}/{num_epochs}], Validation Loss: {val_loss:.6f}")
print("Done!")
Part3 发散思路——简单谈谈上分的技巧
1.baseline主要使用的是卷积神经网络的模型架构,在这个模型架构的基础上,我们可以使用更多的数据,可以调整学习率、批量大小、优化器、优化器的schedual、训练的周期、验证集和训练集划分比例等等。通过这些实验的调整,我们最终会得到一个还不错的分数,我自己的话,这一套下来最优的分数在0.14左右;
2.还要继续上分的话,我们需要从数据入手,进行更多的数据挖掘;
3.这是一个时序任务,模型架构上我们可以选择LSTM,Transformer等,这些模型结构比CNN更加适合;
4.赛事相关的背景知识,其他类似赛事top选手的思路参考等,他山之石,可以攻玉。
5.损失函数,我们可以自定义损失函数,需要对高值敏感, 加更大的惩罚项, 对低值不敏感, 及输出的低值对模型的贡献应相对来说小。
6.数据分布,这是一个订正任务,需要预测异常值,异常值属于小样本,大部分的样本属于正常值,这种样本分布不平衡的小样本任务,如何改善数据分布特征也是我们可以思考的一个方向。
Task3部分的打卡学习就写到这里了,觉得有用的小伙伴,点赞收藏关注吧。