AI夏令营天气预报大赛
AI夏令营第三期
比赛任务
提供10年的再分析数据,通过输入历史70个大气变量数据,预测华东区域未来1-5天的5个地面变量。
赛题数据集
==输入历史2个时刻的多个大气变量,输出未来1-5天每6小时的5个地面变量==
推理文件的输入Input: (2 x 70 x H x W),输出
Output: (20 X 5 X H X W)
==疑问:这个输入输出什么意思?矩阵的意思吗?==
其数据类型是float16(半精度的一个数据类型),存储为pt格式。==可以不使用70个变量作为输入,但是输出必须包含5个地面变量==,且顺序必须按照【T2M(2米温度),U10(10米纬向风),V10(10米径向风), MSL(平均海平面气压),TP(6小时累计降水)】,必须包含20个step对应未来时刻:[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120](小时)
深度学习的宏观框架 训练和推理
训练
打个比方,想要训练一个能区分橙子和苹果的模型,你需要搜索一下苹果和橙子的图片,这些图片放在一起称为训练数据集(training dataset),训练数据集是有标签的,苹果的图片标签就是苹果,橙子依然。一个初始的神经网络通过不断地优化自身参数,来让自己变得准确,可能开始10个苹果图片,只有5张被认为是苹果,另外5张认错了,这个时候通过优化参数让另外5张也被认为是对的。这个过程称之为训练。
疑问:什么是神经网络?如何优化参数?(这些对于比赛没影响有影响的话我再补充)
推理
你训练好了一个模型,在训练数据集中表现良好,我们期望她可以对没看过的图片进行识别,你重新拍一张图片扔进网络让网络做判断,这种图片叫做现场数据,如果现场数据的区分准确率非常高,那么证明网络训练是非常好的,我们把训练好的模型拿出来遛一遛的过程称为推理
评价指标
指标还没搞懂,后面继续更新
解题思路
这个问题属于典型的回归问题
针对这类时间序列预测问题方法比较灵活,传统的时序模型、机器学习、深度学习方法均可以使用。
1、统计策略:使用最近时刻的结果进行均值、中位数、时间衰减等方式直接统计得到未来结果,这种方式比较简单,可以快速得到结果;
2、时序模型:比较常用的方法有指数平滑法、灰色预测模型、ARIMA预测、季节Sarima模型、VAR模型等,仅能刻画序列信息,无法加入其他信息进行训练,比如离散类特征;
3、机器学习模型:常见的为lightgbm、xgboost、catboost,需要构建大量时序相关特征;
4、深度学习模型:常见为rnn、lstm、cnn、transformer这类模型,可以直接输入序列信息,不需要构建大量的人工特征;
因为整体数据处理起来还是比较复杂的,为了快速的得到一个结果,所以我们选择使用统计策略作为baseline方案,后续进阶实践部分则使用机器学习模型。不过我们会在baseline实践部分花些时间进行数据解读,为进阶部分做铺垫。
Stept1 搞懂统计方法的BasLine
1、导入模块(导入工具包)
1 | import os #os库是Python的标准库 |
2、数据探索(将数据分块打印出来看看长啥样)
数据探索性分析,了解数据集,了解变量间的项目关系以及变量与预测值之间的关系,对已有数据在少量的先验假设下,通过作图、制表、方程拟合、计算特征量等手段,探索数据的结构合规律的一种数据分析方法、从而保住我们后期更好的进行特征工程和建立模型。
加载数据及基本的预处理,需要特别注意:
数据分为两部分,训练数据和测试数据,并且读取方式是不一样的;
训练数据非常大,不能直接展开,可以选择一部分数据进行展开,了解数据基本情况;
与常规读取数据的方式不同,这里使用xarray进行数据加载;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75data_dir = 'weather'
# 按时间进行数据块划分
# 定义了一个`chunk_time` 函数(区分时间函数)
def chunk_time(ds):
# 接受一个xarray的数据集(`ds`)作为参数。
#它使用`ds.dims`获取数据集的维度信息,并将其存储在`dims`字典中。
dims = {k:v for k, v in ds.dims.items()}
#将dims中的维度的长度修改为1,为什么要修改呢?
dims['time'] = 1
# 使用了xarray库中的chunk方法来对数据集ds进行分块操作。chunk方法需要传入一个参数dims,它表示要对哪些维度进行分块。
ds = ds.chunk(dims)
# ds.chunk会根据维度进行分块操作,之前把time设成了1所以每个时间维度上有一个元素。
return ds
# 加载训练数据
def load_dataset():
# 将ds定义为空数组
ds = []
# 从2007到2012年进行遍历,并赋值给y
for y in range(2007, 2012):
# os.path.join()函数来创建一个路径字符串,里面传入数据目录路径变量,一开始定的weather,以及f'weather_round1_train_{y}'是一个表示文件名的格式化字符串,其中{y}是一个时间变量也就是从2007到2012。
# data_name 是数据路径的字符串
data_name = os.path.join(data_dir, f'weather_round1_train_{y}')
# xarray库中的open_zarr函数打开Zarr数据集的代码。open_zarr函数用于读取Zarr格式的数据,并将其存储为xarray.Dataset对象。
# 在这里,data_name是指Zarr数据集的文件路径或URL。consolidated=True参数表示如果数据集被分块存储,打开时将会加载所有块的元数据进行合并。
# 通过执行这行代码,可以使用x变量来访问和操作Zarr数据集中的数据。
x = xr.open_zarr(data_name, consolidated=True)
# 这会将data_name打印为数据集的名称,并通过x.time.values[0]和x.time.values[-1]打印出时间范围的起始和结束时间。
print(f'{data_name}, {x.time.values[0]} ~ {x.time.values[-1]}')
# 通过执行ds.append(x),可以将x数据集中的所有变量添加到ds数据集中,从而创建一个包含原始数据和新数据的完整数据集。
ds.append(x)
# xr.concat()函数是用于在指定维度上连接(拼接)多个xarray.Dataset对象的函数。
#在代码中,xr.concat(ds, 'time')用于将 ds 数据集列表沿着 'time' 维度进行连接。
#这个函数会将ds中的多个数据集按照时间维度进行拼接,生成一个新的xarray.Dataset对象。
#注意,ds应该是一个包含多个xarray.Dataset对象的列表,而不是一个单个数据集。
#通过执行ds = xr.concat(ds, 'time'),你可以将列表中的所有数据集按照时间维度进行连接,从而生成一个包含所有数据的新数据集,并将其赋值给变量ds
ds = xr.concat(ds, 'time')
# 将数据集按照时间分块
ds = chunk_time(ds)
return ds
# 调用load_dataset()函数,加载训练数据
# .x表示从加载的数据集中提取一个名为x的变量
ds = load_dataset().x
num_step = 20 # 5天的数据,每6小时一个时刻,5*6/24=20,所以输出的时间维度一共有20个。5个地面变量,H和W都是161表示空间范围
shape = ds.shape # 四维数据,time x channel x lat x lon
times = ds.time.values # 提取对应的所有time值
# times是一个可迭代对象(如列表或数组),而slice(1, -20)则是一个切片对象,用于选择times中的一部分元素。
# 在这里,slice(1, -20)表示从索引1(第二个元素)开始,直到倒数第20个元素之前结束(不包括倒数第20个元素)。(为什么要这样切片呢?)这将创建一个切片对象,包含了times中的一部分元素。
# 然后,init_times将通过这个切片对象来选择times中的相应元素,作为初始化时间(initial times)。
init_times = times[slice(1, -num_step)]
num_data = len(init_times) # 计算init_times中元素的数量,并将结果保存在num_data变量中。
names = list(ds.channel.values) # 提取所有变量名
test_names = names[-5:] # 最后5个变量为目标变量,即我们需要预测的目标
print(f'\n shape: {shape}')
print('\n times: {} ~ {}'.format(times[0], times[-1]))
print('\n init_times: {} ~ {}'.format(init_times[0], init_times[-1]))
print(f'\n names: {names}')
print(f'\n test_names: {test_names}\n')data_dir
是存储数据的目录,可以根据实际情况进行修改。定义一个
chunk_time
函数(区分时间函数),接受一个xarray的数据集(ds
)作为参数。它使用
ds.dims
获取数据集的维度信息,并将其存储在dims
字典中。ds.dims.items()返回的是一个迭代器,它遍历数据集的维度字典的每一个键值对,每次迭代返回一个键值对的元组。
然后,使用
ds.chunk
方法根据dims
字典对数据集进行划分,将时间维度划分为一个chunk。最后,返回划分后的数据集。分块操作是将数据集的维度划分为多个块,以提高计算性能和内存利用率。
将数据打印出来看看
1 | print(ds.time.values) |
1 | ds[0,0,:,:].values |
如果想要看具体的变量值,则可以通过如下代码查看,因为数据比较大,所以仅看第一个time值(2007-01-01T00:00:00.000000000),对应的z50,lat和lon下的所有z50值
可以看到一个时刻对应某个变量的值应该是161*161个,即不同经纬度下的值。所以需要预测的结果是20个时刻对应5个目标变量下不同经纬度下的值,即20 x 5 x 161 x 161。
以上都是训练数据。
接下来看看测试数据,同样先加载下来看看:
1 | file_lists = os.listdir(f'{path}/weather_round1_test/input') |
1 | input_data.shape, input_data |
一般时间序列预测问题都有较长的历史数据,而本次赛题却只有历史两个时刻单位的数据,这就导致常见的时间序列特征提取方式在这个赛题中无法使用,或者极具困难。
3、数据清洗
数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。俗话说:garbage in, garbage out。分析完数据后,特征工程前,必不可少的步骤是对数据清洗。
数据清洗的作用是利用有关技术如数理统计、数据挖掘或预定义的清理规则将脏数据转化为满足数据质量要求的数据。主要包括缺失值处理、异常值处理、数据分桶、特征归一化/标准化等流程。
数据非常干净,不需要进行数据清洗工作。
4、统计策略进行
1 | import torch,os |
1 | # 现在就可以得到答案的压缩包啦~~~ |
1 | #如果上面代码无响应,运行下面这段代码 |
Step2 搞懂机器学习BaseLine
我们将使用机器学习的方法搭建方案,模型选择catboost,在baseline部分已经进行了数据探索和数据清洗两个部分,进阶部分直接跳过。主要围绕数据预处理、特征工程、模型训练、模型验证进展开实践。
实践难点:
- 数据格式与以往的不同:在数据挖掘比赛中,数据是四维的形式的呈现(time,channl,lat,lon),与以往直接转为dataframe格式完全不同,需要我们自行转换,并且自行构建训练样本和测试样本。
- 可用的历史数据非常少:每份训练数据只提供历史2个时刻的输入文件,这也表明仅能使用2个时刻的数据提取特征,如何使用2个时刻数据刻画未来20个时刻的目标变量信息能为难点。
- 数据量非常大:每份测试数据需要预测的结果为(20,5,161,161),对应205161*161条样本,有300份测试数据,那就是777630000条样本,就单单进行样本构建、特征转换和结果预测就非常耗时了。训练数据是远大于测试数据,不过可以提取部分数据输入到模型进行训练,并不需要全部使用。
1、导入模块
1 | # 是一个数学基础库,可以计算矩阵,导入numpy库将其命名为np |
2、加载数据(与前面基本一致)
1 | path = 'weather' |
3、构建训练样本(将特征量进行维度转换和合并,从三维变成一维)
因为数据量比较大,所以仅提取10份训练数据,每份数据大小应该为(22,70,161,161),共22个时刻的数据,前面2个时刻用来提取特征,后面20个时刻用来提取目标变量。另外需要特别注意的是:
整个流程涉及很多数组的操作,如提取数据、维度转换、数组合并等,需要仔细操作;
分别提取时间、经纬度、第一个时刻的70个变量、第二个时刻的70个变量、两个时刻70个变量的差值。需要特别注意,对于未来20个时刻,除了时间信息是不一样的外,其余特征变量完全一致,主要刻画相同特征不同时间的情况下,目标变量的变化;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88# 将ds.lat和ds.lon的值分别转换为列表并赋值给变量lats和lons。
lats = ds.lat.values.tolist()
lons = ds.lon.values.tolist()
# 对齐测试数据,每份数据应该是22个时刻
# 获取多份训练数据,滑动10次,每次24个时刻(6天),每次滑动提取最后22个时刻作为一份训练数据
#首先训练数据是空数组
train_data = []
for i in range(10):
if i == 0:
# 选用最后22个索引,
print(-22, 0)
train_data.append(ds[-22:, :, :, :].values)
else:
# 选择倒数22个时刻的数据作为训练数据
idx = i * 24
train_data.append(ds[-22-idx:-idx, :, :, :].values)
print(-22-idx,-idx)
# 经纬度标识字段
# 每个循环迭代,i 和 j 的范围是从0到160,这将生成 161*161 个坐标对。
# 最后,通过使用 numpy 的 array 函数,将 latlon_vals 转换为一个 numpy 数组,维度为 (161*161, 2)。这意味着该数组的行数是 161*161,每行有两个元素代表一个经纬度坐标。
latlon_vals = []
for i in range(161):
for j in range(161):
latlon_vals.append([lats[i], lons[j]])
latlon_vals = np.array(latlon_vals) #(161*161,2)
# 确定好20份训练数据后,接下来需要提取特征
for flag, dat in tqdm(enumerate(train_data)):
# 第一时刻特征
first_feat = dat[0,:,:,:].reshape(70,161*161).transpose() # (70, 161, 161) --> (161*161, 70)
# 第二时刻特征
second_feat = dat[1,:,:,:].reshape(70,161*161).transpose() # (70, 161, 161) --> (161*161, 70)
# 两个时刻差分特征
diff_feat = (dat[1,:,:,:] - dat[0,:,:,:]).reshape(70,161*161).transpose() # (70, 161, 161) --> (161*161, 70)
# 在这段代码中,使用了 tqdm 函数来迭代 train_data 列表,并给每个迭代元素增加一个索引标志 flag。
# 在每个迭代中,对数据 dat 进行了一系列操作,如下所示:
# 第一时刻特征(first_feat)的计算:通过使用索引 [0,:,:,:],从 dat 中选择第一个时间步的数据,然后通过重塑和转置操作将形状从 (70, 161, 161) 变为 (161*161, 70)。这样做,以便将原始的三维特征数据转换为一个二维特征矩阵。
# 第二时刻特征(second_feat)的计算:通过使用索引 [1,:,:,:],从 dat 中选择第二个时间步的数据,然后进行与上述相似的重塑和转置操作,将形状变为 (161*161, 70)。
# 两个时刻差分特征(diff_feat)的计算:通过将第二个时间步的数据减去第一个时间步的数据,然后进行与上述相似的重塑和转置操作,将形状变为 (161*161, 70)。
# 通过将这个三维矩阵重塑为 (161*161, 70) 的形状,可以将整个矩阵展平为一个长向量,其中每个元素就代表了矩阵中的一个特征值。这样,整个二维矩阵可以被表示为一个特征向量的集合。
# 这些操作的目的是将原始的三维特征数据转换为二维特征矩阵,以便在后续的分析或处理中使用。
# 构建训练样本
tmp_dat = dat[2:,-5:,:,:] # (20, 5, 161, 161)
# 通过切片 [2:,-5:,:,:],你选取了 dat 数组的第3到最后一个时间步、倒数第5到最后一个通道、以及所有行和列。这样,你得到了一个新的数组 tmp_dat,其形状为 (20, 5, 161, 161)。
# 使用了一个循环,迭代了20个时间步。
# 在每个时间步中,进行了以下操作:
for i in range(20): # 20个时刻
# 时间特征、经纬特征
# 时间特征的计算:通过使用 np.array([i]*161*161).reshape(161*161, 1),创建了一个形状为 (161*161, 1) 的数组 time_vals,其中每个元素都是当前时间步 i。
time_vals = np.array([i]*161*161).reshape(161*161,1) #(161*161,1)
# 经纬特征的计算:将之前定义的 latlon_vals 数组与时间特征 time_vals 拼接起来,使用 np.concatenate 函数,按照列的方向(axis=1)进行拼接,得到形状为 (161*161, 3) 的数组 sub_vals。
sub_vals = np.concatenate((time_vals, latlon_vals), axis=1) #(161*161,3)
# 添加历史特征,第一时刻特征、第二时刻特征、两个时刻差分特征
# 添加历史特征:将之前计算的 第一时刻特征 first_feat、第二时刻特征 second_feat 和两个时刻差分特征 diff_feat,依次与 sub_vals 进行拼接。通过多次使用 np.concatenate 函数,将这些特征按照列的方向拼接到 sub_vals 的右侧。最终,sub_vals 的形状将变为 (161*161, 213),其中包含时间特征、经纬特征和历史特征。
sub_vals = np.concatenate((sub_vals, first_feat), axis=1) #(161*161,73)
sub_vals = np.concatenate((sub_vals, second_feat), axis=1) #(161*161,143)
sub_vals = np.concatenate((sub_vals, diff_feat), axis=1) #(161*161,213)
# 添加5个目标变量,(161*161,218)
for j in range(5):
var_vals = tmp_dat[i,j,:,:].reshape(161*161, 1)
sub_vals = np.concatenate((sub_vals, var_vals), axis=1)
# 将当前时间步的特征数据 sub_vals 添加到 all_vals 中。如果 i 是第一个时间步(i == 0),则直接将 sub_vals 赋值给 all_vals;否则,通过使用 np.concatenate 函数将 sub_vals 拼接到 all_vals 的下方,按行的方向进行拼接。
if i == 0 :
all_vals = sub_vals
else:
all_vals = np.concatenate((all_vals, sub_vals), axis=0)
# 最后,通过外层循环的迭代,将每个时间步的特征数据添加到 final_vals 中。如果 flag 是0,则直接将 all_vals 赋值给 final_vals;否则,通过使用 np.concatenate 函数将 all_vals 拼接到 final_vals 的下方,按行的方向进行拼接。
if flag == 0:
final_vals = all_vals
else:
final_vals = np.concatenate((final_vals, all_vals), axis=0)
# 最终,final_vals 将包含所有时间步的特征数据,形状为 (161*161*20, 218)。
将二维数组转为dataframe格式,并设置新的列名,然后选择入模特征,代码如下:
1 | # 首先使用 pd.DataFrame 函数将 final_vals 数组转换为一个 Pandas DataFrame 对象,命名为 train_df。 |
4、训练模型
模型选择使用catboost模型,并且将模型训练和模型推理单独构建,主要是方便对300份测试数据进行单独推理。另外需要注意,模型学习率learning_rate设置的比较大。
1 | def train_model(train_x, train_y, label, seed=2023): |
5、测试集预测
在训练模型阶段提前将训练好的模型进行保存,在模型推理函数部分直接加载对应目标变量的模型即可,代码如下:
1 | def inter_model(test_x, label, seed=2023): |
构建测试样本,及特征提取函数,这里的构建流程与训练集部分基本一致,除了没有5个目标变量外。代码如下:
1 | def get_test_data(dat): |
结果生成函数,代码流程:
- 生成测试样本,并构建特征;
- 调用推理模型函数,分别预测5个目标变量的结果;
- 将结果转为可提交格式(20,5,161,161);
- Array转为Tensor;
- 保存为pt文件,精度为半精度浮点类型。
代码如下:
1 | def get_parallel_result(file): |
为了加快运行速度,这里使用Joblib中的Parallel和delayed实现并行化处理,执行get_parallel_result函数,代码如下:
注:n_jobs为开启的进程数,即是用n_jobs个CPU来计算。
1 | Parallel(n_jobs=32)( |
6、结果输出(统一的没啥区别几乎)
1 | # 现在就可以得到答案的压缩包啦~~~ |
优化1、扩大训练数据为15份(后续想想怎么能更多)
优化 2、学习率和迭代次数105(测试中)
优化3、特征优化
1 | def get_test_data(dat): |
优化4、评价指标
1 |
回归问题与梯度下降
梯度下降:
首先梯度类似于导数,
首先随便找一个点X0,然后算出Y0,然后计算机算出来Y0’,然后给计算机一个迭代的过程。
这个过程大概就是梯度下降的过程。
详细回归问题与梯度下降视频:
那么回归又是什么意思?
由于我们现实中的数据存在一些误差,我们允许一些误差的存在,然后得到一组整体更好的参数,回归