news 2026/10/17 3:07:14

【Bug已解决】How to decide which mode to use for ‘kaiming_normal‘ initialization 解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Bug已解决】How to decide which mode to use for ‘kaiming_normal‘ initialization 解决方案

【Bug已解决】How to decide which mode to use for 'kaiming_normal' initialization 解决方案

问题描述

在 PyTorch 中使用kaiming_normal_(也称为 He 初始化)进行权重初始化时,开发者经常对mode参数的选择感到困惑。mode参数有两个选项:'fan_in'和'fan_out',选择错误的模式可能导致训练不稳定、收敛缓慢甚至梯度爆炸/消失。

import torch import torch.nn as nn # kaiming_normal_ 的签名 # torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu') # 常见的困惑: # 1. mode='fan_in' 和 mode='fan_out' 有什么区别? # 2. 什么时候该用 fan_in,什么时候该用 fan_out? # 3. nonlinearity 参数如何选择? # 4. a 参数代表什么?

mode参数决定了初始化时使用的归一化因子,直接影响权重的方差大小,进而影响前向传播和反向传播中信号的方差。

错误复现

问题 1:使用错误的 mode 导致梯度爆炸

import torch import torch.nn as nn # 深层网络,使用错误的初始化 class DeepNetwork(nn.Module): def __init__(self, depth=20, width=256, init_mode='fan_out'): super().__init__() layers = [] layers.append(nn.Linear(784, width)) for _ in range(depth - 2): layers.append(nn.Linear(width, width)) layers.append(nn.Linear(width, 10)) self.layers = nn.ModuleList(layers) # 使用指定 mode 初始化 for layer in self.layers: nn.init.kaiming_normal_(layer.weight, mode=init_mode, nonlinearity='relu') nn.init.zeros_(layer.bias) def forward(self, x): for i, layer in enumerate(self.layers): x = layer(x) if i < len(self.layers) - 1: x = torch.relu(x) return x # 测试不同 mode 的效果 x = torch.randn(32, 784) # fan_in 模式(推荐用于前向传播) model_in = DeepNetwork(depth=20, init_mode='fan_in') out_in = model_in(x) print(f"fan_in output std: {out_in.std().item():.4f}") # fan_out 模式(推荐用于反向传播) model_out = DeepNetwork(depth=20, init_mode='fan_out') out_out = model_out(x) print(f"fan_out output std: {out_out.std().item():.4f}") # 对比梯度 loss_in = model_in(x).sum() loss_in.backward() grad_in = model_in.layers[0].weight.grad.std().item() loss_out = model_out(x).sum() loss_out.backward() grad_out = model_out.layers[0].weight.grad.std().item() print(f"fan_in gradient std: {grad_in:.6f}") print(f"fan_out gradient std: {grad_out:.6f}")

问题 2:nonlinearity 不匹配

import torch import torch.nn as nn # 错误:使用 ReLU 激活函数但 nonlinearity 设为 'leaky_relu' layer = nn.Linear(100, 200) nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='leaky_relu') # 但实际使用 ReLU,导致初始化方差不正确 # 正确:nonlinearity 应与实际激活函数匹配 nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')

根因分析

1. Kaiming 初始化的数学原理

Kaiming 初始化(He et al., 2015)的目标是:在深层网络中保持每一层输出的方差与输入方差一致,避免信号在前向传播或反向传播中指数级放大或衰减。

对于权重矩阵W,Kaiming 初始化从以下分布中采样:

W ~ N(0, Var(W))

其中Var(W)的计算取决于mode:

mode='fan_in'(保持前向传播方差):

Var(W) = 2 / fan_in

其中fan_in = weight.size(1) * receptive_field_size(输入神经元数量)。

mode='fan_out'(保持反向传播方差):

Var(W) = 2 / fan_out

其中fan_out = weight.size(0) * receptive_field_size(输出神经元数量)。

2. fan_in vs fan_out 的选择原则

  • fan_in:保持前向传播中信号方差的稳定。适用于大多数场景,特别是关注前向传播性能时。PyTorch 默认使用fan_in。

  • fan_out:保持反向传播中梯度方差的稳定。适用于需要特别关注梯度流动的场景,如在非常深的网络中防止梯度消失。

对于全连接层nn.Linear(in_features, out_features):

  • fan_in = in_features
  • fan_out = out_features

当in_features == out_features时,两种模式没有区别。当它们不同时,选择会影响权重的方差。

3. nonlinearity 参数

nonlinearity参数决定了增益(gain)的计算:

nonlinearitygain适用激活函数
'relu'sqrt(2)ReLU, LeakyReLU(a=0)
'leaky_relu'sqrt(2 / (1 + a^2))LeakyReLU(negative_slope=a)
'linear'1恒等映射,无激活
'sigmoid'1Sigmoid
'tanh'5/3Tanh
'selu'3/4SELU

a参数是 LeakyReLU 的负斜率,默认为 0(即标准 ReLU)。

4. PyTorch 默认初始化的问题

PyTorch 的nn.Linear和nn.Conv2d默认使用 Kaiming uniform 初始化(kaiming_uniform_witha=sqrt(5)),这在某些情况下不是最优的。手动指定kaiming_normal_可以获得更好的训练效果。

解决方案

方案一:根据场景选择 mode

import torch import torch.nn as nn def init_weights(model: nn.Module, mode: str = 'fan_in'): """ 统一初始化模型权重 mode='fan_in': 保持前向传播方差(推荐,默认) mode='fan_out': 保持反向传播梯度方差 """ for m in model.modules(): if isinstance(m, nn.Linear): # 全连接层: nonlinearity='relu' 适用于 ReLU 激活 nn.init.kaiming_normal_(m.weight, mode=mode, nonlinearity='relu') if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.Conv2d): # 卷积层: nonlinearity='relu' 适用于 ReLU 激活 nn.init.kaiming_normal_(m.weight, mode=mode, nonlinearity='relu') if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): # BatchNorm: weight=1, bias=0 nn.init.ones_(m.weight) nn.init.zeros_(m.bias) # 使用示例 model = nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 10) ) # 使用 fan_in 初始化(推荐) init_weights(model, mode='fan_in')

方案二:根据激活函数选择 nonlinearity

import torch import torch.nn as nn import torch.nn.functional as F class FlexibleInitNet(nn.Module): """支持不同激活函数的网络,自动匹配初始化""" def __init__(self, activation='relu'): super().__init__() self.activation = activation self.fc1 = nn.Linear(784, 256) self.fc2 = nn.Linear(256, 128) self.fc3 = nn.Linear(128, 10) self._init_weights() def _init_weights(self): """根据激活函数选择正确的 nonlinearity 参数""" nonlinearity_map = { 'relu': 'relu', 'leaky_relu': 'leaky_relu', 'elu': 'relu', # ELU 近似用 relu 'gelu': 'relu', # GELU 近似用 relu 'selu': 'linear', # SELU 有自己的初始化 'tanh': 'tanh', 'sigmoid': 'sigmoid', } nonlinearity = nonlinearity_map.get(self.activation, 'relu') a = 0.01 if self.activation == 'leaky_relu' else 0 for layer in [self.fc1, self.fc2, self.fc3]: if self.activation == 'selu': # SELU 使用特殊的初始化 nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='linear') # SELU 推荐使用 lecun_normal fan_in = layer.weight.size(1) std = (1.0 / fan_in) ** 0.5 nn.init.normal_(layer.weight, mean=0, std=std) else: nn.init.kaiming_normal_(layer.weight, a=a, mode='fan_in', nonlinearity=nonlinearity) nn.init.zeros_(layer.bias) def forward(self, x): x = self._activate(self.fc1(x)) x = self._activate(self.fc2(x)) return self.fc3(x) def _activate(self, x): if self.activation == 'relu': return F.relu(x) elif self.activation == 'leaky_relu': return F.leaky_relu(x, 0.01) elif self.activation == 'elu': return F.elu(x) elif self.activation == 'gelu': return F.gelu(x) elif self.activation == 'selu': return F.selu(x) elif self.activation == 'tanh': return torch.tanh(x) elif self.activation == 'sigmoid': return torch.sigmoid(x) return x # 使用示例 for act in ['relu', 'leaky_relu', 'tanh', 'sigmoid']: model = FlexibleInitNet(activation=act) x = torch.randn(32, 784) out = model(x) print(f"{act}: output std={out.std().item():.4f}")

方案三:对比 fan_in 和 fan_out 的效果

import torch import torch.nn as nn def compare_init_modes(depth=30, width=256, input_dim=784, output_dim=10): """对比 fan_in 和 fan_out 在深层网络中的效果""" def build_model(mode): layers = [nn.Linear(input_dim, width)] for _ in range(depth - 2): layers.append(nn.Linear(width, width)) layers.append(nn.Linear(width, output_dim)) model = nn.Sequential(*layers) for m in model.modules(): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, mode=mode, nonlinearity='relu') nn.init.zeros_(m.bias) return model x = torch.randn(64, input_dim) results = {} for mode in ['fan_in', 'fan_out']: model = build_model(mode) model.eval() with torch.no_grad(): # 前向传播 out = model(x) fwd_std = out.std().item() # 反向传播 model.train() model.zero_grad() loss = model(x).sum() loss.backward() # 第一层梯度的标准差 first_layer = list(model.modules())[1] bwd_std = first_layer.weight.grad.std().item() results[mode] = {'forward_std': fwd_std, 'backward_std': bwd_std} print(f"mode={mode}: forward_std={fwd_std:.4f}, backward_std={bwd_std:.6f}") return results # 运行对比 compare_init_modes(depth=30, width=256)

方案四:卷积层的初始化

import torch import torch.nn as nn class ConvNet(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1)), ) self.classifier = nn.Linear(128, num_classes) self._init_weights() def _init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): # 卷积层使用 fan_in + relu # fan_in = in_channels * kernel_size * kernel_size nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu') if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) elif isinstance(m, nn.Linear): # 最后一层分类器可以用 fan_in + relu 或 fan_in + linear nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu') nn.init.zeros_(m.bias) def forward(self, x): x = self.features(x) x = x.flatten(1) x = self.classifier(x) return x

完整修复代码

""" Kaiming 初始化完整指南 包含: mode 选择、nonlinearity 匹配、效果对比 """ import torch import torch.nn as nn import torch.nn.functional as F import math from typing import Optional, str class KaimingInitializer: """Kaiming 初始化管理器""" # 激活函数到 nonlinearity 的映射 ACTIVATION_MAP = { 'relu': ('relu', 0), 'leaky_relu': ('leaky_relu', 0.01), 'leaky_relu_0.2': ('leaky_relu', 0.2), 'prelu': ('leaky_relu', 0.25), 'elu': ('relu', 0), 'gelu': ('relu', 0), 'selu': ('linear', 0), 'tanh': ('tanh', 0), 'sigmoid': ('sigmoid', 0), 'linear': ('linear', 0), 'softmax': ('linear', 0), } @classmethod def init_layer(cls, layer: nn.Module, activation: str = 'relu', mode: str = 'fan_in'): """ 根据层类型和激活函数进行正确的 Kaiming 初始化 参数: layer: 要初始化的层 (Linear, Conv2d, etc.) activation: 实际使用的激活函数名称 mode: 'fan_in' 或 'fan_out' """ nonlinearity, a = cls.ACTIVATION_MAP.get(activation, ('relu', 0)) if isinstance(layer, nn.Linear): nn.init.kaiming_normal_( layer.weight, a=a, mode=mode, nonlinearity=nonlinearity ) if layer.bias is not None: nn.init.zeros_(layer.bias) elif isinstance(layer, nn.Conv2d): nn.init.kaiming_normal_( layer.weight, a=a, mode=mode, nonlinearity=nonlinearity ) if layer.bias is not None: nn.init.zeros_(layer.bias) elif isinstance(layer, nn.BatchNorm2d) or isinstance(layer, nn.BatchNorm1d): nn.init.ones_(layer.weight) nn.init.zeros_(layer.bias) @classmethod def init_model(cls, model: nn.Module, activation: str = 'relu', mode: str = 'fan_in', verbose: bool = True): """ 初始化整个模型 自动识别层类型并应用正确的初始化 """ stats = {'linear': 0, 'conv': 0, 'bn': 0, 'other': 0} for m in model.modules(): if isinstance(m, nn.Linear): cls.init_layer(m, activation, mode) stats['linear'] += 1 elif isinstance(m, nn.Conv2d): cls.init_layer(m, activation, mode) stats['conv'] += 1 elif isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)): cls.init_layer(m, activation, mode) stats['bn'] += 1 else: stats['other'] += 1 if verbose: print(f"Initialized: {stats}") print(f" mode={mode}, activation={activation}") # ============ 完整训练示例 ============ class ResidualBlock(nn.Module): def __init__(self, in_ch, out_ch, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_ch) self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_ch) self.relu = nn.ReLU(inplace=True) self.downsample = None if stride != 1 or in_ch != out_ch: self.downsample = nn.Sequential( nn.Conv2d(in_ch, out_ch, 1, stride=stride, bias=False), nn.BatchNorm2d(out_ch) ) def forward(self, x): identity = x out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) if self.downsample: identity = self.downsample(x) return self.relu(out + identity) class ResNet(nn.Module): def __init__(self, num_blocks=[2, 2, 2, 2], num_classes=10): super().__init__() self.in_channels = 64 self.conv1 = nn.Conv2d(3, 64, 3, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.layer1 = self._make_layer(64, num_blocks[0], 1) self.layer2 = self._make_layer(128, num_blocks[1], 2) self.layer3 = self._make_layer(256, num_blocks[2], 2) self.layer4 = self._make_layer(512, num_blocks[3], 2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(512, num_classes) def _make_layer(self, out_ch, num_blocks, stride): layers = [ResidualBlock(self.in_channels, out_ch, stride)] self.in_channels = out_ch for _ in range(1, num_blocks): layers.append(ResidualBlock(out_ch, out_ch, 1)) return nn.Sequential(*layers) def forward(self, x): x = self.relu(self.bn1(self.conv1(x))) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = x.flatten(1) return self.fc(x) def main(): """完整使用示例""" print("=" * 60) print("Kaiming 初始化完整示例") print("=" * 60) # 1. 创建模型 model = ResNet(num_classes=10) # 2. 使用 fan_in 初始化(推荐) print("\n--- fan_in 初始化 ---") KaimingInitializer.init_model(model, activation='relu', mode='fan_in') # 3. 检查初始化效果 x = torch.randn(8, 3, 32, 32) model.eval() with torch.no_grad(): out = model(x) print(f"Input std: {x.std().item():.4f}") print(f"Output std: {out.std().item():.4f}") # 4. 对比 fan_out print("\n--- fan_out 初始化 ---") model2 = ResNet(num_classes=10) KaimingInitializer.init_model(model2, activation='relu', mode='fan_out') model2.eval() with torch.no_grad(): out2 = model2(x) print(f"Input std: {x.std().item():.4f}") print(f"Output std: {out2.std().item():.4f}") # 5. 梯度对比 print("\n--- 梯度对比 ---") for name, mdl in [('fan_in', model), ('fan_out', model2)]: mdl.train() mdl.zero_grad() loss = mdl(x).sum() loss.backward() grad_std = mdl.conv1.weight.grad.std().item() print(f"{name}: first layer grad std = {grad_std:.6f}") # 6. 训练对比 print("\n--- 训练对比 (5 epochs) ---") for name, mdl in [('fan_in', model), ('fan_out', model2)]: mdl.train() optimizer = torch.optim.SGD(mdl.parameters(), lr=0.01, momentum=0.9) criterion = nn.CrossEntropyLoss() for epoch in range(5): total_loss = 0 for _ in range(10): bx = torch.randn(16, 3, 32, 32) by = torch.randint(0, 10, (16,)) optimizer.zero_grad() out = mdl(bx) loss = criterion(out, by) loss.backward() optimizer.step() total_loss += loss.item() print(f" {name} epoch {epoch+1}: loss={total_loss/10:.4f}") if __name__ == "__main__": main()

常见陷阱与注意事项

1. fan_in vs fan_out 的选择

  • 大多数情况用fan_in:保持前向传播信号方差稳定,是 PyTorch 的默认值。
  • 深层网络反传困难时用fan_out:保持反向传播梯度方差稳定。
  • 当 fan_in == fan_out 时无区别:如方阵形状的权重。

2. nonlinearity 必须与实际激活函数匹配

使用 ReLU 时设nonlinearity='relu',使用 LeakyReLU 时设nonlinearity='leaky_relu'并正确设置a参数。

3. BatchNorm 层的初始化

BatchNorm 的 weight 初始化为 1,bias 初始化为 0,不要用 Kaiming 初始化。

4. 最后一层的初始化

分类任务的最后一层(分类头)可以考虑使用较小的初始化,以减小初始损失:

# 最后一层用较小的初始化 nn.init.kaiming_normal_(self.fc.weight, mode='fan_in', nonlinearity='linear') nn.init.zeros_(self.fc.bias)

5. 与 Xavier/Glorot 初始化的区别

  • Xavier/Glorot:适用于 tanh、sigmoid 等饱和激活函数,使用fan_in + fan_out的平均。
  • Kaiming/He:适用于 ReLU 等非饱和激活函数,使用fan_in或fan_out。

6. PyTorch 默认初始化

PyTorch 的nn.Linear默认使用kaiming_uniform_witha=sqrt(5),这等价于一个较大的均匀分布初始化。手动使用kaiming_normal_通常能获得更好的训练效果。

总结

Kaiming 初始化的mode参数选择原则:

  1. 默认使用fan_in:保持前向传播信号方差稳定,适用于绝大多数场景。
  2. 深层网络考虑fan_out:当反向传播梯度消失时,使用fan_out保持梯度方差。
  3. 匹配 nonlinearity:nonlinearity参数必须与实际使用的激活函数一致。
  4. 正确设置 a 参数:使用 LeakyReLU 时,a应等于 negative_slope。
  5. BatchNorm 特殊处理:BatchNorm 的 weight=1, bias=0,不用 Kaiming。
  6. 卷积层同样适用:fan_in = in_channels * kernel_size^2。

理解fan_in和fan_out的数学含义,根据网络深度和激活函数选择正确的参数,是确保训练稳定性的关键。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:06:30

叠筐问题详解:C语言图形输出与分层填充的经典入门

叠筐问题是很多刷题人入门阶段都会碰到的一道经典图形输出题。题目本身不复杂&#xff1a;给你一个奇数尺寸的筐和两种花色字符&#xff0c;要求从内到外一圈一圈交替叠出一个正方形筐&#xff0c;而且筐的四个角还必须是空的。我在某个OJ上第一次见到它的时候&#xff0c;以为…

作者头像 李华
网站建设 2026/10/17 3:06:28

七层协议实战指南:从OSI原理到网络排障的完整拆解

做网络这一行&#xff0c;不管你是搞开发、做运维&#xff0c;还是刚入行的萌新&#xff0c;迟早都要和“七层协议”打交道。面试的时候被问过&#xff0c;排查故障的时候被坑过&#xff0c;看抓包文件的时候更是绕不开它。我当年第一次接触这个概念的时候&#xff0c;觉得这就…

作者头像 李华
网站建设 2026/10/17 3:06:25

从调用链到社交网络:ClawLink多Agent协作层设计实践

在现有的Agent实践中&#xff0c;大多数团队做的是"单兵作战"&#xff1a;一个Agent会调用工具&#xff0c;会写代码&#xff0c;会查数据库&#xff0c;但它很难让另一个Agent替自己干活。我们团队在把几十个Agent铺到业务流程里之后&#xff0c;发现真正卡住效率的…

作者头像 李华
网站建设 2026/10/17 3:06:12

ES+RAG混合检索实战:从选型到落地全记录

做搜索的同学对 ElasticSearch 肯定不陌生&#xff0c;搞大模型应用的基本都在聊 RAG。把这两个词拼在一起&#xff0c;是我最近大半个月一直在折腾的事情。这篇就当作一份初使用报告&#xff0c;把我从选型到落地的完整过程、踩过的坑、最后跑通的链路&#xff0c;一次性讲清楚…

作者头像 李华
网站建设 2026/10/17 3:06:10

Git原理拆解:对象模型、分支指针与reflog,从底层理解版本控制

用了好几年 Git&#xff0c;日常无非就是 add、commit、push、pull、merge 这些命令&#xff0c;一开始觉得挺顺手。可一旦遇到 detached HEAD、rebase 冲突、误操作找回这种事&#xff0c;就只能上网搜命令&#xff0c;搜到哪条试哪条&#xff0c;能恢复算运气好&#xff0c;恢…

作者头像 李华
网站建设 2026/10/17 3:05:56

LLM模型微调完整链路:选型、数据准备与LoRA实战指南

做LLM相关开发的朋友应该都有类似经历&#xff1a;评测榜上分数很高的通用模型&#xff0c;一旦落到自己的业务场景里&#xff0c;说话总是隔靴搔痒。让它写代码它能写&#xff0c;让它按你的流程输出结果就各种跑偏。这时候第一条跳进脑子的路就是微调。但微调这个词&#xff…

作者头像 李华