2. 神经网络启蒙:PyTorch 入门实战#

本章我们将正式迈入神经网络的大门,从零开始构建并训练你的第一个神经网络模型。通过本次任务,你将会了解神经网络模型的基本结构、训练流程,并掌握如何使用深度学习框架 PyTorch 快速搭建并进行训练一个多参数回归模型。

2.1. 任务背景#

你是一名程序员。自从上次与小美合作,成功教会机器人制作柠檬水后,你的无人柠檬水摊位生意渐火。看着账户里稳定增长的收入,一个更大胆的想法浮现出来:如果只卖经典原味柠檬水,市场天花板显而易见;但如果能让机器人学会开发多种口味的特调柠檬水,必将大大提升产品的吸引力和营收潜力。

你尝试在基础柠檬水中加入薄荷叶、百香果、黄瓜片或少许玫瑰盐,以创造不同的风味。薄荷带来清凉感,黄瓜增加清新的后韵,而一点点玫瑰盐则能神奇地提升整体的层次感——但这些配料之间会相互影响,如何平衡它们才是真正的艺术。

你不断尝试各种配比组合,邀请顾客品尝并记录评分,逐步优化配方。然而,这种试错过程漫长且成本高昂。于是你想:如果能训练一个模型,自动根据配料配比预测顾客评分,问题便可迎刃而解。

2.2. 最少必要知识#

  • 神经网络概论

  • PyTorch 基础

2.3. 任务鸟瞰#

本次的任务是:训练一个模型,根据薄荷叶、百香果、黄瓜片和玫瑰盐的配比,预测顾客对柠檬水的评分。

image.png

预测一杯柠檬水的口感评分,看似简单,实则受到多种参数的复杂影响。薄荷叶的多少、黄瓜片的厚薄、玫瑰盐的用量……这些细微变量都在悄然塑造着最终的口感体验。面对如此纷繁的因素,我们难以通过手动方式去管理每一个参数,因为很难精确描述它们与口感评分之间的复杂关系。

此时,不妨观察人类如何对柠檬水进行评分,并尝试从生物神经元的机制中汲取灵感。

2.3.1. 神经网络#

在大脑中,神经元是信息处理的基本单元。一个神经元会接收外界的刺激信号,神经突触根据输入信号的重要性进行加权处理;当加权后的信号总和超过某个阈值时,神经元被激活,产生输出信号。

神经元工作过程可分为两步:

  • 加权求和:对多个输入信号进行加权融合;

  • 点火激活:若加权和超过阈值,则触发输出。

当我们品尝柠檬水并给出评分时,大脑中的神经元正在进行类似处理。味觉、嗅觉等感官接收到的特征信息(薄荷的清香、黄瓜的清爽、玫瑰盐的风味等)被传递至相关神经元,经过加权整合,最终形成对口感的综合评判,即评分。

我们可以将上述过程抽象为人工神经元模型。单个神经元的计算可表示为:输出 = 激活函数(Σ(权重 × 输入) + 偏置)

计算的过程如下图所示:

image.png

其中:

  • \(x\):输入信号

  • \(w\):权重

  • \(b\):偏置(可视为阈值)

  • \(∑\):加权求和

  • \(σ\):激活函数

  • \(y\):输出信号

更进一步,我们可以将神经元结构表示为数学公式: \(y = σ(w_1x_1 + w_2x_2 + w_3x_3 + ... + w_nx_n + b)\)

2.3.2. 模型结构#

由大量神经元相互连接形成的系统称为神经网络(Neural Network)。本次任务我们使用神经网络,构建一个用于预测柠檬水评分的模型,学习三种配料与评分之间的关系。其模型结构如下:

image.png

本次的任务也是一个简单的多参数回归模型,所以我们只它使用一个神经网络层构建模型即可,这个神经网络层有3个输入和1个输出。

开发神经网络模型和机器学习模型一样,通常也遵循以下四个步骤:数据准备、模型定义、模型训练与模型评估。下文将依此流程组织内容。在开始之前,我们先配置环境,避免因为环境不同而导致程序不能复现。

2.4. 环境配置#

2.4.1. 安装依赖#

!pip install --upgrade dsxllm -i https://pypi.org/simple

2.4.2. 环境版本#

from dsxllm.util import show_version

show_version()
本书愿景:
+------+--------------------------------------------------------+
| Info |                  《动手学大语言模型》                  |
+------+--------------------------------------------------------+
| 作者 |                       吾辈亦有感                       |
| 哔站 |      https://space.bilibili.com/3546632320715420       |
| 定位 | 基于'从零构建'的理念,用实战帮助程序员快速入门大模型。 |
| 愿景 | 若让你的AI学习之路走的更容易一点,我将倍感荣幸!祝好😄 |
+------+--------------------------------------------------------+
环境信息:
+-------------+--------------+------------------------+
| Python 版本 | PyTorch 版本 | PyTorch Lightning 版本 |
+-------------+--------------+------------------------+
|   3.12.12   |    2.10.0    |         2.6.1          |
+-------------+--------------+------------------------+

2.5. 数据准备#

2.5.1. 数据集下载#

2.5.2. 观察数据#

加载并预览数据集前5行,了解数据结构。

# 读取数据
import pandas as pd

# 读取训练集数据
train_data_frame = pd.read_csv('./dataset/craft_lemonade_train.csv')
train_data_frame[:5]
薄荷叶 黄瓜片 玫瑰盐 顾客评分
0 9 10 6 8.60
1 10 9 7 8.90
2 5 3 7 4.80
3 3 4 6 4.10
4 4 9 2 5.25

从输出结果可以看到数据包含四个字段:薄荷叶、黄瓜片、玫瑰盐(输入特征)和 顾客评分(目标值)。我们需要将每一条训练数据转化成 <输入特征, 目标值> 数据对的形式。

2.5.3. 定义数据加载方法#

根据文件路径加载 csv 数据,分离出输入特征与对应的目标值标签。

def load_data(file_path):
    # 读取 csv 数据
    data_frame = pd.read_csv(file_path)

    # 将DataFrame数据转换为列表格式
    features = data_frame[['薄荷叶', '黄瓜片', '玫瑰盐']].values.tolist()
    target_scores = data_frame[['顾客评分']].values.tolist()

    return features, target_scores

2.5.4. 加载训练集和评估集#

加载训练数据和评估数据,并打印训练集和评估集的样本数量,确认数据加载成功。

train_features, train_targets = load_data('./dataset/craft_lemonade_train.csv')
val_features, val_targets = load_data('./dataset/craft_lemonade_val.csv')

print(f"训练数据集样本数量:{len(train_features)}")
print(f"评估数据集样本数量:{len(val_features)}")
训练数据集样本数量:800
评估数据集样本数量:200

2.6. 模型定义#

2.6.1. PyTorch 核心概念#

要构建并训练一个神经网络模型,离不开强大的深度学习框架。PyTorch 以其灵活性、Python 友好的设计而广受欢迎,它提供了直观的接口来构建和调试复杂神经网络,是目前入门人工智能的主流选择之一。

为了顺利搭建柠檬水评分预测模型,我们需要了解以下 PyTorch 的核心概念:

概念

说明

张量(Tensor)

PyTorch 中数据的基本单元,配料配比、评分等参数都需转换为张量。

网络层(Layer)

模型处理数据的基本模块,需明确其输入输出维度。

模型(Model)

多个网络层的有序堆叠,形成完整的神经网络模型。

2.6.2. 柠檬水评分预测模型的模型结构#

本次任务我们使用 PyTorch 训练一个预测 3 种配料与顾客评分之间关系的神经网络模型。

回顾一下模型结构:

image.png

2.6.3. 柠檬水评分预测模型的代码实现#

定义一个继承自 nn.Module 的柠檬水评分模型类 LemonadeRatingModel,这是 PyTorch 中定义神经网络模型的标准方式。

import torch
import torch.nn as nn


# 定义柠檬水评分预测模型,继承自 nn.Module,这是PyTorch中定义神经网络的标准方式
class LemonadeRatingModel(nn.Module):
    def __init__(self):
        super(LemonadeRatingModel, self).__init__()
        # 创建一个线性层:接受3个输入特征(薄荷叶、黄瓜片、玫瑰盐)并输出1个值(顾客评分)
        self.rating_predictor_layer = nn.Linear(in_features=3, out_features=1, bias=False)

    def forward(self, input_features):
        # 前向计算,定义数据在网络中的流动方式,预测顾客评分,forward 方法会被自动调用
        predicted_rating = self.rating_predictor_layer(input_features)
        return predicted_rating

自定义模型时, 需要重写模型的__init__forward方法,分别完成模型的初始化和前向计算。

  • __init__():定义模型的参数和层结构,创建一个拥有 3 个输入特征,1 个输出特征的线性层(Linear)。

  • forward():定义数据在网络中的流动方式,通过前向计算根据输入特征预测顾客评分。

线性层

在 PyTorch 中,线性层(Linear Layer)是最基础、最常用的神经网络层之一,由于它的每一个神经元都接收来自上一层所有神经元的输入信号,并与每个输入都有一个独立的权重进行连接,所以也称它为“全连接层”。

在PyTorch中,线性层由 torch.nn.Linear 类实现。它的主要参数有:

  • in_features:输入特征的数量(即输入的维度)

  • out_features:输出特征的数量(即输出的维度)

  • bias:布尔值,表示是否使用偏置项,默认为True

当创建一个 Linear 实例时,它会自动初始化权重和偏置。

2.6.4. 柠檬水评分预测模型的结构详情#

创建一个模型实例,并打印模型结构,确认模型创建成功。

from dsxllm.util import print_red

# 创建模型实例
model = LemonadeRatingModel()

print_red("模型结构:")
print(model)
模型结构:
LemonadeRatingModel(
  (rating_predictor_layer): Linear(in_features=3, out_features=1, bias=False)
)

从运行结果可以看到,柠檬水评分模型中只有一个预测评分的线性层 rating_predictor_layer,它有 3 个输入(薄荷叶、黄瓜片、玫瑰盐)、1 个输出(顾客评分)。

2.7. 模型训练#

2.7.1. 神经网络的训练#

既然生物神经网络能产生智能,那么仿照其结构构建的人工神经网络也应具备某种“学习能力”。神经网络的“学习”是指从训练数据中自动获取最优权重参数的过程。神经网络学习思路极其简单:计算神经网络得出的预测值与正解的误差,根据误差更新神经网络的权重,最终使得模型的误差总和达到最小。

使用 PyTorch 训练神经网络模型的流程如下:

image.png

训练神经网络模型和机器学习的流程基本一致,但也稍有不同:

  1. 初始化模型:确定模型结构,并对模型参数进行随机初始化。

  2. 前向传播:通过 forward() 将输入数据输入模型,得到预测结果。

  3. 损失计算:使用损失函数 loss_fn() 计算模型预测结果与真实结果之间的误差,评估模型性能。

  4. 梯度计算:由于神经网络的参数成千上万,无法再手动计算每一个参数的更新方式,利用反向传播算法 backward() 自动计算损失对各参数的梯度,指导参数更新方向。

  5. 参数更新:同样由于参数规模的暴增,手动更新参数也不太可能。另外学习率调度也非常复杂,需要借助优化器 optimizer 更新模型参数。

  6. 重复执行 2~5 步,通过持续训练逐步优化模型,最终得到使损失最小化的模型参数。

每完成一次在数据集上的训练循环,我们称之为一个轮次,也就是 epoch。下面我们定义一个训练方法开始训练顾客评分模型。

2.7.2. 定义训练方法#

PyTorch 经典的训练循环如图所示:

image.png

下面定义训练神经网络模型的方法 train_model,指定训练轮次并在每一轮中执行完整的训练循环。同时,需定期记录训练过程中损失值,以便追踪模型训练的进度。

def train_model(model, train_features, train_targets, loss_fn, optimizer, num_epochs=100, log_interval=5):
    """
    训练模型并记录训练日志
    
    参数:
    model: 要训练的模型
    train_features: 训练特征数据张量
    train_targets: 训练标签数据张量
    loss_fn: 损失函数
    optimizer: 优化器
    num_epochs: 训练轮数,默认为100
    log_interval: 日志记录间隔,默认为5
    
    返回:
    training_logs: 包含训练过程记录的列表
    """

    training_logs = []

    for epoch in range(num_epochs):
        # 1. 前向传播:使用模型预测评分
        predictions = model(train_features)

        # 2. 计算损失:使用损失函数计算预测评分与真实分数的差距
        loss = loss_fn(predictions, train_targets)

        # 3. 清空梯度:将模型参数的梯度清空,避免梯度累积
        optimizer.zero_grad() 
        
        # 4. 反向传播,计算梯度
        loss.backward()

        # 5. 使用优化器,更新参数
        optimizer.step() 


        # 记录训练日志
        if (epoch + 1) % log_interval == 0:
            # 记录模型参数
            with torch.no_grad():
                for param_name, param in model.named_parameters():
                    if param.requires_grad:  # 筛选需要计算计算梯度进行更新的参数
                        weights_list = param.data.numpy()[0].tolist()

                        # 获取模型权重
                        mint_leaf_weight, cucumber_slice_weight, rose_salt_weight = weights_list[0], weights_list[1], weights_list[2]

                        # 记录训练日志
                        training_logs.append({
                            "Epoch": epoch + 1,
                            "Loss": loss.item(),
                            "薄荷叶权重": mint_leaf_weight,
                            "黄瓜片权重": cucumber_slice_weight,
                            "玫瑰盐权重": rose_salt_weight
                        })

    return training_logs

在神经网络模型的训练循环中,每个训练循环通常包含以下核心操作:

  • 前向传播:通过 model() 自动执行前向传播计算,模型会基于输入特征(如薄荷叶、黄瓜片、玫瑰盐的用量)计算预测输出(例如顾客评分)。

  • 损失计算:调用 loss_fn() 利用损失函数量化模型预测值与真实标签之间的差异,反映当前预测的误差大小。

  • 清空历史梯度:执行 optimizer.zero_grad() 将优化器中之前累积的梯度清零,以避免梯度累加影响本轮参数更新。

  • 反向传播:通过 loss.backward() 系统会自动计算损失对模型各参数的梯度,从而确定参数调整的方向与幅度。

  • 参数更新:使用 optimizer.step() 优化器将根据计算得到的梯度更新模型参数,使模型预测逐步向真实值靠近。

这些步骤在训练过程中循环执行,直至训练结束。

2.7.3. 训练模型#

定义好了训练方法后,可以开始训练模型了。训练模型需要以下步骤:

  1. 超参数配置:定义训练轮数、日志记录间隔、学习率等超参数

  2. 准备训练数据:将训练的输入数据和标签数据转换为张量

  3. 初始化模型训练组件:创建模型实例、损失函数和优化器

    • 损失函数:因为此任务为回归任务,使用均方误差损失函数(MSE)

    • 优化器:使用随机梯度下降(SGD)优化器优化模型参数,学习率为 0.001

  4. 执行训练循环:调用 train_model 函数进行 500 轮训练,每 20 轮记录一次训练日志。

损失函数:均方误差(MSE)

MSE 是 均方误差 的缩写,英文全称是 Mean Squared Error。它是评估预测模型(如回归模型)精度最常用、最重要的指标之一。MSE 衡量的是模型预测值与真实值之间差异(即误差)的平方的平均值。

简单来说:

  • 先算出每一个预测值和对应的真实值差多少。

  • 把这个差值平方(目的是消除正负号的影响,并放大较大误差)。

  • 把所有样本的“平方误差”求平均。

你可以把 MSE 理解为“平均平方误差”。数值越小,说明模型的预测值(拟合值)与真实值越接近,模型越好。

优化器:随机梯度下降(SGD)

SGD 是 随机梯度下降 的缩写,英文全称是 Stochastic Gradient Descent。它是一种用于优化机器学习模型参数的核心算法,目的是找到使模型损失函数最小化的参数值。

我们打一个比方,可以帮助你更好的理解 SGD 的工作原理:

有一个性情古怪的探险家。他在广袤的干旱地带旅行,坚持寻找幽深的山谷。他的目标是要到达最深的谷底(他称之为“至深之地”)。这也是他旅行的目的。并且,他给自己制定了两个严格的“规定”:一个是不看地图;另一个是把眼睛蒙上。因此,他并不知道最深的谷底在这个广袤的大地的何处,而且什么也看不见。在这么严苛的条件下,这位探险家如何前往“至深之地”呢?他要如何迈步,才能迅速找到“至深之地”呢?

寻找最优参数时,我们所处的状况和这位探险家一样,是一个漆黑的世界。我们必须在没有地图、不能睁眼的情况下,在广袤、复杂的地形中寻找“至深之地”。大家可以想象这是一个多么难的问题。

在这么困难的状况下,地面的坡度显得尤为重要。探险家虽然看不到周围的情况,但是能够知道当前所在位置的坡度(通过脚底感受地面的倾斜状况)。于是,朝着当前所在位置的坡度最大的方向前进,就是SGD的策略。勇敢的探险家心里可能想着只要重复这一策略,总有一天可以到达“至深之地”。

from dsxllm.util import to_dataframe

# 1️⃣ 超参数配置
num_epochs=500
log_interval=20
learning_rate = 0.001

# 2️⃣ 准备训练数据:将原始特征和目标值数组转换为 PyTorch 张量格式,指定数据类型为float32
train_features_tensor = torch.tensor(train_features, dtype=torch.float32)
train_targets_tensor = torch.tensor(train_targets, dtype=torch.float32)

# 3️⃣ 初始化模型训练组件:创建模型实例、损失函数和优化器
model = LemonadeRatingModel()  # 创建模型实例
loss_fn = nn.MSELoss()  # 使用均方误差损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)  # 使用随机梯度下降优化器

# 4️⃣ 执行训练循环:调用训练方法进行500轮训练
training_logs = train_model(model, train_features_tensor, train_targets_tensor, loss_fn, optimizer, num_epochs=num_epochs, log_interval=log_interval)

# 5️⃣ 训练过程监控:以表格形式展示训练过程中的关键指标变化
print_red("模型训练日志:")
df = to_dataframe(training_logs)
display(df)
模型训练日志:
Epoch Loss 薄荷叶权重 黄瓜片权重 玫瑰盐权重
0 20 1.8945 0.1887 0.7240 0.0711
1 40 0.9545 0.2538 0.6208 0.1263
2 60 0.4852 0.2960 0.5431 0.1616
3 80 0.2466 0.3260 0.4877 0.1868
4 100 0.1254 0.3474 0.4482 0.2048
5 120 0.0637 0.3626 0.4200 0.2177
6 140 0.0324 0.3734 0.3999 0.2269
7 160 0.0165 0.3811 0.3856 0.2335
8 180 0.0084 0.3865 0.3754 0.2382
9 200 0.0043 0.3904 0.3681 0.2415
10 220 0.0022 0.3932 0.3629 0.2440
11 240 0.0011 0.3952 0.3592 0.2457
12 260 0.0006 0.3966 0.3566 0.2469
13 280 0.0003 0.3976 0.3547 0.2478
14 300 0.0001 0.3983 0.3533 0.2484
15 320 0.0001 0.3988 0.3524 0.2489
16 340 0.0000 0.3991 0.3517 0.2492
17 360 0.0000 0.3994 0.3512 0.2494
18 380 0.0000 0.3996 0.3509 0.2496
19 400 0.0000 0.3997 0.3506 0.2497
20 420 0.0000 0.3998 0.3504 0.2498
21 440 0.0000 0.3998 0.3503 0.2498
22 460 0.0000 0.3999 0.3502 0.2499
23 480 0.0000 0.3999 0.3502 0.2499
24 500 0.0000 0.3999 0.3501 0.2499

从训练日志中可以看出随着训练的进行,损失值不断下降,逐步降至接近 0,表示模型预测准确性不断提高。

2.7.4. 训练过程可视化#

绘制训练过程中损失值和重要权重的变化曲线,更直观地观察损失值和各参数权重在训练过程中的变化趋势。

from dsxllm.util import plot_training_curves

# 绘制训练日志图像
plot_training_curves(training_logs)
../_images/b0e13a27c0d009c288eb3dbeed622244f1f8b312787f0fde6317d830f42d2ab5.png

从训练的过程中我们可以看到,所有参数在约 200 轮后基本收敛,三个配料权重都趋于稳定,说明模型确实找到了它们的配比与顾客评分之间的关系。而且总体来看,变化的趋势很平滑,说明模型训练过程非常稳定。

2.8. 模型评估#

模型训练完成后,需要评估模型在未见过的新数据上的表现,即评估模型的泛化能力。具体步骤如下:

  1. 评估时,使用 torch.no_grad() 关闭梯度计算,可以在推理过程中不存储中间结果,减少显存占用

  2. 将评估数据转换为 Pytorch 张量

  3. 使用训练好的模型对新的柠檬水进行评分

  4. 计算预测评分和真实评分的偏差和精度

# 模型评估

# 1️⃣ 关闭梯度计算
with torch.no_grad():  

    # 2️⃣ 将输入特征转换为张量格式
    val_features_tensor = torch.tensor(val_features, dtype=torch.float32)  

    # 3️⃣ 使用训练好的模型对柠檬水进行评分
    predicted = model(val_features_tensor).numpy()  

    # 4️⃣ 计算预测评分和真实评分的偏差和精度
    data = []
    for i in range(len(val_features)):
        residual = predicted[i][0] - val_targets[i][0]  # 预测的偏差
        accuracy_ratio = 1 - abs(residual) / val_targets[i][0]  # 预测的精度

        data.append({
            "薄荷叶": val_features[i][0],
            "黄瓜片": val_features[i][1],
            "玫瑰盐": val_features[i][2],
            "真实评分": val_targets[i][0],
            "模型预测": predicted[i][0],
            "模型偏差": f"{residual:.4f}",
            "模型精度": f"{accuracy_ratio * 100:.2f}%"
        })

    df = to_dataframe(data)
    display(df)
薄荷叶 黄瓜片 玫瑰盐 真实评分 模型预测 模型偏差 模型精度
0 1 8 2 3.7000 3.7007 0.0007 99.98%
1 1 2 3 1.8500 1.8500 0.0000 100.00%
2 9 8 3 7.1500 7.1502 0.0002 100.00%
3 1 10 9 6.1500 6.1506 0.0006 99.99%
4 1 3 9 3.7000 3.6998 -0.0002 99.99%
... ... ... ... ... ... ... ...
195 9 1 2 4.4500 4.4495 -0.0005 99.99%
196 10 4 6 6.9000 6.8995 -0.0005 99.99%
197 9 5 6 6.8500 6.8497 -0.0003 100.00%
198 4 7 3 4.8000 4.8004 0.0004 99.99%
199 4 2 9 4.5500 4.5495 -0.0005 99.99%

200 rows × 7 columns

从评估结果中可以看出,模型在预测柠檬水评分时,基本保持了预测的偏差在 ±0.01 的范围内,准确率也基本保持在 99.99% 以上。说明我们的模型成功学习到了薄荷叶、黄瓜片、玫瑰盐与顾客评分之间的关系,并且在验证集上的表现良好,说明训练出的模型具有较好的泛化能力,完美的完成了本次的任务。

2.9. 为什么模型总是训不好?#

虽然我们已经完美的完成了本次任务,但是很多同学在刚开始手动训练模型时,常常会遇到训练失败的情况。在前面的训练过程中,我们可以看到和模型训练相关的因素主要有两个:训练数据量学习率

下面我们设计3个实验,对比不同数据量和学习率对训练结果的影响:

实验

实验设置

实验 1

样本数多,学习率低

实验 2

样本数多,学习率高

实验 3

样本数少,学习率高

2.9.1. 实验1:样本数多,学习率低#

  • 训练轮数:100

  • 样本数:800

  • 学习率:0.001

# 1️⃣ 超参数配置
num_epochs = 100
samples_num = 800
lr = 0.001

# 2️⃣ 将数组转换为 PyTorch 张量
train_features_tensor = torch.tensor(train_features[:samples_num], dtype=torch.float32)
train_targets_tensor = torch.tensor(train_targets[:samples_num], dtype=torch.float32)

# 3️⃣ 定义损失函数和优化器
model = LemonadeRatingModel()  # 创建模型实例
loss_fn = nn.MSELoss()  # 使用均方误差损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=lr)  # 使用随机梯度下降优化器

# 4️⃣ 调用训练方法
training_logs = train_model(model, train_features_tensor, train_targets_tensor, loss_fn, optimizer, num_epochs=num_epochs, log_interval=1)

# 5️⃣ 可视化训练过程
plot_training_curves(training_logs)
../_images/1e3127f1fb4458dfb02623aa7ec8ba3ba3b123f0c0df3f19985e81cda3655e51.png

实验1 中,我们可以看到,损失下降速度较慢但稳定,训练过程平滑,没有有剧烈波动。

2.9.2. 实验2:样本数多,学习率高#

  • 训练轮数:100

  • 样本数:800

  • 学习率:0.009

# 1️⃣ 超参数配置
num_epochs = 100
samples_num = 800
lr = 0.009


# 2️⃣ 将数组转换为 PyTorch 张量
train_features_tensor = torch.tensor(train_features[:samples_num], dtype=torch.float32)
train_targets_tensor = torch.tensor(train_targets[:samples_num], dtype=torch.float32)

# 3️⃣ 定义损失函数和优化器
model = LemonadeRatingModel()  # 创建模型实例
loss_fn = nn.MSELoss()  # 使用均方误差损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=lr)  # 使用随机梯度下降优化器

# 4️⃣ 调用训练方法
training_logs = train_model(model, train_features_tensor, train_targets_tensor, loss_fn, optimizer, num_epochs=num_epochs, log_interval=1)

# 5️⃣ 可视化训练过程
plot_training_curves(training_logs)
../_images/9d6709923929a0c56385864e5754834286ebeda0b9afc52107de973aa39c6717.png

实验2 中,我们可以看到,当学习率增大时,损失下降速度较快,在第 20 轮权重的值就接近最优值了。虽然增大学习率能加快模型的训练速度,但训练的过程存在剧烈波动,训练过程不平滑,有较大的风险训练失败。

2.9.3. 实验3:样本数少,学习率高#

  • 训练轮数:100

  • 样本数:5

  • 学习率:0.009

# 1️⃣ 超参数配置
num_epochs = 100
samples_num = 5
lr = 0.009

# 2️⃣ 将数组转换为 PyTorch 张量
train_features_tensor = torch.tensor(train_features[:samples_num], dtype=torch.float32)
train_targets_tensor = torch.tensor(train_targets[:samples_num], dtype=torch.float32)

# 3️⃣ 定义损失函数和优化器
model = LemonadeRatingModel()  # 创建模型实例
loss_fn = nn.MSELoss()  # 使用均方误差损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=lr)  # 使用随机梯度下降优化器

# 4️⃣ 调用训练方法
training_logs = train_model(model, train_features_tensor, train_targets_tensor, loss_fn, optimizer, num_epochs=num_epochs, log_interval=1)

# 5️⃣ 可视化训练过程
plot_training_curves(training_logs)
../_images/aecce37b7b778282ff6a9e3d03d36c35a73c98f72989c4c20f9f6004d620b219.png

从结果中我们可以看到 实验3 训练彻底失败。和 实验2 相比,它只训练了 5 个样本,训练数据过少导致训练失败。这说明:“数据就是智能的燃料”,就像一个火箭就算有再强的引擎,如果没有足够的燃料也无法到达期望的目的地。

这是本实验的关键教训:

  • 当数据量不足时,模型无法学习到真正的规律。

  • 越是复杂的模型训练所需的数据量越大。

2.9.4. 实验的启示#

通过这三组对比实验,我们系统分析了不同样本量与学习率的组合对模型训练的影响,具体实验设置及结果如下:

  • 实验1:采用多样本量、低学习率设置 → 训练过程稳定,但收敛速度缓慢;

  • 实验2:采用多样本量、高学习率设置 → 训练收敛速度加快,但过程波动剧烈;

  • 实验3:采用少样本量、高学习率设置 → 训练直接失败。

上述三组实验结果为我们提供了一些关键启示:

  • 首先,学习率对训练效果存在双重影响,需精准平衡。

    • 过小的学习率会显著延长训练周期,会降低训练效率,浪费大量的时间和算力成本;

    • 提升学习率虽能有效加速训练收敛,但会破坏训练稳定性,甚至可能导致训练失效。

因此,好的学习率调度策略对模型的训练至关重要。在后续项目中,我们会采用更先进的优化器动态调度学习率,让模型在训练过程又快又好。

  • 其次,数据对模型训练效果具有根本性决定作用。 数据量不足或质量欠佳,会直接导致训练失败。不少 AI 项目失败的原因,都在于对高质量数据采集的重视不足,反而过度将资源倾斜于模型开发与改进。事实上,数据采集的优先级应高于模型本身,其质量与数量直接决定项目成败,是 AI 项目实施过程中的至关重要的环节。

2.10. 本章小结#

本章使用 PyTorch 训练了我们的第一个神经网络模型。我们掌握了如何使用 PyTorch 训练一个多参数的回归模型,并理解了神经网络模型的构建、训练过程以及损失函数和优化器的作用。这为理解深度学习框架的工作机制奠定了坚实基础。但是,在训练过程中,我们的数据还是比较低效的手动管理,在下一个章节中我们将学习如何使用更深的网络、更高效的数据管理方式处理更复杂的多分类问题。

2.11. 答疑讨论#