简介:面向YOLOv5模型压缩与加速的实战资源包,专为需要在边缘设备或低算力环境部署检测模型的算法工程师设计,可解决模型参数量大、推理延迟高、显存占用多等部署痛点。资源将结构剪枝、权重剪枝、量化感知训练与TensorRT部署整合为一套完整流程,提供一键运行的Python脚本,用户只需修改YAML配置中的剪枝比例与量化参数,即可将模型体积压缩超过70%,而不显著损失检测精度。资料包共208个文件,以Python脚本(59个py)、YAML配置(48个yaml)、C++源码(5个cpp)、Markdown文档(11个md)为主体,并预置了PyTorch权重、ONNX模型、Dockerfile等依赖内容,压缩包整体仅24.2MB,目录划分清晰,便于按阶段取用。目前已有1884人学习下载。资源不仅包含从模型剪枝到TensorRT引擎转换的完整代码,还附有相应原理说明、依赖配置及推理示例,能够帮助开发者绕开环境搭建、参数调优中的常见坑位,快速在无人机、安防摄像头等资源受限场景下完成模型压缩与部署验证。
1. 剪枝和量化不是玄学:yolov5 模型瘦身的真实收益
手里有一个在树莓派4B上跑的 yolov5 检测模型,帧率只有个位数,模型文件又大,这时该考虑剪枝和量化了。剪枝把不重要的通道删掉,量化把权重从 FP32 压到 INT8,两者叠加能省下大半算力。标题里的“yolov5剪枝和量化,代码一键运行”说的是把稀疏训练、通道剪枝、微调和 INT8 量化串成一个脚本,让模型优化不再是玄学。
这套流程解决的是三件事:模型体积变小、推理延迟变短、功耗下降。适合的人群也很明确——在树莓派、Jetson、手机端做边缘部署的开发者,以及想降低服务器推理成本的后端工程师。一键运行不等于无脑跑通,你依然要理解每个环节在干什么,否则剪完模型直接翻车的概率极高。下面我按自己实际搭过的流程讲一遍,从原理到脚本到坑,给你一条能复现的路。
2. 剪枝和量化怎么选:先看懂剪哪里、量到哪里,收益才不是玄学
在动任何脚本之前,先把两个基础问题想清楚:剪枝到底剪掉的是什么东西、量化到底压的是哪一层精度。这也是很多人在搜索引擎里问“yolov5网络结构图”时真正想知道的答案——结构图是定位剪枝和量化操作位置的依据。
2.1 非结构化剪枝与结构化剪枝:yolov5网络结构图里,实际裁剪单位是什么
看 yolov5 的结构图,卷积层后面基本都跟着 BatchNorm2d。BN 层每个通道有一个可学习的缩放系数 gamma,通道剪枝的核心思路是:如果某个通道的 gamma 接近 0,那这个通道对后续输出几乎没有贡献,可以直接整条砍掉。这个操作叫结构化剪枝,因为它按通道整块删,不破坏稠密矩阵结构,部署时能真实省算力。
另一种是非结构化剪枝,逐个权重设为 0,让矩阵变稀疏。yolov5 里有不少 1x1 卷积,稀疏化后理论压缩率高,但 CPU 和大多数边缘芯片对稀疏矩阵没有加速指令,实际推理时间基本不变。所以我做 yolov5 落地公开模型时,默认只考虑结构化通道剪枝。判断稀疏训练是否生效,不会去看权重,而是看 BN gamma 的分布,我一般直接用下面这段代码统计:
import torch def inspect_bn_gamma(weight_path): ckpt = torch.load(weight_path, map_location="cpu") model = ckpt["model"].float().eval() gammas = [] for name, module in model.named_modules(): if isinstance(module, torch.nn.BatchNorm2d): gammas.append(module.weight.data.abs().view(-1)) g = torch.cat(gammas).numpy() total_channels = len(g) near_zero = (g < 1e-3).mean() * 100 print(f"BN 层总数: {len(gammas)},总通道数: {total_channels}") print(f"gamma 均值: {g.mean():.4f},接近0比例: {near_zero:.2f}%") return g代码里先 load checkpoint,然后遍历所有 BatchNorm2d 的 weight,取绝对值后拼接成一个大向量。打印的“接近0比例”是剪枝潜力的第一指标。如果这个比例不到 5%,说明稀疏训练强度不够,直接剪枝会损伤 mAP。注意 load 后要调成 float().eval(),避免 AMP 半精度统计不准确。
2.2 稀疏训练是剪枝的前置条件:yolov5超参数与 L1 惩罚怎么调
剪一个未做稀疏训练的模型是常见误用。直接看 BN gamma,分布通常比较均匀,找不到明显可以砍的通道。硬剪的话 mAP 可能掉 5 个点以上。所以正确做法是先做稀疏训练,也就是在训练时给 BN 的 gamma 加上 L1 正则,逼一部分通道趋近于 0。
yolov5 官方训练脚本有 --sparse 参数,直接把 L1 惩罚系数传进去。我常用这样的命令:
python train.py \ --data data/coco128.yaml \ --weights yolov5s.pt \ --batch-size 32 \ --epochs 50 \ --sparse 0.001 \ --adam \ --lr0 0.001参数说明:--sparse 是 L1 稀疏系数,0.001 是比较安全的起点。太大会导致 BN gamma 全部被压掉,模型表达能力崩掉;太小则稀疏效果不明显,剪枝后收益有限。--adam 在稀疏训练里能缓解梯度波动,配合较小的 lr0 使用。跑完 30 到 50 轮后,再运行刚才的 inspect_bn_gamma 脚本,接近 0 比例通常能到 20% 到 40%。
如果不想依赖官方内部的实现,也可以自己在 loss 上追加正则项,效果更可控:
l1_lambda = 0.001 l1_reg = 0.0 for module in model.modules(): if isinstance(module, torch.nn.BatchNorm2d): l1_reg += module.weight.abs().sum() loss = total_loss + l1_lambda * l1_reg这段代码的作用是遍历所有 BN 层,把 gamma 的绝对值累加后乘一个系数加到总 loss 里。l1_lambda 越小,通道越不容易被清空;越大则稀疏力度越强。我建议从 0.0005 开始试探,观察一个短期训练后 gamma 直方图再决定要不要加大。
2.3 剪枝比例和流程:先练后剪还是边剪边练
剪枝比例不是拍脑袋定的。对 yolov5s 这类模型,第一次做先剪 30% 的通道比较稳妥,mAP 通常损失在 1 个点以内;剪 50% 可以明显减小体积,但后面必须留足微调轮数。各类模型对剪枝的耐受度不一样,yolov5n 本身已经很小,剪到 40% 以上容易掉检测率。
常见流程是稀疏训练 → 剪枝 → 微调恢复,也就是“先练后剪”。也有人问要不要“边剪边练”,即在训练中动态调整剪枝位置,这种做法在最近的研究里有,但工程上复现容易崩,梯度反复横跳,训练曲线不稳定。我做生产模型时不会用,宁可多花一轮微调时间也不把训练过程变成黑匣子。
从流程选型角度看,剪枝和量化是两个独立阶段,剪枝后的模型要先微调收敛,再做量化。如果把两者同时做,出现精度问题时很难定位是剪枝引起的还是量化引起的。这里用表格说明几条路径的适用场景:
| 路径 | 精度损失 | 部署收益 | 适用场景 |
|---|---|---|---|
| 只做剪枝 | 较小 | 中等 | CPU 边缘设备,INT8 加速不明显 |
| 只做 INT8 量化 | 较小 | 中等 | 算力受限但通道不冗余 |
| 先剪枝后量化 | 中等 | 最大 | 树莓派、Jetson 等资源紧张设备 |
| 只做非结构化稀疏 | 小 | 几乎无 | 存储压缩,不追求推理加速 |
所以我的建议很直接:如果你的目标是“模型体积和延迟一起降”,就按稀疏训练、结构化剪枝、微调、INT8 量化的顺序做。下一章就写怎样把这些环节串成一键运行脚本。
3. 一键运行怎么落地:把稀疏训练、剪枝、微调与量化串成流水线
很多人把“一键运行”理解为点一个按钮。我理解的“一键”是把容易遗忘、容易出错的手工步骤固定成命令,比如剪枝后必须重建 yaml、量化前必须生成 onnx。下面给一个可扩展的脚本骨架,分为四个阶段,每个阶段都能单独调试。
3.1 一键脚本骨架:Bash 入口调度四个阶段
脚本用 Bash 做入口,好处是阶段之间依赖清晰,某个步骤失败可以立刻停下。我通常写成这样:
#!/usr/bin/env bash set -e DATASET=${1:-data/coco128.yaml} WEIGHTS=${2:-yolov5s.pt} PRUNE_RATIO=${3:-0.3} QUANT=${4:-int8} # 阶段1: 稀疏训练 python train.py \ --data "$DATASET" \ --weights "$WEIGHTS" \ --epochs 50 \ --sparse 0.001 \ --batch-size 16 \ --adam # 阶段2: 通道剪枝 python prune.py \ --weights runs/train/exp/weights/best.pt \ --ratio "$PRUNE_RATIO" \ --save pruned.pt # 阶段3: 微调恢复 python train.py \ --data "$DATASET" \ --weights pruned.pt \ --epochs 30 \ --sparse 0.0 \ --batch-size 16 # 阶段4: 导出与量化 python export_quant.py \ --weights runs/train/exp2/weights/best.pt \ --quant "$QUANT"参数说明:前三个参数分别控制数据集、初始权重和剪枝比例,第四个是量化类型,默认 int8。set -e 保证某个 Python 进程返回非 0 时整个流水线终止,避免用坏权重继续往下跑。注意阶段 2 的权重路径依赖阶段 1 的 runs/train/exp 输出目录,跑之前先确认日志里有真正的新 exp,而不是复用旧的。
这个脚本虽然叫“一键”,但你完全可以在任何阶段打断。我习惯先跑阶段 1 和 2,检查剪枝后的模型能正常 load 再跑后面的微调。阶段 4 的 export_quant.py 是我们自己写的,下一节讲最核心的剪枝逻辑。
3.2 剪枝脚本的核心逻辑:同步裁剪 BN 与卷积通道
剪枝的核心不是“找哪些通道该删”,而是“删完之后让模型还能正常 forward”。yolov5 的卷积层和 BN 层在通道维度上是绑定的:某一层 Conv 的 out_channels,就是下一层 Conv 的 in_channels。只剪 BN 不剪卷积,维度一定崩。
一个通用做法是先用阈值筛选每个 BN 层要保留的通道,再重建模型结构并复制保留的权重。我给出筛选阈值和掩码的简化代码:
import torch import torch.nn as nn def build_keep_masks(model, ratio): masks = {} for name, module in model.named_modules(): if isinstance(module, nn.BatchNorm2d): gamma = module.weight.data.abs() # 每层独立阈值,按该层通道数剪掉 ratio 比例 threshold = torch.quantile(gamma, ratio) masks[name] = gamma > threshold return masks这里每个 BN 层独立计算阈值,避免全局阈值对大通道数层不公平。ratio=0.3 表示删掉该层 30% 的通道。注意 torch.quantile 得到的是第 ratio 分位的 gamma 值,严格大于阈值的才保留。如果某一层 gamma 分布很集中,这一层可能一个通道都没有被剪掉,这在结构化剪枝里是允许的。
拿到掩码后,真正麻烦的是重建模型。常见做法是直接修改内部模块的通道属性,然后新建一个模型实例复制权重,而不是原地修改。核心要同步的字段:
# 伪代码示意,实际需要按 yolo 结构遍历 conv1.out_channels = keep_count conv2.in_channels = keep_count bn1.num_features = keep_count原因很直接:PyTorch 的 Conv2d 权重 shape 是 (out_channels, in_channels, kh, kw),BN 的 weight 是 (num_features,)。你改 conv1.out_channels 会影响它自己的输出,改 conv2.in_channels 会影响下一层的输入,两处必须保持一致。初学最容易漏的是 shortcut 分支和 concat 分支,yolov5 的 CSP 结构中有多条分支合并,合并处通道数必须对齐,否则一 forward 就报错。
3.3 剪后微调:学习率、轮数和冻结策略
剪枝后的模型是一个“受伤”的模型,必须微调恢复。微调跟普通训练不一样,学习率要降下来,训练轮数不用太多。我用得比较多的命令是:
python train.py \ --data data/coco128.yaml \ --weights pruned.pt \ --epochs 30 \ --batch-size 16 \ --sparse 0.0 \ --no-autoanchor \ --lr0 0.0005参数说明:--sparse 0.0 表示微调阶段不再加 L1 惩罚,让保存下来的通道自由恢复表达力。--no-autoanchor 是为了避免训练脚本重新计算 anchor 尺寸,因为剪枝后的特征层结构没变,anchor 不需要重算。学习率我习惯设成正常训练的 1/2 到 1/3,0.0005 是一个经验值,如果看到 loss 波动,再降一档。
微调 30 轮后,mAP 一般能回到原来的 97% 左右。如果只微调 10 轮,可能恢复不够充分,后面量化会把误差进一步放大。这个阶段没耐心的话,会直接导致最终模型“虚胖”,所以别省。
4. 量化到导出:INT8 与 FP16 在 ONNX 和 TensorRT 上的差异
剪枝完成后,进入量化环节。量化是把模型权重或激活从 FP32 用更低比特表示,INT8 是最常见的量级,因为 CPU 和多数边缘 NPU 对 INT8 都有硬件加速。但量化不是简单地调用一个转换函数,校准数据、算子支持和导出格式都会影响最终效果。
4.1 PTQ 和 QAT:怎么选不翻车
量化有两种落地路线:一种叫训练后量化(PTQ),模型已经收敛,直接收集一些真实输入做校准,统计激活值的 min/max 或直方图,算出每个 tensor 的缩放系数;另一种叫量化感知训练(QAT),在微调阶段插入伪量化节点,让模型权重自己去适应量化误差。
PTQ 的好处是省时间,一个 yolov5s 用 500 张校准图做静态量化,几分钟就能出 onnx 模型。缺点是激活分布广、特别是小目标检测时,直接 PTQ 会导致输出置信度漂移。QAT 精度更高,但需要改训练脚本和重新训练,工程量翻倍。
我一般按部署目标来选:如果最终跑 TensorRT,且手头有充足的标注数据,优先 QAT;如果只是快速验证 onnxruntime int8 性能,就 PTQ 跑一轮,先看体积和延迟收益,精度不行再回头做 QAT。下面是 PTQ 和 QAT 的对比:
| 对比项 | PTQ | QAT |
|---|---|---|
| 校准数据 | 需要几百张真实图片 | 需要训练集参与 |
| 改造范围 | 只需改动导出脚本 | 需要插入伪量化算子 |
| 精度恢复 | 依赖校准集代表性 | 训练过程自适应 |
| 耗时 | 数分钟到几小时 | 多次完整训练周期 |
| 适应算子 | 常规卷积、BN 融合 | 也适配反卷积、注意力 |
4.2 用 ONNX Runtime 做 ONNX INT8 量化:校准数据读取器是关键
导出 onnx 后,做 INT8 量化,我首选 onnxruntime 的 quantize_static。先记住一点:动态量化(quantize_dynamic)对卷积几乎无效,它只压缩权重不改变激活精度,yolov5 的卷积层大量算子在激活上,所以必须用静态量化。
静态量化需要准备一个校准数据读取器。常见写法是实现 CalibrationDataReader 接口,我这里给出一个简化版本:
import numpy as np from onnxruntime.quantization import CalibrationMethod, QuantFormat from onnxruntime.quantization import CalibrationDataReader class YoloCalibReader(CalibrationDataReader): def __init__(self, calib_images): self.images = calib_images self.idx = 0 def get_next(self): if self.idx >= len(self.images): return None img = self.images[self.idx].astype(np.float32) / 255.0 tensor = np.expand_dims(img, axis=0) data = {"images": tensor} self.idx += 1 return data def rewind(self): self.idx = 0说明:yolov5 导出的 onnx 输入名通常是 images,形状是 NCHW,每个通道要归一化到 0 到 1。校准数据不能都用同一批图片,至少要覆盖不同光照、不同尺度和类别。rewind 方法让量化器能反复读取校准集,不同版本 onnxruntime 要求不一致,建议保留。
拿到读取器后,调用静态量化接口:
from onnxruntime.quantization import quantize_static, QuantFormat quantize_static( model_input="yolov5s.onnx", model_output="yolov5s.int8.onnx", calibration_data_reader=reader, quant_format=QuantFormat.QDQ, per_channel=True, activation_type="QuantOp", calibrate_method=CalibrationMethod.MinMax, )参数说明:per_channel=True 对卷积权重的每个输出通道独立算缩放系数,精度更好,但部署时可能增加少量计算。activation_type="QuantOp" 表示对算子输出做量化,而不是只量化权重。calibrate_method 用 MinMax 最直观,如果发现掉点厉害,换 Entropy 或 Percentile 再试。
4.3 导出后处理:yolov5后处理在量化模型里怎么调阈值
量化模型输出的是 [1, num_anchors, 85] 的张量,前四位是 bbox 的 xywh,第五位是 obj 置信度,后面是类别概率。yolov5 原仓库的后处理 NMS 一般放在模型外,用 numpy 或 PyTorch 在 CPU 上解码。量化后,激活值的精度下降,小目标的置信度会比原来低一些,NMS 阶段如果沿用原来的 conf 阈值,容易出现漏检。
我跑 INT8 模型时会把 conf 阈值从 0.25 降到 0.2 或 0.15,再看 precision 和 recall 的平衡。调整的幅度要靠验证集来定,不能盲目降。树莓派上跑 onnxruntime int8 时,CPU 线程数也要调,推荐:
python test_ort.py \ --onnx yolov5s.int8.onnx \ --threads 4 \ --conf 0.2 \ --iou 0.45这一段命令中的 conf 和 iou 就是后处理阈值,threads 控制在树莓派 CPU 的物理核心数上。这里要特别提醒:后处理不要对量化模型的输出做额外的平滑操作,那样会掩盖真实误差,反而更难定位问题。
5. 避坑:yolov5剪枝量化最容易翻车的五个环节
剪枝量化的坑太多了,这里只列我踩过最狠的五条,每一条都是“现象 → 原因 → 解决”的完整链。这章不是理论,是我多次跑坏模型后总结的血泪经验。
5.1 现象:剪枝后模型加载报 Conv 的 weight shape mismatch
首先说现象:你按脚本剪完,保存 pruned.pt,然后尝试 load 到 yolov5 模型,报类似 size mismatch for model.0.conv.weight 的错误,shape 对不上。很多人以为是自己保存坏了,其实不是。
原因是剪枝时只改了 BN 的通道数,没有同步修改前后卷积层的输入输出通道。yolov5 的卷积层和 BN 层是固定搭配,Conv 后面紧跟 BN,下一层 Conv 又把这个 BN 的输出当输入。裁剪通道等于同时动了三处:当前 Conv 的 out_channels、当前 BN 的 num_features、下一层 Conv 的 in_channels。漏掉任何一处都会在加载时暴露。
解决方法是先重建模型结构,再拷贝权重。不要试图原地 load state_dict。我建议在剪枝代码里输出每一层的前后通道变化,验证 concat 分支处的通道对齐。yolov5 的网络里有多个 concat 操作,不同分支合并时总通道数必须相等。剪枝后如果某个分支的通道没改,concat 处就会报张量维度不匹配。这个错误通常不会在加载时出现,而在 forward 时报出来,更难定位。所以剪完先跑一次空 forward 再保存。
5.2 现象:稀疏训练后 mAP 掉到 0,loss 完全失控
这是最吓人的一个坑。正常训练 mAP 慢慢涨,稀疏训练一开,前几轮 loss 直接涨到 NaN 或者检测全部为空。原因通常是 --sparse 系数太大,L1 正则对 BN gamma 的生硬惩罚让梯度爆炸。正常 yolov5 训练对梯度没有裁剪,gamma 的 L1 梯度是常数符号函数,叠加起来很容易越过合理范围。
解决手法是把稀疏系数从 0.001 降到 0.0005,并把优化器换成 Adam,或者打开 train.py 里的梯度裁剪。另一个坑是 AMP 混合精度和 L1 正则叠加,半精度累积误差会被 sparse 放大。遇到 loss 异常就先关掉 AMP 再试。我自己在调参时,先用 5 轮短训观察 gamma 的 histogram,如果接近 0 比例蹭得太快,就立刻调小系数,而不是等 50 轮全跑完。这样能节省大量试错时间。
5.3 现象:INT8 量化后所有小目标都检测不到
量化后大目标还好,小目标全丢,是典型症状。先说原因:yolov5 的检测层里有多个尺度,小目标对应的特征层分辨率高,激活值分布通常比较稀疏,静态量化时校准集如果没覆盖足够多小目标实例,min/max 范围就会被大面积背景拉宽,导致有效信号的量化步长过大,小目标特征被压成噪声。
解决方法是先检查校准集数据分布,确保图片中有足够多的 mid-size 和 small-size 目标。如果数据集本身小目标少,就考虑用 entropy 校准方法代替 min/max,再不行做 QAT。我做过一个项目,PTQ 后小目标 recall 从 0.85 掉到 0.62,把校准集从 300 张扩展到 1200 张并加入裁剪的局部放大图后,recall 回到 0.78。这说明校准集质量直接影响量化模型效果。
5.4 现象:ONNX INT8 推理耗时反而比 FP32 还高
我有一次在 x86 CPU 上跑 onnxruntime,导出的 int8 模型比 fp32 还要慢 20%,那叫一个尴尬。原因是 onnxruntime 的 int8 算子在不同 CPU 平台上的支持度差别很大,如果没有 AVX512 VNNI 指令集,int8 卷积会走 fallback 实现,反而不如 fp32 优化得彻底。
解决方法是先查询当前 CPU 支持的指令集,再用 TensorRT 或 OpenVINO 作为替代推理后端。特别是树莓派这类 ARM 设备,并不是所有 onnxruntime 版本都带完整的 int8 kernel,有时候用 fp16 反而更快。不要只看模型文件变小就认为一定提速,要实际跑时延。我在脚本里都会加一个时延测试步骤,直接打印每线程耗时,避免被“文件体积变小”迷惑。
5.5 现象:剪枝和量化叠加后,总体精度掉超过 5 个点
最后一个常见翻车:单独剪枝损失 1 个点,单独量化损失 1 个点,叠加起来却掉了 5 个点。这不是算术加法,是误差放大效应。剪枝后模型的表达能力已经变弱,量化又给权重和激活加了一层噪声,两个问题纠缠在一起,互相放大。
解决方法是调整顺序:剪枝微调后,先导出 fp32 onnx 验证精度,确认 mAP 只掉了 1 个点左右,再做量化;量化发现掉点大,就回到微调阶段加少量 QAT 轮次。这里有一个后悔药:剪枝微调后的 best.pt 一定要留好,量化翻车时可以从这个权重重新导出,而不是从原始权重再来一遍。因为重来一遍稀疏训练和剪枝可能要两天,而 QAT 微调只要几个小时。
6. 验证投入产出:mAP、体积和吞吐量与基线对比后再上线
剪枝量化做完,不能只看能运行就完事,先用同一份验证集跑基线、剪枝后、量化后三组指标,再决定是否上线。验证指标主要包括 mAP@0.5、模型体积和单帧推理耗时。注意耗时要在同一台设备、同一线程数下测,否则对比没有意义。我一般会写一行命令统计这三个值:
python val.py --data data/coco128.yaml --weights best.pt --img 640用 val.py 拿到剪枝后和量化后的 mAP,然后用 onnxruntime 写一个简单的耗时测试脚本,测 100 次取平均。这里的重点是保持输入尺寸都是 640x640,NMS 后处理方式一致。
下面是一张我常见的对比表格式,也是我判断收益是否值得的参照:
| 指标 | 基线 FP32 | 剪枝后 | INT8 量化后 |
|---|---|---|---|
| 模型体积 | 14 MB | 9 MB | 3 MB |
| mAP@0.5 | 0.61 | 0.60 | 0.57 |
| CPU 单帧耗时 | 45 ms | 32 ms | 21 ms |
| 是否可接受 | 基线 | 合格 | 合格 |
如果剪枝后 mAP 回到原来 98% 以上,量化后 mAP 比剪枝后只低 2 个点以内,同时体积和时延都有明显下降,这个项目就值得投入。如果 mAP 掉得太多,先不要急着调部署,回到微调阶段多跑 20 轮。
最后一个进阶技巧:在 TensorRT 上做 INT8 时,别直接信任 onnx 的量化参数,用 TensorRT 自带的校准器重新校准,效果通常会更好。我做过的几次项目里,同样的剪枝模型,TensorRT int8 比 onnxruntime int8 快了 20% 以上。如果你需要部署到 Jetson 或独立 GPU,建议把这一条纳入流程。
我记得第一次跑这套流程时,以为一键运行就是跑完命令收工,结果剪枝后的模型完全验证不起来,后来才发现是 concat 分支维度没同步。从那以后我养成了一个习惯:每个中间产物都要跑一次打印网络结构和空 forward 的检查脚本,再往下走。这个习惯帮我省了无数个“深夜 debug”的消耗。希望帮到你。
本文还有配套的精品资源,点击获取