第3章 深度学习模型基础回顾:神经网络核心概念、卷积/池化/全连接层、模型文件格式(ONNX/TensorRT)
好,咱们进入第三章。说实话,很多做嵌入式部署的同学,模型跑不起来第一反应是“板子太弱”,其实根子往往在模型本身。你连卷积层里几个参数都没搞明白,怎么去优化推理速度?
这一章,我带你快速过一遍深度学习模型的核心零件。不是从零讲数学,而是从“部署工程师”的视角,告诉你每个组件在嵌入式端意味着什么。
3.1 神经网络核心概念:从“函数”到“计算图”
神经网络说白了,就是一个超级复杂的函数。你给它输入一张图片,它输出一个类别概率。但内部是怎么组织的?
- 神经元:最基本的计算单元,做一次加权求和 + 激活。
- 层(Layer):一堆神经元排在一起,比如卷积层、全连接层。
- 计算图(Computational Graph):所有层串起来,形成一个有向无环图。推理就是沿着这个图做前向传播。
部署视角的关键点:
计算图的拓扑结构直接决定了推理引擎的调度策略。层与层之间的连接越复杂,内存搬运就越频繁,延迟就越高。
我个人习惯,拿到一个模型先画它的计算图。不是用纸笔,而是用 Netron 工具打开 ONNX 文件看一眼。嗯,这一步能帮你省掉后面至少一半的调试时间。
3.2 卷积层:嵌入式优化的主战场
卷积层是 CNN 的绝对主力,也是推理耗时的头号大户。你想想看,一个 MobileNetV2 里,卷积操作占了总计算量的 90% 以上。
3.2.1 标准卷积
输入特征图 (H, W, C_in),卷积核 (K, K, C_in, C_out),输出特征图 (H', W', C_out)。计算量 = H' × W' × C_out × K × K × C_in。
这个公式你得刻在脑子里。为什么?因为嵌入式端的算力瓶颈,往往就卡在这个乘法的数量级上。
3.2.2 深度可分离卷积
这是 MobileNet 系列的核心。它把标准卷积拆成两步:
- Depthwise 卷积:每个输入通道单独用一个 3×3 卷积核处理。
- Pointwise 卷积:用 1×1 卷积把通道数升上去。
计算量对比:标准卷积是 K² × C_in × C_out,深度可分离是 K² × C_in + C_in × C_out。当 C_out 很大时,后者能省 8-9 倍的计算量。
我的经验:在 RK3588 上部署 MobileNetV3,用 NPU 跑深度可分离卷积时,发现 Depthwise 层利用率只有 40%。后来调整了输入张量的内存对齐,利用率直接拉到 85%。
3.2.3 分组卷积与空洞卷积
| 类型 | 原理 | 嵌入式场景 |
|---|---|---|
| 分组卷积 | 将输入通道分成 g 组,每组独立做卷积 | ShuffleNet、EfficientNet 常用,减少参数量 |
| 空洞卷积 | 卷积核元素之间插入空洞,扩大感受野 | 语义分割任务,不增加参数量但增加计算量 |
我曾经在 Jetson Orin 上踩过一个坑:用空洞卷积做实时分割,帧率死活上不去。后来发现是空洞卷积的访存模式不连续,导致缓存命中率极低。换成等效的 stride 卷积 + 上采样,帧率直接翻倍。
3.3 池化层:降采样与特征压缩
池化层的作用很简单:缩小特征图尺寸,减少后续层的计算量,同时提供一定的平移不变性。
- 最大池化(Max Pooling):取窗口内的最大值。保留最显著的特征。
- 平均池化(Average Pooling):取窗口内的平均值。平滑特征。
- 全局平均池化(Global Average Pooling):整个特征图取平均,直接输出一个向量。常用于分类网络的最后一层。
注意:在嵌入式端,池化层的计算量虽然小,但它的 stride 和 kernel size 会影响后续层的输入尺寸。如果尺寸不是 2 的幂次,NPU 的 DMA 搬运效率会下降。我建议你尽量让池化后的尺寸是 4 或 8 的倍数。
3.4 全连接层:参数量黑洞
全连接层(FC)把输入向量和权重矩阵做乘法,输出一个新的向量。它的参数量 = 输入维度 × 输出维度。
举个例子:VGG16 的第一个 FC 层,输入 25088,输出 4096,参数量超过 1 亿。而整个 VGG16 的卷积层加起来才 1400 万参数。你想想看,这 1 亿参数在嵌入式端意味着什么?
- 内存占用:1 亿个 float32 参数 = 400 MB。很多嵌入式设备总共才 1-2 GB 内存。
- 计算延迟:矩阵乘法是 O(n²) 复杂度,输入越大越慢。
所以,现代轻量级网络(MobileNet、EfficientNet-Lite)都尽量用全局平均池化 + 1×1 卷积来替代 FC 层。如果必须用 FC,我建议你至少做量化到 int8,参数量不变但内存占用降到 100 MB。
3.5 模型文件格式:ONNX 与 TensorRT
模型训练好之后,不能直接扔到嵌入式设备上跑。你需要一个中间格式来做转换和优化。这里我重点讲两个:ONNX 和 TensorRT。
3.5.1 ONNX:模型交换的通用语言
ONNX(Open Neural Network Exchange)是微软和 Meta 联合推出的开放格式。它的核心作用:
- 跨框架互转:PyTorch 转 ONNX,ONNX 再转 TensorRT、OpenVINO、TFLite。
- 计算图标准化:所有算子都有统一的定义,方便推理引擎做优化。
导出 ONNX 的代码示例(PyTorch):
import torch
import torchvision.models as models
model = models.resnet18(pretrained=True)
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
"resnet18.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}
)
print("ONNX 导出完成")
避坑指南:我曾经导出一个包含 torch.where 算子的模型,ONNX 死活不支持。后来用 torch.where 等价替换成乘法 + 加法,才顺利导出。建议你在导出前先用 torch.onnx.export 的 check 参数做一次验证。
3.5.2 TensorRT:NVIDIA 的推理加速引擎
TensorRT 是 NVIDIA 专门为自家 GPU 和 Jetson 系列做的推理优化库。它做的事情:
- 图优化:合并 Conv + BN + ReLU 为一个算子。
- 精度校准:FP32 → FP16 → INT8,精度损失可控。
- 内存复用:减少推理过程中的内存分配和释放。
ONNX 转 TensorRT 的流程:
import tensorrt as trt
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, TRT_LOGGER)
with open("resnet18.onnx", "rb") as f:
parser.parse(f.read())
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB
serialized_engine = builder.build_serialized_network(network, config)
with open("resnet18.engine", "wb") as f:
f.write(serialized_engine)
print("TensorRT 引擎构建完成")
注意:TensorRT 引擎是硬件绑定的。你在 Jetson Orin 上构建的 .engine 文件,不能直接拿到 Jetson Xavier 上跑。每次换设备都要重新构建。
3.6 本章知识结构图
下面这张 SVG 图,把本章的核心知识点串起来了。你可以把它当作一张“部署前的模型检查清单”。
3.7 本章小结
这一章我们聊了神经网络的核心组件:卷积层(标准、深度可分离、分组、空洞)、池化层(最大、平均、全局平均)、全连接层(及其替代方案),以及模型文件格式 ONNX 和 TensorRT。
你可能会问:这些基础概念跟嵌入式部署有什么关系?关系大了去了。你后面做量化、做算子融合、做内存优化,每一步都离不开对这些组件的理解。说白了,模型部署不是黑盒调参,而是对模型内部结构的深度理解。
一句话总结:卷积层是性能瓶颈,池化层是尺寸控制,全连接层是参数量黑洞,ONNX 是桥梁,TensorRT 是加速器。把这五样东西搞明白,你的模型部署之路就成功了一半。