ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

不买显卡也能跑深度学习:CPU训练与云GPU实战指南

不买显卡也能跑深度学习:CPU训练与云GPU实战指南 设备管理器里那几张显卡每天被好几个人排队用调度全靠吼。实验室唯一一张2080Ti跑ImageNet都要跟师兄商量三天。更别提你手里这个项目——深度学习模型训练论文要结果导师不拨款显卡抢不到。这种处境我太熟了。早几年我自己做毕业设计实验室唯一的GPU还被师姐的GAN占着连续跑了两个月我硬是用一台四核CPU的破工作站把一篇顶会论文的实验跑完了。所以这篇文章不是教你“凑合”而是把“无GPU深度学习”这件事系统的拆开哪些方案真的可行哪些是伪需求每一步该怎么做踩过的坑我都给你标出来。内容面向两类人实验室确实没有像样GPU的研究生以及想在自己笔记本上入门深度学习、又不想花几千块买显卡的自学者。照着做你完全能把实验跑起来、把论文写出来。1. 先别急着装环境CPU到底能跑什么、不能跑什么很多人的误区是一上来就装PyTorch、下数据集结果训练到半夜发现一个epoch要跑十几个小时心态直接崩了。动手之前先搞清楚CPU训练的真实边界比任何优化技巧都重要。1.1 一张表看懂CPU算力的“行”与“不行”我测试过不少典型任务用一个相对可靠的参考标准六核十二线程的普通桌面CPU比如i5-12400或R5 5600X配16GB内存来对比任务类型CPU训练体验说明与建议经典CNNLeNet、ResNet-18在CIFAR-10/自定义小数据集能用但难受一个epoch 5~20分钟小模型总训练时间控制在几小时内可接受文本分类LSTM、Transformer小模型视序列长度而定短文本没问题长序列会让CPU内存先爆掉目标检测YOLO系列、Faster R-CNN基本劝退单张图前向推理都要好几秒训练不现实建议云GPU图像分割UNet等小图可以256x256以下分辨率、浅层UNet勉强能跑大规模预训练BERT、GPT微调特别难受CPU可以微调小模型7B以上的大模型别想数据预处理/推理部署CPU的强项推理部署、数据增强、评估指标计算等在CPU上效率很高这个表说得很清楚CPU能做的是“小模型小数据可控时间”的实验以及训练后的推理部署工作。你的策略应该是——在小规模上把代码调通、把思路验证完需要大算力时再去租GPU跑最终版本。1.2 为什么CPU训练这么慢卡在哪个环节CPU训练慢不完全是“核心数少”这么简单。真正坑人的是这三个地方内存带宽瓶颈。深度学习训练的核心操作是大量小矩阵乘法比如把一个 [64, 512] 的矩阵乘上 [512, 512] 的权重。GPU的显存带宽能到几百GB/s普通DDR4内存带宽只有二三十GB/s这决定了CPU每次要把数据从内存搬到寄存器计算时搬运本身就耗掉了大部分时间。这不是你多开几个线程能解决的。单指令流处理能力差距。CPU一个核心一次能处理一条指令GPU一个计算单元能并行处理几十上百条。矩阵乘法这种天然并行的任务GPU的并行架构就像是几千个人同时搬砖CPU再怎么超频也只是一个人搬得更快一点。数据搬运路径太长。训练时每个batch要经历“硬盘读数据→内存→CPU缓存→寄存器→算完写回内存”这么长的链路。很多人为了让CPU跑得快直接加大batch size结果内存带宽反而撑不住每个epoch时间不但没缩短反而因为内存换页把系统搞到卡死。1.3 认清现实后的正确姿势接受“CPU只能做小规模实验”这个前提之后训练策略就要跟着变。我在无GPU阶段定了几条规矩每条都是拿时间换来的数据集不是完整数据直接灌进去先做子集验证。比如完整数据有10万张图先用1000张跑通流程确认模型能收敛、loss在下降再考虑全量。模型先用最小的网络结构试。ResNet-18足够验证思路时绝不直接上ResNet-50。等代码逻辑全部验证完再换大模型跑最终实验。epoch数量要算着来。CPU上训练每多一个epoch都是实打实的时间初始设定值不要拍脑袋。先跑3个epoch观察loss下降趋势如果掉了30%以上再决定是加大epoch还是先收手调参。这一节的结论很简单不要指望CPU像GPU一样“大力出奇迹”而是用“小步快跑”的方式把工程问题先解决掉。后面每一节都是围绕这个思路展开的具体操作。2. 代码侧的精打细算CPU训练提速的五个核心操作当你确定要在本地CPU上跑训练代码层面能抠出来的速度提升其实是超出很多人预期的。下面这五个操作我在不同项目里反复验证过每一步都能带来实打实的改变。2.1 线程数设置不是越大越好PyTorch在CPU上默认会使用所有可用的核心但这恰恰是新手最容易犯的错。当你把16个线程全用上时线程切换开销极大反而比只开8个还慢。我自己试过多次对六核十二线程的CPU来说最优值通常是物理核心数也就是6而不是逻辑线程数12。设置方法很简单放在训练脚本的最前面import torch import os # 在导入torch之后、创建任何tensor之前设置 torch.set_num_threads(6) # 用物理核心数不是逻辑线程数 os.environ[OMP_NUM_THREADS] 6 os.environ[MKL_NUM_THREADS] 6这三行代码同时设置了PyTorch内部的线程池、OpenMP的并行线程和Intel MKL数学库的线程数。三个地方必须一致否则会出现线程池冲突反而更慢。我见过有人只设置了torch.set_num_threads忘了设置环境变量结果PyTorch内部的优化器用的线程数和MKL库的线程数不一致训练速度直接折半。2.2 让MKL/oneDNN接管矩阵运算如果你的CPU是Intel的实验室机器大概率是PyTorch默认就启用了oneDNN原MKL-DNN对卷积、矩阵乘法等算子做优化。但很多人不知道这个功能可以被显式加强。在模型初始化之后加上# 启用oneDNN的自调优让PyTorch自动为当前CPU选择最快的卷积算法 torch.backends.mkldnn.enabled True torch.backends.cudnn.enabled False # 这行在CPU上没实际作用但可以避免一些混淆CPU训练时这个设置配合channels_last内存格式能让ResNet系列在CPU上的推理速度提高20%到40%。如果用的是AMD的CPU不用强求MKLPyTorch的原生实现配好线程数也足够用。2.3 数据加载是CPU训练里最容易忽视的巨坑GPU训练时数据加载慢一点无所谓因为GPU算得快数据队列容易吃满。但CPU训练时数据加载和模型计算争抢的是同一批CPU核心如果DataLoader不做优化你会发现模型计算在等数据数据加载也在抢CPU两头都在空转。我通常的做法是from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, # 注意不是越大越好通常2~4合适 pin_memoryFalse, # CPU训练时这个没用设为True反而多一次内存复制 prefetch_factor4, # 每个worker预取4个batch persistent_workersTrue # 多个epoch时复用worker避免反复创建销毁 )num_workers设成4是我在多数主流CPU上试出来的甜点值。设成8以上虽然数据加载速度还能快一点但会占用大量内存而且会让训练进程的CPU资源变少。persistent_workers这个参数很多人忽略它能在多个epoch之间保持数据加载线程存活省去每次重新初始化的开销。如果你的PyTorch版本较老不支持这些参数优先升级到1.12以上。另外批量图不要直接读原图再Resize而是先把所有图片缩放到较小尺寸后缓存成.npy或LMDB格式。我在一个医学图像项目里光这一项优化数据加载时间从每次epoch 80秒降到了8秒。这是因为CPU训练时图像解码尤其是大图JPEG解码消耗的CPU周期比卷积计算还多。2.4 Batch Size和梯度累积CPU的专属方案GPU训练通常喜欢大batch size因为GPU并行计算能力强batch越大效率越高。CPU上反过来batch太大内存带宽率先撑不住所以最优batch size往往偏小。我实测下来在16GB内存的机器上batch size 16到32是最均衡的范围。但batch太小会让梯度估计噪声大模型不容易收敛。解决办法是用梯度累积来模拟更大的batchaccumulation_steps 4 # 相当于把batch扩大4倍 optimizer.zero_grad() for i, (inputs, labels) in enumerate(train_loader): outputs model(inputs) loss criterion(outputs, labels) # 除以累积步数让梯度平均而不是累加 loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这个写法的关键是loss loss / accumulation_steps。如果不做这个归一化累积4步后的梯度相当于用了4倍的学习率模型可能直接发散。我自己踩过这个坑loss在前几十个iteration直接冲到了NaN。梯度累积让CPU能用小batch跑出大batch的效果代价只是多几次前向反向的重复计算对内存带宽的压力小了很多。2.5 早停和周期性的模型保存CPU训练一个epoch可能要几个小时一旦训到一半断电或者崩了没有保存的checkpoint意味着全部白跑。所以我在CPU训练代码里一定会加上from torch.optim.lr_scheduler import ReduceLROnPlateau best_loss float(inf) patience 0 for epoch in range(num_epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss validate(model, val_loader, criterion) # 保存最优模型 if val_loss best_loss: best_loss val_loss torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_loss: best_loss, }, best_checkpoint.pth) patience 0 else: patience 1 # 早停 if patience 5: print(fEarly stopping at epoch {epoch}) break这里保存的不只是模型权重还有优化器状态和当前epoch。这样即使中断了你也可以从best_checkpoint.pth恢复继续训练而不是从头再来。ReduceLROnPlateau这种自适应学习率调整在CPU训练里特别重要因为你没有那么多时间反复手动试学习率。3. 云GPU租用把好钢用在刀刃上单纯靠CPU优化解决不了所有问题。当你的模型复杂度上来了、数据集变大了该花点钱租GPU就花这是性价比最高、也最省时间的选择。但怎么租、怎么衔接本地代码这里面的道道可不少。3.1 直接选租用平台时盯准这五个配置市面上的GPU云平台很多功能上大同小异真正决定体验的是下面几个细节配置项最低标准原因GPU型号RTX 3090 / A5000起步24GB显存能覆盖绝大多数深度学习实验包括中小模型微调显存至少16GB小于这个值跑不了Batch Normalization在大batch下的稳定训练预置镜像必须带PyTorch和CUDA省去装驱动环境的半天时间存储至少50GB SSD数据集加模型权重加环境30GB很容易就打不住了计费方式按小时计费跑完就释放实例不跑不花钱选平台的时候别只看GPU单价来看看镜像是否完善、数据上传是否方便。有的平台GPU一小时便宜五块钱但上传数据集要折腾半天省下的钱还不够时间成本。我自己是优先用那些开箱即用、自带PyTorch完整环境的主流平台。这类服务通常都提供按量付费的实例选择时先看看它们的GPU是否包含3090或以上的卡型同时确认一下存储的数据卷创建方式——因为你需要一个独立于实例的数据盘这样实例释放了数据集和checkpoint还在下次开机直接挂载就行不用重新上传。3.2 从CPU本地到云GPU的代码迁移清单代码在本地CPU上已经调通了现在要搬到云GPU上跑。这个迁移不需要大改代码但有三个地方必须处理不然会出各种奇怪的bug设备切换。不要在代码里写死cuda:0用全局统一的设备判断device torch.device(cuda if torch.cuda.is_available() else cpu) # 之后所有模型和数据都走 .to(device)这样同一份代码既能在本地CPU上跑也能在云GPU上跑切换零成本。我见过有人把代码改成model.cuda()回本地跑的时候又忘了改回来直接报错。数据加载路径。上传数据集到云平台后把数据路径改成云端的绝对路径。建议把数据集放在与实例分离的数据盘上而不是实例系统盘里这样实例释放后数据不丢下次还能接着用。NumPy的随机种子。GPU训练有大量异步操作如果不设随机种子迁移后的结果可能和本地对不上。在代码开头加上import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False注意torch.backends.cudnn.benchmark False这行它会让训练变慢一点点但换来的是结果完全可复现。如果你不需要严格复现这行可以去掉cudnn的自动调优会让训练快不少。3.3 省钱策略计费规则和checkpoint时间管理我见过太多人租了GPU开着机器干瞪眼——代码有bug调试了半天GPU在闲置计费。要想把成本压到最低有几个原则非常重要抢占式实例永远优先。很多平台提供抢占式或Spot实例价格是按量付费的两到三折缺点是实例可能被中断。但深度学习的checkpoint机制天然适配这种场景——每跑完几个epoch自动存一次模型中断了就从最新的checkpoint恢复。我在云上跑一个微调任务用抢占式实例省了差不多60%的费用。先本地调试再上云跑活。所有代码逻辑、数据预处理、模型结构全部在本地CPU上用小子集调试通过确认训练流程能跑通三个小epoch不报错再上云租实例。云端只干“用完整数据训练”这一件事。这样能避免在云上反复调试把GPU的每一分钟都用在真正的计算上。定时保存要勤。因为抢占式实例随时可能被回收云端训练的checkpoint保存频率要比本地高得多。我给云训练的代码加了一个逻辑每完成一个epoch就保存一次模型权重到数据盘同时保留最近三份checkpoint轮换覆盖。这样即使实例被回收最多丢一个epoch的进度。账号里的数据盘才是可靠的。别把模型权重保存在实例本地磁盘上而是保存到独立的数据盘。实例被回收后数据盘通常还能保留一段时间把最新权重下载到本地再按需开新实例继续跑。3.4 免人工盯训练的自动化作业云GPU上还有个很实用的技巧——把训练脚本写成自动作业的形式。很多平台支持提交训练任务后关闭网页训练在后台跑跑完自动把结果上传到对象存储或发邮件通知。这样你不必开着电脑盯着训练进度该睡觉睡觉第二天起来直接看结果。如果用的是Jupyter环境把这个命令挂在后台跑nohup python train.py --config config_cloud.yaml train.log 21 然后定期查看train.log确认loss下降正常。nohup加的组合是我在云端跑长时间训练任务的标准操作比手动开着一个终端窗口反复看可靠太多了。4. 模型侧“降级”选对结构和权重比选对硬件更重要无GPU环境下模型选型直接决定你能不能出结果。同样的任务用ResNet-50还是MobileNet在CPU上训练时间能差五倍。这一节讲的就是怎么在算法层面做减法。4.1 迁移学习是无GPU环境下性价比最高的方案训练一个模型最消耗计算量的部分其实是底层特征的提取——边缘、纹理、形状这些通用特征。这些特征在所有图像任务里都是通用的完全不需要从头学。所以无GPU环境下我的习惯是永远从预训练权重开始绝不随机初始化。实现方式有两种分别应对不同场景固定特征提取器。加载预训练模型冻结backbone所有参数只训练最后的分类层。这种方法计算量最小一个epoch在CPU上可能只需要几分钟而且效果往往不差。适合你的数据集跟预训练数据比如ImageNet比较接近的情况。import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False # 替换最后一层 num_ftrs model.fc.in_features model.fc torch.nn.Linear(num_ftrs, num_classes) # 只优化分类层参数 optimizer torch.optim.Adam(model.fc.parameters(), lr0.001)全模型微调。所有参数都参与训练但初始权重仍然用预训练模型学习率调小一些。这种方法计算量大一点但效果上限更高。在CPU上跑的话建议只对比较小的预训练模型ResNet-18、EfficientNet-B0做全量微调。很多人忽视的一个细节是预训练权重加载时如果你的分类类别数和预训练模型不同最后一层结构对不上model.load_state_dict会报错。需要忽略不匹配的key只加载backbone部分的权重state_dict torch.load(pretrained.pth) # 移除fc层的权重 state_dict.pop(fc.weight, None) state_dict.pop(fc.bias, None) model.load_state_dict(state_dict, strictFalse)4.2 轻量级网络在CPU上的速度对比如果你实在需要从头训练比如数据集和ImageNet差异太大、或者你就是想发方法类论文网络结构的选择就非常关键了。我在同样的CPU上、同样的数据集、同样的训练轮数下对比过几个常用网络的单epoch时间网络结构参数量CPU单epoch耗时相对值适用场景MobileNetV3-Small约250万1x最快嵌入式/移动端场景CPU友好EfficientNet-B0约530万1.5x精度/速度均衡ResNet-18约1170万2.5x通用基准教师模型ResNet-50约2560万6xCPU训练偏重建议云GPUVGG-16约1.38亿12x最慢CPU上几乎不可训练这个表给我最大的教训是参数量跟训练速度不是线性关系。VGG-16参数量是ResNet-18的十倍多但训练时间只差了约五倍因为VGG的计算主要集中在前面的卷积层全连接层的参数虽多算起来却快。所以选网络时不能只看参数量关键是看卷积计算量大不大。MobileNet系列的深度可分离卷积在CPU上特别占便宜精度损失可控是CPU训练的优先选择。4.3 知识蒸馏的思路值得提前了解如果你最终的目的是在一个小模型上部署或者你导师要求你用某个复杂模型做实验知识蒸馏是一个绕过硬件的思路。具体做法是用云GPU把大模型教师模型训练好或下载开源权重在本地CPU上训练一个小模型学生模型损失函数不仅包括和小标签的交叉熵还包括和大模型输出的KL散度。学生模型不需要重新在大数据集上训练它只需要“模仿”教师模型的输出。这个过程的计算量大幅缩减因为在本地只需要跑一次推理从教师模型那里拿到软标签然后在小模型上训练。这里有一个实际可用的实现方式def distillation_loss(student_output, teacher_output, labels, alpha0.7, temperature4.0): student_output: 学生模型的logits teacher_output: 教师模型的logits (预先计算好) labels: 硬标签 alpha: 软标签损失的权重 temperature: 温度系数 import torch.nn.functional as F hard_loss F.cross_entropy(student_output, labels) soft_student F.log_softmax(student_output / temperature, dim1) soft_teacher F.softmax(teacher_output / temperature, dim1) soft_loss F.kl_div(soft_student, soft_teacher, reductionbatchmean) soft_loss soft_loss * (temperature ** 2) # 补偿温度缩放的梯度尺度 return alpha * soft_loss (1 - alpha) * hard_loss注意最后的soft_loss * (temperature ** 2)。这是知识蒸馏里的标准做法因为温度会缩小logits之间的差异导致梯度变小乘以温度的平方能抵消这个影响。不乘的话温度设成4的时候梯度会小到训练几乎不动。温度系数一般取2到6之间数值越大软标签的分布越平滑学生模型能学到更多类别间的相似性信息。4.4 输入分辨率也值得压缩CPU训练的另一个隐藏成本是输入图片的大小。224x224的输入和128x128的输入在卷积层计算量上相差约三倍而精度损失在很多任务上只有一两个点。我习惯在项目一开始就把输入分辨率压到尽可能小等代码跑通、确认能收敛后再逐步提高分辨率看精度变化。如果论文里需要展示你确实是用了224x224这种标准设置可以分两阶段在本地CPU上用低分辨率调通所有代码最后用云GPU跑一次标准分辨率的完整实验作为最终结果。5. 训练之后的CPU部署无GPU环境的另一条出路很多人只盯着训练这一步其实“训练结束后的模型部署”才是无GPU环境发挥价值的更实用方向。你的研究如果涉及把模型落地到实验室的检测系统、嵌入式设备或服务端环境CPU推理优化反而比训练更值得写进论文或交付文档里。5.1 ONNX Runtime加速CPU推理PyTorch模型直接用CPU做推理速度往往不尽如人意。但把模型转换成ONNX格式再用ONNX Runtime跑推理速度通常能提升1.5到3倍。转换过程很简单import torch model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, # 模型 dummy_input, # 输入的示例 model.onnx, # 输出路径 input_names[input], # 输入名 output_names[output], # 输出名 dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, # 动态batch opset_version17 # 转ONNX时用的算子集版本 )dynamic_axes参数值得好好理解。它标记了batch维度是动态的这样部署后你既可以一次推理一张图也可以一次推理一个batch的图灵活性很高。opset_version这个参数建议用17或更高太低的话新版PyTorch里的某些算子无法转换。推理时用ONNX Runtimeimport onnxruntime as ort import numpy as np session ort.InferenceSession(model.onnx, providers[CPUExecutionProvider]) session.set_providers([CPUExecutionProvider]) def predict(image_tensor): input_data image_tensor.numpy().astype(np.float32) result session.run(None, {input: input_data}) return result[0]推理速度的提升主要来自ONNX Runtime内部的图优化和算子融合——它把多个小操作合并成一个大的算子减少了数据在内存里的反复搬运。这个过程对模型精度没有任何影响纯粹是工程优化。5.2 动态量化把模型从FP32压到INT8如果ONNX Runtime的加速还不够还可以叠加动态量化把模型权重从32位浮点数压缩成8位整数。这个过程几乎不会影响精度但推理速度和内存占用都有显著改善from onnxruntime.quantization import quantize_dynamic, QuantType # 只量化权重不量化激活值所以叫动态量化 quantized_model quantize_dynamic( model.onnx, model_quantized.onnx, weight_typeQuantType.QUInt8 # 无符号8位整数 )动态量化之后模型文件大小大约缩小到原来的四分之一推理速度再提升一倍左右。我在CPU上部署目标检测模型时把PyTorch原模型的推理从200毫秒降到了60毫秒左右这个速度已经可以满足很多实时性要求不高的应用场景了。更激进的方案是静态量化需要先准备校准数据集统计激活值的分布然后在量化时把激活也变成INT8。速度提升比动态量化更明显但精度损失也可能更大。我的建议是先用动态量化如果精度在可接受范围内就直接用不够再考虑静态量化。5.3 用CPU做大规模的批量推理CPU做训练慢但做推理其实不慢尤其在batch size较大、模型不太深的场景下CPU多核并行处理大量推理请求的吞吐量相当可观。我在一个图像检索项目里用CPU对一万张图提取特征每张图耗时不到100毫秒总共十几分钟就跑完了。这个场景下CPU完全够用。大规模批量推理的代码也很简单def batch_extract_features(model, dataloader): model.eval() features_list [] with torch.no_grad(): for images in dataloader: features model(images) features_list.append(features) return torch.cat(features_list, dim0)这里的关键是torch.no_grad()。它告诉PyTorch不需要计算梯度前向推理时不会为反向传播保存中间变量内存占用大幅下降推理速度也有提升。我见过有人部署推理时忘了加这个装饰器一个batch的图在CPU上跑出了训练时的内存占用直接被系统杀掉。6. 常见问题与排查技巧CPU训练实战避坑手册最后这部分是我在无GPU环境下摸爬滚打积累起来的排查经验。每个问题我都踩过写出来帮你省掉那些不必要的调试时间。6.1 训练速度突然变慢先看是不是内存爆了CPU训练最容易出现“跑着跑着越来越慢”的情况。绝大多数时候这是因为内存被吃光了系统开始疯狂使用虚拟内存Windows的页面文件或Linux的swap。当你看到训练速度每况愈下同时硬盘指示灯在疯狂闪烁时不用怀疑一定是内存溢出。排查方法很简单打开任务管理器或htop看看内存占用率是不是接近100%。如果是先减少num_workers从4降到2再调小batch_size从32降到16基本都能解决。我当时检查到一个项目里数据增强做了很多冗余的复制操作把整个增强流程移到了GPU之前的预处理阶段内存立刻减半。6.2 输入张量报错“Expected all tensors on the same device”这个报错在无GPU和GPU混用的场景里特别常见。用torch.device(cuda if torch.cuda.is_available() else cpu)统一管理设备之后还会出现这种错误多半是混合精度训练时某些中间张量没有正确转换设备。排查方法是在关键节点打印张量的设备信息print(fimages.device: {images.device}) print(fmodel.device: {next(model.parameters()).device}) print(flabels.device: {labels.device})这三个张量必须保持一致否则就手动.to(device)。最常见的两种情况是labels忘了转换、以及某些预训练模型内部带buffer没有跟着模型走。6.3 模型不收敛先别调参数检查预处理CPU训练本来就很慢如果你训了三个epoch loss纹丝不动心疼那点时间是对的。但这时候不要急着改学习率或加正则化先做最基础的检查标签是不是从0开始连续编号分类模型要求类别索引从0到num_classes-1如果数据集标签文件是1-based模型输出却在0-based索引上计算loss模型怎么训都训不对。数据增强里有没有使用in-place操作改掉原图transforms可以在每个epoch迭代时对同一份数据做不同增强但如果你误用了原地翻转这类操作多次迭代后数据会被增强“污染”模型学到的还是那些图片只是扭曲程度越来越大。归一化参数是否匹配ImageNet预训练模型需要用mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]做归一化。如果用了别的数据集统计量输入分布和预训练权重的分布对不上迁移学习的优势就全没了。这些坑非常低级的但每一条都让我的实验白跑过。6.4 反向传播报错“Ran out of memory”CPU“Out of Memory”和GPU的显示内存耗尽不是同一回事。CPU环境看到这个报错通常不是单个张量太大而是累积的内存碎片加上大量中间变量导致的。排查步骤按优先级排把batch_size直接砍半这通常是最快的解决办法。检查代码里有没有保留多余的计算图。训练循环里不能有类似outputs model(inputs)这样的操作被存放在某个列表里每迭代一次列表里多存一份内存就线性增长。及时del不需要的中间变量。在安全地方加torch.cuda.empty_cache()没用那是GPUCPU环境用gc.collect()强制内存回收虽然PyTorch的内存池不一定完全把空间还给系统但至少能缓解碎片化。我见过的CPU内存溢出有一半以上是因为训练循环里不断向列表添加loss记录跑了几千个iteration之后那个列表已经存了几千个张量。解决办法是用running_loss running_loss * 0.9 loss.item() * 0.1这种滑动平均而不是记一整个list。6.5 离线环境装不上包怎么办实验室的机器很多是内网离线环境PyTorch装不上是常事。我的经验是提前在能联网的机器上把安装包下载好拷贝过去离线安装。关键是选择正确的.whl文件# 在联网机器上 pip download torch torchvision --index-url https://download.pytorch.org/whl/cpu -d ./local_pkgs # 拷贝到离线机器后 pip install --no-index --find-links./local_pkgs torch torchvision注意上面用的是cpu的轮子而不是cu118或cu121这种CUDA版本的轮子。在无GPU机器上装CUDA版PyTorch是常见错误它不会报错还会白白占用十几个G的硬盘空间而且每次import torch的时候还会尝试初始化CUDA上下文白白增加了启动时间。CPU版本的wheel文件小得多而且完全满足CPU训练的需求。6.6 训练中断了怎么恢复这个坑相信每个人都遇到过训练到一半实验室突然断电或者你的云实例被回收一切从零开始。我的标准做法是# 保存时同时保存epoch和优化器状态 checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), best_val_loss: best_val_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]) scheduler.load_state_dict(checkpoint[scheduler_state_dict]) start_epoch checkpoint[epoch] 1 # 从下一个epoch开始优化器状态必须保存因为Adam这类优化器内部维护的动量一阶、二阶矩估计如果丢了训练恢复后前几百个iteration梯度估计是乱的相当于损失了这部分训练进度。scheduler状态也很重要否则学习率会被重置回初始值后面训练可能会乱。我在整个无GPU做深度学习的经历里记忆最深刻的不是某个模型效果有多好而是把“在资源受限条件下把问题拆解清楚”这个能力本身练出来了。没有GPU你会被迫想清楚每一个epoch为什么慢、数据加载卡在哪、模型结构哪里冗余这些思考在GPU充裕后依然是做研究的基本功。GPU可以买到但这些用时间堆出来的排查经验才是在实验室资源紧张时真正值钱的东西。如果你现在正因为没显卡而卡住我的建议很简单先按这篇文章里的思路把本地CPU环境跑通一个小实验用真实数据验证模型能收敛再考虑上云租GPU跑全量——你会发现本地CPU上的每一步调试都在帮你在云上省时间和钱。
返回列表