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——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——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])
    • 这种情况同样会触发高级索引

1…323334…63
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

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