1. 从零搭建深度学习环境:别让第一步就卡住你
1.1 为什么环境配置是新手最大的拦路虎
我见过太多人学深度学习,代码还没写一行,光装环境就折腾了三天。Python版本不对、numpy装不上、显卡驱动和CUDA版本打架,这些问题几乎每个入门的人都会遇到。说实话,这跟深度学习本身没关系,纯粹是工具链的问题。但就是这些工具链问题,劝退了至少一半想学深度学习的人。
所以这一篇,我打算把环境配置这件事彻底讲透。不是那种“你装个Anaconda就行了”的敷衍说法,而是把每个环节为什么这么做、遇到问题怎么排查,都给你掰开揉碎讲清楚。
先说一个核心原则:深度学习环境的核心诉求是版本匹配。Python版本、CUDA版本、cuDNN版本、PyTorch/TensorFlow版本,这四者之间有一个严格的对应关系。你随便挑最新版本装,大概率跑不起来。这不是谁故意为难你,而是GPU加速依赖底层C++库的ABI兼容性,版本错一个数字就可能报一堆看不懂的链接错误。
1.2 Python安装:官网下载还是包管理器
关于Python安装,网上有两种主流做法。一种是从python官网下载安装包,另一种是用Anaconda或者Miniconda。我的建议很明确:做深度学习,直接用Miniconda。
原因很简单。深度学习项目天然需要虚拟环境隔离——你不可能用一个环境跑所有项目,因为PyTorch 1.x和2.x的API有差异,TensorFlow 1和2更是几乎两套东西。Conda的虚拟环境管理比Python自带的venv更顺手,尤其是在处理非Python依赖(比如CUDA运行时库)的时候,Conda能帮你自动解决很多底层库的版本问题。
安装Miniconda的步骤不复杂:
# 下载Miniconda安装包(Linux示例) wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh # 执行安装 bash Miniconda3-latest-Linux-x86_64.sh # 安装完成后初始化shell conda init bash source ~/.bashrcWindows用户直接去Miniconda官网下载exe安装包,双击安装就行。安装时注意勾选“Add to PATH”,虽然官方不推荐,但对新手来说省事很多。
装完之后验证一下:
conda --version python --version如果两条命令都能正常输出版本号,说明基础环境没问题。
注意:不要同时装多个Python版本到系统PATH里,否则后面会出现“明明装了库却提示找不到”的诡异问题。用Conda环境隔离,每个项目一个环境,这是最稳妥的做法。
1.3 创建专属深度学习环境
接下来创建一个专门用于深度学习的Conda环境。我一般用Python 3.8到3.10之间的版本,太新了有些库还没适配,太旧了很多新特性用不了。
# 创建名为dl的环境,指定Python 3.9 conda create -n dl python=3.9 # 激活环境 conda activate dl激活之后,你的命令行前面会出现(dl)的标识,说明当前操作都在这个环境里。
然后是安装核心库。这里有个关键选择:装CPU版还是GPU版。如果你有NVIDIA显卡,并且想用GPU加速训练,那就要装GPU版。如果没有独显或者只是先跑通流程,CPU版完全够用。
CPU版安装很简单:
pip install torch torchvision torchaudioGPU版需要去PyTorch官网查对应的CUDA版本命令。比如CUDA 11.8对应的命令是:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118这里有个坑要提醒:不要自己手动去装CUDA Toolkit和cuDNN,除非你有特殊需求。PyTorch的pip包已经自带了运行时的CUDA库,你只需要保证显卡驱动足够新就行。手动装CUDA Toolkit反而容易导致版本冲突。
验证GPU是否可用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果第一行输出True,恭喜你,GPU环境搞定了。如果输出False,先检查显卡驱动版本,再检查装的PyTorch是不是GPU版。
1.4 常用辅助库的安装与避坑
除了PyTorch,深度学习项目还会用到一堆辅助库。我把常用的列一下,顺便说说哪些容易出问题。
| 库名 | 用途 | 安装命令 | 常见问题 |
|---|---|---|---|
| numpy | 数值计算基础 | pip install numpy | 版本过高可能与旧代码不兼容 |
| matplotlib | 绘图可视化 | pip install matplotlib | 中文显示需要额外配置字体 |
| opencv-python | 图像处理 | pip install opencv-python | 与opencv-contrib版本冲突 |
| pandas | 数据处理 | pip install pandas | 一般无坑 |
| scikit-learn | 传统机器学习 | pip install scikit-learn | 与numpy版本有依赖关系 |
| tqdm | 进度条 | pip install tqdm | 无坑,强烈推荐 |
| tensorboard | 训练可视化 | pip install tensorboard | 端口冲突时需换端口 |
opencv这个库我要特别说一下。很多人搜“python下载cv2”然后直接pip install cv2,这是错的。正确的包名是opencv-python,导入的时候才叫cv2。这个命名不一致坑了无数新手。
# 正确安装 pip install opencv-python # 正确导入 import cv2 print(cv2.__version__)还有一个常见问题是numpy版本。有些老代码用了np.float、np.int这些别名,在新版numpy里已经被移除了。如果你跑别人的代码报AttributeError: module 'numpy' has no attribute 'float',要么降numpy版本,要么把代码里的np.float改成np.float64。我建议改代码,因为降版本会引发更多连锁问题。
2. 前馈神经网络:深度学习的“Hello World”
2.1 从生物神经元到数学公式
前馈神经网络是理解一切深度学习模型的基础。卷积神经网络、循环神经网络、Transformer,本质上都是前馈网络的特化版本。所以这个基础打不牢,后面学什么都费劲。
先说说它为什么叫“前馈”。信息从输入层进入,经过隐藏层逐层变换,最后从输出层出来,整个过程是单向的,没有反馈回路。就像一条单向流水线,原料从一头进,成品从另一头出。
一个神经元做的事情其实很简单:接收输入,做加权求和,加上偏置,然后过一个激活函数。用公式表示就是:
y = f(w1*x1 + w2*x2 + ... + wn*xn + b)其中w是权重,b是偏置,f是激活函数。权重决定了每个输入的重要性,偏置决定了神经元激活的阈值,激活函数引入非线性。
为什么需要激活函数?如果没有激活函数,多层网络叠加起来等价于一个单层线性变换。你堆一百层也没用,表达能力跟一层一样。激活函数打破了这种线性,让网络能拟合任意复杂的函数。
常用的激活函数有这么几个:
- Sigmoid:把输出压缩到(0,1),适合二分类输出层。但深层网络里容易梯度消失,现在隐藏层基本不用了。
- Tanh:压缩到(-1,1),比Sigmoid好一点,但梯度消失问题依然存在。
- ReLU:max(0, x),计算简单,梯度不衰减,是目前隐藏层的默认选择。
- Leaky ReLU:给负半轴一个小斜率,解决ReLU神经元“死亡”问题。
- Softmax:多分类输出层专用,把一组数变成概率分布。
2.2 用PyTorch手写一个前馈网络
理论说再多不如动手写一遍。下面用PyTorch构建一个用于手写数字识别的前馈网络。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # 定义网络结构 class FeedForwardNet(nn.Module): def __init__(self): super(FeedForwardNet, self).__init__() self.flatten = nn.Flatten() self.fc1 = nn.Linear(28*28, 256) self.relu = nn.ReLU() self.fc2 = nn.Linear(256, 128) self.fc3 = nn.Linear(128, 10) def forward(self, x): x = self.flatten(x) x = self.fc1(x) x = self.relu(x) x = self.fc2(x) x = self.relu(x) x = self.fc3(x) return x # 准备数据 transform = transforms.Compose([transforms.ToTensor()]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) # 初始化模型、损失函数、优化器 model = FeedForwardNet() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # 训练循环 for epoch in range(5): running_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}')这段代码虽然短,但包含了深度学习的完整流程:定义模型、准备数据、前向传播、计算损失、反向传播、更新参数。
2.3 反向传播到底在算什么
反向传播是深度学习最核心的算法,但很多人只是知道个名字,不清楚具体在算什么。我用最直白的话解释一下。
训练网络的目标是让损失函数变小。损失函数衡量的是网络输出和真实标签之间的差距。要让损失变小,就得调整权重和偏置。怎么调?沿着梯度的反方向调。
梯度是什么?就是损失函数对每个参数的偏导数。它告诉你:如果把这个参数增大一点点,损失会变大还是变小,变化多少。反向传播就是利用链式法则,从输出层往输入层逐层计算这些偏导数。
为什么叫“反向”?因为计算顺序是从后往前的。先算输出层的梯度,然后利用输出层的梯度算前一层,一直传到输入层。这跟前向传播的方向正好相反。
链式法则的核心思想是:复合函数的导数等于各层导数的乘积。网络每一层都是一个函数,整个网络就是这些函数的复合。所以求导的时候一层层乘下去就行。
这里有个关键点:梯度消失和梯度爆炸。如果每层的导数都小于1,乘多了梯度就趋近于0,前面的层几乎更新不动,这就是梯度消失。如果每层导数都大于1,乘多了梯度就爆炸,参数更新幅度过大导致不收敛。ReLU激活函数、Batch Normalization、残差连接这些技术,本质上都是在解决这个问题。
2.4 损失函数与优化器的选择逻辑
损失函数和优化器的选择不是随便挑的,要根据任务类型来定。
损失函数选择表:
| 任务类型 | 推荐损失函数 | 输出层激活 | 说明 |
|---|---|---|---|
| 二分类 | BCELoss | Sigmoid | 输出一个概率值 |
| 多分类 | CrossEntropyLoss | 无(内部含Softmax) | 输出各类别得分 |
| 回归 | MSELoss | 无 | 输出连续值 |
| 多标签分类 | BCEWithLogitsLoss | 无 | 每个标签独立判断 |
注意PyTorch的CrossEntropyLoss内部已经包含了Softmax,所以网络最后一层不要再加Softmax,否则会重复计算导致结果错误。这个坑我踩过,训练loss降不下去,排查半天才发现是多了个Softmax。
优化器选择:
- SGD:最基础的优化器,配合动量使用效果不错,但学习率需要精细调节。
- Adam:自适应学习率,对超参数不敏感,新手首选。默认学习率0.001通常就能work。
- AdamW:Adam的改进版,权重衰减方式更合理,Transformer类模型常用。
- RMSprop:适合RNN类模型。
我的经验是:不确定用什么就用Adam,学习率设0.001。等模型能跑通了,再尝试换SGD加动量,有时候能获得更好的泛化性能。
3. 卷积神经网络:让机器学会“看”
3.1 卷积到底卷了什么
卷积神经网络是处理图像任务的主力。它的核心思想是利用图像的局部相关性,用一个小窗口(卷积核)在图像上滑动,提取局部特征。
为什么这样做有效?因为图像中相邻像素是相关的,一个边缘、一个角点,都是局部模式。用全连接网络处理图像,每个像素都要和下一层所有神经元连接,参数量爆炸不说,还丢失了空间结构信息。卷积操作保留了空间结构,而且参数共享大大减少了参数量。
一个卷积核就是一个特征检测器。比如一个3x3的卷积核,可能检测的是水平边缘,另一个检测垂直边缘。网络通过训练自动学习这些卷积核的权重。
卷积操作的几个关键参数:
- 卷积核大小:常用3x3、5x5。3x3是最常用的,两个3x3堆叠的感受野等于一个5x5,但参数更少、非线性更强。
- 步长:卷积核每次滑动的距离。步长为1时输出尺寸不变(配合padding),步长为2时输出尺寸减半。
- 填充:在图像边缘补零,控制输出尺寸。
padding=1配合3x3卷积核可以保持尺寸不变。 - 通道数:输入通道数由上一层决定,输出通道数由你设定,表示提取多少种特征。
输出尺寸的计算公式:
输出尺寸 = (输入尺寸 - 卷积核大小 + 2*填充) / 步长 + 1比如输入32x32,卷积核3x3,填充1,步长1,输出就是(32-3+2)/1+1=32,尺寸不变。
3.2 池化层的作用与选择
池化层紧跟在卷积层后面,用来降低特征图的空间尺寸,减少计算量,同时增强特征的平移不变性。
最常用的是最大池化:在一个小窗口里取最大值。比如2x2最大池化,把每2x2区域压缩成一个值,尺寸减半。
为什么用最大池化而不是平均池化?因为最大池化保留了最显著的特征。在图像识别中,一个特征是否存在比它的精确位置更重要。最大池化相当于在问:“这个区域里有没有出现这个特征?”只要有一个强响应,就保留下来。
不过近年来有些网络设计开始用步长为2的卷积代替池化,让网络自己学习降采样的方式。两种做法各有优劣,池化更简单直接,卷积降采样更灵活但参数更多。
3.3 构建一个完整的CNN图像分类器
下面用PyTorch搭建一个用于CIFAR-10分类的CNN。
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.features = nn.Sequential( # 第一层卷积块 nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 32x32 -> 16x16 # 第二层卷积块 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 16x16 -> 8x8 # 第三层卷积块 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 8x8 -> 4x4 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128*4*4, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x这个网络结构有几个设计要点值得说:
BatchNorm放在卷积之后、激活之前。这是标准做法,让每层的输入分布稳定,加速训练收敛。没有BatchNorm的话,学习率要设得很小才能稳定训练。
Dropout放在全连接层。卷积层参数少,过拟合风险低,一般不加Dropout。全连接层参数多,是过拟合的重灾区,加Dropout效果明显。0.5是常用值,表示训练时随机丢弃一半神经元。
特征图尺寸逐层减半,通道数逐层翻倍。这是CNN设计的经典模式。空间信息压缩的同时,特征维度增加,让网络从低级特征逐步组合出高级语义特征。
3.4 图像从输入到高维向量的完整过程
很多人好奇:一张图片进入神经网络,到底经历了什么?我用人脸识别的例子串一遍。
假设输入一张224x224x3的人脸图片。
第一层卷积用64个7x7的卷积核,步长2,输出112x112x64。这一层提取的是边缘、颜色梯度等低级特征。每个通道对应一种边缘模式。
第二层用最大池化降采样到56x56,然后卷积提取更复杂的纹理特征,通道数增加到128。
继续往下,空间尺寸越来越小,通道数越来越多。到了网络的深层,每个位置上的向量已经不再是像素值,而是高度抽象的语义特征。比如某个位置可能编码了“眼睛”的概念,另一个位置编码了“鼻子”的概念。
最后经过全局平均池化,把所有空间位置的特征压缩成一个固定长度的向量,比如512维。这个向量就是这张人脸的“特征嵌入”。同一个人的不同照片,这个向量在空间中的距离会很近;不同人的照片,距离会远。
这就是为什么人脸识别可以用特征向量的余弦相似度来判断。网络学到的不是记住某张具体照片,而是学会了把人脸映射到一个语义空间中,在这个空间里,相似的人脸靠得近。
4. 训练技巧与问题排查:让模型真正跑起来
4.1 学习率设置:最重要的超参数
如果只能调一个超参数,那一定是学习率。学习率太大,loss震荡不收敛;学习率太小,收敛慢还容易陷入局部最优。
我的经验做法是:先用一个较大的学习率跑几百步,观察loss曲线,然后逐步降低。或者用学习率扫描:从1e-1到1e-6每个量级跑几百步,看哪个量级loss下降最快。
更省事的做法是用学习率调度器。PyTorch提供了多种调度策略:
from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR, ReduceLROnPlateau # 每30个epoch学习率降为原来的0.1 scheduler = StepLR(optimizer, step_size=30, gamma=0.1) # 余弦退火,学习率按余弦曲线下降 scheduler = CosineAnnealingLR(optimizer, T_max=100) # 根据验证集loss自动降低学习率 scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5)ReduceLROnPlateau是我最常用的,它不需要你预设训练总轮数,验证集loss不降了就自动降学习率,很省心。
4.2 过拟合与欠拟合的判断和应对
训练过程中最常见的两个问题就是过拟合和欠拟合。判断方法很简单:看训练loss和验证loss的走势。
| 现象 | 训练loss | 验证loss | 诊断 | 应对措施 |
|---|---|---|---|---|
| 欠拟合 | 高 | 高 | 模型能力不足 | 增加层数/宽度、训练更久、减小正则化 |
| 过拟合 | 低 | 高且持续上升 | 模型记住了训练集 | 增加数据、加Dropout、加L2正则、早停 |
| 正常 | 低 | 低且稳定 | 训练良好 | 继续监控 |
L2正则化在PyTorch里通过优化器的weight_decay参数实现:
# weight_decay就是L2正则化系数 optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)weight_decay的作用是让权重倾向于变小,防止某些权重过大导致模型对特定特征过度敏感。1e-4到1e-5是常用范围,太大了会导致欠拟合。
早停是最简单有效的防过拟合手段。每隔几个epoch检查验证集loss,如果连续多轮不下降就停止训练,保存验证集loss最低的那个模型。
4.3 多任务学习中loss权重的调整
多任务学习是一个模型同时做多个任务,比如人脸识别中同时做人脸检测和关键点定位。这时候总loss是多个子loss的加权和:
total_loss = alpha * loss1 + beta * loss2 + gamma * loss3难点在于怎么定这些权重。如果某个任务的loss量级远大于其他任务,梯度会被它主导,其他任务学不好。
几种常用的权重调整策略:
手动调参:先让每个任务单独训练,观察各自的loss量级,然后按比例设置权重让它们对总loss的贡献大致相当。这是最笨但最可控的方法。
不确定性加权:让模型自己学习每个任务的权重。每个任务有一个可学习的参数sigma,权重是1/(2*sigma^2),同时加一个log(sigma)的正则项防止sigma无限增大。这个方法来自一篇经典论文,实现起来也不复杂。
GradNorm:动态调整权重,让每个任务的梯度范数保持相近。训练过程中每隔几步计算各任务梯度对共享层的范数,然后调整权重让它们趋于一致。
我的建议是:先从手动调参开始,把各任务loss调到同一量级。如果效果不好再尝试不确定性加权。GradNorm实现复杂,收益不一定对得起投入。
4.4 常见报错与排查速查表
训练深度学习模型时遇到的报错,八成是这几类。我整理了一个速查表:
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| CUDA out of memory | 显存不够 | 减小batch size、用梯度累积、混合精度训练 |
| RuntimeError: size mismatch | 张量维度不匹配 | 检查全连接层输入维度是否与特征图尺寸匹配 |
| loss is nan | 梯度爆炸或除零 | 降低学习率、加梯度裁剪、检查数据是否有异常值 |
| Expected all tensors on same device | 数据和模型不在同一设备 | 统一.to(device) |
| AttributeError: module 'numpy' has no attribute 'float' | numpy版本过高 | 改代码用np.float64或降numpy版本 |
| cuDNN error: CUDNN_STATUS_NOT_INITIALIZED | CUDA/cuDNN版本不匹配 | 重装对应版本的PyTorch |
梯度裁剪是解决loss nan的常用手段:
# 在loss.backward()之后,optimizer.step()之前 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)这行代码把所有参数的梯度范数限制在1.0以内,防止梯度爆炸。RNN类模型几乎必加。
混合精度训练是省显存提速的利器:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for images, labels in train_loader: optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()混合精度用float16做前向传播,float32做参数更新,显存占用减少近一半,训练速度提升30%以上。几乎不影响精度,强烈推荐。
4.5 数据增强:不花钱涨数据
数据不够是常态。数据增强通过对训练图片做随机变换,人为制造更多样本。注意只在训练时增强,验证和测试时不要增强。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomResizedCrop随机裁剪并缩放到固定尺寸,RandomHorizontalFlip随机水平翻转,RandomRotation随机旋转。这些变换模拟了拍摄角度、距离、光照的变化,让模型学到更鲁棒的特征。
Normalize的均值和标准差是ImageNet数据集的统计值,用预训练模型时必须用这组值。从零训练的话可以用自己数据集的统计值,但用ImageNet的值通常也没问题。
注意:数据增强的强度要适中。过度增强会让模型学到不真实的模式,反而降低性能。比如人脸识别中做大幅度的旋转增强就不合适,因为人脸很少倒着出现。
5. 从理论到落地:几个值得动手的项目
5.1 手写数字识别:入门首选
MNIST手写数字识别是深度学习的“Hello World”。数据集小、任务简单、几分钟就能训练完,非常适合验证环境和流程。
我建议不要直接用torchvision的MNIST,而是自己下载原始IDX文件解析一遍。这样你能理解数据集的存储格式,以后遇到自定义数据集也知道怎么处理。
import struct import numpy as np def load_mnist_images(filename): with open(filename, 'rb') as f: magic, num, rows, cols = struct.unpack('>IIII', f.read(16)) images = np.frombuffer(f.read(), dtype=np.uint8).reshape(num, rows, cols) return images def load_mnist_labels(filename): with open(filename, 'rb') as f: magic, num = struct.unpack('>II', f.read(8)) labels = np.frombuffer(f.read(), dtype=np.uint8) return labels解析出来之后归一化到0-1,就可以喂给网络了。MNIST上达到99%以上的准确率是基本要求,如果达不到说明流程有问题。
5.2 图像分类实战:从CIFAR-10到自定义数据集
CIFAR-10比MNIST难一个档次,32x32的彩色图片,10个类别。在这个数据集上,简单的CNN能到75%左右,加上数据增强和更深的网络能到90%以上。
如果你想用自己的图片训练分类器,目录结构这样组织:
dataset/ ├── train/ │ ├── cat/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── dog/ │ ├── 001.jpg │ └── 002.jpg └── val/ ├── cat/ └── dog/用ImageFolder直接加载:
from torchvision.datasets import ImageFolder train_dataset = ImageFolder('dataset/train', transform=train_transform) val_dataset = ImageFolder('dataset/val', transform=val_transform)每个类别一个文件夹,文件夹名就是类别名。这是最省事的组织方式,不用自己写Dataset类。
5.3 模型保存与加载的几种姿势
训练完的模型要保存下来,否则下次用还得重新训练。PyTorch有两种保存方式:
保存整个模型:
torch.save(model, 'model.pth') model = torch.load('model.pth')这种方式简单,但依赖模型类的定义。如果代码重构了模型类,加载会失败。
只保存参数(推荐):
torch.save(model.state_dict(), 'model_weights.pth') # 加载时需要先实例化模型 model = SimpleCNN() model.load_state_dict(torch.load('model_weights.pth')) model.eval()这种方式更灵活,模型结构变了只要参数名对得上就能加载。而且文件更小,只存参数不存结构。
保存的时候还要存优化器状态,方便断点续训:
checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': loss, } torch.save(checkpoint, 'checkpoint.pth')加载的时候对应恢复:
checkpoint = torch.load('checkpoint.pth') model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict']) start_epoch = checkpoint['epoch']5.4 推理部署:让模型真正用起来
训练好的模型最终要部署出去。推理阶段有几个关键点:
切换到eval模式。这会影响Dropout和BatchNorm的行为。Dropout在推理时不丢弃神经元,BatchNorm用训练时统计的均值和方差。
model.eval() with torch.no_grad(): outputs = model(images)torch.no_grad()关闭梯度计算,减少显存占用,提升推理速度。
批处理推理。不要一张一张推理,攒成batch一起跑,GPU利用率高很多。
导出为ONNX。ONNX是跨框架的模型格式,导出后可以用ONNX Runtime推理,也可以用TensorRT进一步加速。
dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, 'model.onnx', input_names=['input'], output_names=['output'])导出ONNX时注意输入尺寸要固定,动态尺寸需要额外指定dynamic_axes参数。
我在实际部署中踩过的一个坑是:训练时用了RandomResizedCrop,推理时忘了做对应的resize和center crop,导致输入分布不一致,准确率掉了一大截。推理时的预处理必须和验证时完全一致,这个一定要检查。
5.5 边缘设备部署的注意事项
如果要把模型部署到边缘设备上,模型大小和推理速度是硬约束。几个优化方向:
模型剪枝:去掉不重要的权重,把稠密矩阵变稀疏。PyTorch提供了torch.nn.utils.prune工具。
量化:把float32权重转成int8,模型大小减少75%,推理速度提升2-4倍。精度损失通常在1%以内。
# 动态量化(最简单) quantized_model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 )知识蒸馏:用大模型教小模型,让小模型达到接近大模型的性能。损失函数由两部分组成:小模型输出和大模型输出的差异,以及小模型输出和真实标签的差异。
边缘设备上推理框架的选择也很关键。ONNX Runtime通用性好,TensorRT在NVIDIA设备上性能最强,OpenVINO适合Intel平台。选哪个取决于你的硬件。
6. 学习路径与资源推荐
6.1 从零到一的阶段划分
深度学习入门可以分成三个阶段:
第一阶段:跑通流程。目标是能完整跑通一个训练流程,理解每个环节在做什么。MNIST手写数字识别是最合适的项目。这个阶段不要纠结原理,先让代码跑起来,看到loss下降、准确率上升,建立信心。
第二阶段:理解原理。回头补数学基础:链式法则、梯度下降、反向传播。手动推导一遍两层网络的反向传播,比看十遍视频都管用。这个阶段可以看吴恩达的深度学习课程,课后题一定要动手做。
第三阶段:项目实战。找一个自己感兴趣的任务,从头到尾做一遍。数据收集、清洗、模型设计、训练调参、部署推理,完整走一遍。这个阶段会遇到各种实际问题,解决这些问题的过程就是成长。
6.2 值得反复看的资料
- 《动手深度学习》:李沐团队的免费教材,代码和理论结合得很好,PyTorch实现。每个章节都有可运行的notebook。
- 吴恩达深度学习课程:Coursera上的经典课程,理论讲解清晰,课后题质量高。适合补数学基础。
- PyTorch官方教程:最权威的PyTorch学习资料,从基础到进阶都有。遇到API问题首选查官方文档。
- Papers With Code:找论文复现的首选网站,每个任务都有排行榜和开源代码。
6.3 我踩过的那些坑
最后分享几个我实际踩过的坑,希望能帮你省点时间。
不要一上来就追求SOTA。新手最容易犯的错就是直接复现最新论文,结果环境配不通、代码跑不起来、训练不收敛,信心直接崩了。从简单的开始,逐步增加复杂度。
不要盲目调参。调参之前先确保数据没问题、流程没问题。我见过有人调了一周学习率,最后发现是数据标签错了。先用小数据集过拟合一下,如果连几十张图都过拟合不了,说明模型或流程有bug。
不要忽视数据质量。模型再好在脏数据上也白搭。花时间检查数据、清洗数据、可视化数据,比调模型结构收益大得多。
不要只盯着准确率。分类任务要看混淆矩阵,回归任务要看残差分布。准确率高不代表模型好,可能只是在多数类上表现好,少数类全错。
多动手,少空想。看视频、看书都是输入,只有自己写代码才是输出。看懂了和写出来是两回事,写出来和跑通了又是两回事。遇到报错不要怕,排查报错的过程就是学习的过程。
这个系列写到第十篇,覆盖了从环境配置到模型部署的完整链路。深度学习是一个实践性极强的领域,光看不动手永远学不会。找一个你感兴趣的小项目,从今天开始写第一行代码,比什么都强。