目录索引

Pytorch 开发

分类: 算法&模型训练

核心基础:张量

在 Sklearn 中,数据是 Numpy 数组;在 PyTorch 中,一切皆为 Tensor。

什么是 Tensor?

概念:多维矩阵,支持 GPU 加速,是深度学习的“血液”。

与 Numpy 对比:为什么不能直接用 Numpy?(自动求导、GPU支持)。

Tensor 可以记住“自己是怎么算出来的”

基本语法

从数据创建
import torch

a = torch.tensor([1, 2, 3])
b = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)

常用参数:

dtype:数据类型

device:cpu / cuda

requires_grad=True

生成指定形状
torch.zeros(3, 4)
torch.ones(2, 2)
torch.randn(3, 3)   # 正态分布
torch.rand(3, 3)    # 均匀分布
查看“张量长什么样”

查看 Tensor

基本属性

x.shape        # torch.Size([2, 3])
x.ndim         # 维度数
x.numel()      # 元素个数
x.dtype        # 数据类型
x.device       # 在 CPU 还是 GPU
Tensor 运算
基本算术(逐元素)
a + b
a - b
a * b
a / b
矩阵运算
a @ b           # 矩阵乘
torch.matmul(a, b)
常见函数
torch.mean(x)
torch.sum(x)
torch.max(x)
torch.min(x)
张量操作 (维度变换)
改形状(不改数据)
x.view(3, 2)
x.reshape(3, 2)
转置 / 维度交换
x.T
x.transpose(0, 1)
x.permute(1, 0)
增/删维度
x.unsqueeze(0)   # 增加维度
x.squeeze()      # 删除 size=1 的维度
索引 & 切片
x[0]
x[:, 1]
x[0, 1]
x[x > 0]        # 布尔索引

Tensor 之间转换

Tensor ↔ NumPy
x_np = x.numpy()
x_t = torch.from_numpy(x_np)

** 共享内存,改一个另一个也会变**

改类型
x.float()
x.long()
x.int()
设备迁移
x = x.to("cuda")
x = x.to("cpu")

或者

x = x.cuda()

自动求导

开启梯度
x = torch.tensor(2.0, requires_grad=True)
反向传播
y = x ** 2
y.backward()
print(x.grad)
禁用梯度
with torch.no_grad():
    y = model(x)

数据工程:Dataset 与 DataLoader

为什么需要

Dataset 管“数据是什么”,DataLoader 管“数据怎么喂给模型”

Dataset → 定义 “一条数据是什么” DataLoader → 定义 “怎么批量、并行、随机加载”

自定义 Dataset

Dataset 是一个“可被索引的数据集合”

你只需要实现 2 个东西

__len__()
__getitem__(index)
自定义 Dataset 模板
from torch.utils.data import Dataset

class MyDataset(Dataset):
    def __init__(self, data, labels):
        self.data = data
        self.labels = labels

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        x = self.data[idx]
        y = self.labels[idx]
        return x, y

只要能 dataset[i],PyTorch 就能用

真实项目
class ImageDataset(Dataset):
    def __init__(self, img_paths, transform=None):
        self.img_paths = img_paths
        self.transform = transform

    def __getitem__(self, idx):
        img = Image.open(self.img_paths[idx])
        if self.transform:
            img = self.transform(img)
        return img

    def __len__(self):
        return len(self.img_paths)

Dataset 不关心 batch、不关心 shuffle、不关心多进程,它只负责回答一个问题:“第 i 条数据是什么?”

数据加载器 - DataLoader

一个把 Dataset 变成“可直接 for 循环训练”的工具

基本语法
from torch.utils.data import DataLoader

dataloader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True
)

简单的训练循环
for epoch in range(epochs):
    for x, y in dataloader:
        output = model(x)
核心参数

batch_size

每次喂给模型多少条数据

越大:

  • 显存占用越高
  • 梯度越稳定

越小:

  • 更新更频繁
  • 可能更容易震荡

shuffle 是否打乱数据

训练集必须 True

验证 / 测试集一般 False

num_workers

使用多少个子进程加载数据

建议经验值:

  • Linux:CPU 核数 / 2
  • Windows:通常 0 或 2

drop_last

丢掉最后一个不满 batch 的数据

常用于:

  • BatchNorm
  • 分布式训练

pin_memory

加快 CPU → GPU 拷贝

collate_fn

  • 变长文本
  • 不规则数据
  • 自定义 batch 组织方式

Dataset = “第 i 条数据是什么”

DataLoader = “怎么成批喂给模型”

Dataset 只实现 __len____getitem__

训练集必须 shuffle=True

GPU 慢,先看 num_workers

数据预处理:Transforms

Transforms 是对“单条样本”做的可组合预处理操作

from torchvision import transforms
最核心的工具:Compose
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean, std)
])

常用预处理

格式转换类
transforms.ToTensor()

PIL / ndarray → Tensor

像素值:0~255 → 0~1

维度顺序:HWC → CHW

尺寸 & 裁剪
transforms.Resize(256)
transforms.CenterCrop(224)
transforms.RandomCrop(224)

CenterCrop:验证 / 测试集

RandomCrop:训练集(增强)

翻转 & 旋转(增强)
transforms.RandomHorizontalFlip(p=0.5)
transforms.RandomRotation(15)
颜色扰动(高级增强)
transforms.ColorJitter(
    brightness=0.2,
    contrast=0.2,
    saturation=0.2,
    hue=0.1
)

模拟:

  • 光照变化
  • 拍摄设备差异
归一化
transforms.Normalize(
    mean=[0.485, 0.456, 0.406],
    std=[0.229, 0.224, 0.225]
)

含义:

x' = (x - mean) / std

组合操作

标准的图像预处理流水线

训练集:

train_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean, std)
])

验证 / 测试集:

val_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean, std)
])
Transforms 和 Dataset 的关系
class MyDataset(Dataset):
    def __init__(self, transform=None):
        self.transform = transform

    def __getitem__(self, idx):
        img = Image.open(...)
        if self.transform:
            img = self.transform(img)
        return img

模型构建:nn.Module

nn.Module 是 PyTorch 中“所有模型与层的基类

神经网络 = 一个 nn.Module

网络中的每一层 = 一个 nn.Module

甚至 loss 函数,也常是 nn.Module

基本骨架

最小可用模型模板
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 1. 定义层(参数在这里创建)

    def forward(self, x):
        # 2. 定义前向传播
        return x
三条铁律

必须继承 nn.Module

必须调用 super().__init__()

前向逻辑只写在 forward()

nn.Module 的核心能力
自动管理参数
model.parameters()

返回所有 requires_grad=True 的 Tensor

优化器直接接管

自动切换模式
model.train()
model.eval()

影响:

  • Dropout
  • BatchNorm
设备统一迁移
model.to("cuda")

常用层

全连接层
nn.Linear(in_features, out_features)

用途:

  • 分类
  • 回归
  • MLP
卷积层

图像 / 特征图核心

nn.Conv2d(
    in_channels,
    out_channels,
    kernel_size,
    stride=1,
    padding=0
)

池化层(Pool)
nn.MaxPool2d(2)
nn.AvgPool2d(2)

作用:

  • 降维
  • 增强平移不变性
归一化层
nn.BatchNorm2d(num_features)
nn.LayerNorm(normalized_shape)

区别(直觉版):

  • BatchNorm:批内归一
  • LayerNorm:样本内归一
Dropout(防过拟合)
nn.Dropout(p=0.5)

训练时随机失活

推理时自动关闭

激活函数

ReLU
nn.ReLU()

特点:

计算快

不饱和

大多数任务首选

LeakyReLU
nn.LeakyReLU(0.01)解决:

ReLU 死亡问题

Sigmoid
nn.Sigmoid()

用途:

  • 二分类输出
  • 概率建模
Tanh
nn.Tanh()

输出范围:[-1, 1]

比 Sigmoid 好一点,但仍易饱和

Softmax
nn.Softmax(dim=1)

CrossEntropyLoss 不要一起用

训练机制:Loss 与 Optimizer

模型参数与拟合过程

​ 训练 = 让模型参数一步步调整,使 Loss 变小

输入数据 x
   ↓
模型 forward(x)
   ↓
得到预测 ŷ
   ↓
Loss(ŷ, y)  ← 衡量“错得有多离谱”
   ↓
backward()  ← 算参数该怎么改
   ↓
optimizer.step() ← 真正改参数

损失函数 (Loss Function)

Loss 用一个数,量化“模型当前有多差”

回归任务
MSE(均方误差)
nn.MSELoss()

公式直觉:

(预测 - 真实)²

特点:

  • 对异常值敏感
  • 回归最常见
二分类
BCE(二元交叉熵)
nn.BCELoss()

⚠️ 要求:

  • 输出已经是 Sigmoid 后的概率
BCEWithLogitsLoss
nn.BCEWithLogitsLoss()

等价于:

Sigmoid + BCELoss

但:

  • 数值更稳定
  • 实战首选
多分类
CrossEntropyLoss
nn.CrossEntropyLoss()
  1. 模型输出 raw logits
  2. 不要手写 Softmax
  3. 标签是类别索引(不是 one-hot)
# 正确示例
logits = model(x)      # [B, C]
loss = criterion(logits, y)  # y: [B]
分类 + 不平衡数据
nn.CrossEntropyLoss(weight=class_weights)

优化器 (Optimizer)

Optimizer 根据梯度,决定“参数怎么改、改多少”

最基本的:SGD
torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9
)

特点:

可解释性强

但收敛慢

实战王者:Adam
torch.optim.Adam(
    model.parameters(),
    lr=1e-3
)

优点:

  • 自适应学习率
  • 开箱即用
  • 新手首选

Loss 与 Optimizer 如何配合

optimizer.zero_grad()  # 1. 清空旧梯度
loss.backward()        # 2. 反向传播
optimizer.step()       # 3. 更新参数
一个完整的训练 step
for x, y in dataloader:
    x, y = x.to(device), y.to(device)

    optimizer.zero_grad()

    output = model(x)
    loss = criterion(output, y)

    loss.backward()
    optimizer.step()

核心动作:训练循环

这是 PyTorch 与 Sklearn 最大的不同。Sklearn 的一行 .fit(),在 PyTorch 中需要展开为以下 5步标准代码块

  1. 数据传输images, labels = images.to(device), labels.to(device)
  2. 前向传播outputs = model(images) (模型预测)
  3. 计算损失loss = criterion(outputs, labels) (计算差距)
  4. 梯度清零optimizer.zero_grad() (清除上一步的残留)
  5. 反向传播与更新loss.backward() (求导) -> optimizer.step() (更新参数)

每一步在“干什么”

数据传输(Device Placement)
images, labels = images.to(device), labels.to(device)

把数据搬到和模型同一设备

CPU ↔ GPU 不允许混算

前向传播(Forward)
outputs = model(images)

调用 model.forward()

自动构建计算图

中间结果被“记账”

计算损失(Loss)
loss = criterion(outputs, labels)

把预测误差压缩成一个标量

Loss 是反向传播的“起点”

梯度清零(Zero Grad)
optimizer.zero_grad()

PyTorch 的梯度是 累加的

不清零 → 梯度叠加 → 参数乱飞

反向传播 & 参数更新
loss.backward()
optimizer.step()

loss.backward()

  • 从 Loss 出发
  • 沿计算图反向传播
  • 把梯度存到 param.grad

optimizer.step()

  • 真正修改参数值
  • 梯度 用完即丢
标准训练循环
model.train()  # 训练模式

for epoch in range(num_epochs):
    for images, labels in dataloader:

        # 1. 数据传输
        images = images.to(device)
        labels = labels.to(device)

        # 2. 梯度清零
        optimizer.zero_grad()

        # 3. 前向传播
        outputs = model(images)

        # 4. 计算损失
        loss = criterion(outputs, labels)

        # 5. 反向传播 & 更新
        loss.backward()
        optimizer.step()

模型评估与保存

评估模式

为什么要评估模式?

​ 训练时和推理时,模型行为并不一样

受影响的层主要是:

  • Dropout:训练时随机丢,评估时全开
  • BatchNorm:训练用 batch 统计,评估用历史统计
标准验证循环模板
model.eval()

correct = 0
total = 0
val_loss = 0.0

with torch.no_grad():
    for images, labels in val_loader:
        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)
        loss = criterion(outputs, labels)
        val_loss += loss.item()

        preds = outputs.argmax(dim=1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)

accuracy = correct / total

计算指标

分类任务
Accuracy(准确率)
preds = outputs.argmax(dim=1)
acc = (preds == labels).float().mean()

适合:

  • 类别均衡
Precision / Recall / F1

Precision:预测为正的有多准

Recall:正样本找回了多少

F1:二者平衡

from sklearn.metrics import classification_report

保存与加载

只保存参数
torch.save(model.state_dict(), "model.pth")
保存整个模型
torch.save(model, "model.pth")
加载模型
model = MyModel()
model.load_state_dict(torch.load("model.pth"))
model.to(device)
model.eval()
保存训练状态
torch.save({
    "epoch": epoch,
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),
    "loss": loss
}, "checkpoint.pth")
加载
ckpt = torch.load("checkpoint.pth")
model.load_state_dict(ckpt["model"])
optimizer.load_state_dict(ckpt["optimizer"])
start_epoch = ckpt["epoch"] + 1

实战演练

PyTorch 实现线性回归

# =========================
# PyTorch 线性回归(完整示例)
# =========================

import torch
import torch.nn as nn
import torch.optim as optim

# -------------------------
# 1. 构造训练数据
# -------------------------
# 假设真实关系是:y = 2x + 1
# x: 输入特征,形状是 [样本数, 特征数]
x = torch.tensor([[1.0],
                  [2.0],
                  [3.0],
                  [4.0]])

# y: 真实标签
y = torch.tensor([[3.0],
                  [5.0],
                  [7.0],
                  [9.0]])

# -------------------------
# 2. 定义线性回归模型
# -------------------------
# nn.Linear 本质就是 y = x * w + b
# in_features=1  -> 每个样本 1 个特征
# out_features=1 -> 输出 1 个值
model = nn.Linear(in_features=1, out_features=1)

# -------------------------
# 3. 定义损失函数
# -------------------------
# 均方误差(Mean Squared Error)
# loss = mean((y_pred - y_true)^2)
criterion = nn.MSELoss()

# -------------------------
# 4. 定义优化器
# -------------------------
# 使用随机梯度下降(SGD)
# model.parameters() 会返回 w 和 b
# lr 是学习率,控制每一步走多大
optimizer = optim.SGD(model.parameters(), lr=0.01)

# -------------------------
# 5. 训练模型
# -------------------------
epochs = 1000

for epoch in range(epochs):

    # (1)前向传播
    # 把 x 喂给模型,得到预测值 y_pred
    y_pred = model(x)

    # (2)计算损失
    # 衡量预测值和真实值之间的差距
    loss = criterion(y_pred, y)

    # (3)梯度清零
    # PyTorch 的梯度是累加的,所以每轮都要清零
    optimizer.zero_grad()

    # (4)反向传播
    # 自动计算 loss 对 w 和 b 的梯度
    loss.backward()

    # (5)更新参数
    # 根据梯度和学习率更新 w 和 b
    optimizer.step()

    # 每 100 轮打印一次损失
    if epoch % 100 == 0:
        print(f"Epoch {epoch}, Loss = {loss.item():.6f}")

# -------------------------
# 6. 查看训练后的参数
# -------------------------
# model.parameters() 中依次是 w 和 b
w, b = model.parameters()

print("\n训练完成后的参数:")
print("w =", w.item())
print("b =", b.item())

# -------------------------
# 7. 使用模型进行预测
# -------------------------
# 用训练好的模型预测一个新值
x_test = torch.tensor([[5.0]])
y_test_pred = model(x_test)

print("\n预测结果:")
print(f"当 x = 5 时,预测 y = {y_test_pred.item():.2f}")

PyTorch 实现逻辑回归

# =========================
# PyTorch 逻辑回归(完整示例)
# =========================

import torch
import torch.nn as nn
import torch.optim as optim

# -------------------------
# 1. 构造训练数据
# -------------------------
# 二分类数据:输入 x, 标签 y
# 我们用一个简单规则:y = 1 if x > 2 else 0
x = torch.tensor([[1.0],
                  [2.0],
                  [3.0],
                  [4.0]])
y = torch.tensor([[0.0],
                  [0.0],
                  [1.0],
                  [1.0]])

# -------------------------
# 2. 定义逻辑回归模型
# -------------------------
# 逻辑回归本质是:
# y_pred = sigmoid(x * w + b)
class LogisticRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)  # 1 个特征 -> 1 个输出

    def forward(self, x):
        # sigmoid 激活函数把输出映射到 [0,1],表示概率
        return torch.sigmoid(self.linear(x))

model = LogisticRegression()

# -------------------------
# 3. 定义损失函数
# -------------------------
# 二分类任务用二元交叉熵损失
criterion = nn.BCELoss()  # Binary Cross Entropy Loss

# -------------------------
# 4. 定义优化器
# -------------------------
optimizer = optim.SGD(model.parameters(), lr=0.1)

# -------------------------
# 5. 训练模型
# -------------------------
epochs = 1000
for epoch in range(epochs):
    # 前向传播
    y_pred = model(x)

    # 计算损失
    loss = criterion(y_pred, y)

    # 梯度清零
    optimizer.zero_grad()

    # 反向传播
    loss.backward()

    # 更新参数
    optimizer.step()

    if epoch % 100 == 0:
        print(f"Epoch {epoch}, Loss = {loss.item():.6f}")

# -------------------------
# 6. 查看训练后的参数
# -------------------------
w, b = model.linear.parameters()
print("\n训练完成后的参数:")
print("w =", w.item())
print("b =", b.item())

# -------------------------
# 7. 使用模型进行预测
# -------------------------
# 输出的是概率
x_test = torch.tensor([[1.5],
                       [2.5],
                       [3.5]])
y_test_prob = model(x_test)

# 转换为 0 或 1 分类
y_test_pred = (y_test_prob >= 0.5).float()

print("\n预测结果:")
for xi, yi_prob, yi_pred in zip(x_test, y_test_prob, y_test_pred):
    print(f"x={xi.item():.1f}, 概率={yi_prob.item():.3f}, 预测类别={int(yi_pred.item())}")

简单的神经网络

# =========================
# PyTorch 简单神经网络 MNIST 手写数字预测
# =========================

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# -------------------------
# 1. 数据预处理 & 加载
# -------------------------
# MNIST 图片大小 28x28,灰度图
# 转换为 Tensor 并归一化到 [0,1]
transform = transforms.Compose([
    transforms.ToTensor()
])

# 下载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

# DataLoader 批量读取
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=64, shuffle=False)

# -------------------------
# 2. 定义神经网络模型
# -------------------------
# 简单的全连接神经网络:
# 输入层 28*28=784 -> 隐藏层 128 -> 输出层 10
class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(28*28, 128)  # 输入层 -> 隐藏层
        self.relu = nn.ReLU()             # 激活函数
        self.fc2 = nn.Linear(128, 10)     # 隐藏层 -> 输出层

    def forward(self, x):
        x = x.view(x.size(0), -1)  # 展平图片 [batch, 28*28]
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)             # 输出 logits
        return x

model = SimpleNN()

# -------------------------
# 3. 定义损失函数 & 优化器
# -------------------------
# 多分类问题用 CrossEntropyLoss(内部自带 softmax)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# -------------------------
# 4. 训练模型
# -------------------------
epochs = 5
for epoch in range(epochs):
    model.train()  # 训练模式
    running_loss = 0.0
    for images, labels in train_loader:
        # 前向传播
        outputs = model(images)
        loss = criterion(outputs, labels)

        # 梯度清零
        optimizer.zero_grad()
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()

        running_loss += loss.item()

    print(f"Epoch [{epoch+1}/{epochs}], Loss: {running_loss/len(train_loader):.4f}")

# -------------------------
# 5. 测试模型准确率
# -------------------------
model.eval()  # 测试模式
correct = 0
total = 0
with torch.no_grad():  # 测试时不计算梯度
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)  # 取最大值的索引作为预测类别
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f"\n测试集准确率: {100 * correct / total:.2f}%")

# -------------------------
# 6. 使用模型预测单张图片
# -------------------------
import matplotlib.pyplot as plt

# 随机取一张测试图片
image, label = test_dataset[0]
plt.imshow(image.squeeze(), cmap='gray')
plt.title(f"真实标签: {label}")
plt.show()

# 模型预测
model.eval()
with torch.no_grad():
    output = model(image.unsqueeze(0))  # 增加 batch 维度
    pred = torch.argmax(output, dim=1).item()
    print(f"模型预测: {pred}")