端侧模型部署的工程师大概都有同感模型体积可以通过剪枝量化压下来但真正跑起来以后功耗和散热还是经常兜不住。深度学习里的卷积、全连接、注意力机制底层都是海量的乘法和累加而乘法器在硬件世界里是个“吃大户”的角色——同样的一个算术动作它消耗的晶体管、动态功耗和面积比加法器高出太多。AdderNet 这篇 Paper Reading 的主角直接问了一个特别基础的问题深度学习里我们真的需要那么多乘法吗论文的核心行动也相当干脆——把卷积里的内积换成加法距离用绝对差求和得到特征响应让网络在保持精度的同时大幅降低硬件实现成本。这篇文章不打算只复述公式我会把原理动机、训练难点、复现笔记和硬件适配四个层面完整拆一遍适合做模型轻量化和端侧 AI 的工程师也适合刚准备接触神经网络硬件加速的同学。1. 卷积里的乘法为什么“贵”1.1 乘法在硬件里不是简单的“多次加法”先从最基础的数学动作说起。一个卷积核在某个位置的计算可以写成[ y \sum_{i} x_i \times w_i ]看起来就是几个乘法再加起来。但在硬件里“乘”这件事比“加”复杂得多。以最常见的二进制补码乘法为例一个 n 位乘法器需要先产生一整组部分积然后通过多级加法树把部分积压缩成最终结果。关键路径长、寄存器多、布线密面积和同宽度的加法器相比通常要高出 5 到 7 倍。动态功耗也是一个量级的差距这在边缘端芯片里是很大的负担。有人会想CPU 和 GPU 不是有专门的乘加指令吗是的但这些指令本质上是把大量的硬件资源集中到“乘法”上。对 PC 和服务器来说这是划算的。可是对手机、传感器、MCU、FPGA 上自研的小型加速核来说乘法器越少越好最好的情况是一块晶圆上压根没有乘法单元。AdderNet 的思路就是把网络核心路径上的乘法彻底拿掉用“加、减、取绝对值、累加”这些轻量操作来完成同样的特征提取。1.2 内积和绝对差不只是公式长得不一样卷积神经网络里大量使用内积并不仅仅因为内积算起来方便更深层的原因是内积本身是一种相似度度量。两个向量方向一致时内积大方向相反时内积小甚至可以为负。这让卷积天然有了“检测特征是否存在”的能力。AdderNet 的替换思路是既然目的是度量输入 patch 和卷积核之间的匹配程度那我用更便宜的绝对差度量行不行具体形式可以写成[ Y(m, n, t) -\sum_{c,i,j} \left| X(mi, nj, c) - F(i,j,c,t) \right| ]这个形式和内积的最大区别是内积允许正负抵消而绝对差是单调的“距离”度量。特征值和核对上了距离小对不上距离大。外面加一个负号是为了让匹配度高的输出值更大从而能够自然接入后面的 BatchNorm 和 ReLU 这些我们熟悉的操作。从硬件角度看绝对差只需要一个减法器、一个取绝对值电路和一个累加器完全不需要乘法器。从数学角度看这个度量表达的是“特征模板匹配”和传统卷积里的线性投影比起来它的表达能力并不天然更差只是需要不同的训练策略去适配。1.3 为什么不能直接沿用卷积的成熟公式一个很自然的疑问是把内积换成 L1 距离之后网络结构和计算流程几乎没有变那这套东西不是早该有人想到了吗实际上理论上的“想到”和工程上的“能训练”之间有巨大的鸿沟。内积对权重的要求是“各个通道的贡献可以相互抵消”所以权重的符号和幅度都很重要。绝对差的贡献永远是非负的梯度行为和数值范围也完全不同。如果直接把一个已经训练好的卷积网络里的乘法换成绝对值差不重新训练输出分布立刻崩塌。论文真正有贡献的部分不只是提出这个度量而是针对它重新设计了整个训练体系包括梯度、BatchNorm、学习率和权重衰减。后面这一整套东西才是 AdderNet 能work的核心原因。2. 加法卷积的核心设计2.1 加法层的基本计算形式AdderNet 在网络里替代普通卷积的层称为“加法层”。它的计算过程和卷积类似都是滑窗扫描输入特征图但窗口内的聚合方式从“乘加”变成了“绝对差求和”。定义输入特征为 ( X )形状是 ( N \times C_{in} \times H \times W )卷积核权重为 ( F )形状是 ( C_{out} \times C_{in} \times k \times k )。对某个输出位置的响应可以用上面的公式统一描述。整个网络依然有 stride、padding、kernel size 这些参数输出尺寸的计算方法也完全一致。换句话说网络拓扑结构可以原封不动只需要把卷积算子替换成“加法算子”。这里有个容易混淆的点论文说的“加法网络”并不是像某些轻量网络那样“减少乘法次数”或者“共享乘法结果”而是真正意义上把乘号从算子核心路径上去掉。输入和权重之间不再做乘积只做差值运算。2.2 为什么用 L1 距离而不是欧氏距离 L2既然要度量距离那常见的选择有三个L1、L2、还有 smooth L1。论文选择 L1至少有两个层面的考虑。第一是硬件成本。L2 距离要求先做平方再开方平方在硬件里天然对应乘法器开方更是复杂电路。如果目的是“不要乘法”用 L2 就把自己绕回去了。L1 只需要绝对值和累加加法和移位就能解决。第二是数值特性。L2 距离对离群值非常敏感一个像素出现大的偏差梯度会被平方放大。神经网络中间层的特征分布本来就很宽用 L2 会导致梯度的尺度波动非常剧烈训练稳定性更差。L1 的梯度幅度是常数配合专门设计的训练策略更容易收敛。论文在实验里也对比了不同距离函数L1 在精度和收敛速度上综合最优。2.3 BatchNorm 是加法层的“稳定器”绝对差求和的输出范围不固定。它既受卷积核数量影响也受输入特征图尺寸影响。如果不像传统卷积那样有“线性投影”来自然控制尺度加法层的输出方差会随网络深度一路漂移。BatchNorm 在这里起的作用就不只是常规意义上的加速收敛了它是加法层能正常训练的基石。论文中的加法层之后基本都接 BN利用 BN 的可学习 scale 和 shift 把输出分布重新拉回一个合理区间。我在复现时还注意到一个细节BN 的 eps 和 momentum 对最终的精度影响很大。加法层的输出数值通常比传统卷积层小一个量级如果 eps 设置得太小归一化过程会出现数值不稳。实践时把 BN 的动量保持默认或稍微调大一点都能减少训练初期 loss 的异常抖动。3. 训练难点与梯度策略3.1 绝对值函数在反向传播里天生麻烦如果前向传播是绝对差求和那反向传播时就会遇到 ( f(x)|x| ) 在 0 点不可导的问题。更麻烦的是( |x| ) 的导数在大多数区域是 1 或 -1这种常数梯度很容易让底层参数以一种很“粗暴”的方式震荡。传统卷积的梯度来自权重和输入之间的线性关系信号可以很平滑地传播。而 L1 距离的梯度几乎是一个符号函数不带上限不做处理训练几乎不可能稳定。这也是为什么“把卷积替换成加法”听起来简单真的去复现时却一批人翻车的主要原因。3.2 论文对梯度的“重新定义”论文没有简单地把绝对值梯度当成符号函数丢进去而是重新设计了一套适合加法层的反向传播规则。对于输入特征的梯度使用 ( sign(x - F) ) 的形式但对梯度幅度做限制避免单个离群点把特征更新方向彻底带偏。对于权重的梯度则采用类似的符号累加策略。这种策略虽然从严格数学意义上不是“真实梯度”但它保证了两个核心目标更新方向正确、数值范围可控。我在看官方开源实现时发现梯度在进入更底层之前通常还会经过额外的缩放。这个缩放系数在不同任务里不一定相同它和 batch size、特征图大小、通道数都有关系。训练时最好监控每个加法层权重的梯度范数如果发现某一层特别大或特别小就单独做处理。3.3 学习率、权重衰减和 BN 的联动调参AdderNet 的训练策略和传统 CNN 有明显差异。最大的坑是直接把 CNN 训练好的学习率套到加法网络上基本不会收敛。原因在于加法层的梯度幅值天然小于传统卷积层。传统卷积里权重和输入相乘梯度的幅度受到输入特征值尺度影响而加法层里梯度主要是符号函数幅度恒定且偏小。解决思路是给加法层单独放大学习率。我实际测试下来的经验是将 adder 层的学习率设为普通层的 3 到 5 倍同时把学习率衰减的节奏拉长收敛速度和精度都有明显改善。另外一个经验是权重衰减。传统 ResNet 训练时weight decay 能有效抑制过拟合但在 AdderNet 里过强的权重衰减会把网络里的权重直接推向 0导致绝对差趋于恒定特征提取能力退化。复现时我通常把加法层的 weight decay 降到原来的十分之一左右效果明显更稳定。4. 实验复盘与适用边界4.1 精度和能耗的实验结论论文在多个主流视觉任务上做了对比。CIFAR 和 ImageNet 这类图像分类任务上AdderNet 的精度和同结构的 CNN 基本持平差距通常在二十分之一个百分点以内有些配置下甚至能超过基线。在目标检测这类更复杂的任务上加法网络也能取得接近正常卷积的效果但对训练超参数更敏感需要多花精力调。能耗方面的提升来自硬件层面的估算。同一工艺下乘法器功耗约为加法器的数倍如果一个卷积层完全由加法器构成乘累加单元的能耗可以下降一个数量级。这里需要强调这是“纯算数单元”的对比不包含存储器和数据搬运开销。对中小尺寸模型来说计算单元的能耗占比可观AdderNet 的收益才明显。4.2 为什么在 GPU 上反而可能变慢这是很多读者第一次复现时最困惑的地方为什么论文说省电可我在 GPU 上测试速度没有提升原因很简单GPU 是专门为稠密矩阵乘法设计的芯片它的 Tensor Core、SIMD 单元都把乘加融合到了极致。传统卷积在 GPU 上是一条高效的流水线而加法距离计算需要对每个窗口做绝对差和求和这反而增加了指令数和内存访问量。GPU 上的“乘法便宜”AdderNet 的优势发挥不出来。AdderNet 的真正主战场是 FPGA 和 ASIC。在这些平台上工程师可以按需设计数据路径砍掉全部乘法器用加法树和比较器搭出更紧凑的算子。设备特性决定收益这个点必须分清楚。硬件平台乘法器成本加法网络的收益GPU乘法器资源丰富不明显甚至更慢CPU有专门乘加指令但功耗较高有一定收益不如专用硬件明显FPGA乘法器数量受限收益明显节省 DSP 资源ASIC面积功耗敏感收益最大可实现无乘法计算核4.3 适合的应用场景和还存在的问题目前比较适合落地 AdderNet 的场景有三类极低功耗的端侧推理芯片、资源受限的 FPGA 实时处理系统、以及对传感器硬件成本敏感的物联网设备。这些场景共同的特点是乘法器资源宝贵加法器相对充裕。它还没完全解决的问题也很明确一是训练难度比普通卷积高对工程师经验要求更多二是超大规模模型、超大图像输入上的收敛稳定性还有提升空间三是如果和 INT8 量化结合数值分布需要更多处理不能直接沿用普通卷积的量化参数。这些也都是后来各种改进版加法网络继续在啃的硬骨头。5. 复现笔记与关键代码5.1 一个最小可跑的简化实现下面给出一个用 PyTorch 写的简化加法卷积层。它的前向逻辑可以用官方公式逐行对应方便理解不适合直接用于大规模训练因为 unfolded 方式非常吃内存。要复现论文结果建议参考论文提供的官方实现并对比梯度处理细节。import torch import torch.nn as nn import torch.nn.functional as F def adder_forward(x, weight, stride1, padding0): x: [N, C_in, H, W] weight: [C_out, C_in, kh, kw] 返回: [N, C_out, H_out, W_out] batch, c_in, h_in, w_in x.shape c_out, c_in_k, kh, kw weight.shape assert c_in c_in_k h_out (h_in 2 * padding - kh) // stride 1 w_out (w_in 2 * padding - kw) // stride 1 x_pad F.pad(x, (padding, padding, padding, padding)) # [N, C_in*kh*kw, L], L 为滑窗位置总数 x_unfold F.unfold(x_pad, kernel_size(kh, kw), stridestride) # 权重平面化: [C_out, C_in*kh*kw] w_flat weight.view(c_out, -1) # 对每个窗口计算与每个输出卷积核的绝对差之和 # 广播后 shape: [N, C_out, L] dist (x_unfold.unsqueeze(1) - w_flat.unsqueeze(0)).abs().sum(dim2) return -dist.view(batch, c_out, h_out, w_out) class AdderConv2d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride1, padding0, biasFalse): super().__init__() self.stride stride self.padding padding self.weight nn.Parameter( torch.randn(out_channels, in_channels, kernel_size, kernel_size) * 0.05 ) if bias: self.bias nn.Parameter(torch.zeros(out_channels)) else: self.bias None def forward(self, x): out adder_forward(x, self.weight, self.stride, self.padding) if self.bias is not None: out out self.bias.view(1, -1, 1, 1) return out这段代码有几处需要特别说明。第一torch.nn.functional.unfold会先把滑窗复制成展开矩阵内存占用随 kernel size 指数增长所以这个函数只适合在小特征图上验证。第二这里直接用.abs()PyTorch 自动求导在 0 点给出的梯度是 0而论文里的梯度近似是 sign 函数两者有差异。实际训练时如果要完整复现需要自定义torch.autograd.Function在 backward 中实现对梯度的重定义和缩放。5.2 复现时我踩过的几个坑第一坑是 BatchNorm 的位置。如果把 BN 放在加法层之前训练 loss 会掉得非常慢。正确的顺序是“加法层 → BN → 激活函数”和常规卷积块的顺序保持一致。第二坑是学习率调度。我一开始沿用普通 ResNet 的 CosineAnnealing结果前期 loss 下降正常后期精度无论如何上不去。后来单独放大加法层的学习率并让权重衰减绕开 adder 参数问题才解决。第三坑是初始化。直接用常见的 Kaiming 初始化在一部分任务上是 OK 的但如果输入特征很稀疏加法层很容易在训练初期产生大片死区。把初始化尺度适当调小配合 BN 使用能减少这种现象。第四坑是 batch size 的影响。加法的梯度本质是符号函数累加batch size 越大梯度方向越稳定但精度不一定更高。我在 batch size 从 128 调到 256 时精度反而掉了后来发现需要同步调整 BN 的 momentum。5.3 官方代码和论文之间的差异读论文时容易忽略的细节在代码里都会暴露出来。官方实现里除了梯度重定义还有通道维度的缩放处理这是论文正文里只给了公式、没有展开讲的工程细节。这个缩放处理的作用是当输入通道数很多时绝对差累加会让梯度在通道维上被放大很多倍如果不加缩放深层网络会出现梯度爆炸。复现时不要只抄公式一定要对照官方开源实现把 backward 部分的数值范围检查一遍。6. 常见问题速查表与个人心得6.1 常见问题速查表问题现象可能原因处理建议loss 长时间不下降未接 BN或 BN 位于加法层之前调整网络块顺序加法层 → BN → 激活函数训练初期 loss 剧烈震荡学习率过大或未做梯度缩放降低基础学习率单独放大 adder 层权重学习率精度后期上不去weight decay 把权重推向 0单独减小 adder 层的 weight decayGPU 推理速度不升反降平台不适合加法网络明确目标平台在 FPGA/ASIC 场景下评估收益量化后精度崩盘加法层输出分布和卷积层差异大为 adder 层单独标定量化参数或改用更低比特的专门算法这个表基本覆盖了我在复现过程中遇到的主要问题。如果你用的是完全不同的任务比如语音或推荐系统现象可能不一样但排查方向可以参考梯度、BN、学习率、硬件匹配这四个维度按顺序查下来大概率能找到原因。6.2 最后想说的几句实在话我自己第一次读完 AdderNet 时最强烈的感受是这篇论文不是在讲一个“新网络结构”而是在讲一种“硬件和算法协同设计的思路”。它提醒我们深度学习里的很多默认选择并不是数学上唯一正确的选择只是因为硬件生态让它们显得理所当然。如果你所在的团队正在做端侧推理芯片或者你的 FPGA 项目正被 DSP 资源掐着脖子AdderNet 值得认真去复现一遍。而如果你只是想在 GPU 上加速现有模型那这个方向可能并不适合你——这也是我在实际操作中最想提醒别人的一点。读完论文之后花时间搞清楚自己的瓶颈到底在算法层还是硬件层比急着改造模型重要得多。