模型部分补充

    def forward(self, x):
        x = self.fc1(x)  # fully connected 1 全连接
        x = self.relu1(x)
        x = self.fc2(x)

        if len(x.size()) > 1:
            x = x.squeeze(1)

        return x

由于x是二维的张量,其在经过神经网络的计算后,所得到的预测值也会是一个二维的张量。

已知batchsize=16,dataloader每次会取出16个样本,每个样本具有93个特征,因而可以认为输入的x是一个16×93的二维张量,此时x.size()返回 [ 16, 93 ]

第一层全连接层是93×128的,即对应于样本的93个特征,将它们分别送入128个神经元,每个神经元有对应的93个参数w和一个偏置b,可以认为是将16×93矩阵与93×128的矩阵相乘,得到了16×128的新矩阵,此时x.size()返回 [ 16, 128 ]

第二层全连接层同上,该层是输出层,最后会得到一个16×1的二维张量,x.size()返回 [ 16, 1 ]

可见,此时得到的预测值仍然是一个二维张量,形如[ [ 1 ], [ 2 ], [ 3 ], ......]

但是,dataloader中取出的y却是一个一维张量(向量),形如[ 1, 2, 3, 4, ........],二者有本质上的区别

从前面的简单回归中已经知道,在计算loss时必须传入y_pred和y进行计算,那么这二者的维度就必须相同,二维张量和一维张量当然是不能一同运算的。

于是在前向传播的最后,要加上一句把输出的维度压缩成一维的代码。

pytorch描述张量形状的方式是通过一个一维数组来描述的,数组的长度就是张量维度的数量,数组中第 i 个元素代表这个维度的大小,也就是在第 i 维上 有几项数据 

.size()方法就可以返回这个数组。如[ 16 ]代表这个张量是一个一维张量,含有16项数据,即一个 16维向量 (线代中对向量的定义);[ 16, 93 ]就代表这是一个二维张量,即一个16×93的矩阵,在本项目中我认为它反映了一组数据中含有16个样本,每个样本具有93个特征

于是可以知道,len(x.size())说的就是张量的维度,当维度超过1,就将其压缩

.squeeze()方法可以移除所有大小为1的维度,若传入了参数,则可以只移除指定维度(如果大小为1)

所以在这一步之后,形如[ [ 1 ], [ 2 ], [ 3 ], ......]的二维张量y就可以变成形如[ 1, 2, 3, 4, ........]的一维张量,就可以参与loss的计算了

优化器:

使用随机梯度下降进行参数优化,动量系数设置为通常值0.9

optimizer = optim.SGD(params=model.parameters(), lr=lr, momentum=0.9)
# 超参数:momentum,动量(实际上是动量系数,控制历史梯度信息的衰减速度)

快速掌握Momentum - 知乎

momentum动量,可以用于优化梯度下降算法,原理是模拟真实世界中的下滑动作,使小球具有动量,使其具备冲过局部最低点的可能性,同时也使得小球不会在最低点附近抖动,可以加速收敛。

Momentum 的更新过程

以下是一个具体的更新过程示例:

  1. 初始化参数 和动量项 。
  2. 对于每次迭代t
  • 计算当前梯度 。
  • 更新动量项:

更新参数:

重复上述步骤,直到收敛。

到这里为止,一个神经网络项目所应包含的四个部分中的前三个已经解决(Data、Model、HyperPara),最后还要进行训练流程的实现。

训练流程

函数定义和初始化

def train_val(model, train_loader, val_loader, lr, optimizer, device, epochs, save_path):
    model = model.to(device)  # 将模型移到指定设备(GPU/CPU)
    start_time = time.time()
    plt_train_loss = []  # 存储每个epoch的平均训练loss
    plt_val_loss = []    # 存储每个epoch的平均验证loss
    min_val_loss = 9999999.9  # 初始化最小验证loss

.to(device)方法

可以把数据或模型移动到指定的设备,对于模型来说也就是将模型的参数、缓冲区等移动到设备

CPU内存 <-------> GPU内存
    ↑                 ↑
CPU计算           GPU计算(并行)

# 数据必须先在GPU内存中,GPU才能计算
# 模型参数必须在GPU内存中,GPU才能使用它们

外层Epoch循环+内层Batch训练+计算平均训练损失

for epoch in range(epochs):  # 遍历所有epoch
    model.train()            # 将模型设置为训练模式
    train_loss = 0.0         # 重置当前epoch的训练loss

    for x, y in train_loader:  # 遍历所有batch,每次取一个batchsize(16)个样本
        x, y = x.to(device), y.to(device)  # 数据移到设备
        y_pred = model(x)       # 前向传播
        bat_loss = loss(y_pred, y)  # 计算损失
        
        bat_loss.backward()     # 反向传播,计算梯度
        optimizer.step()        # 更新模型参数
        optimizer.zero_grad()   # 清空梯度
    
        train_loss += bat_loss.cpu().item()  # 累加batch loss

    plt_train_loss.append(train_loss / train_loader.__len__())
    # train_loader.__len__() 返回的是批次数(iterations),不是batch_size
    # 也就是训练时一个epoch的迭代次数 = 总样本数 / batch_size

optimizer.step()

由于是基于梯度下降算法,因此:

# optimizer.step() 内部实际上执行类似这样的操作:
with torch.no_grad():  # 禁用梯度计算
    for param in model.parameters():
        if param.grad is not None:
            # 更新参数:param = param - lr * param.grad
            param -= learning_rate * param.grad

基本上跟前面那个简单回归代码做的是一样的事情

plt_train_loss.append(train_loss / train_loader.__len__())

计算当前这一个epoch的平均训练损失,并将其添加到列表中以作记录

验证阶段

    model.eval()  # 将模型设置为评估模式
    val_loss = 0.0

    with torch.no_grad():  # 禁用梯度计算,节省内存
        for val_x, val_y in val_loader:
            val_x, val_y = val_x.to(device), val_y.to(device)
            val_y_pred = model(val_x)
            val_bat_loss = loss(val_y_pred, val_y)
            val_loss += val_bat_loss.cpu().item()
    plt_val_loss.append(val_loss / val_loader.__len__())

torch.no_grad() 核心功能:禁用梯度计算

# no_grad模式:不记录计算图
x2 = torch.tensor([1.0], requires_grad=True)
with torch.no_grad():
    y2 = x2 * 2  # 这个计算不会被跟踪
    z2 = y2.mean()
    z2.backward()  # 这里会报错!

no_grad模式下,计算图不会被记录,因而无法通过.backward()反向传播计算梯度,会报错

使用with语句可以方便的划定哪一片的代码运行在no_grad模式下,就像打开文件那样自动调用__enter__和自动退出

验证阶段:只需要前向传播计算损失/准确率,不需要反向传播计算梯度,不需要更新模型参数

若不调用,则可能导致:1、内存泄漏;2、计算减慢;3、可能意外修改梯度

模型保存

    if val_loss < min_val_loss:
        min_val_loss = val_loss
        torch.save(model, save_path)  # 保存最佳模型

取val_loss最小的一次保存模型,此时的模型就是训练中得到的最佳模型,用于后续的提交测试

进度输出+可视化损失曲线

    print("[%03d/%03d] %2.2f sec(s)  train_loss: %.6f val_loss: %.6f " % \
          (epoch+1, epochs, time.time()-start_time, 
           plt_train_loss[-1], plt_val_loss[-1]))
    plt.plot(plt_train_loss)
    plt.plot(plt_val_loss)
    plt.title("loss")
    plt.legend(["train", "val"])
    plt.show()

测试阶段

加载训练好的PyTorch模型,对测试数据进行预测,并将结果保存为CSV文件。

模型加载与初始化

def evaluate(model_path, test_loader, rel_path, device):
    model = torch.load(model_path, weights_only=False).to(device)

    rel = []  # rel = result
    model.eval()

torch.load()

作用:从指定路径加载已保存的PyTorch模型

model_path:模型文件的存储路径

weights_only=False:加载整个模型(包括架构和参数),而不是仅加载权重

.to(device):将模型移动到指定设备(CPU或GPU)

model.eval()

作用:将模型切换到评估模式,这会关闭Dropout、BatchNorm等训练时的特殊层,确保推理结果稳定

具体来说:

1、禁用训练特有的随机行为:

        Dropout层:不丢弃神经元,所有神经元都参与计算

        BatchNorm层:使用训练时累积的运行统计量(均值和方差),而不是当前批次的统计量

        其他层:可能有的层也会改变行为

2、通常与torch.no_grad()一起使用:

        model.eval():改变模型内部层的行为

        torch.no_grad():不计算梯度,节省内存

与之相对的,model.train()就是将模型切换到训练模式,并开启一些在训练时会用到的功能或者随机行为

对于没有Dropout/BatchNorm的模型,即使模型没有这些层,也应该使用model.eval(),这是因为考虑到:

1、保持代码一致性;2、某些自定义层可能也有不同行为;3、未来添加这些层时无需修改代码

批量推理循环

    with torch.no_grad():
        for x in test_loader:
            x = x.to(device)          # 将数据移动到设备
            pred = model(x)           # 前向传播得到预测
            rel.append(pred.cpu().item())  # 将结果移回CPU并添加到列表

结果保存到CSV文件

    with open(rel_path, "w", newline="") as f:
        csv_writer = csv.writer(f)  # 创建CSV写入器
        csv_writer.writerow(["id", "tested_positive"])  # 写入表头
        for i, pred in enumerate(rel):  # 遍历所有结果
            csv_writer.writerow([str(i), str(pred)])  # 写入每一行
        print("结果保存到了"+rel_path)  # 输出保存成功信息
  • with open(...):打开文件,确保文件正确关闭

  • csv_writer = csv.writer(f):创建CSV写入对象

  • writerow(["id", "tested_positive"]):写入列标题

  • enumerate(rel):同时获取索引 i 和预测值 pred

  • writerow([str(i), str(pred)]):将索引和预测值写入一行

  • print(...):提示用户结果保存位置

更多推荐