7870 字
10 分钟
22 次
- 2026-06-28
1 线性模型
1.1 机器学习流程
机器学习的步骤要求:
- 获取数据集 DataSet
- 选择模型 Model
- 训练 Training
- 推理 Inferring
机器学习的通常步骤:
- 使用训练集训练模型
- 使用开发集(从训练集中划分出来)初步测试
- 使用测试集测试性能
- 得到最终的模型
- 输入新的数据给模型
- 得到预测结果
- 在训练时的拟合效果很好,但是在测试集却不是很好,可能是在训练时将噪声也进行识别了,导致过拟合。
- 在训练时的拟合效果很好,在测试集以及最终上线后,也能正确识别结果,说明模型具有很好的泛化能力。

1.2 线性回归模型学习
线性回归模型:
在模型训练中,对于需要计算实际值(预测值)与真实值之间的差距。对于线性回归模型来说,就是上面蓝色的线或者下面绿色的线,与中间红色的线的垂直高度差。单个样本的差距可以通过以下公式表示:
但为了在数学上便于求导和优化,通常来说,使用平方来代替绝对值,也就是叫做平方误差:
对于单个样本来说,这种函数称为模型的损失函数(Loss Function),针对的是单个样本。
对于包含了 N 个样本的整个训练集,通常取样本损失的平均值,称为均方误差(MSE ,Mean Square Error) ,也称为模型的成本函数(Cost Function):
我们通常通过一些算法让 Cost 尽可能小,不过这只是手段之一。最终目的是要让模型在未知的新数据上的误差尽可能小,也就是让模型的泛化能力更高。

使用一段代码,简单的了解一下模型的训练过程,以及在线性回归模型中,未知权重参数与均值误差之间的关系。
pythonimport 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()
运行结果:

作业,基于完整线性回归模型: ,由于有两个未知参数,即权重 w 和截距 b,所以需要绘制权重 w 和截距 b 与成本函数 Cost 的关系图。

实现代码:
pythonimport 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()
运行结果:

1.3 复盘
- 机器学习的基本过程和步骤。
- 训练集、开发集、测试集的各自作用。
- 过拟合、泛化能力的概念。
- 线性回归模型的公式。
- 线性回归模型的平方误差、均方误差公式。
- 损失函数、成本函数的概念和区别,以及其与模型训练的关系。
- 代码实现简要的模型训练流程,绘图查看位置参数与成本函数之间的关系。
2 梯度下降算法
2.1 基本梯度下降算法学习
权重参数的数量增多,搜索量会指数级增长,为了避免这种情况,使用分治法。
分治法的思路是,在初始时,划分区间,例如横轴四个区间,纵轴四个区间,那只要在这四个区间,找最小值的点。找到这个点以后,在这个点周围,继续划分区间,找这些区间的最小值,以此类推。
好处:能够减少计算量。
弊端:适用于光滑凸函数,但实际的权重参数与成本函数曲线不是光滑的,分治法可能会找到某个位置的最小值,而不是全局的最小值。并且实际的权重参数很多,不止一个,划分的区间可能很多,导致计算量突增。
梯度:成本函数(或损失函数)对权重参数的导数,梯度的公式:
由于导数就是切线斜率,并且我们希望成本函数的结果最小,所以我们必须往梯度递减的方向进行,也就是导数是负数的方向。梯度下降算法(GDA,Gradient Descent Algorithm)的基本公式:
其中 称为学习率。为了防止权重跳跃过大, 需要取很小的值。梯度下降算法使用了一种思想,就是只看眼前获益最大的结果,也就是贪心思想。
而贪心算法的弊端和分治法一样,不一定能找到全局最优点,可能会找到局部最优点。
这里需要了解一下凸函数:简单来说,在函数内,任意两点的连线,其线段的任意点,都在函数图像上方,那么这个函数就是凸函数。
非凸函数,其实就是不满足凸函数定义的,就是非凸函数,它可能是一个波浪形的曲线。
对于非凸函数,可能存在多个最低点(通常都是一阶导数为 0,二阶导数大于 0),这些最低点就是局部最优点。
在这个函数定义域中,如果没有一个局部最优点的函数值,比该点还小,这个点就是全局最优点。
有个很特殊的点,这个点的一阶导数为零,二阶导数可能是大于零,也可能是小于零。在三维上,这个点在某个切面上是最小值,而在零一个方向上的切面,却是最大值。这个特殊点叫做鞍点。
鞍点是比局部最优点更难解决的问题,因为其导数为 0,也就是梯度为 0,可能会导致梯度无法下降,训练无法继续。
接下来,使用梯度下降算法,推导之前的公式: 的梯度下降公式:

使用这个公式,编写一段利用梯度下降训练模型的基本代码:
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()
运行结果:

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

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

但是,实际的训练过程中,轮次与成本并不会是这样光滑的曲线,而是有摆动的,就像股票的线图一样,对于这样的先,我们可以用指数加权均值来平滑它:
其中, 指的是平均后的损失值。
2.2 随机梯度下降算法学习
上述的梯度下降,取的是所有成本的平均损失,而随机梯度下降(SGD,Stochastic Gradient Descent)的思路是,从提供的 N 个样本中,随机选择一个样本的损失,对权重求导更新。基本的公式是:
由于只使用一个样本,所以不需要累加求平均,对于之前的线性回归模型,其梯度公式变成了:
接下来,继续使用代码实现,使用随机梯度下降的方法进行模型训练。由于本身数据量小,就不做真正随机了,使用顺序执行。
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()
运行结果:

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

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

平滑代码:
pythondef 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 两个梯度下降算法的比较
基础的梯度下降算法是这样的:
随机梯度下降算法是这样的:
区别在于,基础的梯度下降使用成本函数,针对整个样本损失值,求和取均值,作为需要求导的原函数;而随机梯度下降算法针对的是单个随机取样的样本,使用损失函数计算单个样本的损失值,作为需要求导的原函数。
在实际训练时,由于梯度下降是针对整个样本的损失值,计算的是整个样本的均方误差,所以在计算梯度时,我们可以将多个样本的梯度计算,并行执行,然后将最终结果汇总计算平均,提高运行效率,减少运行时间。在此之后,就可以使用这个梯度,计算新的权重,继续下一轮训练。
而随机梯度是针对单个随机取样到的样点,虽然计算梯度是独立的,但在计算权重时,你需要知道上一个权重的值。而上一个权重的值,是由上一个随机取样点计算出来的。也就是说,随机取样中,每个权重的计算是串行的,是有依赖关系的,所以是不能并行执行的。这就使得随机梯度在样本量大的情况下的时间复杂度更高。
从之前的代码运行中可以看到了,随机梯度在达到最优解时,使用的轮次少,性能高;而梯度下降使用的轮次多,性能低。但在实际大样本量计算时,由于随机梯度不可并行,计算的时间复杂度更高,而可并行的梯度下降的时间复杂度会相对低。
为了解决这个问题,就采用了一种介于两者之间的方案,叫做 Mini-Batch。Mini-Batch 的思路就是,将若干个样本作为一组,作为一个 Batch,进行梯度计算。
机器学习中 SGD (随机梯度下降),默认就是 Mini-Batch 方案。
2.4 复盘
- 基本的梯度下降算法公式
- 随机梯度下降算法公式
- 指数加权均值公式
- 两个梯度下降算法的比较。
- Mini-Batch 的思路。
3 反向传播
3.1 计算图、正向传播与反向传播
我们前面学习的线性回归模型,是一个非常基础简单的模型,只有一两个权重参数,可以轻松的推导出相应的损失函数和梯度。
但是在实际训练过程中,模型的权重参数不止有一两个,有非常非常多的权重参数。假设输入 x 是一个 m 维的向量,输出 y 是一个 n 维的向量,用简单的线性回归模型: ,我们的 就需要是一个 m x n 维的矩阵,就可能需要计算 m x n 个权重参数。
如果对于每个权重参数,我们都一个个写解析式,求导求梯度,难度非常非常大。
所以,这里就学习一个名为反向传播的思路,将每个神经网络节点当作是一个原子计算操作(计算图),可以正向求出损失值后,反向得出梯度并传递过来,不需要写繁琐的解析式。
对于一个两层的神经网络,其计算图如图右侧所示。

x 是输入,y 是输出,w 和 b 都是权重参数。MM 是矩阵乘法,ADD 是矩阵加法,中间的空心圆表示中间结果。
后续的正向传播,反向传播都是以计算图为基础,其计算过程如下图所示:

从图中,我们可以明确两个信息:
- 正向传播(蓝色箭头方向)的主要目的是计算模型的预期输出以及误差。通过预期输出,传递给损失函数后,就可以得到损失值,或者说误差值。
- 反向传播(红色箭头方向)的主要目的是更新权重,通过计算损失函数与预期输出值的偏导数,也称输出的梯度。然后逐步计算局部梯度,得到最终梯度,最终通过梯度来更新权重。
也就是说:
- 正向传播,传递方向是: ,目的是算误差。
- 反向传播,传递方向是: ,目的是更新权重。
模型通过这两个过程的交替进行,每次正向传播算出损失,反向传播更新权重,反复循环直到模型的损失收敛到最小。
正向传播,依赖于解析式,例如上述的线性回归模型,输入值得出预期输出,并传递给损失函数计算误差。 反向传播,依赖的就是损失函数和解析式,因为计算梯度需要知道损失函数(或成本函数)与权重参数的导数:。而我们反向传播的过程,就是在通过预期输出 z 以及导数的链式法则,计算出这个导数:
代入值,就是这样的:

更加详细的计算过程:

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

- 正向传播
- 第一步:,所以 为 1,导数: 。
- 第二步: ,所以 ,导数: 、 。
- 第三步: ,所以 ,导数:。
- 反向传播
- 第一步:计算 对 的导数:。
- 第二步:计算 对 的导数: ,所以 。
- 第三步:计算 对 的导数:。
- 第四步:计算 对 的导数:。所以 。
结果是 2,2 。
3.2 代码实现
这里使用 PyTorch 来进行正向反向传播的使用。
在进行代码编写之前,先要学习 PyTorch 的基础组件,张量(Torch)。
Torch 是一个类,用于动态构建计算图,它包含两个很重要的属性,就是权重参数和梯度(损失函数对权重的导数)。
所以 Torch 在构建计算图,执行反向传播的时候,其权重的修改和梯度都保存在当前的 Torch 中。
下面是基于线性回归模型,利用正向传播、反向传播来训练模型的代码:
pythonimport 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()用于获取。
运行结果:

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

代码:
pythonfrom 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()
运行结果:

我们把轮次增大到 10000 轮:

3.3 复盘
- 计算图、正向传播、反向传播的关系与设计目的。
- 以线性回归模型为例,正向传播与反向传播的计算过程。
- PyTorch 张量(Torch)的两个重要属性
- 在 PyTorch 使用正向传播、反向传播训练模型的方法
4 PyTorch 实现线性回归
4.1 前置说明
在进行代码实现前,需要知道我们的目标是什么。
我们的目标就是基于线性回归模型,利用 PyTorch 进行模型训练,或者说权重更新。
在 1.1 中,说明了机器学习的四个主要步骤要求:
- 获取数据集 DataSet
- 选择模型 Model
- 训练 Training
- 推理 Inferring
现在的模型训练,也是基于这四个要求,只不过需要进行一些修改:
- 准备数据集
- 设计模型
- 构建损失函数和优化器
- 训练
- 推理
在之前的代码实现中,使用的数据集都是数组形式。但是在 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 的矩阵。而线性回归模型的公式是这样的:
已知输入 x 和输出 y 都是 3 x 1 的矩阵,那 和 是什么矩阵?没错, 需要是一个 3 x 3 的矩阵,而 需要是一个 3 x 1 的矩阵。
在其他教材中,可能会将线性回归模型的公式变成如下两个样子:
这两个的区别很简单,当 和 是一个 m x n 的矩阵,对于第一个公式, 就是需要是一个 n x n 的矩阵,而 b 需要是 m x n 的矩阵;对于第二个公式,其实是教材默认将 和 做了转置,变成了 n x m 的矩阵,所以 就需要做一个转置。
无论它们是什么样子,最终的目的都是让输入参数和输出结果的维度保持一致。
此外,我们还需要理解 PyTorch 中的广播机制。例如 a 是一个 3 x 1 的矩阵,b 是一个 1 x 2 的矩阵,在进行 a + b 等运算时,PyTorch 会自动将 b 的每一行复制,变成 3 x 2 矩阵进行运算。
4.2 代码实现
把思路拉回到我们的目的:基于线性回归模型,利用 PyTorch 进行模型训练,或者说权重更新。
需要进行以下几个步骤:
- 准备数据集 对于数据的准备,目前暂时没有学到,就以之前的三个数据为训练集。
- 设计模型 在 PyTorch 中,我们不需要手动求导算梯度,我们的目的是根据模型,构造出模型的计算图。
- 构建损失函数和优化器 在 PyTorch 中,可以使用其提供的 API 来直接构建损失函数(SGD)和优化器(权重更新)。
- 训练 确定好训练轮次,利用 PyTorch 的正向传播和反向传播进行权重更新,实现模型训练。
- 推理验证 提供一个新的输入给模型,让其求出预期结果,验证训练效果。
代码如下: 创建一个 models 包,创建一个 LinearModel.py 文件:
pythonfrom 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 文件:
pythonfrom 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()
运行结果:

这里还有一个任务,就是 PyTorch 提供了很多个优化器:Adagrad、 Adam、 Adamax、 ASGD、 LBFGS、 RMSprop、 Rprop、 SGD。为了观察它们的性能,我们绘制它们的损失和训练轮次的曲线。
和 main.py 同级,新增一个 optim_compare.py 文件:
pythonimport 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 轮。
运行结果:

绘制出的曲线图:

4.3 复盘
- 使用 PyTorch 实现线性回归的步骤。
- 张量、向量、矩阵、数组之间的区别。
- 代码实现使用 PyTorch 实现线性回归的大致思路。
5 说明
- 视频地址:《PyTorch深度学习实践》完结合集
- 我的笔记中的 demo 代码:NongXC_12-deeplearning_demo