批量归一化

为了加快神经网络训练的收敛,以及提高训练的稳定性,我们会将输入特征进行归一化,使其分布趋近于正态分布。然而,随着网络层数的增加,层次较高的网络,其输入的分布仍然有可能发生明显的变化,包括均值的偏移和方差的缩放。网络输入分布的变化可能会带来如下风险:

1、落入到激活函数饱和区的数据比例过高,导致出现梯度消失的问题,参数得不到有效的优化。

2、如果激活函数是ReLU,如果输入的数据幅度过大,会导致其输出过大,从而使得高一层网络的权重梯度过大。

3、各输入节点所对应的数据方差尺度不统一,可能会影响训练过程中参数的收敛。

批量归一化 (Batch Normalization,简写为BN)是一种对中间层数据 进行归一化 的方法。设中间层数据为,则对于一个微批的输入数据 ,相应的中间层数据为 。 批量归一化的计算公式为:

其中,分别为基于微批计算的均值和标准差:

是两个可训练的参数(可选)(基于梯度下降学习更新)。当激活函数是Sigmoid时,如果简单将输入数据归一化到0附近,可能会影响网络的非线性表达,因此增加了两个可训练的参数。但如果激活函数是ReLU,这两个参数的作用就不明显了。

在PyTorch中,BN也被封装为一层网络 (即nn.Module的子类。在BN对象的内部保存着running_mean和running_var这两个统计变量,在训练模式下 (self.training=True), running_mean和running_var基于输入的微批数据进行更新(不是基于梯度下降的)。在训练结束时,这两个统计变量会随 BN层保存在模型中。在模型的评测阶段,会基于模型中保存的running_mean和running_var来进行归一化,而此时这两个统计变量就不会随数据再进行更新了。

训练时可以控制微批的大小,测试/推理(eval/inference)时不可控制微批的大小。(部署好算法服务上线后等别人来调,可能只有一个样本入参,也可能有多个样本入参)

  • inference 是工程执行层面的 "前向计算过程"
  • reasoning 是能力任务层面的 "逻辑推导能力"

1. Inference(模型推理 / 前向推断)

  • 定位 :和「训练(training)」相对,是工程执行层面的概念。
  • 含义 :训练完成的模型接收输入,通过前向计算得到输出的完整执行过程,统称为 inference。
    • 它不关心模型在做什么任务(是图像分类、文本生成、解题还是对话),只关心 "模型跑起来、算出结果" 这个动作本身。
    • BatchNorm 在 model.eval() 模式下的前向传播,就属于 inference 阶段。
  • 关注指标:推理速度、显存占用、延迟、吞吐量、部署兼容性等工程属性。
  • 常见行业术语:推理部署、推理引擎(TensorRT / ONNX Runtime)、推理加速、端侧推理、批量推理。

2. Reasoning(逻辑推理 / 推理能力)

  • 定位 :属于任务 / 能力层面的概念,描述模型的一种高阶认知能力。
  • 含义 :指模型通过多步逻辑推导、因果分析、规则运用来解决问题的能力,是对模型 "智力水平" 的描述。
    • 典型场景:解数学题、代码排错、法律案例分析、常识因果判断、多步规划。
    • 大模型领域的思维链(CoT)、工具调用,核心目的就是提升模型的 reasoning 能力。
  • 关注指标:推理准确率、步骤正确性、逻辑一致性等能力属性。
  • 常见行业术语:多步推理、常识推理、数学推理、推理能力、推理基准(如 GSM8K、MMLU)。

1、nn.BatchNorm2d (适用于卷积层):

BatchNorm2d --- PyTorch 2.13 documentationhttps://docs.pytorch.org/docs/2.13/generated/torch.nn.BatchNorm2d.html#torch.nn.BatchNorm2d

2、nn.BatchNorm1d (适用于全连接层):

BatchNorm1d --- PyTorch 2.13 documentationhttps://docs.pytorch.org/docs/2.13/generated/torch.nn.BatchNorm1d.html#batchnorm1d

我们来观察一下running_mean和running_var被保存的情况:

test_bn.py:

python 复制代码
import torch.nn as nn
import torch

# 创建一个BatchNorm层对象
bn = nn.BatchNorm2d(num_features=10, track_running_stats=True, affine=False)

# 创建数据
x1 = torch.randn(100, 10, 28, 28) # (batch_size, num_features, hiehgt, width)
x2 = torch.randn(100, 10, 28, 28) # (batch_size, num_features, hiehgt, width)

# 训练模式
bn.train()


y1 = bn(x1)
# 查看bn统计量
print(f'训练 bn1.running_mean:{bn.running_mean}')
print(f'训练 bn1.running_mean 和 torch.mean(x1, dim=[0, 2, 3])*0.1) 是否相等:{torch.allclose(bn.running_mean, torch.mean(x1, dim=[0, 2, 3])*0.1)}')  # true

print(f'训练 bn1.running_var:{bn.running_var}')
# print(torch.allclose(bn.running_var, torch.var(x1, dim=[0, 2, 3])*0.1))  # false

print("\n")

last_running_mean = bn.running_mean.clone()


y2 = bn(x2)
# 查看bn统计量
print(f'训练 bn2.running_mean:{bn.running_mean}')
print(f'训练 bn2.running_mean 和 last_running_mean * 0.9 + 0.1 * torch.mean(x2, dim=[0, 2, 3])) 是否相等:{torch.allclose(bn.running_mean, last_running_mean * 0.9 + 0.1 * torch.mean(x2, dim=[0, 2, 3]))}') # true

print(f'训练 bn2.running_var:{bn.running_var}')

print("\n")

last_running_mean = bn.running_mean.clone()


# 保存模型
torch.save(bn.state_dict(), "bn_model.pth")

# 加载模型
bn2 = nn.BatchNorm2d(num_features=10, track_running_stats=True, affine=False)
bn2.load_state_dict(torch.load("bn_model.pth", weights_only=True))
# 查看加载的统计量
print(f'加载模型的 bn2.running_mean:{bn2.running_mean}')
print(f'加载模型的 bn2.running_mean 和 last_running_mean 是否相等:{torch.allclose(bn2.running_mean, last_running_mean)}')
print(f'加载模型的 bn2.running_var:{bn2.running_var}')

print("\n")


# 推理(评测模式)
bn2.eval()
x3 = torch.randn(100, 10, 28, 28) # (batch_size, num_features, hiehgt, width)
y3 = bn2(x3)
print(f'推理 bn2.running_mean:{bn2.running_mean}')

我们接下来观察一下MNIST数据集上的CNN实验,如果在网络结构中增加了BN层 ,模型的训练情况会发生什么样的变化------收敛速度快效果好。

python 复制代码
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset, random_split
from torch.optim import SGD
from torchvision import datasets, transforms
from typing import Union
import os
from utils import *
#from torch.utils.tensorboard import SummaryWriter


def train(model : nn.Module,
          train_dataset : Dataset,
          valid_dataset : Dataset,
          loss_fn : nn.Module,
          optimizer : torch.optim.Optimizer,
          config : dict) -> None:
    # data loader
    train_dataloader = DataLoader(train_dataset, batch_size=config['train_batch_size'], shuffle=True, drop_last=True)
    eval_dataloader = DataLoader(valid_dataset, batch_size=config['eval_batch_size'], shuffle=False, drop_last=False)
    
    # move model to device
    model = model.to(config['device'])
    
    # 训练的主流程
    best_valid_loss = 1e10
    no_improve_epochs = 0
    
    step = 0
    for epoch in range(config['epochs']):
        model.train()
        
        train_loss = []
        for batch_x, batch_y in train_dataloader:
            # 把数据导入到设备中
            batch_x = batch_x.to(config['device'])
            batch_y = batch_y.to(config['device'])
            
            # forward
            preds = model(batch_x)
            loss = loss_fn(preds, batch_y)
            train_loss.append(loss)
            
            # tensorboard可视化
            #writer.add_scalar('minibatch loss', loss, step)
            
            # backward
            loss.backward()
            
            # update
            optimizer.step()
            step += 1
            
            # 梯度清零
            optimizer.zero_grad()
            
        train_mean_loss = sum(train_loss) / len(train_loss)
        
        # 在验证集上测试
        valid_mean_loss, valid_mean_accuracy = test(model, eval_dataloader, loss_fn, config)
        print(f'epoch: {epoch}, train_loss: {train_mean_loss:.5f}, valid_loss: {valid_mean_loss:.5f}, valid_accuracy: {valid_mean_accuracy:.5f}')
        #writer.add_scalars('epoch loss', {'train': train_mean_loss, 'valid': valid_mean_loss}, epoch)
        
        if valid_mean_loss < best_valid_loss:
            best_valid_loss = valid_mean_loss
            no_improve_epochs = 0
            torch.save(model.state_dict(), config['save_path'])
        else:
            no_improve_epochs += 1
            if no_improve_epochs > config['max_no_improve_epochs']:
                print(f'early stop at epoch: {epoch}')
                break


def test(model : nn.Module, data : Union[DataLoader, Dataset], loss_fn : nn.Module, config : dict) -> list : 
    if next(iter(model.parameters())).device.type != config['device']:
        model = model.to(config['device'])
    model.eval()
    
    if isinstance(data, DataLoader):
        dataloader = data
    else:
        dataloader = DataLoader(data, batch_size=config['eval_batch_size'], shuffle=False, drop_last=False)
    
    loss = []
    accuracy = []
    batch_num = []
    
    with torch.no_grad():
        for batch_x, batch_y in dataloader:
            batch_x = batch_x.to(config['device'])
            batch_y = batch_y.to(config['device'])
            preds = model(batch_x)
            loss.append(loss_fn(preds, batch_y.to(config['device'])))
            accuracy.append((torch.argmax(preds, dim=-1).reshape(-1) == batch_y).float().mean())
            batch_num.append(len(batch_x))
            
    mean_loss = sum([loss[i] * batch_num[i] for i in range(len(loss))]) / sum(batch_num)
    mean_accuracy = sum([accuracy[i] * batch_num[i] for i in range(len(accuracy))]) / sum(batch_num)
    
    return mean_loss, mean_accuracy


def same_seed(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False


if __name__ == '__main__':
    do_training = True
    
    config = {
        "seed": 0,
        "lr": 0.01,
        "weight_decay": 0.001,
        'train_batch_size': 32,
        'eval_batch_size': 32,
        "epochs": 1000,
        "max_no_improve_epochs": 10,
        "valid_ratio": 0.1,
        "save_path": "./model/cnn.ckpt",
        "device": 'cuda' if torch.cuda.is_available() else 'cpu',
        'log_dir': './log/'
    }
    
    if os.path.isdir(config['log_dir']):
        os.system(f"rm -fr {config['log_dir'] + os.sep + '*'}")
    #writer = SummaryWriter("./log/")
    
    # set seed
    same_seed(config['seed'])
    
    # 检查存放模型的目录是否存在,如果不存在则创建
    model_dir = os.path.split(config['save_path'])[0]
    if not os.path.isdir(model_dir):
        os.makedirs(model_dir)
    
    # dataset
    transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.5,), (0.5,))
    ])
    
    train_dataset = datasets.MNIST("../mnist", train=True, download=True, transform=transforms)
    test_dataset = datasets.MNIST("../mnist", train=False, download=True, transform=transforms)
    
    # 拆分训练数据和验证数据
    train_size = int(len(train_dataset) * (1 -config['valid_ratio']))
    valid_size = len(train_dataset) - train_size
    train_dataset, valid_dataset = random_split(train_dataset, [train_size, valid_size])
    
    # 创建model
    model = nn.Sequential(
        nn.Conv2d(in_channels=1, out_channels=6, kernel_size=(5, 5), stride=(1,1), padding=(2, 2)), #output: 6*28*28
        nn.BatchNorm2d(num_features=6),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2)),    # output: 6*14*14

        nn.Conv2d(in_channels=6, out_channels=16, kernel_size=(5, 5), stride=(1, 1), padding=(0, 0)), # output: 16*10*10
        nn.BatchNorm2d(num_features=16),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2)),    # output: 16*5*5

        nn.Flatten(),
        nn.Linear(in_features=16*5*5, out_features=120),   #output: 120
        nn.BatchNorm1d(num_features=120),
        nn.ReLU(),
        nn.Linear(in_features=120, out_features=84),    #output: 84
        nn.BatchNorm1d(num_features=84),
        nn.ReLU(),
        nn.Linear(in_features=84, out_features=10)      #output: 10
    )
    

    # loss function
    loss_fn = nn.CrossEntropyLoss(reduction='mean')
    
    # optimizer
    optimizer = SGD(model.parameters(), lr=config['lr'], weight_decay=config['weight_decay'])
    
    if do_training:
        train(model, train_dataset, valid_dataset, loss_fn, optimizer, config)
        torch.save(model.state_dict(), config['save_path'])
        
    # 加载模型参数
    model.load_state_dict(torch.load(config['save_path'], weights_only =True))
    
    # 测试模型
    test_loss, test_accuracy = test(model, test_dataset, loss_fn, config)
    print(f'test_loss: {test_loss:.5f}, test_accuracy: {test_accuracy:.5f}')

设置affine=False,收敛速度效果会差点:

相关推荐
牧濑红莉1 小时前
零基础认识大语言模型(LLM)工作原理(2.Token 到底是什么?)
人工智能·语言模型·chatgpt
AImoon11.11 小时前
客易云关注“人工智能+”行动深化,智能经济新形态写入政府工作报告
人工智能
IT_陈寒1 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端
Earth explosion1 小时前
大规模向量库的索引选型与查询性能调优:Milvus 实战指南
人工智能·milvus·ai智能体
2601_955759622 小时前
code0 gemini-2.5-pro 企业实战:数据分析团队怎样更快产出报告
大数据·人工智能·数据分析
Ivanqhz2 小时前
刚体的自由度
人工智能·算法
ZZZMMM.zip2 小时前
数据侦探社-数据趋势分析的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙·鸿蒙系统
2601_955759412 小时前
code0 gpt-5.5 场景相关:教育平台智能批改与答疑实操
大数据·人工智能·gpt
KaMeidebaby2 小时前
卡梅德生物技术快报|抗体合成:多肽抗体合成工程化方案:Nsp2 保守肽多抗制备与多维度验证
前端·网络·数据库·人工智能·算法