Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

DL——BN-LN-IN-GN-LRN-WN

本文介绍各种不同的Normalization方法

  • BN: Batch Normalization
  • LN: Layer Normalization
  • IN: Instance Normalization
  • GN: Group Normalization
  • LRN: Local Response Normalization
  • WN: Weight Normalization

Normalization总体介绍

  • BN,LN,IN的归一化的步骤都是使用下面的公式:
    $$
    \begin{align}
    u &= \frac{1}{m}\sum_{k\in S}x_k \\
    \sigma &= \sqrt{\frac{1}{m}\sum_{k\in S}(x_k-u) + \epsilon} \\
    \hat{x} &= \frac{1}{\sigma}(x-u) \\
    y &= \gamma \hat{x} + \beta
    \end{align}
    $$
    • \(u\) 为均值
    • \(\sigma\) 为标准差
    • \(\gamma\) 和 \(beta\) 是可以训练的参数
    • \(\epsilon\) 是平滑因子, 防止分母为0
    • BN,LN,IN三种不同的归一化方法, 对应的数据集 \(S\) 不同
      • BN对同一批数据进行归一化, 不管其他神经元, 只针对某个神经元的Mini Batch个样本输出值做归一化
      • LN对同一个样本的同一层输出进行归一化, 不依赖其他样本, 每次只依赖当前样本本身

BN

Batch Normalization

  • 对一批数据实行归一化

  • 对某个具体的神经元的Mini Batch个样本输出做归一化, 与其他神经元的输出无关

  • 代码:

    1
    2
    3
    4
    mu = np.mean(x,axis=0)
    sigma2 = np.var(x,axis=0)
    x_hat = (x-mu)/np.sqrt(sigma2+eps)
    out = gamma*x_hat + beta
  • 特别说明:TensorFlow在BN训练过程中(trainable=True)使用的是当前批次的均值和方差归一化,同时将均值和方法以滑动平均的方式更新并存储下来。最终,在预估/推断(trainable=False)阶段,则直接使用滑动平均的结果

    • 隐藏问题:当使用BN时,如果更新的轮次不够(训练global step太少),会导致均值和方差滑动平均的结果并未贴近真实的均值和方差,会导致训练时模型输出正常,预测时模型输出异常的情况,且这种问题比较隐晦,难以排查
    • 解决方案:
      • 当训练的轮次较少时,要注意动量不要设置太大,否则更新不足,此时设置小的动量可以缓解BN均值方差更新不足的问题(不建议使用这种方式,原因是:一般来说,动量太小会导致最终的均值方差仅被最近的Batch决定,模型效果波动大)
      • 建议在使用BN时,设置较大的动量,且注意保证足够的训练轮次,充分更新动量和方法

LN

Layer Normalization

  • 对单个训练样本的同一层所有神经元的输入做归一化
  • 与其他样本无关

BN的作用和说明

  • Batch Normalization把网络每一层的输出Y固定在一个变化范围的作用
  • BN都能显著提高训练速度
  • BN可以解决梯度消失问题
    • 归一化操作将每一层的输出从饱和区拉到了非饱和区(导数),从而解决了梯度消失问题
  • 普通的优化器加上BN后效果堪比Adam
    $$ ReLU + Adam \approx ReLU + SGD + BN$$
  • 如果对于具有分布极不平衡的二分类测试任务, 不要使用BN
  • BN一定程度上有归一化作用
    • BN本身就能提高网络模型的泛化能力
    • 使用BN后,不用太依赖Dropout, L2正则化等,可以将L2正则化的参数变小一点

WN

Weight Normalization

  • 对参数做归一化
  • 与数据无关

总结

BN和WN对比

  • BN是对对一个mini batch的数据在同一个神经元计算均值和方差
  • WN对网络的网络权值 W 进行归一化(L2归一化)

BN和LN对比

  • BN高度依赖于mini-batch的大小,实际使用中会对mini-Batch大小进行约束,不适合类似在线学习(mini-batch为1)情况;
  • BN不适用于RNN网络中normalize操作:
    • BN实际使用时需要计算并且保存某一层神经网络mini-batch的均值和方差等统计信息,对于对一个固定深度的前向神经网络(DNN,CNN)使用BN,很方便;
    • 但对于RNN来说,sequence的长度是不一致的,换句话说RNN的深度不是固定的,不同的time-step需要保存不同的statics特征,可能存在一个特殊sequence比其的sequence长很多,这样training时,计算很麻烦
  • 但LN可以有效解决上面这两个问题
  • LN适用于LSTM的加速,但用于CNN加速时并没有取得比BN更好的效果

PyTorch——关于Variable类和Tensor类的类型判断


问题描述

requires_grad=True

等价于requires_grad=a, a为任意非0整数,不能为浮点数
浮点数会报错: TypeError: integer argument expected, got float

  • 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import torch
    from torch.autograd import Variable

    tensor = torch.ones(1)
    variable = Variable(tensor, requires_grad=True)
    print(tensor)
    print(variable)
    print("type1: ", type(tensor), type(variable))
    print(tensor.data)
    print(variable.data)
    print("type2: ", type(tensor.data), type(variable.data))
    print(tensor.data.numpy())
    print(variable.data.numpy())
    print("type3: ", type(tensor.data.numpy()), type(variable.data.numpy()))
    print(tensor.numpy())
    print(variable.numpy())
    print("type4: ", type(tensor.numpy()), type(variable.numpy()))

    # Output:
    tensor([1.])
    tensor([1.], requires_grad=True)
    ('type1: ', <class 'torch.Tensor'>, <class 'torch.Tensor'>)
    tensor([1.])
    tensor([1.])
    ('type2: ', <class 'torch.Tensor'>, <class 'torch.Tensor'>)
    [1.]
    [1.]
    ('type3: ', <type 'numpy.ndarray'>, <type 'numpy.ndarray'>)
    [1.]
    Traceback (most recent call last):
    File "/home/jiahong/JupyterWorkspace/test.py", line 16, in <module>
    print(variable.numpy())
    RuntimeError: Can't call numpy() on Variable that requires grad. Use var.detach().numpy() instead.
  • 从上面的测试用例可以看出:

    • Variable和Tensor在判断类型时都是torch.Tensor
      • type(tensor) == type(variable) == torch.Tensor
    • 几乎所有操作都相同
      • tensor.data == variable.data
      • tensor.data.numpy() == varible.data.numpy()
    • 直接输出变量结果不相同
      • tensor输出时没有requires_grad=True
      • variable输出时有requires_grad=True
    • variable不能直接调用函数variable.numpy(),会报异常
      • 异常描述为: 当前Variable变量要求requires grad,也就是requires_grad属性为真时,变量不能直接使用

requires_grad=False

等价于requires_grad=0
不等价于requires_grad=None, None会报错: TypeError: an integer is required

  • 测试代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import torch
    from torch.autograd import Variable

    tensor = torch.ones(1)
    variable = Variable(tensor, requires_grad=False)
    print(tensor)
    print(variable)
    print("type1: ", type(tensor), type(variable))
    print(tensor.data)
    print(variable.data)
    print("type2: ", type(tensor.data), type(variable.data))
    print(tensor.data.numpy())
    print(variable.data.numpy())
    print("type3: ", type(tensor.data.numpy()), type(variable.data.numpy()))
    print(tensor.numpy())
    print(variable.numpy())
    print("type4: ", type(tensor.numpy()), type(variable.numpy()))

    # Output:
    tensor([1.])
    tensor([1.])
    ('type1: ', <class 'torch.Tensor'>, <class 'torch.Tensor'>)
    tensor([1.])
    tensor([1.])
    ('type2: ', <class 'torch.Tensor'>, <class 'torch.Tensor'>)
    [1.]
    [1.]
    ('type3: ', <type 'numpy.ndarray'>, <type 'numpy.ndarray'>)
    [1.]
    [1.]
    ('type4: ', <type 'numpy.ndarray'>, <type 'numpy.ndarray'>)
  • 从上面的测试用例可以看出:

    • 当variable变量的requires_grad=False时,variable完全退化为tensor
      • 直接输出变量时没有requires_grad=False属性
      • 可以直接使用variable.numpy()函数

Variable的三种等价定义

下面三种定义的Variable类型变量varible等价

  • requires_grad=False

    1
    variable = Variable(tensor, requires_grad=False)
  • 没有requires_grad参数

    1
    variable = Variable(tensor)
  • requires_grad=True,然后variable = variable.detach()

    1
    2
    variable = Variable(tensor, requires_grad=True)
    variable = variable.detach()
  • 上面三种定义都等价于原始的tensor

    • 这里的等价并未经过详细测试,但是至少以下方面等价:
      • 自身类型相同type, 类型为torch.Tensor
      • 可以调用属性.data,类型为torch.Tensor
      • 可以调用.grad,只不过都为None
      • 直接输出对象完全相同,都不包含requires_grad=True属性
      • 可以调用相同的函数.numpy(), 类型为numpy.ndarray
      • 可以调用相同的函数.data.numpy(), 类型为numpy.ndarray

DL——DeepFM

文本介绍DeepFM的理论和实现

  • 原始论文: DeepFM: A Factorization-Machine based Neural Network for CTR Prediction, IJCAI 2017
  • 参考博客: https://www.jianshu.com/p/6f1c2643d31b

回顾特征组合的问题

传统解决方案

  • FM: (Factorization Machines, FM)因子分解机
  • FMM: (Field Factorization Machines, FFM)
存在问题
  • 只能二阶特征组合,无法做到高阶特征组合
    • 理论上来讲FM经过简单的拓展后可以组合高阶特征,但是那样的话参数会爆炸增加,所以实际上使用时一般只是二阶特征.

DNN建模高阶组合特征

优点
  • 理论上DNN建模高阶组合特征是可行的
缺点
  • 由于离散特征中我们使用One-Hot编码,会导致输入维度增加,网络参数很多
解决方案
  • 利用FFM中的思想,特征分为不同的Field
  • 基本思想是从One-Hot编码换成Dense Vector
  • 进一步加上两个全连接层(隐藏层),让刚刚学到的Dense Vector进行组合,于是得到高阶组合特征
  • 此时,高阶和低阶的特征体现在隐藏层中,我们希望把低阶特征组合单独建模,然后融合高阶特征组合
  • 将DNN与FM进行一个合理的融合
  • 二者的融合分两种方式: 串行结构和并行结构

DeepFM

  • 是一种并行化的解决方案

  • 包含 FM 和 DNN 两个部分, FM 负责低阶组合特征的提取,DNN 负责高阶组合特征的提取,两部分共享同样的输入

  • DeepFM的预测结果可以表示为如下的形式
    $$\hat{y} = sigmoid(y_{FM} + y_{DNN})$$

FM部分

  • FM的输入仅仅包含稀疏特征,连续特征不包含在FM部分
  • 输出如下
    $$ y(x) = w_0+ \sum_{i=1}^n w_i x_i + \sum_{i=1}^n \sum_{j=i+1}^n w_{ij} x_i x_j $$

DNN部分

  • DNN的输入包含Combine(稀疏特征,连续特征)
  • DNN部分是一个前馈神经网络
  • 与图像语音的区别:
    • 图像语音输入为连续且密集的
    • CTR中使用的一般是稀疏的
  • 在进入隐藏层之前,使用一个嵌入层(DenseEmbeddings): 将高维稀疏输入向量压缩为低维稠密向量

DL——MLP及其BP算法

多层感知机(Multi-Layer Perception, MLP)及其BP(Back Propagation)算法,本文记录一些有趣的图


多层感知机

  • 图示如下

BP算法

推导

以一维输出(二分类)为例

详细流程

  • 动图

References

References:

  • http://galaxy.agh.edu.pl/~vlsi/AI/backp_t_en/backprop.html
  • https://www.cnblogs.com/ooon/p/5577241.html
  • https://blog.csdn.net/guotong1988/article/details/52096724

DL——深度学习中降低过拟合的方法

添加Dropout

  • 详情可参考: DL——为什么Dropout能防止过拟合

参数范书惩罚

相关参数: Weight decay(权重衰减)
添加L2或L1正则化, 详情可参考: ML——模型的方差与偏差

  • 参考文档:

    • L1正则化与L2正则化的详细讲解(L1具有稀疏性,L2让参数更小):L1正则化和L2正则化
    • L1具有稀疏性的证明:L1正则为什么更容易获得稀疏解
      • 求导后可知,在0点附近,权重大于0和小于0会产生正负不同的梯度值(当原始损失函数关于当前权重在0点的偏导绝对值小于正则化权重时,整体梯度基本由正则化梯度主导),从而使得参数倾向于走到0点
  • L1正则化:

    • L1又称为: Lasso Regularization(稀疏规则算子)
    • 计算公式为: 参数绝对值求和
    • 意义: 趋向于让一些参数为0, 可以起到特征选择的作用
  • L2正则化:

    • L2又称为: Ridge Regression(岭回归)
    • Weight decay 是放在正则项(Regularization)前面的一个系数,正则项一般指模型的复杂度
    • Weight decay 控制模型复杂度对损失函数的影响, 若Weight Decay很大,则模型的损失函数值也就大
    • pytorch中实现了L2正则化,也叫做权重衰减,具体实现是在优化器中,参数是 weight_decay, 默认为0
  • PyTorch中的weight_decay参数说明

weight_decay (float, optional): weight decay (L2 penalty) (default: 0)

  • 我之前的实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # zero the parameter gradients
    optimizer.zero_grad()
    # forward
    outputs = model(inputs)
    # _, preds = torch.max(outputs.data, 1)
    loss = loss_criterion(outputs, labels)

    # L1 regularization
    l1_loss = 0
    for w in model.parameters():
    l1_loss += torch.sum(torch.abs(w))
    loss += l1_rate * l1_loss

    # backward + optimize only if in training phase
    if phase == 'train':
    loss.backward()
    optimizer.step()
    • 其中 # L1 regularization后面是添加的L1 正则化
  • 就整体而言,对比加入正则化和未加入正则化的模型,训练输出的loss和Accuracy信息,我们可以发现,加入正则化后,loss下降的速度会变慢,准确率Accuracy的上升速度会变慢,并且未加入正则化模型的loss和Accuracy的浮动比较大(或者方差比较大),而加入正则化的模型训练loss和Accuracy,表现的比较平滑。并且随着正则化的权重lambda越大,表现的更加平滑。这其实就是正则化的对模型的惩罚作用,通过正则化可以使得模型表现的更加平滑,即通过正则化可以有效解决模型过拟合的问题

数据增强

  • 提高模型的泛化能力最好的办法是, 使用更多的训练数据进行训练
  • 创造一些假数据添加到训练集中
  • 实例:
    • AlexNet中使用对图片旋转等方式生成新的图片作为样本加入训练, 误差能降低1%

提前终止训练

  • 当发现数据在验证集上的损失趋于收敛甚至开始增加时,停止训练
  • 即使模型在验证集上的损失还在减小

参数绑定与参数共享

Soft Weight Sharing

  • 类似于CNN中卷积层的权重共享方法
  • RNN中也有权重共享, 整条时间链上的参数共享

Bagging

  • 其实bagging的方法是可以起到正则化的作用,因为正则化就是要减少泛化误差,而bagging的方法可以组合多个模型起到减少泛化误差的作用
  • 在深度学习中同样可以使用此方法,但是其会增加计算和存储的成本
    • 这一点在Kaggle比赛中有用过,的确有很大提高

Batch Normalization

  • 在Google Inception V2中所采用,是一种非常有用的正则化方法,可以让大型的卷积网络训练速度加快很多倍,同事收敛后分类的准确率也可以大幅度的提高.
  • N在训练某层时,会对每一个mini-batch数据进行标准化(normalization)处理,使输出规范到N(0,1)的正太分布,减少了Internal convariate shift(内部神经元分布的改变),传统的深度神经网络在训练是,每一层的输入的分布都在改变,因此训练困难,只能选择用一个很小的学习速率,但是每一层用了BN后,可以有效的解决这个问题,学习速率可以增大很多倍
  • 更多信息参考: DL——BN-LN-IN-GN-LRN-WN

辅助分类节点

(auxiliary classifiers)

  • 在Google Inception V1中,采用了辅助分类节点的策略,即将中间某一层的输出用作分类,并按一个较小的权重加到最终的分类结果中 ,这样相当于做了模型的融合,同时给网络增加了反向传播的梯度信号,提供了额外的正则化的思想.

尝试不同神经网络架构

  • 尝试替换以下方面:
    • 激活函数
    • 层数
    • 权重?
    • 层的参数?

PyTorch——DataLoader的使用


整体说明

  • PyTorch 中,Global Step 的计算和实现可能会因为不同版本而发生有趣的现象
  • 比如:在一些场景会遇到一些奇怪的现象,相差一个 样本,且不在 Global Batch 的整数倍边界,但是 Global Step 增加了 1

DataLoader 核心使用说明

  • torch.utils.data.DataLoader 用于加载数据集,实现批量读取、多线程加载、数据 Shuffle等功能
  • 用于训练和评估

DataLoader 基本用法

  • 用法简单示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    from torch.utils.data import DataLoader, Dataset

    # 定义数据集(需实现 __len__ 和 __getitem__)
    class MyDataset(Dataset):
    def __init__(self, data):
    self.data = data
    def __len__(self): # 返回总数据量
    return len(self.data)
    def __getitem__(self, idx): # 返回单条数据(idx为索引)
    return self.data[idx]

    # 初始化 DataLoader
    dataset = MyDataset(data=[1,2,3,4,5,6,7])
    dataloader = DataLoader(
    dataset=dataset, # 传入数据集
    batch_size=2, # 每个批次的样本数(默认1)
    shuffle=True, # 每个epoch是否打乱数据(默认False)
    drop_last=False, # 是否丢弃最后不完整批次(默认False)
    num_workers=0, # 加载数据的线程数(默认0,主线程加载)
    pin_memory=False # 是否将数据存入固定内存(加速 GPU 读取,默认False)
    )

    # 迭代使用(返回tensor批次)
    for batch in dataloader:
    print(batch)
  • 关键参数说明:

    • dataset:必须传入的数据集对象,这个类需实现 __len__/__getitem__
    • batch_size:批次大小,控制每次返回的样本数,当存在 Global Batch Size 和 Micro Batch Size,一般是 Micro Batch Size
    • shuffle:训练时建议设为 True,验证/测试时设为 False, 默认为 None
      • 注:设为 True 时,每个 epoch 开始时会重新打乱数据
    • drop_last:数据量无法被batch_size整除时,是否丢弃最后不足一个批次的样本
    • num_workers:多线程加载数据,可根据 CPU 核心数调整
    • pin_memory:若使用 GPU 训练,设为 True 可减少数据拷贝耗时,默认为 False
  • 注意事项

    • 迭代返回的批次默认是 tensor 类型(若数据集返回 numpy 或 列表 类型的数据,会自动转换);
    • shuffle=True 会在每个 epoch 开始时会重新打乱数据,保证训练随机性;
    • len(dataloader) 表示单个 epoch 的批次数量(计算规则:ceil(总数据量/batch_size) 或 floor,取决于 drop_last)

附录:记一次有趣的 Bug 排查

  • 一般的 Global Batch Step 计算实现如下:

    1
    total_global_step = len(dataloader) * epoches // (global_batch_size / micro_batch_size)
    • 整体可以表述为:数据集中包含的 Micro Batch 数量 除以 一个 Global Batch 中包含的 Micro Batch 数量
      • len(dataloader):数据集中包含的 Micro Batch 数量
      • (global_batch_size / micro_batch_size):一个 Global Batch 中包含的 Micro Batch 数量
  • 有趣的问题:假定 drop_last=False, micro_batch_size = 8, global_batch_size = 128, epoch=1

    • 总样本数量为 2040 时,total_global_step = 15
    • 总样本数量为 2041 时,total_global_step = 16
    • 可通过带入上面的代码验证结果
  • 问题出现的原因:

    • 当样本数量为 2040 时,len(dataloader) = 255
    • 当样本数量为 2041 时,len(dataloader) = 256
    • 进一步计算即可发现,虽然只是一个样本,且该数据量并不在 global_batch_size = 128 的整除边界,但是造成了 Global Step 多了 1,这很反直觉切不容易排查

PyTorch——PyTorch中的高级索引


整体说明

  • PyTorch 的高级索引操作允许以非常灵活和强大的方式选择和修改张量中的元素
  • 高级索引包括整数索引、切片(slicing)、布尔索引和整数数组索引等
  • PyTorch 中的索引使用(包括基础索引和高级索引)和 Numpy 中基本一致
  • 可用于选择元素,也可以用于修改元素
  • 高级索引一般不共享存储区(普通索引一般共享存储区)
    • 普通索引一般可以通过修改 Tensor 的偏移量(offset)、步长(stride)或形状实现,不需要修改存储区的数据(使用共享存储区可以节省内存和处理速度)
    • 高级索引则一般都是不规则的变化 ,需要修改存储区,故而不使用共享存储区
    • 这也是高级索引与切片的最大差别
  • 检索维度匹配要求 :多个索引数组的维度必须能够广播成一致的形状,否则报错
  • 高级索引的判定方式:
    • 在 PyTorch 中,当索引对象是一个非元组序列对象、一个 Tensor(数据类型为整数或布尔,在 NumPy 中为 ndarray),或一个至少包含一个序列对象或 Tensor(数据类型为整数或布尔,在 NumPy 中为 ndarray)的元组时,会触发高级索引判定

基础索引回顾

  • 在深入高级索引之前,我们先快速回顾一下基础索引:
    • 单个整数索引 : 选取特定位置的单个元素
    • 切片 : 选取连续的子范围。例如 a[start:end:step]
    • 省略号 (...) : 表示选择所有剩余的维度
      • 这在张量维度较多时非常有用,可以避免写出冗长的 :
  • 基础索引的 Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import torch
    a = torch.arange(27).reshape(3, 3, 3)
    print("原始张量 a:\n", a)
    # 输出:
    # tensor([[[ 0, 1, 2],
    # [ 3, 4, 5],
    # [ 6, 7, 8]],
    #
    # [[ 9, 10, 11],
    # [12, 13, 14],
    # [15, 16, 17]],
    #
    # [[18, 19, 20],
    # [21, 22, 23],
    # [24, 25, 26]]])

    print("a[0, 1, 2]:", a[0, 1, 2]) # 输出:tensor(5)(第一个维度第0个,第二个维度第1个,第三个维度第2个)
    print("a[1, :, 0]:\n", a[1, :, 0]) # 输出:tensor([ 9, 12, 15])(第一个维度第1个,第二个维度所有,第三个维度第0个)
    print("a[..., 1]:\n", a[..., 1]) # 选取所有维度,最后一个维度第1个,等价于 a[:,:,1]
    # 输出:
    # tensor([[ 1, 4, 7],
    # [10, 13, 16],
    # [19, 22, 25]])

高级索引-整数数组索引 (Integer Array Indexing)

  • 当使用 torch.Tensor (类型为 torch.long 或 torch.int) 或 Python 列表作为索引时,这被称为整数数组索引(高级索引的一种)

    • 第一步:广播 : 如果有多个整数数组索引,并且它们的形状不同,PyTorch 会尝试对它们进行广播(broadcasting),广播为相同形状
    • 第二步:返回结果 : 整数数组索引的返回张量的形状由广播后的索引张量的形状决定 ,当部分维度没有被索引时,默认保留该维度的所有值
  • 示例 1 : 在一个维度上使用整数数组索引

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import torch

    a = torch.arange(12).reshape(3, 4)
    print("原始张量 a:\n", a)
    # 输出:
    # tensor([[ 0, 1, 2, 3],
    # [ 4, 5, 6, 7],
    # [ 8, 9, 10, 11]])

    # 选取第0行和第2行
    indices = torch.tensor([0, 2])
    result = a[indices]
    print("\na[torch.tensor([0, 2])]:\n", result)
    # 输出:
    # tensor([[ 0, 1, 2, 3],
    # [ 8, 9, 10, 11]])

    # 选取第1列和第3列
    result = a[:, torch.tensor([1, 3])]
    print("\na[:, torch.tensor([1, 3])]:\n", result)
    # 输出:
    # tensor([[ 1, 3],
    # [ 5, 7],
    # [ 9, 11]])
  • 示例 2: 在多个维度上使用整数数组索引(“坐标”式选择)

    • 注:当你在多个维度上同时使用整数数组索引时,它们会被解释为坐标对(这与 NumPy 的行为非常相似)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      import torch

      a = torch.arange(12).reshape(3, 4)
      print("原始张量 a:\n", a)

      # 选取 (0, 1), (1, 3), (2, 0) 这些位置的元素
      row_indices = torch.tensor([0, 1, 2])
      col_indices = torch.tensor([1, 3, 0])
      result = a[row_indices, col_indices]
      print("\na[torch.tensor([0, 1, 2]), torch.tensor([1, 3, 0])]:", result)
      # 解释:
      # 结果的第一个元素是 a[0, 1]
      # 结果的第二个元素是 a[1, 3]
      # 结果的第三个元素是 a[2, 0]
      # 输出: tensor([ 1, 7, 8])

      # 索引张量形状不一致时会进行广播
      a = torch.arange(27).reshape(3, 3, 3)
      print("\n原始张量 a (3x3x3):\n", a)

      # 选取多个坐标,例如:
      # 维度0取索引0和2
      # 维度1取索引1和2
      # 维度2取索引0和1
      idx0 = torch.tensor([[0], [2]]) # shape (2, 1)
      idx1 = torch.tensor([1, 2]) # shape (2,)
      idx2 = torch.tensor([0, 1]) # shape (2,)

      # PyTorch 会尝试广播这些索引
      # idx0: [[0], [2]] -> 广播为 [[0, 0], [2, 2]] (因为 idx1 和 idx2 的大小是2)
      # idx1: [1, 2] -> 广播为 [[1, 2], [1, 2]]
      # idx2: [0, 1] -> 广播为 [[0, 1], [0, 1]]

      # 最终会选择以下坐标的元素:
      # (0, 1, 0), (0, 2, 1)
      # (2, 1, 0), (2, 2, 1)
      result = a[idx0, idx1, idx2]
      print("\na[idx0, idx1, idx2]:\n", result)
      # 输出:
      # tensor([[ 3, 5], # a[0,1,0], a[0,2,1]
      # [21, 23]]) # a[2,1,0], a[2,2,1]

布尔索引 (Boolean Indexing)

  • 当使用一个布尔张量作为索引时,PyTorch 会选择布尔张量中值为 True 的所有元素
    • 形状要求 :布尔张量的形状必须与被索引张量的一个或多个维度匹配
    • 返回结果 :布尔张量索引的结果张量通常是 1 维的张量,包含所有满足条件(True)的元素
    • 特别说明:布尔张量通常被称为“掩码”
  • 布尔张量的 Demo:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import torch

    a = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    print("原始张量 a:\n", a)

    # 选取所有大于 5 的元素
    mask = a > 5
    print("\n布尔掩码 (a > 5):\n", mask)
    # 输出:
    # tensor([[False, False, False],
    # [False, False, True],
    # [ True, True, True]])

    result = a[mask]
    print("\na[a > 5]:", result)
    # 输出: tensor([6, 7, 8, 9])
    # 注意以上输出是 1 维的

混合索引操作:a[index1, :, index2] 和 a[index1, index2, :]

  • 特别说明广播机制:当索引数组的维度不匹配时,PyTorch 会尝试运用广播规则来使它们的维度变得兼容,即使混合索引也适用,详情如下:

    1
    2
    3
    4
    5
    # 行索引是[0, 2],列索引对所有行都是0
    rows = torch.tensor([0, 2]) # 形状为(2,)
    cols = torch.tensor([0]) # 形状为(1,)
    result = a[rows, :, cols] # 广播后形状变为(2, 3, 1)
    print(result.shape) # 输出: torch.Size([2, 3, 1])
  • 关于索引后的形状:

    • 若高级索引没被隔离,则正常覆盖高级索引所在的维度区域
    • 若高级索引被隔离,则对齐维度后,高级索引需要合并,并放到最前面(注意:不是第一个高级索引所在的位置,而是从第 0 维开始的最前面几维)
  • 这种索引方式是在多个维度上混合使用高级索引和切片操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    import torch
    a = torch.arange(27).reshape(3, 3, 3)
    print(a)
    # 输出:
    # tensor([[[ 0, 1, 2],
    # [ 3, 4, 5],
    # [ 6, 7, 8]],
    # [[ 9, 10, 11],
    # [12, 13, 14],
    # [15, 16, 17]],
    # [[18, 19, 20],
    # [21, 22, 23],
    # [24, 25, 26]]])

    index1 = torch.tensor([[0, 2]]) # 使用 [[0,2]] 也可以
    index2 = torch.tensor([[0]]) # 1)使用 [[0]] 也可以;2)[0] 也可以得到相同结果,因为广播后结果是一样的
    result = a[index1, index2, :]
    print(result.shape) # 输出: torch.Size([1, 2, 3])
    print(result)
    # 输出:
    # tensor([[[ 0, 1, 2],
    # [18, 19, 20]]])
    # 理解:index1和index2广播以后是shape=[1,2],结果中,第0,1维的[3,3]会被[1,2]替换

    index1 = torch.tensor([[0, 2]]) # 使用 [[0,2]] 也可以
    index2 = torch.tensor([[0]]) # 1)使用 [[0]] 也可以;2)[0] 也可以得到相同结果,因为广播后结果是一样的
    result = a[index1, :, index2]
    print(result.shape) # 输出: torch.Size([1, 2, 3])
    print(result)
    # 输出:
    # tensor([[[ 0, 3, 6],
    # [18, 21, 24]]])
    # 理解:相当于先调用 a.transpose_(2,1) 对齐索引维度,然后再调用 a[index1,index2,:],因为 高级索引需要合并到一起去广播并放到最前面
    # # 也可以用permute替代transpose,但permute不能inplace
    # 测试下面的代码替换 result = a[index1, :, index2] 后与上述输出一致
    # a.transpose_(2,1)
    # result = a[index1, index2, :]


    index1 = torch.tensor([0, 2])
    index2 = torch.tensor([0, 2])
    result = a[index1, :, index2]
    print(result.shape) # 输出: torch.Size([2, 3])
    print(result)
    # 输出:
    # tensor([[[ 0, 3, 6],
    # [20, 23, 26]]])
    # tensor([[ 0, 3, 6],
    # [20, 23, 26]])
    # 理解1:index1和index2广播以后是shape=[2],结果中,第0,1维的[3,3]会被[2]替换
    # 理解2:相当于先调用 a.transpose_(2,1) 对齐索引维度,然后再调用 a[index1,index2,:],因为 高级索引需要合并到一起去广播并放到最前面
    # 说明:已经测试下面的代码替换 result = a[index1, :, index2] 后与上述输出一致
    # a.transpose_(2,1)
    # result = a[index1, index2, :]
  • 高级索引被隔离的补充实验 Demo:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    import torch
    a = torch.arange(3*4*5*6).reshape(3, 4, 5, 6)
    # print(a)

    index1 = torch.tensor([[0, 2]])
    index2 = torch.tensor([[0, 2]])
    result = a[:, index1, :, index2]
    print(result.shape) # 输出:torch.Size([1, 2, 3, 3])
    print(result)
    # 输出:
    # tensor([[[[ 0, 6, 12, 18, 24],
    # [120, 126, 132, 138, 144],
    # [240, 246, 252, 258, 264]],
    #
    # [[ 62, 68, 74, 80, 86],
    # [182, 188, 194, 200, 206],
    # [302, 308, 314, 320, 326]]]])

    index1 = torch.tensor([[0, 2]])
    index2 = torch.tensor([[0, 2]])
    a = a.permute(1,3,0,2)
    result = a[index1,index2,:,:]
    print(result.shape) # 输出:torch.Size([1, 2, 3, 3])
    print(result)
    # 输出:
    # tensor([[[[ 0, 6, 12, 18, 24],
    # [120, 126, 132, 138, 144],
    # [240, 246, 252, 258, 264]],
    #
    # [[ 62, 68, 74, 80, 86],
    # [182, 188, 194, 200, 206],
    # [302, 308, 314, 320, 326]]]])

    # 理解1:高级索引index1和index2被基础索引隔断了,index1和index2广播后为shape=[1,2],然后高级索引的维度会替换所在维度的[3,3]后放到最前面
    # 理解2:相当于先执行 a = a.permute(1,3,0,2)(注意permute操作不能inplace),然后再执行 result = a[index1,index2,:,:]
    # 测试说明:从上面的代码输出可以看到,result = a[:, index1, :, index2] 替换为下面语句后,结果一致:
    # a = a.permute(1,3,0,2)
    # result = a[index1,index2,:,:]

附录:高级索引的一些常见用法

批量数据选择

  • 在处理批量数据时,可以利用高级索引为每个样本选择不同的元素
    1
    2
    3
    4
    5
    6
    7
    8
    batch_size = 4
    features = 10
    data = torch.randn(batch_size, features)

    # 为每个样本选择不同的特征
    indices = torch.tensor([2, 5, 1, 8]) # 为4个样本分别选择第2、5、1、8个特征
    selected = data[torch.arange(batch_size), indices]
    print(selected.shape) # 输出: torch.Size([4])

并行更新

  • 借助高级索引,能够并行地更新张量中的多个位置
    1
    2
    3
    4
    5
    6
    7
    x = torch.zeros(5, 5)
    rows = torch.tensor([0, 1, 2])
    cols = torch.tensor([1, 2, 3])
    values = torch.tensor([10, 20, 30])

    x[rows, cols] = values # 将(0,1)、(1,2)、(2,3)位置的值分别更新为10、20、30
    print(x)

附录:高级索引出发条件总结

  • 在 PyTorch 中,当索引对象是一个非元组序列对象、一个 Tensor(数据类型为整数或布尔,在 NumPy 中为 ndarray),或一个至少包含一个序列对象或 Tensor(数据类型为整数或布尔,在 NumPy 中为 ndarray)的元组时,会触发高级索引判定

  • 以下是会触发高级索引判定的情况

  • 索引对象为单个高维数组或张量 :索引对象不是一个元组序列,而是一个高维数组或者张量,其中布尔型比整数型更常见

    • 举例:

      1
      2
      x = torch.arange(24)
      y = x.reshape(6, 4)[x.reshape(6, 4) > 10] # y.shape = torch.Size([13]),以为数组
    • 这里 torch.arange(24) > 10 是布尔型张量,会触发高级索引

  • 索引对象为整数型数组或张量组成的元组序列 :索引对象是一个元组序列,并且元组序列完全由整数型高维数组或者整数型张量组成

    • 举例:

      1
      2
      3
      4
      x = torch.randn(3, 4)
      rows = torch.tensor([0, 2])
      cols = torch.tensor([1, 3])
      y = x[(rows, cols)] # y.shape = torch.Size([2])
    • 其中 (rows, cols) 构成的元组序列就是由整数型张量组成,会触发高级索引

  • 索引对象为列表序列组成的元组序列 :索引对象是一个元组序列,并且元组序列完全由列表序列组成

    • 举例:

      1
      2
      x = torch.randn(3, 4)
      y = x[([0, 2], [1, 3])] # y.shape = torch.Size([2])
    • 这里 ([0, 2], [1, 3]) 是由 列表序列组成的元组 ,会触发高级索引

  • 索引对象为混合组成的元组序列(包含数组或张量与序列对象) :索引对象是一个元组序列,元组序列不仅包含高维整数型数组或者高维整数型张量,还包括序列对象

    • 举例:

      1
      2
      3
      x = torch.randn(3, 4)
      rows = torch.tensor([0, 2])
      y = x[(rows, [1, 3])] # y.shape = torch.Size([2])
    • 此元组序列中既有整数型张量rows,又有列表[1, 3],会触发高级索引

  • 索引对象为混合组成的元组序列(包含数组或张量与整数标量) :索引对象是一个元组序列,元组序列不仅包含高维整数型数组或者高维整数型张量,还包括整数标量

    • 举例:

      1
      2
      3
      x = torch.randn(3, 4)
      rows = torch.tensor([0, 2])
      y = x[(rows, 2)] # y.shape = torch.Size([2])
    • 元组序列中包含整数型张量 rows 和整数标量2,会触发高级索引

  • 索引对象为混合组成的元组序列(包含数组、张量、标量和序列对象) :索引对象是一个元组序列,元组序列包含高维整数型数组或者高维整数型张量、整数标量和序列对象

    • 举例:

      1
      2
      3
      x = torch.randn(3, 4, 5)
      rows = torch.tensor([0, 2])
      y = x[(rows, 2, [1, 3])] # y.shape = torch.Size([2])
    • 这种情况同样会触发高级索引

PyTorch——Tensor的内存布局Layout


整体说明

  • 在 PyTorch 里,张量的 layout 属性主要用于表明内存的组织形式(tensor.layout 属性可查看张量当前的布局类型)
  • 张量的存储主要分为稀疏布局(Sparse Layout)和稠密布局(Dense Layout)两种
    • 稠密布局适合进行常规的张量运算
    • 稀疏布局在处理大规模稀疏数据时,能够显著减少内存占用和计算量
  • 在使用稀疏张量进行计算时,需要注意:
    • 并非所有的 PyTorch 操作都支持稀疏张量,部分操作在执行前可能需要先将稀疏张量转换为稠密张量

稠密布局(torch.strided)

  • torch.strided 是标准的多维数组布局,采用连续的内存存储方式
  • torch.strided 在每一个维度都具备步长(stride)属性,借助该属性可以计算出内存中的偏移量
  • 在 PyTorch 1.13 及后续版本中,很多张量创建函数(如 torch.ones 等)的参数的默认值是都是 torch.strided
  • 稠密布局示例:
    1
    2
    x = torch.tensor([[1, 2, 3], [4, 5, 6]])
    print(x.layout) # 输出:torch.strided

稀疏 CSR 布局(torch.sparse_csr)

  • torch.sparse_csr 布局适用于存储稀疏矩阵,能有效节省内存
  • torch.sparse_csr 运用压缩稀疏行(Compressed Sparse Row)格式,借助三个张量来表示:
    • crow_indices:记录每行在 col_indices 和 values 中的起始位置
    • col_indices:存储非零元素所在的列索引
    • values:存放非零元素的值
  • 稀疏 CSR 布局示例:
    1
    2
    3
    4
    5
    crow_indices = torch.tensor([0, 2, 3])
    col_indices = torch.tensor([0, 1, 2])
    values = torch.tensor([1, 2, 3])
    sparse_tensor = torch.sparse_csr_tensor(crow_indices, col_indices, values, (2, 3))
    print(sparse_tensor.layout) # 输出:torch.sparse_csr

其他稀疏布局

  • PyTorch 还支持多种稀疏布局,以满足不同的访问和计算需求
  • 稀疏 COO(torch.sparse_coo)布局
  • 稀疏 CSC(torch.sparse_csc)布局

布局转换方法示例

  • 可以使用以下方法在不同布局之间进行转换:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 从稠密布局转换为稀疏 COO 布局
    dense_tensor = torch.tensor([[0, 1], [2, 0]])
    sparse_coo = dense_tensor.to_sparse() # 默认为 COO 格式

    # 从稀疏 COO 布局转换回稠密布局
    dense_tensor = sparse_coo.to_dense()

    # 稀疏布局之间的转换
    sparse_csr = sparse_coo.to_sparse_csr()

PyTorch——Module中的Parameter和Buffer


Parameter 和 Buffer 的整体理解

  • 在 PyTorch 的 nn.Module 里,Parameter 和 Buffer 都是张量类型(是两种类型的张量)
  • 两者的关键区别有:
    • 可训练性 :
      • Parameter 可训练 ,通常用于模型的权重和偏置,会在反向传播时被优化器更新
      • Buffer 不可训练 ,通常用于存储需要在训练或推理过程中保留,但不需要梯度更新的值(如 BatchNorm 的统计信息)
    • 注册方式 :
      • Parameter 显示定义字段:通过 nn.Parameter() 初始化,或通过 nn.Linear() 等类初始化
      • Buffer 一般是隐式定义:通过 register_buffer 或 BN 层等隐式自动定义
    • 访问方式:
      • Parameter 作为可训练的张量,会被自动添加到模型的 parameters() 迭代器中
      • Buffer 是不可训练的张量,不会被添加到 parameters() 中,也不会被优化器更新
  • 两者的共同点有:
    • 两者都会被保存在模型的 state_dict 中,因此在保存/加载模型时都会被保留
    • 当调用 model.to(device) 时,Parameter 和 Buffer 都会被移动到指定设备
  • 最佳实践:
    • Parameter用于
      • 定义模型权重、偏置等需要学习的参数
    • Buffer用于
      • 非训练状态的统计量(如 BatchNorm 的均值/方差)
      • 固定的预训练权重或常量张量
      • 以及其他需要与模型一起保存 ,但不需要梯度的中间结果

Parameter 和 Buffer 的定义方式

  • 需要注意的关键经验和知识点:
    • 定义位置 :建议将 Parameter 和 Buffer 都定义在 __init__ 中(虽然可以动态定义到 __init__ 之外)
    • 同名覆盖规则 :
      • Buffer 对象之间可以互相覆盖
      • Parameter 对象之间可以互相覆盖
      • Parameter 对象可以覆盖 Buffer 对象
      • Buffer 对象不可以覆盖 Parameter 对象
    • Parameter 或 Buffer 为 None 时 ,仅仅是一个声明 ,不会被 parameters()、buffers()或state_dict()等包含(注:named_xx() 和 xx() 数量一样,也不会包含)
    • 冻结参数 :属性为 requires_grad=False 的参数不会被更新(但可以被 parameters() 返回,也可以被加入优化器,此时有优化器状态,但是没有梯度,也不会被更新)
    • 初始化类型要求 :
      • Parameter 对象一定要用 nn.Parameter 对象初始化
      • Buffer 对象可以用 Tensor 对象初始化
    • Parameter 和 Buffer 更新规则 :
      • in-place update :
        • 可使用 model.x.data += 2 或 model.x.data.fill_(2.0) 的方式修改 Buffer 或 Parameter 的值,实现 in-place update
        • 此时针对 Parameter,不需要重新初始化 优化器
      • 替换张量数据:
        • 当使用类似 model.x.data = torch.tensor(2.0) 的方式修改,或重新注册新的参数对象时,此时会替换整个 Buffer 或 Parameter 对象
        • 此时针对 Parameter,需要重新初始化 优化器 ,否则优化器无法识别到被修改后的参数的张量
    • 优化器更新规则 :
      • 对于被重新赋值data张量的参数,需要重新初始化 优化器 ,否则优化器无法识别到被修改后的参数的张量
      • 对于新增加的参数 ,必须重新初始化优化器 ,以保证优化器能够优化到新的参数
      • 如果一个参数没有被优化器追踪(被追踪的参数在 optimizer.param_groups() 中),该参数不会被更新(即使在 loss.backward() 阶段已经计算了梯度,参数也不会被更新)
      • 更多补充见附录
  • Parameter 和 Buffer 定义代码和测试(详细阅读注释和输出部分)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    import torch
    import torch.nn as nn
    import math

    class RegistrationDemo(nn.Module):
    def __init__(self):
    super().__init__()

    # 1. 直接定义Parameter
    self.direct_param = nn.Parameter(torch.randn(3, 3))

    # 2. 使用register_parameter()方法
    custom_tensor = torch.ones(2, 2) * 0.5
    self.register_parameter('explicit_param', nn.Parameter(custom_tensor)) # 必须是 nn.Parameter 对象或 None,如果是其他对象会出错

    # 3. 通过nn.Linear隐式注册Parameter
    self.linear = nn.Linear(8 * 32 * 32, 4) # 输入维度=8×32×32

    # 4. 通过nn.Conv2d隐式注册Parameter
    self.conv = nn.Conv2d(3, 8, kernel_size=3, padding=1)

    # 5. 通过nn.BatchNorm2d隐式注册Parameter和Buffer
    self.bn = nn.BatchNorm2d(8) # weight(Parameter)和bias(Parameter), running_mean和running_var(Buffer)

    # 6. 自定义Buffer
    self.register_buffer('custom_buffer', torch.tensor([math.pi])) # 必须是 Tensor 对象或 None,如果是其他对象会出错

    # 7. 使用不同初始化方式的Parameter
    self.xavier_init = nn.Parameter(torch.zeros(5, 5))
    nn.init.xavier_uniform_(self.xavier_init)

    self.kaiming_init = nn.Parameter(torch.zeros(5, 5))
    nn.init.kaiming_normal_(self.kaiming_init)

    # 8. 可学习的标量Parameter
    self.scalar_param = nn.Parameter(torch.tensor(0.1))

    # 9. 冻结的Parameter (requires_grad=False)
    self.frozen_param = nn.Parameter(torch.tensor(0.1), requires_grad=False)

    # 10. 预声明动态注册的参数名(重要!避免state_dict问题,注意,不推荐这么使用,但部分情况下可以用来做高阶的模型设计)
    self.register_parameter('dynamic_param', None) # 仅声明参数名,但该参数不会被加入 state_dict,直到被初始化为实际的 Parameter 对象
    self.register_buffer('dynamic_buffer', None) # 仅声明缓冲区名,但该参数不会被加入 state_dict,直到被初始化为实际的 Buffer 对象

    # 11. 测试通过register重复注册对象
    self.register_parameter('test_multiple_param', nn.Parameter(torch.tensor(1.0))) # 定义参数 test_multiple_param
    self.register_parameter('test_multiple_param', nn.Parameter(torch.tensor(2.0))) # 重新修改参数对象,值为2.0(注意不是简单的修改值)
    self.test_multiple_param = nn.Parameter(torch.tensor(3.0)) # 重新修改参数对象,值为3.0(注意不是简单的修改值)
    self.test_multiple_param = nn.Parameter(torch.tensor(4.0)) # 重新修改参数对象,值为4.0(注意不是简单的修改值)
    self.register_parameter('test_multiple_param', nn.Parameter(torch.tensor(5.0))) # 重新修改参数对象,值为5.0(注意不是简单的修改值)

    # 11(a)特别地,buffer的注册和参数类似,但 Parameter 可以覆盖 Buffer 对象,但 Buffer 不可覆盖 Parameter 对象
    self.register_buffer('test_multiple_buffer', nn.Parameter(torch.tensor(1.0)))
    self.register_buffer('test_multiple_buffer', nn.Parameter(torch.tensor(2.0)))
    self.test_multiple_buffer = nn.Parameter(torch.tensor(3.0)) # 重新修改 Buffer 对象为 Parameter 对象,值为3.0(注意不是简单的修改值)
    self.test_multiple_buffer = nn.Parameter(torch.tensor(4.0)) # 重新修改 Parameter 对象,值为4.0(注意不是简单的修改值)
    # self.register_buffer('test_multiple_buffer', nn.Parameter(torch.tensor(5.0))) # 这行会报错 KeyError: "attribute 'test_multiple_buffer' already exists":Buffer 不允许使用 register_buffer 覆盖 Parameter对象

    # 11(b) Buffer 不可 覆盖 Parameter 对象的再次验证
    self.register_buffer('test_multiple_buffer_param', nn.Parameter(torch.tensor(1.0)))
    self.test_multiple_buffer_param = torch.tensor(1.5) # 仍然是 Buffer 对象,值变成1.5
    self.test_multiple_buffer_param += 10 # 仍然是 Buffer 对象,值变成11.5,且是 原地修改(in-place update)
    # self.register_parameter('test_multiple_buffer_param', nn.Parameter(torch.tensor(2.0))) # 这行会报错,KeyError: "attribute 'test_multiple_buffer_param' already exists",不允许 Buffer 和 Parameter 互相覆盖
    self.test_multiple_buffer_param = nn.Parameter(torch.tensor(3.0)) # 通过重写将 Buffer修改为 Parameter 对象
    self.register_parameter('test_multiple_buffer_param', nn.Parameter(torch.tensor(4.0))) # 这行不会报错,因为test_multiple_buffer_param已经是 Parameter 对象了,可以被参数重写

    # 11(c) Buffer 不可 覆盖 Parameter 对象的再次验证(即使是通过 register_parameter 注册的参数也不能覆盖)
    self.register_parameter('test_multiple_param_buffer', nn.Parameter(torch.tensor(1.0)))
    # self.register_buffer('test_multiple_param_buffer', nn.Parameter(torch.tensor(2.0))) # 这行会报错,KeyError: "attribute 'test_multiple_param_buffer' already exists",不允许 Buffer 和 Parameter 互相覆盖


    def init_dynamic_params(self, input_size):
    """在__init__外动态注册Parameter和Buffer(注意,不推荐在 __init__ 外定义 Parameter 和 Buffer ,但部分情况下可以用来做高阶的模型设计)"""
    # 动态注册Parameter(注册__init__中声明过的参数)
    self.register_parameter('dynamic_param', nn.Parameter(torch.randn(input_size)))

    # 动态注册Buffer(注册__init__中声明过的参数)
    self.register_buffer('dynamic_buffer', torch.arange(5).float())

    # 注册__init__中未声明的参数,也可以被正常更新,但需要保证定义执行此语句后再执行 forward 操作
    self.register_parameter('dynamic_param2', nn.Parameter(torch.randn(1)))

    def forward(self, x):
    x = self.conv(x)
    x = self.bn(x)
    x = x * self.scalar_param
    x = x * self.frozen_param

    if self.dynamic_param is not None: # 不论是否提前初始化,均没有问题,因为 __init__ 中已经声明
    x = x + self.dynamic_param.mean()

    if self.dynamic_param2 is not None: # 如果在调用 forward 前未定义 dynamic_param2,会报错,建议像是 dynamic_param 一样在 __init__ 函数中进行声明
    x = x + self.dynamic_param2

    x = x.view(x.size(0), -1) # 展平为 (batch_size, 8*32*32)
    x = self.linear(x)
    return x

    def print_registration_info(model):
    print("\n=== 模型整体结构 ===")
    print(model)

    print("\n=== Parameters ===")
    for name, param in model.named_parameters():
    print(f"参数名称: {name}, 形状: {param.shape}, 类型: {type(param)}, 是否可训练: {param.requires_grad}")

    print("\n=== Buffers ===")
    for name, buffer in model.named_buffers():
    print(f"缓冲区名称: {name}, 形状: {buffer.shape}, 类型: {type(buffer)}")

    print("\n=== state_dict ===")
    for key in model.state_dict().keys():
    print(f"Key: {key}")

    if __name__ == "__main__":
    # 创建模型但不初始化动态参数
    model = RegistrationDemo()

    # 打印注册信息(动态参数尚未初始化)
    print("初始化前的参数状态:")
    print_registration_info(model)

    # 初始化动态参数
    model.init_dynamic_params(input_size=4)

    # 打印注册信息(动态参数已初始化)
    print("\n初始化后的参数状态:")
    print_registration_info(model)

    # 测试前向传播
    x = torch.randn(2, 3, 32, 32)
    output = model(x)

    # 验证动态参数是否参与训练
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    loss = output.sum()
    loss.backward()

    print()

    print("训练前 dynamic_param:", model.dynamic_param.data)
    print("训练前 dynamic_param2:", model.dynamic_param2.data)
    print("训练前 scalar_param:", model.scalar_param.data)
    print("训练前 frozen_param:", model.frozen_param.data)

    optimizer.step() # 更新参数,注意:这里只更新需要更新的参数,不参与训练的参数不会被更新(即使他们被加入了优化器中)

    print("训练后 dynamic_param:", model.dynamic_param.data)
    print("训练前 dynamic_param2:", model.dynamic_param2.data)
    print("训练后 scalar_param:", model.scalar_param.data)
    print("训练后 frozen_param(未发生改变):", model.frozen_param.data)

    print()
    print("test_multiple_param:", model.test_multiple_param)
    print("test_multiple_buffer:", model.test_multiple_buffer)

    # 初始化前的参数状态:
    #
    # === 模型整体结构 ===
    # RegistrationDemo(
    # (linear): Linear(in_features=8192, out_features=4, bias=True)
    # (conv): Conv2d(3, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    # (bn): BatchNorm2d(8, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    # )
    #
    # === Parameters ===
    # 参数名称: direct_param, 形状: torch.Size([3, 3]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: explicit_param, 形状: torch.Size([2, 2]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: xavier_init, 形状: torch.Size([5, 5]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: kaiming_init, 形状: torch.Size([5, 5]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: scalar_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: frozen_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: False
    # 参数名称: test_multiple_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_buffer, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_buffer_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_param_buffer, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: linear.weight, 形状: torch.Size([4, 8192]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: linear.bias, 形状: torch.Size([4]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: conv.weight, 形状: torch.Size([8, 3, 3, 3]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: conv.bias, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: bn.weight, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: bn.bias, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    #
    # === Buffers ===
    # 缓冲区名称: custom_buffer, 形状: torch.Size([1]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.running_mean, 形状: torch.Size([8]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.running_var, 形状: torch.Size([8]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.num_batches_tracked, 形状: torch.Size([]), 类型: <class 'torch.Tensor'>
    #
    # === state_dict ===
    # Key: direct_param
    # Key: explicit_param
    # Key: xavier_init
    # Key: kaiming_init
    # Key: scalar_param
    # Key: frozen_param
    # Key: test_multiple_param
    # Key: test_multiple_buffer
    # Key: test_multiple_buffer_param
    # Key: test_multiple_param_buffer
    # Key: custom_buffer
    # Key: linear.weight
    # Key: linear.bias
    # Key: conv.weight
    # Key: conv.bias
    # Key: bn.weight
    # Key: bn.bias
    # Key: bn.running_mean
    # Key: bn.running_var
    # Key: bn.num_batches_tracked
    #
    # 初始化后的参数状态:
    #
    # === 模型整体结构 ===
    # RegistrationDemo(
    # (linear): Linear(in_features=8192, out_features=4, bias=True)
    # (conv): Conv2d(3, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    # (bn): BatchNorm2d(8, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    # )
    #
    # === Parameters ===
    # 参数名称: direct_param, 形状: torch.Size([3, 3]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: explicit_param, 形状: torch.Size([2, 2]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: xavier_init, 形状: torch.Size([5, 5]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: kaiming_init, 形状: torch.Size([5, 5]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: scalar_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: frozen_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: False
    # 参数名称: dynamic_param, 形状: torch.Size([4]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_buffer, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_buffer_param, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: test_multiple_param_buffer, 形状: torch.Size([]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: dynamic_param2, 形状: torch.Size([1]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: linear.weight, 形状: torch.Size([4, 8192]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: linear.bias, 形状: torch.Size([4]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: conv.weight, 形状: torch.Size([8, 3, 3, 3]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: conv.bias, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: bn.weight, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    # 参数名称: bn.bias, 形状: torch.Size([8]), 类型: <class 'torch.nn.parameter.Parameter'>, 是否可训练: True
    #
    # === Buffers ===
    # 缓冲区名称: custom_buffer, 形状: torch.Size([1]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: dynamic_buffer, 形状: torch.Size([5]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.running_mean, 形状: torch.Size([8]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.running_var, 形状: torch.Size([8]), 类型: <class 'torch.Tensor'>
    # 缓冲区名称: bn.num_batches_tracked, 形状: torch.Size([]), 类型: <class 'torch.Tensor'>
    #
    # === state_dict ===
    # Key: direct_param
    # Key: explicit_param
    # Key: xavier_init
    # Key: kaiming_init
    # Key: scalar_param
    # Key: frozen_param
    # Key: dynamic_param
    # Key: test_multiple_param
    # Key: test_multiple_buffer
    # Key: test_multiple_buffer_param
    # Key: test_multiple_param_buffer
    # Key: dynamic_param2
    # Key: custom_buffer
    # Key: dynamic_buffer
    # Key: linear.weight
    # Key: linear.bias
    # Key: conv.weight
    # Key: conv.bias
    # Key: bn.weight
    # Key: bn.bias
    # Key: bn.running_mean
    # Key: bn.running_var
    # Key: bn.num_batches_tracked
    #
    # 训练前 dynamic_param: tensor([-1.2286, 0.4382, 2.0483, 0.1235])
    # 训练前 dynamic_param2: tensor([-0.3353])
    # 训练前 scalar_param: tensor(0.1000)
    # 训练前 frozen_param: tensor(0.1000)
    # 训练后 dynamic_param: tensor([-1.2186, 0.4482, 2.0583, 0.1335])
    # 训练前 dynamic_param2: tensor([-0.3253])
    # 训练后 scalar_param: tensor(0.0900)
    # 训练后 frozen_param(未发生改变): tensor(0.1000)
    #
    # test_multiple_param: Parameter containing:
    # tensor(5., requires_grad=True)
    # test_multiple_buffer: Parameter containing:
    # tensor(4., requires_grad=True)

附录:loss.backward() 和 optimizer.step() 的工作流程

  • loss.backward() 负责计算梯度并存储到参数的 .grad 中(若 requires_grad = False 则不会计算梯度)
  • optimizer.step() 负责根据梯度更新参数(.grad为 None时不更新)
  • 如果在 loss.backward() 之前执行 requires_grad = False 可保证 .grad 为 None,参数不会更新
  • 如果在 loss.backward() 和 optimizer.step() 中间执行 requires_grad = False,参数会更新这一次,下次不会更新
    • 梯度计算发生在 loss.backward() 阶段 :此时参数的 requires_grad 为 True,梯度已被计算并存储在 param.grad 中
    • 优化器只检查 .grad 是否为 None :修改 requires_grad = False 不会清除已计算的梯度,因此优化器仍会使用已有的梯度更新参数
    • 后续迭代中参数被忽略:一旦 requires_grad = False,后续的 loss.backward() 将不再计算该参数的梯度,优化器也会跳过它

PyTorch——Random相关状态管理


整体说明

  • PyTorch 中,包含很多随机操作,比如
    • 可以使用 torch.rand() 等函数获取随机数
    • 可以使用 torch.nn.functional.dropout() 实现随机 drop 一些神经元
    • 可以使用 tensor.random_() 等函数随机初始化参数
  • 这些涉及随机数/采样的方法均受限于一个随机状态管理

torch Seed 打印

  • torch Seed 打印代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 打印 torch 的随机种子情况
    def print_torch_seeds():
    print("=" * 30 + "PyTorch Random Seeds Status")
    print("=" * 30)
    cpu_seed = torch.initial_seed()
    print(f"[CPU] Seed: {cpu_seed}")

    if torch.cuda.is_available():
    try:
    gpu_seed = torch.cuda.initial_seed()
    current_device = torch.cuda.current_device()
    device_name = torch.cuda.get_device_name(current_device)

    print(f"[GPU] Seed: {gpu_seed}")
    print(f" Device: {current_device} ({device_name})")
    except Exception as e:
    print(f"[GPU] Error getting seed: {e}")
    else:
    print("[GPU] CUDA is not available.")

    print("=" * 30)
    print_torch_seeds()

torch Seed 设置

  • 全局 torch Seed 设置代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import torch

    # 固定CPU种子
    torch.manual_seed(42)

    # 固定所有GPU的种子(单GPU/多GPU通用)
    if torch.cuda.is_available():
    torch.cuda.manual_seed_all(42) # 替代 torch.cuda.manual_seed(42)(单GPU)

    # GPU上生成随机排列
    perm = torch.randperm(10, device="cuda") # 注意:需要指定 "cuda" 才会在 GPU 上执行
    print("GPU随机排列:", perm) # 每次运行结果一致
    print("draw a random number:", torch.rand()) # 每次运行结果一致
  • 使用独立的 torch 生成器(独立管理自己的随机生成器):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import torch

    # 创建独立的生成器并设置种子
    generator = torch.Generator()
    generator.manual_seed(42)
    # generator = torch.Generator().manual_seed(42) # 等价实现

    # 生成随机排列时指定生成器
    perm1 = torch.randperm(10, generator=generator)
    perm2 = torch.randperm(10, generator=generator)

    print("独立生成器-第一次:", perm1) # tensor([2, 7, 3, 1, 0, 9, 4, 5, 8, 6])
    print("独立生成器-第二次:", perm2) # tensor([2, 0, 7, 9, 8, 4, 3, 6, 1, 5])

    # 重置生成器种子,结果重复
    generator.manual_seed(42)
    perm3 = torch.randperm(10, generator=generator)
    print("重置生成器后:", perm3) # tensor([2, 7, 3, 1, 0, 9, 4, 5, 8, 6])(和perm1一致)
    • 说明:torch.Generator 是 PyTorch 中统一的随机数生成器(RNG)核心对象,几乎所有 PyTorch 内置的随机操作都支持通过 generator 参数指定该生成器

附录:torch.Generator 详细说明

  • torch.Generator 是 PyTorch 中统一的随机数生成器(RNG)核心对象 ,几乎所有 PyTorch 内置的随机操作都支持通过 generator 参数指定该生成器,仅极少数场景不支持(或无需支持)
  • torch.Generator 的核心作用是隔离随机状态 :任何依赖 PyTorch 内置随机数生成的操作,只要支持 generator 参数,就能通过该生成器控制随机行为;无 generator 参数的操作,要么不依赖随机数,要么复用全局生成器(CPU/CUDA)
  • 所有需要随机逻辑的场景均支持 torch.Generator 的随机操作(全场景)
  • 注:无随机逻辑的操作本身无随机行为,因此不需要(也无法)指定 generator:
    • 张量基础操作:torch.ones()、torch.zeros()、torch.arange()、torch.cat()、torch.matmul() 等
    • 数学运算:torch.sin()、torch.exp()、torch.mean()、torch.argmax() 等
    • 索引/切片:x[:, 0]、x.index_select() 等
    • 设备/类型转换:x.to('cuda')、x.float() 等

随机逻辑的场景示例

  • 所有操作均可通过 generator 参数指定自定义 torch.Generator,实现随机状态隔离
  • 基础随机数生成
    函数/方法 用途 示例
    torch.rand() 均匀分布随机数 torch.rand(3, generator=g)
    torch.randn() 标准正态分布随机数 torch.randn(2, 4, generator=g)
    torch.randint() 整数随机数 torch.randint(0, 10, (3,), generator=g)
    torch.randperm() 随机排列 torch.randperm(5, generator=g)
    torch.rand_like() 按形状生成均匀随机数 torch.rand_like(torch.ones(2), generator=g)
    torch.randn_like() 按形状生成正态随机数 torch.randn_like(torch.ones(2), generator=g)
    torch.normal() 自定义均值/方差正态分布 torch.normal(0, 1, (3,), generator=g)
    torch.poisson() 泊松分布随机数 torch.poisson(torch.ones(3), generator=g)
    torch.exponential() 指数分布随机数 torch.exponential(1.0, (3,), generator=g)
    torch.cauchy() 柯西分布随机数 torch.cauchy(0, 1, (3,), generator=g)
    torch.log_normal() 对数正态分布随机数 torch.log_normal(0, 1, (3,), generator=g)
    torch.multinomial() 多项分布采样 torch.multinomial(torch.ones(5), 3, generator=g)
    torch.bernoulli() 伯努利分布(0/1) torch.bernoulli(torch.ones(3)*0.5, generator=g)
    • 注:指定参数 generator 时,前面的参数也需要指定(Python 本身的规则)
  • 张量随机初始化
    函数/方法 用途 示例
    tensor.random_() 原地随机初始化(整数) tensor.random_(generator=g)
    tensor.uniform_() 原地均匀分布初始化 tensor.uniform_(0, 1, generator=g)
    tensor.normal_() 原地正态分布初始化 tensor.normal_(0, 1, generator=g)
    tensor.cauchy_() 原地柯西分布初始化 tensor.cauchy_(0, 1, generator=g)
  • 随机采样/变换(数据增强等)
    函数/方法 用途 示例
    torch.utils.data.RandomSampler 数据集随机采样 RandomSampler(dataset, generator=g)
    torch.nn.functional.dropout() Dropout层随机失活 F.dropout(x, p=0.5, generator=g)
    torch.nn.functional.dropout2d() 2D Dropout F.dropout2d(x, p=0.5, generator=g)
    torch.nn.functional.dropout3d() 3D Dropout F.dropout3d(x, p=0.5, generator=g)
    torchvision.transforms 中的随机变换 图像随机增强(如RandomCrop) transforms.RandomCrop(32, generator=g)(需torchvision)
    torch.distributions 分布采样 概率分布采样(如Normal、Uniform) dist = Normal(0, 1); dist.sample((3,), generator=g)

特殊说明:随机场景但不支持 torch.Generator 的场景

  • 有随机逻辑但不支持自定义 generator 的场景;依赖随机数,但 PyTorch 未开放 generator 参数,只能复用全局生成器(CPU/CUDA):
    操作 原因 替代方案
    torch.shuffle() 底层绑定全局生成器 用 torch.randperm(generator=g) 手动实现洗牌
    torch.nn.Dropout 模块(默认) 模块初始化时未绑定生成器 改用 F.dropout(generator=g) 或自定义模块绑定生成器
    部分第三方库的随机操作(如某些数据增强) 未适配 generator 参数 替换为 PyTorch 原生实现或手动设置全局种子
    torch.multiprocessing 多进程随机 进程间生成器隔离限制 每个进程内重新初始化 generator
  • 实践思考:
    • 1)凡是生成随机数的 PyTorch 原生函数,优先检查是否有 generator 参数,有则建议使用(隔离随机状态)
    • 2)对不支持 generator 的随机操作,要么手动实现(如用 randperm 替代 shuffle),要么临时设置全局种子并尽快恢复
    • 3)CUDA 场景务必创建对应设备的 generator,避免跨设备混用导致随机状态混乱

最佳实践:torch.Generator 隔离随机状态

  • 多个 torch.Generator 隔离随机状态示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import torch

    # 创建两个独立生成器
    g1 = torch.Generator().manual_seed(42)
    g2 = torch.Generator().manual_seed(42)

    # 用g1生成随机数(消耗g1的状态)
    a = torch.rand(2, generator=g1)
    b = torch.rand(2, generator=g1)

    # 用g2生成随机数(g2状态未被消耗,结果和g1初始一致)
    c = torch.rand(2, generator=g2)

    print("a (g1第一次):", a) # tensor([0.8823, 0.9150])
    print("b (g1第二次):", b) # tensor([0.3829, 0.9593])
    print("c (g2第一次):", c) # tensor([0.8823, 0.9150])(和a一致)

附录:GPU 下的 torch.Generator

  • torch.Generator 必须与操作的设备(CPU/CUDA)对齐 ,否则会导致隐式设备拷贝、性能损耗,甚至随机状态混乱
    • CPU 操作时使用 CPU 生成器
    • CUDA 操作时使用对应 CUDA 设备的生成器
    • 核心目的:避免隐式跨设备拷贝,保证随机状态的隔离性和可复现性
  • 所有支持 CUDA 的随机操作(如 torch.rand(3, device='cuda', generator=g)),需指定与生成器同设备的 generator
  • CUDA 生成器的随机状态与 CPU 生成器完全隔离,互不干扰
生成器的设备属性
  • torch.Generator 可通过 device 参数绑定具体设备,默认是 CPU:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import torch

    # CPU生成器(默认)
    g_cpu = torch.Generator() # 等价于 torch.Generator(device="cpu")
    print("CPU生成器设备:", g_cpu.device) # 输出:cpu

    # CUDA生成器(需显式指定)
    if torch.cuda.is_available():
    g_cuda = torch.Generator(device="cuda:0") # 绑定cuda:0
    print("CUDA生成器设备:", g_cuda.device) # 输出:cuda:0
对齐 vs 不对齐的示例对比
  • 正确:生成器设备 等于 操作 device(推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    if torch.cuda.is_available():
    # 创建cuda:0的生成器
    g_cuda = torch.Generator(device="cuda:0").manual_seed(42)
    # 操作指定device=cuda:0,与生成器对齐
    perm = torch.randperm(10, device="cuda:0", generator=g_cuda)

    print("结果设备:", perm.device) # cuda:0
    print("无隐式拷贝,效率最高")
  • 错误:生成器设备 不等于 操作 device(性能坑)

    1
    2
    3
    4
    5
    6
    7
    if torch.cuda.is_available():
    # 生成器是cuda:0,但操作指定 device=cpu
    g_cuda = torch.Generator(device="cuda:0").manual_seed(42)
    perm = torch.randperm(10, device="cpu", generator=g_cuda)

    print("结果设备:", perm.device) # cpu
    print("隐式拷贝:GPU生成随机数 拷贝到CPU(额外开销)")
  • 更隐蔽的错误:CUDA操作用CPU生成器

    1
    2
    3
    4
    5
    6
    7
    if torch.cuda.is_available():
    # 生成器是CPU,操作指定device=cuda
    g_cpu = torch.Generator().manual_seed(42)
    perm = torch.randperm(10, device="cuda", generator=g_cpu)

    print("结果设备:", perm.device) # cuda:0
    print("隐式拷贝:CPU生成随机数 拷贝到GPU(额外开销)")
为什么必须对齐?
  • 随机数生成器的硬件绑定 :
    • CPU 生成器依赖 CPU 的随机数算法
    • CUDA 生成器依赖 GPU 的 cuRAND 库,直接在 GPU 显存生成随机数;
      • 跨设备使用时,PyTorch 会先在生成器设备生成随机数,再通过 PCIe 总线拷贝到操作指定的设备,产生额外耗时
  • 随机状态的隔离性(容易因为误用而出错) :
    • CUDA生成器的随机状态(get_state())和 CPU 生成器完全隔离,若跨设备使用,会导致“生成器状态和操作设备不匹配”,破坏随机种子的可复现性:
      1
      2
      3
      4
      5
      6
      7
      if torch.cuda.is_available():
      g_cuda = torch.Generator(device="cuda").manual_seed(42)
      # 第一次:跨设备使用(cuda生成器 到 cpu操作)
      perm1 = torch.randperm(10, device="cpu", generator=g_cuda)
      # 第二次:直接用cuda生成器 到 cuda操作
      perm2 = torch.randperm(10, device="cuda", generator=g_cuda)
      # perm2的结果不等于“重新seed后cuda操作的结果”(状态已被跨设备操作消耗)
torch.Generator 的最佳实践
  • 创建生成器时显式指定设备 :

    • 不要依赖默认的CPU生成器,GPU场景务必创建 device="cuda" 的生成器
  • 封装成函数,强制对齐 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def get_generator(device: str = "cpu", seed: int = 42):
    g = torch.Generator(device=device)
    g.manual_seed(seed)
    return g

    # 使用
    if torch.cuda.is_available():
    g = get_generator(device="cuda:0", seed=42)
    perm = torch.randperm(10, device="cuda:0", generator=g)
  • 多GPU场景:每个GPU对应独立生成器 :

    1
    2
    3
    4
    5
    6
    7
    if torch.cuda.is_available() and torch.cuda.device_count() > 1:
    # 为cuda:0和cuda:1分别创建生成器
    g0 = torch.Generator(device="cuda:0").manual_seed(42)
    g1 = torch.Generator(device="cuda:1").manual_seed(100)

    perm0 = torch.randperm(10, device="cuda:0", generator=g0)
    perm1 = torch.randperm(10, device="cuda:1", generator=g1)
1…353637…66
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

659 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4