【RustyML入门】3.8. 正则化与归一化层

3.8. 正则化与归一化层

本页讲的这些层,与其说是在学习某种映射,不如说是在重塑流经网络的信号:dropout 及其空间变体、2 个高斯噪声层,以及 4 个归一化层(batch、layer、group、instance)。它们都住在 rustyml::neural_network::layers::regularization 里,原因只有一个:每一层在训练模式下的行为都和推理模式下不同,而 crate 把这个区别拆解成了一套共享机制。先把这套机制弄清楚,本页余下的内容才只是细节;跳过它,batch normalization 尤其容易悄悄算出错误的数字。

本页的每个层都能用同一个 .add(...) 调用接入 Sequential 模型,就像全连接层与激活函数里那样。每个构造函数都返回 Result,原因见错误处理

use rustyml::neural_network::layers::*; 就能导入全部内容。它重新导出了 DropoutSpatialDropout1D/2D/3DGaussianNoiseGaussianDropoutBatchNormalizationLayerNormalization(连同它的 LayerNormalizationAxis)、GroupNormalizationInstanceNormalization。和这个 crate 里到处一样,Tensor 就是 ndarray::ArrayD<f32>:单精度、动态秩。

3.8.1. 训练模式与推理模式

每个依赖模式的层都持有一个私有的 training: bool 标志,并提供 2 种前向执行方式。Layer trait 定义了这两个方法:

rust,ignore 复制代码
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error>;   // 记录缓存,尊重标志
fn predict(&self, input: &Tensor) -> Result<Tensor, Error>;       // 始终走求值路径,不写缓存

forward 接收 &mut self,读取 training 标志,并存下反向传播需要的一切:掩码、batch 统计量,或者噪声抽样。predict 接收 &self,始终走推理路径,什么也不记录。这正是编译好的模型能够跨线程为并发请求提供服务的原因。用 set_training_if_mode_dependent(is_training)(trait 方法)或固有方法 set_training(is_training) 来翻转这个标志。那些不依赖模式的层,比如 Dense、激活函数和池化层,两个方法都继承了空操作实现。

Sequential 模型内部,你很少需要自己去碰这个标志。fitfit_with_batches 会把每一层设为训练模式并调用 forwardpredictevaluate 则改为调用每一层的 predict 方法。要留意一个坑:手动调用 forward 来做推理。一个标志仍为 trueBatchNormalization 层,会拿你的测试 batch 去算 batch 统计量,还会改动它的滑动平均。凡是不属于训练步骤的场合,都调用 model.predict(...),或者把标志设为 false。下表精确列出了每一族层在各个模式下的行为:

训练模式下的 forward 推理 / predict 下的 forward 推理模式下的 backward
DropoutSpatialDropout* 丢弃一部分单元,对存活的重新缩放 恒等(原样透传) 梯度原样透传
GaussianNoise 加上 N(0, stddev^2) 恒等 梯度原样透传
GaussianDropout 乘以 N(1, sigma) 恒等 梯度原样透传
BatchNormalization batch 统计量归一化,更新滑动统计量 滑动统计量归一化 梯度原样透传
LayerNormalizationGroupNormalizationInstanceNormalization 用当前输入算出的统计量归一化 用当前输入算出的同样统计量 梯度原样透传

注意最后一行。layer、group 和 instance normalization 在任何模式下都从当前输入计算统计量,所以无论标志开还是关,它们的 forward 输出都一样,predictforward 逐位相等。只有它们的反向传播依赖模式:在推理模式下,它原样返回上游梯度,而不去计算真正的梯度。batch normalization 是这里唯一持有状态(滑动均值与方差)的层,也是唯一一个在两种模式下确实算出不同结果的层。

这道分界也解释了同一个模型为什么会报出 2 个不一致的损失。fitfit_with_batches 返回的 History 里,那些逐 epoch 的数字来自训练模式的前向传播。训练模式下,dropout 会把一部分激活置零并重新缩放存活的部分,batch norm 则用各自 batch 的统计量做归一化,并把它们并进滑动平均。evaluate 走的是推理路径:dropout 是恒等映射,batch norm 只读滑动统计量、不去改动它们。只要模型里有这两类层中的任何一个,这 2 个数字就不会一致,而且哪个都不算错:训练时的那个数字,是一张被刻意打了折扣的网络的损失,测量于该批次自己的权重更新之前;evaluate 给出的才是你手上这张网络的真实分数。挑选 checkpoint、判断早停,都应该看 evaluate。它在训练的任何时刻都能放心调用:它借的是 &self,什么都不更新,也不抽随机数,所以既不会消耗掉一个 dropout 掩码,也不会打乱它正在度量的这次训练的洗牌顺序。

3.8.2. Dropout

Dropout::new(rate, input_shape) 构造经典的 dropout 层。rate 是要置零的单元比例,必须落在闭区间 [0.0, 1.0] 内,否则调用会返回 Error::InvalidParameterinput_shape 会对照每个输入做校验,但只校验秩和逐样本的那些轴。第 0 维是 batch 轴,它随调用 forward 的人而变,所以校验会跳过它。因此一个声明为 vec![4, 8] 的层接受任意大小的 batch,却仍会拒掉宽度不是 8 的行。通配的 vec![] 会把形状校验整个关掉。

它的实现是 inverted dropout :训练 forward 时,它抽一个均匀掩码,以概率 1 - rate 保留每个单元,再把存活的单元乘上 1 / (1 - rate),从而让激活的期望值保持不变。这个缩放正是关键所在:它让推理成为一次纯粹的恒等操作(predict 原样返回输入),服务端代码无需再记着做任何缩放。2 个边界值会让这套逻辑短路:rate == 0.0 是恒等,rate == 1.0 把每个单元都置零。

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    // 50% 的丢弃率,期望 [4, 8] 的输入。给掩码播种,让这次运行可复现。
    let mut dropout = Dropout::new(0.5, vec![4, 8]).unwrap().with_random_state(7);
    let input = Array::ones((4, 8)).into_dyn();

    // 训练:大约一半单元变为 0,存活的变为 1/(1-0.5) = 2.0。
    dropout.set_training_if_mode_dependent(true);
    let train_out = dropout.forward(&input).unwrap();
    let kept = train_out.iter().filter(|&&v| v != 0.0).count();
    println!("kept {kept}/{} units, each rescaled to 2.0", input.len());

    // 推理:inverted dropout 让这个层成为恒等映射。
    dropout.set_training_if_mode_dependent(false);
    assert_eq!(dropout.forward(&input).unwrap(), input);

    // predict() 无论标志如何都走求值路径,且从不缓存掩码。
    assert_eq!(dropout.predict(&input).unwrap(), input);
}

掩码抽自每层独立的 StdRng。默认情况下 Dropout::new 用全局种子给它播种(没设全局种子就取自系统熵);with_random_state(seed) 会用确定的种子重新播种。由于 RNG 每抽一次就会前移,连续 2 次训练 forward 调用会产生不同的掩码。播种固定的是跨进程运行的序列 ,而不是两次调用之间的相等性。全局种子如何贯穿每个随机化组件,见可复现性与随机种子。Dropout 没有可训练参数。在 forward 之前调用 backward(此时没有缓存掩码)会返回 NnError::ForwardPassNotRun。在推理模式下,或者 rate == 0.0 时,反向传播只是把梯度原样透传。

3.8.3. 面向特征图的空间 Dropout

普通 dropout 并不适合卷积特征图。一个通道内相邻的像素高度相关,所以零星地把单个元素置零几乎抹不掉多少信息:被丢掉的像素值基本能从邻居那里恢复回来,正则化效果也就被稀释了。SpatialDropout1DSpatialDropout2DSpatialDropout3D 的做法是一次丢弃一整个通道。某个通道被丢弃时,它所有的空间位置会一起归零,这就逼着网络不去依赖任何单一的特征图。这对应了 Keras 的 SpatialDropout* 系列层。

这 3 个层只在期望的秩上有区别,三者都采用 channels-last 布局。SpatialDropout1D 期望三维 (batch, length, channels) 输入。SpatialDropout2D 期望四维 (batch, height, width, channels) 输入。SpatialDropout3D 期望五维 (batch, depth, height, width, channels) 输入。秩不对会返回 Error::InvalidInput

在内部,每个层为每个 (batch, channel) 组合抽一个保留/丢弃值:一个小小的 [batch, channels] 掩码。它对整个通道套用同样的 1 / (1 - rate) inverted-dropout 缩放,所以这个层从不会构建一个满尺寸的掩码。构造方式、用 with_random_state 播种、边界行为,以及 ForwardPassNotRun 的约定,都跟普通 Dropout 一致。错误里甚至会点明具体的层:在 SpatialDropout2D 上过早调用 backward 会返回 ForwardPassNotRun("SpatialDropout2D")

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    // (batch=1, height=4, width=4, channels=8):整个通道作为一个整体被丢弃。
    let mut sd = SpatialDropout2D::new(0.5, vec![1, 4, 4, 8]).unwrap().with_random_state(3);
    sd.set_training_if_mode_dependent(true);

    let input = Array::ones((1, 4, 4, 8)).into_dyn();
    let out = sd.forward(&input).unwrap();

    // 同一通道内的每个位置共享 1 个值:0.0(被丢弃)或 2.0(保留并重新缩放)。
    for c in 0..8 {
        let first = out[[0, 0, 0, c]];
        assert!((0..4).all(|h| (0..4).all(|w| out[[0, h, w, c]] == first)));
        println!("channel {c}: all 16 spatial positions hold {first}");
    }
}

3.8.4. 高斯噪声与高斯 Dropout

GaussianNoiseGaussianDropout 靠注入噪声而非置零来做正则化。GaussianNoise::new(stddev, input_shape)加性 的:训练时它加上零均值的 N(0, stddev^2) 噪声,output = input + noise。这是一种数据增强风味的正则化:它扰动输入却不改变其期望值,但确实抬高了方差。stddev 足够大时,一个正的输入也可能变负。stddev 必须非负且有限。构造时会拒绝负值、NaNInf。这项检查是有意为之:如果不做检查,采样器会在第一次前向调用时 panic。它的反向传播在任何模式下都是纯透传,因为噪声不依赖于输入,所以 d(x + noise)/dx = 1。它不缓存任何东西,也从不返回 ForwardPassNotRun

GaussianDropout::new(rate, input_shape)乘性 的,跟普通 dropout 更接近。它给每个输入乘上一个来自 N(1, sigma) 的抽样,其中 sigma = sqrt(rate / (1 - rate))。这个均值为 1 的噪声让 E[output] = input。这跟 inverted dropout 保持期望不变是同一个思路,只不过用连续的乘数代替了硬性置零。这里 rate 必须落在 [0.0, 1.0),不含 1,因为 rate 趋近 1 时 sigma 会发散。跟 GaussianNoise 不同,这个层会缓存确切的噪声抽样,好让反向传播复用它。因为 y = x * noise,所以 dx = grad * noise。在任何训练前向之前调用 backward 会返回 ForwardPassNotRun。两个层在推理时都是恒等映射,stddevrate 为 0 时也一样。两个层都没有可训练参数。

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    let input = Array::from_elem((2, 4), 3.0_f32).into_dyn();

    // 加性:output = input + N(0, 0.5^2)。均值保持,方差增加。
    let mut noise = GaussianNoise::new(0.5, vec![2, 4]).unwrap().with_random_state(1);
    noise.set_training_if_mode_dependent(true);
    let noisy = noise.forward(&input).unwrap();

    // 乘性:output = input * N(1, sqrt(rate/(1-rate)))。E[output] 仍等于 input。
    let mut gdrop = GaussianDropout::new(0.3, vec![2, 4]).unwrap().with_random_state(1);
    gdrop.set_training_if_mode_dependent(true);
    let scaled = gdrop.forward(&input).unwrap();

    println!("additive[0] = {}, multiplicative[0] = {}", noisy[[0, 0]], scaled[[0, 0]]);

    // 两者在服务时都是恒等映射。
    noise.set_training_if_mode_dependent(false);
    gdrop.set_training_if_mode_dependent(false);
    assert_eq!(noise.forward(&input).unwrap(), input);
    assert_eq!(gdrop.forward(&input).unwrap(), input);
}

3.8.5. 归一化层速览

这 4 个归一化层共用一副骨架:每一层都减去一个均值,再除以在某组轴上算出的标准差,然后套一个可学习的、逐通道的仿射变换 gamma * x_normalized + betagamma 初始化为全 1,beta 初始化为全 0。优化器把两者都当作可训练参数,并标记为 no-decay,于是权重衰减会跳过它们:归一化的缩放/平移不该被拉向零。每个层还接收一个 epsilon 值(通常是 1e-5),加在平方根下面以保证数值稳定。这个 epsilon 也让零方差输入产生有限的全零输出,而不是 NaN。区分这 4 个层的,只在于统计量在哪些轴上归约 。设想一个形状为 [N, ...spatial, C] 的输入,batch N 在最外层,通道 C 在最内层:

均值/方差归约的轴 每个统计量对应 依赖 batch? gamma/beta 长度
BatchNormalization batch N(及所有空间轴) 通道 C
LayerNormalization 归一化轴(默认最后一个) 除该轴外的一切 归一化轴的大小
GroupNormalization 每个样本内一组通道 + 空间轴 (样本, 组) C
InstanceNormalization 仅空间轴,逐样本逐通道 (样本, 通道) C

把"依赖 batch?"这一列当作选层的实际判据。batch norm 通过共享统计量把样本耦合在一起,这既让它有效,也让它在小 batch 下变得脆弱。另外 3 个层各自独立地归一化每个样本,batch 大小对它们没有影响。这 4 个层都保持输入形状不变。下面这些形状校验一个都不能少:group 和 instance normalization 要求秩至少为 3(二维输入会返回 Error::InvalidInput),batch norm 则接受二维及以上。

3.8.6. BatchNormalization

BatchNormalization::new(input_shape, momentum, epsilon) 的参数顺序是先形状,再 2 个标量。input_shape 必须非空,否则调用会返回 Error::EmptyInputmomentum 必须落在 [0.0, 1.0] 之间。epsilon 必须为正。第 0 维是 batch,最后一维 是通道/特征轴。逐通道参数的长度是 input_shape.last()

对二维 [N, C] 输入,这就是普通的逐特征 batch norm。对秩为 3 或更高的 [N, ...spatial, C] 输入,统计量会在 batch 每个空间位置上归约,于是每个通道对应 1 份均值、方差、gamma 和 beta:即"spatial" batch norm,与 Keras 的 axis=-1 默认一致。两种情况走的是同一条代码路径。因为通道轴在最内层,[N, ...spatial, C] 本身就是 逐通道折叠要读的那个 [M, C] 矩阵。把前导轴合起来,只是对同一批字节的重新解读,而不是一次 reshape。

一维的 input_shape,比如 vec![4],是一个没有通道轴的特例。它用长度为 1 的标量参数,广播到整个输入上。

让 batch norm 依赖模式的那份状态,是一对滑动统计量。训练 forward 调用时,这个层先用当前 batch 的均值和方差做归一化,再按 running = running * momentum + batch * (1 - momentum) 更新滑动统计量。这是 Keras 的约定:像 0.99 这样高的 momentum,会重仓历史,让滑动估计缓慢移动。PyTorch 用的是相反的约定:它的 momentum 加权的是 batch。直接从 PyTorch 代码里照搬一个值过来,在这里会得到相反的行为。

momentum == 0.0 会把历史彻底丢掉,滑动统计量等于最后一个 batch 的。momentum == 1.0 会把滑动统计量冻结在初始值(均值 0、方差 1)。这会悄悄让求值模式下的归一化变成一次近乎恒等的操作,通常是个失误。推理时,forwardpredict 用的是滑动统计量而非 batch 本身,这正是为什么在训练模式下把测试 batch 喂进 forward 会毁掉模型。

它相对激活函数放在哪里是有讲究的,而 Dense 融合激活的设计限定了你的选择。原始的 batch norm 论文把它放在非线性之前Dense::new(.., .., Activation::ReLU) 把激活折进了线性层,所以要做到"BN 在激活之前",就需要一个线性的 Dense 层,再接 BatchNormalization,再接一个独立的激活层:

rust,ignore 复制代码
use rustyml::neural_network::layers::activation::relu::ReLU;
// Dense(线性) -> BatchNorm -> ReLU (BN 在非线性之前,论文顺序)
model
    .add(Dense::new(4, 8, Activation::Linear).unwrap())
    .add(BatchNormalization::new(vec![6, 8], 0.99, 1e-5).unwrap())
    .add(ReLU::new());

常见的另一种做法是先做激活、再归一化,也就是 Dense::new(.., .., Activation::ReLU) 后面接一个 BatchNormalization。两种顺序都能训练成功。选一种,然后保持一致。下面的例子展示一个完整可运行的模型,用的是更简单的"激活融进 Dense"写法:

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::sequential::Sequential;
use rustyml::neural_network::optimizers::Adam;
use rustyml::neural_network::losses::MeanSquaredError;
use ndarray::Array;

fn main() {
    // BatchNormalization 形状 [6, 8] 里的 6 不受校验,真正被校验的只有末尾的 8。
    let x = Array::ones((6, 4)).into_dyn();
    let y = Array::ones((6, 1)).into_dyn();

    let mut model = Sequential::new();
    model
        .add(Dense::new(4, 8, Activation::ReLU).unwrap())
        .add(BatchNormalization::new(vec![6, 8], 0.9, 1e-5).unwrap())
        .add(Dropout::new(0.3, vec![6, 8]).unwrap())
        .add(Dense::new(8, 1, Activation::Linear).unwrap())
        .compile(
            Adam::new(0.01, 0.9, 0.999, 1e-8, 0.0).unwrap(),
            MeanSquaredError::new(),
        );

    // fit() 走训练路径(batch 统计量、更新滑动统计量、dropout 开启)。
    // predict() 走求值路径(滑动统计量、dropout 为恒等)。
    model.fit(&x, &y, 3).unwrap();
    println!("prediction shape: {:?}", model.predict(&x).unwrap().shape());
}

input_shape 的第一维只是记录一个 batch 大小,这个层并不会去校验它。一个为 [6, 8] 声明的 BatchNormalization 层接受任意大小的 batch。所以 fit_with_batches 用任何 batch_size 都能训练,包括数据集除不尽时剩下的那个偏短的末尾批次。这个短批次也不会把该 epoch 报出的损失带偏:fit_with_batches 按每个批次实际装了多少样本给它加权,而不是每批一票。所以不管数据怎么切,History 里的那一项始终是整个数据集上的逐样本平均损失,这也是 Keras 的算法。

真正会被校验的是秩和每一条逐样本的轴。所以 [n, 16] 的输入,或者秩不对的输入,仍然会返回 Error::ShapeMismatch。这也意味着 vec![] 通配并不是让某个层挺过小批量训练的手段,只有当逐样本形状本身会变化时才该用它。

你也可以用 set_weights(gamma, beta, running_mean, running_var) 直接注入已知权重。形状不匹配会返回 NnError::WeightShape。加载模型时正是靠这个来恢复它的推理统计量。见下一节以及权重保存与加载

3.8.7. LayerNormalization

LayerNormalization::new(input_shape, epsilon)每个样本内部 跨特征做归一化。它没有 batch 耦合,也没有滑动统计量。这正是它成为循环模型首选归一化方式的原因。它也适合 batch 小、可变,或者大小为 1 的任何场景。batch norm 的统计量在寥寥几个样本上会变得嘈杂或失去意义,layer norm 则不受影响,因为每个样本都各自独立。它的 forward 输出在训练和推理下相同,所以 predict 等于 forward。只有它的反向传播依赖模式。

默认情况下它归一化最后一个(特征)维度。with_normalized_axis(...) 可以改变这一点,它接收一个 LayerNormalizationAxis 值:Default(最后一个轴)、Custom(axis)(另指一个单独的轴),或者 Multiple(vec![...])(一次性在多个轴上联合归一化)。Multiple 是 Keras 风格的:统计量横跨这些轴组合出的元素,gamma/beta 变成它们乘积长度的一维。改动轴会把 gammabeta 调整到匹配的大小,所以要在赋权重之前调用这个方法。这个层会拒绝空的、有重复的,或者越界的轴列表。

一个非末尾的 Custom 轴依然能正确工作,只是它走的是一条更慢的跨步路径,而不是融合的行级路径。一个轴不是已经处于末尾且按序排列的 Multiple 列表,同样能正确工作:它会先转置输入,把要归一化的轴聚到一起,跑融合的行级路径,再把结果转置回去。这两种情况都比默认的末轴路径开销更大,但都不会改变结果。

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    let input = Array::from_shape_vec((2, 4), vec![1.0, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0])
        .unwrap()
        .into_dyn();

    // 默认:每一行(最后一个轴)被归一化到均值 0、方差 1。
    let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
    let out = ln.forward(&input).unwrap();

    // Custom(0):改为沿每一列(跨 batch 轴)向下归一化。
    let mut ln_cols = LayerNormalization::new(vec![2, 4], 1e-5)
        .unwrap()
        .with_normalized_axis(LayerNormalizationAxis::Custom(0))
        .unwrap();
    let out_cols = ln_cols.forward(&input).unwrap();

    println!("row-norm {:?}, col-norm {:?}", out.shape(), out_cols.shape());

    // 统计量来自当前输入,所以 predict() 精确重现 forward()。
    assert_eq!(ln.predict(&input).unwrap(), out);
}

3.8.8. GroupNormalization 与 InstanceNormalization

这 2 个层填补了中间地带:卷积模型里 batch 太小、batch norm 表现不佳的场合。常见的例子包括检测与分割骨干网络,以及生成模型。GroupNormalization::new(input_shape, num_groups, epsilon) 把通道切成 num_groups 个连续的组,在每个样本内部按组做归一化。只有 1 个组时,它就变成了覆盖全部通道的 layer norm;组数和通道数相等时,它就变成了 instance norm。InstanceNormalization::new(input_shape, epsilon) 单独归一化每个 (sample, channel) 平面,这是风格迁移的标准选择,因为它剥掉了每个实例内部的对比度,同时不动 batch 之间的关系。

两个层都要求秩至少为 3。两者都像其他空间层一样,把通道轴当作最末轴。跟 layer norm 一样,两者都不带滑动统计量,在前向方向上都与模式无关。它们的逐组均值和方差来自对数据的单一遍扫描。这 2 个累加器会累计"相对于取自数据本身的某个值的偏差"之和,于是方差不需要第二遍走样本就能算出来。这样做也避开了均值远大于离散度时,朴素的 E[x^2] - E[x]^2 公式会遭遇的灾难性抵消。

instance norm 就是 num_groups 等于通道数的 group norm,crate 也正是这么实现的。当两者配置成对应关系时,这 2 个层会产生完全相同的输出:

rust 复制代码
use rustyml::neural_network::layers::*;
use rustyml::neural_network::traits::Layer;
use ndarray::Array;

fn main() {
    // [batch=1, positions=4, channels=4]
    let input = Array::from_shape_vec((1, 4, 4), (0..16).map(|v| v as f32).collect::<Vec<_>>())
        .unwrap()
        .into_dyn();

    // InstanceNorm 独立归一化每个 (sample, channel) 平面......
    let mut inn = InstanceNormalization::new(vec![1, 4, 4], 1e-5).unwrap();
    let out_in = inn.forward(&input).unwrap();

    // ......这恰好等于每通道 1 个组的 GroupNorm。
    let mut gn = GroupNormalization::new(vec![1, 4, 4], 4, 1e-5).unwrap();
    let out_gn = gn.forward(&input).unwrap();

    let max_diff = out_in
        .iter()
        .zip(out_gn.iter())
        .map(|(a, b)| (a - b).abs())
        .fold(0.0_f32, f32::max);
    println!("max |InstanceNorm - GroupNorm(groups=channels)| = {max_diff}");
    assert!(max_diff < 1e-6);
}

有 1 条整除规则对 GroupNormalization 很重要:num_groups 必须整除通道数。crate 在 forward 时才检查这一点,而不是在构造函数里,所以一个不匹配的层能顺利构造,却会在第一次前向调用时以 Error::InvalidParameter 失败。GroupNormalization::new 还会在构造时就拦下 num_groups == 0。两个构造函数都会提前拦下空的 input_shapeError::EmptyInput),以及非正或非有限的 epsilonError::InvalidParameter)。两个层都接受 set_weights(gamma, beta)

3.8.9. 播种、确定性与保存内容

本页的每个层还共享 2 个横切的性质。第一个是随机性:只有 dropout 和高斯这几个层会抽随机数,各自从自己的 StdRng 里抽,种子经由 crate 的全局种子机制设定。new 用全局种子或系统熵播种。with_random_state(seed) 会把种子固定住。归一化层则完全是确定性的。就连这些层内部用的并行/串行阈值也是逐位不变的:并行路径产出的结果和串行路径一致。所以开启多线程从不会改变你的数字。多线程只是一个性能旋钮,见性能调优与并行。要把一切一起播种,走可复现性与随机种子

第二个性质是持久化。这里有一处不对称,值得在保存模型前先记住。dropout 和噪声层不持有可训练参数:它们序列化为空,而且 crate 不会 保存它们的 RNG 状态。这没什么害处,反正它们在推理时就是恒等映射。LayerNormalizationGroupNormalizationInstanceNormalization 只序列化各自的 gammabeta 值,这已经完整了,因为它们会从每个输入重新计算统计量。BatchNormalization 是例外。它的权重记录里带着 gammabeta还有 running_meanrunning_var。推理用的正是这些滑动统计量。如果 crate 把它们丢掉,加载后的模型就会拿垃圾值去做归一化。正因为 crate 把它们持久化了,一个存下来再重新加载的 batch-norm 模型,预测结果和原来的一模一样。这趟往返的机制见权重保存与加载深入模型持久化

相关推荐
__zRainy__1 小时前
Node系列 · Node基础:全局变量与全局对象
开发语言·前端·javascript
zhiSiBuYu05171 小时前
Flask Session 与 Cookie 新手实战指南
后端·python·flask
2601_953988071 小时前
Ricon组态系统vs传统组态软件:为什么选择新一代Web组态平台
前端·后端·物联网·tcp/ip·数学建模·前端框架
IT_陈寒1 小时前
SpringBoot自动配置坑了我三天,原来漏了这个注解
前端·人工智能·后端
鬼手点金1 小时前
Scrapy + Playwright 完整示例(JS 动态渲染网页)
开发语言·javascript·爬虫·python·scrapy·html·json
Mr. zhihao1 小时前
深度解析:为什么Java序列化需要搭配ByteArrayOutputStream?IO装饰器模式的精妙设计
java·开发语言·装饰器模式
心运软件2 小时前
基于深度学习的宝石图像分类系统
人工智能·python·深度学习·机器学习·分类·数据挖掘
格林威2 小时前
多相机并行采图最佳实践:Task.WhenAll + 异常处理 + 资源释放
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·机器视觉
djjjx.2 小时前
【 C++ 】多态
开发语言·c++·多态