Pytorch深度学习实践

本博客是根据Bilibil《PyTorch深度学习实践》完结合集_哔哩哔哩_bilibili所做的笔记

名词解释

  • 数据集(Data Set)

    • 训练集(Traing Set)

      • 已知输入x输出y,用于训练模型

      • 训练集可在分一部分出来作为开发集,用于评估模型的泛化能力,防止过拟合

    • 测试集(Test Set)

      • 只知道输入x
  • 过拟合(Overfitting)

    • 是模型在训练集上表现很好,但在新数据上表现很差
    • 但它并没有真正学到数据背后的普遍规律,而是把训练数据中的所有特征,包括噪声,都当作了学习目标。当它遇到新数据时,它的表现会急剧下降,预测结果非常不准确
  • 泛化(Generalization)

    • 泛化是机器学习模型的真正目标,它是指模型在未见过的新数据上的表现能力

线性模型(Linear Model)

拿到数据集,先试着用线性模型试一下,即找到一个权重w和截距b,使得训练集满足:

image-20250915205727712

  • ŷ读作y_hat,表示预测的值,并不是准确值

  • 为了方便学习,这里选择简化模型,将截距b去掉

    image-20250915210137135

所以线性模型的关键是找到一个合适的权重w,而什么叫做”合适呢“?即误差小。

  • 初始数据集可能并不是严格的在直线上的点集,会是离散的,而我们预设的线性模型是一条严格的直线

    image-20250915211004528

  • 而预测的ŷ(a)会和对应的同一个横坐标a的真实值y(a)有差值

    image-20250915211334561

  • 但是由于差值有正有负,所以对差值进行求平方(当然也有其他方法,取绝对值等,但是平方对于误差大的惩罚越大)即可消去负值影响,而这些平方值即为损失(loss)

    • loss只是针对单独一个样本的

    image-20250915212029351

  • 得到单独样本损失后,对所有样本的损失进行求和,再取平均值,即得到训练集的误差(Error)

    • 采用平方来计算这种方法得到的误差叫做平均平方误差或者均方误差(Mean Squared Error, MSE
    • 误差(Error)是针对训练集training set的

    image-20250915213243814

    [!IMPORTANT]

    注意:
    Loss Function (损失函数)是衡量模型在单个数据点上的表现有多差,比如上述差值的平方

    Cost Function (成本函数 或 目标函数)是衡量模型在整个数据集上的总体表现有多差,比如MSE

    虽然技术上有区别,但在实际的工程对话中,人们经常用 Loss 来泛指整个优化过程中的误差指标(也就是成本函数 Cost),例如:“我们看一下训练 Loss 是多少。”

  • 最终找到一个误差最小的权重w,就是线性模型的关键。

    比如下图的数据集与权重选择

    image-20250915213736813

穷举法找权重w

  • 先随机找一个大的步长范围,尝试找到一个可能(可能有增或减的趋势)存在最小权重的范围

  • 再在这个范围内,缩短步长,以此找到产生最小误差的权重

    image-20250915214623127

  • 代码演示,绘制图如上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    # 导入计算和绘图用的包
    import numpy as np
    import matplotlib.pyplot as plt

    # 定义数据集
    x_data = [1.0, 2.0, 3.0] # 输入
    y_data = [2.0, 4.0, 6.0] # 输出

    # 用于计算 y 的预测值
    def forward(x, w):
    return x * w

    # 单个样本的损失loss
    def loss(x, y, w):
    y_pred = forward(x, w)
    return (y_pred - y) * (y_pred - y)

    # MES损失函数
    def MSE(xd, yd, w):
    loss_sum = 0
    for x_val, y_val in zip(xd, yd):
    loss_sum += loss(x_val, y_val, w)
    return loss_sum / len(xd)

    # 权重值列表
    w_list = []
    # MSE误差列表
    mse_list = []

    # w权重取值范围在 [0.0, 4.0], 步长间隔为 0.1
    for w in np.arange(0.0, 4.1, 0.1):
    print('w=', w)
    mes = MSE(x_data, y_data, w)
    print('MSE=', mes)
    w_list.append(w)
    mse_list.append(mes)

    plt.plot(w_list, mse_list)
    plt.ylabel('Loss')
    plt.xlabel('w')
    plt.show()
  • 非简化版本,基本模型:ŷ = x*w + b; w,b,mse,三维绘制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    # 导入计算和绘图用的包
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib import cm
    from mpl_toolkits.mplot3d import Axes3D # 导入3D绘图模块

    # --- 添加这三行,设置中文显示 ---
    plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体为黑体
    plt.rcParams['axes.unicode_minus'] = False # 解决保存图像时负号 '-' 显示为方块的问题
    # --------------------------------

    # 定义数据集
    x_data = [1.0, 2.0, 3.0] # 输入
    y_data = [2.0, 4.0, 6.0] # 输出

    # 用于计算 y 的预测值
    def forward(x, w, b):
    return x * w + b

    # 单个样本的损失loss
    def loss(x, y, w, b):
    y_pred = forward(x, w, b)
    return (y_pred - y) * (y_pred - y)

    # MES损失函数
    def MSE(xd, yd, w, b):
    loss_sum = 0
    for x_val, y_val in zip(xd, yd):
    loss_val = loss(x_val, y_val, w, b)
    loss_sum += loss_val
    return loss_sum / len(xd)

    # 定义 w 和 b 的取值范围
    # arange是 Numpy 库中的一个函数,它类似于 Python 内置的 range() 函数,但主要用于创建包含浮点数的数组。
    # 第一行代码会创建一个从 0.0 开始,到 4.1 结束(不包含 4.1),步长为 0.1 的一维数组。
    # 第二行代码会创建一个从 -2.0 到 2.1 结束(不包含 2.1),步长为 0.1 的数组。
    w_range = np.arange(0.0, 4.1, 0.1)
    b_range = np.arange(-2.0, 2.1, 0.1)

    # 创建一个用于存储 MSE 值的二维网格
    # 分别创建两个矩阵 w_values 和 b_values, 用于绘制 3D 图时的 x, y 坐标
    # w_values:这个矩阵的每一 行 都是 w_range 数组的重复
    # b_values:这个矩阵的每一 列 都是 b_range 数组的重复。
    # 故此将两个矩阵重叠,相同位置的两个元素(w,b)就代表了所有可能的点
    w_values, b_values = np.meshgrid(w_range, b_range)

    # 创建全零矩阵保存所有的损失值
    # np.zeros_like():这个函数会创建一个与 w_values 形状完全相同(即行列数一样)的全零矩阵。
    mse_values = np.zeros_like(w_values)

    # 遍历 w 和 b 的所有组合,计算并存储 MSE 值
    for i, w in enumerate(w_range):
    for j, b in enumerate(b_range):
    mse_values[j, i] = MSE(x_data, y_data, w, b)

    # 绘制 3D 图像
    # figure函数 创建一个新的图形窗口
    # figsize 参数设置了窗口的尺寸(长和宽),当然figsize不填也行
    fig = plt.figure(figsize=(10, 8))

    # add_subplot()函数 在图形窗口中添加一个子图(subplot)
    # 111 表示将图形窗口划分为 1x1 的网格,并在第 1 个子图上绘图
    # 下列方式是简便的工厂模式。告诉 add_subplot 函数你想要一个 3D 坐标系,它就会在后台为你创建一个 Axes3D 对象。
    # projection='3d' 告诉 Matplotlib 要创建一个 3D 坐标系,而不是默认的 2D 坐标系
    # 这种方式代码更简洁,但可能会让 IDE 产生误判,导致最开始的import显示未引用
    ax = fig.add_subplot(111, projection='3d')

    # 绘制曲面图
    # plot_surface() 绘制曲面图函数
    # w_values, b_values, mse_values:这三个参数是 3D 表面图的三个坐标轴:x 轴、y 轴和 z 轴
    # cmap=cm.viridis:cmap 是 "colormap"(颜色映射)的缩写,它定义了曲面图的颜色方案。viridis 是一种常用的、颜色渐变平滑的方案
    # alpha=0.9:alpha 设置了曲面的透明度,0.0 是完全透明,1.0 是完全不透明。
    surf = ax.plot_surface(w_values, b_values, mse_values, cmap=cm.viridis, alpha=0.9)

    # 设置图像标题和标签
    ax.set_title("3D 损失函数(MSE)表面图", fontsize=16)
    ax.set_xlabel('w (权重)', fontsize=12)
    ax.set_ylabel('b (截距)', fontsize=12)
    ax.set_zlabel('MSE (损失)', fontsize=12)

    # 添加颜色条
    fig.colorbar(surf, shrink=0.5, aspect=5)

    # 找到损失函数的最小值点(理论上,w=2.0, b=0.0 时损失为0)
    # np.argmin():这个函数返回一个数组中最小值元素的索引
    # axis=None:忽略数组的维度,而是在整个二维矩阵中寻找唯一的一个最小值。它会返回这个最小值在一维展平(flattened)后的数组中的索引
    # axis=0:表示沿着列(columns)进行操作,有几列就会返回几个元素的数组
    # axis=1:表示沿着行(rows)进行操作,有几行就返回几个元素的数组
    # 例如取None时 mse_values 矩阵是 [[10, 20], [3, 40]],那么它的最小值是 3
    # 在一维展平后,数组变为 [10, 20, 3, 40],3 的索引是 2。所以 np.argmin() 会返回 2
    # np.unravel_index():反向将一维数组的索引转换为二维,但是要填入参数 shape:即我们传入的是原始矩阵的形状
    # 最后返回的是二维坐标 (w, b)
    min_mse_index = np.unravel_index(np.argmin(mse_values, axis=None), mse_values.shape)
    # 在两个矩阵中获取最小MES对应的具体值
    min_b = b_values[min_mse_index]
    min_w = w_values[min_mse_index]

    # 在图上标记最小值点
    # 参数依次对应:颜色、形状、大小、标签说明
    ax.scatter(min_w, min_b, mse_values.min(), color='red', marker='o', s=100,
    label=f'最小损失点\n(w={min_w:.2f}, b={min_b:.2f})')

    # 显示散点图的图例
    ax.legend()

    # 调整视角以获得更好的可视化效果
    # elev 是仰角,azim 是方位角
    ax.view_init(elev=20, azim=-120)

    # 显示最终绘制好的图形窗口
    plt.show()

    image-20250917225211998

分治法求取多维权重(被舍弃的方法)

  • 当涉及的权重有多个时,以二维为例,假设每个权重取100个值,则一共有100^2种组合(维度的诅咒)
  • 首先取一个稀疏点阵,比如 4 x 4 的稀疏点阵,求取这16个点的MSE值
  • 再选取MSE最小的那个点,在它的周围再次取一个 4 x 4 的稀疏点阵,重复计算
  • 以此试图找到使得MSE最小的权重组合

[!TIP]

弊端:为什么被舍弃

  • 机器学习中的优化问题(如线性回归、神经网络训练)通常是连续可导的,不适合用分治法分成若干个小问题

梯度下降算法(推荐)

  • 先随机取一个权重组合
  • 计算取该组合时,损失函数在此对权重的导数(也叫梯度);
    • 导数为正,代表往右递增,往左递减
    • 导数为负,代表往右递减,往左递增
  • 以导数为基准,对权重进行迭代,而不是像穷举法那样以相同的步长进行迭代

一维权重求导过程

权重迭代公式

  • 权重迭代公式中的 α 的名称叫做学习率,是一个很重要的参数,相当于穷举法中的步幅
    • 它不是通过训练学习出来的,而是需要我们在训练前手动设置。选择一个合适的学习率是训练一个高性能模型的关键一步。
    • 步子迈得太大,可能会直接跳过损失函数的最小值点,甚至在“山谷”的两侧来回震荡,导致损失值越来越大,永远无法收敛。这就像你下山时,每一步都跨得太大,结果不是跑到山脚下,而是跳到了对面的悬崖上。
    • 步子迈得太小,虽然能够保证每一步都朝着正确的方向前进,但收敛速度会非常慢,需要进行大量的迭代才能到达最小值。这会极大地增加训练所需的时间和计算资源。这就像你在下山时,每一步都只挪动一小段距离,要花很长时间才能到达山脚。

梯度下降算法的问题

  • 同样是局部最优解,但是比分治法处理的更平滑,也更容易克服,比如我采用多轮次,每轮选取不同的初始点。
  • 鞍点问题:
    • 一维权重时,也就是驻点,即导数为零的点,会导致迭代停止
    • 多维权重时,可能会形成像马鞍(薯片)一样的曲面,从某个方向看,这个点是局部最高点(比如马鞍的前后方向),从另一个方向看,这个点又是局部最低点(比如马鞍的左右方向)。

简化模型使用梯度下降算法预测权重(无截距)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 导入计算和绘图用的包
import matplotlib.pyplot as plt

# 定义数据集
x_data = [1.0, 2.0, 3.0] # 输入
y_data = [2.0, 4.0, 6.0] # 输出

def forward(x, w):
return x*w

# 单个样本的损失loss
def loss(x, y, w):
y_pred = forward(x, w)
return (y_pred - y) * (y_pred - y)

# MSE损失函数
def MSE(xd, yd, w):
loss_sum = 0
for x_val, y_val in zip(xd, yd):
loss_sum += loss(x_val, y_val, w)
return loss_sum / len(xd)

# 定义梯度函数
def gradient(xd, yd, w):
grad_sum = 0
for x_val, y_val in zip(xd, yd):
grad_sum += 2 * x_val * (x_val * w - y_val)
return grad_sum / len(xd)

# 初始随机权重
w = 1.0
# 定义学习率
a = 0.01
# 轮次值列表
epoch_list = []
# MSE误差列表
mse_list = []

print('训练前的预测值 x =', 4, 'y =', forward(4, w))
for epoch in range(100):
mes_val = MSE(x_data, y_data, w)
mse_list.append(mes_val)
grad_val = gradient(x_data, y_data, w)
w -= a * grad_val
epoch_list.append(epoch)
# 详细值
print(f"epoch: {epoch}, w: {w}, mes: {mes_val}")
# 保留两位小数的值
# print(f"epoch: {epoch}, w: {round(w, 2)}, mes: {round(mes_val, 2)}")
print('训练后的预测值 x =', 4, 'y =', forward(4, w))

plt.plot(epoch_list, mse_list)
plt.xlabel('epoch')
plt.ylabel('MES')
plt.show()

image-20250918204114132

随机梯度下降算法(SGD)

  • 上述的梯度算法,对于每次权重迭代的大小是由所有数据的损失汇总取平均值得到的误差来计算的,这样的方式在遇到鞍点时可能会停滞不前

  • 而随机梯度算法,权重的迭代大小是由随机取一个样本(也可以按照顺序随机,也叫顺序随机梯度算法),用其损失对权重求导,这样的方式可能可以跨过鞍点,使权重继续前进

    image-20250918213038164

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 导入计算和绘图用的包
import random
import matplotlib.pyplot as plt

# 定义数据集
x_data = [1.0, 2.0, 3.0] # 输入
y_data = [2.0, 4.0, 6.0] # 输出

# 用于计算 y 的预测值
def forward(x, w):
return x * w

# 单个样本的损失loss
def loss(x, y, w):
y_pred = forward(x, w)
return (y_pred - y) * (y_pred - y)

# 定义梯度函数
def gradient(x, y, w):
return 2 * x * (x * w - y)

# 初始化权重和学习率
w = 1.0
a = 0.01

# 用于记录每个轮次的平均损失
epochs_list = []
costs_list = []

print('训练前的预测值 x = 4, y =', forward(4, w))

# 训练循环
for epoch in range(100):
# 在每个轮次开始时,初始化总损失
epoch_loss_sum = 0

# 随机选择一个样本进行训练
index = random.randint(0, 2)# 这里是真随机
x_val = x_data[index]
y_val = y_data[index]

# 计算梯度并更新权重
grad = gradient(x_val, y_val, w)
w -= a * grad

# 计算当前样本的损失并累加到总损失中
current_loss = loss(x_val, y_val, w)

# 因为是SGD,一个轮次只训练一个样本,所以直接记录当前损失即可
epoch_loss = current_loss / 1 # loss的计算只是为了输出体现为0而已

# 打印当前轮次的信息
print(f'Epoch: {epoch}, w = {w:.2f}, loss = {epoch_loss:.2f}')

# 记录每个轮次的平均损失和轮次数,用于绘图
epochs_list.append(epoch)
costs_list.append(epoch_loss)

print('训练后的预测值 x = 4, y =', forward(4, w))

# 绘制损失曲线
plt.plot(epochs_list, costs_list)
plt.ylabel('Cost')
plt.xlabel('Epoch')
plt.show()
  • 输出结果(由于每次都取单独一个样本进行计算梯度,所以波动会很大,每次图像基本不一样)

    image-20250918221902242

小批量梯度下降算法(Mini-BGD)

  • 现在普遍也称BGD
  • 比如训练集有8对数据,每个小批次为2,则会分为4个批次batch
  • 梯度计算是取当前批次内的样本所有梯度取平均,再用这个平均梯度更新权重
  • 计算并记录当前 Epoch 的最终性能指标(Cost 或 Loss)时,都遵循,使用更新后的权重 w,对整个数据集MSE(或平均损失)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
import matplotlib.pyplot as plt

# 假设数据集有8组
x_data = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
y_data = np.array([2.3, 4.1, 5.8, 8.4, 10.3, 11.7, 14.5, 15.9])

# 用于计算 y 的预测值
def forward(x, w):
return x * w

# 使用 NumPy 计算整个样本的平均损失
def avg_loss(x, y, w):
y_pred = forward(x, w)
return np.mean((y_pred - y) ** 2)

# 使用 NumPy 计算整个Batch的平均梯度
def avg_gradient(x, y, w):
return np.mean(2 * x * (x * w - y))

# 初始化
w = 1.0 # 权重
a = 0.0001 # 学习率
batch_size = 2 # 批次Batch大小
num_samples = len(x_data) # 数据总组数
num_batches = num_samples // batch_size # 批次Batch数量

# 轮次列表 与 轮次的平均损失列表
epochs_list = []
costs_list = []

print('训练前的预测值 x = 4, y =', forward(4, w))

# 训练循环
for epoch in range(200):
# 第一步:打乱数据
indices = np.random.permutation(num_samples) # 生成一个包含从 0 到 num_samples-1 的随机排列数组
# 每个轮次的 数据顺序都不同
shuffled_x = x_data[indices]
shuffled_y = y_data[indices]

# 第二步:创建批次Batch并循环
for i in range(num_batches):
# 得到当前批次Batch的 起始 和 终止 索引
start_index = i * batch_size
end_index = start_index + batch_size
# 获取当前批次Batch的数据
batch_x = shuffled_x[start_index:end_index]
batch_y = shuffled_y[start_index:end_index]

# 计算当前批次Batch的平均梯度
avg_batch_grad = avg_gradient(batch_x, batch_y, w)

# 更新权重
w -= a * avg_batch_grad

# 计算并记录当前轮次(epoch)的平均损失
avg_epoch_loss = avg_loss(x_data, y_data, w)

# 记录 当前轮次 和 当前轮次 的平均损失至数组方便绘图
epochs_list.append(epoch)
costs_list.append(avg_epoch_loss)

print(f'Epoch: {epoch}, w = {w}, avg_loss = {avg_epoch_loss}')

print('训练后的预测值 x = 4, y =', forward(4, w))

# 绘制损失曲线
plt.plot(epochs_list, costs_list)
plt.ylabel('Cost')
plt.xlabel('Epoch')
plt.show()

image-20250919174947709

  • 上述代码在求取平均值时采用了np.mean()的方法,使代码更简洁。

复杂神经网络初体验

上述所实现的模型都只是很简单的少量权重,少量层数的线性模型。

那考虑如下图的大量权重,大量层数的多隐层前馈全连接神经网络

image-20250919214751381

  • x11 先变成 y11,得通过H1,H2,H3,H4,这四层权重
  • 输入X,是一个 5 * 1 的矩阵,第一层H1,是一个 6 * 1 的矩阵
  • 而矩阵X变换成矩阵H1,需要权重矩阵 W1 与 X 相乘,即 W * X = H
  • 也就是说 W1 是一个 6 * 5 的矩阵
  • 即 [6 * 5] * [5, 1] = [6 * 1]
  • 同理 H1 变换至 H2,需要一个 [7 * 6] 的W2矩阵

反向梯度传播算法

上述的全连接神经网络,有好几百个权重,如果每个都采用先写解析式是很困难的,而且每层之间都是复合函数,这是个相当复杂的工作量。

故此考虑,把这样一个复杂的神经网络,看作一个图,通过图来传播梯度,根据链式法则求取梯度,这就叫反向梯度传播算法。

两层简单图

image-20250919221832160

  • 每一层的结构都是一样的

  • 权重矩阵 W 与输入矩阵相乘,得到一个新的矩阵

  • 新的矩阵与偏置值矩阵 b 相加,得到输出结果

  • 当前层的输出作为下一层的输入

  • 最终得到预测解析式,类似于迭代套娃

    image-20250919222136045

  • 但是这样建立模型会有一个问题,因为不管你套娃多少层,都可以通过计算展开成单层模型的样子(又回到最初的起点),不管多少层都变成单一线性的,如下图

    image-20250919222551620

激活函数

  • 解决方法也很简单,即在每一层的输入之前,对上一层的输出加上一个非线性的变化函数,整个函数也叫做激活函数,这样就不能对迭代式子进行展开了

    image-20250919222858405

手算最小简单图

3c740378c7b44b705869e7192572877

bd0537a18fae60f6476a65d7d9e471c

Tensor的作用

Tensor 是一个多维数组,可以用来存储标量(0 维)、向量(1 维)、矩阵(2 维)乃至更高维度的数据。

  • 存储所有数据类型:在 PyTorch 中,无论是模型的输入数据(如图像的像素值、文本的词向量),模型的参数(权重 W 和偏置 b),还是中间计算结果,一切皆为 Tensor

Tensor是 PyTorch 框架中动态计算图的基石,负责存储数据和梯度

  • Tensor 是构建计算图的基本单元。在一个深度学习模型中,所有运算(如加、乘、矩阵乘法等)都是 Tensor 之间的操作
    • 动态 (Dynamic):这是 PyTorch 的关键特性。计算图是在运行时(即每次前向传播时)动态构建的。这意味着图的结构可以根据输入数据的不同或程序逻辑(如条件判断)而改变。
  • Tensor 内部存储了两个至关重要的数值
    • 数据 (Data):主体部分,即节点的值,用于存储实际的输入数据、模型参数(权重 W、b)以及前向传播过程中的所有中间结果。
    • 梯度 (Grad):Tensor 的 .grad 属性,存储了梯度值 (gradient),这个值代表了损失函数 (loss) 对该 Tensor 的导数

image-20250924205707890

使用Tensor实现反向传播

关键代码详解:

  • loss_val.backward() 执行后,具体哪些对象及其值会发生改变?
    • 唯一会发生实质性改变的对象是具有 requires_grad=True 属性的叶子 Tensor,在本例中就是您的权重 w
    • w.grad:值被累加
    • w:值保持不变(w的值默认表示data)
    • 其他中间 Tensor:值保持不变(forward() 过程中产生的中间 Tensor(如 y_pred)的值保持不变,它们所包含的梯度历史信息会被用于反向传播,然后通常会被释放。)
  • 为什么要使用 with torch.no_grad(): 包裹更新权重的代码?
    • 防止构建计算图:梯度下降的目标是修改权重 w 的值,而不是将这个修改过程作为一次可微分的计算。torch.no_grad() 告诉 PyTorch:“以下操作只是数据管理,不要追踪。”
    • 避免循环依赖和错误:如果没有 no_grad(),PyTorch 会将 w_new = w_old - a * w.grad 这个操作记录到计算图中。
  • 为什么梯度要使用 w.grad.zero_() 置零,以及为什么它不用被 torch.no_grad() 包裹?
    • backward() 方法的机制是累加梯度,除非确实需要累加,否则要显示的使用.zero_()置零
    • w.grad 存储的是上一次计算的结果,它是一个非活跃的 Tensor。我们并不需要对梯度本身求梯度。PyTorch 已经明确地将对 .grad 属性执行的原地操作(如 zero_())视为纯粹的数据管理,并默认允许它绕过梯度追踪系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import torch
import matplotlib.pyplot as plt

# 定义数据集
x_data = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
y_data = torch.tensor([2.0, 4.0, 6.0], dtype=torch.float32)
a = 0.01

# 创建一个权重tensor,将值用[]框起来
w = torch.tensor([1.0])
# 启动自动计算loss对w的梯度,默认关闭
w.requires_grad_(True)

def forward(x):
# 这里刚开始 w 是个tensor,x 是个数
# 当他俩相乘时,乘号*会自动重载,把x也化为一个tensor对象
# 最终做乘法返回一个tensor
return x * w

# 每调用一次loss函数,就动态的构建了计算图
def loss(x, y):
y_pred = forward(x)
return (y_pred - y) ** 2

# 轮次列表 与 轮次的平均损失列表
epochs_list = []
costs_list = []

print('训练前的预测值 x =', 4, 'y =', forward(4).item())

for epoch in range(100):
for x_val, y_val in zip(x_data, y_data):
loss_val = loss(x_val, y_val)
loss_val.backward() # 将梯度反向传播至w的tensor,并且与该样本相关的计算图就会被释放
print(f"x = {x_val}, y = {y_val}, w = {w.item()}, grad = {w.grad.item():.4f}, loss = {loss_val.item():.4f}")

with torch.no_grad():
w -= a * w.grad # 使用no_grad()包裹,使tensor只更新data值,不生成计算图

w.grad.zero_() # 更新完成后,将梯度置零,防止累加

with torch.no_grad():
# 计算整个数据集的均方误差 (MSE)
final_epoch_loss = torch.mean(loss(x_data, y_data)).item()
print(f"------------------ Epoch {epoch:03d} End ------------------")
epochs_list.append(epoch)
costs_list.append(final_epoch_loss)

print('训练后的预测值 x = 4, y =', forward(4).item())

# 绘制损失曲线
plt.plot(epochs_list, costs_list)
plt.ylabel('Cost')
plt.xlabel('Epoch')
plt.show()

image-20250924221417266

课后作业:二维权重计算题手算与代码实现

2d4c3d1409871e726d1c69164a19260

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import numpy as np
import torch
import matplotlib.pyplot as plt

# 定义数据集
x_data = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
y_data = torch.tensor([2.0, 4.0, 6.0], dtype=torch.float32)
a = 0.01

w1 = torch.tensor([1.0])
w2 = torch.tensor([1.0])
b = torch.tensor([1.0])
w1.requires_grad_(True)
w2.requires_grad_(True)
b.requires_grad_(True)

def forward(x):
return w1 * x ** 2 + w2 * x + b

def loss(x, y):
y_pred = forward(x)
return (y_pred - y) ** 2

# 轮次列表 与 轮次的平均损失列表
epochs_list = []
costs_list = []

print('训练前的预测值 x =', 4, 'y =', forward(4).item())

for epoch in range(5000):
for x_val, y_val in zip(x_data, y_data):
loss_val = loss(x_val, y_val)
loss_val.backward()
with torch.no_grad():
w1 -= a * w1.grad
w2 -= a * w2.grad
b -= a * b.grad

print(f"x = {x_val}, y = {y_val}\n"
f"w1 = {w1.item():.3f}, w1's new grad = {w1.grad.item():.3f}\n"
f"w2 = {w2.item():.3f}, w2's new grad = {w2.grad.item():.3f}\n"
f"b = {b.item():.3f}, b's new grad = {b.grad.item():.3f}\n"
f"single_loss = {loss_val.item():.3f}\n")

w1.grad.zero_()
w2.grad.zero_()
b.grad.zero_()
with torch.no_grad():
# 计算整个数据集的均方误差 (MSE)
final_epoch_loss = torch.mean(loss(x_data, y_data)).item()

print(f"------------ Epoch {epoch:03d} End ----------- Epoch's MSE: {final_epoch_loss:.4f}-----------")
epochs_list.append(epoch)
costs_list.append(final_epoch_loss)

print('训练后的预测值 x = 4, y =', forward(4).item())

# 绘制损失曲线
plt.plot(epochs_list, costs_list)
plt.ylabel('Cost')
plt.xlabel('Epoch')
plt.show()

image-20250925222226179

阶段小结

SGD与Mini-BGD、BGD的区别

  • Mini-batch 梯度下降 (MBGD) 中,梯度计算权重更新 都是基于当前批次平均值

  • SGD 的标准定义是:在每一次权重更新时,只使用一个样本的梯度。就算他是每个都算,并且按顺序算,但是他更新权重的时机是在单一样本后的。

    • 一个样本更新一次w
  • BGD(Batch Gradient Descent,批量梯度下降)的定义是:在进行一次权重更新时,必须使用整个数据集的平均梯度。

    • N个样本更新一次w
  • Mini-BGD,梯度计算权重更新 都是基于当前批次平均值

计算当前Epoch的时机

  • 不管采用哪种梯度下降算法(BGD、MBGD、SGD),计算并记录当前 Epoch 的最终性能指标(Cost 或 Loss)时,都遵循,使用更新后的权重 w,对整个数据集MSE(或平均损失)

使用PyTorch实现线性回归

名词解释:张量、标量、向量、矩阵

  • 张量:即前面提到的Tensor,是 PyTorch 存储数据和执行计算的基本单位,张量是一个多维数组,它是标量、向量和矩阵的广义统称。

  • 标量:标量是一个零维张量 (0-D Tensor) ,它只包含一个数值,没有方向或大小之分。

    • torch.tensor(5.0)
  • 向量:向量是一个一维张量 (1-D Tensor),它包含一列有序的数值,只有大小和方向。

    • torch.tensor([1, 2, 3])
  • 矩阵:矩阵是一个二维张量 (2-D Tensor) ,它由行和列组成,是两个向量的排列。

    • torch.tensor([[1, 2], [3, 4]])

[!TIP]

几个 [] 就是几维张量

前置关键代码解释

nn.Linear(in_features, out_features, bias=true)

一共三个主要参数需要填写,Linear源码如下图:

image-20251011191525307

  • in_features:输入特征数,或者说输入的维度,比如说你的输入矩阵是一个3*2矩阵,每一行代表单个样本,也就是说一个样本有两个维度,即列数,那么这里就应该设置in_features=2
  • out_features:输出特征数,同上。如果输出是一个4*3的矩阵,每一行代表单个样本,也就是有3个维度,那么就应该设置out_features=3
  • bias:默认为True,用于是否要在计算过程中加上偏置项b

[!NOTE]

这里有一个很重要的思想转变,即输入输出的值,从”数字”变成“矩阵”

看网课时我一开始一直没理解老师为什么要一直使用矩阵的知识,我默认认为一个线性模型y = w*x + b中的x、y、w、b都只是一个数字。

但是现在要转变观念,X可以是一个m * n的矩阵,Y可以是一个 m * q 的矩阵

那么又引申出一个问题,W应该是怎样的矩阵?

当使用PyTorch进行线性回归时,将会遵循以下乘法规则:

image-20251011193345138

至于为什么要对W进行转置,请往下看:

  • 当你输入nn.Linear(3,2)时,也就意味着你的输入数据集X_Data是形状是 N * 3,输出数据集Y_Data的形状是 N * 2,总共有N个样本

  • 如果采用上图的乘法顺序,W的性质应该刚好是 3 * 2,和你括号中输入的顺序是一致的,但是由于PyTorch内部规定了需要取转置后再乘(如下图),所以为了符合直觉,源代码中特地反转了。

    image-20251011194135976

optimizer = torch.optim.SGD(my_model.parameters(), lr=0.01)

  • my_model.parameters() 的作用是返回一个迭代器 (iterator) ,其中包含了 my_model 实例中所有需要学习和更新的参数 (Parameter) 。
  • 反正是pytorch帮忙找,用迭代找的,后续需要再深入学习。

执行my_model(x_data)会自动执行forward返回预测值

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import torch
from torch import nn ## nn 是 neural network 神经网络的缩写
import matplotlib.pyplot as plt

#定义数据集,设置为矩阵模式,3*1 的矩阵
x_data = torch.tensor([[1.0], [2.0], [3.0]], dtype=torch.float)
y_data = torch.tensor([[2.0], [4.0], [6.0]], dtype=torch.float)

class LinearRegression(nn.Module):
def __init__(self):
# 调用父类的初始化方法
super(LinearRegression, self).__init__()
# nn.Linear 自动创建并初始化 w 和 b,反正会自动,具体可以后续详细了解
self.linear = nn.Linear(1, 1)

# 前馈
def forward(self, x):
# 默认执行 y = xW^T + b.
return self.linear(x)

# 反馈,PyTorch会自动帮你求导,除非你自己写的效率比PyTorch还高

# 实例化自己的模型
my_model = LinearRegression()
# 均方误差损失函数
criterion = nn.MSELoss()
# 使用随机梯度下降优化器(即更新权重的算法)
optimizer = torch.optim.SGD(my_model.parameters(), lr=0.01)

# 打印训练前的参数
w_init = my_model.linear.weight.item()
b_init = my_model.linear.bias.item()
print(f"训练前参数: w={w_init:.4f}, b={b_init:.4f}")

# 3. 训练参数
num_epochs = 500
costs_list = []
epochs_list = []

for epoch in range(num_epochs):
# 每次迭代都需要执行的操作:

# (A) 梯度清零:使用优化器内置的方法,替代 w.grad.zero_()
optimizer.zero_grad()

# (B) 前向传播
y_pred = my_model(x_data)

# (C) 计算损失
loss = criterion(y_pred, y_data)

# (D) 反向传播:计算梯度
loss.backward()

# (E) 更新权重:使用优化器内置的方法,替代 with torch.no_grad(): W -= lr * W.grad
optimizer.step()

# 记录损失(使用更新后的权重计算)
# 如果不在意小精度,也可以直接costs_list.append(loss.item()),也就是使用旧的W
with torch.no_grad():
costs_list.append(criterion(my_model(x_data), y_data).item())
epochs_list.append(epoch)

if (epoch + 1) % 10 == 0:
current_w = my_model.linear.weight.item()
current_b = my_model.linear.bias.item()
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.6f}, w: {current_w:.4f}, b: {current_b:.4f}')

# 5. 最终预测和结果
print("-" * 40)
final_w = my_model.linear.weight.item()
final_b = my_model.linear.bias.item()

# 预测 x=4
x_test = torch.tensor([[4.0]], dtype=torch.float32)
y_test_pred = my_model(x_test).item()

print(f"训练后参数: w={final_w:.4f}, b={final_b:.4f}")
print(f"训练后预测 x=4, y_pred={y_test_pred:.4f}")

# 6. 绘图
plt.plot(epochs_list, costs_list)
plt.ylabel('MSE Loss')
plt.xlabel('Epoch')
plt.show()

训练结果最好的一次

使用其他优化器

image-20251011202737953

逻辑斯蒂回归(Logistic Regression)

逻辑斯蒂回归本质上是一个二分类算法,并不是像线性回归那样输出一个具体的值,而是输出该样本属于某个分类的概率

  • 例如手写的0~9的数据集,难道要拿到一张手写图,直接输出这是具体的几吗?并不是,而是输出它是0的概率P(0),它是1的概率P(1)……,最终选择概率最大的

  • 而逻辑斯蒂回归主要解决的是二分类,也就是只有两个分类。

  • 由于概率总和肯定等于1,所以我们只需要计算其中一个分类的概率即可。

  • 而逻辑斯蒂回归的核心在于将线性回归的结果通过一个非线性函数——Sigmoid 函数(或 Logit 函数)——映射到 (0,1) 的概率区间。所以要先进行线性回归

  • 其中最经典的函数是Logit 函数(如下图),它完美的把整个实数域限制在了[-1, 1]之间,当然还有很多其他的Sigmoid 函数,但是由于Logit 函数太过经典,所以一般说Sigmoid 函数时,往往代指的就是Logit 函数

    最经典的Logit函数

其他的Sigmoid函数

  • 与线性回归对比,回顾一下前面提到的激活函数,以及为什么需要激活函数

image-20251011213958565

image-20251011214036393

损失函数的改变:BCE

  • 原本线性回归的MSE是求两个具体值的差值,而逻辑斯蒂得到的是两个概率分布,而概率分布是不能直接相减的

  • 而求概率的差值用的是交叉熵,举例如下:

    image-20251014223102425

  • 而逻辑斯蒂是二分类问题,只有两种分布,真实值的分布和预测值的分布,即y和ŷ,那么得到的形式如下:

image-20251011214218535

  • y=1 时,即真实值属于class_1,(1-y) = 0,则损失函数就变成了:loss = -log ŷ,而损失是要越小越好,由于是前面有负号,则表示log ŷ越大越好,根据对数函数的图像,且定义域 ŷ 属于[0, 1],所以当 ŷ 越接近 1 时,loss的值越小越接近0,也就代表 ŷ 越接近真实值 y = 1
  • 当 y = 0 时反之;

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import numpy as np
import torch
import torch.optim as optim
from torch import nn ## nn 是 neural network 神经网络的缩写
import matplotlib.pyplot as plt
import torch.nn.functional as F
#定义数据集,设置为矩阵模式,3*1 的矩阵
# X 表示学习时长
x_data = torch.tensor([[1.0], [2.0], [3.0]], dtype=torch.float)
# Y 表示是否及格
y_data = torch.tensor([[0.0], [0.0], [1.0]], dtype=torch.float)

class LogisticRegressionModel(nn.Module):
def __init__(self):
# 调用父类的初始化方法
super(LogisticRegressionModel, self).__init__()
# nn.Linear 自动创建并初始化 w 和 b,反正会自动,具体可以后续详细了解
self.linear = nn.Linear(1, 1)
# 前馈
def forward(self, x):
return F.sigmoid(self.linear(x))

# 反馈,PyTorch会自动帮你求导,除非你自己写的效率比PyTorch还高

# 实例化自己的模型
my_model = LogisticRegressionModel()
# 使用 BCELoss (二元交叉熵损失)
criterion = nn.BCELoss()
# 使用 SGD 优化器,优化模型的参数 (w 和 b)
optimizer = optim.SGD(my_model.parameters(), lr=0.05)

# 打印训练前的参数
w_init = my_model.linear.weight.item()
b_init = my_model.linear.bias.item()
print(f"训练前参数: w={w_init:.4f}, b={b_init:.4f}")

# 训练参数及记录
num_epochs = 2000
costs_list = []
epochs_list = []

print("--- 开始训练 ---")

for epoch in range(num_epochs):

# (A) 梯度清零
optimizer.zero_grad()

# (B) 前向传播: 得到预测概率 (Y_pred)
y_pred = my_model(x_data)

# (C) 计算损失
loss = criterion(y_pred, y_data)

# (D) 反向传播: 计算梯度
loss.backward()

# (E) 更新权重
optimizer.step()

# 记录损失
costs_list.append(loss.item())
epochs_list.append(epoch)

if (epoch + 1) % 200 == 0:
current_w = my_model.linear.weight.item()
current_b = my_model.linear.bias.item()
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.6f}, w: {current_w:.4f}, b: {current_b:.4f}')

# 6. 最终预测和结果
print("-" * 30)
print("--- 训练结束,查看结果 ---")

# 切换到评估模式,并禁用梯度计算
my_model.eval()
with torch.no_grad():
# 预测整个数据集的概率
y_prob = my_model(x_data)
# 将概率转换为类别 (P >= 0.5 视为及格 1)
y_predicted_classes = (y_prob >= 0.5).float()

print(f"学习时长 (X):\n{x_data.squeeze()}")
print(f"预测概率 (P):\n{y_prob.squeeze()}")
print(f"预测类别 (Y):\n{y_predicted_classes.squeeze()}")
print(f"真实类别 (Y_data):\n{y_data.squeeze()}")

# 7. 绘图 (可选: 可视化损失曲线)
plt.plot(epochs_list, costs_list)
plt.ylabel('BCE Loss')
plt.xlabel('Epoch')
plt.show()

# 在 0 到 10 的范围内均匀地生成 200 个数据点
x = np.linspace(0, 10, 200)
# 把 x 用Tensor转换成 200 * 1 的矩阵
x_test = torch.Tensor(x).view(200, 1)
# 用训练好的模型输出y_test
y_test = my_model(x_test)
# Tensor 转列表
y = y_test.data.numpy()
plt.plot(x, y)
plt.plot([0, 10], [0.5, 0.5], c='r')
plt.xlabel('Hours')
plt.ylabel('Probability of Pass')
plt.grid()
plt.show()

image-20251014233538882

image-20251014233447064

多维特征的输入(示例:疾病风险预测)

  • 其实前面已经提到了,线性模型中的输入输出数据集可以不在是一个具体的数,而是可以是多维的矩阵

  • 采用矩阵加广播机制,可以不再使用for循环来训练,减轻代码量

  • 而此时可以回答复杂神经网络的问题,它中间有很多层,每层都是一次线性回归,但是层与层之间要加上激活函数

  • 当然你也可以手动的增加层数,比如你的输入集是8维的,输出集是一维的,直接使用(8 * 1)的权重当然可以,但是这样泛化能力不强。所以可以在中间加上几层神经网络,比如8–10–6–4—2–1,不但可以降维,甚至可以升维。

    image-20251015201229862

示例代码

主要训练代码和逻辑斯蒂回归一致,主要变化在于数据集的准备和构造模型上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data = np.loadtxt('DataSet/diabetes.csv.gz', delimiter=',', dtype=np.float32)
x_data = torch.from_numpy(data[:, :-1]) # 所有行,除了最后一列
y_data = torch.from_numpy(data[:, [-1]]) # 所有行,最后一列,加[]的原因是防止numpy自动把矩阵降级成向量

class MDIModel(nn.Module):
def __init__(self):
super(MDIModel, self).__init__()
self.linear1 = nn.Linear(8, 6)
self.linear2 = nn.Linear(6, 4)
self.linear3 = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid()
self.relu = nn.ReLU()

def forward(self, x):
# 注意:连续使用 Sigmoid 可能会导致梯度消失,一般最后一层才用逻辑斯蒂
# 现代网络中内部层通常使用 ReLU
x = self.relu(self.linear1(x))
x = self.relu(self.linear2(x))
x = self.sigmoid(self.linear3(x))
return x

image-20251015205313987

Dataset and DataLoader

Dataset

  • 总不能还在代码里面手写数据集吧,更何况数据集还可能包括图片,语言这种形式
  • Dataset是用来构造一个数据集的,在torch中是一个抽象类,需要自定义类然后继承它
1
2
3
4
5
6
7
8
9
10
class DiabetesDataset(Dataset):
def __init__(self, filepath):
xy = np.loadtxt(filepath, delimiter=',', dtype=np.float32)
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:, :-1])
self.y_data = torch.from_numpy(xy[:, [-1]])
def __getitem__(self, index):
return self.x_data[index], self.y_data[index]
def __len__(self):
return self.len
  • init方法,如果数据集小可以像上述一样直接全部加载到内存里,如果过大就要采用特殊的方法,比如类似页表?
  • getitem方法,根据索引返回单个样本,dataset[index]
  • len方法,返回数据集的样本个数

DataLoader

  • 主要是用来实现将数据集分批加载

  • 基本概念

    • Epoch(轮次):一个完整的训练周期。 当所有训练数据在神经网络中完整地进行了一次前向传播(计算预测值)和一次反向传播(更新权重)时,就完成了一个 Epoch。Epoch 标志着模型对整个数据集的学习次数。通常需要多个 Epoch(如 100 次、1000 次)才能使模型收敛。

    • Batch Size(批次大小):每次模型(权重)更新时使用的样本数量。 由于完整数据集通常太大,无法一次性放入内存或进行高效计算,因此训练数据会被分成许多小批次(Batch)。决定了权重的更新频率和稳定性。

      • 小 Batch Size (如 1 - 32): 权重更新频繁,但梯度计算有较大随机性(SGD)。
      • 大 Batch Size (如 64 - 256): 权重更新次数少,梯度计算更稳定(近似 BGD)。
    • Iteration(迭代):完成一个 Epoch 所需的权重更新次数。 每处理完一个 Batch 的数据,就会完成一次迭代,并更新一次模型的权重。在数量上等于batch的个数

      image-20251015221336256

  • 基本参数

    • dataset:数据集的实例
    • batch_size:batch的大小
    • shuffle:是否打乱数据集
    • num_workers:训练时的线程数量
1
train_loader = DataLoader(dataset=dataset, batch_size=32, shuffle=True, num_workers=2)

image-20251015221814219

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import numpy as np
import torch
from matplotlib import pyplot as plt
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader


class DiabetesDataset(Dataset):
def __init__(self, filepath):
xy = np.loadtxt(filepath, delimiter=',', dtype=np.float32)
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:, :-1])
self.y_data = torch.from_numpy(xy[:, [-1]])
def __getitem__(self, index):
return self.x_data[index], self.y_data[index]
def __len__(self):
return self.len

dataset = DiabetesDataset('DataSet/diabetes.csv.gz')
train_loader = DataLoader(dataset=dataset, batch_size=32, shuffle=True, num_workers=0)

class MDIModel(nn.Module):
def __init__(self):
super(MDIModel, self).__init__()
self.linear1 = nn.Linear(8, 6)
self.linear2 = nn.Linear(6, 4)
self.linear3 = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid()
self.relu = nn.ReLU()

def forward(self, x):
x = self.relu(self.linear1(x))
x = self.relu(self.linear2(x))
x = self.sigmoid(self.linear3(x))
return x

my_model = MDIModel()
# criterion = nn.BCELoss(size_average=True) # <-- 旧写法
criterion = nn.BCELoss(reduction='mean') # <-- 新写法
optimizer = optim.SGD(my_model.parameters(), lr=0.05)

# 训练参数及记录
num_epochs = 500
costs_list = []
epochs_list = []

if __name__ == '__main__':
for epoch in range(num_epochs):
total_batch_loss = 0.0
num_batches = 0
for i, data in enumerate(train_loader, 0):
x_data, y_data = data # 自动把x和y从train_loader中分离出来,并且是二维Tensor
optimizer.zero_grad()
y_pred = my_model(x_data)
loss = criterion(y_pred, y_data)

total_batch_loss += loss.item() # 累加本 Batch 的损失
num_batches += 1

loss.backward()
optimizer.step()
# 在 Epoch 结束后,计算平均损失
# 这样虽然不是特别准确, 但是胜在简洁高效,适用于训练集,如果是测试集则需要更准确的loss统计
average_epoch_loss = total_batch_loss / num_batches

# 记录 Epoch 损失
costs_list.append(average_epoch_loss)
epochs_list.append(epoch)

# 可以输出 Epoch 结果
print(f'Epoch [{epoch + 1}/{num_epochs}], Avg Loss: {average_epoch_loss:.6f}')

plt.plot(epochs_list, costs_list)
plt.ylabel('BCE Loss')
plt.xlabel('Epoch')
plt.show()

image-20251015224237023


Pytorch深度学习实践
https://blog.gutaicheng.top/2025/09/16/Pytorch深度学习实践/
作者
GuTaicheng
发布于
2025年9月16日
许可协议