首页 > 其他分享 >十四、神经网络工具箱nn

十四、神经网络工具箱nn

时间:2024-10-29 09:35:09浏览次数:6  
标签:__ nn self Module 神经网络 module input 工具箱

  使用autograd可实现深度学习模型,但其抽象程度较低,如果用其来实现深度学习模型,则需要编写的代码量极大。在这种情况下,torch.nn应运而生,其是专门为深度学习而设计的模块。torch.nn的核心数据结构是Module,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络/层。

  下面先来看看如何用nn.Module实现自己的全连接层。全连接层,又名仿射层,输出y和输入x满足y=Wx+b,W和b是可学习的参数。

import torch as t
from torch import nn

# 定义一个继承自nn.Module的线性层类Linear
class Linear(nn.Module):
    # 初始化函数,定义输入和输出特征维度
    def __init__(self, in_features, out_features):
        # 调用父类nn.Module的初始化方法
        super(Linear, self).__init__()
        # 创建可学习的权重参数w,形状为[in_features, out_features]
        self.w = nn.Parameter(t.randn(in_features, out_features))
        # 创建可学习的偏置参数b,形状为[out_features]
        self.b = nn.Parameter(t.randn(out_features))

    # 定义前向传播函数
    def forward(self, x):
        # 使用矩阵乘法计算输入x和权重w的乘积,输出形状为[batch_size, out_features]
        x = x.mm(self.w)
        # 将偏置b扩展到与x相同的形状,并将其加到结果上
        return x + self.b.expand_as(x)

# 实例化Linear类,指定输入特征维度为4,输出特征维度为3
layer = Linear(4, 3)
# 创建一个形状为[2, 4]的随机输入张量,代表2个样本,每个样本有4个特征
input = t.randn(2, 4)
# 将输入张量传入线性层,得到输出结果
output = layer(input)
print(output)

# 遍历并打印线性层中的参数名称和对应的张量
for name, parameter in layer.named_parameters():
    print(name, parameter)
    
tensor([[ 1.3758, -0.3045, -2.7855],
        [ 2.0449,  2.1037, -3.9691]], grad_fn=<AddBackward0>)
w Parameter containing:
tensor([[-0.7288,  0.9462,  0.1709],
        [-0.5582, -0.1212,  1.1237],
        [-0.4097, -0.2673, -0.9135],
        [-0.0122,  2.1431, -0.0641]], requires_grad=True)
b Parameter containing:
tensor([ 0.9055, -0.0583, -1.6974], requires_grad=True)

全连接层的实现比较简单,但需注意以下几点:

  • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear, self).__init__() 或nn.Module.__init__(self),推荐使用第一种用法,尽管第二种写法更直观。
  • 在构造函数__init__中必须自己定义可学习的参数,并封装成Parameter,如在本例中我们把wb封装成parameterparameter是一种特殊的Tensor但其默认需要求导(requires_grad = True)
  • forward函数实现前向传播过程,其输入可以是一个或多个tensor。
  • 无需写反向传播函数,nn.Module能够利用autograd自动实现反向传播,这点比Function简单许多。
  • 使用时,直观上可将layer看成数学概念中的函数,调用layer(input)即可得到input对应的结果。它等价于layers.__call__(input),在__call__函数中,主要调用的是 layer.forward(x),另外还对钩子做了一些处理。所以在实际使用中应尽量使用layer(x)而不是使用layer.forward(x)
  • Module中的可学习参数可以通过named_parameters()或者parameters()返回迭代器,前者会给每个parameter都附上名字,使其更具有辨识度。

  Module能够自动检测到自己的Parameter,并将其作为学习参数。除了parameter之外,Module还包含子Module主Module能够递归查找子Module中的parameter。下面再来看看稍微复杂一点的网络,多层感知机。

  层感知机的网络结构如下图所示,它由两个全连接层组成,采用sigmoid函数作为激活函数,图中没有画出。

    

class Perceptron(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        nn.Module.__init__(self)
        # 定义第一层,输入特征数为 in_features,隐藏层特征数为 hidden_features
        self.layer1 = Linear(in_features, hidden_features)
        # 定义第二层,输入特征数为 hidden_features,输出特征数为 out_features
        self.layer2 = Linear(hidden_features, out_features)

    # 定义前向传播函数 forward
    def forward(self, x):
        # 首先将输入 x 传入第一层,得到隐层输出
        x = self.layer1(x)
        # 对隐层输出应用 sigmoid 激活函数
        x = t.sigmoid(x)
        # 将激活后的输出传入第二层,得到最终输出
        return self.layer2(x)

# 创建感知器实例,输入特征数为3,隐藏层特征数为4,输出特征数为1
Perceptron = Perceptron(3, 4, 1)

# 打印感知器中的参数名称和对应的值
for name, param in Perceptron.named_parameters():
    print(name, param)
    
layer1.w Parameter containing:
tensor([[-0.0791, -1.0982,  1.1377,  1.1678],
        [ 0.3987, -1.3077, -1.0768,  0.1234],
        [ 0.3717, -1.4077, -0.2922,  0.5084]], requires_grad=True)
layer1.b Parameter containing:
tensor([0.3347, 0.5307, 1.2622, 1.9496], requires_grad=True)
layer2.w Parameter containing:
tensor([[ 0.5427],
        [-0.9131],
        [ 0.0052],
        [-1.5111]], requires_grad=True)
layer2.b Parameter containing:
tensor([-0.9579], requires_grad=True)

  可见,即使是稍复杂的多层感知机,其实现依旧很简单。 构造函数__init__中,可利用前面自定义的Linear层(module),作为当前module对象的一个子module,它的可学习参数,也会成为当前module的可学习参数。

module中parameter的命名规范:

  • 对于类似self.param_name = nn.Parameter(t.randn(3, 4)),命名为param_name
  • 对于子Module中的parameter,会其名字之前加上当前Module的名字。如对于self.sub_module = SubModel(),SubModel中有个parameter的名字叫做param_name,那么二者拼接而成的parameter name 就是sub_module.param_name

在查阅函数使用文档时,要关注以下几点:

  • 构造函数的参数,如nn.Linear(in_features, out_features, bias),需关注这三个参数的作用。
  • 属性、可学习参数和子module。如nn.Linear中有weightbias两个可学习参数,不包含子module。
  • 输入输出的形状,如nn.linear的输入形状是(N, input_features),输出为(N,output_features),N是batch_size。

  这些自定义layer对输入形状都有假设:输入的不是单个数据,而是一个batch。输入只有一个数据,则必须调用tensor.unsqueeze(0) 或 tensor[None]将数据伪装成batch_size=1的batch。

1.1 常用神经网络层

1.1.1 图像相关层

  图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中可分为一维(1D)、二维(2D)、三维(3D),池化方式又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。而卷积层除了常用的前向卷积之外,还有逆卷积(TransposeConv)。下面举例说明一些基础的使用。

from PIL import Image  # Python Imaging Library,用于图像处理
from torchvision.transforms import ToTensor, ToPILImage  # 转换工具,从图像到张量,和从张量到图像的转换
import torch as t
from torch import nn  # 引入PyTorch的神经网络模块

# 创建图像到张量的转换器
to_tensor = ToTensor()  # 将图像转换为张量,范围为[0, 1]
# 创建张量到图像的转换器
to_pil = ToPILImage()  # 将张量转换回图像
# 打开一幅名为 'lena.png' 的图像
lena = Image.open('imgs/lena.png')

# 将图像转换为张量,并增加一个维度以形成一个批次,batch_size = 1
input = to_tensor(lena).unsqueeze(0)  # input 形状为 [1, C, H, W],C 为通道数,H 为高度,W 为宽度
# 定义锐化卷积核,卷积核用于增强图像的边缘
# 创建一个 3x3 的张量,初始值为 -1/9,其他值为 1
kernel = t.ones(3, 3) / -9.0  # 所有元素为 -1/9
kernel[1][1] = 1  # 中间的元素设为 1,形成锐化效果
# 创建一个卷积层,输入通道数和输出通道数都为 1,卷积核的大小为 (3, 3),步幅为 1
conv = nn.Conv2d(1, 1, (3, 3), stride=1, bias=False)
# 将定义好的卷积核赋值给卷积层的权重
conv.weight.data = kernel.view(1, 1, 3, 3)  # kernel 需要调整形状为 [out_channels, in_channels, height, width]
# 对输入张量进行卷积运算,得到输出
out = conv(input)
# 将卷积输出张量转换回图像,并去掉批次维度
# out.data.squeeze(0) 将输出的形状从 [1, 1, H, W] 转换为 [1, H, W]
output_image = to_pil(out.data.squeeze(0))  # 转换为 PIL 图像

  池化层可以看作是一种特殊的卷积层,用来下采样。但池化层没有可学习参数,其weight是固定的。

    

除了卷积层和池化层,深度学习中还将常用到以下几个层:

  • Linear:全连接层。
  • BatchNorm:批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
  • Dropout:dropout层,用来防止过拟合,同样分为1D、2D和3D。

  下面通过例子来说明它们的使用。

# 输入 batch_size=2,维度3
input = t.randn(2, 3)
linear = nn.Linear(3, 4)
h = linear(input)
h

## tensor([[ 0.6993, -1.1460,  0.5710, -0.2496],
##         [-0.1921,  0.8154, -0.3038,  0.1873]])

# 4 channel,初始化标准差为4,均值为0
bn = nn.BatchNorm1d(4)
bn.weight.data = t.ones(4) * 4
bn.bias.data = t.zeros(4)

bn_out = bn(h)
# 注意输出的均值和方差
# 方差是标准差的平方,计算无偏方差分母会减1
# 使用unbiased=False 分母不减1
bn_out.mean(0), bn_out.var(0, unbiased=False)

## (tensor(1.00000e-07 *
##         [ 1.1921,  0.0000,  0.0000,  0.0000]),
##  tensor([ 15.9992,  15.9998,  15.9992,  15.9966]))

# 每个元素以0.5的概率舍弃
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
o # 有一半左右的数变为0

## tensor([[ 7.9998, -8.0000,  0.0000, -7.9992],
##         [-0.0000,  8.0000, -7.9998,  7.9992]])

1.1.2 激活函数

  PyTorch实现了常见的激活函数,其具体的接口信息可参见官方文档,这些激活函数可作为独立的layer使用。这里将介绍最常用的激活函数ReLU,其数学表达式为:

relu = nn.ReLU(inplace=True)
input = t.randn(2, 3)
print(input)
output = relu(input)
print(output) # 小于0的都被截断为0
# 等价于input.clamp(min=0)

  ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如tensor.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作

  在以上的例子中,基本上都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(feedforward neural network)。对于此类网络如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的module,它包含几个子Module,前向传播时会将输入一层接一层的传递下去。ModuleList也是一个特殊的module,可以包含几个子module,可以像用list一样使用它,但不能直接把输入传给ModuleList。下面举例说明。

import torch.nn as nn
from collections import OrderedDict

# net1 使用 nn.Sequential() 和 add_module 方法逐步添加模块
net1 = nn.Sequential()

# 添加卷积层 'conv'
# Conv2d 参数含义:输入通道数(3),输出通道数(3),卷积核大小(3x3)
net1.add_module('conv', nn.Conv2d(3, 3, 3))

# 添加批归一化层 'batchnorm'
# BatchNorm2d 参数含义:需要归一化的通道数为 3
net1.add_module('batchnorm', nn.BatchNorm2d(3))

# 添加激活函数 ReLU 'activation_layer'
# ReLU 是一种常用的激活函数,将负值置为 0,保持正值不变
net1.add_module('activation_layer', nn.ReLU())

# net2 使用 nn.Sequential 直接传递模块的顺序列表
# 其中包含卷积层、批归一化层和 ReLU 激活函数
net2 = nn.Sequential(
    nn.Conv2d(3, 3, 3),       # 卷积层:输入通道 3,输出通道 3,卷积核 3x3
    nn.BatchNorm2d(3),        # 批归一化:通道数为 3
    nn.ReLU()                 # ReLU 激活函数
)

# net3 使用 OrderedDict 明确命名每个层,利用 nn.Sequential 构建模型
# OrderedDict 保证了字典中键值对的顺序
net3 = nn.Sequential(OrderedDict([
    ('conv1', nn.Conv2d(3, 3, 3)),   # 卷积层:命名为 conv1,输入通道 3,输出通道 3,卷积核 3x3
    ('bn1', nn.BatchNorm2d(3)),      # 批归一化层:命名为 bn1,通道数为 3
    ('relu1', nn.ReLU())             # ReLU 激活函数:命名为 relu1
]))

# 打印出 net1, net2 和 net3 的结构
print('net1:', net1)
print('net2:', net2)
print('net3:', net3)

## 输出结果
net1: Sequential(
  (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (activation_layer): ReLU()
)
net2: Sequential(
  (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (2): ReLU()
)
net3: Sequential(
  (conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu1): ReLU()
)

# 可根据名字或序号取出子module
net1.conv, net2[0], net3.conv1
(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
 Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
 Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))
import torch as t
import torch.nn as nn

# 其中 1 是批量大小 (batch size),3 是通道数 (channels),4x4 是特征图大小 (height x width)
input = t.rand(1, 3, 4, 4)
output = net1(input)
output = net2(input)
output = net3(input)
output = net3.relu1(net1.batchnorm(net1.conv(input)))


# 创建一个 nn.ModuleList,其中包含两个线性层和一个 ReLU 激活函数
modellist = nn.ModuleList([nn.Linear(3, 4), nn.ReLU(), nn.Linear(4, 2)])
# 创建一个形状为 (1, 3) 的随机输入张量
input = t.randn(1, 3)
# 通过遍历 modellist,将 input 依次通过每个层,手动向前传播
for model in modellist:
    input = model(input)   
# 下面会报错,因为 modellist 没有实现 forward 方法
output = modellist(input)

   需要注意的是,nn.ModuleList 是 PyTorch 中的一个容器,用来保存子模块的列表。你可以将多个模型层按顺序存储在 ModuleList 中,但它 自动不会实现 forward() 方法,可以向上述代码一样,手动遍历 modellist 来进行前向传播。

1.1.3 损失函数

  在深度学习中要用到各种各样的损失函数(loss function),这些损失函数可看作是一种特殊的layer,PyTorch也将这些损失函数实现为nn.Module的子类。然而在实际使用中通常将这些loss function专门提取出来,和主模型互相独立。下面以分类中最常用的交叉熵损失CrossEntropyloss为例说明。

import torch as t
import torch.nn as nn

# 创建一个形状为 (3, 2) 的张量,表示 3 个样本,每个样本有 2 个类别的分数
score = t.randn(3, 2)  # 随机生成的分数 (logits),每个样本两个类别
# 定义每个样本的真实标签
# 第一个样本属于类别 1,第二个样本属于类别 0,第三个样本属于类别 1
# 标签必须是 LongTensor 类型,因为 PyTorch 的 CrossEntropyLoss 要求标签类型为 long
label = t.Tensor([1, 0, 1]).long()
# 然后用真实的标签与预测的概率进行对比,计算损失
criterion = nn.CrossEntropyLoss()
# 计算损失,传入的参数是预测的分数 (logits) 和真实标签
# 这里 score 是模型的输出 (logits),label 是真实标签
loss = criterion(score, label)

1.2 优化器

  PyTorch将深度学习中常用的优化方法全部封装在torch.optim中,其设计十分灵活,能够很方便的扩展成自定义的优化方法。

  所有的优化方法都是继承基类optim.Optimizer,并实现了自己的优化步骤。下面就以最基本的优化方法——随机梯度下降法(SGD)举例说明。这里需重点掌握:

  • 优化方法的基本使用方法
  • 如何对模型的不同部分设置不同的学习率
  • 如何调整学习率
import torch.nn as nn

# 定义一个名为 Net 的神经网络类,继承自 nn.Module
class Net(nn.Module):
    # 初始化方法,定义网络结构
    def __init__(self):
        super(Net, self).__init__()  # 调用父类 nn.Module 的构造函数
        
        # 定义特征提取部分 (卷积层和池化层)
        self.features = nn.Sequential(
            nn.Conv2d(3, 6, 5),    # 第一个卷积层,输入通道为3 (RGB图像),输出通道为6,卷积核大小为5x5
            nn.ReLU(),             # ReLU 激活函数
            nn.MaxPool2d(2, 2),    # 第一个最大池化层,池化窗口大小为2x2,步长为2
            nn.Conv2d(6, 16, 5),   # 第二个卷积层,输入通道为6,输出通道为16,卷积核大小为5x5
            nn.ReLU(),             # ReLU 激活函数
            nn.MaxPool2d(2, 2)     # 第二个最大池化层,池化窗口大小为2x2,步长为2
        )
        
        # 定义分类器部分 (全连接层)
        self.classifier = nn.Sequential(
            nn.Linear(16 * 5 * 5, 120),  # 第一个全连接层,将16*5*5的张量转为120维的向量
            nn.ReLU(),                   # ReLU 激活函数
            nn.Linear(120, 84),          # 第二个全连接层,将120维向量转为84维
            nn.ReLU(),                   # ReLU 激活函数
            nn.Linear(84, 10)            # 第三个全连接层,将84维向量转为10维(输出为10个类别)
        )

    # 定义前向传播函数,描述数据如何通过网络
    def forward(self, x):
        x = self.features(x)           # 输入先经过特征提取部分 (卷积 + 池化)
        
        # 将四维的特征图展平为二维张量,用于输入全连接层
        # .view() 方法用于改变张量的形状,-1 表示自动推断这个维度的大小
        x = x.view(-1, 16 * 5 * 5)     # 将特征图展平为 (batch_size, 16*5*5) 的大小
        
        x = self.classifier(x)          # 然后输入到分类器部分 (全连接层)
        return x                        # 返回分类结果

# 实例化一个 Net 类,创建网络
net = Net()
from torch import optim
import torch as t

# params=net.parameters() 将网络的所有参数传递给优化器
# lr=1 是学习率,控制每次参数更新的步长
optimizer = optim.SGD(params=net.parameters(), lr=1)

# 在每次进行反向传播之前,需要将梯度清零
# PyTorch 中的梯度是累加的,因此在执行反向传播之前调用 zero_grad() 清除上一次的梯度
optimizer.zero_grad()  # 等价于 net.zero_grad(),将网络中所有参数的梯度置为0

# 创建一个形状为 (1, 3, 32, 32) 的随机输入张量,表示1个样本,3个通道,大小为32x32的图像
input = t.randn(1, 3, 32, 32)

# 通过网络前向传播,得到输出
output = net(input)

# 进行反向传播计算梯度
# output.backward(output) 是一种伪造的反向传播,通常 output 是损失函数的值
output.backward(output)  
# loss = criterion(output, target)  # 计算损失
# loss.backward()  # 反向传播计算梯度

# 执行优化步骤,更新网络参数
# optimizer.step() 会根据计算出的梯度和设置的学习率更新网络的参数
optimizer.step()
# 为不同子网络设置不同的学习率,在finetune中经常用到
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
optimizer =optim.SGD([
                {'params': net.features.parameters()}, # 学习率为1e-5
                {'params': net.classifier.parameters(), 'lr': 1e-2}
            ], lr=1e-5)
optimizer

## 输出结果
SGD (
Parameter Group 0
    dampening: 0
    lr: 1e-05
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.01
    momentum: 0
    nesterov: False
    weight_decay: 0
)
# 只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0], net.classifier[3]])
# 用map获取这两个特定层的参数 ID,以便后续操作中进行区分
special_layers_params = list(map(id, special_layers.parameters()))
# 用filter过滤获取模型中除特殊层外的其他层的参数
base_params = filter(lambda p: id(p) not in special_layers_params,
                     net.parameters())

optimizer = t.optim.SGD([
            {'params': base_params},
            {'params': special_layers.parameters(), 'lr': 0.01}
        ], lr=0.001 )
optimizer

## 输出结果
SGD (
Parameter Group 0
    dampening: 0
    lr: 0.001
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.01
    momentum: 0
    nesterov: False
    weight_decay: 0
)

  对于如何调整学习率,主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。

# 方法1: 调整学习率,新建一个optimizer
old_lr = 0.1
optimizer1 =optim.SGD([
                {'params': net.features.parameters()},
                {'params': net.classifier.parameters(), 'lr': old_lr*0.1}
            ], lr=1e-5)

# 方法2: 调整学习率, 手动decay, 保存动量
for param_group in optimizer.param_groups:
    param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
    
# 假设你想将第二组参数(即 special_layers 的参数)的学习率调整为之前的 0.1 倍
for i, param_group in enumerate(optimizer.param_groups):
    if i == 1:  # 假设我们要调整第二组参数的学习率
        param_group['lr'] *= 0.1  # 将第二组参数的学习率调整为之前的 0.1 倍

1.3 nn.functional

  nn中还有一个很常用的模块:nn.functional,nn中的大多数layer,在functional中都有一个与之相对应的函数。nn.functional中的函数和nn.Module的主要区别在于,用nn.Module实现的layers是一个特殊的类,都是由class layer(nn.Module)定义,会自动提取可学习的参数。而nn.functional中的函数更像是纯函数,由def function(input)定义。下面举例说明functional的使用,并指出二者的不同之处。

input = t.randn(2, 3)
model = nn.Linear(3, 4)
output1 = model(input)
output2 = nn.functional.linear(input, model.weight, model.bias)
output1 == output2
b = nn.functional.relu(input)
# 使用ReLU的另一种调用方式
b2 = nn.ReLU()(input)
b == b2

  当模型有可学习的参数,最好用nn.Module,否则既可以使用nn.functional也可以使用nn.Module,二者在性能上没有太大差异,具体的使用取决于个人的喜好。如激活函数(ReLU、sigmoid、tanh),池化(MaxPool)等层由于没有可学习参数,则可以使用对应的functional函数代替,而对于卷积、全连接等具有可学习参数的网络建议使用nn.Module。

  下面举例说明,如何在模型中搭配使用nn.Module和nn.functional。另外虽然dropout操作也没有可学习操作,但建议还是使用nn.Dropout而不是nn.functional.dropout,因为dropout在训练和测试两个阶段的行为有所差别,使用nn.Module对象能够通过model.eval操作加以区分。

from torch.nn import functional as F
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.pool(F.relu(self.conv1(x)), 2)
        x = F.pool(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

  对于不具备可学习参数的层(激活层、池化层等),将它们用函数代替,这样则可以不用放置在构造函数__init__中。对于有可学习参数的模块,也可以用functional来代替,只不过实现起来较为繁琐,需要手动定义参数parameter。

1.4 初始化策略

  在深度学习中参数的初始化十分重要,PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter时,自定义初始化则尤为重要,因t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。PyTorch中nn.init模块就是专门为初始化而设计,如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。

# 利用nn.init初始化
from torch.nn import init
# 定义一个线性层,输入特征数为 3,输出特征数为 4
linear = nn.Linear(3, 4)
# 设置随机种子,以确保每次运行代码时产生相同的随机数
t.manual_seed(1)
# 等价于 linear.weight.data.normal_(0, std)
# 使用 Xavier 正态分布初始化线性层的权重,根据输入和输出的特征数量自动设置权重的标准差
init.xavier_normal_(linear.weight) #  # 初始化权重

# 直接初始化
import math
t.manual_seed(1)

# xavier初始化的计算公式
std = math.sqrt(2)/math.sqrt(7.)
linear.weight.data.normal_(0,std)
# 遍历模型 net 的所有参数,包括参数名称和参数本身
for name, params in net.named_parameters():
    # 检查参数名称中是否包含 'linear',如果是,则进行线性层的初始化
    if name.find('linear') != -1:
        # init linear
        # params[0] 表示线性层的权重(weight)
        # 可以在这里添加权重的初始化方法,例如 Xavier 或 He 初始化
        # 例如: init.xavier_normal_(params[0]) 
        weight = params[0]  # 获取权重参数
        
        # params[1] 表示线性层的偏置(bias)
        # 可以在这里对偏置进行初始化,例如将偏置设置为零
        # 例如: nn.init.zeros_(params[1])
        bias = params[1]  # 获取偏置参数
        
    # 检查参数名称中是否包含 'conv',如果是,则可以进行卷积层的初始化
    elif name.find('conv') != -1:
        # 在这里添加卷积层的初始化方法
        # 例如: init.kaiming_normal_(params[0])  # 对卷积层的权重进行 Kaiming 初始化
        pass
        
    # 检查参数名称中是否包含 'norm',如果是,则可以进行归一化层的初始化
    elif name.find('norm') != -1:
        # 在这里添加归一化层的初始化方法
        # 例如: params[0].data.fill_(1)  # 设置归一化层的权重为1
        #       params[1].data.fill_(0)  # 设置归一化层的偏置为0
        pass

1.5 nn.Module深入分析

  如果想要更深入地理解nn.Module,究其原理是很有必要的。首先来看看nn.Module基类的构造函数:

def __init__(self):
    self._parameters = OrderedDict()
    self._modules = OrderedDict()
    self._buffers = OrderedDict()
    self._backward_hooks = OrderedDict()
    self._forward_hooks = OrderedDict()
    self.training = True

其中每个属性的解释如下:

  • _parameters:字典,保存用户直接设置的parameter,self.param1 = nn.Parameter(t.randn(3, 3))会被检测到,在字典中加入一个key为'param',value为对应parameter的item。而self.submodule = nn.Linear(3, 4)中的parameter则不会存于此。
  • _modules:子module,通过self.submodel = nn.Linear(3, 4)指定的子module会保存于此。
  • _buffers:缓存。如batchnorm使用momentum机制,每次前向传播需用到上一次前向传播的结果。
  • _backward_hooks_forward_hooks:钩子技术,用来提取中间变量,类似variable的hook。
  • training:BatchNorm与Dropout层在训练阶段和测试阶段中采取的策略不同,通过判断training值来决定前向传播策略。

  上述几个属性中,_parameters_modules_buffers这三个字典中的键值,都可以通过self.key方式获得,效果等价于self._parameters['key'],下面举例说明。

import torch as t
import torch.nn as nn

# 定义自定义神经网络类Net,继承自nn.Module
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 使用nn.Parameter定义一个3x3的参数矩阵param1
        # 这个参数矩阵将在反向传播中进行训练
        # 等价于self.register_parameter('param1', nn.Parameter(t.rand(3, 3)))
        self.param1 = nn.Parameter(t.rand(3, 3))
        
        # 定义一个线性层submodel1,输入维度为3,输出维度为4
        # 该层也包含可训练的权重和偏置
        self.submodel1 = nn.Linear(3, 4) 

    # 定义前向传播过程
    def forward(self, input):
        # 使用矩阵乘法将param1与输入input相乘,生成中间结果x
        # param1为3x3矩阵,因此input的维度需要满足乘法规则
        x = self.param1.mm(input)
        
        # 将中间结果x输入到submodel1线性层中
        # submodel1会对x进行线性变换,输出4维结果
        x = self.submodel1(x)
        
        # 返回最终的前向传播结果
        return x

# 实例化Net模型
net = Net()
net  # 查看模型结构

Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)

   nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为方便用户访问各个子module,如函数children可以查看直接子module,函数module可以查看所有的子module(包括当前module)。与之相对应的还有函数named_childennamed_modules,其能够在返回module列表的同时返回它们的名字。

# t.arange(0, 12):生成一个从 0 到 11(不包括 12)的张量,.view(3, 4):将张量的形状变为 (3, 4),
input = t.arange(0, 12).view(3, 4)
model = nn.Dropout()
# 在训练阶段,会有一半左右的数被随机置为0
model(input)

# 结果如下
tensor([[  0.,   2.,   0.,   0.],
        [  8.,   0.,  12.,  14.],
        [ 16.,   0.,   0.,  22.]])

model.training  = False
# 在测试阶段,dropout什么都不做
model(input)

   对于batchnorm、dropout、instancenorm等在训练和测试阶段行为差距巨大的层,如果在测试时不将其training值设为True,则可能会有很大影响,虽然可通过直接设置training属性,来将子module设为train和eval模式,但这种方式较为繁琐,更为推荐的做法是调用model.train()函数,它会将当前module及其子module中的所有training属性都设为True,相应的,model.eval()函数会把training属性都设为False。

  register_forward_hookregister_backward_hook,可在module前向传播或反向传播时注册钩子,每次前向传播执行结束后会执行钩子函数(hook)。前向传播的钩子函数具有如下形式:hook(module, input, output) -> None,而反向传播则具有如下形式:hook(module, grad_input, grad_output) -> Tensor or None。需要注意钩子函数使用后应及时删除,以避免每次都运行钩子增加运行负载。 

model = VGG()
# 创建一个空的 Tensor,用于存储钩子函数捕获的输出数据
features = t.Tensor()
def hook(module, input, output):
    '''把这层的输出拷贝到features中'''
    features.copy_(output.data)
    
# 将钩子函数 `hook` 注册到 `model.layer8`(VGG 网络的第 8 层),每当该层执行前向传播时,钩子函数 `hook` 会自动执行。
handle = model.layer8.register_forward_hook(hook)
_ = model(input)
# 用完hook后删除
handle.remove()

  在 PyTorch 的 nn.Module 中,__getattr____setattr__ 经常用于自定义属性访问和赋值的行为。getattr(obj, 'attr1')等价于obj.attrsetattr(obj, 'name', value)等价于obj.name=value,总结一下就是:

  • result = obj.name会调用buildin函数getattr(obj, 'name'),如果该属性找不到,会调用obj.__getattr__('name')
  • obj.name = value会调用buildin函数setattr(obj, 'name', value),如果obj对象实现了__setattr__方法,setattr会直接调用obj.__setattr__('name', value')

  nn.Module实现了自定义的__setattr__函数,当执行module.name=value时,会在__setattr__中判断value是否为Parameternn.Module对象,如果是则将这些对象加到_parameters_modules两个字典中,而如果是其它类型的对象,如Variablelistdict等,则调用默认的操作,将这个值保存在__dict__中。

  在PyTorch中保存模型十分简单,所有的Module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()函数将状态加载进来。优化器(optimizer)也有类似的机制,不过一般并不需要保存优化器的运行状态。

# 保存模型
t.save(net.state_dict(), 'net.pth')

# 加载已保存的模型
net2 = Net()
net2.load_state_dict(t.load('net.pth'))

  将Module放在GPU上运行也十分简单,只需两步:

  • model = model.cuda():将模型的所有参数转存到GPU
  • input.cuda():将输入数据也放置到GPU上

  至于如何在多个GPU上并行计算,PyTorch也提供了两个函数,可实现简单高效的并行GPU计算

  • nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)
  • class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

  可见二者的参数十分相似,通过device_ids参数可以指定在哪些GPU上进行优化,output_device指定输出到哪个GPU上。唯一的不同就在于前者直接利用多GPU并行计算得出结果,而后者则返回一个新的module,能够自动在多GPU上进行并行加速。

# method 1
new_net = nn.DataParallel(net, device_ids=[0, 1])
output = new_net(input)

# method 2
output = nn.parallel.data_parallel(new_net, input, device_ids=[0, 1])

1.6 nn和autograd的关系

  nn.Module利用的也是autograd技术,其主要工作是实现前向传播。在forward函数中,nn.Module对输入的tensor进行的各种操作,本质上都是用到了autograd技术。这里需要对比autograd.Function和nn.Module之间的区别:

  • autograd.Function利用了Tensor对autograd技术的扩展,为autograd实现了新的运算op,不仅要实现前向传播还要手动实现反向传播
  • nn.Module利用了autograd技术,对nn的功能进行扩展,实现了深度学习中更多的层。只需实现前向传播功能,autograd即会自动实现反向传播
  • nn.functional是一些autograd操作的集合,是经过封装的函数

  作为两大类扩充PyTorch接口的方法,我们在实际使用中应该如何选择呢?如果某一个操作,在autograd中尚未支持,那么只能实现Function接口对应的前向传播和反向传播。如果某些时候利用autograd接口比较复杂,则可以利用Function将多个操作聚合,实现优化,正如第三章所实现的Sigmoid一样,比直接利用autograd低级别的操作要快。而如果只是想在深度学习中增加某一层,使用nn.Module进行封装则更为简单高效。

1.7 搭建ResNet

  深度残差网络(ResNet)在深度学习的发展中起到了很重要的作用,这一结构解决了训练极深网络时的梯度消失问题。

  首先来看看ResNet的网络结构,这里选取的是ResNet的一个变种:ResNet34。ResNet的网络结构如下图所示,可见除了最开始的卷积池化和最后的池化全连接之外,网络中有很多结构相似的单元,这些重复单元的共同点就是有个跨层直连的shortcut。

  ResNet中将一个跨层直连的单元称为Residual block(残差块),其结构如下图所示,左边部分是普通的卷积网络结构,右边是直连,但如果输入和输出的通道数不一致,或其步长不为1,那么就需要有一个专门的单元将二者转成一致,使其可以相加。

  另外我们可以发现Residual block的大小也是有规律的,在最开始的pool之后有连续的几个一模一样的Residual block单元,这些单元的通道数一样,在这里我们将这几个拥有多个Residual block单元的结构称之为layer,注意和之前讲的layer区分开来,这里的layer是几个层的集合。

  考虑到Residual block和layer出现了多次,我们可以把它们实现为一个子Module或函数。这里我们将Residual block实现为一个子moduke,而将layer实现为一个函数。下面是实现代码,规律总结如下:

  • 对于模型中的重复部分,实现为子module或用函数生成相应的modulemake_layer
  • nn.Module和nn.Functional结合使用
  • 尽量使用nn.Seqential
from torch import nn
import torch as t
from torch.nn import functional as F

class ResidualBlock(nn.Module):
    """
    实现子模块:残差块(Residual Block)
    每个残差块包含两个卷积层和一个可选的捷径连接(shortcut)。
    """

    def __init__(self, in_channels, out_channels, stride=1, shortcut=None):
        # 调用父类构造函数,初始化基础属性
        super(ResidualBlock, self).__init__()
        
        # 定义残差块的左分支(即普通卷积分支)
        # 左分支的第一个卷积层:卷积核大小为3x3,步幅由参数`stride`指定,`padding`=1使输出大小不变,禁用偏置(bias)
        self.left = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_channels),       # BN层标准化输出
            nn.ReLU(inplace=True),              # 使用ReLU激活函数进行非线性变换
            nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=0, bias=False),  # 第二层卷积,步幅固定为1,输出维度保持不变
            nn.BatchNorm2d(out_channels)        # BN层标准化输出
        )
        
        # 右分支:捷径连接,用于在输入维度和输出维度不一致时调整维度(即残差映射)
        # 如果维度匹配,不需要捷径连接(shortcut=None),否则提供一个卷积层调整维度
        self.right = shortcut

    def forward(self, x):
        # 计算左分支的输出
        out = self.left(x)
        
        # 计算右分支的输出:如果没有捷径连接,则使用原始输入 x,否则通过捷径连接映射得到 residual
        residual = x if self.right is None else self.right(x)
        
        # 左分支和右分支输出相加(残差连接)
        out += residual
        
        # 将结果通过 ReLU 激活函数返回
        return F.relu(out)


class ResNet(nn.Module):
    """
    实现主模块:ResNet34
    ResNet34 包含多个 layer,每个 layer 又包含多个 residual block。
    """

    def __init__(self, num_classes=1000):
        # 调用父类构造函数,初始化基础属性
        super(ResNet, self).__init__()
        
        # 前几层图像转换层(图像预处理)
        # 输入 3 通道(RGB图像),输出 64 通道,卷积核大小为 7x7,步幅为 2,`padding` 为 3 使得输出大小与输入相近
        self.pre = nn.Sequential(
            nn.Conv2d(3, 64, 7, 2, 3, bias=False),
            nn.BatchNorm2d(64),         # BN层标准化输出
            nn.ReLU(inplace=True),      # ReLU 激活
            nn.MaxPool2d(3, 2, 1)       # 3x3 最大池化层,步幅为 2,`padding` 为 1 使输出大小减半
        )

        # 构建 ResNet34 的 4 个 layer,每个 layer 包含不同数量的 Residual Block
        # 第一个 layer 包含 3 个残差块,输入通道为 64,输出通道为 64,步幅为 1(大小不变)
        self.layer1 = self._make_layer(64, 64, 3)
        
        # 第二个 layer 包含 4 个残差块,输入通道为 64,输出通道为 128,步幅为 2(大小减半)
        self.layer2 = self._make_layer(64, 128, 4, stride=2)
        
        # 第三个 layer 包含 6 个残差块,输入通道为 128,输出通道为 256,步幅为 2(大小减半)
        self.layer3 = self._make_layer(128, 256, 6, stride=2)
        
        # 第四个 layer 包含 3 个残差块,输入通道为 256,输出通道为 512,步幅为 2(大小减半)
        self.layer4 = self._make_layer(256, 512, 3, stride=2)

        # 分类用的全连接层,将最后输出映射到类别数量(num_classes)
        self.fc = nn.Linear(512, num_classes)

    def _make_layer(self, inchannel, outchannel, block_num, stride=1):
        """
        构建 layer,包含多个 residual block。
        :param inchannel: 输入通道数
        :param outchannel: 输出通道数
        :param block_num: 残差块数量
        :param stride: 步幅
        """
        
        # 捷径连接,用于调整第一个残差块的输入尺寸和输出尺寸
        # 使用 1x1 卷积,将输入通道数从 inchannel 转换为 outchannel,步幅为 stride
        shortcut = nn.Sequential(
            nn.Conv2d(inchannel, outchannel, 1, stride, bias=False),
            nn.BatchNorm2d(outchannel)
        )

        layers = []
        # 构建第一个 Residual Block,使用捷径连接
        layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut))

        # 构建后续的 Residual Block,不需要捷径连接,输入和输出通道一致
        for i in range(1, block_num):
            layers.append(ResidualBlock(outchannel, outchannel))
        
        # 将所有 Residual Block 封装成 nn.Sequential 以方便调用
        return nn.Sequential(*layers)

    def forward(self, x):
        # 前几层的图像转换操作
        x = self.pre(x)

        # 依次通过 4 个 layer,每个 layer 包含多个残差块
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        # 通过全局平均池化,将输出大小缩小到 1x1(相当于对每个通道的空间位置求平均)
        x = F.avg_pool2d(x, 7)
        
        # 将池化后的张量展平成一维向量,准备输入全连接层
        x = x.view(x.size(0), -1)
        
        # 通过全连接层,将特征映射为类别数量的输出
        return self.fc(x)

# 实例化 ResNet 模型
model = ResNet()

# 生成一个随机输入张量(batch size=1,3 通道,224x224),模拟输入图片
input = t.randn(1, 3, 224, 224)

# 通过模型前向传播获得输出
o = model(input)

 

标签:__,nn,self,Module,神经网络,module,input,工具箱
From: https://www.cnblogs.com/pgl6/p/18493640

相关文章