文章
努力加载图片中...
【学习笔记】刘二《PyTorch深度学习实践》- 1
  • 7870 字

  • 10 分钟

  • 22 次

  • 2026-06-28
标签:

1 线性模型

1.1 机器学习流程

机器学习的步骤要求:

  1. 获取数据集 DataSet
  2. 选择模型 Model
  3. 训练 Training
  4. 推理 Inferring

机器学习的通常步骤:

  1. 使用训练集训练模型
  2. 使用开发集(从训练集中划分出来)初步测试
  3. 使用测试集测试性能
  4. 得到最终的模型
  5. 输入新的数据给模型
  6. 得到预测结果
  • 在训练时的拟合效果很好,但是在测试集却不是很好,可能是在训练时将噪声也进行识别了,导致过拟合
  • 在训练时的拟合效果很好,在测试集以及最终上线后,也能正确识别结果,说明模型具有很好的泛化能力。

image.png

1.2 线性回归模型学习

线性回归模型:

y^=wx+b\hat{y} = w x + b

在模型训练中,对于需要计算实际值(预测值)与真实值之间的差距。对于线性回归模型来说,就是上面蓝色的线或者下面绿色的线,与中间红色的线的垂直高度差。单个样本的差距可以通过以下公式表示:

loss=y^yloss = \lvert \hat{y} - y \rvert

但为了在数学上便于求导和优化,通常来说,使用平方来代替绝对值,也就是叫做平方误差

loss(i)=(y^iyi)2loss\left( i \right) = \left( \hat{y}_i - y_i \right)^2

对于单个样本来说,这种函数称为模型的损失函数(Loss Function),针对的是单个样本。

对于包含了 N 个样本的整个训练集,通常取样本损失的平均值,称为均方误差(MSE ,Mean Square Error) ,也称为模型的成本函数(Cost Function)

我们通常通过一些算法让 Cost 尽可能小,不过这只是手段之一。最终目的是要让模型在未知的新数据上的误差尽可能小,也就是让模型的泛化能力更高。

image.png

使用一段代码,简单的了解一下模型的训练过程,以及在线性回归模型中,未知权重参数与均值误差之间的关系。

python
import matplotlib.pyplot as plt import numpy as np # 训练集 x_data = [1.0, 2.0, 3.0] y_data = [2.0, 4.0, 6.0] # 线性回归模型,去掉了截距 b def forward(x, w): return x * w # 损失函数 def loss(y_pred, y): return (y_pred - y) * (y_pred - y) # 调起的训练函数 def training(): w_list = [] mse_list = [] # 设置权重 w 的范围(随机猜测) for w in np.arange(0.0, 4.1, 0.1): print(f"w = {w}") # 计算预测值与损失值 los_sum = 0 for x_val, y_val in zip(x_data, y_data): y_pred = forward(x_val, w) los_val = loss(y_pred, y_val) los_sum += los_val print(f"\t x = {x_val}, y = {y_val}, y_pred = {y_pred}, loss = {los_val}") # 计算该权重 wi 下的均方误差 mse_val = los_sum / 3 print(f"\t >>> MSE = {mse_val} \n") # 记录 wi 下的均方误差 mi w_list.append(w) mse_list.append(mse_val) # 绘制图 plt.plot(w_list, mse_list) plt.ylabel("Loss") plt.xlabel("w") plt.show() if __name__ == "__main__": training()

运行结果: image.png

作业,基于完整线性回归模型:y^=wx+b\hat{y} = w x + b ,由于有两个未知参数,即权重 w 和截距 b,所以需要绘制权重 w 和截距 b 与成本函数 Cost 的关系图。 image.png

实现代码:

python
import matplotlib.pyplot as plt import matplotlib.ticker as ticker import numpy as np # 训练集 x_data = [1.0, 2.0, 3.0] y_data = [3.0, 5.0, 7.0] # 线性回归模型 def forward(x, w, b): return x * w + b # 损失函数 def loss(y_pred, y): return (y_pred - y) * (y_pred - y) # 调起训练的方法 def training(): # 定义权重 w 和截距 b 的范围 w_list = np.arange(0.0, 4.1, 0.1) b_list = np.arange(-2.0, 2.1, 0.1) # 由于绘制需要一个二维矩阵,这里先定义出来 mse_matrix = np.zeros((len(b_list), len(w_list))) # 计算 MSE for i in range(len(b_list)): # 获取 bi b_val = b_list[i] print(f"b = {b_val}") # 获取 wj for j in range(len(w_list)): w_val = w_list[j] print(f" w = {w_val}") # 计算在 bi 和 wj 下的损失值 los_sum = 0 for x_val, y_val in zip(x_data, y_data): y_pred = forward(x_val, w_val, b_val) los_val = loss(y_pred, y_val) los_sum += los_val # 计算并记录在 bi 和 wj 下的 MSE mse_matrix[i, j] = los_sum / 3 # 通过 meshgrid 方法,将权重 w 和截距 b 变成二维坐标点 # 就是将行 w 和列 b,w 作为行向量,复制扩展成 len(b_list) 行的二维矩阵 X # b 作为列向量,复制扩展成 len(w_list) 列的二维矩阵 Y # 这样,它们对应位置(X[i, j], Y[i, j])就对应参数集合(w_list[j], b_list[i]) # 就是相应的二维平面坐标点 X, Y = np.meshgrid(w_list, b_list) # 之前用 mse_matrix[i, j] = los_sum / 3 # 也是通过坐标方式,定位和记录相应点的 MSE # 这些这么做都是为了下面的绘图需要 Z = mse_matrix # 绘制 3d 曲面图 fig = plt.figure() ax = fig.add_subplot(projection="3d") ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) surf = ax.plot_surface(X, Y, Z, cmap="viridis") fig.colorbar(surf) plt.xlabel("w") plt.ylabel("b") plt.show() if __name__ == "__main__":     training()

运行结果: image.png

1.3 复盘

  1. 机器学习的基本过程和步骤。
  2. 训练集、开发集、测试集的各自作用。
  3. 过拟合、泛化能力的概念。
  4. 线性回归模型的公式。
  5. 线性回归模型的平方误差、均方误差公式。
  6. 损失函数、成本函数的概念和区别,以及其与模型训练的关系。
  7. 代码实现简要的模型训练流程,绘图查看位置参数与成本函数之间的关系。

2 梯度下降算法

2.1 基本梯度下降算法学习

权重参数的数量增多,搜索量会指数级增长,为了避免这种情况,使用分治法。

分治法的思路是,在初始时,划分区间,例如横轴四个区间,纵轴四个区间,那只要在这四个区间,找最小值的点。找到这个点以后,在这个点周围,继续划分区间,找这些区间的最小值,以此类推。

好处:能够减少计算量。

弊端:适用于光滑凸函数,但实际的权重参数与成本函数曲线不是光滑的,分治法可能会找到某个位置的最小值,而不是全局的最小值。并且实际的权重参数很多,不止一个,划分的区间可能很多,导致计算量突增。

梯度:成本函数(或损失函数)对权重参数的导数,梯度的公式:

grad=costωgrad = \frac{\partial cost}{\partial \omega }

由于导数就是切线斜率,并且我们希望成本函数的结果最小,所以我们必须往梯度递减的方向进行,也就是导数是负数的方向。梯度下降算法(GDA,Gradient Descent Algorithm)的基本公式:

ω=ωαcostω\omega = \omega - \alpha \frac{\partial cost}{\partial \omega}

其中 α\alpha 称为学习率。为了防止权重跳跃过大,α\alpha 需要取很小的值。梯度下降算法使用了一种思想,就是只看眼前获益最大的结果,也就是贪心思想

而贪心算法的弊端和分治法一样,不一定能找到全局最优点,可能会找到局部最优点

这里需要了解一下凸函数:简单来说,在函数内,任意两点的连线,其线段的任意点,都在函数图像上方,那么这个函数就是凸函数。

非凸函数,其实就是不满足凸函数定义的,就是非凸函数,它可能是一个波浪形的曲线。

对于非凸函数,可能存在多个最低点(通常都是一阶导数为 0,二阶导数大于 0),这些最低点就是局部最优点

在这个函数定义域中,如果没有一个局部最优点的函数值,比该点还小,这个点就是全局最优点

有个很特殊的点,这个点的一阶导数为零,二阶导数可能是大于零,也可能是小于零。在三维上,这个点在某个切面上是最小值,而在零一个方向上的切面,却是最大值。这个特殊点叫做鞍点

鞍点是比局部最优点更难解决的问题,因为其导数为 0,也就是梯度为 0,可能会导致梯度无法下降,训练无法继续。

接下来,使用梯度下降算法,推导之前的公式: y^=xω\hat{y} = x · \omega 的梯度下降公式: image.png

使用这个公式,编写一段利用梯度下降训练模型的基本代码:

python
# 训练集 x_data = [1.0, 2.0, 3.0] y_data = [2.0, 4.0, 6.0] # 线性回归模型 def forward(x, w): return x * w # 成本函数 def cost(x_data, y_data, w): mse = 0 for x_val, y_val in zip(x_data, y_data): y_pred = forward(x_val, w) loss = (y_pred - y_val) ** 2 mse += loss return mse / len(x_data) # 梯度计算 def gradient(x_data, y_data, w): w_grad = 0 for x_val, y_val in zip(x_data, y_data): w_grad += 2 * x_val * (x_val * w - y_val) return w_grad / len(x_data) # 训练 def training(): # 确定预测值 w = 1.0 a = 0.01 # 训练 100 轮 for i in range(100): # 计算成本(输出用)以及梯度 cost_val = cost(x_data, y_data, w) grad_val = gradient(x_data, y_data, w) # 梯度下降 w -= a * grad_val print(f"Epoch: {i}, w = {w:.6f}, cost = {cost_val:.6f}") # 测试模型最终结果 print(f"Predict(after training) >>> input = 4, output = {forward(4, w):.6f}") if __name__ == "__main__": training()

运行结果: image.png

可以看到,100轮的训练,结果还略有偏差,把轮次加到 1000 轮看看: image.png

训练轮次还是模型最终性能有影响的。把轮次和成本的关系图绘制出来,更能直观的感受: image.png

但是,实际的训练过程中,轮次与成本并不会是这样光滑的曲线,而是有摆动的,就像股票的线图一样,对于这样的先,我们可以用指数加权均值来平滑它:

c0=c0,ci=βci+(1β)ci1c'_0 = c_0 , c'_i = \beta c_i + \left( 1 - \beta \right)c'_{i-1}

其中,cc' 指的是平均后的损失值。

2.2 随机梯度下降算法学习

上述的梯度下降,取的是所有成本的平均损失,而随机梯度下降(SGD,Stochastic Gradient Descent)的思路是,从提供的 N 个样本中,随机选择一个样本的损失,对权重求导更新。基本的公式是:

ω=ωαlossω\omega = \omega - \alpha \frac{ \partial loss }{ \partial \omega }

由于只使用一个样本,所以不需要累加求平均,对于之前的线性回归模型,其梯度公式变成了:

lossnω=2x(xnωyn)\frac{ \partial loss_n }{ \partial \omega } = 2x \left( x_n \omega - y_n \right)

接下来,继续使用代码实现,使用随机梯度下降的方法进行模型训练。由于本身数据量小,就不做真正随机了,使用顺序执行。

python
# 数据集 x_data = [1.0, 2.0, 3.0] y_data = [2.0, 4.0, 6.0] # 线性回归模型 def forward(x, w): return x * w # 损失函数(由于只计算单个样本,不需要使用均方误差) def loss(x, y, w): y_pred = forward(x, w) return (y_pred - y) ** 2 # 梯度计算 def gradient(x, y, w): return 2 * x * (x * w - y) # 训练 def training(): # 预测权重 w = 1.0 a = 0.01 # 训练 100 轮 for i in range(100): # 随机取样(由于数据量小,这里就顺序取样) for x_val, y_val in zip(x_data, y_data): # 计算损失值(打印用) loss_val = loss(x_val, y_val, w) # 计算梯度 grad_val = gradient(x_val, y_val, w) # 梯度下降 w -= a * grad_val print(f"Epoch: {i}, w = {w:.6f}, cost = {loss_val:.6f}") # 测试模型最终结果 print(f"Predict(after training) >>> input = 4, output = {forward(4, w):.6f}") if __name__ == "__main__": training()

运行结果: image.png

这用 100 轮就达到了之前 1000 轮的效果,优势还是很明显的,这里将损失与轮次的关系图绘制一下: image.png

可以看到,这里的图出现了锯齿,主要原因是在训练时,对于每一轮,都做了三次取样,进行了三次梯度下降,所以导致一轮出现三个损失值。这里尝试使用指数加权均值平滑一下,当然,更好的做法是将这三次损失值做平均,作为该轮的损失值: image.png

平滑代码:

python
def smooth(loss_list): c_list = [] c_list.append(loss_list[0]) beta = 0.1 for i in range(len(loss_list)): if i > 0: c_current = beta * loss_list[i] + (1 - beta) * c_list[i - 1] c_list.append(c_current) return c_list

2.3 两个梯度下降算法的比较

基础的梯度下降算法是这样的:

ω=ωαcostω\omega = \omega - \alpha \frac{\partial cost}{\partial \omega}

随机梯度下降算法是这样的:

ω=ωαlossω\omega = \omega - \alpha \frac{ \partial loss }{ \partial \omega }

区别在于,基础的梯度下降使用成本函数,针对整个样本损失值,求和取均值,作为需要求导的原函数;而随机梯度下降算法针对的是单个随机取样的样本,使用损失函数计算单个样本的损失值,作为需要求导的原函数。

在实际训练时,由于梯度下降是针对整个样本的损失值,计算的是整个样本的均方误差,所以在计算梯度时,我们可以将多个样本的梯度计算,并行执行,然后将最终结果汇总计算平均,提高运行效率,减少运行时间。在此之后,就可以使用这个梯度,计算新的权重,继续下一轮训练。

而随机梯度是针对单个随机取样到的样点,虽然计算梯度是独立的,但在计算权重时,你需要知道上一个权重的值。而上一个权重的值,是由上一个随机取样点计算出来的。也就是说,随机取样中,每个权重的计算是串行的,是有依赖关系的,所以是不能并行执行的。这就使得随机梯度在样本量大的情况下的时间复杂度更高。

从之前的代码运行中可以看到了,随机梯度在达到最优解时,使用的轮次少,性能高;而梯度下降使用的轮次多,性能低。但在实际大样本量计算时,由于随机梯度不可并行,计算的时间复杂度更高,而可并行的梯度下降的时间复杂度会相对低。

为了解决这个问题,就采用了一种介于两者之间的方案,叫做 Mini-Batch。Mini-Batch 的思路就是,将若干个样本作为一组,作为一个 Batch,进行梯度计算。

机器学习中 SGD (随机梯度下降),默认就是 Mini-Batch 方案。

2.4 复盘

  1. 基本的梯度下降算法公式
  2. 随机梯度下降算法公式
  3. 指数加权均值公式
  4. 两个梯度下降算法的比较。
  5. Mini-Batch 的思路。

3 反向传播

3.1 计算图、正向传播与反向传播

我们前面学习的线性回归模型,是一个非常基础简单的模型,只有一两个权重参数,可以轻松的推导出相应的损失函数和梯度。

但是在实际训练过程中,模型的权重参数不止有一两个,有非常非常多的权重参数。假设输入 x 是一个 m 维的向量,输出 y 是一个 n 维的向量,用简单的线性回归模型:y^=xω\hat{y} = x·\omega ,我们的 ω\omega 就需要是一个 m x n 维的矩阵,就可能需要计算 m x n 个权重参数。

如果对于每个权重参数,我们都一个个写解析式,求导求梯度,难度非常非常大。

所以,这里就学习一个名为反向传播的思路,将每个神经网络节点当作是一个原子计算操作(计算图),可以正向求出损失值后,反向得出梯度并传递过来,不需要写繁琐的解析式。

对于一个两层的神经网络,其计算图如图右侧所示。 image.png

x 是输入,y 是输出,w 和 b 都是权重参数。MM 是矩阵乘法,ADD 是矩阵加法,中间的空心圆表示中间结果。

后续的正向传播,反向传播都是以计算图为基础,其计算过程如下图所示: image.png

从图中,我们可以明确两个信息:

  1. 正向传播(蓝色箭头方向)的主要目的是计算模型的预期输出以及误差。通过预期输出,传递给损失函数后,就可以得到损失值,或者说误差值。
  2. 反向传播(红色箭头方向)的主要目的是更新权重,通过计算损失函数与预期输出值的偏导数,也称输出的梯度。然后逐步计算局部梯度,得到最终梯度,最终通过梯度来更新权重。

也就是说:

  • 正向传播,传递方向是: (x,ωzLoss)\left ( x, \omega \rightarrow z \rightarrow Loss \right),目的是算误差。
  • 反向传播,传递方向是: (Losszω)\left ( Loss \rightarrow z \rightarrow \omega \right),目的是更新权重。

模型通过这两个过程的交替进行,每次正向传播算出损失,反向传播更新权重,反复循环直到模型的损失收敛到最小。

正向传播,依赖于解析式,例如上述的线性回归模型,输入值得出预期输出,并传递给损失函数计算误差。 反向传播,依赖的就是损失函数和解析式,因为计算梯度需要知道损失函数(或成本函数)与权重参数的导数:Lω\frac{ \partial L }{ \partial \omega }。而我们反向传播的过程,就是在通过预期输出 z 以及导数的链式法则,计算出这个导数:

Lω=Lzzω\frac{ \partial L }{ \partial \omega } = \frac{ \partial L }{ \partial z } · \frac{ \partial z }{ \partial \omega }

代入值,就是这样的: image.png

更加详细的计算过程: image.png

我们来计算下面这个练习题: image.png

  1. 正向传播
  • 第一步:ωx=11=1\omega x = 1 * 1 = 1,所以 z1z_1 为 1,导数:z1ω=x\frac{ \partial z_1 }{ \partial \omega } = x
  • 第二步:z1+b=1+2=3z_1 + b = 1 + 2 = 3 ,所以 y^=3\hat{y} = 3,导数: y^z1=1\frac{ \partial \hat{y} }{ \partial z_1 } = 1y^b=1\frac{ \partial \hat{y} }{ \partial b } = 1
  • 第三步:loss(y^,y)=(xω+by)2=(11+22)2=1loss\left(\hat{y}, y \right) = \left(x \omega + b - y \right)^2 = \left(1·1+2-2 \right)^2 = 1 ,所以 loss=1loss = 1,导数:Ly^=2(y^y)\frac{ \partial L }{ \partial \hat{y} } = 2 \left(\hat{y} - y \right)
  1. 反向传播
  • 第一步:计算 losslossy^\hat{y} 的导数:Ly^=2(y^y)=2(32)=2\frac{ \partial L }{ \partial \hat{y} } = 2 \left(\hat{y} - y \right) = 2 · \left(3 - 2 \right) = 2
  • 第二步:计算 losslossbb 的导数: Lb=Ly^y^b=21=2\frac{ \partial L }{ \partial b } = \frac{ \partial L }{ \partial \hat{y} } · \frac{ \partial \hat{y} }{ \partial b } = 2 · 1 = 2,所以 lossb=2\frac{ \partial loss }{ \partial b } = 2
  • 第三步:计算 losslossz1z_1 的导数:Lz1=Ly^y^z1=21=2\frac{ \partial L }{ \partial z_1 } = \frac{ \partial L }{ \partial \hat{y} } · \frac{ \partial \hat{y} }{ \partial z_1 } = 2 · 1 = 2
  • 第四步:计算 losslossω\omega 的导数:Lω=Lz1z1ω=2x=21=2\frac{ \partial L }{ \partial \omega } = \frac{ \partial L }{ \partial z_1 } · \frac{ \partial z_1 }{ \partial \omega } = 2 · x = 2 · 1 = 2。所以 lossω=2\frac{ \partial loss }{ \partial \omega } = 2

结果是 2,2 。

3.2 代码实现

这里使用 PyTorch 来进行正向反向传播的使用。

在进行代码编写之前,先要学习 PyTorch 的基础组件,张量(Torch)。

Torch 是一个类,用于动态构建计算图,它包含两个很重要的属性,就是权重参数和梯度(损失函数对权重的导数)。

所以 Torch 在构建计算图,执行反向传播的时候,其权重的修改和梯度都保存在当前的 Torch 中。

下面是基于线性回归模型,利用正向传播、反向传播来训练模型的代码:

python
import torch # 数据集 x_data = [1.0, 2.0, 3.0] y_data = [2.0, 4.0, 6.0] # 正向传播的线性回归模型 def forward(x, w) -> torch.Tensor: return x * w # 损失函数 def loss(x, y, w) -> torch.Tensor: y_pred = forward(x, w) return (y_pred - y) ** 2 # 训练 def training(): # 定义张量 w = torch.Tensor([1.0]) # 这里要启用梯度计算 w.requires_grad = True a = 0.01 # 进行 100 轮训练 for epoch in range(100): print("Epoch-", epoch) # 进行权重更新 for x_val, y_val in zip(x_data, y_data): # 利用损失值和反向传播计算梯度 loss_val = loss(x_val, y_val, w) loss_val.backward() # 更新梯度 w.data -= a * w.grad.data # type: ignore # 我们不需要梯度累加,所以更新权重后要将梯度清零 w.grad.data.zero_() # type: ignore print("\t w = ", w.item()) # 进行预测,判断模型训练结果 print(f"Predict: x = 4, y = {forward(4, w).item()}") if __name__ == "__main__": training()

由于 w 是张量,并且 Torch 是用于动态构建计算图的,所以我们的 forward 函数和 loss 函数,其运算符会被重载为张量计算。由于进行的是张量计算,所以 x * w(y_pred - y) ** 2 本质上不是在进行张量值的计算,而是在构建计算图

需要计算 w 的梯度,就需要利用计算图进行反向传播,于是调用了 loss_val.backward(),就能得到 w 的梯度。

==当看见张量之间进行运算符运算时,请不要认为是数值计算,它们是在构建计算图。==你的脑中要自动的联想出这个计算式的计算图。

从代码中,还可以看到,在获取张量值时,既有用 .data,也有用 .item(),其实最好的做法是用 .item().data 用于更新,而 .item() 用于获取。

运行结果: image.png

接下来,我们做下面这个练习,训练集使用上述的训练集: image.png

代码:

python
from torch import Tensor # 训练集 x_data = [1.0, 2.0, 3.0] y_data = [2.0, 4.0, 6.0] # 模型 def forward(x: float, w_1: Tensor, w_2: Tensor, b: Tensor) -> Tensor: return w_1 * (x**2) + w_2 * x + b # 损失函数 def loss(x: float, y: float, w_1: Tensor, w_2: Tensor, b: Tensor) -> Tensor: y_pred = forward(x, w_1, w_2, b) return (y_pred - y) ** 2 # 更新权重的方法 def updateWeight(des: Tensor, a: float) -> Tensor: return des.data - a * des.grad.data # type: ignore # 训练 def training(): # 猜测权重值 w_1 = Tensor([-1.0]) w_2 = Tensor([1.0]) b = Tensor([-1.0]) a = 0.01 w_1.requires_grad = True w_2.requires_grad = True b.requires_grad = True # 训练 1000 轮 for epoch in range(1000): print(f"Epoch - {epoch}") # 更新权重 for x_val, y_val in zip(x_data, y_data): # 构建计算图,反向传播 loss_val = loss(x_val, y_val, w_1, w_2, b) loss_val.backward() # 更新权重 w_1.data = updateWeight(w_1, a) w_2.data = updateWeight(w_2, a) b.data = updateWeight(b, a) print(f"\tGrad-w_1: {w_1.grad.data.item():.6f}") # type: ignore print(f"\tGrad-w_2: {w_2.grad.data.item():.6f}") # type: ignore print(f"\tGrad-b: {b.grad.data.item():.6f} \n") # type: ignore # 清空梯度 w_1.grad.data.zero_() # type: ignore w_2.grad.data.zero_() # type: ignore b.grad.data.zero_() # type: ignore print(f"\n\tw_1: {w_1.item():.6f}") print(f"\tw_2: {w_2.item():.6f}") print(f"\tb: {b.item():.6f}") # 预测新输入 print(f"Predict: x = 4.0, y = {forward(4.0, w_1, w_2, b).item():.6f}") if __name__ == "__main__": training()

运行结果: image.png

我们把轮次增大到 10000 轮: image.png

3.3 复盘

  1. 计算图、正向传播、反向传播的关系与设计目的。
  2. 以线性回归模型为例,正向传播与反向传播的计算过程。
  3. PyTorch 张量(Torch)的两个重要属性
  4. 在 PyTorch 使用正向传播、反向传播训练模型的方法

4 PyTorch 实现线性回归

4.1 前置说明

在进行代码实现前,需要知道我们的目标是什么。

我们的目标就是基于线性回归模型,利用 PyTorch 进行模型训练,或者说权重更新。

在 1.1 中,说明了机器学习的四个主要步骤要求:

  1. 获取数据集 DataSet
  2. 选择模型 Model
  3. 训练 Training
  4. 推理 Inferring

现在的模型训练,也是基于这四个要求,只不过需要进行一些修改:

  1. 准备数据集
  2. 设计模型
  3. 构建损失函数和优化器
  4. 训练
  5. 推理

在之前的代码实现中,使用的数据集都是数组形式。但是在 PyTorch 中,基本的运算都是使用 Torch 进行。

所以,在 PyTorch 中,我们需要将思路从普通的数组运算,转向线性代数,我们要以标量、向量、矩阵的思路进行运算。

张量、向量、数组之间有什么关系?

  • 标量(Scalar):0 维数组。比如 1、2、3 这种单独的数字。
  • 向量(Vector):1 维数组。一列数,比如 [1, 2, 3, 4],形状是 (4,)
  • 矩阵(Matrix):2 维数组。一张表格,有行有列,形状类似 (3, 4)
  • 张量(Tensor):3 维以上的数组,所有高维数据的统称,形状类似 (2, 3, 4)

所以说,向量是 1 维的张量,矩阵是 2 维的张量,张量是它们的通用说法,而数组则是它们在计算机上的表示方式。

怎么转向线性代数呢?其实就是要把我们的训练集,看成一个 3 x 1 的矩阵。而线性回归模型的公式是这样的:

y^=ωx+b\hat{y} = \omega · x + b

已知输入 x 和输出 y 都是 3 x 1 的矩阵,那 ω\omegabb 是什么矩阵?没错,ω\omega 需要是一个 3 x 3 的矩阵,而 bb 需要是一个 3 x 1 的矩阵。

在其他教材中,可能会将线性回归模型的公式变成如下两个样子:

y^=xω+b\hat{y} = x·\omega + b y^=ωTx+b\hat{y} = \omega^T · x + b

这两个的区别很简单,当 y^\hat{y}xx 是一个 m x n 的矩阵,对于第一个公式,ω\omega 就是需要是一个 n x n 的矩阵,而 b 需要是 m x n 的矩阵;对于第二个公式,其实是教材默认将 y^\hat{y}xx 做了转置,变成了 n x m 的矩阵,所以 ω\omega 就需要做一个转置。

无论它们是什么样子,最终的目的都是让输入参数和输出结果的维度保持一致。

此外,我们还需要理解 PyTorch 中的广播机制。例如 a 是一个 3 x 1 的矩阵,b 是一个 1 x 2 的矩阵,在进行 a + b 等运算时,PyTorch 会自动将 b 的每一行复制,变成 3 x 2 矩阵进行运算。

4.2 代码实现

把思路拉回到我们的目的:基于线性回归模型,利用 PyTorch 进行模型训练,或者说权重更新。

需要进行以下几个步骤:

  1. 准备数据集 对于数据的准备,目前暂时没有学到,就以之前的三个数据为训练集。
  2. 设计模型 在 PyTorch 中,我们不需要手动求导算梯度,我们的目的是根据模型,构造出模型的计算图。
  3. 构建损失函数和优化器 在 PyTorch 中,可以使用其提供的 API 来直接构建损失函数(SGD)和优化器(权重更新)。
  4. 训练 确定好训练轮次,利用 PyTorch 的正向传播和反向传播进行权重更新,实现模型训练。
  5. 推理验证 提供一个新的输入给模型,让其求出预期结果,验证训练效果。

代码如下: 创建一个 models 包,创建一个 LinearModel.py 文件:

python
from torch import nn class LinearModel(nn.Module): def __init__(self) -> None: super(LinearModel, self).__init__() self.linear = nn.Linear(1, 1) # 这里手动重写了 forward 函数 def forward(self, x): y_pred = self.linear(x) return y_pred

和 models 包同级,创建一个 main.py 文件:

python
from models.LinearModel import LinearModel from torch import Tensor, nn, optim # 训练集 x_data = Tensor([[1.0], [2.0], [3.0]]) y_data = Tensor([[2.0], [4.0], [6.0]]) # 训练方法 def training(): # 准备模型 model = LinearModel() # 使用 MES 损失函数 criterion = nn.MSELoss(size_average=False) # 使用 SGD 更新权重 optimizer = optim.SGD(model.parameters(), lr=0.01) # 训练 1000 轮 for epoch in range(1000): # 计算预测值 y_pred = model(x_data) # 计算损失 loss = criterion(y_pred, y_data) print(f"Epoch - {epoch}, loss = {loss.item():.10f}") # 清空梯度 optimizer.zero_grad() # 反向传播 loss.backward() # 更新权重 optimizer.step() print(f"w = {model.linear.weight.item():.10f}") print(f"b = {model.linear.bias.item():.10f}") # 提供新输入给模型,测试训练效果 x_test = Tensor([[4.0]]) print(f"Predict: x = 4.0, y = {model(x_test).data}") if __name__ == "__main__": training()

运行结果: image.png

这里还有一个任务,就是 PyTorch 提供了很多个优化器:Adagrad、 Adam、 Adamax、 ASGD、 LBFGS、 RMSprop、 Rprop、 SGD。为了观察它们的性能,我们绘制它们的损失和训练轮次的曲线。

main.py 同级,新增一个 optim_compare.py 文件:

python
import matplotlib.pyplot as plt from models.LinearModel import LinearModel from torch import Tensor, nn, optim x_data = Tensor([[1.0], [2.0], [3.0]]) y_data = Tensor([[2.0], [4.0], [6.0]]) optimizer_names = [ "Adagrad", "Adam", "Adamax", "ASGD", "LBFGS", "RMSprop", "Rprop", "SGD", ] def getOptimizer(index: int, model: LinearModel): optimizers = [ optim.Adagrad(model.parameters(), lr=0.5), optim.Adam(model.parameters(), lr=0.01, betas=(0.5, 0.999)), optim.Adamax(model.parameters(), lr=0.01, betas=(0.5, 0.999)), optim.ASGD(model.parameters(), lr=0.01), optim.LBFGS(model.parameters(), lr=0.01), optim.RMSprop(model.parameters(), lr=0.01, alpha=0.5), optim.Rprop(model.parameters(), lr=0.01), optim.SGD(model.parameters(), lr=0.1), ] return optimizers[index] def training(optimizer, model: LinearModel) -> dict: criterion = nn.MSELoss() epoch_list, loss_list = [], [] for epoch in range(2000): if isinstance(optimizer, optim.LBFGS): def closure(): optimizer.zero_grad() y_pred = model(x_data) loss = criterion(y_pred, y_data) loss.backward() return loss loss = optimizer.step(closure) else: y_pred = model(x_data) loss = criterion(y_pred, y_data) optimizer.zero_grad() loss.backward() optimizer.step() epoch_list.append(epoch) loss_list.append(loss.item()) # type: ignore print( f"Weight: {model.linear.weight.data.item():.4f}, Bias: {model.linear.bias.data.item():.4f}" ) x_test = Tensor([[4.0]]) print(f"Predict: x = 4.0, y = {model(x_test).data}") return {"epoch": epoch_list, "loss": loss_list} def compare(): plt.figure(figsize=(15, 8)) for index in range(8): model = LinearModel() result = training(getOptimizer(index, model), model) plt.plot(result["epoch"], result["loss"], label=optimizer_names[index]) plt.legend(loc="best") plt.title("Loss Comparison of Optimizers") plt.xlabel("Epoch") plt.ylabel("Loss") plt.grid(True) plt.xlim(0, 200) plt.show() if __name__ == "__main__": compare()

我将一些优化器的初始化参数,调整到了最终的预测值,大致接近真实值的样子,所以每个优化器的初始化参数可能不同。 训练轮次均是 2000 轮,绘制图表时只会绘制前 200 轮。

运行结果: image.png

绘制出的曲线图: image.png

4.3 复盘

  • 使用 PyTorch 实现线性回归的步骤。
  • 张量、向量、矩阵、数组之间的区别。
  • 代码实现使用 PyTorch 实现线性回归的大致思路。

5 说明

作者: Xigrut发布时间: 2026-06-28 12:54:57上次编辑时间: 2026-06-30 15:21:05 许可协议: CC BY-NC-SA 4.0
留言区