Title

李宏毅ML_Spring2021HW01学习记录

李宏毅ML_2021Spring_HW1

写在前面

可能会有一些小错误,会持续检查和更正的


题目如下

Step1. 导入相关库

# 导入PyTorch相关库
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

# 导入数据处理相关库
import numpy as np
import csv
import os

# 导入绘图相关库
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure

myseed = 42069
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(myseed)
torch.manual_seed(myseed)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(myseed)

这里设置随机数种子是为了保证实验可复现性,定义一个固定的随机种子,种子是随机数生成器的初始值,只要种子相同,每次运行程序时生成的随机数序列就会完全一样。

torch.backends.cudnn.deterministic = True

  • 强制CuDNN使用确定性算法。CuDNN在某些情况下为了追求速度,可能会使用非确定性的算法(即在相同的输入下,多次运行的结果可能略有差异)。设置为True可以确保结果的一致性,但可能会牺牲一点点性能

torch.backends.cudnn.benchmark = False

  • 禁用CuDNN的自动寻找最优卷积算法的功能。当输入数据的尺寸固定时,CuDNN可以在第一次运行时测试多种卷积算法,并选择最快的一种,但这本身就是一个随机的过程,会导致不确定性,将其设置为False可以确保每次都使用相同的算法

np.random.seed(myseed)

  • 设置NumPy库的随机数生成器种子,影响所有使用np.random的操作

torch.manual_seed(myseed)

  • 设置PyTorch的随机数生成器种子,影响PyTorch中的随机操作

设置随机数种子也是为了保证可重现性


Step2. 一些函数 (Ulitity不知道咋翻译)

如果GPU能用,就用GPU,不然就用CPU

def get_device():
    return "cuda" if torch.cuda.is_available() else "cpu"

绘制模型在训练过程中的学习曲线,即训练损失(train loss)和验证损失(dev loss)随训练步数变化的趋势。

def plot_learning_curve(loss_record, title = ''):
    total_steps = len(loss_record['train'])
    x_1 = range(total_steps)
    x_2 = x_1[::len(loss_record['train']) // len(loss_record['dev'])]
    figure(figsize = (6, 4))
    plt.plot(x_1, loss_record['train'], c = 'tab:red', label = 'train')
    plt.plot(x_2, loss_record['dev'], c = 'tav:cyan', label = 'dev')
    plt.ylim(0.0, 5.)
    plt.xlabel('Training steps')
    plt.ylabel('MES loss')
    plt.title('Learning curve of{}'.format(title))
    plt.legend()
    plt.show()
  • 它接收一个名为loss_record的字典,这个字典存了两个列表:一个是每一步(batch)训练的损失(train loss),另一个是每一轮(epoch)训练结束后的验证损失(dev loss)
  • 它为训练步骤创建一个x轴x_1
  • x_2 = x_1[::len(loss_record['train'])] // len(loss_record['dev'])是一个小技巧。因为验证损失的记录频率远(一个epoch)低于训练损失(一个batch)(训练时每批数据都记,验证时跑完一整轮才记一次),这行代码能巧妙地在x轴上找到对应的位置来画验证损失的点,确保两条线能够对齐。

评估模型的最终预测能力,通过画散点图的方式将模型的“预测值”与“真实值”进行比较

def plot_pred(dv_set, model, device, lim = 35, preds = None, targets = None):
    if preds is None or targets is None:
        model.eval()
        preds, targets = [], []
        for x, y in dv_set:
            x, y = x.to(device), y.to(device)
            with torch.no_grad():
                pred = model(x)
                preds.append(pred.detach().cpu())
                targets.append(y.detach().cpu())
        preds = torch.cat(preds, dim = 0).numpy()
        targets = torch.cat(targets, dim = 0).numpy()
    
    figure(figsize = (5, 5))
    plt.scatter(targets, preds, c = 'r', alpha = 0.5)
    plt.plot([-0.2, lim], [-0.2, lim], c = 'b') # 直线 y = x
    plt.xlim(-0.2, lim)
    plt.ylim(-0.2, lim)
    plt.xlabel('ground truth value')
    plt.ylabel('predicted value')
    plt.title('Ground Truth v.s. Prediction')
    plt.show()
  • moedl.eval():将模型切换到"评估模式",这会关闭一些只在训练时才开启的功能(比如Dropout)
  • with torch.no_grad():告诉PyTorch在这个代码块中不要计算梯度,因为我们是在做预测,不是在训练,这样做可以节省计算资源和内存,运行得更快。不然的话PyTorch会继续构建计算图累加到原来的结果上,就糟糕了
  • 它会遍历验证集(dv_set)的每一个批次(batch),用模型(model)去进行预测
  • x, y = x.to(device), y.to(device)将数据和标签移动到指定的计算设备上
  • preds.append(pred.detach().cpu()): 将预测结果从计算设备(GPU)移回 CPU,并从计算图中分离(detach()),然后存入列表。
  • targets.append(y.detach().cpu()): 同样处理真实标签。
  • torch.cat(..., dim=0).numpy(): 将所有批次的预测值和真实值拼接成一个大的张量,然后转换为 NumPy 数组,以便 Matplotlib 绘图。
  • 然后,会用plt.scatter()绘制散点图
    • x轴代表真实值
    • y轴代表模型的预测值
  • 同时会画一条呈45度角的蓝色对角线,这条线代表"完美预测"(预测值 = 真实值)

Step3. 预处理

我们已经有了数据集了:

  • train:训练集
  • dev:验证集
  • test:测试集

Dataset -- 图书管理员

class COVID19Dataset(Dataset):

    def __init__(self, path, mode = 'train', target_only = False):
        self.mode = mode
    
        # 把数据读到NumPy的arrays中
        with open(path, 'r') as fp:
            data = list(csv.reader(fp))
            data = np.array(data[1:])[:, 1:].astype(float)
        
        if not target_only:
            feats = list(range(93))
        else:
            # 使用 40 个州的特征,以及索引为 57 和 75 的两个tested_positive特征
            feats = list(range(40)) + [57, 75]

        if mode == 'test':
            # 测试数据 Testing data
            # data: 893 x 93 40个列用one-hot表示州 day1(18), day2(18), day3(17)
            data = data[:, feats]
            self.data = torch.FloatTensor(data)
        
        else:
            # 训练数据 Training data (train/dev sets)
            # data: 2700 x 94 day1(18), day2(18), day3(18) 
            target = data[:, -1]
            data = data[:, feats]
            
            # 分割训练数据为训练集和验证集
            if mode == 'train':
                indices = [i for i in range(len(data)) if i % 10 != 0]
            elif mode == 'dev':
                indices = [i for i in range(len(data)) if i % 10 == 0]
            
            # 把数据变成PyTorch的tensors
            self.data = torch.FloatTensor(data[indices])
            self.target = torch.FloatTensor(target[indices])

            #数据归一化(有的Error Surface梯度下降可能比较困难,归一化使得训练更加顺利)
            self.data[:, 40:] = (self.data[:, 40:] - self.data[:, 40:].mean(dim = 0, keepdim = True)) / self.data[:, 40:].std(dim = 0, keepdim = True)
            
            self.dim = self.data.shape[1]

        
        def __getitem__(self, index):

            if self.mode in ['train', 'dev']:
                # 训练
                return self.data[index], self.target[index]
            else:
                # 测试
                return self.data[index]
        
        def __len__(self):
            # 
            return len(self.data)

注意
Dataset是一个抽象类,不能创造实例,只能用来继承
图书管理员从来都不是特定的某个人,只能得到对应的职权

读取和解析csv文件

path:csv文件的路径
mode:数据集的模式,有三种可能的值:train训练集 ,dev验证集,test测试集,默认为train
target_only:一个布尔值,用于决定是否只使用部分特定的特征,还是使用全部特征

with open(path, 'r') as fp:python打开指定文件的方式,with语句可以确保文件在操作结束后被正确关闭

data = np.array(data[1:])[:, 1:].astype(float)跳过第0行和第0列,因为第0行是表头不是数据,第0列是样本id不是特征信息,然后读进来的数据是字符类型,转换一下方便后续计算

特征选择

如果使用全部特征,就把93个特征列全列进来,不然的话,就按照题目要求使用前40个州的特征以及索引为57和75的两个tested_positive特征

区分不同模式(训练,验证,测试)

对于测试集

data = data[:, feats]
self.data = torch.FloatTensor(data)

这里我们把需要的列取出来并把它从np.ndarry转换成tensors

对于训练/验证集

target = data[:, -1]
data = data[:, feats]

target就是我们理想的输出值
data同上,选出用于训练的特征列

划分数据集(train)和验证集(dev)

if mode == 'train':
    indices = [i for i in range(len(data)) if i % 10 != 0]
elif mode == 'dev':
    indices = [i for i in range(len(data)) if i % 10 == 0]

self.data = torch.FloatTensor(data[indices])
self.target = torch.FloatTensor(target[indices])

这里没有直接分割data数组,而是通过生成索引indices列表的方式来分割数据

  • i % 10 != 0: i % 10 是求 i 除以 10 的余数。这个条件的意思是“如果行号不能被 10 整除”。所以,行号为 0-8, 10-18, 20-28... 的数据会被选为训练集(每 10 条里选 9 条)。

  • i % 10 == 0: 相反,行号为 0, 10, 20... 的数据会被选为验证集(每 10 条里选 1 条)。

data[indices]: 最后,用这个 indices 列表一次性地从 datatarget 中取出所有对应的行,完成数据集的切分。

数据标准化

  • 切片 self.data[:, 40:]: 这里的 40:` 表示“从第 40 列开始,取到最后一列”。为什么从 40 开始?因为根据数据描述,前 40 列是代表不同州的特征(one-hot 编码),它们的值只有 0 或 1,不需要标准化。而后面的列是数值型特征,数值范围可能很大,需要标准化。

  • 公式 (x - mean) / std: 这就是标准的 Z-score 标准化公式。它会把数据的均值变为 0,标准差变为 1,使得所有特征都在一个相似的尺度上。

    • .mean(dim=0): 沿着列(维度0)的方向计算每一列的平均值。

    • .std(dim=0): 沿着列的方向计算每一列的标准差。

def __getitem__(self, index)

  • 作用:根据DataLoader传过来的一个具体的索引号(index),从数据集中取出那一条对应的数据
  • 根据这个代码,DataLoader会传给它一个数字index, 如0, 1, 2...
  • 如果当前是train或者dev模式,模型需要数据(问题)和标签(答案)来进行学习和评估,所以它会返回一个元组(tuple),包含两条信息self.data[index]self.target[index](第index条的特征数据和第index条对应的目标值)
  • 如果是test模式,我们只有数据,没有标签,因为答案是需要模型去预测的,所以只返回self.data[index]

def __len__(self)

  • __init__方法中,我们已经把所有处理好的数据都存放在self.data这个变量里了。
  • len(self.data)就可以返回样本总数(也就是行数)

DataLoader --高效的数据搬运工

Dataset 就像是整个图书馆的藏书清单和图书管理员。它知道总共有多少本书 (__len__),并且你告诉它书号 (index),它就能帮你准确地把那一本书取出来 (__getitem__)

但是,在训练模型时,我们面临一个问题:我们不希望一本一本地去借书(效率太低),也不可能一次性把整个图书馆的书都搬过来(内存会爆炸)。

这时,DataLoader 就登场了。它就像一个超级智能的物流团队,负责高效地从图书馆那里(Dataset)搬运书籍(数据)给在办公室里等着工作的你(模型)。

def prep_dataloader(path, mode, batch_size, n_jobs=0, target_only=False):
    ''' Generates a dataset, then is put into a dataloader. '''
    dataset = COVID19Dataset(path, mode=mode, target_only=target_only)  # Construct dataset
    dataloader = DataLoader(
        dataset, batch_size,
        shuffle=(mode == 'train'), drop_last=False,
        num_workers=n_jobs, pin_memory=True)                            # Construct dataloader
    return dataloader

这个函数是一个"包装"函数,它做了两件事情:

  1. 创建Dataset对象:就是利用我们之前定义的COVID19Dataset类创建了一个数据集实例

  2. 创建DataLoader对象dataloader = DataLoader(), 它接收上一步创建的dataset,并用一系列参数对它进行配置,把它变成一个数据加载器

DataLoader的核心参数

datloader = DataLoader(
    dateset,            # 1. 数据集
    batch_size,         # 2. 批次大小
    shuffle = ...       # 3. 是否打乱
    drop_last = False,  # 4. 是否丢弃最后一个不完整的批次
    num_workers = ...   # 5. 使用多个子进程加载数据
    pin_memory = True   # 6. 是否锁页内存
)
  • dataset

    • 它是什么:我们传入的COVID19Dataset实例
    • 为什么需要DataLoader需要知道它的数据源头在哪里,也就是要去哪个"图书馆"搬书
  • batch_size(批次大小)

    • 决定了每次打包多少条数据(详见mini-batch
  • shuffle = (mode = 'train') 是否打乱

    • 如果为True,DataLoader会在每一轮(epoch)训练开始前,都将数据的顺序完全随机打乱

小结

整个数据流:

原始CSV文件 \(\rightarrow\) Daset(定义了如何读取和处理单条数据) \(\rightarrow\) DataLoader(负责高效地、批量地、可选地打乱数据,并将其打包好) \(\rightarrow\) 一个个批次(batch)的数据(最终送入模型训练)

Step4. 深度神经网络(DNN)

完整代码

class NeuralNet(nn.Module):
    ''' A simple fully-connected deep neural network '''
    def __init__(self, input_dim):
        super(NeuralNet, self).__init__()

        # Define your neural network here
        # TODO: How to modify this model to achieve better performance?
        self.net = nn.Sequential(
            nn.Linear(input_dim, 64),
            nn.ReLU(),
            nn.Linear(64, 1)
        )

        # Mean squared error loss
        self.criterion = nn.MSELoss(reduction='mean')

    def forward(self, x):
        ''' Given input of size (batch_size x input_dim), compute output of the network '''
        return self.net(x).squeeze(1)

    def cal_loss(self, pred, target):
        ''' Calculate loss '''
        # TODO: you may implement L2 regularization here
        return self.criterion(pred, target)

class NeuralNet(nn.Module) 相当于乐高创意工坊

class NeuralNet(nn.Module): 

在PyTorch中,所有自定义的模型都必须继承自torch.nn.Module这个类,它可以:

  • 参数跟踪:它会自动识别我们模型中所有需要学习的参数,我们不需要手动管理
  • 设备转移:我们可以使用.to('cuda')这样的命令,吧整个模型(包括所有参数)搬到GPU上加速运算
  • 模型保存与加载:提供了方便的.state_dict().load_state_dict()方法来保存和加载你训练好的模型
  • 模式切换: 可以用.train().eval()切换训练模式和评估模式

__init__(self, input_dim) 准备乐高积木
这个方法是模型的构造函数,负责定义初始化我们的神经网络拥有的所有"积木块"(

def __init__(self, input_dim):
    super(NeuralNet, self).__init__() #必须的开场白

    #--- 把积木搭好 ---#
    self.net = nn.Sqeuential(
        nn.Linear(input_dim, 64),
        nn.ReLU(),
        nn.Linear(64, 1)
    )

    # --- 准备好评分标准 ---
    self.criterion = nn.MSELoss(reduction='mean')
  1. super(NeurlaNet, self).__init__()
    这是一句必须在最开始调用的代码,它会运行父类nn.Module的初始化逻辑,确保我们的模型工坊能正常运作

  2. self.net = nn.Sequential()
    nn.Sequential是一个非常有用的“容器”或者“流水线管道”,我们可以吧一系列乐高积木块()按顺序放进去,当数据从管道一头进去时,会自动地、依次地通过所有积木块,最后从另一头出来,这让我们的forward方法可以写得很简洁

  3. 流水线里的“积木块”

    • nn.Linear(input_dim, 64)全连接层,这个应该都知道了,毕竟接触到第一个神经网络就是这个,它对输入数据进行一次线性变换(y = Wx + b),可以理解为将输入的特征进行加权、混合,然后提炼出新的特征
      • nn.Linear内部已经包含了需要学习的权重矩阵W和偏置向量b,并且PyTorch会自动对它们进行随机初始化
      • input_dim:输入特征数量。比如,我们的数据有93个特征,这里就是93
      • 64:输入神经元的数量,这代表我们希望这个层提炼出64个新的特征,这是一个可以自己调整的超参数
  • nn.ReLU():激活函数
    • 给网络引入非线性,如果没有非线性层,那么无论我们堆叠多少个nn.Linear层,整个网络本质上还是线性的,理论上一个线性层就替代了,(那么堆叠多层纯纯小丑),学习能量非常有限,非线性激活函数能增加模型复杂度,提高模型的弹性
    • nn.Linear(64, 1):输出层
      • 64: 它的输入数量必须和上一层的输出数量保持一致,这样才能衔接起来
      • 1:它的输出数量是1,因为我们的任务是预测一个单独的数值(确诊人数),所以最终只需要一个输出结果
  1. self.criterion = nn.MSELoss()
    • 模型的“评分标准”,也就是损失函数,nn.MSELoss()是PyTorch内置的均方误差损失函数

forward(self, x)前向传播 说明书

这个方法定义了数据如何流过我们在__init__中准备好的积木块

def forward(self, x):
    # 数据 x 直接通过 self.net 这条搭建好的流水线
    return self.net(x).squeeze(1)
  • x: 代表一批输入的数据(一个Tensor)
  • self.net(x): 因为我们使用了nn.Sequential,所以这里的代码异常简洁,我们直接吧数据x喂给self.net这个管道,它就会自动地按照Linear -> ReLU -> Linear的顺序进行计算,并返回最终结果
  • .squeeze(1): 这是一个形状调整操作。self.net输出的形状是[批次大小, 1],而我们的真实标签是[批次大小].squeeze(1)会挤掉那个多余的维度1,让预测和标签的形状匹配,方便后续计算损失

小结

\(NeuralNet\)如何工作?

  1. 创建模型: 当我们写 model = NeuralNet(input_dim=93) 时,__init__ 方法被调用,模型的所有“积木块”(层)都被创建并准备好。

  2. 进行预测: 在训练循环中,当我们写 prediction = model(data) 时,PyTorch 会自动调用 forward(data) 方法。数据会按照我们定义的路径流过整个网络,最终得到预测结果。

  3. 计算误差: 接着,我们调用 loss = model.cal_loss(prediction, target) 来计算预测的好坏。


Step5. Train/Dev/Test

5.1 Train

def train(tr_set, dv_set, model, config, device):
    ''' DNN training '''

    n_epochs = config['n_epochs']  # Maximum number of epochs

    # Setup optimizer
    optimizer = getattr(torch.optim, config['optimizer'])(
        model.parameters(), **config['optim_hparas'])

    min_mse = 1000.
    loss_record = {'train': [], 'dev': []}      # for recording training loss
    early_stop_cnt = 0
    epoch = 0
    while epoch < n_epochs:
        model.train()                           # set model to training mode
        for x, y in tr_set:                     # iterate through the dataloader
            optimizer.zero_grad()               # set gradient to zero
            x, y = x.to(device), y.to(device)   # move data to device (cpu/cuda)
            pred = model(x)                     # forward pass (compute output)
            mse_loss = model.cal_loss(pred, y)  # compute loss
            mse_loss.backward()                 # compute gradient (backpropagation)
            optimizer.step()                    # update model with optimizer
            loss_record['train'].append(mse_loss.detach().cpu().item())

        # After each epoch, test your model on the validation (development) set.
        dev_mse = dev(dv_set, model, device)
        if dev_mse < min_mse:
            # Save model if your model improved
            min_mse = dev_mse
            print('Saving model (epoch = {:4d}, loss = {:.4f})'
                .format(epoch + 1, min_mse))
            torch.save(model.state_dict(), config['save_path'])  # Save model to specified path
            early_stop_cnt = 0
        else:
            early_stop_cnt += 1

        epoch += 1
        loss_record['dev'].append(dev_mse)
        if early_stop_cnt > config['early_stop']:
            # Stop training if your model stops improving for "config['early_stop']" epochs.
            break

    print('Finished training after {} epochs'.format(epoch))
    return min_mse, loss_record

1. 训练前的准备工作

n_epochs = config['n_epochs'] # 学习轮数
# --- 定义优化器 ---
optimizer = getattr(torch.optim, config['optimizer'])(
    model.parameters(), **config['optim_hparas'])
# --- 状态跟踪变量 ---
min_mse = 1000.
loss_record = {'train': [], 'dev': []}
early_stop_cnt = 0
epoch = 0
  • n_epoches:从配置中读取“训练轮数”,也就是我们打算让模型吧整个训练数据集重复学习多少遍
  • optimizer:优化器
    • 简单说,就是一种优化梯度下降的方法,由于有的损失函数的梯度不是很容易降下来,我们可以利用各种优化算法。比如 SGD (随机梯度下降), Adam 等。这里的代码 getattr(torch.optim, config['optimizer']) 是一种非常灵活的写法,可以根据我们在config字典里设置的字符串(比如 'SGD''Adam')来自动选择并创建对应的优化器
    • model.parameters(): 这是nn.Module带来的便利之一,整个方法会自动返回模型中所有需要学习的参数,也就是所有nn.Linear层的权重和偏置,我们把这些参数交给优化器,优化器就知道去更新谁了
  • min_mse, loss_record, early_step_cnt: 用于记录和监控训练过程的变量
    • min_mse: 记录验证集上出现过的最小均方误差,用来判断是否要保存模型
    • loss_record: 记录每一轮训练和验证的损失值,方便后续画出“学习曲线”
    • early_stop_cnt: 用于提前停止,用来防止模型在没有进步的情况下继续浪费时间训练

2. 主学习循环

while epoch < n_epochs:
    # ... 一轮学习的完整过程 ...
    epoch += 1
  • Epoch : 所有batch跑一遍,也就是把整个训练数据集从头到尾学习一遍,也就是while循环执行一次,就是一个Epoch(轮)

3. 批次学习循环

在每一个Epoch学习中,模型会一小批一小批地(mini-batch)看数据

model.train() # 1. 切换到训练模式
    for x, y in tr_set: # tr_set 就是我们的 DataLoader
        # --- 核心训练五步法 ---
        optimizer.zero_grad()               # 2. 梯度归零
        x, y = x.to(device), y.to(device)   # 3. 数据上膛(放到GPU)
        pred = model(x)                     # 4. 前向传播(做预测)
        mse_loss = model.cal_loss(pred, y)  # 5. 计算损失
        mse_loss.backward()                 # 6. 反向传播(计算梯度)
        optimizer.step()                    # 7. 更新权重
        
        loss_record['train'].append(mse_loss.detach().cpu().item())
  • model_train(): 这是nn.Module的一个重要方法。这会启用一些只有在训练时才使用的功能(比如Dropout),与之对应的是model.eval(),在验证集和测试时使用
  • for x, y in tr_set: tr_set 是我们的 DataLoader。这个循环会不断地从 DataLoader 中取出打包好的小批次数据,x 是特征,y 是标签。

接下来的几步是 PyTorch 中最经典、最核心的训练流程, 几乎所有的训练代码都遵循这个模式,甚至可以当成模板


  • optimizer.zero_grad() 梯度归零
    • PyTorch会默认累积梯度,因此,在计算新一批数据的梯度之前,必须手动清空上一批的梯度。否则,梯度会越加越大,导致错误的更新
  • x, y = x.to(device), y.to(device):
    • 将这一批数据和标签都移动到之前设定的设备(GPU或者CPU)上
  • pred = model(x)
    • 把数据 x 喂给模型,模型会调用自己的 forward 方法,得出一个预测结果 pred
  • mes_loss = model.cal_loss(pred, y) 计算损失
    • 调用我们之前定义的 cal_loss 方法,用损失函数(MSE)比较预测值 pred 和真实值 y,得到一个代表“差距”的数值 mse_loss
  • mes_loss.backward() 反向传播
    • autograd:这是PyTorch的“自动求导引擎”,调用.backward()后,PyTorch会自动计算出mes_loss相对于模型中每一个需要学习的参数的梯度(偏导数),这个梯度指明了参数应该朝哪个方向调整才能让损失变小
  • optimizer.step() 更新权重
    • 优化器optimizer会根据上一步计算出的梯度,使用它自己的更新规则(比如 SGDAdam算法)来微调模型中所有的参数 (model.parameters()。这一步是模型真正在“学习”和“成长”的时刻。


4. 验证和保存

在一轮(Epoch)的所有批次都学习完后,我们需要对模型进行一次验证,看看它学得怎么样。

dev_mse = dev(dv_set, model, device) # 在验证集上考试
    if dev_mse < min_mse:
        # 如果这次考试成绩是历史最好成绩...
        min_mse = dev_mse
        print('Saving model ...')
        torch.save(model.state_dict(), config['save_path']) # 保存模型
        early_stop_cnt = 0 # 重置早停计数器
    else:
        early_stop_cnt += 1 # 否则,没进步的次数+1
  • dev(dv_set, model, device): 调用 dev 函数在验证集上进行评估,得到一个均方误差 dev_mse

  • if dev_mse < min_mse: 如果这次的验证误差比历史最低误差 min_mse 还要低,说明模型取得了进步。

  • torch.save(model.state_dict(), ...): 这是保存模型的标准方法。model.state_dict() 会返回一个包含模型所有学习到的参数(权重和偏置)的字典。torch.save 将这个字典保存到文件中,这样我们以后就可以随时加载这个表现最好的模型了。


5. 早停机制

if early_stop_cnt > config['early_stop']:
        # 如果连续很多轮考试成绩都没进步...
        break # ...就提前结束训练

这个机制可以防止在模型性能不再提升时继续浪费时间训练,同时也能有效避免过拟合。


小结

train 函数模拟了学习过程:它让模型在一轮轮(epoch)的学习中,一小批一小批(batch)地看数据,并通过 前向传播 -> 计算损失 -> 反向传播 -> 更新权重 的核心流程来不断优化自己。同时,它还通过验证集来监控学习效果,只保存最好的模型,并在必要时提前终止训练。

5.2 Validation

def dev(dv_set, model, device):
    model.eval()                                # 1. 切换到评估模式
    total_loss = 0
    for x, y in dv_set:                         # 2. 遍历验证数据
        x, y = x.to(device), y.to(device)
        with torch.no_grad():                   # 3. 关闭梯度计算
            pred = model(x)                     # 4. 做预测
            mse_loss = model.cal_loss(pred, y)  # 5. 计算损失
        total_loss += mse_loss.detach().cpu().item() * len(x)
    total_loss = total_loss / len(dv_set.dataset)
    return total_loss

def dev(dv_set, model, device) 模拟考试

在每一个epoch结束后,用验证集dv_set来评估一下模型当前的表现,得到一个客观的分数

5.3 Testing

def test(tt_set, model, device):
    model.eval()                                # 同样切换到评估模式
    preds = []
    for x in tt_set:                            # 1. 遍历测试数据 (注意这里只有 x)
        x = x.to(device)
        with torch.no_grad():                   # 同样关闭梯度计算
            pred = model(x)                     # 2. 做预测
            preds.append(pred.detach().cpu())   # 3. 收集预测结果
    preds = torch.cat(preds, dim=0).numpy()     # 4. 拼接并转换为 NumPy 数组
    return preds

def test(tt_set, model, device) 高考

这个函数在所有训练都完成之后才被调用。它使用我们保存下来的表现最好的模型,对从未见过的测试集 tt_set 进行预测,并生成最终的结果文件。

注意
流程与 dev 的异同

  • 相同点: 同样需要 model.eval()with torch.no_grad(),因为这也是一个评估过程。

  • 不同点:

    • 输入: 遍历测试集 tt_set 时,DataLoader 只返回 x,因为测试集没有提供标签 y

    • 目的: test 函数的目的不是计算损失,而是收集所有预测结果 pred

    • 输出: dev 返回一个数字(损失值),而 test 返回一个包含所有预测值的数组。

  1. preds.append(pred.detach().cpu())
    在循环中,我们将每一批的预测结果(一个Tensor)从GPU上分离并移到CPU,然后存入一个Python列表preds

  2. preds = torch.cat(preds, dim = 0).numpy()
    这个是循环结束后的一个重要处理步骤

    • torch.cat(preds, dim=0): preds 现在是一个 Tensor 列表 [tensor_batch1, tensor_batch2, ...]torch.cat 函数的作用是将这个列表中的所有 Tensor 拼接成一个大的、完整的 Tensordim=0 指定了沿着第0个维度(也就是行)进行拼接。

    • .numpy(): 将最终的 PyTorch Tensor 转换成 NumPy 数组。这样做是为了方便后续的处理,比如保存成 CSV 文件,或者使用其他非 PyTorch 的库(如 Scikit-learn)进行分析

小结

阶段 训练 (Training) 验证 (Validation) 测试 (Testing)
目的 学习知识,更新权重 检查学习效果,调整策略 最终评估模型性能
模式 model.train() model.eval() model.eval()
梯度 需要 (.backward()) 不需要 (torch.no_grad) 不需要 (torch.no_grad)
数据 特征 x 和标签 y 特征 x 和标签 y 只有特征 x
输出 无(只更新模型内部参数) 损失值(一个数字) 所有预测结果(一个数组)


考虑优化

好了,这样整个流程就基本结束了,我们就可以考虑去优化我们的神经网络了,这里作业给的源码其实是可以直接跑的,但是因为神经网络设计得比较简单所以最后的结果会比较差

说明模型的准确率不够,也就是说,模型的学习能力比较弱,可能有这几点原因

  • 数据量不够大
  • 特征太少
  • 结构太简单
  • 梯度降不下去,损失函数的值一直很大

作业中的数据集是给定的,我们没法改了
我们可以从剩下的方面考虑

--------------------------------------------Waiting for update----------------------------------------------------------

posted @ 2025-10-12 17:55  栗悟饭与龟功気波  阅读(7)  评论(0)    收藏  举报