1. 参数量不是玄学:从一次模型部署翻车说起
去年帮一个做嵌入式AI的朋友看模型移植问题,他拿着一个在服务器上跑得好好的图像分类网络,想塞进一块算力只有零点几TOPS的开发板里。移植工具链报错说内存不够,他第一反应是“模型文件才十几兆,怎么会不够”。我让他把每一层的参数量和特征图尺寸都列出来,结果发现光是第一个全连接层就占了整个网络百分之九十以上的参数,而那块板子的内存根本扛不住这种量级的权重加载。这件事让我意识到,神经网络参数量计算这个看起来像是“数数”的基础功,实际上直接决定了模型能不能落地、往哪落地、怎么改才能落地。
很多人对参数量的理解停留在“模型文件大小除以四”这种粗糙换算上,或者干脆用现成工具跑一下打印个数字就完事。但真正做过模型压缩、剪枝、量化或者跨平台部署的人都知道,参数量这个指标背后牵扯的是内存占用、计算量估算、层间设计合理性判断,甚至是训练时梯度更新对显存的需求。你如果不知道每一层参数怎么来的,就没办法判断一个模型是不是“虚胖”,也没办法在精度和体积之间做有依据的取舍。
这篇内容适合几类人看:刚入门深度学习、对网络结构只有模糊概念的学生;需要把模型往边缘设备上搬、被内存和算力卡住的工程师;以及想自己设计轻量网络、但不确定每一层该给多少通道数的开发者。我会从最基础的参数定义讲起,把卷积层、全连接层、批归一化层、嵌入层这些常见结构的参数量公式一个一个拆开,然后讲清楚计算时容易踩的坑,最后给出一套可以手工验算的流程和几个实际案例。你不需要装任何框架,拿纸笔或者计算器就能跟着算。
2. 参数到底存在哪:逐层拆解参数量的来源
2.1 卷积层:权重加偏置,但偏置经常被忽略
卷积层的参数量公式看起来简单:参数量等于卷积核数量乘以每个卷积核的大小乘以输入通道数,再加上偏置项。用符号表示就是K × (C_in × H_k × W_k) + K,其中K是输出通道数,C_in是输入通道数,H_k和W_k是卷积核的高和宽。这里最容易出错的地方是偏置项。很多现代网络为了减少参数会关掉偏置,也就是设置bias=False,这时候参数量就只有权重部分。但如果你在手工计算时默认每个卷积层都有偏置,算出来的结果就会比实际多出K个参数。单个卷积层多算K个看起来不多,但一个几十层的网络累积下来,误差可能达到几千甚至上万。
举个例子,一个输入通道为64、输出通道为128、卷积核为3×3的卷积层,如果带偏置,参数量是128 × 64 × 3 × 3 + 128 = 73728 + 128 = 73856。如果关掉偏置,就是73728。差了一百多个参数,对于小模型来说这个比例不算小。另外要注意的是,卷积核的尺寸通常写成“高×宽”,但有些框架的权重张量排列顺序是(输出通道, 输入通道, 高, 宽),有些是(高, 宽, 输入通道, 输出通道),计算时只要抓住“每个输出通道对应一组输入通道的卷积核”这个本质,就不会被排列顺序绕晕。
还有一个特殊情况是深度可分离卷积。它把标准卷积拆成两步:先对每个输入通道单独做卷积,再用1×1卷积做通道融合。第一步的参数量是C_in × H_k × W_k,第二步是C_out × C_in × 1 × 1,加起来通常比标准卷积少很多。比如输入64通道、输出128通道、3×3核,标准卷积是73856,深度可分离是64×3×3 + 128×64 = 576 + 8192 = 8768,只有原来的八分之一左右。这也是为什么移动端网络大量使用这种结构。
2.2 全连接层:参数量的大头,也是内存的噩梦
全连接层的参数量公式是输入神经元数量乘以输出神经元数量,再加上输出神经元数量的偏置,即N_in × N_out + N_out。这个公式简单到不需要解释,但它的数值往往大得惊人。比如一个输入维度是512、输出维度是4096的全连接层,参数量是512 × 4096 + 4096 = 2097152 + 4096 = 2101248,超过两百万。如果输入维度是25088(比如7×7×512的特征图展平),输出是4096,参数量直接飙到一亿以上。这就是为什么早期的卷积网络在进入全连接层之前要反复做池化降采样,目的就是把特征图尺寸压到足够小,否则全连接层的参数会撑爆显存。
在实际项目中,我见过不少人为了省事,在卷积层后面直接接一个全局平均池化,然后接一个输出维度等于类别数的全连接层。这样做的好处是全连接层的参数量只有C × N_class + N_class,其中C是最后一个卷积层的通道数,通常几百到几千,N_class是类别数,通常几十到几百,总参数量可能只有几万。相比传统做法,参数量减少了几个数量级,而且精度损失往往很小。这也是现代网络设计的一个趋势:用全局池化替代大尺寸全连接。
2.3 批归一化层:每个通道两个参数,但别漏算
批归一化层在训练时维护均值和方差,但这两个是统计量,不是通过梯度更新的可学习参数。真正参与训练的参数是缩放因子gamma和偏移因子beta,每个通道各一个。所以一个批归一化层的参数量是2 × C,其中C是该层的通道数。这个数值通常很小,比如512通道的批归一化层只有1024个参数。但如果你在计算整个网络的参数量时完全忽略批归一化层,虽然绝对误差不大,但在做精细的模型对比时就会显得不够严谨。尤其是在比较两个只有批归一化层差异的网络时,这部分参数就不能忽略。
另外要注意的是,有些框架在推理阶段会把批归一化层的参数融合进前面的卷积层,这时候模型文件里就不再单独存储批归一化的参数了。但在训练阶段,这些参数是实实在在存在的,计算训练时的显存占用必须把它们算进去。
2.4 嵌入层与注意力层:容易被遗忘的参数角落
嵌入层常见于自然语言处理模型,它的参数量是词表大小乘以嵌入维度。比如词表有30000个词,嵌入维度是512,参数量就是30000 × 512 = 15360000,超过一千五百万。这个数值往往比模型里所有卷积层加起来还大。很多人在估算文本模型参数量时只算Transformer块里的矩阵,忘了嵌入层才是真正的大头。
注意力层里的参数主要来自查询、键、值的线性投影矩阵,以及最后的输出投影矩阵。假设隐藏维度是d,那么每个投影矩阵的参数量是d × d + d(带偏置),四个矩阵加起来大约是4 × d × d。如果d等于768,这部分参数量大约是4 × 768 × 768 = 2359296,两百多万。多头注意力并不会增加参数量,因为每个头的维度是d除以头数,所有头拼起来的总维度还是d,投影矩阵的尺寸不变。
3. 手工验算的完整流程:从输入尺寸到总参数量
3.1 先画一张层表,把每一层的输入输出通道数标清楚
手工计算参数量最怕的就是算到一半忘了某一层的输入通道数是多少。我的习惯是先在纸上画一张表,每一行代表一层,列包括层名称、输入通道、输出通道、卷积核大小、是否带偏置、参数量。对于全连接层,列就是输入维度、输出维度、是否带偏置、参数量。这张表不需要很精确,但通道数必须准确,因为卷积层的参数量对输入通道数非常敏感。
画表的时候有一个技巧:从输入层开始,逐层往后推,每一层的输出通道就是下一层的输入通道。如果遇到池化层或者步长大于1的卷积层,特征图的空间尺寸会变小,但通道数不变。全连接层之前通常有一个展平操作,展平后的维度等于最后一层特征图的高乘以宽乘以通道数。把这个维度记下来,作为全连接层的输入维度。
3.2 逐层套公式,注意偏置和批归一化的取舍
有了层表之后,就可以逐层套公式了。卷积层用K × C_in × H_k × W_k + K(带偏置)或K × C_in × H_k × W_k(不带偏置)。全连接层用N_in × N_out + N_out(带偏置)或N_in × N_out(不带偏置)。批归一化层用2 × C。嵌入层用V × D,其中V是词表大小,D是嵌入维度。
这里有一个容易混淆的地方:卷积层的偏置数量等于输出通道数,而不是输入通道数。因为每个输出通道对应一个偏置标量。全连接层的偏置数量等于输出维度。批归一化层的参数数量是通道数的两倍,因为每个通道有gamma和beta两个参数。
我一般会先用带偏置的公式算一遍,再用不带偏置的公式算一遍,看看差值有多大。如果差值占总参数量的比例很小,比如不到百分之一,那就可以忽略;如果比例较大,就需要确认实际模型到底有没有用偏置。很多现代卷积网络在卷积层后面接了批归一化层,这时候卷积层的偏置其实是多余的,因为批归一化会减去均值,偏置的作用会被抵消。所以这些网络通常会设置bias=False来省掉这部分参数。
3.3 用一个小型网络完整走一遍计算过程
假设我们有一个简单的卷积网络,结构如下:输入是3通道的32×32图像;第一层是卷积层,输出16通道,卷积核3×3,带偏置;第二层是批归一化,16通道;第三层是卷积层,输出32通道,卷积核3×3,不带偏置;第四层是批归一化,32通道;第五层是全连接层,输入维度是32×8×8(假设经过两次池化后特征图变成8×8),输出10个类别,带偏置。
逐层计算:第一层卷积参数量是16 × 3 × 3 × 3 + 16 = 432 + 16 = 448。第二层批归一化是2 × 16 = 32。第三层卷积是32 × 16 × 3 × 3 = 4608,不带偏置。第四层批归一化是2 × 32 = 64。第五层全连接是32×8×8 × 10 + 10 = 2048 × 10 + 10 = 20480 + 10 = 20490。总参数量是448 + 32 + 4608 + 64 + 20490 = 25642。这个数字和用框架打印出来的结果应该是一致的,如果差了几十个,大概率是偏置或者批归一化的取舍问题。
3.4 用表格对照不同层的参数占比,找出“参数大户”
算完总参数量之后,我建议再算一下每一层的参数占总参数的比例。这个比例能告诉你很多信息。如果全连接层的占比超过百分之八十,那这个模型的主要参数都集中在最后几层,剪枝的时候应该优先考虑全连接层。如果卷积层的占比很高,那就要看是不是某些层的通道数给得太大了。下面是一个示例表格,展示了一个小型网络中各层的参数占比。
| 层名称 | 参数量 | 占比 |
|---|---|---|
| 卷积层1 | 448 | 1.75% |
| 批归一化1 | 32 | 0.12% |
| 卷积层2 | 4608 | 17.97% |
| 批归一化2 | 64 | 0.25% |
| 全连接层 | 20490 | 79.91% |
| 总计 | 25642 | 100% |
从这个表可以清楚看到,全连接层占了将近百分之八十的参数。如果要把这个模型部署到内存受限的设备上,最有效的做法就是想办法减小全连接层的输入维度,比如增加池化层让特征图变得更小,或者用全局平均池化替代全连接层。
4. 那些年我踩过的参数量计算坑
4.1 把特征图尺寸当成参数量的一部分
这是新手最容易犯的错误。参数量只和层的权重、偏置有关,和输入图像的大小、特征图的空间尺寸没有直接关系。卷积层的参数量只取决于卷积核大小、输入通道数、输出通道数,跟你输入的是32×32的图还是224×224的图完全无关。输入尺寸影响的是计算量(浮点运算次数)和中间激活值的内存占用,但不影响参数量。我见过有人在计算参数量时把特征图的宽高也乘进去,结果算出来的数字大得离谱。
4.2 忽略批归一化层的参数,或者把它算成四个
批归一化层在训练时有四个张量:均值、方差、gamma、beta。但均值和方差是根据当前批次数据统计出来的,不是通过反向传播学习到的参数。真正可学习的只有gamma和beta。所以参数量是2 × C,不是4 × C。有些框架在保存模型时会把均值和方差也存下来,导致模型文件变大,但这些不算参数量。如果你在做模型大小估算,可以把均值和方差算进去;如果是在做参数量对比,就应该只算可学习参数。
4.3 全连接层的输入维度算错,导致参数量差一个数量级
全连接层的输入维度等于展平后的特征图大小,也就是高 × 宽 × 通道数。这个数值很容易算错,尤其是当网络里有多个池化层或者步长卷积时,特征图尺寸的变化需要一步一步推导。我一般会用一个简单的公式来跟踪特征图尺寸:对于卷积层,输出尺寸等于(输入尺寸 - 卷积核大小 + 2×填充) / 步长 + 1;对于池化层,输出尺寸等于(输入尺寸 - 池化核大小) / 步长 + 1。把每一层的输出尺寸记下来,到最后展平时就不会搞错。
4.4 混淆参数量和计算量,导致优化方向跑偏
参数量和计算量是两个不同的概念。参数量决定模型文件大小和内存占用,计算量决定推理速度和能耗。一个模型可能参数量很小但计算量很大,比如深度可分离卷积;也可能参数量很大但计算量很小,比如大嵌入层的文本模型。如果你要优化模型大小,关注参数量;如果你要优化推理速度,关注计算量。把这两个搞混,可能会在剪枝时剪掉计算量大的层,结果参数量没降多少,速度也没提升。
5. 从参数量反推网络设计:几个实用的判断准则
5.1 卷积层通道数翻倍时,参数量翻四倍
这个规律在标准卷积中成立:如果输入通道数和输出通道数同时翻倍,卷积核大小不变,那么参数量变成原来的四倍。因为参数量正比于C_in × C_out。这个规律在手工估算时非常有用。比如你知道某个卷积层的参数量是十万,现在想把输入输出通道都扩大一倍,那参数量就会变成四十万。如果你没有足够的显存或者内存来承载这个增长,就需要重新考虑通道数的分配。
5.2 全连接层参数量正比于输入维度和输出维度的乘积
全连接层的参数量对输入维度非常敏感。如果输入维度从512增加到1024,输出维度不变,参数量就翻倍。如果输入维度从512增加到2048,参数量就变成四倍。所以在设计网络时,控制全连接层的输入维度是控制总参数量的关键。全局平均池化之所以有效,就是因为它把输入维度从H × W × C降到了C,通常能减少几十倍甚至上百倍的参数量。
5.3 嵌入层参数量正比于词表大小,压缩词表比压缩模型更有效
对于文本模型,嵌入层的参数量往往占总参数量的很大一部分。如果词表有三万词,嵌入维度五百,嵌入层就有一千五百万参数。如果把词表压缩到一万词,嵌入层参数量就降到五百万,直接少了三分之二。所以在做文本模型轻量化时,压缩词表是一个被低估的手段。当然,压缩词表可能会影响模型对罕见词的处理能力,需要根据具体任务权衡。
5.4 批归一化层参数量虽小,但层数多了也不可忽略
单个批归一化层的参数量是2 × C,看起来微不足道。但如果网络有上百层,每层都有批归一化,通道数平均五百,那么批归一化层的总参数量就是100 × 2 × 500 = 100000,十万个参数。虽然和全连接层比起来不算多,但在做精细的参数量对比时,这部分不能完全无视。尤其是在比较两个只有批归一化层差异的网络时,这部分参数就是差异的来源。
6. 手工计算之外:什么时候该用工具,什么时候该自己算
6.1 工具能帮你省时间,但不能帮你做决策
现在有很多工具可以一键打印模型的参数量,比如深度学习框架自带的模型摘要函数,或者一些专门的模型分析库。这些工具在验证手工计算结果时非常有用,但它们不能替代你对参数量来源的理解。工具只会告诉你总数和每一层的数字,不会告诉你为什么这一层参数这么多,也不会告诉你如果要减少参数量应该改哪里。手工计算的价值在于建立直觉,让你在看到网络结构时就能大致判断出参数量在什么量级,哪些层是参数大户。
6.2 手工计算和工具验证的配合方式
我的习惯是:先手工算一遍,把每一层的参数量列出来,然后写几行代码用框架加载模型,打印出每一层的参数量和总参数量,和手工结果对照。如果完全一致,说明我对网络结构的理解是正确的;如果有差异,就逐层排查,看看是偏置设置不同、批归一化层处理方式不同,还是某一层的输入维度算错了。这个过程本身就是在加深对网络结构的理解。几次下来之后,你对手工计算的准确度会越来越有信心,速度也会越来越快。
6.3 什么时候可以跳过手工计算
如果你只是想知道一个现成模型的大致参数量,比如从模型库下载一个预训练网络,那直接看文档或者用工具打印一下就行,没必要手工算。但如果你要修改网络结构、设计新层、做模型压缩,或者需要在没有工具的环境下快速估算,手工计算就是必备技能。另外,在面试或者技术讨论中,能够快速估算出一个网络结构的参数量,也是一个很能体现基本功的能力。
7. 参数量之外:那些同样影响模型落地的指标
参数量只是模型属性的一个维度。在实际项目中,我还会同时关注另外几个指标:浮点运算次数决定推理速度,中间激活值的内存占用决定推理时的峰值内存,模型文件大小决定存储需求。这四个指标一起看,才能对模型的落地难度有一个完整的判断。有时候一个模型参数量很小,但中间激活值很大,推理时内存峰值很高;有时候参数量很大,但计算量很小,推理速度反而很快。所以不要只盯着参数量一个数字,要结合具体部署环境综合判断。
另外,参数量在训练阶段和推理阶段的意义也不同。训练时参数量影响梯度存储和优化器状态的内存占用,通常是参数量的三到四倍。推理时参数量主要影响模型加载的内存占用。如果你在做训练相关的优化,比如分布式训练或者梯度累积,那参数量乘以几倍才是你真正需要关心的内存数字。
我在实际项目中的体会是,参数量计算这个技能,学起来只要半个小时,但真正用熟、用出感觉,需要你在每一个模型上都动手算一遍,和工具结果对照,慢慢建立起对数字的敏感度。等到你看到一个网络结构就能大致说出参数量在百万级还是千万级,并且能指出哪一层是参数大户的时候,这个技能才算真正长在你身上了。