李宏毅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 列表一次性地从 data 和 target 中取出所有对应的行,完成数据集的切分。
数据标准化
-
切片
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
这个函数是一个"包装"函数,它做了两件事情:
-
创建
Dataset对象:就是利用我们之前定义的COVID19Dataset类创建了一个数据集实例 -
创建
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')
-
super(NeurlaNet, self).__init__()
这是一句必须在最开始调用的代码,它会运行父类nn.Module的初始化逻辑,确保我们的模型工坊能正常运作 -
self.net = nn.Sequential()
nn.Sequential是一个非常有用的“容器”或者“流水线管道”,我们可以吧一系列乐高积木块(层)按顺序放进去,当数据从管道一头进去时,会自动地、依次地通过所有积木块,最后从另一头出来,这让我们的forward方法可以写得很简洁 -
流水线里的“积木块”
nn.Linear(input_dim, 64):全连接层,这个应该都知道了,毕竟接触到第一个神经网络就是这个,它对输入数据进行一次线性变换(y = Wx + b),可以理解为将输入的特征进行加权、混合,然后提炼出新的特征nn.Linear内部已经包含了需要学习的权重矩阵W和偏置向量b,并且PyTorch会自动对它们进行随机初始化input_dim:输入特征数量。比如,我们的数据有93个特征,这里就是9364:输入神经元的数量,这代表我们希望这个层提炼出64个新的特征,这是一个可以自己调整的超参数
nn.ReLU():激活函数- 给网络引入非线性,如果没有非线性层,那么无论我们堆叠多少个
nn.Linear层,整个网络本质上还是线性的,理论上一个线性层就替代了,(那么堆叠多层纯纯小丑),学习能量非常有限,非线性激活函数能增加模型复杂度,提高模型的弹性 nn.Linear(64, 1):输出层64: 它的输入数量必须和上一层的输出数量保持一致,这样才能衔接起来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\)如何工作?
-
创建模型: 当我们写
model = NeuralNet(input_dim=93)时,__init__方法被调用,模型的所有“积木块”(层)都被创建并准备好。 -
进行预测: 在训练循环中,当我们写
prediction = model(data)时,PyTorch会自动调用forward(data)方法。数据会按照我们定义的路径流过整个网络,最终得到预测结果。 -
计算误差: 接着,我们调用
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会根据上一步计算出的梯度,使用它自己的更新规则(比如SGD或Adam算法)来微调模型中所有的参数(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返回一个包含所有预测值的数组。
-
-
preds.append(pred.detach().cpu())
在循环中,我们将每一批的预测结果(一个Tensor)从GPU上分离并移到CPU,然后存入一个Python列表preds中 -
preds = torch.cat(preds, dim = 0).numpy()
这个是循环结束后的一个重要处理步骤-
torch.cat(preds, dim=0):preds现在是一个Tensor列表[tensor_batch1, tensor_batch2, ...]。torch.cat函数的作用是将这个列表中的所有Tensor拼接成一个大的、完整的Tensor。dim=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----------------------------------------------------------

浙公网安备 33010602011771号