正则化方法(Regularization Method)- DropPath
flyfish
我们知道,深度神经网络越深,理论上表达能力越强,但实际训练时却非常困难。 网络太深容易出现梯度消失、过拟合,而且层与层之间会产生强烈的协同适应(co-adaptation)------ 后面的层过度依赖前面某一层的特定输出,导致网络整体非常脆弱。
有人提出一个很有意思的想法:既然 Dropout 对单个神经元有效,那能不能对整个路径也做类似的事情呢?
于是,DropPath(随机路径) 诞生了。DropPath 其实就是 Stochastic Depth(随机深度) 的具体实现方式之一,所以有的地方DropPath翻译成随机深度。
DropPath是按 batch 中的每个样本独立决策:同一个 batch 里,一部分样本会完整走残差分支,另一部分样本的残差分支会被整体置 0(相当于该样本跳过残差分支,只走主通路),并不是整个网络的某条分支被全局丢弃。
DropPath 的本质是:在训练时,随机掐断网络中的某些残差路径(residual path),但保证网络仍然是连通的。
想象一个残差块(Residual Block):
输入 → 主分支(恒等映射) ─────────────┐
└→ 残差分支(要做很多计算) ─── 加法 → 输出
传统的残差网络每次都把两个分支都算完然后相加。
DropPath 则会在训练时这样做:
以一定概率(比如 0.2),把残差分支整个丢掉(直接乘以 0)
这时候这个样本的输出就只剩下主分支(也就是输入本身)
或者以概率 0.8,正常走残差分支
更重要的是:每个样本的丢弃情况是独立的,同一个 batch 里不同样本可能走完全不同的子网络。
因为它丢弃的不是单个神经元,而是一整条计算路径(Path)。
不同样本每次看到的都是网络的不同"版本",迫使每一层都更加鲁棒,不能过度依赖某一条特定路径。
训练时相当于同时在训练很多个不同深度的子网络,推理时把它们融合起来。
DropPath 就是训练时随机掐掉部分残差路径,推理时全部保留的一种正则化技巧。
训练 vs 推理
训练时:随机 DropPath(每个样本独立)
推理时:不丢弃,所有路径都保留(通常还会对残差分支做缩放,保持期望值一致)
这和 Dropout 的行为是一致的。
python
import torch
import torch.nn as nn
class DropPath(nn.Module):
"""
随机深度(Stochastic Depth)
训练时按样本随机丢弃残差分支,推理时直通
返回:(输出张量, 丢弃掩码)
"""
def __init__(self, drop_prob: float = 0., scale_by_keep: bool = True):
super().__init__()
self.drop_prob = drop_prob
self.scale_by_keep = scale_by_keep
def forward(self, x):
if self.drop_prob == 0. or not self.training:
mask = x.new_ones((x.shape[0],) + (1,) * (x.ndim - 1))
return x, mask
keep_prob = 1 - self.drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1)
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
if self.scale_by_keep:
random_tensor.div_(keep_prob)
return x * random_tensor, random_tensor
class ResidualBlock(nn.Module):
"""
带 DropPath 的残差块
主分支:恒等映射
残差分支:Linear + ReLU + Linear + DropPath
输出 = 主分支 + 经DropPath的残差分支
"""
def __init__(self, dim: int, drop_path_prob: float = 0.):
super().__init__()
self.residual_branch = nn.Sequential(
nn.Linear(dim, dim),
nn.ReLU(),
nn.Linear(dim, dim)
)
self.drop_path = DropPath(drop_path_prob)
def forward(self, x):
identity = x
res_raw = self.residual_branch(x)
res_out, drop_mask = self.drop_path(res_raw)
output = identity + res_out
return output, drop_mask, identity, res_raw, res_out
def round_tensor(tensor: torch.Tensor, decimals: int = 4) -> torch.Tensor:
"""
将 PyTorch 张量四舍五入到指定小数位
Args:
tensor: 输入张量
decimals: 保留的小数位数
"""
if decimals < 0:
raise ValueError("decimals must be non-negative")
scale = 10 ** decimals
return torch.round(tensor * scale) / scale
# ===================== 主程序 =====================
if __name__ == "__main__":
BATCH_SIZE = 3
FEATURE_DIM = 6
DROP_PROB = 0.5
NUM_EPOCHS = 3
torch.manual_seed(123)
x = torch.randn(BATCH_SIZE, FEATURE_DIM)
# 构建模型
model = ResidualBlock(dim=FEATURE_DIM, drop_path_prob=DROP_PROB)
model.train()
print("=" * 70)
print("【全局固定网络结构】(物理结构始终不变)")
print("=" * 70)
print(model)
print("\n说明:模块和参数永远存在,变化的只是每个样本的有效计算路径")
for epoch in range(NUM_EPOCHS):
print(f"\n{'='*25} Epoch {epoch+1} 训练模式 {'='*25}")
output, mask, identity, res_raw, res_out = model(x)
mask_1d = mask.squeeze()
# 判断每个样本是否被丢弃
dropped_mask = (mask_1d == 0.0)
print(f"\n本轮丢弃情况:")
for i in range(BATCH_SIZE):
status = "丢弃" if dropped_mask[i] else "保留"
print(f" 样本 {i}:残差分支{status} | 掩码值 = {mask_1d[i]:.4f}")
# 打印每个样本的有效结构 + 数值对比
print("\n【本轮各样本数值与结构对比】")
for i in range(BATCH_SIZE):
is_dropped = dropped_mask[i].item()
print(f"\n 数值对比(样本 {i}):")
print(f" 1 主分支原值(恒等映射):{round_tensor(identity[i]).tolist()}")
print(f" 2 残差分支原始输出: {round_tensor(res_raw[i]).tolist()}")
print(f" 3 DropPath后残差输出: {round_tensor(res_out[i]).tolist()}")
print(f" 4 最终输出(1+3): {round_tensor(output[i]).tolist()}")
if is_dropped:
equal = torch.allclose(output[i], identity[i], atol=1e-6)
print(f" 结论:残差丢弃 → 最终输出 == 主分支原值({equal})")
else:
equal = torch.allclose(output[i], identity[i], atol=1e-6)
print(f" 结论:残差保留 → 最终输出 != 主分支原值({not equal})")
print("-" * 50)
# 推理模式对比
print(f"\n{'='*27} 推理模式 (eval) {'='*27}")
model.eval()
eval_output, eval_mask, eval_id, eval_res_raw, eval_res_out = model(x)
print("推理模式下所有样本残差分支均保留,结构固定,输出唯一")
print(f"\n推理模式最终输出(所有样本均为主分支+完整残差):")
print(round_tensor(eval_output))
bash
======================================================================
【全局固定网络结构】(物理结构始终不变)
======================================================================
ResidualBlock(
(residual_branch): Sequential(
(0): Linear(in_features=6, out_features=6, bias=True)
(1): ReLU()
(2): Linear(in_features=6, out_features=6, bias=True)
)
(drop_path): DropPath()
)
说明:模块和参数永远存在,变化的只是每个样本的有效计算路径
========================= Epoch 1 训练模式 =========================
本轮丢弃情况:
样本 0:残差分支保留 | 掩码值 = 2.0000
样本 1:残差分支丢弃 | 掩码值 = 0.0000
样本 2:残差分支丢弃 | 掩码值 = 0.0000
【本轮各样本数值与结构对比】
数值对比(样本 0):
1 主分支原值(恒等映射):[0.33739998936653137, -0.1777999997138977, -0.16899999976158142, 0.9178000092506409, 1.5809999704360962, 1.3009999990463257]
2 残差分支原始输出: [-0.027300000190734863, -0.10220000147819519, -0.043299999088048935, 0.17249999940395355, -0.4903999865055084, 0.3052000105381012]
3 DropPath后残差输出: [-0.054499998688697815, -0.2045000046491623, -0.08649999648332596, 0.3449000120162964, -0.9807999730110168, 0.6104999780654907]
4 最终输出(1+3): [0.28279998898506165, -0.3822999894618988, -0.2554999887943268, 1.2626999616622925, 0.6001999974250793, 1.9114999771118164]
结论:残差保留 → 最终输出 != 主分支原值(True)
--------------------------------------------------
数值对比(样本 1):
1 主分支原值(恒等映射):[1.2753000259399414, -0.20100000500679016, -0.16060000658035278, -0.40149998664855957, 0.9666000008583069, -1.1481000185012817]
2 残差分支原始输出: [0.32179999351501465, -0.006000000052154064, 0.07109999656677246, -0.026799999177455902, -0.1339000016450882, 0.08429999649524689]
3 DropPath后残差输出: [0.0, -0.0, 0.0, -0.0, -0.0, 0.0]
4 最终输出(1+3): [1.2753000259399414, -0.20100000500679016, -0.16060000658035278, -0.40149998664855957, 0.9666000008583069, -1.1481000185012817]
结论:残差丢弃 → 最终输出 == 主分支原值(True)
--------------------------------------------------
数值对比(样本 2):
1 主分支原值(恒等映射):[-1.1589000225067139, 0.3255000114440918, -0.6315000057220459, -2.8399999141693115, -0.7849000096321106, -1.409600019454956]
2 残差分支原始输出: [0.45500001311302185, 0.09799999743700027, 0.34040001034736633, 0.018200000748038292, -0.24320000410079956, -0.02329999953508377]
3 DropPath后残差输出: [0.0, 0.0, 0.0, 0.0, -0.0, -0.0]
4 最终输出(1+3): [-1.1589000225067139, 0.3255000114440918, -0.6315000057220459, -2.8399999141693115, -0.7849000096321106, -1.409600019454956]
结论:残差丢弃 → 最终输出 == 主分支原值(True)
--------------------------------------------------
========================= Epoch 2 训练模式 =========================
本轮丢弃情况:
样本 0:残差分支丢弃 | 掩码值 = 0.0000
样本 1:残差分支丢弃 | 掩码值 = 0.0000
样本 2:残差分支保留 | 掩码值 = 2.0000
【本轮各样本数值与结构对比】
数值对比(样本 0):
1 主分支原值(恒等映射):[0.33739998936653137, -0.1777999997138977, -0.16899999976158142, 0.9178000092506409, 1.5809999704360962, 1.3009999990463257]
2 残差分支原始输出: [-0.027300000190734863, -0.10220000147819519, -0.043299999088048935, 0.17249999940395355, -0.4903999865055084, 0.3052000105381012]
3 DropPath后残差输出: [-0.0, -0.0, -0.0, 0.0, -0.0, 0.0]
4 最终输出(1+3): [0.33739998936653137, -0.1777999997138977, -0.16899999976158142, 0.9178000092506409, 1.5809999704360962, 1.3009999990463257]
结论:残差丢弃 → 最终输出 == 主分支原值(True)
--------------------------------------------------
数值对比(样本 1):
1 主分支原值(恒等映射):[1.2753000259399414, -0.20100000500679016, -0.16060000658035278, -0.40149998664855957, 0.9666000008583069, -1.1481000185012817]
2 残差分支原始输出: [0.32179999351501465, -0.006000000052154064, 0.07109999656677246, -0.026799999177455902, -0.1339000016450882, 0.08429999649524689]
3 DropPath后残差输出: [0.0, -0.0, 0.0, -0.0, -0.0, 0.0]
4 最终输出(1+3): [1.2753000259399414, -0.20100000500679016, -0.16060000658035278, -0.40149998664855957, 0.9666000008583069, -1.1481000185012817]
结论:残差丢弃 → 最终输出 == 主分支原值(True)
--------------------------------------------------
数值对比(样本 2):
1 主分支原值(恒等映射):[-1.1589000225067139, 0.3255000114440918, -0.6315000057220459, -2.8399999141693115, -0.7849000096321106, -1.409600019454956]
2 残差分支原始输出: [0.45500001311302185, 0.09799999743700027, 0.34040001034736633, 0.018200000748038292, -0.24320000410079956, -0.02329999953508377]
3 DropPath后残差输出: [0.9100000262260437, 0.19609999656677246, 0.6808000206947327, 0.036400001496076584, -0.48649999499320984, -0.04659999907016754]
4 最终输出(1+3): [-0.24879999458789825, 0.5214999914169312, 0.049300000071525574, -2.8036000728607178, -1.271399974822998, -1.4560999870300293]
结论:残差保留 → 最终输出 != 主分支原值(True)
--------------------------------------------------
========================= Epoch 3 训练模式 =========================
本轮丢弃情况:
样本 0:残差分支保留 | 掩码值 = 2.0000
样本 1:残差分支保留 | 掩码值 = 2.0000
样本 2:残差分支保留 | 掩码值 = 2.0000
【本轮各样本数值与结构对比】
数值对比(样本 0):
1 主分支原值(恒等映射):[0.33739998936653137, -0.1777999997138977, -0.16899999976158142, 0.9178000092506409, 1.5809999704360962, 1.3009999990463257]
2 残差分支原始输出: [-0.027300000190734863, -0.10220000147819519, -0.043299999088048935, 0.17249999940395355, -0.4903999865055084, 0.3052000105381012]
3 DropPath后残差输出: [-0.054499998688697815, -0.2045000046491623, -0.08649999648332596, 0.3449000120162964, -0.9807999730110168, 0.6104999780654907]
4 最终输出(1+3): [0.28279998898506165, -0.3822999894618988, -0.2554999887943268, 1.2626999616622925, 0.6001999974250793, 1.9114999771118164]
结论:残差保留 → 最终输出 != 主分支原值(True)
--------------------------------------------------
数值对比(样本 1):
1 主分支原值(恒等映射):[1.2753000259399414, -0.20100000500679016, -0.16060000658035278, -0.40149998664855957, 0.9666000008583069, -1.1481000185012817]
2 残差分支原始输出: [0.32179999351501465, -0.006000000052154064, 0.07109999656677246, -0.026799999177455902, -0.1339000016450882, 0.08429999649524689]
3 DropPath后残差输出: [0.6435999870300293, -0.012000000104308128, 0.14219999313354492, -0.053599998354911804, -0.2678999900817871, 0.16859999299049377]
4 最终输出(1+3): [1.9189000129699707, -0.21299999952316284, -0.018400000408291817, -0.45509999990463257, 0.6987000107765198, -0.9796000123023987]
结论:残差保留 → 最终输出 != 主分支原值(True)
--------------------------------------------------
数值对比(样本 2):
1 主分支原值(恒等映射):[-1.1589000225067139, 0.3255000114440918, -0.6315000057220459, -2.8399999141693115, -0.7849000096321106, -1.409600019454956]
2 残差分支原始输出: [0.45500001311302185, 0.09799999743700027, 0.34040001034736633, 0.018200000748038292, -0.24320000410079956, -0.02329999953508377]
3 DropPath后残差输出: [0.9100000262260437, 0.19609999656677246, 0.6808000206947327, 0.036400001496076584, -0.48649999499320984, -0.04659999907016754]
4 最终输出(1+3): [-0.24879999458789825, 0.5214999914169312, 0.049300000071525574, -2.8036000728607178, -1.271399974822998, -1.4560999870300293]
结论:残差保留 → 最终输出 != 主分支原值(True)
--------------------------------------------------
=========================== 推理模式 (eval) ===========================
推理模式下所有样本残差分支均保留,结构固定,输出唯一
推理模式最终输出(所有样本均为主分支+完整残差):
tensor([[ 0.3101, -0.2800, -0.2122, 1.0902, 1.0906, 1.6063],
[ 1.5971, -0.2070, -0.0895, -0.4283, 0.8326, -1.0639],
[-0.7038, 0.4235, -0.2911, -2.8218, -1.0281, -1.4329]],
grad_fn=<DivBackward0>)
代码解释 bernoulli_
bernoulli_() 采样的是伯努利分布(Bernoulli Distribution),这个分布的定义是只有 0 和 1 两种取值,所以输出结果天然只能是 0 或 1。
伯努利分布对应的是「单次只有两种结果的随机试验」(比如抛硬币、抽中/没抽中),数学上严格定义:
试验成功 → 取值为 1 ,发生概率为 p
试验失败 → 取值为 0 ,发生概率为 1-p
它的样本空间只有 {0, 1} 两个值,不存在 0.5、2 之类的中间值或其他数值。
bernoulli_(p) 做的事情就是:对张量里的每一个元素,独立做一次这样的二元试验,把结果(0 或 1)填进对应位置。
python
import torch
torch.manual_seed(0)
x = torch.empty(10)
x.bernoulli_(p=0.3) # 30%概率取1,70%概率取0
print(x)
输出一定是一串由 0 和 1 组成的数,不会出现其他值:
tensor([0., 0., 0., 0., 1., 0., 0., 0., 0., 0.])
为什么 DropPath 里要用 0/1 的结果?
这是为了做掩码乘法 ,适配开关逻辑:
掩码值为 1:和原特征相乘后数值不变 → 该样本的残差分支保留
掩码值为 0:和原特征相乘后全变为 0 → 该样本的残差分支丢弃
0/1 掩码正好实现了要么整个分支全留、要么整个分支全丢的效果。
缩放
python
random_tensor.div_(keep_prob) # 除以保留概率
bernoulli_ 输出:[1, 0, 1, 0](只有 0 和 1)
除以 0.5 之后:[2, 0, 2, 0](出现了非 0/1 的值)
这一步是为了对齐训练和推理的数学期望,属于额外的数值修正,和伯努利采样本身无关。
代码解释 shape = (x.shape0,) + (1,) * (x.ndim - 1)
python
shape = (x.shape[0],) + (1,) * (x.ndim - 1)
整行是元组拼接操作,由两部分相加组成:
1. 第一部分:(x.shape[0],)
x.shape[0]:输入张量第 0 维的大小,也就是 batch 样本数。
后面的逗号不能省略 :Python 中(4)是整数,(4,)才是元组,只有元组才能和后面的元组做拼接。
作用:保留 batch 维度的真实大小,让每个样本拥有独立的掩码值。
2. 第二部分:(1,) * (x.ndim - 1)
x.ndim:输入张量的总维度数(比如 2D 张量是 2,4D 图像特征是 4)。
x.ndim - 1:去掉 batch 维后,剩余的维度数量。
(1,) * N:元组乘法,生成一个包含 N 个 1 的元组。
比如 (1,) * 3 → (1, 1, 1)
比如 (1,) * 1 → (1,)
作用:除了 batch 维之外,剩下所有维度都填 1,为广播做准备。
3. 整体:+ 元组拼接
把batch 维元组和全 1 元组首尾拼接,得到最终的掩码形状。
例1:2D 全连接特征 [B, D]
python
x.shape = (4, 8) # batch=4, 特征维度=8
x.ndim = 2
计算过程:
(x.shape[0],) → (4,)
(1,) * (2-1) → (1,)
拼接结果:(4, 1)
例2:4D 图像特征 [B, C, H, W](最常见,ConvNeXt 里就是这种)
python
x.shape = (4, 3, 32, 32) # batch=4, 3通道, 32x32
x.ndim = 4
计算过程:
(x.shape[0],) → (4,)
(1,) * (4-1) → (1, 1, 1)
拼接结果:(4, 1, 1, 1)
例3:3D 序列特征 [B, T, D]
python
x.shape = (4, 10, 64) # batch=4, 10个时间步, 特征64维
x.ndim = 3
计算过程:
拼接结果:(4, 1, 1)
广播的效果
以 4D 场景为例:
掩码形状:(4, 1, 1, 1)
输入形状:(4, 3, 32, 32)
两者相乘时,PyTorch 会自动把掩码的后三个维度复制扩展 到和输入一致:
通道维:1 → 3,同一个样本的 3 个通道用同一个掩码值
高维:1 → 32,同一行所有像素用同一个掩码值
宽维:1 → 32,同一列所有像素用同一个掩码值
最终效果:同一个样本的所有通道、所有像素,都会乘上完全相同的一个系数(要么是缩放后的保留值,要么是 0)。

黑色实线:当前保留、正常传输信息的连接
彩色虚线(红、蓝、绿、黄):被 Drop-path 随机丢弃、断开的连接
训练每一轮都会随机断开不同的连接,让不同的路径组合生效,避免网络过度依赖某几条路径。
DropPath(随机路径)是一种在分形网络块中使用的正则化技术。它通过随机禁用层间部分连接来采样子网络,但始终确保至少存在一条从输入到输出的有效路径。在训练时,每个 mini-batch 呈现不同的激活子网络,从而防止并行路径间的协同适应。全局采样策略每次选择单个列作为子网络,配合局部采样可促使每个列独立发展成高性能的子网络。
分形卷积网络采用的droppath
参考 FRACTALNET: ULTRA-DEEP NEURAL NETWORKS WITHOUT RESIDUALS

粉色方块 :Convolution 卷积层,负责特征提取
绿色长条 :Join 融合层,对多条并行路径的输出做逐元素取平均 (element-wise mean)
黄色方块 :Pool 池化层,用于降低特征图的空间分辨率(宽高减半)
蓝色方块:Prediction 预测层,网络最终的分类/输出头
左侧:分形扩展规则(Fractal Expansion Rule)
这是分形网络的递归生成逻辑,用自相似的方式不断扩展网络深度。
-
基础情况 fC(z)f_C(z)fC(z)(C=1)
最左边是最简结构:输入 zzz 经过1个卷积层 直接输出 fC(z)f_C(z)fC(z)。这是分形的最小单元,只有1条路径、1层卷积。
-
扩展后 fC+1(z)f_{C+1}(z)fC+1(z)(C升一级)
把原本的单层卷积,替换成两条并行路径 :
左侧短路径:1个卷积层(保留原有的浅层通路)
右侧长路径:2个卷积层串联(加深的深层通路)
最后通过绿色 Join 层将两条路径的输出取平均融合,得到最终输出。
这就是分形的自相似扩展:每升一阶,就把原来的单层替换成短路径 + 双卷积分支的并行结构,递归下去就会生成多条不同深度的交织路径。
中间:C=4 的分形块 f4(z)f_4(z)f4(z)
这是按照上面的规则,递归扩展到 C=4(4阶分形) 的单个分形块效果:
块内有 4 条交织的并行路径(对应 C 条"列"),路径长度从短到长各不相同
最短的路径只有1层卷积,最长的路径有 2C−1=82^{C-1}=82C−1=8 层卷积
所有路径最终汇聚到最底部的绿色 Join 层,融合后输出
视觉上呈现嵌套、自相似的分形结构,这也是 FractalNet 名字的由来
Drop-path 机制:训练时会随机丢弃块内的部分路径,只保留部分通路生效,强制不同深度的路径都能独立学习特征。
完整深度网络的堆叠结构
这是实际用于图像任务的完整网络结构:
- 整个网络由 B=5 个分形块(Block 1~5) 纵向堆叠而成
- 每两个分形块之间插入黄色池化层(Pool),对特征图降采样(宽高减半,通道数通常翻倍)
- 最后一个块之后接蓝色预测层(Prediction) ,输出最终分类结果 yyy
总深度计算公式
网络总卷积层数的公式:
总深度=B⋅2C−1\text{总深度} = B \cdot 2^{C-1}总深度=B⋅2C−1
本例子中 B=5B=5B=5(5个块),C=4C=4C=4(每个块是4阶分形),代入得:
5×24−1=5×8=405 \times 2^{4-1} = 5 \times 8 = 405×24−1=5×8=40
也就是整个网络共有 40 层卷积。