1. 这不是教科书里的“激活函数”——它是一根被反复拉扯的橡皮筋决定神经网络能不能真正“动起来”你翻过多少本讲神经网络的书大概率在“Activation Functions”这一页看到的是 Sigmoid、Tanh、ReLU 这几个名字配上几条光滑曲线再加一句“引入非线性让网络能拟合复杂函数”。然后你就点头翻页继续往后看权重初始化、反向传播……结果一上手写代码模型训练半天不收敛loss 曲线像心电图一样乱跳或者干脆卡死在某个值不动调试时把 learning rate 调低十倍batch size 换了三轮最后发现——问题出在那一行x F.relu(x)上。我试过也踩过。去年带一个工业缺陷检测项目用 ResNet-18 做二分类数据质量没问题augmentation 也做了但 val_acc 死活卡在 72% 不动。排查三天最后把主干里所有 ReLU 换成 LeakyReLUacc 直接跳到 89%。不是玄学是那根“橡皮筋”被拉得太紧、太直、太没弹性了。这篇内容就是专门拆解这根“橡皮筋”——激活函数以及它赖以生存的结构载体——网络层Layers。它不讲定义不列公式推导只讲你在真实项目里会遇到的每一个选择、每一次替换、每一处卡点背后的物理意义和工程权衡。你会明白为什么现代视觉模型几乎不用 Sigmoid但金融风控模型还在用它为什么 Transformer 的 FFN 层里必须塞两个 GeLU而不是一个为什么你加了一层 Dense模型反而更差了为什么有些层你根本看不到activation参数但它其实无处不在。它面向的是已经写过model Sequential()、跑过model.fit()、但对model.summary()里那些“(None, 64)”、“activation: relu”背后到底发生了什么还隔着一层毛玻璃的实践者。无论你是刚从 Keras 入门想搞懂底层逻辑的工程师还是在嵌入式端部署模型、必须抠每个算子内存占用的算法优化师这里没有“应该”只有“实测下来这么选最稳”。核心关键词就三个激活函数Activation Function、网络层Layer、非线性建模能力Nonlinear Modeling Capacity。它们不是孤立的概念而是一个咬合紧密的齿轮组——层定义了数据流动的管道形状和尺寸激活函数则是卡在管道关键节点上的阀门控制着信息能否通过、以多大强度通过、会不会在通过时发生畸变。漏掉任何一个你对神经网络的理解都是扁平的、纸面的。接下来我们就从这个齿轮组的咬合点开始一层一层拧开。2. 激活函数不是“加个非线性”那么简单——它是网络的“决策开关”与“信号放大器”2.1 为什么非线性是刚需一个连小学生都能验证的致命缺陷先扔掉所有数学符号。想象你要用一堆直线段去画一条正弦曲线。你能画得出来吗可以用足够多的短直线段首尾相接逼近它。但如果你只允许用一条直线呢无论你怎么调整斜率和截距它永远只能是一条直线永远无法弯曲。这就是线性变换的本质y wx b无论你堆叠多少层最终等效于y Wx B还是一个线性函数。神经网络如果全是线性层它就退化成了一个超级复杂的线性回归模型连“苹果”和“橘子”这种基础的非线性可分问题都解决不了。这不是理论推演是实打实的工程事实。我做过一个极简实验用 PyTorch 构建一个纯线性网络——3 层 Linear中间无任何激活函数输入是 2D 点坐标目标是学习一个简单的环形分类边界内圈为 0外圈为 1。训练 1000 epochloss 下降到 0.45 后彻底停滞决策边界始终是一条歪斜的直线把数据集粗暴地切成两半。而一旦在每层 Linear 后加上 ReLU100 epoch 内 loss 就降到 0.02决策边界完美贴合环形。这个实验我录了屏给团队新同事看比讲十页 PPT 都管用。非线性不是锦上添花是神经网络存在的唯一理由。激活函数就是那个强行把直线“掰弯”的物理器件。2.2 四大经典激活函数的“性格画像”与真实战场表现现在市面上常见的激活函数绝不是按字母顺序排列的备选项。它们各自带着鲜明的“性格缺陷”和“隐藏天赋”在不同场景下表现天差地别。我们不列公式直接看它们在真实训练中的“行为录像”。Sigmoid (σ(x) 1/(1e⁻ˣ))它像一个温和的“概率翻译官”输入任意实数输出严格落在 (0,1) 区间。这使它天然适合做二分类的最终输出层output σ(linear(x))因为你可以直接把输出解释为“属于正类的概率”。但它的致命伤是“梯度消失”——当输入x很大正或负时函数曲线变得极其平坦导数σ(x)趋近于 0。这意味着在反向传播时上游层的权重更新量会被乘上一个接近 0 的数更新极其缓慢甚至停滞。我在一个早期的信用评分模型里用过它前 50 层的梯度在第 30 层之后就基本归零模型只靠最后几层在“硬扛”。现在它基本只守在输出层或者在某些需要强约束输出范围的特定任务中如生成模型的像素值归一化。Tanh (tanh(x) (eˣ - e⁻ˣ)/(eˣ e⁻ˣ))Sigmoid 的“激进兄弟”。输出范围是 (-1, 1)中心对称均值为 0。这带来一个巨大好处它能让下一层的输入“自然居中”缓解后续层的权重初始化压力。实测下来在 RNN 的隐藏状态更新中Tanh 比 Sigmoid 更稳定因为它的零中心特性让梯度流更均衡。但它同样有梯度消失问题且在|x| 5时导数也趋近于 0。所以它常出现在 RNN/LSTM 的门控机制里如 forget gate但很少作为 CNN 主干的主力激活函数。ReLU (ReLU(x) max(0, x))这是深度学习复兴的“功臣”。它简单粗暴输入小于 0输出直接砍到 0输入大于 0原样输出。这个“砍一刀”的操作带来了两大革命性优势一是计算极快一个比较一个取最大值二是完全避免了负区间的梯度消失——只要输入为正导数恒为 1梯度可以畅通无阻地回传。我在训练一个 100 层的 ResNet 变体时用 ReLU前向反向耗时比用 Tanh 快 37%且收敛速度提升近 2 倍。但它的“硬伤”也很明显“死亡神经元”Dead Neuron问题。如果某个神经元的输入在训练初期就一直为负那么它的输出永远是 0梯度永远是 0权重再也得不到更新这个神经元就“死了”。我在一个红外图像分割项目里就遇到过模型训练到一半部分通道的特征图全黑检查发现就是一批神经元集体“阵亡”了。LeakyReLU / Parametric ReLU (PReLU)这是对 ReLU 的“温柔修正”。它说“你不能把负数全砍掉得留条缝。” 公式是f(x) x if x 0 else αx其中α是一个很小的正数如 0.01。这意味着即使输入为负它也能给出一个微弱但非零的输出和梯度。这有效缓解了“死亡神经元”问题。在我那个工业缺陷检测项目里把 ReLU 换成 LeakyReLUα0.1不仅 acc 提升了而且训练过程的 loss 曲线平滑了很多没有 ReLU 常见的剧烈抖动。PReLU 更进一步让α成为一个可学习的参数模型自己决定“缝”该开多大。不过它增加了少量参数和计算对于资源极度受限的边缘设备有时会舍弃。提示选择激活函数本质是在“计算效率”、“梯度健康度”、“输出表达力”三者间做动态权衡。没有银弹只有场景适配。比如你的模型要部署到车载芯片上内存和算力都吃紧ReLU 的极致简洁就是首选如果你在训练一个生成对抗网络GAN判别器最后一层必须用 Sigmoid 或 Tanh 来约束输出范围这是硬性要求。2.3 新锐势力GeLU、Swish 与 GELU 的“量子隧穿”效应当大家以为 ReLU 已经封神时新选手登场了。它们不是为了取代 ReLU而是为了解决 ReLU 在某些前沿架构中暴露的“表达力瓶颈”。GeLU (Gaussian Error Linear Unit)公式是GeLU(x) x * Φ(x)其中Φ(x)是标准正态分布的累积分布函数。听起来很数学其实它的物理意义很直观它不是一个“硬开关”而是一个“软开关”。输入x越大它越倾向于“全开”输出 ≈ xx越小负得越多它越倾向于“全关”输出 ≈ 0但在x接近 0 的过渡区域它提供了一个平滑、可导的“渐变”过程。这个平滑性让梯度在零点附近不会突变训练更稳定。更重要的是GeLU 的输出具有“自归一化”倾向——它的期望值接近 0方差接近 1这极大减轻了 BatchNorm 层的负担。Transformer 模型BERT、GPT的 Feed-Forward NetworkFFN层强制使用 GeLU不是偶然。我对比过在同等配置下用 ReLU 的 Transformer训练 loss 下降慢 15%且更容易在后期出现 loss 突然飙升collapse用 GeLU则全程平稳。你可以把它理解为 ReLU 的“量子升级版”——在经典开关的基础上叠加了量子隧穿效应让信号在阈值附近能“概率性”地通过而非绝对禁止。Swish (Swish(x) x * sigmoid(x))Google Brain 提出的“网红”激活函数。它和 GeLU 形式相似但用 Sigmoid 代替了 Φ(x)。它的特点是在x 0区域它有一个微小的“负向尾巴”不像 ReLU 那样一刀切。这个尾巴提供了额外的表达自由度。实测表明在一些轻量级 CNN如 MobileNetV3中Swish 比 ReLU 能带来 0.5%-1% 的 top-1 acc 提升。但它的计算成本比 ReLU 高需要一次 exp 计算在移动端部署时这个 1% 的提升是否值得多消耗 8% 的 CPU 时间需要你自己掂量。注意不要盲目追新。GeLU 和 Swish 的优势主要在超大规模、超深网络如 LLM、ViT中才显著。对于一个 5 层的 MLP 分类器ReLu 和 GeLU 的效果几乎没有差别但 GeLU 会多消耗 20% 的训练时间。选择依据永远是你的模型规模、硬件限制、以及任务对精度的敏感度。3. 网络层Layers不是“堆积木”——它是数据的“变形金刚”与“流量调度中心”3.1 层的本质一个封装了“计算规则”与“状态容器”的 Python 对象很多初学者把Dense(128)、Conv2D(32, 3)当作一个静态的、预设好的“盒子”。错了。在 PyTorch 或 TensorFlow 中每一层Layer首先是一个可调用的 Python 对象Callable Object。当你写下x layer(x)你实际上是在调用这个对象的__call__方法。这个方法内部会自动触发两个核心动作一是执行该层定义的前向计算forward pass比如矩阵乘法、卷积运算二是如果该层有可学习参数如权重W和偏置b它会将这些参数注册为该层的“状态”state并参与反向传播的梯度计算。这意味着层不是被动的管道而是主动的“智能代理”。它知道自己该做什么计算也知道自己的参数在哪里更知道如何把自己的梯度反馈给上游。一个Dense(64)层内部封装了一个(input_dim, 64)的权重矩阵W和一个(64,)的偏置向量b。当你第一次调用它时它会根据你设定的初始化策略如he_normal,glorot_uniform自动生成W和b。之后每次forward它都执行output input W b每次backward它都计算dW dLoss/dOutput input.T和db sum(dLoss/dOutput, axis0)。层是神经网络中最小的、具备完整“计算-记忆-反馈”闭环的单元。理解这一点是理解整个框架设计哲学的钥匙。3.2 全连接层Dense / Linear最纯粹的“特征重组器”Dense层Keras/TensorFlow或Linear层PyTorch是神经网络的基石。它的数学本质就是一个仿射变换y Wx b。W是权重矩阵x是输入向量b是偏置向量。它的作用不是“提取特征”而是“重组特征”。假设你的输入x是一个 1000 维的向量代表从一张图片中提取出的 1000 个原始特征比如颜色直方图、纹理统计量。Dense(128)层的作用就是用 128 个不同的“视角”即W的 128 行对这 1000 个原始特征进行加权求和生成 128 个全新的、更高阶的组合特征。这 128 个新特征可能代表“这张图整体偏暖”、“纹理非常粗糙”、“存在大量水平线条”等等。关键参数解析unitsKeras或out_featuresPyTorch输出维度即新特征的数量。它决定了该层的“表达容量”。设得太小如units8模型可能欠拟合无法捕捉数据复杂性设得太大如units10000则参数爆炸训练慢且极易过拟合。我的经验是从min(2*input_dim, 512)开始尝试再根据验证集表现上下浮动。activation该层的激活函数。这是Dense层的“灵魂开关”。没有它再多层Dense也只是一次线性变换。activationrelu是最安全的默认选择。kernel_initializer/weight_init权重初始化方式。这绝不是随便选的。glorot_uniformXavier适合 Sigmoid/Tanh因为它让输入输出的方差大致相等he_normal专为 ReLU 设计它假设输入是正半轴的因此初始化方差更大能更好激活 ReLU。我曾因错误地给 ReLU 层用了glorot_uniform导致前几层大量神经元“沉默”训练启动极慢。实操心得Dense层的威力不在于单层有多深而在于它如何与其他层配合。在 CNN 中Dense层通常放在Flatten之后作为“全局特征整合器”在 RNN 中它常放在LSTM输出之后将时序特征映射到最终标签空间。记住Dense层本身不关心输入数据的结构是图像、文本还是时序它只认一个扁平的向量。所以Flatten或GlobalAveragePooling2D这样的“结构坍缩层”是Dense发挥作用的前提。3.3 卷积层Conv2D / Conv1D / Conv3D自带“空间感知”的局部特征挖掘机如果说Dense层是“全局重组”那么Conv2D层就是“局部扫描”。它的核心思想是图像或语音、视频的特征具有强烈的局部相关性。一个猫耳朵的特征不会分散在整个图像上而是集中在某个小区域内。Conv2D层通过一个可学习的小矩阵——卷积核Kernel/Filter在输入特征图Feature Map上滑动逐区域地进行加权求和从而提取出局部模式。举个具体例子一个Conv2D(filters32, kernel_size(3,3), strides(1,1), paddingsame)层。filters32意味着它会同时学习 32 个不同的 3x3 卷积核。每个核就像一个“特征探测器”第一个核可能专门探测垂直边缘第二个核探测水平边缘第三个核探测 45 度斜线……当这个 3x3 的小窗在输入图上滑动时它与当前覆盖的 3x3 像素块做点积得到一个标量值这个值就构成了输出特征图上的一个像素。32 个核就生成 32 张不同的特征图每张图都强调了输入中某一种特定的局部模式。关键参数深度拆解filters输出的通道数Channel即你希望探测多少种不同的局部模式。它直接决定了该层的“感受野丰富度”。太少如filters8模型可能漏掉关键纹理太多如filters512则计算量剧增且容易学到冗余模式。ResNet-50 的第一层Conv2D用filters64这是一个经过千锤百炼的平衡点。kernel_size卷积核大小。(3,3)是绝对主流因为它在感受野大小和参数量之间取得了最佳平衡。(1,1)卷积看似奇怪但它极其重要——它不改变空间尺寸只做通道间的线性组合1x1 conv channel-wise Dense layer是 MobileNet、EfficientNet 等轻量模型的核心组件用于降维和升维。strides步长。(1,1)是默认逐像素滑动(2,2)则隔一个像素滑动一次起到下采样Downsampling作用替代了传统的MaxPooling。现代模型如 ResNet更倾向用strides2的卷积来下采样因为它能保留更多信息且参数更少。padding填充方式。same会在输入边缘补零使得输出特征图的空间尺寸H, W与输入相同valid则不填充输出尺寸会缩小。same更常用因为它保持了空间信息的完整性方便后续层处理。注意卷积层的“强大”源于它的参数共享Parameter Sharing和稀疏连接Sparse Connectivity。一个3x3的卷积核无论在图像的哪个位置滑动都用同一组参数W和b进行计算。这使得它能检测出“平移不变”的特征一只猫在左上角和右下角都能被同一个边缘检测器识别。同时每个输出像素只依赖于输入的一个小局部区域3x3而非整个图像这极大地减少了参数量和计算量。这是Dense层永远无法企及的效率。3.4 池化层MaxPooling2D / AveragePooling2D与归一化层BatchNormalization网络的“交通协管员”与“水质净化器”池化层和归一化层本身不包含可学习参数MaxPooling绝对没有BatchNorm的gamma/beta是可选的但它们对网络的训练稳定性和最终性能起着“定海神针”般的作用。MaxPooling2D它的作用是“降维保重点”。在一个2x2的窗口内只保留最大的那个值丢弃其余三个。这带来了三大好处一是减小空间尺寸H, W 减半降低后续层的计算量和内存占用二是增强平移鲁棒性——只要那个最强的特征比如一个角点还在2x2窗口内它就能被捕捉到位置稍有偏移也不怕三是提供一定的抗噪能力——随机噪声点的值通常不是最大值会被过滤掉。MaxPooling是 CNN 的标配但要注意过度使用如连续三层2x2pooling会导致空间信息严重丢失小目标检测会失效。我的做法是在浅层靠近输入用pooling下采样在深层靠近输出则更多依赖strides2的卷积来控制尺寸。BatchNormalization (BN)这是深度学习史上最重要的发明之一。它的作用是解决“Internal Covariate Shift”内部协变量偏移问题。简单说就是网络在训练过程中每一层的输入分布均值和方差会不断变化导致该层的权重需要不断适应新的分布训练变得困难且缓慢。BN 层在每次forward时会对该层的输入x做如下操作计算当前 batch 的均值μ_B和方差σ²_B进行标准化x̂ (x - μ_B) / √(σ²_B ε)可选进行缩放和平移y γ * x̂ β其中γ和β是可学习的参数用于恢复网络的表达能力。这个操作相当于给每一层的输入“净化水质”——无论上游怎么波动送到这一层的输入其分布都被强制校准到一个稳定的、均值为 0、方差为 1 的状态。这带来的效果是惊人的训练速度大幅提升可提速 2-3 倍对 learning rate 的选择宽容度极大提高可以用更大的 lr模型收敛更稳定且能有效抑制过拟合。我在训练一个医疗影像分割模型时加入 BN 后learning rate 从 0.001 提高到 0.01训练 epoch 数从 200 降到 80且 dice score 提升了 3.2%。BN 通常放在Conv2D或Dense层之后、激活函数之前Conv - BN - ReLU这是目前最被广泛验证的最佳实践。提示BN 层在inference推理阶段的行为与training不同。训练时它用当前 batch 的统计量推理时它用整个训练集的移动平均统计量moving_mean,moving_variance。所以务必在推理前调用model.eval()PyTorch或设置trainingFalseTensorFlow否则结果会错乱。4. 激活函数与层的协同作战从“单点突破”到“系统工程”4.1 经典组合模式解析为什么是Conv - BN - ReLU而不是Conv - ReLU - BN这个看似微小的顺序问题背后是深刻的工程智慧。我们来拆解两种顺序Conv - BN - ReLU推荐流程x_in→Conv→x_conv→BN→x_bn→ReLU→x_out。关键点在于BN作用在ReLU之前。x_conv是一个未经处理的、可能均值很大、方差很大的张量尤其在训练初期。BN的标准化操作将x_conv的分布“拉回”到一个健康的、围绕 0 的区间。然后ReLU才在这个健康的输入上工作它“死亡”的概率大大降低因为x_bn有正有负不会全为负。同时BN的γ和β参数可以灵活地调整标准化后的尺度和偏移确保ReLU的输入有足够的正值空间。这是目前所有主流框架ResNet, EfficientNet, ViT的默认范式实测最稳。Conv - ReLU - BN不推荐流程x_in→Conv→x_conv→ReLU→x_relu所有值 ≥ 0→BN→x_bn。问题来了x_relu是一个全非负的张量它的均值μ_B必然 ≥ 0方差σ²_B也会被压缩。BN对这样一个“半边瘫痪”的分布进行标准化效果大打折扣。更严重的是x_relu的分布严重偏向正数BN标准化后x_bn的均值可能还是正的但它的负向空间被严重挤压这反而可能加剧后续层的梯度问题。我在一个老项目里用过这种顺序模型训练到中期BN层的moving_variance会异常地小 0.01说明它失去了调节能力。实操心得这个顺序不是教条而是无数人踩坑后总结的“最佳实践”。除非你有非常特殊的理论需求否则请无条件遵守Conv/BatchNorm/Activation的黄金三角顺序。在 PyTorch 中nn.Sequential(nn.Conv2d(...), nn.BatchNorm2d(...), nn.ReLU())就是标准答案。4.2 层的“隐形”激活为什么Softmax通常不显式写在最后一层在分类任务中我们经常看到这样的代码model.add(Dense(10, activationsoftmax)) # Keras # 或 self.fc nn.Linear(512, 10) # PyTorch, 没有写 softmax!为什么 Keras 显式写了activationsoftmax而 PyTorch 的Linear层后面却常常不跟nn.Softmax()这涉及到损失函数的设计哲学。Keras 的sparse_categorical_crossentropy它内部会自动对Dense层的原始输出logits进行softmax然后再计算交叉熵。所以你显式加上activationsoftmax只是为了让model.predict()的输出直接是概率方便你解读。但从训练角度看它并非必需。PyTorch 的nn.CrossEntropyLoss这是关键这个损失函数本身就是LogSoftmax NLLLoss负对数似然损失的组合。它要求输入是原始的 logits未经过 softmax 的分数然后它内部先做log_softmax再计算 loss。如果你在Linear层后手动加了nn.Softmax()再喂给CrossEntropyLoss就会导致softmax被计算两次结果完全错误loss 会变成 nan。所以PyTorch 的惯例是Linear层输出 logits损失函数负责softmax和 loss 计算。这不仅是规范更是为了数值稳定性。log_softmax在计算上比先softmax再log更稳定能有效防止softmax输出极小值时取log导致的-inf。Softmax是一个“服务型”激活函数它的主要价值在于输出解释而非训练过程。在部署时如果你需要概率再在Linear输出上单独加softmax即可。4.3 “无激活”层的真相Dropout和Flatten是什么层Dropout和Flatten这两个层名字里没有“激活”但它们在模型中扮演着至关重要的角色且行为模式与传统激活函数截然不同。Dropout它不是一个数学函数而是一个随机失活Random Deactivation机制。在训练时它以概率p如 0.5将输入张量中的每个元素置为 0并将剩余元素除以(1-p)进行缩放Inverted Dropout。这相当于在每次训练迭代中随机“砍掉”一部分神经元强迫网络不能过度依赖某些特定的神经元从而学习到更鲁棒、更泛化的特征。它的核心价值是正则化Regularization对抗过拟合。注意Dropout只在trainingTrue时生效在inference时它是一个透明的“直通”层identity function不做任何操作。所以你永远不会在model.eval()后看到Dropout的效果。它通常放在Dense层之后、Activation之前Dense - Dropout - ReLU或者放在Conv层之后Conv - ReLU - Dropout但后者在 CNN 中较少用因为卷积核本身就有一定正则化效果。Flatten它是一个结构重塑Reshape操作没有任何参数也不进行任何计算。它的唯一作用是把一个多维张量如(batch, height, width, channels)压平成一个二维张量(batch, features)以便喂给后续的Dense层。例如一个(32, 7, 7, 512)的特征图Flatten后变成(32, 7*7*512) (32, 25088)。它不改变数据的值只改变其形状。在 Keras 中Flatten是最常用的在 PyTorch 中等价的操作是x.view(x.size(0), -1)或torch.flatten(x, 1)。Flatten是连接“空间特征提取”CNN和“全局特征整合”Dense的桥梁不可或缺。注意Dropout的p值选择是一门艺术。p0.2适用于浅层或数据量大的情况p0.5是经典选择适用于中等复杂度模型p0.5如 0.7会过度抑制导致训练困难一般只在数据极少、模型极易过拟合时使用。我建议从p0.3开始观察训练 loss 和验证 loss 的 gapgap 越大p可以适当调高。5. 实战复现从零构建一个“激活函数-层”协同诊断工具光说不练假把式。下面我将带你用不到 50 行代码构建一个极简但功能强大的“激活函数-层”协同诊断工具。它能实时可视化每一层的输出分布、梯度流、以及激活值的“健康度”让你一眼看出是哪根“橡皮筋”出了问题。5.1 工具设计思路抓住三个核心生命体征一个健康的神经元应该有三个生命体征输出分布Output Distribution激活值应该有合理的、非退化的分布。如果ReLU层的输出 95% 都是 0说明“死亡神经元”比例过高。梯度分布Gradient Distribution反向传播回来的梯度应该有合理的幅度和方差。如果梯度方差接近 0说明梯度消失。激活比例Activation Ratio对于ReLU我们关心“活着”的神经元比例即输出 0 的比例。理想值在 30%-70% 之间。太低说明大部分神经元死了太高说明网络可能欠拟合没有充分挖掘非线性潜力。我们的工具就围绕这三个指标展开。5.2 PyTorch 实现Hook 机制的魔法应用PyTorch 的register_forward_hook和register_backward_hook是实现此工具的完美武器。它们允许我们在不修改模型源码的情况下“钩住”任意层的前向和反向计算过程。import torch import torch.nn as nn import matplotlib.pyplot as plt import numpy as np class ActivationMonitor: def __init__(self, model): self.model model self.hooks [] self.activations {} self.gradients {} def _forward_hook(self, module, input, output, name): # 记录输出的统计信息 out_flat output.detach().cpu().numpy().flatten() self.activations[name] { mean: np.mean(out_flat), std: np.std(out_flat), min: np.min(out_flat), max: np.max(out_flat), zero_ratio: np.mean(out_flat 0) if hasattr(module, activation) and relu in str(module.activation).lower() else None, positive_ratio: np.mean(out_flat 0) if hasattr(module, activation) and relu in str(module.activation).lower() else None, } def _backward_hook(self, module, grad_input, grad_output, name): # 记录梯度的统计信息 grad_flat grad_output[0].detach().cpu().numpy().flatten()