怎么衡量一个神经网络的「好」与「快」

做模型优化,第一步往往不是动手改网络,而是想清楚一件事:我到底在优化什么,又怎么知道它变好了。 有句话说得好——If you can not measure it, you can not improve it。所以在谈任何加速、压缩之前,先得把”性能”这个词拆开。

一个神经网络的性能可以分成两条相互独立的线:

  • 面向任务的性能:模型在它要解决的任务上做得有多好,比如分类准确率、检测的 mAP。
  • 面向效率的性能:模型跑起来要花多少代价,包括时间(时延)和空间(内存)。

我们的目标,是在这两个维度上同时把模型往前推。下面分别来看。

面向任务的性能指标

卷积神经网络(CNN)的应用大多落在计算机视觉上,这里挑几类最常见的任务——图像分类、目标检测、语义分割、超分辨率——看看它们各自用什么指标。

分类分单标签和多标签两种场景。单标签分类一般看 Top-1 和 Top-5 准确率:都是”分类正确的样本数 / 总样本数”,区别在于 Top-1 要求模型输出概率最大的类别与标签一致,Top-5 则只要概率最高的前五个类别里有一个命中即可。但准确率本身不可导,没法直接拿来当优化目标,单标签分类通常用交叉熵损失:

Loss(y,c)=logexp(y[c])jexp(y[j])w[c]\text{Loss}(y, c) = -\log\frac{\exp(y[c])}{\sum_j \exp(y[j])}\cdot w[c]

其中 yy 是输出的概率向量(维度等于类别数),cc 是标签类别,ww 是各类别的权重。多标签分类则常把每个标签看作一个二分类问题,最后把所有标签上的损失求和或求平均归约成一个数。以求平均、总类别数为 NN 为例:

Loss(y,t)=1Nn=1Nln,ln=[tnlogyn+(1tn)log(1yn)]wn\text{Loss}(y, t)=\frac{1}{N}\sum_{n=1}^{N} l_n,\qquad l_n = -\big[\,t_n\log y_n+(1-t_n)\log(1-y_n)\,\big]\cdot w_n

目标检测最常用的指标是 mAP(mean Average Precision)。它由各个类别的 AP(Average Precision)取平均得到,而每个类别的 AP 等于该类别 PR 曲线(Precision-Recall Curve)下方的面积。算法大致是:模型在整个数据集上预测出一批检测框后,对某个具体类别,先取出该类别在所有图片上的全部预测框,按置信度从高到低排序;再依次和标签框(ground truth)比对,如果预测框与某个标签框的 IoU(Intersection over Union)超过阈值(通常取 0.5),就记为 TP(True Positive),否则记为 FP(False Positive),并把该标签框标记为已检测、不再参与后续匹配;通过设定不同的置信度阈值,可以得到一系列 TP/FP,进而算出一系列精确率和召回率:

Precision=TPTP+FP,Recall=TPTP+FN\text{Precision} = \frac{TP}{TP+FP},\qquad \text{Recall} = \frac{TP}{TP+FN}

以召回率为横轴、精确率为纵轴描点并插值,就得到 PR 曲线,曲线下面积即为 AP。

目标检测任务示意
目标检测:框出图中目标并给出类别。
IoU 的计算
IoU = 预测框与标签框的交集面积 / 并集面积。
PR 曲线
PR 曲线下方的面积即为该类别的 AP。

语义分割常用 mIoU(mean Intersection over Union):和检测类似,在每个类别上分别计算 IoU(算法同上图),再求平均。

语义分割任务示意
语义分割:为每个像素分配类别。

超分辨率、图像修复、图像压缩等任务常用峰值信噪比 PSNR(Peak Signal to Noise Ratio)。PSNR 越高,代表压缩/复原后的图像与原图误差越小,图像压缩中典型值在 30dB 到 50dB 之间:

PSNR=10log10MAXI213mnc{R,G,B}i=0m1j=0n1[Ic(i,j)Kc(i,j)]2\text{PSNR}=10\log_{10}\frac{\text{MAX}_I^2}{\frac{1}{3mn}\sum_{c\in\{R,G,B\}}\sum_{i=0}^{m-1}\sum_{j=0}^{n-1}\big[I_c(i,j)-K_c(i,j)\big]^2}

面向效率的性能

效率这条线分两个方面:时间上的和空间上的。因为网络训练只做一次,部署之后反复跑的是推理,所以这里主要讨论推理过程中的效率(训练过程本身的加速是另一个话题)。

时间:时延、FLOPs 与它的局限

时间上的效率主要指推理时延(latency)——做一次前向传播要花多久。时延和三件事有关:模型本身、软件实现、硬件平台。测量时这三者都会显著影响结果,所以面向模型做优化时,必须在一套稳定的软硬件环境里测,排除其他干扰:用同一个 docker 镜像、同一块硬件、测之前对 cache 做预热(warmup)、多组取平均,等等。

但不同研究者很难在统一的软硬件平台上对比,于是常用一个间接指标 FLOPs(floating point operations,有些论文也叫 Mult-Adds)来替代时延。CNN 的 FLOPs 主要集中在卷积层和全连接层。卷积层:

FLOPs=H×W×(K×K×Cin+1)×Cout\text{FLOPs}=H\times W \times (K\times K \times C_{in}+1)\times C_{out}

其中 H,WH,W 是输出特征图的长宽,Cin,CoutC_{in},C_{out} 是输入/输出通道数,KK 是卷积核大小。全连接层:

FLOPs=(2×Nin1)×Nout\text{FLOPs}=(2\times N_{in}-1)\times N_{out}

Nin,NoutN_{in},N_{out} 为输入/输出维度。算某个网络的 FLOPs 已有现成工具,比如基于 PyTorch 的 torchprofile

间接指标自然有局限:它算不进访存开销,也算不进并行化、高性能算子实现带来的提速。为了看清这个差距,我对一批经典网络分别测了 FLOPs 和统一平台上的 GPU 时延,并统计了卷积、矩阵相乘(gemm)和其他操作各自的 FLOPs/时延占比:

经典网络的 FLOPs 与时延对比
经典网络结构的 FLOPs 与时延(输入 224×224×3)。整体正相关,但有明显的例外。

整体上 FLOPs 与时延正相关,但有几个扎眼的例外,最典型的是 VGG16 与 DenseNet161 反了。VGG16 有 15470.3M FLOPs、1384.11ms 的 GPU 时延(转成 TensorRT 预热后连续 200 次推理的总时间);DenseNet161 只有 7757.6M FLOPs——FLOPs 仅为 VGG16 的一半,时延却超过它两倍。原因在于 FLOPs 只算了计算量,没算访存。DenseNet161 里有大量 Copy 操作(转 TensorRT 后约为卷积操作的 10 倍),这些开销完全不反映在 FLOPs 上。即便扣掉 Copy,DenseNet161 仍略慢于 VGG16:它有 160 个卷积操作,而 VGG16 只有 13 个——VGG16 的 FLOPs 更大,卷积操作数却少得多,访存开销小,反而更快。ShuffleNetV2 也类似,其中的 SplitReshapeTranspose 占了约 20% 的时间,但都不体现在 FLOPs 上。

还有个值得注意的现象:AlexNet 里 gemm 操作的 FLOPs 占比只有 8.2%,时延占比却高达 67.7%(可惜实验跑在 docker 容器里,没权限访问 NVIDIA GPU Performance Counters,没法对 kernel 进一步深挖)。

最后看深度分离卷积(depthwise convolution)。下图里 MobileNetV2_d 用深度分离卷积,MobileNetV2_n 把所有深度分离卷积换成普通卷积。MobileNetV2_d 相比 MobileNetV2_n,FLOPs 小了 18 倍,实际提速却不到 4 倍——逐层对比可以看到,深度分离卷积大幅砍掉了 FLOPs,却并不怎么减小访存开销:

MobileNetV2 每层的 FLOPs 与时延
MobileNetV2 每一层的 FLOPs 与时延对比。

既然时间主要花在卷积层和全连接层上,在 GPU 平台上就可以用 NVIDIA Nsight Systems 或 TensorRT 自带的 Profiler 做性能分析(profiling),直接测出各部分的时间开销,再有针对性地优化——因为优化不同部分对整体的提升是不一样的,这就是 Amdahl 加速定律:

Speedupoverall=1(1Fenhanced)+FenhancedSenhanced\text{Speedup}_{\text{overall}}=\frac{1}{(1-F_{\text{enhanced}})+\dfrac{F_{\text{enhanced}}}{S_{\text{enhanced}}}}

其中 FenhancedF_{\text{enhanced}} 是被优化部分占原执行时间的比例,SenhancedS_{\text{enhanced}} 是这部分本身的加速比。

用 NVIDIA Nsight Systems 做 profiling
用 NVIDIA Nsight Systems 对推理过程做 profiling。

空间:参数量与峰值内存

空间上的效率主要看运行时的内存开销(memory footprint)。它同样取决于网络结构和软件实现(比如有的推理框架会及时释放已被下一层用过的特征图,或对深度分离卷积用 inplace 实现,或用 FP16 推理)。这里假定所有参数都以 32 位浮点存储和计算,只围绕网络结构本身讨论。

内存开销有两个来源:一是网络自身的参数量(运行时占内存,存储时占磁盘);二是运行时的峰值内存,主要来自运行中的特征图,它不仅和网络结构有关,还取决于输入大小(batch、长宽、通道数)。学界讨论参数量的多,讨论峰值内存的少,但对多分支结构来说,在分支较多的节点做运算时要同时保存大量特征图,峰值内存可能很大,哪怕这类网络自身参数量并不多。

和 FLOPs 一样,内存开销也能在不依赖运行时平台的情况下较准确地算出来。先看参数量,同样主要来自卷积层和全连接层。卷积层(含 bias):

#Params=(K×K×Cin+1)×Cout\#\text{Params}=(K\times K \times C_{in}+1)\times C_{out}

全连接层:

#Params=(Nin+1)×Nout\#\text{Params}=(N_{in}+1)\times N_{out}

至于峰值内存,假定推理过程中只存网络权重和当前需要的特征图,那么运行时的最大内存开销,就约等于全部权重参数占用的内存,加上运行中最大的那一组特征图占用的内存。

把这两条线——任务精度与运行效率——的指标都定义清楚,后面所有的结构设计、训练技巧、剪枝、量化,才有一个能对齐的标尺。

参考资料

  • He, Kaiming, et al. Deep Residual Learning for Image Recognition. CVPR, 2016.
  • Howard, Andrew G., et al. MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications. arXiv:1704.04861, 2017.
  • Sandler, Mark, et al. MobileNetV2: Inverted Residuals and Linear Bottlenecks. CVPR, 2018.
  • Howard, Andrew, et al. Searching for MobileNetV3. arXiv:1905.02244, 2019.
  • Paszke, Adam, et al. PyTorch: An Imperative Style, High-Performance Deep Learning Library. NeurIPS, 2019.
  • Abadi, Martín, et al. TensorFlow: A System for Large-Scale Machine Learning. OSDI, 2016.