3. PyTorch基础回顾:张量操作、自动求导、nn.Module构建模型
说实话,很多做工业缺陷检测的朋友,一上来就急着调模型、跑训练。结果呢?模型结构写错了都不知道,梯度传不下去也查不出原因。我个人习惯是,先把PyTorch这三块基本功打扎实——张量、自动求导、模型构建。这三样搞明白了,后面转ONNX、做推理,你心里才有底。
3.1 张量操作:一切数据的起点
张量(Tensor)就是PyTorch里的“数组”,但它比NumPy的ndarray多了GPU加速和自动求导的能力。我在做缺陷检测项目时,经常需要把工业相机采集的原始图像数据转成张量,再做归一化、裁剪、拼接。
3.1.1 创建张量的几种方式
| 方法 | 说明 | 常用场景 |
|---|---|---|
torch.tensor() |
从列表或NumPy数组创建 | 加载单张图像数据 |
torch.zeros() |
创建全零张量 | 初始化特征图或掩码 |
torch.ones() |
创建全一张量 | 偏置初始化 |
torch.randn() |
标准正态分布随机张量 | 权重初始化 |
torch.arange() |
创建等差序列 | 生成标签索引 |
import torch
# 从列表创建
data = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
print(data.shape) # torch.Size([2, 2])
# 创建全零张量,指定形状和设备
mask = torch.zeros((1, 256, 256), device='cuda' if torch.cuda.is_available() else 'cpu')
dtype。工业模型推理时,ONNX默认用float32,你训练时如果用float64,转ONNX会出警告。我踩过这个坑,后来一律用 torch.float32。
3.1.2 张量的形状操作
缺陷检测模型里,特征图的形状变换特别频繁。比如从卷积层出来的特征图是 [B, C, H, W],要送入全连接层就得先展平。
x = torch.randn(4, 3, 224, 224) # 模拟一个batch的4张图像
# 展平:从第1维开始展平
x_flat = x.view(4, -1) # 形状变为 [4, 3*224*224]
print(x_flat.shape) # torch.Size([4, 150528])
# 转置:交换维度
x_transposed = x.transpose(1, 3) # 形状变为 [4, 224, 224, 3]
# 增加/减少维度
x_unsqueeze = x.unsqueeze(0) # 在batch维前加一维,[1, 4, 3, 224, 224]
x_squeeze = x_unsqueeze.squeeze(0) # 去掉第0维,回到 [4, 3, 224, 224]
view() 要求张量在内存中是连续的。如果你做了转置或切片操作,记得先调 .contiguous() 再 view()。我曾经在写自定义网络时,因为没加 contiguous(),模型跑着跑着就报错,查了半天才发现是形状不连续的问题。
3.1.3 张量的数学运算
这些跟NumPy几乎一样,但多了自动求导的追踪能力。
a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
b = torch.tensor([4.0, 5.0, 6.0])
c = a + b # 加法
d = a * b # 逐元素乘
e = torch.matmul(a, b) # 点积,结果是 1*4 + 2*5 + 3*6 = 32.0
f = torch.mean(a) # 求均值
3.2 自动求导:梯度从哪来
自动求导(Autograd)是PyTorch训练模型的基石。说白了,你定义好前向计算图,PyTorch自动帮你算梯度。你想想看,如果没有这个机制,手写反向传播得有多痛苦?
3.2.1 计算图与梯度
每次你对一个张量做运算,PyTorch就会在后台构建一个计算图。这个图记录了数据流向和运算关系。当你调用 .backward() 时,它沿着图反向传播,计算每个节点的梯度。
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1 # y = x^2 + 3x + 1
y.backward() # 反向传播
print(x.grad) # dy/dx = 2x + 3 = 2*2 + 3 = 7.0
requires_grad=True 的张量才会被追踪。模型参数默认就是 requires_grad=True,而输入数据通常不需要梯度。
3.2.2 梯度清零与detach
训练时,每次反向传播后梯度会累积。所以每个batch都要手动清零。另外,有时候你不想让某个操作影响梯度,就用 .detach() 把它从计算图中剥离。
# 梯度清零
optimizer.zero_grad() # 或者手动:model.zero_grad()
# detach示例:比如你想看中间特征图的值,但不希望它影响梯度
features = model(x)
features_detached = features.detach() # 从计算图中剥离
print(features_detached.numpy()) # 可以转成numpy,不影响原图
3.3 nn.Module构建模型:搭积木的艺术
PyTorch用 nn.Module 来组织模型。你只需要继承这个类,在 __init__ 里定义层,在 forward 里定义前向传播逻辑。我习惯把模型拆成几个子模块,每个子模块负责一个功能,这样调试起来特别方便。
3.3.1 一个简单的缺陷检测分类模型
import torch.nn as nn
import torch.nn.functional as F
class DefectClassifier(nn.Module):
def __init__(self, num_classes=5):
super(DefectClassifier, self).__init__()
# 卷积层:输入3通道,输出16通道,卷积核3x3
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(16)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(32)
# 全连接层
self.fc = nn.Linear(32 * 56 * 56, num_classes) # 假设输入224x224,两次池化后56x56
def forward(self, x):
x = self.pool(F.relu(self.bn1(self.conv1(x)))) # [B, 16, 112, 112]
x = self.pool(F.relu(self.bn2(self.conv2(x)))) # [B, 32, 56, 56]
x = x.view(x.size(0), -1) # 展平
x = self.fc(x) # [B, num_classes]
return x
3.3.2 模型参数管理与设备迁移
模型建好后,你需要管理它的参数,并把它放到GPU上。这个操作在工业部署时特别重要——你总不能在CPU上跑推理吧?
model = DefectClassifier(num_classes=5)
# 查看模型参数
for name, param in model.named_parameters():
print(f"{name}: {param.shape}")
# 迁移到GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
# 训练时,输入数据也要迁移
inputs = torch.randn(4, 3, 224, 224).to(device)
labels = torch.randint(0, 5, (4,)).to(device)
Expected all tensors to be on the same device。这个错误很常见,排查时先检查设备一致性。
3.3.3 训练循环模板
下面是我常用的训练循环模板,简洁但完整。工业项目里,你只需要在这个框架里加数据加载、验证逻辑和模型保存就行。
import torch.optim as optim
model = DefectClassifier(num_classes=5).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(10):
model.train()
running_loss = 0.0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}")
3.4 本章知识结构图
下面这张图帮你理清PyTorch三块核心内容的关系。记住:张量是数据载体,自动求导是训练引擎,nn.Module是模型骨架。三者缺一不可。
3.5 小结
这一章我们回顾了PyTorch最核心的三个基础:张量操作、自动求导和nn.Module模型构建。说白了,这些就是你在工业缺陷检测项目里每天都要打交道的东西。我个人建议,别急着往下赶,先把这几个代码示例自己敲一遍,跑通。后面转ONNX的时候,你会感谢现在打下的基础。