0.Tensor
Tensor 是 PyTorch 中最基础的数据结构。模型的输入、输出、权重和梯度,最终都使用 Tensor 表示。
Tensor = 存储区 (data) + 元信息头部 (tensor header) 两块是分开存的。
Tensor Header(头部元信息,很小)
保存这些信息:
python
shape:各维度大小
stride:步长,跨多少个内存元素跳到下一个该维度的元素
storage_offset:数据起始偏移
dtype:数据类型 (float32/float64/int32 等)
device:cpu / cuda
requires_grad:是否求导
指向真实数据存储的指针
头部是复制开销很小的结构体;真正的数值数据在 Storage 里。
Storage(存储区,真正存数字)
一块连续内存数组,一维扁平缓冲区,所有 tensor 的原始数据都存在这里。
多个 Tensor header 可以共享同一块 Storage 内存 → 视图 view。
(1)创建Tensor
python
import torch
# 标量:0 维
a = torch.tensor(5)
# 向量:1 维
b = torch.tensor([1, 2, 3])
# 矩阵:2 维
c = torch.tensor([
[1, 2, 3],
[4, 5, 6]
])
返回类型都是 torch.Tensor
python
import torch
torch.zeros(2,3):全 0 张量,shape [2,3],默认 float32
torch.ones(2,3):全 1 张量,shape [2,3]
torch.rand(2,3):\([0,1)\) 均匀分布,float
torch.randn(2,3):标准正态分布 \(\mathcal N(0,1)\),float
torch.arange(start, end, step):整数序列,左闭右开
torch.arange(0,10,2)
torch.linspace(start, end, num):均分 num 个点,首尾都包含,浮点型
torch.linspace(0,1,5)
(2)Tensor 的属性
python
x = torch.tensor([
[1., 2., 3.],
[4., 5., 6.]
])
print("形状:", x.shape)
print("维度数量:", x.ndim)
print("元素数量:", x.numel())
print("数据类型:", x.dtype)
print("所在设备:", x.device)
输出:
形状:torch.Size([2, 3])
维度数量:2
元素数量:6
数据类型:torch.float32
所在设备:cpu
shape=2, 3 表示有 2 行、3 列。
在机器学习中,二维数据通常采用:
样本数量, 特征数量
(3)数据类型
python
a = torch.tensor([1, 2, 3])
b = torch.tensor([1., 2., 3.])
print(a.dtype) # torch.int64
print(b.dtype) # torch.float32
手动指定类型:
python
x = torch.tensor(
[1, 2, 3],
dtype=torch.float32
)
转换类型:
python
x = x.float()
x = x.long()
x = x.to(torch.float32)
常见类型:
python
torch.float32:模型输入和参数常用
torch.float64:精度更高,但速度更慢
torch.int64:分类标签常用
torch.bool:逻辑判断结果
(4)索引和切片
Tensor 的索引方式与 Python 列表、NumPy 类似:
python
x[行索引, 列索引]
行索引和列索引都可以使用以下两种形式:
1.整数索引
2.start:stop:step
start 省略:默认从开头开始
stop 省略:默认到结尾结束
step 省略:默认步长为 1
python
x = torch.tensor([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
print(x[0]) # 第一行
print(x[1, 2]) # 第二行第三列
print(x[:, 0]) # 第一列
print(x[0:2, :]) # 前两行
print(x[:, 1:]) # 从第二列开始
条件筛选:
python
print(x[x > 5])
tensor([6, 7, 8, 9])
(5)修改形状
python
tensor.reshape(*shape)
返回新形状的张量,数据共享(视图,不拷贝数据)
只能有一个 -1,代表自动计算该维度大小,元素总数必须匹配。
python
import torch
x = torch.arange(12)
print(x.shape) # torch.Size([12])
y = x.reshape(3, 4)
print(y)
print(y.shape) # torch.Size([3, 4])
# -1自动推断维度
y = x.reshape(3, -1)
print(y.shape) # torch.Size([3, 4])
python
tensor.flatten(start_dim=0, end_dim=-1)
将指定维度区间展平为一维;默认从 0 维到最后一维全部展平。
python
y = torch.arange(12).reshape(3,4)
z = y.flatten()
print(z.shape) # torch.Size([12])
unsqueeze () 增加维度
python
tensor.unsqueeze(dim)
在指定dim位置插入一个大小为 1 的维度;返回视图,不拷贝数据。
设张量为 N 维
unsqueeze dim:正数[0,N];负数-[N+1,-1],dim = -1 → 最后一维,dim = -2 → 倒数第二维
python
在最外层,行的外面包一层括号。
x = torch.tensor([1, 2, 3])
a = x.unsqueeze(0)
b = x.unsqueeze(1)
print(a.shape) # torch.Size([1, 3])
print(b.shape) # torch.Size([3, 1])
x = torch.tensor([[1, 2, 3],
[4, 5, 6]])
a = x.unsqueeze(0)
print(a.shape) # torch.Size([1, 2, 3])
print(a)
'''
tensor([[[1, 2, 3],
[4, 5, 6]]])
'''
python
在原来行、列中间插入 size=1 维度。
dim0 保持 = 2;新 dim1=1;原来 dim1 (3) 变成 dim2
输出 shape:[2, 1, 3]
b = x.unsqueeze(1)
print(b.shape) # torch.Size([2, 1, 3])
print(b)
'''
tensor([[[1, 2, 3]],
[[4, 5, 6]]])
'''
squeeze () 删除大小为 1 的维度
python
tensor.squeeze(dim=None)
不传参数:删除所有size=1 的维度
指定dim:仅删除该维度(该维度 size 必须是 1,否则不变);返回视图,不拷贝数据
dim 取值规则
N 维张量
dim 可取:0 ~ N‑1;负数 -N ~ -1
python
x = torch.rand(1,2,1,3)
print(x.shape) # [1,2,1,3]
y1 = x.squeeze(dim=0)
print(y1.shape) # [2, 1, 3] 删除dim0(size=1)
y2 = x.squeeze(dim=2)
print(y2.shape) # [1, 2, 3] 删除dim2(size=1)
# dim位置size不等于1,调用squeeze无任何变化
y3 = x.squeeze(dim=1)
print(y3.shape) # [1, 2, 1, 3] dim1=2≠1,不生效
1.线性模型
(1)线性模型的基本形式是:
y = wx + b
x:输入特征
y:模型输出
w:权重
b:偏置
例如,根据房屋面积预测房价:
房价 = w * 房屋面积 + b
python
import torch
# 输入数据
x = torch.tensor([1., 2., 3., 4.])
# 权重和偏置
w = torch.tensor(2.)
b = torch.tensor(1.)
# 线性模型
y = w * x + b
print("输入数据:", x)
print("模型输出:", y)
输入数据: tensor([1., 2., 3., 4.])
模型输出: tensor([3., 5., 7., 9.])
模型的计算过程是:
输入 x
↓
乘以权重 w
↓
加上偏置 b
↓
得到输出 y
现在的 w 和 b 是我们手动指定的,还没有自动学习。
假设真实数据是:
python
x = torch.tensor([1., 2., 3., 4.])
true_y = torch.tensor([2.8, 5.1, 6.9, 8.8])
w = torch.tensor(2.)
b = torch.tensor(1.)
y = w * x + b
print("模型输出:", y)
print("真实值:", true_y)
模型输出:
tensor([3., 5., 7., 9.])
真实值:
tensor([2.8, 5.1, 6.9, 8.8])
两者之间存在误差。后面我们会学习损失函数,用来衡量这个误差,再使用梯度下降调整 w 和 b。
(2)多个特征的线性模型
如果输入只有一个特征:
y = wx + b
如果输入有多个特征:
y = w1*x1 + w2*x2 + w3*x3 + b
例如,根据房屋面积、房间数量和楼层预测房价:
y = w1*面积 + w2*房间数 + w3*楼层 + b
矩阵形式可以写成:
y = Xw + b
在 PyTorch 中:
python
import torch
X = torch.tensor([
[80., 2., 5.],
[100., 3., 8.],
[60., 1., 3.]
])
w = torch.tensor([
[2.],
[10.],
[1.]
])
b = torch.tensor([5.])
y = X @ w + b
print(y)
其中:
X.shape:3 个样本,每个样本有 3 个特征
w.shape:3 个权重
y.shape:3 个预测结果
(3)PyTorch 中的线性层
python
torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)
python
参数:
in_features:输入特征维度,输入张量最后一维的大小
out_features:输出特征维度,线性变换之后的特征数
bias=True:是否开启偏置项;bias=False 则没有偏置向量
数学公式:y = xW^T + b
W:权重矩阵,shape [out_features, in_features]
b:偏置,shape [out_features],bias 关闭时不存在
注意:PyTorch Linear 权重存储是 [out, in],不是 [in, out]。计算时用权重转置。
python
输入:(*, in_features),* 代表任意多的前置维度(batch、seq 长度等)
输出:(*, out_features),前置维度保持不变,仅最后一维被线性层变换
python
import torch
import torch.nn as nn
2D 输入 [batch, in_features]
# 输入特征4,输出特征2
linear = nn.Linear(in_features=4, out_features=2)
# batch=3,每个样本4维
x = torch.randn(3, 4)
y = linear(x)
print(y.shape) # torch.Size([3, 2])
3 维输入 [batch, seq_len, in_features]
linear = nn.Linear(4,2)
x = torch.randn(5,10,4) # [batch=5, seq_len=10, in=4]
y = linear(x)
print(y.shape) # torch.Size([5, 10, 2])
只会变换最后一维;前面 batch、序列维度全部保留。输入最后一维必须等于 in_features,否则直接报错
2.梯度下降算法
核心思想:沿着损失函数梯度的反方向更新参数 ,最小化损失函数 J (θ)。
梯度 ∇J (θ):指向损失上升最快的方向;取负号,就是损失下降最快方向。
更新公式
python
θ = θ - η · ∇J(θ)
θ:模型参数(权重 w、偏置 b)
η:学习率 learning rate,控制每一步走多大
∇J (θ):损失函数对参数的梯度
三种主流梯度下降变体
(1)BGD 批量梯度下降 Batch Gradient Descent
每一轮,使用全部训练样本计算梯度,再更新一次参数。
优点:梯度准确,收敛稳定;可以得到精确最优解
缺点:数据集很大时,计算很慢,内存压力大
更新:全部样本算完梯度 → 更新 1 次参数
(2)SGD 随机梯度下降 Stochastic Gradient Descent
每次只用 1 个随机样本计算梯度,立刻更新参数。
python
θ = θ - η ∇J(θ;x_i,y_i)
优点:速度快;可以跳出局部极小;适合大数据
缺点:梯度噪声大,震荡,很难收敛到最低点,会在最优附近抖动
更新:取 1 个样本 → 算梯度 → 更新参数
(3)Mini‑batch GD 小批量梯度下降(工业最常用)
取一小批样本(batch,比如 32/64/128)计算梯度,执行更新。
兼顾 BGD 和 SGD 优点。
梯度相对稳定,计算效率高,深度学习默认方案。
深度学习几乎全部使用 Mini‑batch。
| 算法 | 每轮使用样本 | 特点 |
|---|---|---|
| BGD | 全部样本 | 稳定,大数据慢 |
| SGD | 单样本 | 快,震荡严重 |
| Mini‑batch | 一小批 (batch) | 平衡速度与稳定,实际工程首选 |
关键超参数:学习率 η
- 学习率太大:步子过大,来回震荡,损失发散不收敛
- 学习率太小:更新很慢,训练耗时久,困在局部极小值
局部极小值、鞍点:
- 局部极小值:周围都比它高,但不是全局最小;梯度 = 0,参数停止更新。
- 鞍点:部分方向上升,部分方向下降,梯度也等于 0。高维空间鞍点比局部极小更常见。
SGD/Mini‑batch 的噪声有助于逃离鞍点、局部极小。
在线性模型中:y = x * w,已知数据:
x_data = [1.0, 2.0, 3.0]
y_data = [2.0, 4.0, 6.0]
我们希望找到一个合适的 w,使得:y = x * w 尽量接近真实值。
(1)模型预测
python
def forward(x, w):
return x * w
(2)损失函数
损失函数用来衡量预测值和真实值之间的差距。
单个样本的平方损失:
loss = (预测值 - 真实值) * (预测值 - 真实值)
因为:
预测值 = x * w
所以:
loss = (x * w - y) * (x * w - y)
所有样本的平均损失:
cost = 所有样本损失之和 / 样本数量
python
def cost(x_data, y_data, w):
total_loss = 0
for x, y in zip(x_data, y_data):
prediction = x * w
loss = (prediction - y) ** 2
total_loss += loss
return total_loss / len(x_data)
(3)梯度
梯度表示:
当前参数应该向哪个方向调整
单个样本的损失是:
loss = (x * w - y) ** 2
对 w 求导后:
dloss/dw = 2 * x * (x * w - y)
所有样本的平均梯度:
python
gradient = 所有样本梯度之和 / 样本数量
python
def gradient(x_data, y_data, w):
total_gradient = 0
for x, y in zip(x_data, y_data):
total_gradient += 2 * x * (x * w - y)
return total_gradient / len(x_data)
(4)参数更新公式
梯度下降的更新公式是:
python
w_new = w_old - learning_rate * gradient
ω = ω − α * ∂cost/∂ω
w_old:更新前的参数
w_new:更新后的参数
learning_rate:学习率
gradient:当前梯度
为什么要减去梯度?
因为梯度指向损失增加最快的方向。我们要让损失下降,所以要向梯度的反方向移动。
如果 gradient > 0,w_new = w_old - 一个正数,w 会变小。
如果 gradient < 0,那么 w_new = w_old - 一个负数, w 会变大。
(5)第一次更新
python
初始参数:w = 1
learning_rate = 0.01
数据:
x = [1, 2, 3]
y = [2, 4, 6]
预测值:[1, 2, 3]
误差:[-1, -2, -3]
梯度:
python
gradient =(
2 * 1 * (1 * 1 - 2)
2 * 2 * (2 * 1 - 4)
2 * 3 * (3 * 1 - 6)
) / 3
计算结果:gradient = -9.3333
更新参数:
python
w_new = 1 - 0.01 * (-9.3333)
w_new = 1.093333
因为梯度是负数,所以 w 增大了,更接近正确答案 2。
(6)完整代码
python
x_data = [1.0, 2.0, 3.0]
y_data = [2.0, 4.0, 6.0]
w = 1.0
learning_rate = 0.01
def forward(x, w):
return x * w
def cost(x_data, y_data, w):
total_loss = 0
for x, y in zip(x_data, y_data):
prediction = forward(x, w)
loss = (prediction - y) ** 2
total_loss += loss
return total_loss / len(x_data)
def gradient(x_data, y_data, w):
total_gradient = 0
for x, y in zip(x_data, y_data):
prediction = forward(x, w)
total_gradient += 2 * x * (prediction - y)
return total_gradient / len(x_data)
for epoch in range(100):
cost_value = cost(x_data, y_data, w)
gradient_value = gradient(x_data, y_data, w)
w = w - learning_rate * gradient_value
if epoch % 10 == 0:
print(
"epoch:",
epoch,
"w:",
w,
"loss:",
cost_value
)
print("训练结束后的 w:", w)
print("x=4 时的预测值:", forward(4, w))
3.反向传播
反向传播的目的,是计算"损失对每个参数的影响",也就是梯度。有了梯度,才能用梯度下降更新参数。
python
前向传播:输入 x -> 预测 y_pred -> 计算 loss
反向传播:从 loss 倒推 -> 计算参数 w 的梯度
参数更新:w = w - 学习率 * w 的梯度
这一行代码就是执行反向传播的过程。
(1)推导反向传播
python
y^ = x * w
python
Loss=(xw−y)^2
a=xw−y -> Loss=a^2
根据链式法则:
将复杂函数拆成多个简单函数,然后从输出层向输入层逐层计算梯度。
计算每一层的梯度:
- Loss 对 a 的梯度:
- 计算 a 对 w 的梯度:
最终得到递推公式:
这就是 PyTorch 计算得到的 w.grad。
(2)为什么要反向传播
我们需要知道w 改变一点,loss 会改变多少?
这就是
python
dloss/dw
得到梯度之后,需要更新参数:
(3)单个样本的训练代码
python
import torch
x = torch.tensor(2.0)
y = torch.tensor(4.0)
w = torch.tensor(1.0, requires_grad=True)
learning_rate = 0.01
for epoch in range(10):
# 前向传播:预测和计算损失
y_pred = x * w
loss = (y_pred - y) ** 2
# 反向传播:计算 dloss/dw
loss.backward()
# 参数更新不需要计算梯度
with torch.no_grad():
w -= learning_rate * w.grad
# PyTorch 默认累加梯度,因此必须清空
w.grad.zero_()
print(
"epoch:", epoch,
"w:", w.item(),
"loss:", loss.item()
)
(4)为什么需要 zero_grad()
PyTorch 默认会累积梯度。
例如,第一次 grad=−32,第二次 grad=−20,如果不清零 grad=−32+(−20),因此每一次训练前:optimizer.zero_grad(),清空旧梯度。
(5)为什么参数更新要放在 torch.no_grad() 中
python
with torch.no_grad():
w -= learning_rate * w.grad
这里的 `with` 会创建一个临时的上下文环境。进入这个代码块时,
`torch.no_grad()` 暂时关闭 PyTorch 的梯度记录;
离开代码块后,PyTorch 会自动恢复之前的梯度记录状态。
`torch.no_grad()` 的作用是:在更新参数 `w` 时,
不追踪这次运算,也不把参数更新操作加入计算图。
因为:
反向传播负责计算梯度
参数更新只是根据梯度修改参数
如果不关闭梯度记录,PyTorch 会把每次更新操作也加进计算图,训练过程会出错或不断占用内存。
执行 loss.backward() 后 w.grad保存了损失对 w 的梯度。
接下来要根据梯度更新 w,w -= learning_rate * w.grad,这一步只是修改参数,不需要再计算梯度,所以写成 w -= learning_rate * w.grad
(6)和梯度下降的关系
反向传播和梯度下降不是同一个概念:
反向传播:计算梯度
梯度下降:使用梯度更新参数
完整流程:
python
前向传播
y_pred = x * w
loss = (y_pred - y) ** 2
反向传播
loss.backward()
得到 w.grad
梯度下降
w = w - learning_rate * w.grad
后续的线性回归、逻辑回归、CNN、RNN 都遵循这个基本流程,只是模型和参数更多。
4.线性回归
线性回归是监督学习、回归任务,用来学习输入和输出之间的线性关系。
模型假设输出可以由输入的线性组合得到:ŷ = w * x + b
4.1 整体流程
整体四步流程
-
准备数据集
-
自定义模型类,继承nn.Module
-
构造损失函数、优化器
-
训练循环:前向传播 → 清零梯度 →反向传播 → 参数更新
python
import torch
# 1.准备数据集
x_data = torch.Tensor([[1.0], [2.0], [3.0]])
y_data = torch.Tensor([[2.0], [4.0], [6.0]])
# 2.设计模型类,继承torch.nn.Module
class LinearModel(torch.nn.Module):
def __init__(self):
super(LinearModel, self).__init__()
# nn.Linear包含两个成员张量:weight(w)、bias(b)
self.linear = torch.nn.Linear(1, 1)
def forward(self, x):
# forward前向传播,会被自动调用
y_pred = self.linear(x)
return y_pred
model = LinearModel()
# 3.构建损失函数、优化器
criterion = torch.nn.MSELoss(size_average=False)
# SGD随机梯度下降;传入模型全部参数、学习率lr
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 4.训练循环
for epoch in range(100):
y_pred = model(x_data) # 前向传播 forward,计算预测值
loss = criterion(y_pred, y_data)# 计算MSE损失
print(f"epoch:{epoch}, loss:{loss.item()}")
optimizer.zero_grad() # 梯度清零!梯度会累加,每次backward前必须置0
loss.backward() # 反向传播,计算各参数梯度
optimizer.step() # 优化器更新权重w、bias
# 打印训练后的权重与偏置
print('w = ', model.linear.weight.item())
print('b = ', model.linear.bias.item())
# 模型推理测试
x_test = torch.Tensor([[4.0]])
y_test = model(x_test)
print("y_pred = ", y_test.data)
(1)构建张量
python
x_data = torch.Tensor([[1.0], [2.0], [3.0]])
这里构建二维张量 [N, input_dim],N = 样本数量,input_dim = 特征数 = 1
(2)torch.nn.Module
torch.nn.Module 是所有网络层、模型的基类,PyTorch 里 nn.Linear、nn.Conv2d、自定义模型全部继承它。必须实现 init() 和 forward()。
python
import torch.nn as nn
# 自定义模型,继承nn.Module
class LinearModel(nn.Module):
def __init__(self):
super().__init__() # 必须调用父类构造
# 在这里定义网络层(可学习参数层)
self.linear = nn.Linear(1, 1)
def forward(self, x):
# 经过self.linear层运算,输出out
out = self.linear(x)
return out
# 实例化模型
model = LinearModel()
print(model)
python
torch.nn.Linear(in_features, out_features, bias=True)
参数:
in_features=1:输入特征维度
out_features=1:输出特征维度
bias=True(默认):是否启用偏置 b;False 则没有 bias,只有权重 w
内部公式:y = x @ W.T + b
内部可训练参数:
self.linear.weight:权重张量,形状[out_features,in_features]
self.linear.bias:偏置张量,形状[out_features]
forward() 前向传播:不要手动调用 model.forward(x),而是直接 model(x)
python
x = torch.Tensor([[2.0]])
y = model(x) # 内部自动调用forward()
(3)torch.nn.MSELoss() 均方误差损失
| 旧参数写法 | 等价 reduction |
说明 |
|---|---|---|
size_average=True |
reduction="mean" |
全部元素求平均(默认值) |
size_average=False |
reduction="sum" |
全部元素直接求和,不除以样本数 |
reduce=False |
reduction="none" |
不做归约,返回每个位置单独的损失张量 |
python
公式:loss = sum((y_pred - y_true)**2)。
**model.parameters():**返回生成器,迭代模型中所有需要训练的参数(weight、bias),优化器需要拿到这些张量去更新。
python
torch.optim.SGD(params, lr, momentum=0, dampening=0, weight_decay=0, nesterov=False)
params:待更新参数,传入model.parameters()
lr=0.01:learning_rate 学习率,参数更新步长
momentum=0:动量,默认关闭;动量可以加速收敛,抑制震荡
weight_decay=0:L2 正则化系数,防止过拟合
model.parameters() 会得到模型内部所有需要学习的参数,当前就是 model.linear.weight、model.linear.bias,SGD 记住了这两个参数和学习率 0.01。
4.2 torch.nn
torch.nn,神经网络模块:网络层、激活函数、损失函数、容器、初始化。继承基类 nn.Module
(1)网络层
| 类 | 作用 |
|---|---|
nn.Linear(in_features, out_features, bias=True) |
全连接线性层 |
nn.Conv1d / Conv2d / Conv3d |
一维 / 二维 / 三维卷积 |
nn.MaxPool1d / MaxPool2d |
最大池化 |
nn.AvgPool1d / AvgPool2d |
平均池化 |
nn.Flatten() |
展平,保留 batch 维度 |
nn.Dropout(p) |
Dropout 随机失活,训练时随机置 0 |
nn.BatchNorm1d / BatchNorm2d |
批归一化 |
nn.Embedding(num_embeddings, embedding_dim) |
词嵌入层 |
(2)激活函数
python
nn.ReLU()
nn.Sigmoid()
nn.Tanh()
nn.LeakyReLU(negative_slope=0.01)
nn.GELU()
nn.Softmax(dim=1)
nn.LogSoftmax(dim=1)
(3)损失函数
| 损失 | 用途 |
|---|---|
nn.MSELoss(reduction="mean") |
均方误差,回归任务 |
nn.L1Loss() |
L1 绝对值损失 |
nn.CrossEntropyLoss() |
多分类,输入原始 logits,不要自己 softmax |
nn.BCELoss() |
二分类,输入已经 sigmoid 后的概率 |
nn.BCEWithLogitsLoss() |
二分类,输入原始 logits,内部自带 sigmoid,推荐 |
(4)容器(Container)用来组装网络
1.nn.Sequential:串行堆叠层,按顺序执行
python
net = nn.Sequential(
nn.Linear(1, 8),
nn.ReLU(),
nn.Linear(8, 1)
)
2.nn.ModuleList(...):模块列表,可以 for 循环遍历,不会自动前向传播,需要自己写 forward 调用
3.nn.ModuleDict({...}):字典形式存放模块,按 key 取层
(5)参数相关
nn.Parameter(tensor):把普通张量标记为可训练参数,会被model.parameters()收集,参与优化更新。
4.3 torch.optim
用来更新模型权重,不创建网络,只接收 model.parameters ()
python
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
python
训练循环内三步
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向求梯度
optimizer.step() # 根据梯度更新权重
常用优化器
| 优化器 | 参数要点 | 特点 |
|---|---|---|
optim.SGD(params, lr, momentum=0, weight_decay=0) |
lr 学习率;momentum 动量;weight_decay L2 正则 | 随机梯度下降,传统,调参敏感 |
optim.Adagrad(params, lr) |
自适应学习率,历史梯度累加 | 适合稀疏数据 |
optim.RMSprop(params, lr, alpha=0.9) |
平方梯度平滑 | RNN 常用 |
optim.Adam(params, lr=0.001, betas=(0.9,0.999)) |
一阶、二阶动量,最常用 | 自适应,大部分任务首选 |
optim.AdamW(params, lr, weight_decay) |
Adam 的改进,正确实现 weight_decay | 现在优先推荐替代 Adam |
5.逻辑回归做分类
逻辑回归虽然名字里有"回归",但它主要用于二分类问题。它的核心思想是先用线性模型计算一个分数,再用 sigmoid 把分数转换成概率,最后根据概率判断类别。
(1)sigmoid 函数
线性模型是:z = x * w + b,z 的取值范围是负无穷到正无穷,它不能直接表示概率,因为概率必须在0 到 1之间。
python
torch.sigmoid(x)
把任意实数转换成 0 到 1 之间的数
输入很小:输出接近 0
输入等于 0:输出等于 0.5
输入很大:输出接近 1
import torch
x = torch.tensor([-2.0, 0.0, 2.0])
p = torch.sigmoid(x)
print(p)
# tensor([0.1192, 0.5000, 0.8808])
逻辑回归的完整模型是:
python
z = x * w + b
p = sigmoid(z)
z:线性模型得到的分数
p:预测为类别 1 的概率
(2)二分类损失函数
二分类使用二元交叉熵损失:
python
loss = -[y * log(p) + (1 - y) * log(1 - p)]
其中:
y:真实标签,只能是 0 或 1
p:模型预测为类别 1 的概率
当真实标签是 1,如果 y = 1,损失变成 loss = -log(p)
p 越接近 1,损失越小
p 越接近 0,损失越大
当真实标签是 0,如果y = 0,损失变成 loss = -log(1 - p)
p 越接近 0,损失越小
p 越接近 1,损失越大
PyTorch 中可以写:
python
criterion = torch.nn.BCELoss(reduction="sum")
BCELoss 要求模型输出已经是概率,也就是已经经过 sigmoid。
模型训练时使用概率 p 和损失函数,模型预测类别时才使用 0.5 进行判断。
(3)逻辑回归和线性回归的区别
python
线性回归:
z = x * w + b
输出是连续数值
损失常用 MSELoss
逻辑回归:
z = x * w + b
p = sigmoid(z)
输出是 0 到 1 的概率
损失常用 BCELoss
对比:
线性回归:预测房价、温度、身高
逻辑回归:预测是否通过、是否患病、属于哪一类
(3)逻辑回归模型结构
python
import torch
# 1. 准备数据
x_data = torch.tensor([[1.0],[2.0],[3.0]])
y_data = torch.tensor([[0.0],[0.0],[1.0]])
# 2. 定义逻辑回归模型
class LogisticRegressionModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(1, 1)
def forward(self, x):
logits = self.linear(x)
return logits
model = LogisticRegressionModel()
# 3. 定义损失函数和优化器
criterion = torch.nn.BCEWithLogitsLoss(reduction="sum")
optimizer = torch.optim.SGD(model.parameters(),lr=0.01)
# 4. 训练
for epoch in range(1000):
# 前向传播
logits = model(x_data)
# BCEWithLogitsLoss 内部包含 sigmoid
loss = criterion(logits, y_data)
# 清空旧梯度
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
if epoch % 100 == 0:
print(
"epoch:",
epoch,
"loss:",
loss.item()
)
# 5. 查看训练后的参数
print("w =", model.linear.weight.item())
print("b =", model.linear.bias.item())
# 6. 模型预测
x_test = torch.tensor([[1.5],[2.5],[4.0]])
model.eval()
with torch.no_grad():
# 模型输出 logits
logits = model(x_test)
# logits 转换为概率
probability = torch.sigmoid(logits)
# 概率大于等于 0.5,预测为类别 1
prediction = (probability >= 0.5).float()
print("logits:")
print(logits)
print("概率:")
print(probability)
print("预测类别:")
print(prediction)
6.处理多维特征的输入
前面的逻辑回归只有一个特征,x = 一个数字,例如 x = 学习时间,y = 是否通过考试。
现实中通常有多个特征,例如 x1:学习时间;x2:考试次数;x3:作业完成率。这就是多维特征输入。
(1)单个样本和多个样本
单个样本有 3 个特征,x = 学习时间, 考试次数, 作业完成率
用 Tensor 表示:
python
x = torch.tensor([2.0, 3.0, 0.8])
它的形状是 3,表示有 3 个特征。
多个样本需要写成二维 Tensor:
python
x_data = torch.tensor([
[2.0, 3.0, 0.8],
[5.0, 4.0, 0.9],
[1.0, 1.0, 0.3],
[6.0, 5.0, 1.0]
])
print(x_data.shape) # torch.Size([4, 3])
4:有 4 个样本
3:每个样本有 3 个特征
多维特征数据通常使用这个形状:样本数量, 特征数量
(2)多维线性模型
单个特征时:
python
z = x * w + b
三个特征时:
python
z = x1 * w1 + x2 * w2 + x3 * w3 + b
x1、x2、x3:三个输入特征
w1、w2、w3:三个权重
b:偏置
矩阵形式:
z = Xw + b
在 PyTorch 中,多个样本会同时计算:
z = x_data @ w + b
假设:
python
x_data.shape = [4, 3]
w.shape = [3, 1]
矩阵乘法后:
z.shape = [4, 1]
也就是每个样本得到一个输出。
nn.Linear(3, 1) 表示:
-
每个样本有 3 个输入特征
-
每个样本输出 1 个结果
python
它内部包含:
weight:形状 [1, 3]
bias:形状 [1]
注意,weight 的形状是:
[输出数量, 输入特征数量]
所以:
model.linear.weight.shape
输出:
torch.Size([1, 3])
它内部完成的计算可以理解为:
z = x1 * w1 + x2 * w2 + x3 * w3 + b
(3)为什么通常需要标准化
假设两个特征:
年龄:0 到 100
收入:0 到 100000
收入的数值范围远大于年龄,训练时可能导致收入对应的梯度影响过大,模型训练不稳定,学习率难以选择。
常见标准化公式:
python
新特征 = (原特征 - 平均值) / 标准差
python
简单示例:
mean = x_data.mean(dim=0)
std = x_data.std(dim=0)
x_data = (x_data - mean) / std
查看结果:
print(x_data.mean(dim=0))
print(x_data.std(dim=0))
注意:真实项目中,验证集和测试集必须使用训练集的 mean 和 std:
python
x_train = (x_train - train_mean) / train_std
x_val = (x_val - train_mean) / train_std
x_test = (x_test - train_mean) / train_std
不能分别计算各自的平均值和标准差,否则会造成数据泄漏。
一维输入和多维输入的主要区别是训练数据 x_data 的列数不同 、Linear 层的输入特征数不同,其他代码基本不变。
7.数据加载
数据很多时,如何按批次读取数据并送入模型训练。
前面我们直接把全部数据放在x_data、y_data
然后一次性训练 logits = model(x_data)
真实项目中数据通常很多,不能一次性全部放进内存,所以需要:
python
Dataset:保存和读取样本
DataLoader:按 batch 提供样本
(1)TensorDataset
最简单的方式是使用 TensorDataset:
python
from torch.utils.data import TensorDataset
dataset = TensorDataset(x_data, y_data)
它会把对应位置的输入和标签配成一个样本:
python
第 0 个样本:(x_data[0], y_data[0])
第 1 个样本:(x_data[1], y_data[1])
第 2 个样本:(x_data[2], y_data[2])
例如:
python
x_data = torch.tensor([[1.0], [2.0], [3.0]])
y_data = torch.tensor([[0.0], [0.0], [1.0]])
dataset = TensorDataset(x_data, y_data)
print(len(dataset)) # 3
print(dataset[0])
# (tensor([1.]), tensor([0.]))
第0个样本对应:
# x_data[0] # tensor([1.])
# y_data[0] # tensor([0.])
(2)DataLoader
python
DataLoader(dataset, batch_size, shuffle)
dataset:数据集
batch_size:每批读取多少个样本
shuffle:每轮训练前是否打乱数据
例如
python
from torch.utils.data import DataLoader
loader = DataLoader(dataset, batch_size=2, shuffle=True)
如果有 3 个样本,batch_size=2,数据会分成:
第 1 批:2 个样本
第 2 批:1 个样本
读取数据:
for x_batch, y_batch in loader:
print(x_batch)
print(y_batch)
(3)使用 DataLoader 训练
python
import torch
from torch.utils.data import TensorDataset, DataLoader
x_data = torch.tensor([[1.0], [2.0], [3.0]])
y_data = torch.tensor([[0.0], [0.0], [1.0]])
dataset = TensorDataset(x_data, y_data)
loader = DataLoader(dataset, batch_size=2, shuffle=True)
class LogisticRegressionModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(1, 1)
def forward(self, x):
return self.linear(x)
model = LogisticRegressionModel()
criterion = torch.nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(1000):
for x_batch, y_batch in loader:
logits = model(x_batch)
loss = criterion(logits, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print("epoch:", epoch, "loss:", loss.item())
和之前相比,核心变化是:
之前:
python
logits = model(x_data)
loss = criterion(logits, y_data)
现在:
python
for x_batch, y_batch in loader:
logits = model(x_batch)
loss = criterion(logits, y_batch)
也就是说,现在每次使用一个小批次训练。
(4)epoch 和 batch 的区别
epoch:完整遍历一次数据集
batch:一次取出的一小部分数据
假设总样本数:100,batch_size:10
那么每个 epoch 有 10 个 batch
python
训练结构是:
for epoch in range(epochs):
for x_batch, y_batch in loader:
# 一个 batch 的训练
执行顺序:
第 1 个 epoch
第 1 个 batch
第 2 个 batch
...
第 10 个 batch
第 2 个 epoch
第 1 个 batch
第 2 个 batch
...
shuffle=False:按原顺序分 batch
shuffle=True:每个 epoch 先随机打乱,再分 batch
使用 batch 有几个好处:
-
不需要一次性加载全部数据
-
节省内存
-
可以使用 GPU 加速
-
参数会进行多次更新
-
shuffle 可以减少数据顺序带来的影响
(5)常见的三种梯度下降方式
Batch Gradient Descent:一次使用全部数据
Stochastic Gradient Descent:一次只使用一个样本
Mini-batch Gradient Descent:一次使用一小批样本
DataLoader 最常用于第三种:Mini-batch Gradient Descent
(6)自定义 Dataset
TensorDataset 适合已经在 Tensor 中的数据。
如果数据来自文件、图片或数据库,就需要自定义 Dataset:
python
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, x_data, y_data):
self.x_data = x_data
self.y_data = y_data
def __len__(self):
return len(self.x_data)
def __getitem__(self, index):
x = self.x_data[index]
y = self.y_data[index]
return x, y
使用:
python
dataset = MyDataset(x_data, y_data)
loader = DataLoader(dataset, batch_size=2, shuffle=True)
自定义 Dataset 必须实现两个方法:
python
__len__()
作用:返回数据集的样本数量
__getitem__(index)
作用:根据索引返回一个样本
例如:dataset[0]
实际上会调用:dataset.__getitem__(0)
8.多分类问题
前面的逻辑回归是二分类,类别只有 0 和 1
多分类表示类别有 3 个或更多,模型要从多个类别中选出一个。
(1)二分类和多分类的区别
二分类模型:
python
self.linear = torch.nn.Linear(输入特征数, 1)
输出 1 个 logit:logit = 2.3
经过 sigmoid 后得到:类别 1 的概率 = 0.91
多分类模型:
python
self.linear = torch.nn.Linear(输入特征数, 类别数量)
例如有 3 个类别:self.linear = torch.nn.Linear(2, 3)
模型会输出 3 个 logits:[-1.2, 0.5, 2.1]
分别对应:
类别 0 的分数:-1.2
类别 1 的分数:0.5
类别 2 的分数:2.1
这些 logits 不是概率,取值可以是任意实数。
(2)softmax 函数
多分类通常使用 softmax 把多个 logits 转成概率。
python
torch.softmax(logits, dim=1)
import torch
logits = torch.tensor([[-1.2, 0.5, 2.1]])
probability = torch.softmax(logits, dim=1)
print(probability)
print(probability.sum(dim=1))
输出大致是:
tensor([[0.0301, 0.1643, 0.8056]])
tensor([1.])
结果含义:
类别 0 的概率:3.01%
类别 1 的概率:16.43%
类别 2 的概率:80.56%
softmax 的特点:
- 每个概率都在 0 到 1 之间
- 同一个样本的所有类别概率之和等于 1
- 分数最大的类别,概率也最大
dim=1 表示沿着"类别"这一维计算 softmax。
如果 logits.shape 是:样本数量, 类别数量
那么:torch.softmax(logits, dim=1),会让每一行的概率和等于 1。
(3)多分类标签格式
假设有 3 个类别:
0:猫
1:狗
2:鸟
标签应该是类别编号:
python
y_data = torch.tensor([0, 1, 2, 1, 0])
标签形状是 [样本数量]
标签类型必须是 torch.long
例如:
y_data = torch.tensor([0, 1, 2], dtype=torch.long)
不要写成二分类的格式:
y_data = torch.tensor([[0.0],[1.0],[2.0]])
因为 CrossEntropyLoss 接收的是真实类
(4)CrossEntropyLoss
多分类交叉熵损失,等价于两步:
-
对输入 logits 做Softmax
-
计算交叉熵(衡量预测概率分布p和真实标签分布y之间差距)
python
nn.CrossEntropyLoss(weight=None, reduction="mean", label_smoothing=0.0)
reduction:"mean"(默认)/"sum"/"none",和 MSELoss、BCELoss 完全一致
weight:给不同类别设置权重,处理类别不平衡
label_smoothing:标签平滑,防止过拟合,0~1 之间
训练时写:
python
criterion = torch.nn.CrossEntropyLoss()
logits = model(x_data)
loss = criterion(logits, y_data)
CrossEntropyLoss 内部完成:logits -> softmax -> 计算交叉熵损失
因此模型的 forward() 中不要写 softmax。
python
def forward(self, x):
logits = self.linear(x)
return torch.softmax(logits, dim=1)
再配合:
python
criterion = torch.nn.CrossEntropyLoss()
会重复执行 softmax,影响训练。
(5)完整代码:2 个特征、3 个类别
python
import torch
# 每个样本有 2 个特征
x_data = torch.tensor([
[1.0, 1.0],
[1.0, 2.0],
[2.0, 1.0],
[5.0, 5.0],
[5.0, 6.0],
[6.0, 5.0],
[9.0, 1.0],
[9.0, 2.0],
[10.0, 1.0]
])
# 每个样本的类别编号
# 类别 0:前 3 个样本
# 类别 1:中间 3 个样本
# 类别 2:最后 3 个样本
y_data = torch.tensor([0, 0, 0, 1, 1, 1, 2, 2, 2], dtype=torch.long)
class MultiClassModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(2, 3)
def forward(self, x):
return self.linear(x)
model = MultiClassModel()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(1000):
logits = model(x_data)
loss = criterion(logits, y_data)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print("epoch:", epoch, "loss:", loss.item())
print("weight =", model.linear.weight)
print("bias =", model.linear.bias)
x_test = torch.tensor([
[1.5, 1.5],
[5.5, 5.5],
[9.5, 1.5]
])
model.eval()
with torch.no_grad():
logits = model(x_test)
probability = torch.softmax(logits, dim=1)
prediction = logits.argmax(dim=1)
print("logits =", logits)
print("probability =", probability)
print("prediction =", prediction) # tensor([0, 1, 2])
python
torch.nn.Linear(2, 3)
每个样本有 2 个输入特征
模型输出 3 个类别的 logits
python
logits = model(x_data)
print(logits.shape) # torch.Size([9, 3])
9 个样本,每个样本有 3 个类别分数
预测值:argmax(dim=1) 的作用是从每个样本的所有类别分数中,找出最大值所在的位置。
训练中其实不必计算 softmax 才能得到预测类别,softmax 不会改变最大值所在的位置。
prediction = logits.argmax(dim=1)
如果你查看每个类别的概率,才使用:
probability = torch.softmax(logits, dim=1)
(6)二分类和多分类对照
|--------|---------------------|----------------------|
| 项目 | 二分类 | 多分类 |
| 类别数量 | 2 | 3 个或更多 |
| 模型输出数量 | 1 个 logit | 每类 1 个 logit |
| Linear | Linear(特征数, 1) | Linear(特征数, 类别数) |
| 标签 | 样本数, 1,float32 | 样本数,long |
| 损失函数 | BCEWithLogitsLoss | CrossEntropyLoss |
| 概率转换 | sigmoid | softmax |
| 预测类别 | probability >= 0.5 | logits.argmax(dim=1) |
(7)多维特征与多分类
|-----------|----------------|--------------------|
| 项目 | 多维特征 | 多分类 |
| 关注对象 | 输入 | 输出 |
| 含义 | 一个样本有多个输入值 | 一个样本有多个可能类别 |
| 例子 | 年龄, 收入, 身高 | 猫, 狗, 鸟 |
| 影响 Linear | 输入数量 | 输出数量 |
| Linear | Linear(3, 1) | Linear(2, 3) |
| 标签 | 取决于任务 | 类别编号 |
| 损失函数 | 取决于任务 | 通常CrossEntropyLoss |
9.CNN
卷积神经网络,CNN 是一种专门处理图像数据的神经网络,常用于:
图片分类:判断图片是猫、狗还是鸟
目标检测:找出图片中目标的位置
图像分割:识别每个像素属于什么物体
人脸识别、医学影像分析等
为什么不能直接用全连接层处理图片?
一张灰度图片可以表示为数字矩阵。例如一张 3 x 3 图片:
[[0, 1, 0],[1, 1, 1],[0, 1, 0]]
最直接的方式是把图片展开成一行:0, 1, 0, 1, 1, 1, 0, 1, 0,再输入 Linear 层。
但这样有问题:
-
图片原本的上下、左右位置关系丢失了
-
图片尺寸稍大,参数数量会非常多
-
同一个特征移动位置后,模型可能无法识别
例如"眼睛"出现在图片左上角或中间,仍然应该被识别为眼睛。CNN 能利用图片的空间结构解决这个问题。
(1)CNN 的核心思想
CNN 不会一开始就看整张图片,而是使用一个小窗口在图片上移动,逐块提取特征。
这个小窗口叫卷积核,也常叫filter、kernel。
卷积核可以学习识别边缘、横线、竖线、纹理、局部形状。
CNN 的典型流程:
图片
↓
卷积层:提取局部特征
↓
激活函数:增加非线性能力
↓
池化层:压缩特征图
↓
重复多次
↓
全连接层:输出类别分数
CNN 输入图片时,常用形状是:
[batch_size, channel, height, width]
batch_size:一次输入多少张图片
channel:图片通道数
height:图片高度
width:图片宽度
例如,32 张黑白图片,每张大小是 28 x 28:32, 1, 28, 28
RGB 彩色图片有 3 个通道:32, 3, 224, 224
其中:
1:灰度图
3:RGB 彩色图
(2)一个最简单的 CNN 结构
python
import torch
class SimpleCNN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv = torch.nn.Conv2d(1, 8, kernel_size=3)
self.relu = torch.nn.ReLU()
self.pool = torch.nn.MaxPool2d(kernel_size=2)
self.flatten = torch.nn.Flatten()
self.linear = torch.nn.Linear(8 * 13 * 13, 10)
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
x = self.pool(x)
x = self.flatten(x)
return self.linear(x)
Conv2d:从图片中提取特征
ReLU:保留有效特征,增加模型表达能力
MaxPool2d:缩小图片特征的尺寸
Flatten:把多维特征变成一行
Linear:输出 10 个类别的分数
9.1 卷积层 Conv2d
python
Conv1d:一维卷积,常用于时间序列、音频、文本序列。
Conv2d:二维卷积,常用于图片。
Conv3d:三维卷积,常用于视频、医学 CT、三维数据。
卷积层的作用是从图片中提取局部特征,例如边缘、线条和纹理。
它使用一个小矩阵在图片上滑动。这个小矩阵叫卷积核。
卷积核 = kernel = filter
(1)卷积的计算
假设输入图片是 4 x 4:
python
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
]
卷积核大小是 2 x 2:
python
[
[1, 0],
[0, 1]
]
先放在图片左上角,取出局部区域:
python
[
[1, 2],
[5, 6]
]
对应位置相乘后求和:
python
1 * 1 + 2 * 0 + 5 * 0 + 6 * 1 = 7
卷积核向右移动一格,计算:
python
[
[2, 3],
[6, 7]
]
不断滑动后,得到新的矩阵:
python
[
[7, 9, 11],
[15, 17, 19],
[23, 25, 27]
]
这个输出叫:
特征图 feature map
卷积核的数值不是手工固定的,而是模型训练时通过反向传播自动学习的。
(2)PyTorch 中的 Conv2d
语法:
python
torch.nn.Conv2d(in_channels, out_channels, kernel_size)
in_channels:输入图片的通道数
out_channels:卷积核数量,也就是输出特征图数量
kernel_size:卷积核的高度和宽度
例如:
python
conv = torch.nn.Conv2d(1, 8, kernel_size=3)
输入是灰度图片,有 1 个通道
使用 8 个 3 x 3 卷积核
输出 8 张特征图
关于通道:
python
灰度图:1 个通道
灰度图每个像素只有一个亮度值:
0:黑色
255:白色
中间值:灰色
RGB 彩色图:3 个通道
彩色图每个像素包含:
R:红色强度
G:绿色强度
B:蓝色强度
RGBA 彩色图:4 个通道
R:红色
G:绿色
B:蓝色
A:透明度 alpha
(3)输入输出形状
输入图片形状:
python
[batch_size, channel, height, width]
例如:
python
x = torch.randn(32, 1, 28, 28)
32 张图片
每张是灰度图
高度 28
宽度 28
经过卷积:
python
conv = torch.nn.Conv2d(1, 8, kernel_size=3)
y = conv(x)
print(y.shape) #torch.Size([32, 8, 26, 26])
输出:
python
torch.Size([32, 8, 26, 26])
(4)stride
stride 表示卷积核每次移动多少格。默认是 stride=1。
例如:
python
conv = torch.nn.Conv2d(1, 8, kernel_size=3, stride=2)
输入仍然是:
[32, 1, 28, 28]
输出空间尺寸是:
(28 - 3) / 2 + 1 = 13.5
向下取整得到13。
(5)padding
padding 是在图片边缘补数字,通常补 0。
python
conv = torch.nn.Conv2d(1, 8, kernel_size=3, padding=1)
padding=1 表示在输入图片的四个边缘,各补 1 层 0。
输入是:
[32, 1, 28, 28]
输出形状:
[32, 8, 28, 28]
因为卷积前四周补了一圈 0,抵消了卷积后尺寸缩小的影响。
对于:
kernel_size=3
stride=1
padding=1
输入和输出高度、宽度相同。
这常被称为:same padding
输出尺寸公式
高度和宽度的计算公式:
输出大小 = (输入大小 + 2 * padding - kernel_size) / stride + 1
实际计算时,结果会向下取整。
检查卷积层参数
python
import torch
conv = torch.nn.Conv2d(
in_channels=3,
out_channels=16,
kernel_size=3,
stride=1,
padding=1
)
print("卷积层:", conv)
print("weight shape:", conv.weight.shape)
print("bias shape:", conv.bias.shape)
print("weight 参数数量:", conv.weight.numel())
print("bias 参数数量:", conv.bias.numel())
print("总参数数量:", sum(p.numel() for p in conv.parameters()))
卷积层: Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
bias shape: torch.Size([16])
weight 参数数量: 432
bias 参数数量: 16
总参数数量: 448
python
weight shape: torch.Size([16, 3, 3, 3])
16:有 16 个卷积核
3:每个卷积核处理 3 个输入通道
3:卷积核高度为 3
3:卷积核宽度为 3
python
weight 参数数量: 432
因为输入是 RGB 图片,所以有 3 个输入通道:
红色通道 R、绿色通道 G、蓝色通道 B。
一个卷积核不是只有一个 3 x 3 矩阵,而是包含:
R 通道:一个 3 x 3 矩阵
G 通道:一个 3 x 3 矩阵
B 通道:一个 3 x 3 矩阵
因此,一个卷积核的参数数量是 3 * 3 * 3 = 27
总共有 16 个卷积核 16 * 3 * 3 * 3 = 432
所以 weight 参数数量 = 432
python
bias 参数数量: 16
每个输出通道都有一个 bias。因为 out_channels = 16,所以 bias 有 16 个。
9.2 ReLU 激活函数
卷积层虽然能提取特征,但如果只叠加卷积层,整个网络仍然只是线性计算,表达能力有限。ReLU 用来加入非线性。
python
ReLU(x) = max(0, x)
规则很简单:
python
输入大于 0:保留原值
输入小于等于 0:变成 0
在 CNN 中通常写在卷积层之后:
python
import torch
x = torch.randn(32, 3, 28, 28)
conv = torch.nn.Conv2d(3, 16, kernel_size=3, padding=1)
relu = torch.nn.ReLU()
# 前向传播
y = conv(x)
y = relu(y)
print(y.shape)
ReLU 不改变形状:
输入:[32, 16, 28, 28]
输出:[32, 16, 28, 28]
它也没有需要训练的参数:
python
print(list(relu.parameters())) # []
CNN 中常见顺序:
输入图片
↓
Conv2d:提取特征
↓
ReLU:加入非线性
↓
MaxPool2d:缩小特征图
9.3 MaxPool2d 最大池化层
MaxPool2d 的作用是缩小特征图的高度和宽度,同时保留每个区域中最明显的特征。
CNN 中常见顺序:
python
Conv2d -> ReLU -> MaxPool2d
(1)最大池化的计算方式
假设输入特征图是:
python
[
[1, 3, 2, 4],
[5, 6, 1, 2],
[7, 2, 8, 3],
[4, 9, 0, 1]
]
使用:
python
pool = torch.nn.MaxPool2d(kernel_size=2)
默认:stride = kernel_size = 2,也就是每次取一个 2 x 2 区域,向右和向下移动 2 格。
第一个区域最大值为 6;向右移动两步,第二个区域最大值为4。
最终输出:
[
[6, 4],
[9, 8]
]
因此:
4 x 4 -> 2 x 2
(2)PyTorch 写法
python
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0)
常用写法:
pool = torch.nn.MaxPool2d(kernel_size=2)
等价于:
pool = torch.nn.MaxPool2d(kernel_size=2, stride=2)
也可以写成:
pool = torch.nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2))
其中两个数都是:(高度, 宽度)
输出大小公式:
最大池化和卷积层的输出尺寸计算方式基本相同:
python
输出大小 =
floor((输入大小 + 2 * padding - kernel_size) / stride) + 1
(3)MaxPool2d 没有可训练参数
卷积层有卷积核和 bias,需要通过训练更新。池化层只做"取最大值",没有需要学习的参数:
python
pool = torch.nn.MaxPool2d(kernel_size=2)
print(list(pool.parameters())) # []
(4)卷积、激活和池化组合
python
import torch
x = torch.randn(32, 3, 28, 28)
conv = torch.nn.Conv2d(3, 16, kernel_size=3, padding=1)
relu = torch.nn.ReLU()
pool = torch.nn.MaxPool2d(kernel_size=2)
x = conv(x)
print("卷积后:", x.shape)
x = relu(x)
print("ReLU 后:", x.shape)
x = pool(x)
print("池化后:", x.shape)
卷积后: torch.Size([32, 16, 28, 28])
ReLU 后: torch.Size([32, 16, 28, 28])
池化后: torch.Size([32, 16, 14, 14])
池化的主要作用:
1. 缩小特征图,减少计算量
2. 减少后续全连接层的参数数量
3. 保留局部区域中最强的特征
4. 对特征的小幅位置移动更不敏感
(5)MaxPool2d 和 Conv2d 的区别
项目 Conv2d MaxPool2d
作用 提取特征 缩小特征图
是否有参数 有 没有
是否改变通道数 可以改变 不改变
是否改变宽高 可以改变 通常缩小
核心计算 卷积核乘法、加 bias 取局部最大值
9.4 Flatten 和 Linear
经过卷积层和池化层后,图片仍然是四维 Tensor:
python
[batch_size, channels, height, width]
但全连接层 Linear 接收的输入通常是二维 Tensor:
python
[batch_size, features]
所以需要使用 Flatten 把每张图片的多维特征展开成一行。
(1)Flatten 的作用
python
torch.nn.Flatten(start_dim=1, end_dim=-1)
参数含义:
start_dim:从哪个维度开始展平
end_dim:展平到哪个维度,默认是最后一维
CNN 中通常写:flatten = torch.nn.Flatten(start_dim=1)
这里的 start_dim=1 表示:保留第 0 维 batch_size,从第 1 维开始展平。
示例
python
import torch
x = torch.randn(32, 16, 14, 14)
flatten = torch.nn.Flatten(start_dim=1)
y = flatten(x)
print("展平前:", x.shape)
print("展平后:", y.shape)
展平前: torch.Size([32, 16, 14, 14])
展平后: torch.Size([32, 3136])
(2)Linear 层的作用
python
torch.nn.Linear(in_features, out_features)
参数含义:
in_features:输入特征数量
out_features:输出特征数量
Linear 的输出形状
python
import torch
x = torch.randn(32, 3136)
linear = torch.nn.Linear(3136, 10)
y = linear(x)
print("输入形状:", x.shape)
print("输出形状:", y.shape)
输出:
输入形状: torch.Size([32, 3136])
输出形状: torch.Size([32, 10])
含义:
32:32 个样本
10:每个样本得到 10 个类别分数
9.5 完整模型代码
python
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 1. 加载并预处理 MNIST 数据集
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root="./data", train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root="./data", train=False, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=0)
# 2. 定义 CNN 模型
class CNN(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv1 = torch.nn.Conv2d(1, 16, kernel_size=3, padding=1)
self.relu = torch.nn.ReLU()
self.pool = torch.nn.MaxPool2d(kernel_size=2)
self.conv2 = torch.nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.flatten = torch.nn.Flatten(start_dim=1)
self.linear = torch.nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = self.flatten(x)
return self.linear(x)
# 3. 创建模型、损失函数和优化器
model = CNN()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
print("模型总参数数量:", sum(p.numel() for p in model.parameters()))
# 4. 训练模型
for epoch in range(5):
model.train()
total_loss = 0.0
for images, labels in train_loader:
logits = model(images)
loss = criterion(logits, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
average_loss = total_loss / len(train_loader)
print("epoch:", epoch + 1, "loss:", average_loss)
# 5. 在测试集上评估模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
logits = model(images)
prediction = logits.argmax(dim=1)
correct += (prediction == labels).sum().item()
total += labels.size(0)
# 6. 计算并输出准确率
accuracy = correct / total
print("测试集准确率:", accuracy)
(1)使用 MNIST 数据集
MNIST 是手写数字数据集,包含 10 个类别:
python
类别 0:数字 0
类别 1:数字 1
...
类别 9:数字 9
每张图片:
灰度图
大小为 28 x 28
通道数为 1
一批图片的形状:
python
[batch_size, 1, 28, 28]
加载 MNIST
python
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root="./data", train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root="./data", train=False, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=0)
ToTensor() 的作用:
python
1. 把图片转换成 Tensor
2. 把像素值从 0 到 255 转换到 0 到 1
3. 把灰度图片转换成 [1, 28, 28] 的形状
取出一个 batch:
python
images, labels = next(iter(train_loader))
print("图片形状:", images.shape)
print("标签形状:", labels.shape)
输出:
python
图片形状: torch.Size([64, 1, 28, 28])
标签形状: torch.Size([64])
标签形状是:64,因为每张图片只对应一个类别编号。
(2)定义损失函数和优化器
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
Adam:对于每个参数,Adam 会保存两个额外的变量:
python
m:梯度的移动平均值,表示梯度方向和动量
v:梯度平方的移动平均值,表示梯度大小
每次得到新的梯度 g 后,Adam 会更新:
python
m = beta1 * m + (1 - beta1) * g
v = beta2 * v + (1 - beta2) * g * g
最后使用它们更新参数:
参数 = 参数 - 学习率 * m / (sqrt(v) + epsilon)
python
项目 SGD Adam
更新依据 当前梯度 当前梯度和历史梯度
学习率 通常需要手动调整 会对不同参数自适应调整
训练速度 可能较慢 通常前期收敛较快
内存占用 较少 需要保存额外状态
使用难度 简单 使用方便
常见写法 lr=0.01 lr=0.001
(3)训练模型
python
for epoch in range(5):
model.train()
total_loss = 0.0
for images, labels in train_loader:
logits = model(images)
loss = criterion(logits, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
average_loss = total_loss / len(train_loader)
print("epoch:", epoch + 1, "loss:", average_loss)
每个 batch 的训练流程:
1. 读取一批图片和标签
2. 图片经过 CNN 得到 logits
3. logits 和标签计算损失
4. 清空旧梯度
5. 反向传播
6. 更新卷积层和全连接层参数
和线性模型的训练流程完全相同,变化的只是模型结构。
(4)在测试集上评估
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
logits = model(images)
prediction = logits.argmax(dim=1)
correct += (prediction == labels).sum().item()
total += labels.size(0)
accuracy = correct / total
print("测试集准确率:", accuracy)
这里:
logits.argmax(dim=1)
表示在 10 个类别中,选择分数最高的类别。
准确率计算:
accuracy = 预测正确的数量 / 总样本数量
python
CNN 的训练流程没有改变,
改变的是模型内部的计算结构。
Conv2d 负责提取特征,
ReLU 负责增加非线性,
MaxPool2d 负责缩小特征图,
Flatten 负责连接卷积层和全连接层,
Linear 负责输出类别分数。
(5)过拟合
过拟合是指模型在训练集上表现很好,但在验证集或测试集上表现较差。
如何通过损失判断过拟合:
正常训练时:训练损失下降,验证损失下降
出现过拟合后:训练损失继续下降,验证损失开始上升
训练损失很高,验证损失也很高:模型可能欠拟合
如何减轻过拟合
python
1. 增加训练数据
2. 使用数据增强
3. 减小模型规模
4. 添加 Dropout
5. 使用 BatchNorm
6. 使用权重衰减
7. 提前停止训练