一.使用PyTorch编写卷积神经网络
使用Pytorh编写卷积神经网络,一般需要导入以下模块:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models
使用PyTorch实现卷积网络的基本运算:
一个经典的卷积网络中包含了三类操作:卷积(Convolution)、池化(Pooling)、全连接(FC),并且卷积与全连接层一般都会使用非线性响应函数(Activation Function)。
在PyTorch中,torch.nn实现了以上的操作,它们对应的各个函数如下所示:
卷积:conv2d
这个函数有几个重要的参数,对应了卷积层的超参数:
- in_channels:卷积层的输入层特征图的通道数。
- out_channels:卷积层输出的特征图通道数(即卷积核的数目)。
- kernel_size:卷积核的大小。
- padding:输入特征图每边的填充量(填充值为0)。默认的padding=0,表明不填充,对应VALID CONVOLUTION;padding = (kernel_size - 1)/2,对应SAME CONVOLUTION,输出的特征图的尺寸与输入特征图的尺寸相同。
- stride:步长(也称跨度),通常为0(该参数的默认值为0)。
- bias:布尔型参数,False代表该层没有偏置参数;默认为Ture,表示该层有偏置参数。
接下来我们来举一个栗子:我们用下面的代码定义一个卷积操作,其中输入的特征图的通道为16,输出通道为32,卷积核的大小为3 * 3,使用VALID CONVOLUTION:
conv = nn.conv2d(in_channels=16,out_channels=32,kernel_size=3,padding=0,bias=True)
池化:MaxPool2d,AvgPool2d
这两个函数的主要参数是kernel_size和stride,通常stride=kernel_size。
我们也来举个例子:我们定义一个2 * 2的最大池化操作:
pool = nn.MaxPool2d(kernel_size=2,stride=2)
全连接:Linear
该函数有三个参数,分别为:
- in_features:表示输入特征的维度。
- out_features:表示输出特征的维度。
- bias:布尔型参数,False表示该层没有偏置参数;默认为Ture,表示该层有偏置函数。
我们还是举一个栗子:我们定义一个全连接层,输入与输出特征维数分别是128,256:
fc = nn.Linear(in_features=128, out_features=256, bias=True)
非线性响应函数(激活函数)
这些函数由torch.nn.functional提供:最常用的是torch.nn.functional.relu、torch.nn.functional.sigmoid、torch.nn.functional.softmax。其中第一个是作用于神经元的响应函数,后面两个通常用作分类函数。
二.编写简单的卷积神经网络
要创建一个神经网络,需要从nn.Module派生出一个子类,并实现该子类的初始化方法以及前向传播方法。PyTorch通过自动求导(AutoGrad)机制实现后向传播。
下面的代码实现了一个简单的卷积神经网络,这个网络的输入为28 * 28的彩色图像(CIFAR10),输出为10个类别。
class CNN(nn.Module):
def __init__(self):
# 注意:首先调用父类的初始化函数
super(CNN, self).__init__()
# 定义卷积、池化以及全连接操作
self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5)
self.pool2 = nn.MaxPool2d(kernel_size=3, stride=3)
self.conv3 = nn.Conv2d(in_channels=32, out_channels=128, kernel_size=3)
self.fc1 = nn.Linear(1 * 1 * 128, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 10)
def forward(self,x):
#在前向函数中构造出卷积网络
#注意这里的x把不同层连接起来
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = self.conv3(x)
#使用torch.Tensor.view函数,把张量reshape成适合全连接层的维度
x = x.view(-1, 128)
#全连接层
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
三.训练神经网络
1.准备训练样本
训练神经网络需要先准备好训练数据,卷积网络的训练数据通常是图像数据,torchvision模块提供了很多常见的图像数据库的封装,比如MNIST,CIFAR,IMAGENET等,这些标准数据库都定义为torchvision.datasets模块中的类,比如MNIST数据集对应于torchvision.datasets.MNIST类。
构造图像数据集:
除了这些标准的数据集之外,也可以使用torchvision.datasets.ImageFolder类构造图像数据集。构造自己的图像数据集时,需要把图像按照不同的类别放入不同目录中。
下面的代码定义了一个数据集,该数据集中的数据位于目录’./data’中:
data_set = torchvision.datasets.ImageFolder(root='./data')
定义数据变换:
在卷积网络的训练中,通常会对图像数据做预处理和各种变换,比如对图像像素值做归一化、图像翻转、裁剪等等。除此之外,在PyTorch中,图像数据要转换为torch.Tensor才能输入到网络中。
torchvision.transforms模块提供了大量图像变换的类和函数。常见的有:
- torchvision.transforms.ToTensor,把一个PIL图像或numpy.ndarray转换为torch.Tensor。
- torchvision.transforms.ColorJitter(brightness=0,contrast=0,saturation=0,hue=0),对PIL图像的亮度、对比度、饱和度以及色调进行随机扰动。
- torchvision.transforms.RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode=‘constant’),对图像进行随机裁剪,裁剪后的图像大小由参数size确定。
- torchvision.transforms.RandomHorizontalFlip(p=0.5),根据指定的概率p随机对图像做左右翻转。
- torchvision.transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.33333333333), interpolation=2),根据指定的缩放系数范围scale和长宽比例范围ratio,对图像进行随机缩放和比例变换,然后随机裁剪为一幅size大小的图像。
- torchvision.transforms.Resize(size, interpolation=2),对图像进行缩放,缩放后的尺寸为size。这是最常用的图像变换,因为卷积网络的输入图像大小是恒定的,因此所有图像样本都必须要缩放到符合网络的输入尺寸。
- torchvision.transforms.Normalize(mean, std, inplace=False),对一个张量进行规范化处理,这里的两个参数分别表示均值和标准差。
除了上面讲到的变换之外,torchvision.transforms还提供了一个类Compose,可以把多个变换组合在一起。
下面定义一个数据变换,它由随机缩放裁剪、随机水平翻转以及规范化等变换组合而成:
normalize = transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
data_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),# 把图像变换为张量
normalize,# 注意规范化要在ToTensor之后
])
如果需要对图像数据做变换,那就在构造数据集对象时指定初始化参数transform的值。下面的代码构造了一个数据集对象data_set,使用了上面定义的数据变换:
data_set = torchvision.datasets.ImageFolder(root='./data', transform=data_transform)
构造数据加载器:
torch.utils.data.DataLoader类是一个数据加载器,实现了对数据集进行随机采样和多轮次迭代的功能。
在网络训练过程中,借助这个加载器可以非常方便的实现多轮次小批量梯度随机下降训练。下面的代码构造了一个数据加载器,用于加载数据集data_set,并指定batch_size为32,其中参数shuffle=True表明每一个轮次之前都会对数据做随机排列,这在随机梯度下降法中是必要的:
data_loader = torch.utils.data.DataLoader(dataset=data_set, batch_size=32, shuffle=True)
2.训练网络参数
定义损失函数:
torch.nn模块中定义了很多标准的损失函数。下面的代码定义了一个交叉熵函数对象:
entropy = nn.CrossEntropyLoss()
损失函数对象有一个重要的方法:backward(),该方法是使用BP算法训练网络参数的关键,通过这个方法实现了误差反向传播。
假设网络的输出为outputs,真实类别为labels,下面的代码计算了两者之间的损失函数值,然后进行误差反向传播:
loss = enytropy(outputs, labels)
loss.backward()
定义优化器:
torch.optim模块提供了很多优化算法类,比如:torch.optim.SGD,torch.optim.Adam,torch.optim.RMSprop。
构造优化器对象有两个重要参数,一个是优化变量,另一个是学习率。接下来我们写一个代码构造一个SGD优化器,用于优化前面定义的CNN网络模型的参数,设置学习率为0.01,动量参数为0.9:
net = CNN()
optimizer = torch.optim.SGD(params=net.parameters(), lr=0.01, momentum=0.9)
优化器的方法step()用于执行一步梯度下降,更新模型参数。
优化过程:
神经网络训练过程的一步迭代包含四个主要步骤:
- 前向运算,计算给定输入的预测结果。
- 计算损失函数值。
- 反向传播(BP),计算参数梯度。
- 使用梯度下降法更新参数值。
在PyTorch中,参数的梯度由变量维护,在每一步计算梯度之前应该先把前一步计算的梯度清除掉(调用优化器的zero_grad()函数)。
下面我们编写以下代码详细解释这个迭代过程:
#(inputs,labels)为训练样本
#1)首先要把前一步的梯度清除,即设置梯度值为0:
optimizer.zero_grad()
#2)前向运算,计算网络在inputs上的输出outputs
outputs = net(inputs)
#3)计算损失函数
loss = entropy(outputs, labels)
#4)反向传播,计算梯度
loss.backward()
#5)梯度下降,更新模型参数
optimizer.step()
3.完整的训练和测试代码
下面的函数train实现了模型训练的完整过程。
这里需要注意的是:训练模型之前需要调用模型的train()函数,进入训练模式。通过调用模型和数据张量的to函数,实现在指定的设备(GPU、CPU)进行训练。
def train(net, optimizer, loss_fn, num_epoch, data_loader, device):
net.train()#进入训练模式
for epoch in range(num_epoch):
running_loss = 0.0
for i, data in enumerate(data_loader):
inputs, labels = data[0].to(device), data[1].to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99:
print('[%d, %5d] loss = %.3f' % (epoch+1, i +1, running_loss/100))
running_loss 0.0
接着我们再编写以下测试代码:
测试模型的精度时,需要调用模型的eval()函数,进入评估模式。
下面的函数evaluate实现了完整的模型测试过程,函数返回分类准确率。
def evaluate(net, data_loader, device):
net.eval()#进入模型评估模式
correct = 0
total = 0
with torch.no_grad():
for data in data_loader:
images, labels = data[0].to(device), data[1].to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = correct / total
return acc
四.我们给出一个完整的实例代码:CIFAR10图像分类
def main():
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))]
)
train_set = torchvision.datasets.CIFAR10(root='./data',
train=true,
download=True,
transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_set,
batch_size=32,
shuffle=true,
num_workers=2)
test_set = trochvision.datasets.CIFAR10(root='./data',
train=False,
download=true,
transform=transform)
test_loader = torch.utils.data.DataLoader(datasets=test_set,
batch_size=32,
shuffle=False,
num_workers=2)
device = torch.device("cuda:0")
net = CNN()
net.to(device)
entropy = nn.CrossEntropyLoss()
optimizer = optim.RMSprop(net.paramerters(), lr=0.001)
num_epoch = 20
train(net=net,
optimizer=optimizer,
loss_fn=entropy,
num_epoch=num_epoch,
data_loader=train_loader,
device=device)
test_acc = evaluate(net=net,
data_loader=test_loader,
device=device)
print('training Accuracy: %.2f %%' %(100 * train_acc))
print('Test Accuracy: %.2f %%' % (100 * test_acc))
if __name__ == '__mian__':
main()
模型保存与加载
模型优化好之后,通常要把模型参数保存下来,以便后续使用。
PyTorch推荐的保存方法是保存状态词典(state_dict),状态词典是网络模型每一层到对应参数张量的映射,通过调用nn.Module类的方法state_dict()获得模型的状态词典。
下面我们编写一下相应的代码具体理解一下:
model = CNN()
state_dict = model.state_dict()
print('State_dict of CNN model:')
for param_tensor in state_dict:
print(param_tensor, '\t', state_dict[param_tensor].szie())
状态词典的保存和加载分别由torch.save()和torch.load()实现。状态词典通常保存为一个.pth文件。
如果要使用已经保存的模型参数继续训练模型或者对新的数据做预测,需要调用模型的load_state_dict()函数把从磁盘文件中读入的状态词典加载到模型中。
下面的代码演示了保存和加载状态词典到一个模型中:
model = CNN()
#训练模型......
#保存训练好的模型参数到文件model.pth中
state_dict = model.state_dict()
torch.save(state_dict, 'my_model.pth')
#使用保存的模型参数:
state_dict = torch.load('my_model.pth')
#把读入的模型参数加载到模型model中:
model.load_state_dict(state_dict)