文章目录
- 1 案例介绍
- 1.1 应用场景:美国房价预测
- 1.2 核心假设
- 1.3 线性回归与神经网络的关系
- 1.4 平方损失(L2 Loss)
- 1.5 训练模型:最小化损失
- 2 基础优化算法
- 2.1 梯度下降
- 2.2 小批量随机梯度下降(Mini-batch SGD)
- 3 实战:线性回归的从零实现
- 3.1 生成数据集
- 3.2 读取数据集
- 3.3 初始化模型参数
- 3.4 定义模型
- 3.5 定义损失函数
- 3.6 优化算法
- 3.7 训练
- 4 实战:线性回归的简易实现
- 4.1 实现
- 4.2 练习
硬件配置:
- Windows 11
- Intel®Core™i7-12700H
- NVIDIA GeForce RTX 3070 Ti Laptop GPU
软件环境:
- Pycharm 2025.1
- Python 3.12.9
- Pytorch 2.6.0+cu124

1 案例介绍
1.1 应用场景:美国房价预测
在美国买房时,买家需根据房屋信息(如卧室数量、卫生间数量、面积等)预测合理的成交价。
- 输入数据:房屋特征(如
X1=卧室数
,X2=卫生间数
,X3=面积
)。 - 输出目标:预测成交价
Y
。 - 实际挑战:卖家的标价和网站估价(如Redfin)仅为参考,最终需通过竞价决定成交价,因此准确预测至关重要。
1.2 核心假设
-
房价由关键特征的加权和决定:
Y = W 1 X 1 + W 2 X 2 + W 3 X 3 + b Y=W_1X_1+W_2X_2+W_3X_3+b Y=W1X1+W2X2+W3X3+bW1, W2, W3
:权重(反映各特征对价格的影响)。b
:偏差项(基础价格,如地段附加值)。

-
一般化形式(N 维输入):
Y = W 1 X 1 + W 2 X 2 + ⋯ + W N X N + b Y=W_1X_1+W_2X_2+\cdots+W_NX_N+b Y=W1X1+W2X2+⋯+WNXN+b- 向量表示:
Y = W^T X + b
(W
为权重向量,X
为特征向量)。
- 向量表示:

1.3 线性回归与神经网络的关系
单层神经网络:线性回归可视为最简单的神经网络。

- 结构:输入层(特征)→ 输出层(预测值),无隐藏层。
- 权重
W
对应神经元的连接强度,b
对应激活阈值。 - 类比生物神经元:输入信号加权求和后,若超过阈值则输出信号。

1.4 平方损失(L2 Loss)
L = 1 2 ( Y − Y ^ ) 2 L=\frac{1}{2}(Y-\hat{Y})^2 L=21(Y−Y^)2
-
Y
:真实成交价,Ŷ
:模型预测价。 -
作用:量化预测误差,误差越小模型越准。
-
为什么用1/2?
求导时简化计算(导数变为
Y - Ŷ
)

1.5 训练模型:最小化损失
- 训练数据:收集历史成交记录(如过去 6 个月的房屋数据)。
- 数据矩阵
X
(每行一个样本,每列一个特征),向量Y
(真实价格)。
- 数据矩阵

- 损失函数(全体数据):
- 目标:找到
W
和b
使L(W, b)
最小。
- 目标:找到
L ( W , b ) = 1 2 N ∑ i = 1 N ( Y i − Y ^ i ) 2 L(W,b)=\frac1{2N}\sum_{i=1}^N(Y_i-\hat{Y}_i)^2 L(W,b)=2N1i=1∑N(Yi−Y^i)2

-
求解方法:
-
闭式解(显示解):仅线性回归等简单模型存在。
W ∗ = ( X T X ) − 1 X T Y W^*=(X^TX)^{-1}X^TY W∗=(XTX)−1XTY- 通过矩阵运算直接计算最优权重。
-
凸函数性质:损失函数是“碗形”,仅有一个全局最小值。
-

2 基础优化算法
线性回归有显示解(闭式解),但大多数机器学习模型(如神经网络)无法直接求解,需通过迭代优化逼近最优解。
核心目标是找到模型参数(如权重 W
和偏差 b
),使损失函数(如平方误差)最小化。
2.1 梯度下降
基本思想
-
初始化:随机选择参数初始值
W₀
。 -
迭代更新:沿损失函数下降最快的方向(负梯度)逐步调整参数:
W t = W t − 1 − η ⋅ ∇ L ( W t − 1 ) W_t=W_{t-1}-\eta\cdot\nabla L(W_{t-1}) Wt=Wt−1−η⋅∇L(Wt−1)η
:学习率(步长),控制更新幅度。∇L
:损失函数对参数的梯度(偏导数向量)。

直观理解
- 梯度方向:函数值上升最快的方向,负梯度即下降最快方向。
- 学习率的作用:
- 太小:收敛慢,需大量计算(如蜗牛爬坡)。
- 太大:可能跳过最优解,甚至发散(如迈步过大跌入山谷)。
示例
- 假设损失函数是“碗形”(凸函数),初始点
W₀
在碗边缘。 - 每次迭代沿最陡方向(负梯度)移动,逐步接近碗底(最小值)。

2.2 小批量随机梯度下降(Mini-batch SGD)
动机
- 传统梯度下降:每次计算需遍历全部样本(计算代价高)。
- 随机梯度下降(SGD):每次随机选一个样本计算梯度(噪声大,不稳定)。
- 折中方案:小批量(
batch_size = B
)随机采样,平衡效率和稳定性。

批量大小(batch_size
)的选择
- 太小(如
B=1
):- 无法利用 GPU 并行计算,训练波动大。
- 太大(如
B=全数据集
):- 内存不足,且可能包含冗余样本(如相似数据)。
- 经验值:常用
32
、64
、128
(需根据硬件和数据调整)。
3 实战:线性回归的从零实现
首先导入相关包。
%matplotlib inline
import random
import torch
from d2l import torch as d2l
d2l 包下载链接:https://github.com/d2l-ai/d2l-zh?tab=readme-ov-file。
只要将 d2l 文件夹放在项目中即可。d2l 导入了 numpy,pandas,matplotlib 这些常用包,自己手动安装即可。

3.1 生成数据集
为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集,任务是使用这个有限样本的数据集来恢复这个模型的参数。
生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的 2 个特征。我们的合成数据集是一个矩阵 X ∈ R 1000 × 2 \mathbf{X}\in \mathbb{R}^{1000 \times 2} X∈R1000×2。使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ \mathbf{w} = [2, -3.4]^\top w=[2,−3.4]⊤、 b = 4.2 b = 4.2 b=4.2 和噪声项 ϵ \epsilon ϵ 生成数据集及其标签( ϵ \epsilon ϵ 可以视为模型预测和标签时的潜在观测误差):
y = X w + b + ϵ \mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon y=Xw+b+ϵ
在这里我们认为标准假设成立,即 ϵ \epsilon ϵ 服从均值为 0 的正态分布。为了简化问题,我们将标准差设为 0.01。
def synthetic_data(w, b, num_examples): #@save# type: (Tensor, float, int) -> tuple[Tensor, Tensor]"""通过噪声生成y=Xw+b的数据集"""X = torch.normal(0, 1, size=(num_examples, len(w)))y = torch.matmul(X, w) + by += torch.normal(0, 0.01, y.shape)return X, y.reshape((-1, 1))true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features.shape, labels.shape

通过生成第二个特征 features[:, 1]
和 labels
的散点图, 可以直观观察到两者之间的线性关系。
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1)
d2l.plt.show()

运行
d2l.plt.show()
时报错:OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized. OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see http://www.intel.com/software/products/support/.
这个错误是由于 OpenMP 运行时库冲突 导致的,
libiomp5md.dll
被多次加载。PyTorch 和某些科学计算库(如 NumPy、Scipy)可能各自链接了不同版本的 OpenMP 运行时库。当多个副本被加载时,会导致冲突,出现OMP: Error #15
。常见于 Windows 系统上运行 PyTorch 或 NumPy。解决方法
方法 1(推荐):设置环境变量
KMP_DUPLICATE_LIB_OK=TRUE
在代码 最开头 添加:
import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" # 允许重复加载 OpenMP 库(临时解决方案)
方法 2:更新或重新安装 PyTorch 和 NumPy
冲突可能是由于版本不匹配导致的,尝试:
pip install --upgrade torch numpy
3.2 读取数据集
定义一个data_iter
函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size
的小批量。 每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):# type: (int, Tensor, Tensor) -> Iterable[tuple[Tensor, Tensor]]num_examples = len(features)indices = list(range(num_examples))# 随机打乱样本顺序以实现无偏采样random.shuffle(indices)# 按批次大小遍历整个数据集for i in range(0, num_examples, batch_size):# 获取当前批次的索引范围,处理最后一个批次可能不足的情况batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])# 生成当前批次的特征和标签张量yield features[batch_indices], labels[batch_indices]
3.3 初始化模型参数
在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。
在下面的代码中,我们通过从均值为 0、标准差为 0.01 的正态分布中采样随机数来初始化权重, 并将偏置初始化为 0。
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
w, b

3.4 定义模型
计算输入特征 X \mathbf{X} X 和模型权重 w \mathbf{w} w 的矩阵-向量乘法后加上偏置 b b b。
注意,上面的 X w \mathbf{Xw} Xw 是一个向量,而 b b b 是一个标量。回想一下广播机制:当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。
def linreg(X, w, b): #@save# type: (Tensor, Tensor, Tensor) -> Tensor"""线性回归模型"""return torch.matmul(X, w) + b
3.5 定义损失函数
使用平方损失函数。在实现中,我们需要将真实值y
的形状转换为和预测值y_hat
的形状相同。
def squared_loss(y_hat, y): #@save# type: (Tensor, Tensor) -> Tensor"""均方损失"""return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
3.6 优化算法
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。接下来,朝着减少损失的方向更新我们的参数。
下面的函数实现小批量随机梯度下降更新。该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save# type: (Iterable[Tensor], float, int) -> None"""小批量随机梯度下降"""with torch.no_grad():for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_()
3.7 训练
在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。计算完损失后,我们开始反向传播,存储每个参数的梯度。最后,我们调用优化算法sgd
来更新模型参数。
概括一下,我们将执行以下循环:
- 初始化参数
- 重复以下训练,直到完成
- 计算梯度 g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b) g←∂(w,b)∣B∣1∑i∈Bl(x(i),y(i),w,b)。
- 更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)←(w,b)−ηg。
在每个迭代周期(epoch)中,使用data_iter
函数遍历整个数据集,并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。
这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为 3 和 0.03。设置超参数很棘手,需要通过反复试验进行调整。
lr = 0.03 # 学习率
num_epochs = 3 # 迭代次数
batch_size = 10 # 批量大小
net = linreg
loss = squared_lossfor epoch in range(num_epochs):for X, y in data_iter(batch_size, features, labels):l = loss(net(X, w, b), y) # X和y的小批量损失# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,# 并以此计算关于[w, b]的梯度l.sum().backward()sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数# print('w:', w, 'b:', b)with torch.no_grad():train_l = loss(net(features, w, b), labels)print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

上述训练将 1000 个数据分为 100 批次,每个批次大小为 10。我们认为每个批次的 w 和 b 与总体一致。
因此,每一次迭代中,训练 100 批次,一共迭代 3 次,即训练了 300 批次。
通过比较真实参数和通过训练学到的参数来评估训练的成功程度。事实上,真实参数和通过训练学到的参数确实非常接近。
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')

绘制图像比较真实数据(黄色直线)与拟合结果(红色虚线)。
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1) # 绘制数据点
d2l.plt.plot(d2l.np.arange(-2, 3), d2l.np.arange(-2, 3) * true_w[1].detach().numpy() + true_b, color='yellow') # 绘制真实直线
d2l.plt.plot(d2l.np.arange(-2, 3), d2l.np.arange(-2, 3) * w[1, :].detach().numpy() + b.detach().numpy(), color='red', linestyle='--') # 绘制拟合直线
d2l.plt.show(), w, b

4 实战:线性回归的简易实现
4.1 实现
与上章类似,我们首先生成数据集。
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2ltrue_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
我们可以调用框架中现有的 API 来读取数据。 我们将features
和labels
作为 API 的参数传递,并通过数据迭代器指定batch_size
。 此外,布尔值is_train
表示是否希望数据迭代器对象在每个迭代周期内打乱数据。
def load_array(data_arrays, batch_size, is_train=True): #@save# type: (tuple[Tensor, Tensor], int, bool) -> data.DataLoader"""构造一个PyTorch数据迭代器"""dataset = data.TensorDataset(*data_arrays)return data.DataLoader(dataset, batch_size, shuffle=is_train) # 随机选取 batch_size 大小的数据batch_size = 10
data_iter = load_array((features, labels), batch_size)
使用data_iter
的方式与上章中使用data_iter
函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。 这里我们使用iter
构造 Python 迭代器,并使用next
从迭代器中获取第一项。
next(iter(data_iter))

对于标准深度学习模型,我们可以使用框架的预定义好的层。
首先定义一个模型变量net
,它是一个Sequential
类的实例。Sequential
类将多个层串联在一起。当给定输入数据时,Sequential
实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,以此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential
。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential
会让你熟悉“标准的流水线”。
这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。
# nn是神经网络的缩写
from torch import nnnet = nn.Sequential(nn.Linear(2, 1))
在使用net
之前,我们需要初始化模型参数。通过net[0]
选择网络中的第一个图层, 然后使用weight.data
和bias.data
方法访问参数。 我们还可以使用替换方法normal_
和fill_
来重写参数值。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

计算均方误差使用的是MSELoss
类,也称为平方 L2 范数。 默认情况下,它返回所有样本损失的平均值。
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim
模块中实现了该算法的许多变种。 当我们实例化一个SGD
实例时,我们要指定优化的参数 (可通过net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr
值,这里设置为 0.03。
loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
在每个迭代周期里,我们将完整遍历一次数据集(train_data
), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l
(前向传播)。 - 通过进行反向传播来计算梯度。
- 通过调用优化器来更新模型参数。
为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。
num_epochs = 3
for epoch in range(num_epochs):for X, y in data_iter:l = loss(net(X), y)trainer.zero_grad()l.backward()# print(net[0].weight.grad)trainer.step()l = loss(net(features), labels)print(f'epoch {epoch + 1}, loss {l:f}')

下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数,我们首先从net
访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)

4.2 练习
-
如果我们用
nn.MSELoss(reduction=‘sum’)
替换nn.MSELoss()
,为了使代码的行为相同,需要怎么更改学习速率?为什么?答:这样替换的结果是会使得梯度值放大为原来的 num_example 倍,原有的学习率显得过大,使得其出现了振荡,即步长过长导致。应该把学习率除以
batch_size
,因为默认参数是'mean'
,换成'sum'
需要除以批量数。一般会采用默认,因为这样学习率可以跟batch_size
解耦。 -
如何访问线性回归的梯度?
答:使用
net[0].weight.grad
。num_epochs = 3 for epoch in range(num_epochs):for X, y in data_iter:l = loss(net(X), y)trainer.zero_grad()l.backward()print(net[0].weight.grad)trainer.step()l = loss(net(features), labels)print(f'epoch {epoch + 1}, loss {l:f}')