# 大作业内容
根据人们填写的日常调查问卷,设计并实现一个深度学习模型用于 COVID-19 感染人数的预测。
具体的作业手册请参考官方手册。
# 详细描述
- 目标:预测 COVID-19 感染人数
- 数据来源:Delphi group @ CMU「A daily survey April 2020 via facebook」
- 输入输出要求:提供美国某州过去 4 天的调查问卷结果,然后预测第 5 天的新增阳性病例的百分比。
# 数据格式
- 州(37 个,用 one-hot 向量的方式编码)
- 类似新冠的疾病
- cli、ili ...
- 行为指标
- wearing_mask、travel_out_state ...
- 精神健康指标
- anxious、depressed ...
- 确诊阳性病例
- Tested_positive(这个就是我们希望预测的指标)
# 评价指标
采用均方误差(Mean Squared Error,MSE)来评价误差。
- :模型预测值
- :实际值
# 作业目的
- 通过深度神经网络(deep neural networks,DNN)解决一个回归问题
- 理解基本的 DNN 训练技巧
- 超参数调优、特征选择、正则化、...
- 熟悉 PyTorch
# 环境准备
# Conda
本文不在赘述 Conda 的安装,参考另一篇博客。
# 数据集
gdown
是一个 github 项目,是一个由 python 实现的从 Google Drive 下载大文件的工具(由于安全策略无法使用 wget/curl 进行下载)。如果之前从未用 gdown
下载过东西,需要现在 conda
的 base
环境安装 gdown
。
mamba install gdown |
然后就可以直接用 gdown 下载文件了:
gdown --id '1kLSW_-cW2Huj7bh84YTdimGBOJaODiOS' --output covid.train.csv | |
down --id '1iiI5qROrAhZn-o4FPqsE97bMzDEFvIdg' --output covid.test.csv |
此时,就得到了训练数据集和测试数据集。
# 依赖库
作业需要依赖一些库,提前安装一下,作业中给出了 import
部分的代码:
# Numerical Operations | |
import math | |
import numpy as np | |
# Reading/Writing Data | |
import pandas as pd | |
import os | |
import csv | |
# For Progress Bar | |
from tqdm import tqdm | |
# Pytorch | |
import torch | |
import torch.nn as nn | |
from torch.utils.data import Dataset, DataLoader, random_split | |
# For plotting learning curve | |
from torch.utils.tensorboard import SummaryWriter | |
# For plot | |
import matplotlib.pyplot as plt | |
import seaborn as sns |
因此,我们创建一个虚拟环境并安装对应的库:
mamba create -n MLlearnEnv python=3.9 | |
conda activate MLlearnEnv | |
mamba install pytorch::pytorch torchvision torchaudio -c pytorch | |
mamba install pandas tqdm packaging tensorboard matplotlib seaborn |
注意,由于电脑原因,这里安装 PyTorch 的命令是安装 CPU 版本的,对于初学者来说 CPU 的算力其实已经够了,如果安装 GPU 版本需要去对应安装 CUDA 驱动,所以具体安装命令请参考 PyTorch 官网。
# 知识准备
这里仅对已给出的源代码进行解释,请自行进行成体系的工具学习。
# 可视化
我们可以使用 tensorboard
来可视化训练过程,使用起来非常简单,只需要在作业目录下执行:
tensorboard --logdir=runs |
logdir
指向训练过程中生成的运行数据文件夹,默认为 ./runs
。
执行后会在本地监听 6006
端口,访问对应的 web 页面即可看到训练过程的图表。
# 实用函数
针对源代码给出的无需修改的工具函数,简单做一些解释。
# 设置随机数种子
为了保证 reproducibility(可复现性),需要设置随机数生成种子,官方文档给出了详细解释,这里简单说明一下。
# seed: 自行设定的随机数种子 | |
def same_seed(seed): | |
"""Fixes random number generator seeds for reproducibility.""" | |
torch.backends.cudnn.deterministic = True # 强制使用确定性算法 | |
torch.backends.cudnn.benchmark = False # 禁用 cuDNN 的卷积基准测试 | |
np.random.seed(seed) # 设置 numpy 的随机数生成 | |
torch.manual_seed(seed) # 设置 PyTorch 的随机数生成 | |
if torch.cuda.is_available(): # 可以不要,新版本 PyTorch 中 manual_seed 会同时设置 CPU 和 GPU 版本的随机数种子 | |
torch.cuda.manual_seed_all(seed) |
# 数据集分割
由于需要计算损失函数来对模型参数进行动态的调整,我们需要将 训练数据集
分割成 训练集
和 验证集
,源代码中给出了一个通用的分割函数。
# data: 训练集(矩阵) | |
# valid_ratio: 验证集的比例 (validation_size = train_size * valid_ratio) | |
# seed: 随机数种子 | |
def train_valid_split(data_set, valid_ratio, seed): | |
"""Split provided training data into training set and validation set""" | |
valid_set_size = int(valid_ratio * len(data_set)) | |
train_set_size = len(data_set) - valid_set_size | |
train_set, valid_set = random_split(data_set, [train_set_size, valid_set_size], generator=torch.Generator().manual_seed(seed)) # 将一个数据集随机拆成两个无重叠的数据集,两个数据集的大小由第二个参数指定 | |
return np.array(train_set), np.array(valid_set) |
# 预测函数
一个预测函数,当我们希望用我们之前训练的模型去预测未知数据时可以调用这个函数,其中:
model.eval()
将模型设置为 eval 模式,此时,模型中的某些层会不起作用,来保证预测的同时不会改变训练好的模型参数。- tqdm 是一个 github 项目,用于展示进度条。注意
tqdm(test_loader)
只会对进度计数,而不会改变迭代逻辑,即x
是从test_loader
中取出的tensor
类型数据。 model(x)
=model.forward(x)
,在 PyTorch 中直接调用模型并传递输入数据时,会自动调用模型的forward
方法。即这一行的代码的含义是:调用模型进行预测。Tensor.detach()
从当前计算图中分离出新的 tensor,该 tensor 不具有梯度,也不会反向传播。由于在 PyTorch 中,变量不仅是数值和数据类型,还包含了该变量的计算过程,而通过detach()
可以将计算过程剥离只剩下数值。.cpu()
将变量放在 cpu 上。torch.cat()
用于将两个tensor
拼接在一起,dim
参数指定了是横着拼接还是竖着拼接(对于矩阵).numpy()
用于将Tensor
类型转成Numpy
类型。CUDA 只接受Tensor
类型,不接受其他变量类型。
def predict(test_loader, model, device): | |
model.eval() # Set your model to evaluation mode. | |
preds = [] | |
for x in tqdm(test_loader): # test_loader 是测试数据 | |
x = x.to(device) # 将模型加载到 CPU 上 | |
with torch.no_grad(): # 禁用梯度计算,节省时间 | |
pred = model(x) # 调用模型进行预测 | |
preds.append(pred.detach().cpu()) # pred 可直接当参数也不会报错,这里是标准化处理 | |
preds = torch.cat(preds, dim=0).numpy() | |
return preds |
# 预测结果保存
将模型预测的结果保存成 csv,这个其实是因为原本本作业的完成方式是上传保存的文件,我们也就保留下来了。由于只涉及 csv 的操作比较简单,就不过多解释了。
def save_pred(preds, file): | |
""" Save predictions to specified file """ | |
with open(file, 'w') as fp: | |
writer = csv.writer(fp) | |
writer.writerow(['id', 'tested_positive']) # 保存成 id, tested_positive 两列 | |
for i, p in enumerate(preds): | |
writer.writerow([i, p]) |
# 数据集
# 自定义数据集
当我们不使用 PyTorch 官方整理好的数据集,而是使用自己的数据集时,就必须按照 PyTorch 的定义,自行实现一个继承 torch.utils.data.Dataset
这个抽象类的子类,PyTorch 才可以正确处理。
自定义数据集必须继承 Dataset
并至少覆盖以下两个方法:
__len__
: 实现len(dataset)
返还数据集的尺寸。__getitem__
:用来获取索引数据,例如dataset[i]
class COVID19Dataset(Dataset): | |
""" | |
x: Features. | |
y: Targets, if none, do prediction. | |
""" | |
def __init__(self, x, y=None): # 定义初始化方法 | |
if y is None: | |
self.y = y | |
else: | |
self.y = torch.FloatTensor(y) # 预测目标是浮点数 | |
self.x = torch.FloatTensor(x) # 特征也是浮点数 | |
def __getitem__(self, idx): # 定义数据集索引方法 | |
if self.y is None: | |
return self.x[idx] | |
else: | |
return self.x[idx], self.y[idx] | |
def __len__(self): # 定义获取数据长度方法 | |
return len(self.x) |
# Dataloader
pd.read_csv()
:从指定位置读取 csv 表格,并转化为DataFrame
的类型,可以通过.values
取出DataFrame
中的值并转换为多维列表。select_feat()
:从数据集中提取特征,该函数的实现会在下面讲到。
整体过程其实就是:
- 读取 csv 表格中的数据
- 提取数据中的特征
- 将特征转化为数据集
Dataset
- 根据配置,将
Dataset
转化成DataLoader
用于后续训练
# Set seed for reproducibility | |
same_seed(config['seed']) | |
# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days) | |
# test_data size: 1078 x 117 (without last day's positive rate) | |
train_data, test_data = pd.read_csv('./covid.train.csv').values, pd.read_csv('./covid.test.csv').values | |
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed']) | |
# Print out the data size. | |
print(f"""train_data size: {train_data.shape} | |
valid_data size: {valid_data.shape} | |
test_data size: {test_data.shape}""") | |
# Select features | |
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all']) | |
# Print out the number of features. | |
print(f'number of features: {x_train.shape[1]}') | |
# initialize datasets with the data | |
train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \ | |
COVID19Dataset(x_valid, y_valid), \ | |
COVID19Dataset(x_test) | |
# Pytorch data loader loads pytorch dataset into batches. | |
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True) | |
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True) | |
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True) |
# 全局配置
device
:设置训练使用的硬件,优先 CUDA,如果没有的话就使用 CPU。
config
中的内容比较顾名思义了,稍微有点机器学习基础应该都能看懂。
device = 'cuda' if torch.cuda.is_available() else 'cpu' | |
config = { | |
'seed': 5201314, # Your seed number, you can pick your lucky number. :) | |
'select_all': True, # Whether to use all features. | |
'valid_ratio': 0.2, # validation_size = train_size * valid_ratio | |
'n_epochs': 3000, # Number of epochs. | |
'batch_size': 256, | |
'learning_rate': 1e-5, | |
'early_stop': 400, # If model has not improved for this many consecutive epochs, stop training. | |
'save_path': './models/model.ckpt' # Your model will be saved here. | |
} |
# 关键函数编写
由于关键函数的编写也正是作业的内容,所以将该内容放在了最后,我们需要完成 3 个关键函数的编 / 改写,分别是:
- 模型
My_Model()
- 特征提取算法
select_feat()
- 训练算法
trainer()
# My_Model()
基础模型如下:
class My_Model(nn.Module): | |
def __init__(self, input_dim): # 定义模型初始化方法 | |
super(My_Model, self).__init__() # 调用父类的__init__方法 | |
# TODO: modify model's structure, be aware of dimensions. | |
self.layers = nn.Sequential( # Sequential 中前一层输出大小必须与下一层的输入大小一致 | |
nn.Linear(input_dim, 16), # 定义一个全连接层 | |
nn.ReLU(), # 使用 ReLU 作为激活函数 | |
nn.Linear(16, 8), # 该层输入 tensor 为 16,输出 tensor 为 8 | |
nn.ReLU(), | |
nn.Linear(8, 1) | |
) | |
def forward(self, x): # 定义前向传播算法 | |
x = self.layers(x) # 将 x 传递给 layers 的各层进行运算,默认写法。 | |
x = x.squeeze(1) # (B, 1) -> (B) | |
return x |
使用该模型训练出的 loss 为:
Epoch [1869/3000]: Train loss: 41.3380, Valid loss: 37.6867
同时,使用此训练的模型去预测会发现预测出的 tested_positive
都会是同一个值,这显然是不对的,猜测可能是因为模型神经元数目太少,而特征又太多,导致模型无法学习到有效特征,loss 的下降其实也只是类似求一个最佳均值的过程,所以我们尝试通过增加层数的方法来优化他(称为优化 1):
self.layers = nn.Sequential( | |
nn.Linear(input_dim, 16), | |
nn.ReLU(), | |
nn.Linear(16, 8), | |
nn.ReLU(), | |
nn.Linear(8, 4), | |
nn.ReLU(), | |
nn.Linear(4, 1) | |
) |
loss 有显著降低:
Epoch [3000/3000]: Train loss: 1.5405, Valid loss: 1.5447
为了防止过拟合问题,我们跑一下验证集试试,结果为
Loss: tensor(3.2558, dtype=torch.float64)
# select_feat()
默认的特征提取函数默认情况下是使用全部特征(不提取特征)的,代码如下:
def select_feat(train_data, valid_data, test_data, select_all=True): | |
"""Selects useful features to perform regression""" | |
y_train, y_valid = train_data[:, -1], valid_data[:, -1] | |
raw_x_train, raw_x_valid, raw_x_test = train_data[:, :-1], valid_data[:, :-1], test_data # 分离 tested_positive 列和其他特征列 | |
if select_all: | |
feat_idx = list(range(raw_x_train.shape[1])) | |
else: | |
feat_idx = [0, 1, 2, 3, 4] # TODO: Select suitable feature columns. | |
return raw_x_train[:, feat_idx], raw_x_valid[:, feat_idx], raw_x_test[:, feat_idx], y_train, y_valid |
其中 train_data
和 valid_data
都是多维数组, train_data[:, -1]
是指将列表中的每一个子列表的最后一项提取出来组成一个新的列表:
观察训练集可以发现,最后一列就是 tested_positive
列,即 y_train
为训练集的 tested_positive
列表, y_valid
为验证集的 tested_positive
列表;另外, raw_x_train
就是将 tested_positive
排除的其他数据特征, raw_x_valid
同理。
若配置 select_all=true
时,会将其他特征全部返回,否则只会返回指定的特征列。考虑到原本数据集中的部分特征可能对我们想要的预测的内容没什么贡献,反倒可能会影响机器的学习,那么我们就可以考虑人为分析一下样本中的不同字段对结果的贡献从而优化机器学习的效果(称为优化 2)。
先分析一下样本数据:
样本列 | 含义 |
---|---|
AL、AK、AZ、AR、...... | 美国不同州的缩写 |
cli、ili | COVID-like illness、influenza-like illness |
hh_cmnty_cli、nohh_cmnty_cli | 社区报告病例(包括 / 不包括家庭) |
wearing_mask | 戴口罩 |
travel_outside_state、work_outside_home | 外出旅行、非居家办公 |
shop、restaurant | 商场、餐厅 |
spent_time | 过去 24 小时内与当前不在身边的人在一起 |
large_event | 出席 10 人以上的活动 |
public_transit | 过去 24 小时使用公共交通 |
anxious、depressed、worried_finances | 焦虑、压力、担心财务状况 |
如果纯靠人为经验来分析的话,可能会觉得都有关系,这个时候我们可以借助 Person 相关性分析方法,来分析各个变量与 tested_positive
的相关性,通过具体的数值来找到最相关的列。 pandas
库提供了封装好的 Person 相关性分析算法,直接使用即可:
import pandas as pd | |
df = pd.read_csv('./data/covid.train.csv') | |
print(df.corr()['tested_positive'].sort_values(ascending=False)) # 降序排列 | |
# ---- Output ---- | |
tested_positive 1.000000 | |
tested_positive.1 0.985053 | |
tested_positive.2 0.969467 | |
tested_positive.3 0.953463 | |
tested_positive.4 0.935388 | |
... | |
public_transit -0.411916 | |
public_transit.1 -0.415510 | |
public_transit.2 -0.418523 | |
public_transit.3 -0.421868 | |
public_transit.4 -0.424169 |
我们把这些强相关的列作为特征在优化 1 的基础上去优化模型(称为优化 2)。
feat_idx = list(range(38)) + [ | |
53, 69, 85, 101, # tested_positive | |
49, 65, 81, 97, 113, # public_transit | |
40, 41, 56, 57, 72, 73, 88, 89, 104, 105, # hh_cmnty_cli & nohh_cmnty_cli | |
38, 39, 54, 55, 70, 71, 86, 87, 102, 103, # cli & ili | |
50, 66, 82, 98, 114 # anxious | |
] # TODO: Select suitable feature columns. |
此时,模型在训练集上的 loss 有所上升,但是在验证集上有所下降。
# --- 测试集 --- | |
Epoch [2547/3000]: Train loss: 1.8915, Valid loss: 1.6332 | |
# --- 验证集 --- | |
Loss: tensor(3.1981, dtype=torch.float64) |
# trainer()
源码中给出基本的 trainer()
函数,注释已经非常清楚了,代码如下:
def trainer(train_loader, valid_loader, model, config, device): | |
criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this. | |
# Define your optimization algorithm. | |
# TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms. | |
# TODO: L2 regularization (optimizer(weight decay...) or implement by your self). | |
optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.9) | |
writer = SummaryWriter() # Writer of tensoboard. | |
if not os.path.isdir('./models'): | |
os.mkdir('./models') # Create directory of saving models. | |
n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0 # initialize some variables | |
for epoch in range(n_epochs): | |
model.train() # Set your model to train mode. | |
loss_record = [] | |
# tqdm is a package to visualize your training progress. | |
train_pbar = tqdm(train_loader, position=0, leave=True) | |
for x, y in train_pbar: | |
optimizer.zero_grad() # Set gradient to zero. | |
x, y = x.to(device), y.to(device) # Move your data to device. | |
pred = model(x) | |
loss = criterion(pred, y) | |
loss.backward() # Compute gradient(backpropagation). | |
optimizer.step() # Update parameters. | |
step += 1 | |
loss_record.append(loss.detach().item()) | |
# Display current epoch number and loss on tqdm progress bar. | |
train_pbar.set_description(f'Epoch [{epoch + 1}/{n_epochs}]') | |
train_pbar.set_postfix({'loss': loss.detach().item()}) | |
mean_train_loss = sum(loss_record) / len(loss_record) | |
writer.add_scalar('Loss/train', mean_train_loss, step) | |
model.eval() # Set your model to evaluation mode. | |
loss_record = [] | |
for x, y in valid_loader: | |
x, y = x.to(device), y.to(device) | |
with torch.no_grad(): | |
pred = model(x) | |
loss = criterion(pred, y) | |
loss_record.append(loss.item()) | |
mean_valid_loss = sum(loss_record) / len(loss_record) | |
tqdm.write(f'Epoch [{epoch + 1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, Valid loss: {mean_valid_loss:.4f}') | |
writer.add_scalar('Loss/valid', mean_valid_loss, step) # 向 tensoboard 添加图表 | |
if mean_valid_loss < best_loss: | |
best_loss = mean_valid_loss | |
torch.save(model.state_dict(), config['save_path']) # Save your best model | |
tqdm.write('Saving model with loss {:.3f}...'.format(best_loss)) | |
early_stop_count = 0 | |
else: | |
early_stop_count += 1 | |
if early_stop_count >= config['early_stop']: | |
tqdm.write('\nModel is not improving, so we halt the training session.') | |
return |
提示中给出了两个优化方向,引入 L2 正则化和寻找更好的 optimizer
,经过实际测试,如果不引入 L2 正则化就直接换用别的 optimizer
会导致过拟合的问题,所以我们先引入 L2 正则化。
# L2 regularization & Dropout
简单来说,L2 正则化可以直观理解为它对于大数值的权重向量进行严厉惩罚,倾向于更加分散的权重向量。由于输入和权重之间的乘法操作,这样就有了一个优良的特性:使网络更倾向于使用所有输入特征,而不是严重依赖输入特征中某些小部分特征。 L2 惩罚倾向于更小更分散的权重向量,这就会鼓励分类器最终将所有维度上的特征都用起来,而不是强烈依赖其中少数几个维度。这样做可以提高模型的泛化能力,降低过拟合的风险。
与 L2 正则化类似的是 L1 正则化,也是引入惩罚措施来降低过拟合的风险,一般情况下,L2 正则化模型效果会更好一些。
👆理论来说是这样的,但是在实际测试中,引入 L2 正则化之后仍然会有过拟合的问题,效果不是很好,我们可以使用 Dropout
的方法来缓解过拟合问题。
Dropout
的思想是在训练过程中随机让部分神经元 “失活”,通过这种方式丢失部分特征,限制模型的学习能力,通过这种方法,可以使神经网络更加关注于 “通用的” 特征,而不是训练集中的特化特征。使用 Dropout
需要对模型做出变化:
class My_Model(nn.Module): | |
def __init__(self, input_dim): | |
super(My_Model, self).__init__() | |
# TODO: modify model's structure, be aware of dimensions. | |
self.layers = nn.Sequential( | |
nn.Linear(input_dim, 32), | |
nn.LeakyReLU(0.1), | |
nn.Dropout(p=0.2), # 添加 Dropout,指定丢弃概率 | |
nn.Linear(32, 16), | |
nn.LeakyReLU(0.1), | |
nn.Linear(16, 8), | |
nn.LeakyReLU(0.1), | |
nn.Linear(8, 1) | |
) |
在实际测试中,将激活函数从 ReLU
替换成了 LeakyReLU
并增加模型神经元数,取得了更好的效果(关于激活函数后面单独拿一章讲)。
# optimizer
经过测试,选用 Adam
作为 optimizer
会有更好的效果, Adam
是一个自适应的梯度下降算法,自动的调整 learning-rate
,我们知道,设置合适的超参数 learning-rate
是非常困难的,若 lr
过大,会导致 loss 在最低点出震荡;若 lr
过小,则会导致训练速度过慢。直接使用 Adam
可以帮助我们解决这个烦恼,我们只需要在学习的开始设置一个较大的 lr
, Adam
会自动随着计算调整合适的 lr
,代码如下:
optimizer = torch.optim.Adam(model.parameters(), lr=config['learning_rate']) | |
# 调整 超参数 | |
config = { | |
'seed': 194343431, | |
'select_all': False, | |
'valid_ratio': 0.2, | |
'n_epochs': 3000, | |
'batch_size': 256, | |
'learning_rate': 1e-3, # 这里从 1e-5 改成了 1e-3 | |
'early_stop': 400, | |
'save_path': './models/model.ckpt' | |
} |
这样的优化(称为优化 3)的结果为:
# --- 测试集 --- | |
Epoch [498/15000]: Train loss: 1.3171, Valid loss: 5.3677 | |
# --- 验证集 --- | |
Loss: tensor(2.2407, dtype=torch.float64) |
通过选取合适的特征,loss 可以进一步下降:
# 修改 feat 选取 | |
feat_idx = list(range(38)) + [ | |
53, 69, 85, 101, # tested_positive | |
49, 65, 81, 97, 113, # public_transit | |
] # TODO: Select suitable feature columns. | |
# --- 测试集 --- | |
Epoch [447/3000]: Train loss: 1.6422, Valid loss: 2.8130 | |
# --- 验证集 --- | |
Loss: tensor(1.4970, dtype=torch.float64) |
在实际测试中,如果不加上州的特征(前 38 个)的话,Loss 会比较高,加上之后,在测试集上的 loss 会低一些。理论来说,州的特征与 tested_positive
的相关性并不高,如果只是解释为过拟合的话我觉得不是很合理,所以这里的特征选择就没有什么理论指导了,纯粹是试出来的,如果有大佬能解释为什么求求解释一下 Orz
# 总结
可以看到随着预测的准确率提升,预测结果和实际结果的拟合是越来越好的。整个项目至此先告一段落了,来来回回测试了不下 100 多种优化的组合,感觉继续胡乱尝试没有什么意义,通过这个作业熟悉 PyTorch、学习神经网络知识的目标也已经达到了,接下来将会继续往后学习,进一步提高能力才是王道。
# 整体代码
main.py
# Numerical Operations | |
import math | |
import numpy as np | |
# Reading/Writing Data | |
import pandas as pd | |
import os | |
import csv | |
# For Progress Bar | |
from tqdm import tqdm | |
# Pytorch | |
import torch | |
import torch.nn as nn | |
from torch.utils.data import Dataset, DataLoader, random_split | |
# For plotting learning curve | |
from torch.utils.tensorboard import SummaryWriter | |
# For plot | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
device = 'cuda' if torch.cuda.is_available() else 'cpu' | |
config = { | |
'seed': 7777777, # Your seed number, you can pick your lucky number. :) | |
'select_all': False, # Whether to use all features. | |
'valid_ratio': 0.2, # validation_size = train_size * valid_ratio | |
'n_epochs': 3000, # Number of epochs. | |
'batch_size': 256, | |
'learning_rate': 1e-3, | |
'early_stop': 400, # If model has not improved for this many consecutive epochs, stop training. | |
'save_path': './models/model.ckpt' # Your model will be saved here. | |
} | |
def same_seed(seed): | |
"""Fixes random number generator seeds for reproducibility.""" | |
torch.backends.cudnn.deterministic = True | |
torch.backends.cudnn.benchmark = False | |
np.random.seed(seed) | |
torch.manual_seed(seed) | |
if torch.cuda.is_available(): | |
torch.cuda.manual_seed_all(seed) | |
# Set seed for reproducibility | |
same_seed(config['seed']) | |
def train_valid_split(data_set, valid_ratio, seed): | |
"""Split provided training data into training set and validation set""" | |
valid_set_size = int(valid_ratio * len(data_set)) | |
train_set_size = len(data_set) - valid_set_size | |
train_set, valid_set = random_split(data_set, [train_set_size, valid_set_size], | |
generator=torch.Generator().manual_seed(seed)) | |
return np.array(train_set), np.array(valid_set) | |
def predict(test_loader, model, device): | |
model.eval() # Set your model to evaluation mode. | |
preds = [] | |
for x in tqdm(test_loader): | |
x = x.to(device) | |
with torch.no_grad(): | |
pred = model(x) | |
preds.append(pred.detach().cpu()) | |
preds = torch.cat(preds, dim=0).numpy() | |
return preds | |
def save_pred(preds, file): | |
""" Save predictions to specified file """ | |
with open(file, 'w') as fp: | |
writer = csv.writer(fp) | |
writer.writerow(['id', 'tested_positive']) | |
for i, p in enumerate(preds): | |
writer.writerow([i, p]) | |
class COVID19Dataset(Dataset): | |
""" | |
x: Features. | |
y: Targets, if none, do prediction. | |
""" | |
def __init__(self, x, y=None): | |
if y is None: | |
self.y = y | |
else: | |
self.y = torch.FloatTensor(y) | |
self.x = torch.FloatTensor(x) | |
def __getitem__(self, idx): | |
if self.y is None: | |
return self.x[idx] | |
else: | |
return self.x[idx], self.y[idx] | |
def __len__(self): | |
return len(self.x) | |
class My_Model(nn.Module): | |
def __init__(self, input_dim): | |
super(My_Model, self).__init__() | |
# TODO: modify model's structure, be aware of dimensions. | |
self.layers = nn.Sequential( | |
nn.Linear(input_dim, 32), | |
nn.LeakyReLU(0.1), | |
nn.Dropout(p=0.2), # 添加 Dropout,指定丢弃概率 | |
nn.Linear(32, 16), | |
nn.LeakyReLU(0.1), | |
nn.Linear(16, 8), | |
nn.LeakyReLU(0.1), | |
nn.Linear(8, 1) | |
) | |
def forward(self, x): | |
x = self.layers(x) | |
x = x.squeeze(1) # (B, 1) -> (B) | |
return x | |
def select_feat(train_data, valid_data, test_data, select_all=True): | |
"""Selects useful features to perform regression""" | |
y_train, y_valid = train_data[:, -1], valid_data[:, -1] | |
raw_x_train, raw_x_valid, raw_x_test = train_data[:, :-1], valid_data[:, :-1], test_data | |
if select_all: | |
feat_idx = list(range(raw_x_train.shape[1])) | |
else: | |
feat_idx = list(range(38)) + [ | |
53, 69, 85, 101, # tested_positive | |
49, 65, 81, 97, 113, # public_transit | |
] # TODO: Select suitable feature columns. | |
return raw_x_train[:, feat_idx], raw_x_valid[:, feat_idx], raw_x_test[:, feat_idx], y_train, y_valid | |
def trainer(train_loader, valid_loader, model, config, device): | |
criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this. | |
# Define your optimization algorithm. | |
# TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms. | |
# TODO: L2 regularization (optimizer(weight decay...) or implement by your self). | |
optimizer = torch.optim.Adam(model.parameters(), lr=config['learning_rate']) | |
writer = SummaryWriter() # Writer of tensoboard. | |
if not os.path.isdir('./models'): | |
os.mkdir('./models') # Create directory of saving models. | |
n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0 | |
for epoch in range(n_epochs): | |
model.train() # Set your model to train mode. | |
loss_record = [] | |
# tqdm is a package to visualize your training progress. | |
train_pbar = tqdm(train_loader, position=0, leave=True) | |
for x, y in train_pbar: | |
optimizer.zero_grad() # Set gradient to zero. | |
x, y = x.to(device), y.to(device) # Move your data to device. | |
pred = model(x) | |
loss = criterion(pred, y) | |
loss.backward() # Compute gradient(backpropagation). | |
optimizer.step() # Update parameters. | |
step += 1 | |
loss_record.append(loss.detach().item()) | |
# Display current epoch number and loss on tqdm progress bar. | |
train_pbar.set_description(f'Epoch [{epoch + 1}/{n_epochs}]') | |
train_pbar.set_postfix({'loss': loss.detach().item()}) | |
mean_train_loss = sum(loss_record) / len(loss_record) | |
writer.add_scalar('Loss/train', mean_train_loss, step) | |
# 在验证集上进行模型准确率的分析验证。 | |
model.eval() # Set your model to evaluation mode. | |
loss_record = [] | |
for x, y in valid_loader: | |
x, y = x.to(device), y.to(device) | |
with torch.no_grad(): | |
pred = model(x) | |
loss = criterion(pred, y) | |
loss_record.append(loss.item()) | |
mean_valid_loss = sum(loss_record) / len(loss_record) | |
tqdm.write(f'Epoch [{epoch + 1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, ' | |
f'Valid loss: {mean_valid_loss:.4f}') | |
writer.add_scalar('Loss/valid', mean_valid_loss, step) | |
if mean_valid_loss < best_loss: # 如果当前 loss 低于过去最低的 loss,则记录 loss,并保存当前最好的模型。 | |
best_loss = mean_valid_loss | |
torch.save(model.state_dict(), config['save_path']) # Save your best model | |
tqdm.write('Saving model with loss {:.3f}...'.format(best_loss)) | |
early_stop_count = 0 | |
else: | |
early_stop_count += 1 | |
if early_stop_count >= config['early_stop']: | |
tqdm.write('\nModel is not improving, so we halt the training session.') | |
return | |
# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days) | |
# test_data size: 1078 x 117 (without last day's positive rate) | |
train_data, test_data = pd.read_csv('./data/covid.train.csv').values, pd.read_csv('./data/covid.test.csv').values | |
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed']) | |
# Print out the data size. | |
tqdm.write(f""" | |
train_data size: {train_data.shape} | |
valid_data size: {valid_data.shape} | |
test_data size: {test_data.shape}""") | |
# Select features | |
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all']) | |
# Print out the number of features. | |
tqdm.write(f'number of features: {x_train.shape[1]}') | |
train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \ | |
COVID19Dataset(x_valid, y_valid), \ | |
COVID19Dataset(x_test) | |
# Pytorch data loader loads pytorch dataset into batches. | |
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True) | |
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True) | |
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True) | |
model = My_Model(input_dim=x_train.shape[1]).to(device) # put your model and data on the same computation device. | |
# 开始训练模型 | |
trainer(train_loader, valid_loader, model, config, device) | |
# 使用进行模型预测 | |
# model.load_state_dict(torch.load(config['save_path'])) | |
# preds = predict(test_loader, model, device) | |
# save_pred(preds, 'pred.csv') | |
# | |
# predict_data, real_data = pd.read_csv('./pred.csv'), pd.read_csv('./data/covid.test.csv') | |
# # 计算预测值和实际值的 MSE | |
# loss = nn.MSELoss() | |
# out = loss(torch.tensor(predict_data['tested_positive'].values), torch.tensor(real_data['tested_positive'].values)) | |
# print('Loss: ', out) | |
# df = pd.concat ([predict_data ['tested_positive'], real_data ['tested_positive']], axis=1) # 拼接两个 Series | |
# df.columns = ['predicted', 'ground truth'] # 重命名列名 | |
# sns.lmplot (x='predicted', y='ground truth', data=df) # 绘制拟合线性回归图 | |
# plt.show() |
# 参考
- https://speech.ee.ntu.edu.tw/~hylee/ml/ml2022-course-data/HW01.pdf
- https://www.bilibili.com/video/BV1m3411p7wD?p=1