云计算百科
云计算领域专业知识百科平台

python打卡第52天

知识点回顾:

  • 随机种子
  • 内参的初始化
  • 神经网络调参指南
  • 参数的分类
  • 调参的顺序
  • 各部分参数的调整心得
  •  ## 随机种子

    import torch
    import torch.nn as nn

    # 定义简单的线性模型(无隐藏层)
    # 输入2个纬度的数据,得到1个纬度的输出
    class SimpleNet(nn.Module):
    def __init__(self):
    super(SimpleNet, self).__init__()
    # 线性层:2个输入特征,1个输出特征
    self.linear = nn.Linear(2, 1)

    def forward(self, x):
    # 前向传播:y = w1*x1 + w2*x2 + b
    return self.linear(x)

    # 创建模型实例
    model = SimpleNet()

    # 查看模型参数
    print("模型参数:")
    for name, param in model.named_parameters():
    print(f"{name}: {param.data}")

    ### 随机种子

    之前我们说过,torch中很多场景都会存在随机数

    1. 权重、偏置的随机初始化

    2. 数据加载(shuffling打乱)与批次加载(随机批次加载)的随机化

    3. 数据增强的随机化(随机旋转、缩放、平移、裁剪等)

    4. 随机正则化dropout

    5. 优化器中的随机性

    import torch
    import numpy as np
    import os
    import random

    # 全局随机函数
    def set_seed(seed=42, deterministic=True):
    """
    设置全局随机种子,确保实验可重复性

    参数:
    seed: 随机种子值,默认为42
    deterministic: 是否启用确定性模式,默认为True
    """
    # 设置Python的随机种子
    random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed) # 确保Python哈希函数的随机性一致,比如字典、集合等无序

    # 设置NumPy的随机种子
    np.random.seed(seed)

    # 设置PyTorch的随机种子
    torch.manual_seed(seed) # 设置CPU上的随机种子
    torch.cuda.manual_seed(seed) # 设置GPU上的随机种子
    torch.cuda.manual_seed_all(seed) # 如果使用多GPU

    # 配置cuDNN以确保结果可重复
    if deterministic:
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

    # 设置随机种子
    set_seed(42)

     

    介绍一下这个随机函数的几个部分

    1. python的随机种子,需要确保random模块、以及一些无序数据结构的一致性

    2. numpy的随机种子,控制数组的随机性

    3. torch的随机种子,控制张量的随机性,在cpu和gpu上均适用

    4. cuDNN(CUDA Deep Neural Network library ,CUDA 深度神经网络库)的随机性,针对cuda的优化算法的随机性

    上述种子可以处理大部分场景,实际上还有少部分场景(具体的函数)可能需要自行设置其对应的随机种子。

    日常使用中,在最开始调用这部分已经足够

    我们都知道,神经网络的权重需要通过反向传播来实现更新,那么最开始肯定需要一个值才可以更新参数

    这个最开始的值是什么样子的呢?如果恰好他们就是那一组最佳的参数附近的数,那么可能我训练的速度会快很多

    为了搞懂这个问题,帮助我们真正理解神经网络参数的本质,我们需要深入剖析一下,关注以下几个问题:

    1. 初始值的区间

    2. 初始值的分布

    3. 初始值是多少

    先介绍一下神经网络的对称性—-为什么神经元的初始值需要各不相同?

    本质神经网络的每一个神经元都是在做一件事,输入x–输出y的映射,这里我们假设激活函数是sigmoid

    y=sigmoid(wx+b),其中w是连接到该神经元的权重矩阵,b是该神经元的偏置

    如果所有神经元的权重和偏置都一样,

    1. 如果都为0,那么所有神经元的输出都一致,无法区分不同特征;此时反向传播的时候梯度都一样,无法学习到特征,更新后的权重也完全一致。

    2. 如果不为0,同上

    所以,无论初始值是否为 0,相同的权重和偏置会导致神经元在训练过程中始终保持同步。(因为神经网络的前向传播是导致权重的数学含义是完全对称的)具体表现为:

    同一层的神经元相当于在做完全相同的计算,无论输入如何变化,它们的输出模式始终一致。例如:输入图像中不同位置的边缘特征,会被这些神经元以相同方式处理,无法学习到空间分布的差异。

    所以需要随机初始化,让初始的神经元各不相同。即使初始差异很小,但激活函数的非线性(梯度不同)会放大这种差异。随着训练进行,这种分歧会逐渐扩大,最终形成功能各异的神经元。

    所以,明白了上述思想,你就知道初始值之前的差异并不需要巨大。

    事实上,神经网络的初始权重通常设置在接近 0 的小范围内(如 [-0.1, 0.1] 或 [-0.01, 0.01]),或通过特定分布(如正态分布、均匀分布)生成小值,有很多好处 ![image.png](attachment:image.png)

    避免梯度消失 / 爆炸: 以 sigmoid 激活函数为例,其导数在输入绝对值较大时趋近于 0(如 | x|>5 时,导数≈0)。若初始权重过大,输入 x=w・input+b 可能导致激活函数进入 “饱和区”,反向传播时梯度接近 0,权重更新缓慢(梯度消失)。 类比:若初始权重是 “大值”,相当于让神经元一开始就进入 “极端状态”,失去对输入变化的敏感度。

    如果梯度相对较大,就可以让变化处于sigmoid函数的非饱和区

    所以其实对于不同的激活函数 ,都有对应的饱和区和非饱和区,深层网络中,饱和区会使梯度在反向传播时逐层衰减,底层参数几乎无法更新;

    注意下,这里是wx后才会经过激活函数,是多个权重印象的结果,不是收到单个权重决定的,所以单个权重可以取负数,但是如果求和后仍然小于0,那么输出会为0  

    import torch
    import torch.nn as nn
    import matplotlib.pyplot as plt
    import numpy as np

    # 设置设备
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # 定义极简CNN模型(仅1个卷积层+1个全连接层)
    class SimpleCNN(nn.Module):
    def __init__(self):
    super(SimpleCNN, self).__init__()

    # 卷积层:输入3通道,输出16通道,卷积核3×3
    self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)

    # 池化层:2×2窗口,尺寸减半
    self.pool = nn.MaxPool2d(kernel_size=2)

    # 全连接层:展平后连接到10个输出(对应10个类别)
    # 输入尺寸:16通道 × 16×16特征图 = 16×16×16=4096
    self.fc = nn.Linear(16 * 16 * 16, 10)

    def forward(self, x):
    # 卷积+池化
    x = self.pool(self.conv1(x)) # 输出尺寸: [batch, 16, 16, 16]

    # 展平
    x = x.view(-1, 16 * 16 * 16) # 展平为: [batch, 4096]

    # 全连接
    x = self.fc(x) # 输出尺寸: [batch, 10]

    return x

    # 初始化模型
    model = SimpleCNN()
    model = model.to(device)

    # 查看模型结构
    print(model)

    # 查看初始权重统计信息
    def print_weight_stats(model):
    # 卷积层
    conv_weights = model.conv1.weight.data
    print("\\n卷积层 权重统计:")
    print(f" 均值: {conv_weights.mean().item():.6f}")
    print(f" 标准差: {conv_weights.std().item():.6f}")
    print(f" 理论标准差 (Kaiming): {np.sqrt(2/3):.6f}") # 输入通道数为3

    # 全连接层
    fc_weights = model.fc.weight.data
    print("\\n全连接层 权重统计:")
    print(f" 均值: {fc_weights.mean().item():.6f}")
    print(f" 标准差: {fc_weights.std().item():.6f}")
    print(f" 理论标准差 (Kaiming): {np.sqrt(2/(16*16*16)):.6f}")

    # 改进的可视化权重分布函数
    def visualize_weights(model, layer_name, weights, save_path=None):
    plt.figure(figsize=(12, 5))

    # 权重直方图
    plt.subplot(1, 2, 1)
    plt.hist(weights.cpu().numpy().flatten(), bins=50)
    plt.title(f'{layer_name} 权重分布')
    plt.xlabel('权重值')
    plt.ylabel('频次')

    # 权重热图
    plt.subplot(1, 2, 2)
    if len(weights.shape) == 4: # 卷积层权重 [out_channels, in_channels, kernel_size, kernel_size]
    # 只显示第一个输入通道的前10个滤波器
    w = weights[:10, 0].cpu().numpy()
    plt.imshow(w.reshape(-1, weights.shape[2]), cmap='viridis')
    else: # 全连接层权重 [out_features, in_features]
    # 只显示前10个神经元的权重,重塑为更合理的矩形
    w = weights[:10].cpu().numpy()

    # 计算更合理的二维形状(尝试接近正方形)
    n_features = w.shape[1]
    side_length = int(np.sqrt(n_features))

    # 如果不能完美整除,添加零填充使能重塑
    if n_features % side_length != 0:
    new_size = (side_length + 1) * side_length
    w_padded = np.zeros((w.shape[0], new_size))
    w_padded[:, :n_features] = w
    w = w_padded

    # 重塑并显示
    plt.imshow(w.reshape(w.shape[0] * side_length, -1), cmap='viridis')

    plt.colorbar()
    plt.title(f'{layer_name} 权重热图')

    plt.tight_layout()
    if save_path:
    plt.savefig(f'{save_path}_{layer_name}.png')
    plt.show()

    # 打印权重统计
    print_weight_stats(model)

    # 可视化各层权重
    visualize_weights(model, "Conv1", model.conv1.weight.data, "initial_weights")
    visualize_weights(model, "FC", model.fc.weight.data, "initial_weights")

    # 可视化偏置
    plt.figure(figsize=(12, 5))

    # 卷积层偏置
    conv_bias = model.conv1.bias.data
    plt.subplot(1, 2, 1)
    plt.bar(range(len(conv_bias)), conv_bias.cpu().numpy())
    plt.title('卷积层 偏置')

    # 全连接层偏置
    fc_bias = model.fc.bias.data
    plt.subplot(1, 2, 2)
    plt.bar(range(len(fc_bias)), fc_bias.cpu().numpy())
    plt.title('全连接层 偏置')

    plt.tight_layout()
    plt.savefig('biases_initial.png')
    plt.show()

    print("\\n偏置统计:")
    print(f"卷积层偏置 均值: {conv_bias.mean().item():.6f}")
    print(f"卷积层偏置 标准差: {conv_bias.std().item():.6f}")
    print(f"全连接层偏置 均值: {fc_bias.mean().item():.6f}")
    print(f"全连接层偏置 标准差: {fc_bias.std().item():.6f}")

    那我们监控权重图的目的是什么呢?

    训练时,权重会随反向传播迭代更新。通过权重分布图,能直观看到其从初始化(如随机分布)到逐渐收敛、形成规律模式的动态变化,理解模型如何一步步 “学习” 特征 。比如,卷积层权重初期杂乱,训练后可能聚焦于边缘、纹理等特定模式。

    识别梯度异常:

    1. 梯度消失:若权重分布越来越集中在 0 附近,且更新幅度极小,可能是梯度消失,模型难学到有效特征(比如深层网络用 Sigmoid 激活易出现 )。

    2. 梯度爆炸:权重值突然大幅震荡、超出合理范围(比如从 [-0.1, 0.1] 跳到 [-10, 10] ),要警惕梯度爆炸,可能让训练崩溃。

    借助tensorboard可以看到训练过程中权重图的变化

    ## 神经网络调参指南

    大部分时候,由于光是固定超参数的情况下,训练完模型就已经很耗时了,所以正常而言,基本不会采用传统机器学习的那些超参数方法,网格、贝叶斯、optuna之类的,看到一些博主用这些写文案啥的,感觉这些人都是脑子有问题的,估计也没学过机器学习直接就学深度学习了,搞混了二者的关系。

    工业界卡特别多的情况下,可能可以考虑,尤其是在探究一个新架构的时候,我们直接忽视这些即可,只有手动调参这一条路。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » python打卡第52天
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!