PyTorch

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

现在的 wb 是我们手动指定的,还没有自动学习。

假设真实数据是:

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])

两者之间存在误差。后面我们会学习损失函数,用来衡量这个误差,再使用梯度下降调整 wb

(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 有几个好处:

  1. 不需要一次性加载全部数据

  2. 节省内存

  3. 可以使用 GPU 加速

  4. 参数会进行多次更新

  5. 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 层。

但这样有问题:

  1. 图片原本的上下、左右位置关系丢失了

  2. 图片尺寸稍大,参数数量会非常多

  3. 同一个特征移动位置后,模型可能无法识别

例如"眼睛"出现在图片左上角或中间,仍然应该被识别为眼睛。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. 提前停止训练
相关推荐
code_pgf1 小时前
SAMLabeler-MNN 项目使用与编译问题解决
人工智能·深度学习·mnn
LoveAmySun1 小时前
AI智能体如何落地公交营运真实业务
大数据·人工智能
深圳雨林凯AI1 小时前
雨林凯AI四方连图技术原理拆解:无缝拼接、元素重排与密度变化的实现
人工智能
DS随心转插件1 小时前
ChatGPT生成的pdf怎么导出 只要加个“AI导出鸭”
人工智能·chatgpt·pdf·deepseek·ai导出鸭
鸽芷咕1 小时前
5 分钟给 Claude/Cursor 接上“实时联网”能力|Bright Data MCP 实测
人工智能
adinnet20261 小时前
辅助写作与文档整理,从会议纪要到标书,让智能体当好“笔杆子“
大数据·人工智能
LadiesAndGentlemen1 小时前
概览篇:世界模型、空间智能与地理空间智能是什么关系
数据库·人工智能·自然语言处理·开源·aigc
卷无止境1 小时前
Python的contextlib与 FastAPI 中的上下文管理
后端·python·fastapi
fly76327851 小时前
免费篮球高光剪辑开源工具
人工智能