Datawhale夏令营AI for Science(AI+气象)学习笔记1

如何针对降水预测问题搭建模型

回顾baseline, 我们可以大致将搭建模型并解决问题分为以下几个步骤:

  1. 定义数据集, 建立起训练数据和标签之间的关系;定义数据加载器(DataLoader), 方便取数据进行训练

  2. 定义模型, 利用PyTorch搭建网络,根据输入输出数据维度实例化模型

  3. 定义损失函数, 优化器, 训练周期, 训练模型并保存模型参数

  4. 模型加载及推理(模型预测),输入测试数据输出要提交的文件

第一步

安装对应库,数据集

!pip install xarray[complete]
!apt update&&apt install axel

第二步

导入库函数

import os
import pandas as pd
import xarray as xr
from torch.utils.data import Dataset, DataLoader

第三步 

数据集路径配置设置

  • 比赛的数据部分分为数据特征数据真值两部分,数据特征是模型训练的输入,数据真值是模型训练的标签

  • 其中数据特征部分 输入的路径目录下包含年份文件夹

  • 例如示例给出的 “输入路径/2021/…” 各年份文件夹下包含从官网下载的压缩包(e.g. weather.round1.train.ft.2021.1.zip) 解压后文件夹下有不同时段的数据文件夹(e.g. 20210101-00), 内部包含6个nc文件, 是从伏羲大模型中获取的从第6小时到第72小时的数据

  • 数据真值部分 输入的路径目录下包含3个年份的.nc数据, 其中选择哪些年份的特征数据作为输入, 就在years中添加哪些年份

  • fcst_steps指预测的时间步长, 从第1小时到第72小时, 间隔为1小时

# path config
feature_path = 'feature' #自定义路径并修改为自己的路径
gt_path = 'groundtruth' #自定义路径并修改为自己的路径
years = ['2021']
fcst_steps = list(range(1, 73, 1))

第四步 

Feature类和GroundTruth类是数据集的定义 方便后续自定义数据集和数据加载类, 方便我们训练时取数据

  1. 我们在这里定义了两个类, 一个是Feature类, 另外一个是GT类:

    1. Feature:

      1. 第一部分是构造函数, 定义了feature的输入路径、使用到的年份、预测的时间点、还有获取到具体某个feature数据的路径;

      2. get_features_paths函数是构造函数中self.features_paths_dict的具体实现, 通过遍历 feature-2021(年份)-20210101-00(具体时间戳),获取到每个具体时间点存放数据特征的路径;

      3. 最后的get_fts函数, 通过使用xarray库中加载cdf数据,其中sel()中要增加对应的预测间隔, isel()中time要设置为0, 表示从头开始组成数据. 这听起来可能有些绕, 我们直接从数据类型上说话.如下图, 我们可以看到在.sel()后, 数据的维度是(1, 6, 24, 57, 81), 其中的6是因为我们看到的feature中的.nc数据, 是每隔6小时一取的;再经过.isel()方法后, 数据维度就变成了(6, 24, 57, 81), 即通过取time=0处索引的数据, 去除了作为数据输入时, 用不到的时间戳,这样实现了数据集的结构化处理。

  1. get_features_paths: 这是函数的名称。
  2. self: 这表示函数是类的一个方法,self是指向类实例的引用。
  3. init_time_path_dict: 这是一个空字典,用于存储初始化时间的路径。
  4. self.years: 这应该是类的一个属性,包含一个年份列表。
  5. os.listdir: 这是Python标准库中的一个函数,用于列出指定目录中的文件和目录名。
  6. os.path.join: 这是另一个Python标准库函数,用于将多个路径组件合并成一个路径。
  7. pd.to_datetime: 这看起来像是pandas库中的函数,用于将字符串转换为日期时间格式。
  8. sorted: Python内置函数,用于对可迭代对象进行排序。

函数的工作流程如下:

  • 首先,函数初始化一个空字典init_time_path_dict
  • 然后,它遍历self.years中的每一个年份。
  • 对于每个年份,它使用os.listdir列出该年份目录中的所有文件和目录。
  • 接着,它遍历这些文件和目录,使用sorted函数对它们进行排序。
  • 对于每个初始化时间,它使用pd.to_datetime将字符串转换为日期时间对象,并使用os.path.join构建完整的文件路径。
  • 最后,它将日期时间对象和对应的路径作为键值对添加到init_time_path_dict字典中。
  1. GT:

    1. 构造函数与Feature部分类似, 其中gt_path如此设计是因为groundtruth文件夹下就有三个.nc文件, 2021.nc等,相比feature, 多出来的self.gts即添加了年份中所有的gt数据, 之后根据需要再去取对应的部分即可

    2. parser_gt_timestamps这个函数是根据输入获取到时间戳, 如果我们查看gt中.nc数据的话会发现gt中包含的时间戳及对应数据要比feature中的数据更全, 因此我们采用从feature中获取到时间戳, 然后使用针对的时间戳取对应的groundtruth数据

    3. get_gts中, 跟feature中的类似, 取对应时间戳的数据即可, 同样, 我们也可以打印出这个函数的返回值查看数据, 是一个(72, 57, 81)维度的数据:

第五步 

mydataset类的定义, 整合了加载特征和特征对应真值的功能, 方便后续训练时取数据

# 构建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]
        ft_item = self.ft.get_fts(init_time).to_array().isel(variable=0).values
        print(type(ft_item))
        gt_item = self.gt.get_gts(init_time).to_array().isel(variable=0).values
        print(type(gt_item))
        return ft_item, gt_item

    def __len__(self):
        return len(list(self.init_times))
  1. 完成了Feature类和GT类的分别构建后, 就可以定义数据集了~ 数据集首先是继承torch.utils.data中的Dataset类

    1. 首先是构造函数, 分别实例化了Feature和GT类, 定义了取feature的路径还有feature中含有的时间戳数据, 方便在GT中找到对应时间的数据

    2. __get_item__方法是我们进行取数据的操作, 通过传入的index, 可以获取到对应的时间戳, 然后通过此时间戳, 可以从feature和gt中获取到对应的数据. ft.get_ftsgt.get_gts在上面已经提到过, 其中 ft_item = self.ft.get_fts(init_time).to_array().isel(variable=0).values这行代码是将提取到的feature数据和gt数据转化成numpy格式,便于做数据处理

    3. __len__()函数返回数据长度, 在此不用再过赘述

第六步 

前五步已经完成了数据预处理加载的相关类和函数的准备, 这里我们可以通过实例化mydataset类来查看数据数量 同时完成数据集的构建后, 我们可以通过DataLoader来查看数据集的数据

# 可以查看一下已经构建的dataset
# define dataset
my_data = mydataset()
print('sample num:', mydataset().__len__())
train_loader = DataLoader(my_data, batch_size=1, shuffle=True)

第七步 

  • 完成了数据的准备工作, 接下来就是构建模型的部分
  • Model这个类, 对我们的模型进行定义, 方便后续训练时调用
  • 这里我们以一个简单的只有一个卷积层的网络为例
  • 在本次比赛中, 我们的输入数据维度是(1, 24, 72, W, H), 输出数据维度是(1, 72, W, H) 可以在赛题中查看
# 模型构建部分
import torch.nn as nn

class Model(nn.Module):
    def __init__(self, num_in_ch, num_out_ch):
        super(Model, self).__init__()
        self.conv1 = nn.Conv2d(num_in_ch, num_out_ch, 3, 1, 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 = 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 = Model(input_size, output_size).cuda()
  1. 首先, 要定义一个模型类, 他是继承于nn.Module类的, 这个类是pytorch中构建网络时最常用的类之一, 方便了我们做初始化操作, 其中super(Model, self).__init__()就是用来使用父类nn.Module中的初始化方式; 在构造函数中, 我们可以看到baseline只简单的定义了一层卷积, 其中的num_in_ch和num_out_ch分别指输入的通道数和输出的通道数(特征数)

  2. 接下来的forward()函数是我们在使用网络时做前向传播的函数, 其底层的实现是利用了__call__()这个内置函数, 方便我们实例化模型以后直接可以通过model(inputs)形式调用forward函数。forward函数中, 首先记录了输入数据feature的shape, 然后将输入数据的时间维度和特征维度叠加到一起,这样再经过卷积层得到输出, 得到输出以后再对输出数据做一下维度上的reshape, 保证跟赛题中要求的输出维度一致

  3. 定义模型的输入输出维度, 确保模型可以做实例化, 另外时间维度和特征维度需要按照定义模型中的shape操作来对应相乘, 否则就会有维度上的报错; 根据channel进行实例化以后, 因为我们本次使用魔搭的GPU资源, 就需要使用.cuda()将模型放在gpu上

第八步 

定义模型的损失函数部分, 用于模型训练做反向传播

# define loss
loss_func = nn.MSELoss()
  1. 定义模型的损失函数, 一般对于回归任务而言, MSELoss是最常用的

第九步 

模型训练部分

import numpy as np
import torch
# from tqdm import tqdm
# Train the model
num_epochs = 1
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# for epoch in tqdm(range(num_epochs)):
for epoch in range(num_epochs):
    for index, (ft_item, gt_item) in enumerate(train_loader):
        ft_item = ft_item.cuda().float()
        gt_item = gt_item.cuda().float()
        print(type(ft_item))
        print(type(gt_item))
        
        # Forward pass
        output_item = model(ft_item)
        loss = loss_func(output_item, gt_item)
        
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # Print the loss for every 10 steps
        if (index+1) % 10 == 0:
            print(f"Epoch [{epoch+1}/{num_epochs}], Step [{index+1}/{len(train_loader)}], Loss: {loss.item():.4f}")

# Save the model weights
torch.save(model.state_dict(), 'model_weights.pth')
  1. 首先要定义优化器和训练轮数, 这里我们使用了Adam优化器, 设置的学习率为0.001

  2. 接下来就是一个二层的循环, 外层循环是在整个训练轮数上做的, 每次经过这个循环就表示已经遍历了一遍全局数据集;内层循环是根据dataloader中设置的batchsize, 每次循环取batchsize个数据进行训练和优化

  3. 获取到feature和gt数据后, 我们要把数据放到GPU上, 保证和模型.cuda()一致,接下来, 就可以将训练数据(feature)输入到模型中获取输出了, 得到这个输出以后, 就可以计算输出和gt之间的损失函数, 以便模型进行后续优化, 这一步骤在模型内部也叫做正向传播

  4. 正向传播以后, 我们需要梯度回传反向传播来优化我们的模型, 首先使用optimizer.zero_grad()先清空梯度, 再计算模型输出和loss, 然后使用.backward()进行反向传播, optimizer.step()用于更新模型参数. 这三个方法一般都是一起使用, 用于反向传播和更新模型参数. task1中给大家的思考题里面有提到这里, baseline中计算完loss后再使用optimizer.zero_grad()是不合理的.

  5. 最后要将模型参数数据(包括权重weight和偏置bias)进行保存, 以便后续的推理使用

第十步 

  • 模型推理部分, 通过加载模型使用测试数据作为输入, 得到预测结果
  • 其中test_data_path需要给出从下载测试数据解压后的目录路径
# Inference
# Load the model weights
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
import os

test_data_path = "test/weather.round1.test"
os.makedirs("./output", exist_ok=True)
for index, test_data_file in enumerate(os.listdir(test_data_path)):
    test_data = torch.load(os.path.join(test_data_path, test_data_file))
    test_data = test_data.cuda().float()
    
    # Forward pass
    output_item = model(test_data)
    
    # Print the output shape
    print(f"Output shape for sample {test_data_file.split('.')[0]}: {output_item.shape}")
    
    # Save the output
    output_path = f"output/{test_data_file}"
    torch.save(output_item.cpu(), output_path)
    # Load the model weights
    model.load_state_dict(torch.load("model_weights.pth"))
  1. 模型推理部分, 这部分就是我们生成数据提交结果的最后一步:

    1. 首先需要先加载模型权重, 并使用model.eval()将模型置于推理模式

    2. 接下来, 要将测试输入数据的路径进行指定, 这部分已经在魔搭的云空间里面给大家定义好了

    3. 接着一个循环来遍历测试数据路径下所有的数据, 因为测试数据都为.pt文件, 直接使用torch.load加载就好了;取到输入数据以后, 需要使用.cuda()将数据放到GPU上,和model保持一致

    4. 接着调用模型, 输入测试数据, 得出输出数据

    5. 最后, 对输出的数据进行保存(注意最后保存的数据要放在cpu上, 否则提交会报错)

这样, baseline的整个流程就跑完了, 主要是包含数据集准备和处理、模型定义、模型训练、模型推理四个大部分.

总结

构建baseline的基础方法

首先肯定是导入函数库和数据集,然后此项目在一开始定义了两个类方便后续数据集加载与获取,

,定义了mydataset类, 整合了加载特征和特征对应真值的功能, 方便后续训练时取数据。接着实例化数据集,再就是构建模型,Model类进行模型的定义,模型中有前向传播函数,函数中有叠加维度的函数,然后经过卷积层,最后reshape到要求的维度输出。还定义了模型的loss损失函数,用于模型做反向传播,然后进行的就是模型的训练,训练的步骤也是基本的核心步骤,1.定义优化器和训练轮数,学习率。2.循环遍历模型训练和优化。3.获取到数据后输入到模型中计算得到损失函数,以便模型的优化,这步也叫正向传播。4.正向传播后,我们需要梯度回传反向传播来优化模型,首先清空梯度,再计算模型输出和loss,然后反向传播,更新模型参数。5.最后将模型数据保存。最后进行了模型推理。

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值