构建神经网络
模型创建
- 模型创建步骤:
- 通过示例LeNet学习模型创建的步骤:
LeNet网络结构图:
所有的模型、网络层都继承nn.module类。
它属于神经网络模块 torch.nn
nn.module有8个属性(有序字典)
self._parameters = OrderedDict()
self._buffers = OrderedDict()
self._modules = OrderedDict()
self._backward_hooks = OrderedDict()
self._forward_hooks = OrderedDict()
self._forward_pre_hooks = OrderedDict()
self._state_dict_hooks = OrderedDict()
self._load_state_dict_pre_hooks = OrderedDict()
parameters:存储管理nn.Parameter类
modules:存储管理nn.Module类
buffers:存储管理缓冲属性 ,如BN层中的running_mean
***_hooks:存储管理钩子函数
LeNet网络模型如下:
class LeNet(nn.Module):
def __init__(self, classes):
super(LeNet, self).__init__() # 8个有序字典的初始化
# 建立子模块
self.conv1 = nn.Conv2d(3, 6, 5) # 会记录在modules中
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, classes)
def forward(self, x):
out = F.relu(self.conv1(x))
out = F.max_pool2d(out, 2)
out = F.relu(self.conv2(out))
out = F.max_pool2d(out, 2)
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = F.relu(self.fc2(out))
out = self.fc3(out)
return out
def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.xavier_normal_(m.weight.data)
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, 0, 0.1)
m.bias.data.zero_()
- 构建子模块是在__init__实现,拼接子模块在前向传播forward()实现
构建模型时,在__init__函数中构建子模块,运行完跳出LeNet类,模型初始化完成,得到LeNet的实例。
调用LeNet实例(LeNet继承于module,molule中有__call__函数),进而调用LeNet中的self.forward(),实现前向传播,得到输入(分类的概率向量)。 - LeNet网络模块的构建过程
LeNet继承于nn.module,在LeNet的__init__中调用父类的__init__函数,nn.module的__init__函数里面先执行_construct()函数,再将training这个flag设置为true,标识训练状态。在_construct()函数中为8个属性(有序字典,比如_modules,_parameters)初始化。 - 构建子网络,并将它们存储到_modules中管理
执行完父类的__init__后,构建子模块,子模块用到nn.Conv2d,它继承于_ConvNd,_ConvNd继承于Module,所以conv2d还是一个module,它的创建也是初始化8个属性,其中的_modules是空的,因为它是一个子层,它没有子模块了;其中的_parameters属性中存有weight和bias,weight属于Parameter类(继承于张量),所以weight是一个特殊的张量。创建完的一个nn.Conv2d,记录在LeNet的_modules属性中。 - nn.Module的属性如何构建?
构建完一个子模块后,也就是实例化了一个子网络后,在赋值的时候,Module会拦截所有的类属性赋值操作,即将赋值的时候,会跳转到Module中的__setattr__函数中,这个函数首先会拦截所有类属性的赋值,然后会对value进行一个类型的判断,是Parameter还是Module等,比如判断了它是个Module,就会把这个子网络存到modules[name]类属性中,name是’conv2’,value是conv2这个{Conv2d}模块,此时,LeNet的_modules中就会存有conv2这个子module了,接下来一步步每进行一次类属性的赋值,都会判断类型,再存储到LeNet相应的_modules属性或者_parameters属性中。 - 总结:
一个module可以包含多个子module
一个module相当于一次运算,必须实现forward()函数
每个module都有8个有序字典管理它的属性
module会拦截所有类属性的赋值
模型容器
nn.Sequential:顺序性,各网络层之间严格按顺序执行,常用于block构建
nn.ModuleList:迭代性,常用于大量重复网构建,通过for循环实现重复构建
nn.ModuleDict:索引性,常用于可选择的网络层
nn.Sequential
nn.Sequential是继承module的,是nn.module的容器,用于按顺序包装一组网络层
顺序性:各网络层之间严格按照顺序构建
自带forward():自带的forward里,通过for循环依次执行前向传播运算
卷积层用于自动学习特征,全连接层用于输出分类结果,于是将模型以全连接层为界限,分成特征提取模块和分类模块
class LeNet2(nn.Module):
def __init__(self, classes):
super(LeNet2, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
self.classifier = nn.Sequential(
nn.Linear(16*5*5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, classes)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size()[0], -1)
x = self.classifier(x)
return x
以上,容器中的网络层是用序号来索引的,如果想通过名称索引网络层,则使用有序字典:
nn.Sequential(OrderedDict({
'conv1': nn.Conv2d(3, 6, 5),
'relu1': nn.ReLU(),
'mp1': nn.MaxPool2d(2, 2),
'conv2': nn.Conv2d(6, 16, 5),
'relu2': nn.ReLU(),
'mp2': nn.MaxPool2d(2, 2),
}))
nn.ModuleList
nn.ModuleList是nn.module的容器,用于包装一组网络层,以迭代方式调用网络层
主要方法:
append():在ModuleList后面添加网络层
extend():拼接两个ModuleList
insert():指定在ModuleList中位置插入网络层
class ModuleList(nn.Module):
def __init__(self):
super(ModuleList, self).__init__()
self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(20)])
def forward(self, x):
for i, linear in enumerate(self.linears):
x = linear(x)
return x
nn.ModuleDict
nn.ModuleDict是nn.module的容器,用于包装一组网络,以索引方式调用网络层
主要方法:
clear():清空ModuleDict
items():返回可迭代的键值对(key-value pairs)
keys():返回字典的键(key)
values():返回字典的值(value)
pop():返回一堆键值,并从字典中删除
class ModuleDict(nn.Module):
def __init__(self):
super(ModuleDict, self).__init__()
self.choices = nn.ModuleDict({
'conv': nn.Conv2d(10, 10, 3),
'pool': nn.MaxPool2d(3)
})
self.activations = nn.ModuleDict({
'relu': nn.ReLU(),
'prelu': nn.PReLU()
})
def forward(self, x, choice, act):
x = self.choices[choice](x)
x = self.activations[act](x)
return x
AlexNet
AlexNet特点如下:
- 采用ReLU:替换饱和激活函数,减轻梯度消失
- 采用LRN(Local Response Normalization):对数据归一化,减轻梯度消失
- Dropout:提高全连接层的鲁棒性,增加网络的泛化能力
- Data Augmentation:TenCrop,色彩修改
class AlexNet(nn.Module):
def __init__(self, num_classes: int = 1000, dropout: float = 0.5) -> None:
super().__init__()
_log_api_usage_once(self)
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(p=dropout),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=dropout),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
网络层
1d/2d/3d 卷积
卷积运算:卷积核在输入信号(图像)上滑动,相应位置上进行乘加运算。
卷积核:又称为滤波器,过滤器,可认为是某种模式,某种特征。
卷积过程类似于用一个模板去图像上寻找与它相似的区域,与卷积核模式越相似,激活值越高,从而实现特征提取。
卷积核学习到的是边缘,条纹,色彩这一些细节模式。具体是哪种特征、哪种模式,是由模型学习而来的。
卷积维度:一般情况下,卷积核在几个维度上滑动,就是几维卷积(一个卷积核在一个信号上是几维卷积)。
二维卷积参数weight是一个4维张量(输出通道数,输入通道数,卷积核尺寸
x
∗
x
x*x
x∗x),如
1
∗
3
∗
3
∗
3
1*3*3*3
1∗3∗3∗3,3个卷积核执行3个通道上的卷积运算得到3个输出值,把这3个输出值相加再加上偏置bias得到输出的特征图上的一个像素值,所以三维的卷积核执行的是二维卷积。
nn.Conv2d
nn.Conv2d(in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros')
功能:对多个二维信号进行二维卷积
主要参数:
in_channels:输入通道数
out_channels:输出通道数,等价于卷积核个数
kernel_size:卷积核尺寸
stride:步长
padding:填充个数,用来保持输入和输出图像尺寸匹配的。
dilation:空洞卷积大小
groups:分组卷积设置
bias:偏置
尺寸计算:
简化版:输出尺寸
o
u
t
s
i
z
e
=
i
n
s
i
z
e
−
k
e
r
n
e
l
s
i
z
e
s
t
r
i
d
e
+
1
out_{size} = \frac{in_{size} - kernel_{size}}{stride} + 1
outsize=strideinsize−kernelsize+1
完整版:输出尺寸
o
u
t
s
i
z
e
=
i
n
s
i
z
e
+
2
∗
p
a
d
d
i
n
g
−
d
i
l
a
t
i
o
n
∗
(
k
e
r
n
e
l
s
i
z
e
−
1
)
−
1
s
t
r
i
d
e
+
1
out_{size} = \frac{in_{size} + 2*padding - dilation*(kernel_{size} - 1) - 1}{stride} + 1
outsize=strideinsize+2∗padding−dilation∗(kernelsize−1)−1+1
转置卷积
也叫反卷积(Deconvolution)和部分跨越卷积(Fractionally-strided Convolution),用于对图像进行上采样(UpSample),在图像分割任务中经常使用。
- 为什么称为转置卷积?
正常卷积:
假设图像尺寸为4×4,卷积核为3×3,padding = 0,stride = 1
用矩阵乘法来实现此运算,做如下转换
图像:I16×1 ,卷积核:K4×16 ,输出:O4×1 = K4×16 × I16×1
卷积核尺寸由来:卷积核在输入图像上共滑动4次,16是由3×3的9个数字补零得到的,输入图像上不参与运算的相应位置补零。
转化为矩阵后:
转置卷积:
假设图像尺寸为2×2,卷积核为3×3,padding = 0,stride = 1
转化为矩阵
图像:I4×1 ,卷积核:K16×4 ,输出:O16×1 = K16×4 × I4×1
卷积核的尺寸由来:卷积核在输入图像上共滑动16次,4是由3×3的9个数字剔除后得到的,将卷积核中不参与运算的位置剔除。
由此可见,正常卷积和转置卷积的卷积核形状是转置的关系,但仅仅是形状上转置,权值是完全不相同的,所以正常卷积和转置卷积是不可逆的。
nn.ConvTranspose2d
nn.ConTranspose2d(in
nn.ConTranspose2d(in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode='zeros')
功能:转置卷积实现上采样
尺寸计算:
简化版:
o
u
t
s
i
z
e
=
(
i
n
s
i
z
e
−
1
)
∗
s
t
r
i
d
e
+
k
e
r
n
e
l
s
i
z
e
out_{size} = (in_{size} - 1) * stride + kernel_{size}
outsize=(insize−1)∗stride+kernelsize
完整版:
H
o
u
t
=
(
H
i
n
−
1
)
∗
s
t
r
i
d
e
−
2
∗
p
a
d
d
i
n
g
+
d
i
l
a
t
i
o
n
∗
(
k
e
r
n
e
l
s
i
z
e
−
1
)
+
o
u
t
p
u
t
p
a
d
d
i
n
g
+
1
H_{out} = (H_{in} - 1) * stride - 2 * padding + dilation * (kernel_{size} - 1) + output_{padding} + 1
Hout=(Hin−1)∗stride−2∗padding+dilation∗(kernelsize−1)+outputpadding+1
转置卷积可能会导致棋盘效应,是由不均匀重叠导致。
池化层
池化运算:对信号进行“收集”并“总结”,类似水池收集水资源,因而得名池化层
“收集”:多变少
“总结”:最大值/平均值
冗余信息剔除,减小计算量
nn.MaxPool2d
nn.MaxPool2d(kernel_size,
stride=None,
padding=0,
dilation=1,
return_indices=False,
ceil_mode=False)
功能:对二维信号(图像)进行最大值池化
主要参数:
kernel_size:池化核尺寸,比如二维池化(2, 2)
stride:步长
padding:填充个数
dilation:池化核间隔大小
ceil_mode:尺寸向上取整(输出特征图尺寸)True是向上取整
return_indices:记录池化像素索引(最大值反池化上采样)
如果设置了为True,则会返回此索引:
maxpool_layer = nn.MaxPool2d((2, 2), stride=(2, 2), return_indices=True)
img_pool, indices = maxpool_layer(img_tensor)
nn.AvgPool2d
nn.AvgPool2d(kernel_size,
stride=None,
padding=0,
ceil_mode=False,
count_include_pad=True,
divisor_override=None)
功能:对二维信号(图像)进行平均池化
主要参数:
ceil_mode:尺寸向上取整
count_include_pad:填充值用于计算,True的话会将填充值也加入计算
divisor_override:除法因子,分母正常是像素的个数,可以让设置的除法因子作分母
nn.MaxUnpool2d
nn.MaxUnpool2d(kernel_size,
stride=None,
padding=0)
功能:对二维信号(图像)进行最大值池化上采样
forward(self, input, indices, output_size=None)
将像素值放到最大值池化时记录的最大的像素值所在的位置上,将最大的像素值所在索引传入到反池化层中
线性层
线性层又称全连接层,其每个神经元与上一层所有神经元相连实现对前一层的线性组合,线性变换,可采用矩阵乘法实现
nn.Linear
nn.Linear(in_features, out_features, bias=True)
功能:对一维信号(向量)进行线性组合
主要参数:
in_featrues:输入结点数
out_features:输出结点数
bias:是否需要偏置
输入输出结点数用来确定权值矩阵的shape,转置后做乘法
计算公式:
y
=
x
W
T
+
b
i
a
s
y = xW^T + bias
y=xWT+bias
激活函数层
激活函数对特征进行非线性变换,赋予多层神经网络具有深度的意义
nn.Sigmoid
计算公式:
y
=
1
1
+
e
−
x
y = \frac1{1 + e^{-x}}
y=1+e−x1
梯度公式:
y
′
=
y
∗
(
1
−
y
)
y^{'} = y * (1 - y)
y′=y∗(1−y)
特性:
- 输出值在(0, 1),符合概率
- 导数范围是[0, 0.25],易导致梯度消失(导数叠加相乘,越乘越小)
- 输出为非0均值,破坏数据分布
nn.tanh
计算公式:
y
=
sin
x
cos
x
=
e
x
−
e
−
x
e
x
+
e
−
x
=
2
1
+
e
−
2
x
+
1
y = \frac {\sin{x}}{\cos{x}} = \frac{e^x - e^-x}{e^x + e^-x} = \frac 2{1 + e^-2x} +1
y=cosxsinx=ex+e−xex−e−x=1+e−2x2+1
梯度公式:
y
′
=
1
−
y
2
y^{'} = 1 - y^2
y′=1−y2
特性:
- 输出值在(-1, 1),数据符合0均值
- 导数范围是(0, 1),易导致梯度消失
nn.ReLU
计算公式:
y
=
m
a
x
(
0
,
x
)
y=max(0,x)
y=max(0,x)
梯度公式:
y
=
{
1
,
x
>
0
u
n
d
e
f
i
n
e
d
,
x
=
0
0
,
x
<
0
y=\left\{ \begin{aligned} 1 & , & x > 0 \\ undefined & , & x = 0 \\ 0 & , & x < 0 \end{aligned} \right.
y=⎩
⎨
⎧1undefined0,,,x>0x=0x<0
特性:
- 输出值均为正数,负半轴导致死神经元
- 导数是1,缓解梯度消失,但易引发梯度爆炸
改进的ReLU
改进ReLU负半轴输出全为0而导致死神经元的弊端
nn.LeakyReLU
在负半轴增加了一个很小很小的斜率,使负半轴输出不再是0。
negative_slope:负半轴斜率
nn.PReLU
P是parameter,也就是说负半轴的斜率是可学习的参数。
init:可学习斜率的初始化,随着网络更新,这个参数会变
nn.RReLU
R是random,也就是说负半轴的斜率每次都是随机的从均匀分布当中采样,可设置均匀分布的上下限。
lower:均匀分布下限
upper:均匀分布上限
举个例子
构建一个神经网络来对 FashionMNIST 数据集中的图像进行分类。
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
获取用于训练的设备
device = (
"cuda"
if torch.cuda.is_available()
else "mps"
if torch.backends.mps.is_available()
else "cpu"
)
print(f"Using {device} device")
类的定义
我们通过定义nn.Module
的子类来定义我们的神经网络,并且在__init__
中初始化神经网络层。每个nn.Module
子类实现往forward
方法中输入数据的操作。
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
创建的一个NeuralNetwork
实例,把它放到device
上,并打印它的结构。
model = NeuralNetwork().to(device)
print(model)
输出的结果:
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
)
)
使用这个model时,将输入数据传递给它,这将执行模型的forward
,以及一些后台操作。不要直接调用model.forward()
。
在输入的数据上调用model将返回一个二维张量,其中 dim=0 对应于每个类的 10 个原始预测值的每个输出,dim=1 对应于每个输出的各个值。 我们通过把输出传递给nn.softmax
模块的一个实例来获取预测概率。
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
接下来分解一下FashionMNIST模型中的各个层。为了说明这一点,我们将取一个包含 3 张大小为 28x28 的图像的样本小批量,看看当我们通过网络传递它时,会发生什么 。
input_image = torch.rand(3,28,28)
- nn.Flatten
初始化nn.Flatten
层,将每个28x28 的2D图像转换为 784 像素的连续数组(保持minibatch在dim=0上的维度)。
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size()) # torch.Size([3, 784])
- nn.Linear
线性层是一个使用其存储的权值和偏置对输入数据应用线性变换的模块。
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size()) # torch.Size([3, 20])
- nn.ReLU
非线性激活是在模型的输入和输出之间创建复杂映射的东西。 它们被应用在线性变换之后引入非线性,帮助神经网络学习各种各样的现象。
在这个模型中,我们把nn.ReLU
用在线性层之间,其实还有其他非线性激活可以在模型中引入。
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")
- nn.Sequential
nn.Sequential是一个有序的模块容器。数据按照定义的相同顺序传递到所有模块。可以使用顺序容器用于组合一个快速网络,比如seq_modules
。
seq_modules = nn.Sequential(
flatten,
layer1,
nn.ReLU(),
nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image)
- nn.Softmax
神经网络的最后一个线性层返回的原始值的区间是 [-infty, infty],这些值被传递给 nn.Softmax 模块。将返回的原始值按比例缩放到区间 [0, 1] 上,表示模型对每个类的预测概率。参数 dim 表示沿这个维度的值总和必须为1。
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
模型的参数
神经网络中的许多层都是参数化的,即具有相关的权值和偏置,它们在训练期间不断被优化。自动追踪模型对象内定义的所有字段创建nn.Module的子类,并使用模型的parameters()
或named_parameters()
方法创建所有参数。
在此示例中,我们迭代每个参数,并打印其尺寸及其值的预览。
print(f"Model structure: {model}\n\n")
for name, param in model.named_parameters():
print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")
更详细的内容参见:torch.nn API