李宏毅老师 2022 年春季学习机器中留的第一次大作业,需要根据各项疫情相关数据,自行实现网络结构并训练模型,最终实现一个可以精准预测疫情增长人数的深度学习模型。

# 大作业内容

根据人们填写的日常调查问卷,设计并实现一个深度学习模型用于 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)来评价误差。

MSE=1Ni=1N(yiy^i)2MSE = {1 \over N} \sum_{i=1}^{N}(y_i- \hat y_i)^2

  • yiy_i:模型预测值
  • y^i\hat y_i:实际值

# 作业目的

  • 通过深度神经网络(deep neural networks,DNN)解决一个回归问题
  • 理解基本的 DNN 训练技巧
    • 超参数调优、特征选择、正则化、...
  • 熟悉 PyTorch

# 环境准备

# Conda

本文不在赘述 Conda 的安装,参考另一篇博客

# 数据集

gdown 是一个 github 项目,是一个由 python 实现的从 Google Drive 下载大文件的工具(由于安全策略无法使用 wget/curl 进行下载)。如果之前从未用 gdown 下载过东西,需要现在 condabase 环境安装 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 页面即可看到训练过程的图表。

tensorboard

# 实用函数

针对源代码给出的无需修改的工具函数,简单做一些解释。

# 设置随机数种子

为了保证 reproducibility(可复现性),需要设置随机数生成种子,官方文档给出了详细解释,这里简单说明一下。

由于机器学习中涉及到很多随机数和随机过程,那么这些随机数和随机过程就可能直接对结果产生影响,从实验的可复现性来讲,我们希望同一个代码的多次运行结果是一致的,我们分别讨论。

  • 随机数:计算机生成随机数的方法其实是根据一个随机数 “种子”,然后以某种确定性的算法来实现的,这个种子常常是一个变化的值(例如时间戳),以此来保证生成的随机序列不可预测。那么在当前环境下,我们就可以通过给定一个随机数种子的方式来尽可能的保证随机数序列相同,减少对可复现性的干扰。

    由于程序中可能涉及到多个库,而不同库又采用不同的随机数生成器,所以需要分别对所有涉及到的库设置随机数种子,如代码中就同时设置了 PyTorchnumpy 的种子。

  • 随机过程:在使用 CUDA 对训练过程进行加速时,cuDNN 可能会通过基准测试来从多个卷积中选择最快的卷积,成为可能的不确定来源之一。

    另一方面,cuDNN 在训练过程中采用的某些算法本身可能就是不确定的,每次运算会产生不同的结果。这些都是我们需要避免的。

遗憾的是,即使我们选用相同的随机数种子,PyTorch 仍无法确保可复现性,设置同一随机数种子和确定过程仅能保证在同一平台、同一硬件、同一 PyTorch 版本的下,多次实验结果一致。

# 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() :从数据集中提取特征,该函数的实现会在下面讲到。

整体过程其实就是:

  1. 读取 csv 表格中的数据
  2. 提取数据中的特征
  3. 将特征转化为数据集 Dataset
  4. 根据配置,将 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 个关键函数的编 / 改写,分别是:

  1. 模型 My_Model()
  2. 特征提取算法 select_feat()
  3. 训练算法 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_datavalid_data 都是多维数组, train_data[:, -1] 是指将列表中的每一个子列表的最后一项提取出来组成一个新的列表:

[[1,2,3],[4,5,6],[7,8,9]][3,6,9][[1,2,3],[4,5,6],[7,8,9]] \rightarrow [3,6,9]

观察训练集可以发现,最后一列就是 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、iliCOVID-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 可以帮助我们解决这个烦恼,我们只需要在学习的开始设置一个较大的 lrAdam 会自动随着计算调整合适的 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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝