Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

ML——AUC和GAUC

本文介绍AUC和GAUC


参考链接

  • 图解AUC和GAUC-知乎

编程实现

  • AUC计算的3种实现方法
    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
    # encoding=utf8
    from sklearn.metrics import roc_auc_score

    ## 实现1:O(Nlog(N))
    def calculate_auc1(labels, predictions):
    # 将预测结果和真实标签按照预测结果从大到小的顺序进行排序
    sorted_predictions = [l for _, l in sorted(zip(predictions, labels), reverse=True)]
    print(sorted_predictions)
    # 统计正样本和负样本的数量
    positive_count = sum(labels)
    negative_count = len(labels) - positive_count

    neg_found_count = 0
    pos_gt_neg_count = 0
    # 计算正样本大于负样本的数量之和
    for label in sorted_predictions:
    if label == 1:
    pos_gt_neg_count += negative_count - neg_found_count
    else:
    neg_found_count += 1

    # 计算AUC
    auc = 1.0 * pos_gt_neg_count / (positive_count * negative_count)

    return auc

    ## 实现2:O(N^2)
    def calculate_auc2(labels, predictions):
    pos_indexes = [i for i in range(len(labels)) if labels[i] == 1]
    neg_indexes = [i for i in range(len(labels)) if labels[i] == 0]
    p = len(pos_indexes)
    n = len(neg_indexes)

    pos_gt_neg_count = 0
    for i in pos_indexes:
    for j in neg_indexes:
    if predictions[i] > predictions[j]:
    pos_gt_neg_count += 1
    elif predictions[i] == predictions[j]:
    pos_gt_neg_count += 0.5
    return pos_gt_neg_count/(p*n)

    ## 实现3:O(Nlog(N))
    def calculate_auc3(labels, predictions):
    # 将预测结果和真实标签按照预测结果从小到大的顺序进行排序,注意:排序是从小到大
    sorted_predictions = [[p,l] for p, l in sorted(zip(predictions, labels))]
    print(sorted_predictions)
    # 统计正样本和负样本的数量
    positive_count = sum(labels)
    negative_count = len(labels) - positive_count
    # 统计正样本的序号和,注意:序号从1开始
    positive_count_indexes_sum = sum([i+1 for i in range(len(sorted_predictions)) if sorted_predictions[i][1] == 1])
    return (positive_count_indexes_sum - 0.5*positive_count*(positive_count+1))/(positive_count*negative_count)
    pass

    # 真实标签
    labels = [1, 1, 0, 0, 1, 1]
    # 预测结果
    predictions = [0.2, 0.8, 0.3, 0.4, 0.5, 0.6]

    # 计算AUC
    auc1 = calculate_auc1(labels, predictions)
    print("AUC1:", auc1)

    # 计算AUC
    auc2 = calculate_auc2(labels, predictions)
    print("AUC2:", auc2)

    # 计算AUC
    auc3 = calculate_auc3(labels, predictions)
    print("AUC3:", auc3)

    # 调用官方库计算AUC
    auc = roc_auc_score(labels, predictions)
    print("AUC:", auc)

SQL实现

  • 详情见:深入理解AUC

  • 推导思路:

    • 统计每个正样本大于负样本的概率(排在该正样本后面的负样本数/总的负样本数)
    • 对所有正样本的概率求均值
  • 整体推导流程:
    $$
    \begin{align}
    AUC &= \frac{1}{N_+} \sum_{j=1}^{N_+}\frac{(r_j - j)}{N_-} \\
    &= \frac{\sum_{j=1}^{N_+}r_j - N_+(N_+ + 1)/2}{N_+N_-} \\
    &= \frac{\sum_{j =1}^{N_+} r_j - N_+(N_+ + 1)/2}{N_+ N_-}
    \end{align}
    $$

    • 注意:以上公式是在按照预估值从大到小排序后的基础上计算的,实际应用上述公式时需要先排序
    • 公式符号说明:对于第 \(j\) 个正样本 ,假定其排序定义为 \(r_j\),则在这个正样本之前共有 \((r_j-1)\) 个样本,其中有 \((j - 1)\) 个正样本,\((r_j-j)\) 个负样本,此时该正样本的预估值大于负样本的概率为:\(\frac{(r_j - j)}{N_-}\)
  • SQL实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    select
    (ry - 0.5*n1*(n1+1))/n0/n1 as auc
    from(
    select
    sum(if(y=0, 1, 0)) as n0,
    sum(if(y=1, 1, 0)) as n1,
    sum(if(y=1, r, 0)) as ry
    from(
    select y, row_number() over(order by score asc) as r
    from(
    select y, score
    from some.table
    )A
    )B
    )C
  • SQL实现(分场景+pcoc实现)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    select 
    scene,
    (ry - 0.5*n1*(n1+1))/n0/n1 as auc,
    n1/(n1+n0) as ctr,
    pctr,
    pctr/(n1/(n1+n0)) as pcoc,
    from(
    select
    scene,
    sum(if(y=0, 1, 0)) as n0,
    sum(if(y=1, 1, 0)) as n1,
    sum(if(y=1, r, 0)) as ry,
    avg(score) as pctr
    from(
    select scene, score, y, row_number() over(partition by scene order by score asc) as r
    from(
    select scene, y, score
    from some.table
    )A
    )B
    )C

General——深刻认识URL

你真的认识URL了吗?

URL中的#字符

  • #在URL中与服务器无关,也就是说正常访问服务器的URL不包含#
  • #仅仅与本地浏览器对网页的定位相关
  • #由于不影响对远程服务器的访问,自然也不会存在于软件包的下载连接中

URL的正则表达式

参考博客:https://blog.csdn.net/qq_25384945/article/details/81219075

  • Python

    1
    http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+
  • JavaScript

    1
    /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/
  • Java

    1
    ^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]

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——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…67
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

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