
本文介绍如何通过合理设置卷积层的 padding,确保各层输出空间尺寸一致,从而安全地对不同层的 goodness 分数(如平方均值)进行逐元素求和,解决 Forward-Forward 算法中因 shape 不匹配导致的 sum() 报错问题。
本文介绍如何通过合理设置卷积层的 padding,确保各层输出空间尺寸一致,从而安全地对不同层的 goodness 分数(如平方均值)进行逐元素求和,解决 forward-forward 算法中因 shape 不匹配导致的 `sum()` 报错问题。
在 Forward-Forward 算法中,模型需为每个类别计算一个总 goodness 分数,该分数由网络各层的局部 goodness(通常定义为激活张量的平方均值:x.pow(2).mean(1))累加得到。然而,标准卷积(无 padding 或 padding 不足)会不断缩小特征图的空间尺寸(如 14×14 → 7×7 → 3×3),导致各层 goodness 张量形状不一致(例如 [50000, 14, 14]、[50000, 7, 7]、[50000, 3, 3]),无法直接调用 sum(goodness) —— PyTorch 的 sum() 对列表中 shape 不同的 Tensor 会抛出 RuntimeError。
根本解法不是后处理“拉平再插值”或“裁剪对齐”,而是从网络设计源头保证空间维度一致:使用 same convolution(即输出尺寸 ≈ 输入尺寸),其关键在于为每个卷积层显式配置合适的 padding,使得:
[ \text{Output_size} = \left\lfloor \frac{H + 2 \times \text{padding} - \text{kernel_size}}{\text{stride}} \right\rfloor + 1 \approx H ]
当 stride=1 时,满足此条件的最小非负 padding 是:
[
\text{padding} = \left\lfloor \frac{\text{kernel_size} - 1}{2} \right\rfloor
]
例如,对 kernel_size=3,padding=1;对 kernel_size=5,padding=2。这样,无论经过多少层卷积,只要 stride 保持为 1,空间尺寸将基本不变(忽略边界舍入误差),各层 goodness 张量便具有相同 shape(如全为 [N, H, W]),可直接逐元素相加。
以下是推荐的模块化实现方式(适配 Forward-Forward 架构):
import torch
import torch.nn as nn
class SameConvBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, bias=True, device="cuda"):
super().__init__()
# ✅ 关键:自动计算 same-padding
padding = (kernel_size - 1) // 2
self.conv = nn.Conv2d(
in_channels, out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias
).to(device)
self.bn = nn.BatchNorm2d(out_channels).to(device)
self.relu = nn.ReLU().to(device)
# 注意:若需下采样,应在 relu 后接独立的 MaxPool2d(而非用 stride>1 的 conv)
# 这样可清晰分离「特征提取」与「空间压缩」逻辑
def forward(self, x):
return self.relu(self.bn(self.conv(x)))
# 在模型定义中统一使用 SameConvBlock
class FFConvNet(nn.Module):
def __init__(self, input_channels=1, output_dim=10, device="cuda"):
super().__init__()
self.layers = nn.Sequential(
SameConvBlock(input_channels, 6, kernel_size=3, device=device), # → [N,6,H,W]
SameConvBlock(6, 16, kernel_size=3, device=device), # → [N,16,H,W]
SameConvBlock(16, 120, kernel_size=3, device=device), # → [N,120,H,W]
).to(device)
self.output_dim = output_dim
self.device = device
def predict(self, x):
goodness_score_per_label = []
for label in range(self.output_dim):
encoded = overlay_y_on_x(x, label) # 假设已定义,shape: [N,C,H,W]
goodness = []
for layer in self.layers:
encoded = layer(encoded) # shape 保持 [N,C,H,W]
g = encoded.pow(2).mean(dim=1) # → [N,H,W],每层一致!
goodness.append(g)
# ✅ 现在所有 goodness[i] 形状相同,可安全 sum → [N,H,W]
total_goodness = sum(goodness) # 自动广播逐元素相加
goodness_score_per_label.append(total_goodness.unsqueeze(1)) # → [N,1,H,W]
# 拼接并取 argmax(按 channel 维度)
scores = torch.cat(goodness_score_per_label, dim=1) # → [N, output_dim, H, W]
return scores.mean(dim=[2,3]).argmax(dim=1) # 全局平均后分类(或保留 spatial-aware 逻辑)⚠️ 重要注意事项:
- 避免混合 stride > 1 的卷积与 same-padding:若需降维,请显式使用 nn.MaxPool2d(kernel_size=2, stride=2) 或 nn.AvgPool2d,并在 pooling 后重新校准后续层的 padding(因输入尺寸已变);
- 全局池化替代方案:若坚持使用原始尺寸递减结构,可在每层 goodness 后添加自适应池化(如 nn.AdaptiveAvgPool2d((1,1))),将 [N,H,W] 统一压缩为 [N,1,1],再展平为 [N] 后相加 —— 但会丢失空间结构信息,违背 Forward-Forward 的局部 goodness 设计初衷;
-
验证 shape 一致性:在 predict 中加入断言可增强鲁棒性:
assert all(g.shape == goodness[0].shape for g in goodness), "Goodness tensors must have identical shape!"
综上,通过 padding = (kernel_size - 1) // 2 实现 same convolution,是从架构层面根治跨层求和 shape 冲突的最简洁、高效且语义清晰的方案,既符合 Forward-Forward 的理论要求,也便于调试与扩展。










