1. pytorch基础配置
# 首先导入必须的包
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer
# 如下几个超参数可以统一设置,方便后续调试时修改
batch_size = 16
# 批次的大小
lr = 1e-4
# 优化器的学习率
max_epochs = 100
2. 数据读入
可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承PyTorch自身的Dataset类,主要包含三个函数:
- init: 用于向类中传入外部参数,同时定义样本集
- getitem: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据
- len: 用于返回数据集的样本数
构建dataset类的定义
# 以cifar10数据集为例给出构建Dataset类的方式
import torch
from torchvision import datasets
# path为存图片的地址,其中“data_transform”可以对图像进行一定的变换,如翻转、裁剪等操作,可自己定义
train_data = datasets.ImageFolder(train_path, transform=data_transform)
val_data = datasets.ImageFolder(val_path, transform=data_transform)
dataset类的定义
# dataset类的定义
class MyDataset(Dataset):
def __init__(self, data_dir, info_csv, image_list, transform=None):
# 向类中传入外部参数,同时定义样本集
"""
Args:
data_dir: path to image directory.
info_csv: path to the csv file containing image indexes
with corresponding labels.
image_list: path to the txt file contains image names to training/validation set
transform: optional transform to be applied on a sample.
"""
label_info = pd.read_csv(info_csv)
image_file = open(image_list).readlines()
self.data_dir = data_dir
self.image_file = image_file
self.label_info = label_info
self.transform = transform
def __getitem__(self, index):
# 逐个读取样本集合中的元素,可以进行一定变换,并返回所需要的数据
"""
Args:
index: the index of item
Returns:
image and its labels
"""
image_name = self.image_file[index].strip('\n')
raw_label = self.label_info.loc[self.label_info['Image_index'] == image_name]
label = raw_label.iloc[:,0]
image_name = os.path.join(self.data_dir, image_name)
image = Image.open(image_name).convert('RGB')
if self.transform is not None:
image = self.transform(image)
return image, label
def __len__(self):
# 返回数据集的样本数
return len(self.image_file)
使用DataLoader按批次读取数据
# 使用DataLoader按批次读取数据
from torch.utils.data import DataLoader
# batch_size 是指按批读入数据,batch_size指读入样本数的大小
# num_workers 有多少个进程用于读取数据,cpu的话就是0
# shuffle 是否将读入的数据打乱
# drop_last 对于样本最后一部分没有达到批次的样本,使其不再参与悬链
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=4, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size, num_workers=4, shuffle=False)
DataLoader的读取可以使用next和iter来完成
# PyTorch中的DataLoader的读取可以使用next和iter来完成
import matplotlib.pyplot as plt
images, labels = next(iter(val_loader))
print(images.shape)
plt.imshow(images[0].transpose(1,2,0))
plt.show()
3. 模型构建
Module 类是 nn 模块里提供的一个模型构造类,是所有神经⽹网络模块的基类,我们可以继承它来定义我们想要的模型。下面继承 Module 类构造多层感知机。这里定义的 MLP 类重载了 Module 类的 init 函数和 forward 函数。它们分别用于创建模型参数和定义前向计算。前向计算也即正向传播。类中⽆须定义反向传播函数。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。
简单的构建模型和使用模型
from turtle import forward
import torch
from torch import nn
class MLP(nn.Module):
def __init__(self):
# 声明带有模型参数的层,这里声明了两个全连接层
# 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
super(MLP, self).__init__()
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256, 10)
def forward(self, x):
'''定义模型的前向计算,即如何根据输入的X计算返回需要的模型输出'''
o = self.act(self.hidden(x))
return self.output(o)
# MLP 类中⽆须定义反向传播函数。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。
# 我们可以实例化 MLP 类得到模型变量 net 。
# 下⾯的代码初始化 net 并传入输⼊数据 X 做一次前向计算。其中, net(X) 会调用 MLP 继承⾃自 Module 类的 call 函数,这个函数将调⽤用 MLP 类定义的forward 函数来完成前向计算。
X = torch.rand(2,784)
net = MLP()
print(net)
net(X)
# 输出
MLP(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(act): ReLU()
(output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[-1.1488e-01, 4.5542e-02, 1.9966e-02, 1.2349e-01, 6.1301e-03,
1.5450e-01, -1.8760e-02, -6.6931e-02, 2.1294e-01, -4.4249e-01],
[-6.8225e-02, -3.5446e-02, 2.7967e-02, 1.0570e-01, 1.4333e-04,
1.4723e-01, 1.4794e-02, -1.2326e-01, 2.8167e-01, -3.2002e-01]],
grad_fn=<AddmmBackward>)
- 含模型参数的层
- Parameter 类其实是 Tensor 的子类,如果一 个 Tensor 是 Parameter ,那么它会⾃动被添加到模型的参数列表里。所以在⾃定义含模型参数的层时,我们应该将参数定义成 Parameter ,除了直接定义成 Parameter 类外,还可以使⽤ ParameterList 和 ParameterDict 分别定义参数的列表和字典。
模型参数层定义
# 我们还可以自定义含模型参数的自定义层。其中的模型参数可以通过训练学出。
# Parameter 类其实是 Tensor 的子类,如果一 个 Tensor 是 Parameter ,那么它会⾃动被添加到模型的参数列表里。所以在⾃定义含模型参数的层时,我们应该将参数定义成 Parameter ,除了直接定义成 Parameter 类外,还可以使⽤ ParameterList 和 ParameterDict 分别定义参数的列表和字典。
class MyListDense(nn.Module):
def __init__(self):
super(MyListDense, self).__init__()
self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
self.params.append(nn.Parameter(torch.randn(4, 1)))
def forward(self, x):
for i in range(len(self.params)):
x = torch.mm(x, self.params[i])
return x
net = MyListDense()
print(net)
# 输出
MyListDense(
(params): ParameterList(
(0): Parameter containing: [torch.FloatTensor of size 4x4]
(1): Parameter containing: [torch.FloatTensor of size 4x4]
(2): Parameter containing: [torch.FloatTensor of size 4x4]
(3): Parameter containing: [torch.FloatTensor of size 4x1]
)
)
定义参数的字典
# param dict定义参数的字典
class MyDictDense(nn.Module):
def __init__(self):
super(MyDictDense, self).__init__()
self.params = nn.ParameterDict({
'linear1': nn.Parameter(torch.randn(4, 4)),
'linear2': nn.Parameter(torch.randn(4, 1))
})
self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增
def forward(self, x, choice='linear1'):
return torch.mm(x, self.params[choice])
net = MyDictDense()
print(net)
# 输出
MyDictDense(
(params): ParameterDict(
(linear1): Parameter containing: [torch.FloatTensor of size 4x4]
(linear2): Parameter containing: [torch.FloatTensor of size 4x1]
(linear3): Parameter containing: [torch.FloatTensor of size 4x2]
)
)
3.1 二维卷积层的创建
- 卷积层的手动创建
import torch
from torch import nn
# 卷积运算(二维互相关)
def corr2d(X, K):
h, w = K.shape
X, K = X.float(), K.float()
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
# 卷积运算,k是卷积核,对X进行卷积
Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
return Y
# 二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
- 调用卷积层创建方法
nn.Conv2d() 卷积运算函数,可以指定卷积核,根据输入和相关参数,自动计算卷积结果
import torch
from torch import nn
#设置在文件最开始部分
import os
# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
import torch
from torch import nn
# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
# (1, 1)代表批量大小和通道数
X = X.view((1, 1) + X.shape)
Y = conv2d(X)
return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道
# 创建二维卷积,注意这里是两侧分别填充1⾏或列,所以在两侧一共填充2⾏或列,这个函数可以一键创建卷积核
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3,padding=1)
X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape
# 输出
torch.Size([8, 8])
# 当卷积核的高和宽不同时,我们也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。
# 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
# 输出
torch.Size([8, 8])
- nn.Conv2d里的stride指移动步长,当移动步长大于1时,相对于对输入进行降维
- 填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。
- 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 ( 为大于1的整数)。
- 当步长为1时,卷积后的维度等于 原维度 - 卷积维度 + 1
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
# 这里的结果为什么输出是size([2,2]),没看懂?
comp_conv2d(conv2d, X).shape
# 输出
torch.Size([2, 2])
3.2 池化层
- 池化常用为最大池化层,也有平均池化;
- 池化层直接计算池化窗口内元素的最大值或者平均值,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当池化窗口滑动到某⼀位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。
import torch
from torch import nn
# 池化层的前向计算
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
# 池化窗口取最大值
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
# 池化窗口取平均值
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]], dtype=torch.float)
print(X)
print(pool2d(X, (2, 2)))
print(pool2d(X, (2, 2), 'avg'))
# 输出
tensor([[0., 1., 2.],
[3., 4., 5.],
[6., 7., 8.]])
tensor([[4., 5.],
[7., 8.]])
tensor([[2., 3.],
[5., 6.]])
3.3 LeNet模型示例
神经网络的典型训练过程如下:
定义包含一些可学习参数(或者叫权重)的神经网络
在输入数据集上迭代
通过网络处理输入
计算 loss (输出和正确答案的距离)
将梯度反向传播给网络的参数
更新网络的权重,一般使用一个简单的规则:weight = weight - learning_rate * gradient
- LetNet的构建如下
import torch
import torch.nn as nn
import torch.nn.functional as F
# LeNet构建
class Net(nn.Module):
def __init__(self):
# 调用Net父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
super(Net, self).__init__()
# 第一层卷积,输入1个通道,输出6个通道,卷积核为5*5
self.conv1 = nn.Conv2d(1, 6, 5)
# 第二层卷积,输入6个通道,输出16个通道,卷积核为5*5
self.conv2 = nn.Conv2d(6, 16, 5)
# 全连接层,线性函数 y = wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 第一层全连接,输入16通道的5*5,输出120维
self.fc2 = nn.Linear(120, 84) # 第二层全连接,输入120维,输出84维
self.fc3 = nn.Linear(84, 10) # 第三层全连接,输入84维,输出10维
def forward(self, x):
# 前向传播函数
# 2*2 最大池化层,如果池化层是方阵,可以用一个数字表示
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 第一次池化
x = F.max_pool2d(F.relu(self.conv2(x)), 2) # 第二次池化
# print(x.size())
x = x.view(-1, self.num_flat_features(x)) # 特征维度平展到二维
# print(x.size())
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def num_flat_features(self, x):
# 批处理函数,将输入的多通道和维数转换为批不变,其他维度相乘,返回相乘维度,将输入的多维度(x, y, z)转换为(x, y*z)的维度
size = x.size()[1:] # 除去批处理的所有维度
num_features = 1
for s in size:
num_features *= s
return num_features
# 函数调用
net = Net()
print(net)
# 模型的可学习参数,模型的学习参数通过net.parameters()返回
params = list(net.parameters())
# print(params)
print(len(params))
print(params[0].size()) # conv1的权重
# 输出
Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
10
torch.Size([6, 1, 5, 5])
- 卷积中只需要定义 forward 函数,backward函数会在使用autograd时自动定义,backward函数用来计算导数。我们可以在 forward 函数中使用任何针对张量的操作和计算。
- 尝试一个随机的 32x32 的输入。注意:这个网络 (LeNet)的期待输入是 32x32 的张量。如果使用 MNIST 数据集来训练这个网络,要把图片大小重新调整到 32x32。
# 查看LeNet模型每层输出和参数量
from torchsummary import summary
summary(net, ( 1, 32, 32))
# 输出
torch.Size([2, 16, 5, 5])
torch.Size([2, 400])
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 6, 28, 28] 156
Conv2d-2 [-1, 16, 10, 10] 2,416
Linear-3 [-1, 120] 48,120
Linear-4 [-1, 84] 10,164
Linear-5 [-1, 10] 850
================================================================
Total params: 61,706
Trainable params: 61,706
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.05
Params size (MB): 0.24
Estimated Total Size (MB): 0.29
----------------------------------------------------------------
# 输入一个32*32的张量进行测试
input = torch.randn(1, 1, 32, 32) #
out = net(input)
print(out)
net.zero_grad() # 清零所有参数的梯度缓存
y = torch.randn(1, 10)
out.backward(y) # 然后进行随机梯度的反向传播
print(y)
# 输出
torch.Size([1, 16, 5, 5])
torch.Size([1, 400])
tensor([[-0.0152, -0.0361, 0.1007, 0.0999, -0.1195, 0.2055, 0.1756, 0.0438,
-0.0579, 0.0826]], grad_fn=<AddmmBackward>)
tensor([[ 0.8293, -2.5508, 0.1251, 0.4230, 0.7099, -0.3670, -0.0923, 0.1974,
0.1538, 1.8457]])
注意:
- torch.nn只支持小批量处理 (mini-batches)。整个 torch.nn 包只支持小批量样本的输入,不支持单个样本的输入。比如,nn.Conv2d 接受一个4维的张量,即nSamples x nChannels x Height x Width 如果是一个单独的样本,只需要使用input.unsqueeze(0) 来添加一个“假的”批大小维度。
- torch.Tensor - 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。
- nn.Module - 神经网络模块。是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。
- nn.Parameter - 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。
- autograd.Function - 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。
3.4 AlexNet
AlexNet包含5个卷积层,2个全连接隐藏层,1个全连接输出层。
代码构建如下:
class AlexNet(nn.Module):
def __init__(self):
# 调用父类的block构造函数,进行初始化
super(AlexNet, self).__init__()
# 卷积层
self.conv = nn.Sequential(
nn.Conv2d(3, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # 池化维度3*3, 步长为2
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2), #输入96通道,输出256通道,卷积核5*5, 步长1,填充2
nn.ReLU(),
nn.MaxPool2d(3, 2),
# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)
# 全连接层
# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 2048*2),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(2048*2, 2048*2),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(2048*2, 10) # 输出层,此处数据集的类别数为10,故输出10个
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
net = AlexNet()
print(net)
# 输出
AlexNet(
(conv): Sequential(
(0): Conv2d(3, 96, kernel_size=(11, 11), stride=(4, 4))
(1): ReLU()
(2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(4): ReLU()
(5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(7): ReLU()
(8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(9): ReLU()
(10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU()
(12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc): Sequential(
(0): Linear(in_features=6400, out_features=4096, bias=True)
(1): ReLU()
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU()
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=10, bias=True)
)
)
4. 模型初始化
- 常见的初始化函数
- 初始化函数的使用
4.1 torch.nn.init 初始化内容
注:如下函数后缀带"_"的函数表示会直接原地更改输入张量的值,除了caluate_gain其他都后缀带下划线
- torch.nn.init.uniform_(tensor, a=0.0, b=1.0) 用均匀分布值初始化权值
- torch.nn.init.normal_(tensor, mean=0.0, std=1.0) 用均匀分布值初始化权值
- torch.nn.init.constant_(tensor, val) 用常数初始化权值
- torch.nn.init.ones_(tensor) 全1初始化权值
- torch.nn.init.zeros_(tensor) 全0初始化权值
- torch.nn.init.eye_(tensor) 初始化为对角线为1,其余为0的权值
- torch.nn.init.dirac_(tensor,groups=1) 狄拉克函数初始化权值
- torch.nn.init.xavier_uniform__(tensor, gain=1.0) xavier初始化方法中服从均匀分布
- torch.nn.init.xavier_normal_(tensor, gain=1.0) xavier初始化方法中服从正态分布
- torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu') kaiming均匀分布
- torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu') kaiming正态分布
- torch.nn.init.orthogonal_(tensor, gain=1) 正交初始化权值
- torch.nn.init.sparse_(tensor, sparsity, std=0.01) 稀疏初始化,从正态分布N~(0. std)中进行稀疏化,使每一个column有一部分为0,sparsity 稀疏比例
-
torch.nn.init.calculate_gain(nonlinearity, param=None) 对于给定的非线性函数,返回推荐的增益值
# 简单练习
import torch.nn as nn
a = torch.empty(3, 6)
nn.init.xavier_uniform_(a, gain=nn.init.calculate_gain('leaky_relu'))
# 输出
tensor([[-0.4986, 0.6315, 1.1384, -0.8920, 0.1416, 0.6140],
[ 0.5029, -1.1283, -0.5017, 0.5786, 0.3405, 0.8477],
[-0.9166, 0.5869, -0.7927, -0.1856, 0.3337, -0.7702]])
torch.nn.init 权值初始化函数的使用
import torch
import torch.nn as nn
conv = nn.Conv2d(1, 3, 3)
linear = nn.Linear(10, 1)
# isinstance 判断模块属于什么类型
print(isinstance(conv, nn.Conv2d))
print(isinstance(linear, nn.Conv2d))
# 查看随机初始化化的conv和linear参数
print(conv.weight.data)
print(linear.weight.data)
# 输出
True
False
tensor([[[[-0.1006, 0.1492, 0.1672],
[ 0.1167, -0.1052, 0.0135],
[ 0.1805, -0.1558, -0.2909]]],
[[[ 0.2568, 0.1546, 0.0893],
[ 0.0225, -0.0839, 0.2826],
[-0.0809, 0.3102, 0.1544]]],
[[[ 0.3137, 0.1671, -0.2037],
[-0.0067, -0.2407, 0.0020],
[ 0.0813, -0.1400, -0.1205]]]])
tensor([[ 0.0726, 0.0475, 0.2055, -0.1105, 0.2350, 0.2639, -0.2084, 0.2437,
-0.0127, 0.1337]])
# 对前面构造的conv卷积进行kaiming均匀化初始权值
print('kaiming_normal_ 初始化conv卷积')
print(torch.nn.init.kaiming_normal_(conv.weight.data))
# 用常数值0.3对linear全连接层进行初始化
print('torch.nn.init.constant_ 初始化全连接')
print(torch.nn.init.constant_(linear.weight.data, 0.3))
# 输出
kaiming_normal_ 初始化conv卷积
tensor([[[[ 0.0538, 0.2147, -0.0225],
[ 0.1700, 0.3073, -0.0834],
[-0.3649, 0.0164, -0.1545]]],
[[[-0.6510, 0.9935, -0.1549],
[ 0.0458, 0.8514, 0.3266],
[-0.0984, -0.5209, 1.5362]]],
[[[ 0.9757, 0.6893, -0.2505],
[-0.2212, -0.0265, 0.5607],
[ 0.5993, -0.0175, -0.1020]]]])
torch.nn.init.constant_ 初始化全连接
tensor([[0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000,
0.3000]])
4.2 初始化函数的封装
人们常常将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。
# 模型的定义
class MLP(nn.Module):
# 声明带有模型参数的层
def __init__(self):
# 调用父类Block的构造函数来进行必要的初始化
super(MLP, self).__init__()
self.hidden = nn.Conv2d(1, 1, 3)
self.act = nn.ReLU()
self.output = nn.Linear(10, 1)
# 定义模型的向前计算,即如何根据输入x计算返回所需要的输出
def forward(self, x):
o = self.act(self.hidden(x))
return self.output(o)
# 定义模型的参数部分,遍历模型的每一层,判断当前层是什么类型,然后根据不同的类型设定不同的权值初始化方法
def initialize_weights(self):
for m in self.modules:
# 判断模型每层的类型
if isinstance(m, nn.Conv2d):
torch.nn.init.xavier_normal_(m.weight.data)
# 判断是否有偏置,有则用常数初始化
if m.bias is not None:
torch.nn.init.constant_(m.bias.data, 0.3)
elif isinstance(m, nn.Linear):
torch.nn.init.normal_(m.weight.data, 0.1)
if m.bias is not None:
torch.nn.init.zeros_(m.bias.data)
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
mlp = MLP()
print(list(mlp.parameters()))
# 输出
[Parameter containing:
tensor([[[[-0.2622, 0.2832, -0.1288],
[ 0.2878, 0.2217, -0.3297],
[-0.1407, -0.0966, 0.1863]]]], requires_grad=True), Parameter containing:
tensor([-0.1137], requires_grad=True), Parameter containing:
tensor([[ 0.2379, 0.0030, 0.1281, -0.1914, -0.2264, -0.0353, 0.1904, -0.2900,
-0.0179, -0.0621]], requires_grad=True), Parameter containing:
tensor([0.0286], requires_grad=True)]
5. 损失函数
- 二分类交叉熵损失函数
- torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')
- weight:每个类别的loss设置权值
- size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。
- reduce:数据类型为bool,为True时,loss的返回是标量。
- 交叉熵损失函数
- torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')
- L1损失函数
- torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')
- MSE损失函数
- torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')
- 平滑L1(Smooth L1)损失函数
- torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction='mean', beta=1.0)
- 目标泊松分布的负对数似然损失
- torch.nn.PoissonNLLLoss(log_input=True, full=False, size_average=None, eps=1e-08, reduce=None, reduction='mean')
- KL散度
- torch.nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)
- MargingRankingLoss
- torch.nn.MarginRankingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')
- 多标签边界损失函数
- torch.nn.MultiLabelMarginLoss(size_average=None, reduce=None, reduction='mean')
- 二分类损失函数
- torch.nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')torch.nn.(size_average=None, reduce=None, reduction='mean')
- 多分类的折页损失
- torch.nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')
- 三元组损失
- torch.nn.TripletMarginLoss(margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')
- HingEmbeddingLoss
- torch.nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean')
- 余弦相似度
- torch.nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')
- CTC损失函数
- torch.nn.CTCLoss(blank=0, reduction='mean', zero_infinity=False) 用于解决时序类数据的分类
简单的小练习
# 二分类交叉熵损失函数
m = nn.Sigmoid()
loss = nn.BCELoss()
# 输入和输出
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
output.backward()
print('BCELoss损失函数的计算结果为',output)
# 输出
BCELoss损失函数的计算结果为 tensor(1.0232, grad_fn=<BinaryCrossEntropyBackward>)
# 交叉熵损失函数
m = nn.Sigmoid()
loss = nn.CrossEntropyLoss()
# 输入和输出
input = torch.randn(2, 5, requires_grad=True)
target = torch.empty(2, dtype=torch.long).random_(2)
output = loss(m(input), target)
output.backward()
print(input)
print(target)
print('交叉熵损失函数的计算结果为',output)
# 输出
tensor([[-0.7215, -0.2738, -0.4215, 2.3478, 0.2908],
[ 1.4625, -0.6483, -0.4866, -0.2385, -0.5162]], requires_grad=True)
tensor([1, 1])
交叉熵损失函数的计算结果为 tensor(1.7406, grad_fn=<NllLossBackward>)
# L1损失函数
m = nn.Sigmoid()
loss = nn.L1Loss()
# 输入和输出
input = torch.randn(2, requires_grad=True)
target = torch.empty(2, dtype=torch.long).random_(2)
output = loss(m(input), target)
output.backward()
print(input)
print(target)
print('L1损失函数的计算结果为',output)
# 输出
tensor([-0.6597, 0.2193], requires_grad=True)
tensor([0, 0])
L1损失函数的计算结果为 tensor(0.4477, grad_fn=<L1LossBackward>)
# 余弦相似度
loss_f = nn.CosineEmbeddingLoss()
inputs_1 = torch.tensor([[0.3, 0.5, 0.7], [0.3, 0.5, 0.7]])
inputs_2 = torch.tensor([[0.1, 0.3, 0.5], [0.1, 0.3, 0.5]])
target = torch.tensor([1, -1], dtype=torch.float)
output = loss_f(inputs_1,inputs_2,target)
print('CosineEmbeddingLoss损失函数的计算结果为',output)
# 输出
CosineEmbeddingLoss损失函数的计算结果为 tensor(0.5000)
6. 训练和评估
- 设置模型状态,如下两个操作二选一即可,按模型状态区分:如果是训练状态,那么模型的参数应该支持反向传播的修改;如果是验证/测试状态,则不应该修改模型参数
- model.train() # 训练状态
- model.eval() # 验证/测试状态
- 读取数据,用for循环读取DataLoader中的全部数据
- for data, label in train_loader:
- 将数据放到GPU上用于后续计算,此处以.cuda()为例
- data, label = data.cuda(), label.cuda()
- 开始用当前批次数据做训练时,应当先将优化器的梯度置零
- optimizer.zero_grad()
- 将data送入模型中训练
- output = model(data)
- 根据预先定义的criterion计算损失函数
- loss = criterion(output, label)
- 将loss反向传播回网络
- loss.backward()
- 使用优化器更新模型参数
- optimizer.step()
这样一个训练过程就完成了,后续还可以计算模型准确率等指标,验证/测试的流程基本与训练过程一致,不同点在于:
- 需要预先设置torch.no_grad,以及将model调至eval模式
- 不需要将优化器的梯度置零
- 不需要将loss反向回传到网络
- 不需要更新optimizer
# 完整的图像分类训练过程如下所示
def train(epoch):
# 训练状态
model.train()
train_loss = 0
# 循环读取数据
for data, label in train_loader:
# 数据放到gpu进行计算
data, label = data.cuda(), label.cuda()
# 将优化器的梯度置0
optimizer.zero_grad()
# 将数据送入模型中进行训练
output = model(data)
# 根据预先定义的标准计算损失函数
loss = criterion(label, output)
# 将loss反向传播回网络
loss.backward()
# 使用优化器更新模型参数
optimizer.step()
train_loss += loss.item()*data.size(0)
# 训练损失求平均
train_loss = train_loss/len(train_loader.dataset)
print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))
# 完整的图像分类验证过程如下
def val(epoch):
# 进入验证模式
model.eval()
val_loss = 0
# 不需要将loss反向传播
with torch.no_grad():
for data, label in val_loader:
data, label = data.cuda(), label.cuda()
output = model(data)
preds = torch.argmax(output, 1)
loss = criterion(output, label)
val_loss += loss.item()*data.size(0)
running_accu += torch.sum(preds == label.data)
val_loss = val_loss/len(val_loader.dataset)
print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, val_loss))
7. pytorch优化器
优化器是根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签。
pytorch提供了优化器的库torch.optim,这里面有十种优化器,这些优化算法均继承于Optimizer,优化器的基类
- torch.optim.ASGD
- torch.optim.Adadelta
- torch.optim.Adagrad
- torch.optim.Adam
- torch.optim.AdamW
- torch.optim.Adamax
- torch.optim.LBFGS
- torch.optim.RMSprop
- torch.optim.Rprop
- torch.optim.SGD
- torch.optim.SparseAdam
优化器基类 Optimizer的三个属性说明:
- defaults 存储的是优化器的超参数,如{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}
- state:参数的缓存,
- param_groups:管理的参数组,是一个list,其中每个元素是一个字典,顺序是params,lr,momentum,dampening,weight_decay,nesterov
其他方法
- zero_grad():清空所管理参数的梯度,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。
- step():执行一步梯度更新,参数更新
- add_param_group():添加参数组
- load_state_dict() :加载状态参数字典,可以用来进行模型的断点续训练,继续上次的参数进行训练
- state_dict():获取优化器当前状态信息字典
# 优化器初始化的三个属性
class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []
# zero_grad():清空所管理参数的梯度,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。
def zero_grad(self, set_to_none: bool = False):
for group in self.param_groups:
for p in group['params']:
if p.grad is not None: #梯度不为空
if set_to_none:
p.grad = None
else:
if p.grad.grad_fn is not None:
p.grad.detach_()
else:
p.grad.requires_grad_(False)
p.grad.zero_()# 梯度设置为0
# step():执行一步梯度更新,参数更新
def step(self, closure):
raise NotImplementedError
import os
import torch
# 设置权重,服从正态分布 --> 2 x 2
weight = torch.randn((2, 2), requires_grad=True)
# 设置梯度为全1矩阵 --> 2 x 2
weight.grad = torch.ones((2, 2))
# 输出现有的weight和data
print("执行step前数据:\n{}".format(weight.data))
print("执行step前梯度:\n{}".format(weight.grad))
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
# 执行一步梯度更新,更新参数
optimizer.step()
# 查看进行一步后的值,梯度
print("执行一个step后的数据:\n{}".format(weight.data))
print("执行一个step后的梯度:\n{}".format(weight.grad))
# 权重清零
optimizer.zero_grad()
# 检验权重是否为0
print("权重清零后的梯度:\n{}".format(weight.grad))
# 输出参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})
# 查看现有的参数
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行5次step操作
for _ in range(50):
optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
# 保存参数信息
torch.save(optimizer.state_dict(),os.path.join("/Users/anker/Desktop/python_code/03_practise/", "optimizer_state_dict.pkl"))
print("----------done-----------")
# 加载参数信息
state_dict = torch.load("/Users/anker/Desktop/python_code/03_practise/optimizer_state_dict.pkl") # 需要修改为你自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))
# 输出
Output exceeds the [size limit]
执行step前数据: tensor([[-0.1998, -0.1699], [-0.6031, 0.2590]])
执行step前梯度: tensor([[1., 1.], [1., 1.]])
执行一个step后的数据: tensor([[-0.2998, -0.2699], [-0.7031, 0.1590]])
执行一个step后的梯度: tensor([[1., 1.], [1., 1.]])
权重清零后的梯度: tensor([[0., 0.], [0., 0.]])
... ...
# 剩余参数太多不一一展示,最好自己实操一遍
- 每个优化器都是一个类,我们一定要进行实例化才能使用
- optimizer在一个神经网络的epoch中需要实现两个步骤:梯度置零、梯度更新
- 给网络不同的层赋予不同的优化器参数。
代码举例:
# 实例化优化器
class Net(nn.Moddule):
···
net = Net()
optim = torch.optim.SGD(net.parameters(),lr=lr)
optim.step()
# 每个epoch中的梯度置0和梯度更新
optimizer = torch.optim.SGD(net.parameters(), lr=1e-5)
for epoch in range(EPOCH):
...
optimizer.zero_grad() #梯度置零
loss = ... #计算loss
loss.backward() #BP反向传播
optimizer.step() #梯度更新
# 给网络不同的层赋予不同的优化器参数。
from torch import optim
from torchvision.models import resnet18
net = resnet18()
optimizer = optim.SGD([
{'params':net.fc.parameters()},#fc的lr使用默认的1e-5
{'params':net.layer4[0].conv1.parameters(),'lr':1e-2}],lr=1e-5)
# 可以使用param_groups查看属性