图像融合这个任务,过去几年的主流做法一直是"一次性前向、一次性出图":两个模态各走一个编码器,中间拼一拼、加一加、注意力调一调,解码器吐一张融合图完事。所以当我第一次看到脉冲神经网络(SNN)被搬来做图像融合,并且整套跨模态电流要在T 个时间步里一拍一拍地积分、发放、复位,我的第一反应是:这不就是把简单问题复杂化吗?但把这套被命名为 CIS-Fuse 的方案拆开看之后,我发现它其实回答了一个被传统融合网络长期忽略的问题——融合的本质不是"把两张图叠起来",而是"在有限的计算预算下,决定哪一部分信息值得被传递下去"。脉冲发放恰好就是一种天然的"值得/不值得"二值决策,而 T 步累积则给了这个决策一个可以反复修正的过程。
这篇文章不打算复述任何结论性的指标表格,而是从我自己的复现经验出发,把这套东西的技术骨架拆开:跨模态电流到底注入在哪一步、T 步展开在代码里长什么样、替代梯度怎么接、训练时哪些参数一改就崩。适合已经写过基本融合网络、想搞清楚 SNN 那套时序逻辑怎么落到图像任务上的读者;如果你连 LIF 是什么都还没接触过,我也会在必要的地方把基础补上,不至于掉队。
1. 为什么要把图像融合塞进脉冲神经网络的 T 个时间步里
先说清楚一件事:把融合网络改成脉冲形式,不是为了追新。传统融合网络在推理阶段有一个很尴尬的特性——无论输入图像是纹理密集的复杂场景,还是大片平坦天空,它的计算量是一模一样的。卷积核该乘多少次还是多少次,激活值该是多少还是多少。而 SNN 的激活是二值的 0/1 脉冲,理论上前者可以跳过大量"没必要算"的位置,这个特性在融合任务上其实有实际价值,因为融合图的大部分区域本来就只需要照抄某一个模态。
1.1 一次性前向与 T 拍累积:两种范式的账本差异
传统融合网络的账本是"深度换精度":加深网络、加宽通道、加注意力模块,参数量和 FLOPs 线性甚至超线性上涨。SNN 的账本逻辑不太一样,它多了一个时间维度的杠杆:你可以不加宽、不加深,只把 T 从 4 提到 8,让同一个卷积层在时间上复用 8 次,用时间换精度。这个杠杆的好处是参数不涨、模型体积不涨,坏处是延迟会涨、反向传播的显存会涨。
我给这两条路做了个粗略对照,方便你判断自己该走哪条:
| 维度 | 传统 CNN 融合 | SNN 时序融合(T 步) |
|---|---|---|
| 参数量增长方式 | 加宽/加深,直接涨 | 基本不变,靠 T 调节 |
| 单次推理延迟 | 与深度强相关 | 与 T 线性相关 |
| 反向传播显存 | 与 batch×通道×分辨率相关 | 额外乘一个 T 的系数 |
| 激活值形态 | 连续浮点 | 0/1 二值脉冲 + 浮点膜电位 |
| 精度提升手段 | 换结构、加损失 | 调 T、调阈值、调电流配比 |
看这张表就能明白,SNN 融合的调参空间和 CNN 完全不是一回事。你在 CNN 上积累的"加个注意力就涨点"的直觉,到了这里大概率失效——因为真正的瓶颈往往在膜电位饱和、首步全零、放电率塌缩这些时序层面的问题上。
1.2 稀疏放电在融合任务上到底省在哪
很多人对 SNN 节能的理解停留在"脉冲是二值的,所以乘加可以用加法代替"。这个说法在硬件层面成立,但在软件仿真层面意义有限,因为你在 GPU 上跑的还是浮点矩阵乘,二值化只是把激活值量化了,并不会真的省 FLOPs。真正省的地方在于稀疏性:如果某一层在某个时间步的发放率只有 15%,那这一拍的乘加实际只有 15% 的位置是有效的。
融合任务恰恰很容易产生这种稀疏。举个具体场景:红外与可见光融合里,红外模态提供的是热目标(人、车、发热设备),这些区域通常只占画面的很小一部分;可见光模态提供的是纹理和边缘,平坦区域(墙面、天空、路面)的梯度接近零。一个训练良好的脉冲编码器在这两类区域上的发放率都会很低,只有在目标边缘和纹理交界处才会密集放电。这个特性如果能在硬件上兑现,收益是实打实的;即使在 GPU 上,稀疏张量也可能带来实际的加速。
注意:稀疏性不是免费的。如果你在训练时用了 L1 稀疏正则去压发放率,压过头会导致融合图大面积失真,尤其是暗部细节会直接消失。这个度我后面会具体讲。
1.3 CIS-Fuse 里三个关键词的各自含义
这套方案的名字里有三个词,每个词都对应一个具体的技术决策,不是随便起的。
"CIS"对应的是跨模态的电流注入机制(Cross-modal Current Injection 这一类思路)。传统融合网络在特征层面做融合——把两个模态的特征图 concat、add 或者加权求和。而这里是把另一个模态的特征转成输入电流,直接注入到本模态脉冲神经元的充电方程里。差别在哪?特征级融合是在"神经元已经算出结果之后"才合并,而电流级融合是在"神经元做决策之前"就把另一个模态的信息掺进去了,后者对脉冲发放时机的影响更直接。
"Fuse"就是融合本身,但这里的融合发生在时间维度上:每个时间步都产生一对脉冲图,最终用 T 步的累积发放率作为融合特征。这和单步的加权融合在数值行为上差别很大——单步融合是确定性的线性组合,T 步累积则带有一种"投票"的性质,偶尔某一拍发错脉冲,会被其他拍的多数结果稀释掉。
"T 步放电"是最容易被低估的部分。T 不是一个超参数那么简单,它决定了膜电位能积分到多深、梯度能传多远、显存能扛多大。T 太小信息不足,T 太大梯度消失加显存爆炸,而且收益是递减的。
2. 跨模态电流注入:把另一个模态写进膜电位方程
要理解电流注入,得先老老实实把 LIF 的方程写一遍。我知道很多人看到微分方程就想跳过,但这里跳不过去,因为"注入在哪"这个问题的答案,完全藏在方程的项里。
2.1 LIF 的三个动作:充电、发放、复位
一个标准的漏积分发放神经元,每个时间步干三件事:
充电:膜电位接收输入电流,同时按时间常数泄漏一部分。离散化之后写成
H[t] = V[t-1] + (X[t] - (V[t-1] - V_reset)) / tau其中X[t]是这一拍的输入电流,tau是膜时间常数。tau越大,泄漏越慢,膜电位越像"积分器";tau越小,泄漏越快,神经元越像"边缘检测器",只对瞬时变化响应。这一点在融合任务里很关键——后面会讲。
发放:膜电位超过阈值V_th就发一个脉冲,
S[t] = 1 if H[t] >= V_th else 0复位:发完脉冲之后膜电位要处理一下,有硬复位(直接归零)和软复位(减去阈值)两种,这个选择对训练稳定性影响极大,第 4 节会专门讲。
关键点在于:X[t]是唯一一个可以合法"塞东西"的入口。你可以改tau、改V_th、改复位方式,但那些都是全局的、与输入无关的调整。只有X[t]是每一拍、每个位置都可以不同的。所谓跨模态电流注入,本质上就是把另一个模态的特征投影成X[t]的一部分。
2.2 三种"插电"位置:输入端、膜电位端、门控端
从公开的思路和常见的实现方式归纳,跨模态的信息往 LIF 里插,大致有三种接法,各有优劣。
第一种是输入端直加:X[t] = I_self + I_cross,两个模态各自投影出一份电流,直接相加。这是最简单的,代码三行就能写完。缺点是两个模态的电流尺度如果不匹配,弱的那个会被强的淹没。红外目标的响应强度通常远高于可见光纹理,直加的结果往往是融合图里红外目标很亮、纹理全糊。
第二种是膜电位端耦合:不改输入电流,而是让 A 模态的膜电位去影响 B 模态的充电过程。比如H_B[t] = V_B[t-1] + X_B[t] + gamma * tanh(V_A[t-1])。这种做法保留了各模态自身的积分动态,耦合发生在状态层面。实测下来这种方式对"模态间强弱失衡"的鲁棒性更好,但训练初期容易不稳定,因为两个模态的膜电位是互相喂养的,容易出现正反馈。
第三种是门控注入:先算一个逐位置的门控系数g = sigmoid(conv(cat(F_A, F_B))),然后X_A = I_A + g * I_B,X_B = I_B + (1-g) * I_A。门控的好处是它可以根据局部内容动态决定"这块区域该听谁的"——目标区域g接近 1,让红外的电流主导;纹理区域g接近 0,让可见光主导。这其实就是把注意力机制搬到了电流层,只不过作用对象是膜电位而不是特征图。
我在复现时对比过这三种接法,结论是:门控注入在多数场景下最稳,输入端直加最容易训崩,膜电位耦合在 T 较大时反而有优势。原因推测是门控本身带了一层非线性,相当于给电流加了个软约束,不至于让某一模态的极端值直接冲垮膜电位。
2.3 我实测下来最稳的一种接法
把门控注入具体化,我用的结构是这样:
import torch import torch.nn as nn class CrossModalCurrent(nn.Module): def __init__(self, c): super().__init__() self.proj_a = nn.Conv2d(c, c, 1, bias=False) self.proj_b = nn.Conv2d(c, c, 1, bias=False) # 门控由两个模态的原始特征共同决定 self.gate = nn.Sequential( nn.Conv2d(2 * c, c, 1, bias=False), nn.Sigmoid() ) # 电流增益,防止初始电流过大直接击穿阈值 self.gain = nn.Parameter(torch.tensor(0.5)) def forward(self, fa, fb): ia = self.proj_a(fa) ib = self.proj_b(fb) g = self.gate(torch.cat([fa, fb], dim=1)) cur_a = ia + self.gain * g * ib cur_b = ib + self.gain * (1.0 - g) * ia return cur_a, cur_b几个设计细节值得说一下。第一,proj_a和proj_b用 1×1 卷积不用 3×3,是因为这里只是做通道映射、把特征转成电流,空间混合交给后面的脉冲卷积层去做,电流层本身不应该引入额外的空间感受野,否则两个模态的电流在空间上就错位了。第二,gate的输入用的是融合前的原始特征而不是投影后的电流,这样门控能看到更完整的模态信息。第三,gain是一个可学习标量,初值给 0.5 而不是 1.0,是因为训练初期电流总和容易过大,导致首步膜电位直接越过阈值、所有位置同时放电,整个网络变成常数输出。
提示:
gain这个参数看着不起眼,但它是我调参时改动最频繁的一个。如果发现训练前几十个 step 里 loss 几乎不动、输出图是纯灰,八成是电流太大把神经元全部打饱和了,先把gain初值降到 0.1 试试。
3. T 步展开的工程实现:从手写神经元到反向传播
这一节是最实操的部分。现成的脉冲算子库当然能用,但我建议至少手写一遍,因为这套方案里"电流注入"是自定义逻辑,你必须清楚每一步张量的形状和含义,才能在出问题时定位到底。
3.1 一个能跑通的最小 LIF 单元
先写替代梯度的部分,这是整个 SNN 训练的地基。
import torch class SurrogateSpike(torch.autograd.Function): @staticmethod def forward(ctx, x, alpha=4.0): ctx.save_for_backward(x) ctx.alpha = alpha return (x > 0).float() # 前向:硬阶跃 @staticmethod def backward(ctx, grad_output): (x,) = ctx.saved_tensors alpha = ctx.alpha s = torch.sigmoid(alpha * x) # 反向:sigmoid 的导数做替代 return grad_output * alpha * s * (1 - s), None spike = SurrogateSpike.apply然后是 LIF 单元本身:
class LIFCell(nn.Module): def __init__(self, tau=2.0, v_th=1.0, v_reset=0.0, alpha=4.0): super().__init__() self.tau, self.v_th, self.v_reset = tau, v_th, v_reset self.alpha = alpha def forward(self, current, v): # 1) 充电 h = v + (current - (v - self.v_reset)) / self.tau # 2) 发放 s = spike(h - self.v_th, self.alpha) # 3) 软复位:保留超出阈值的部分 v_next = h - s * self.v_th return s, v_next这段代码很短,但每个符号都有讲究。h - self.v_th把阈值挪进了替代函数的自变量里,这样替代梯度在h = v_th附近最陡,符合"阈值附近最需要精细梯度"的直觉。软复位写成h - s * v_th,意思是发放之后只扣掉一个阈值的量,剩下的继续留在膜电位上。这个写法在融合任务里比硬复位稳定得多,后面第 4.2 节会展开。
3.2 替代梯度:阶跃函数不能直接求导的那道坎
脉冲发放是一个阶跃函数,S = Θ(H - V_th)。这个函数的导数在除H = V_th之外处处为零,在H = V_th处不存在。如果你直接把它塞进反向传播,梯度会全部变成零,整个网络学不到任何东西。
替代梯度的思路是:前向用真的阶跃,反向用一个假的、光滑的、形状接近阶跃的函数的导数。上面用的 sigmoid 是常见选择之一,另一种常见的是
grad = 1 / (1 + (pi * alpha * x) ** 2)这两种我都试过。sigmoid 版本在alpha较小时梯度比较平缓,训练稳定但收敛慢;那个 arctan 版本在零点附近梯度更尖锐,收敛快但对初始化更敏感。alpha这个超参实际上控制的是"梯度的作用窗口宽度"——alpha越大,窗口越窄,梯度越接近真实的阶跃(也就越容易梯度消失);alpha越小,窗口越宽,相当于每个神经元都在做很模糊的学习。
我自己的经验是:融合任务上alpha取 2 到 4 之间比较合适,别用默认的 1.0,也别超过 8。取 1.0 时融合图的边缘会很软,因为替代梯度窗口太宽,模型学不到锐利的边缘特征;取 8 以上时训练在几十个 epoch 之后会突然崩掉,原因是大部分神经元的膜电位离阈值都很远,落进了梯度几乎为零的区域。
3.3 时序聚合:发放率、膜电位加权与末步读出
T 步跑完之后,每一拍都有一张脉冲图S[t],得把它们合成一张融合特征。有几种常见的聚合方式,行为差别很大。
发放率平均:F = (1/T) * Σ S[t]。这是最常用的,本质上是对 T 次二值投票取平均,输出范围是[0, 1],可以直接当特征图用。优点是稳定、可解释;缺点是它丢掉了"什么时候发的"这个信息——早发和晚发在平均之后是一样的。
时序加权平均:F = Σ w[t] * S[t],其中w[t]是可学习的权重。一开始我以为这个会比平均好,毕竟多了自由度。实测下来收益很小,而且权重容易塌缩成 one-hot(只有某一拍的权重接近 1),那就退化成单选某一拍了。如果数据量不大,不建议引入。
膜电位读出:不用脉冲,直接用最后一步的V[T]或者膜电位的衰减和。优点是它是连续值,信息量比二值脉冲大;缺点是它没有经过阈值化,数值范围不稳定,不同图像之间差异很大,后面接损失函数时得做归一化。
我最后采用的是发放率平均 + 末步膜电位的混合读出:主干特征用发放率平均,辅助输出分支用末步膜电位做一条捷径。这条捷径在训练早期尤其有用,因为早期脉冲几乎全是零,光靠发放率传不回梯度。
3.4 显存账单:T 翻倍时到底翻了什么
这一点必须提前算清楚,否则你会在训练跑到一半的时候被 OOM 打断。
反向传播需要在时间维度上展开(BPTT),意味着 T 步的中间激活都得留着。粗略估算:一个[B, C, H, W]的特征图,float32 下占用B*C*H*W*4字节。取B=8, C=64, H=W=256,单张特征图约8*64*256*256*4 = 134 MB。而一个包含 4 个卷积层的编码器,每层至少保存输入和输出两份,再乘上 T=16,轻松超过 15 GB。
我在B=4, C=64, 256×256, T=8的配置下测过,单卡峰值显存大概在 9 到 11 GB 之间浮动,具体取决于有没有用梯度检查点。几个能立刻见效的省显存手段:
| 手段 | 显存节省 | 代价 |
|---|---|---|
| 梯度检查点(只存每层输入,反向时重算) | 约 40%~60% | 训练慢 20%~30% |
| 混合精度(前向 fp16,膜电位保持 fp32) | 约 30% | 需要处理膜电位溢出 |
| 降低 T(16 → 8) | 约 50% | 精度小幅下降 |
| 训练时随机裁剪到 192×192 | 约 44% | 需要改推理流程 |
注意:膜电位不要用 fp16 存。膜电位是逐步累积的,fp16 的精度损失会在 T 步之后被放大成明显的量化误差,表现为融合图出现横向的条纹伪影。激活可以用 fp16,膜电位老老实实用 fp32。
4. 训练阶段真正决定成败的几个参数
结构搭好了,代码能跑通了,接下来才是真正耗时间的部分。这一节列的几个点,都是我在复现过程中反复踩到、并且每次都要回头检查的。
4.1 首步全零:初始膜电位与输入增益
这是 SNN 训练里最经典的坑之一,也是很多人第一次跑 SNN 发现 loss 完全不降的原因。
假设初始膜电位V[0] = 0,那么在第一个时间步,充电方程给出H[0] = X[0] / tau。要让神经元发放,需要X[0] / tau >= V_th,也就是输入电流至少要达到tau * V_th的量级。而如果输入电流的量级只有V_th左右,那么第一拍所有神经元都不发放,输出全零,梯度全零。
更隐蔽的是,如果输入电流刚好卡在临界值附近,第一拍只有极少数像素发放,这些像素在后续的复位操作中又会把膜电位拉低,导致后面几拍也发不出来。整个网络陷入一种"低发放率死锁"状态,表现为训练几十个 epoch 之后 loss 仍然在原地震荡。
我自己用的解法有三个,一般组合使用:
- 输入增益缩放:在电流注入之后乘一个可学习的标量(就是前面那个
gain),初值给 0.5 甚至 0.1,让初始状态远离阈值边界。 - 初始膜电位随机化:
V[0]从一个小的均匀分布采样,而不是全零。这样不同像素处于不同的"起跑线",避免了同步死锁。噪声幅度取0.1 * V_th就够了。 - 首步直通:第一拍不做阈值化,直接把
H[0]当脉冲输出。这一招在推理时也可以用,代价是首步没有稀疏性。我在训练时用它来加速收敛,推理时关掉。
4.2 复位方式:hard reset 与 soft reset 的差别
硬复位是V = V_reset * S + H * (1 - S),发了脉冲就把膜电位清空;软复位是V = H - S * V_th,只扣掉一个阈值的量。
这两个的差别在论文里往往一句话带过,但实际影响很大。硬复位的问题是它破坏信息的连续性:一个神经元如果连续两拍都收到强电流,第一拍发了脉冲被清零,第二拍又得从零开始积分,导致它只能以固定的节奏发放,丢失了输入强度的细节。软复位保留了超出阈值的部分,输入的强弱可以直接体现在"补扣之后还剩多少"上。
融合任务对这一点格外敏感,因为融合图里目标区域的电流强度变化是渐进的(从目标边缘到中心逐渐增强),硬复位会把这种渐变压成二值台阶,表现为融合图的红外目标出现"同心环"状的分层伪影。我换成软复位之后,这个伪影基本消失了。
软复位的代价是膜电位可能无限累积。解决办法是加一个上限截断,或者在损失里加一项膜电位正则。我经验上用clamp(max=4 * V_th)就够了,很少真的触发。
4.3 损失配比:强度项、梯度项、结构项
融合任务的损失函数基本是三件套,SNN 版本也逃不掉:
def fusion_loss(fused, ir, vis): # 强度项:目标区域向红外靠,整体亮度向两模态均值靠 l_int = torch.mean((fused - torch.max(ir, vis)) ** 2) # 梯度项:用 Sobel 算子保证边缘锐度 g_f = sobel(fused) g_max = torch.max(sobel(ir).abs(), sobel(vis).abs()) l_grad = torch.mean(torch.abs(g_f - g_max)) # 结构项:SSIM l_ssim = 1 - ssim(fused, ir) * 0.5 - ssim(fused, vis) * 0.5 return 10.0 * l_int + 5.0 * l_grad + 1.0 * l_ssim配比不是随便写的。强度项权重给到 10 是因为它的数值本身很小(归一化图像的平方误差在 1e-3 量级),不加权的话梯度项会直接压过它。梯度项给 5 是为了保住边缘,给太高会导致融合图出现"过锐化"的黑边,尤其是在平坦区域会凭空长出纹理。结构项权重最小,它起的是稳定作用,不是主导作用。
SNN 版本还得额外加一项:发放率正则。不加的话,训练到中期会出现发放率塌缩——大部分神经元常年不发,直接退化成常数零输出,只剩少数神经元在撑场面。正则项写成mean(spike_rate) * lambda,lambda取 1e-3 量级。这个值给大了会压制所有神经元的活跃度,导致融合图整体变暗,我在 1e-2 的时候踩过一次,整张图灰得像蒙了层雾。
4.4 T 的选择:4、8、16 步的收益曲线
T 是最玄学的一个参数。我把它当成一个独立的实验变量跑了三组,其余全部固定:
| T | 训练耗时(相对 T=4) | 显存峰值 | 主观质量 | 客观指标趋势 |
|---|---|---|---|---|
| 4 | 1.0× | 低 | 边缘略软,目标区域偏灰 | 基线 |
| 8 | 1.85× | 中 | 边缘明显改善 | 稳定提升 |
| 16 | 3.6× | 高 | 与 T=8 肉眼难分辨 | 提升极小,部分指标回落 |
结论很清楚:T 从 4 到 8 的收益远大于 8 到 16。原因是膜电位积分的收敛速度很快,大部分信息在前几步就已经被捕获了,后面的步数都在重复同样的贡献。T=16 时反而出现了部分指标回落,我怀疑是过长的时序链导致替代梯度的累积误差变大,反向传播到早期层时梯度已经失真。
所以我的建议是:先把 T 定在 6 或 8,把所有其他参数调好,最后再回来微调 T。别一上来就堆 T=16,那只是在用显存和电费换微乎其微的收益。
5. 融合图出问题时的排查链路
调试 SNN 融合网络和调试普通 CNN 最大的不同是:出问题的时候,你看到的往往不是"图有点糊",而是"图完全崩了"。这一节把我遇到过、并且能找到明确根因的几种崩坏现象整理出来,配上排查顺序。
5.1 全灰、全黑与棋盘格:三种典型崩坏
全灰:输出图接近均匀的灰色,方差极小。绝大多数情况是发放率饱和。诊断方法很简单,在网络里插一个 hook,打印每一层每个时间步的平均发放率。如果某一层的发放率接近 1.0(全发)或者接近 0.0(全不发),问题就在那一层。全发通常是电流过大或V_th过小,全不发是电流过小或tau过大导致泄漏太多。
全黑:输出图整体接近零,但方差正常。这通常发生在解码器的最后一层之后——如果解码器输出的动态范围被脉冲量化压得太窄,比如所有值都落在[0, 0.1]区间,直接可视化的结果就是一片黑。这不是 bug,是归一化没做。我在第一次跑通的时候被这个坑了整整一天,还以为模型训崩了,实际上是imshow的显示范围问题。
棋盘格:输出图出现规律性的网格伪影。这是转置卷积的经典问题,和 SNN 本身没多大关系。解码器如果用ConvTranspose2d,stride=2、kernel_size不能整除时会留下棋盘纹。解法是换成Upsample + Conv2d,或者把 kernel size 调到能被 stride 整除。这个问题在 SNN 里会被放大,因为脉冲的二值化会把细微的棋盘纹变成明显的黑白格。
5.2 热目标变淡:电流被纹理项淹没
这是融合任务特有的问题,也是最难调的一个。表现是:输出的融合图整体观感不错,边缘清楚、纹理丰富,但红外目标(人、车)的亮度明显弱于原始红外图,看起来"被冲淡了"。
根因是电流尺度失衡。可见光模态的纹理在整张图上分布密集,产生的电流总量大;红外模态的目标虽然局部很强,但空间占比小,总电流量反而小。门控机制如果训练不充分,g会倾向于给可见光一侧更高的权重(因为它在全局统计上更"活跃"),导致红外的电流被压制。
修法有几条,我按有效性排序:
- 给门控加空间监督。在损失里加一项,约束门控图
g在红外高亮区域(ir > 阈值)的值应该大于 0.5。这相当于直接告诉门控"这块地方要听红外的"。这一条最有效,但需要阈值参数调一调。 - 电流归一化。在注入之前对两个模态的电流各自做一次逐样本的归一化(除以通道维度上的均值),强制拉到同一量级。简单粗暴但有效。
- 提高红外的损失权重。在强度项里,对红外高亮区域加一个空间 mask,让这些区域的损失权重更高。这条治标不治本,但见效快。
5.3 边缘重影与网格纹:对齐、padding 与卷积核尺寸
重影的表现是目标边缘出现"双线"或者半透明的拖尾。三个可能的根因,按排查顺序:
第一,模态间没对齐。如果你的红外和可见光是分别采集的,即使做了配准,边缘也可能有几个像素的偏移。SNN 因为多了一层脉冲量化,对这种偏移比 CNN 更敏感——CNN 的连续激活会把偏移模糊掉,脉冲化则会把它放大成两条清晰的边缘。检查方法是单独可视化两个模态的 Sobel 梯度图,叠在一起看是否重合。
第二,padding 模式不一致。编码器里如果有的层用zerospadding、有的用reflect,在图像边界处会产生不连续,表现为一圈明显的边框伪影。统一成reflect或replicate,别用zeros。
第三,卷积核是偶数尺寸。kernel_size=4这种偶数核在 stride=2 时会引入半像素偏移,多层叠加之后边缘会越偏越远。全部换成奇数核(3、5、7)。
6. 想把这套东西用起来,还得算清几笔账
前面讲的都是"怎么训出来",这一节讲"训出来之后能不能用"。SNN 在论文里的能耗优势很多是在特定硬件假设下成立的,落到实际工程里,有几笔账必须提前算。
6.1 推理延迟与能耗:T 步不等于 T 倍耗时
很多人第一反应是"T=8 不就是慢 8 倍吗",这个理解不准确。
在 GPU 上做仿真推理时,T 步确实是 T 次前向,但因为每一步的激活是二值的,如果你用了稀疏张量或者条件计算,实际耗时可能是 T 的 0.3 到 0.5 倍。我实测在T=8时,用稠密张量推理的耗时约是同等规模 CNN 的 3.2 倍,改成稀疏卷积之后降到 1.6 倍左右。这个数字不算好看,所以如果你的场景对延迟敏感(比如实时视频融合),T 必须压到 4 以下,甚至考虑单步近似。
能耗的账更复杂。脉冲的能效优势要在事件驱动的硬件上才成立:没有脉冲到达时,电路不翻转,功耗为零。在 GPU/CPU 上,你付的还是全额的时钟功耗,二值化只是省了带宽。所以如果你的部署目标不是类脑芯片,那么"SNN 省电"这个卖点基本用不上,你得到的就是一个精度还可以、但推理慢了几倍的融合网络。这一点在自己动手之前必须想清楚。
6.2 与现有融合流水线对接时的接口设计
如果你手上已经有一套融合流水线(比如上游有配准模块、下游有检测模块),把 SNN 融合器插进去的时候,接口设计要注意两点。
输出的数值范围。发放率平均的输出天然在[0, 1]区间,而传统融合器的输出往往是你自定义的归一化范围。如果下游检测器是按传统范围训练的,直接接上去会导致检测框整体偏移。我在对接时就遇到过这个问题,最后是在融合器后面加了一个可学习的仿射层y = a * x + b,用少量样本微调对齐。
批处理与变分辨率。SNN 对输入尺寸的敏感度比 CNN 高,因为膜电位的累积步数 T 是固定的,输入尺寸变了之后,感受野覆盖的相对范围就变了。如果你的流水线里有变分辨率输入,最好固定一个训练尺寸,推理时用 padding 补齐,别让它自由缩放。
最后分享一个小技巧:如果你只是想在现有融合网络上验证 SNN 带来的增益,而不想重写整个架构,有个低成本的路径——保留原有的编码器解码器结构,只把中间的融合层换成一个 T 步的脉冲融合模块,其余全部冻结。这样训练量小、风险低,能快速判断这套时序融合机制在你的数据上到底值不值。我自己就是这么起步的,先用一个 3 层的小融合模块替代原来的 concat 层,跑出来的第一版结果虽然指标一般,但能明显看出红外目标区域的对比度比原来高,这才让我有动力去搭完整版本。这个判断过程比直接照搬一整套架构要省太多时间了。