PyTorch实现神经网络图像分类

PyTorch实现神经网络图像分类

本文章主要参考《深度学习与图像识别 原理与实践一书》

上篇文章介绍了KNN分类算法,并用其来实现MNISTCifar10数据集的分类任务,这篇文章同样是做这两个数据集的分类,但是使用的Pytorch构建神经网络算法来完成实验,是我自己对Pytorch的一个初步学习。

之前一直是使用Keras,之所以学习Pytorch是因为,Keras比较适合作为练习使用的深度学习框架,因为其过度的封装导致学习时无法理解深度学习的真正内涵。

PyTorch的使用

Tensor

Tensor是Pytorch中的基础组件,Tensor与Numpy中的ndarrays非常类似,但是Tensor可以使用GPU加速而ndarrays不可以。在pytorch下使用如下命令来进行GPU运算:

1
2
3
4
5
6
import torch

if torch.cuda.is_available():
x = x.cuda()
y = y.cuda()
print(x+y)

x和y为Tensor类型的数据,如果电脑无GPU,则无法进行GPU运算。

TersonNumpy互相转换

1
2
3
4
5
6
7
8
9
10
11
import torch
import numpy as np

np_data = np.arange(8).reshape((2, 4))
# 将numpy数组转换为tensor结构
torch_data = torch.from_numpy(np_data)
print(np_data)
print(torch_data)
# 转回numpy
np_data2 = torch_data.numpy()
print(np_data2)

Tensor做矩阵运算(矩阵相乘)

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import numpy as np

np_data = np.array([[1, 2], [3, 5]])
torch_data = torch.from_numpy(np_data)
print(np_data)
# numpy中 *号并非真正的矩阵乘法,只是对应位置相乘
print(np_data*np_data)
# numpy中使用dot进行矩阵乘法
print(np_data.dot(np_data))
# tensor中使用mm方法进行矩阵乘法
print(torch_data.mm(torch_data))

Variable

简单理解,Variable是对Tensor的一种封装,每个Variable中包含三个属性(data、grad以及creator):Variable中的Tensor本身(通过.data进行访问)、对应Tensor的梯度(通过.grad进行访问)以及创建这个Variable的Function引用(通过.grad_fn进行访问),该引用可用于回溯整个创建链路,如果是用户自己创建Variable,则其grad_fn为None。

1
2
3
4
5
6
7
8
9
from torch.autograd import Variable

# 从标准正太分布中返回多个样本值
x_tensor = torch.randn(10, 5)
# Variable默认不需要求梯度,这里申明需要求梯度
x = Variable(x_tensor, requires_grad=True)
print(x.data)
print(x.grad)
print(x.grad_fn)

激活函数

之前的版本通过import torch.nn.functional as F来加载激活函数,随着pytorch版本的更新,如今通过torch可以直接加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import matplotlib.pyplot as plt

# 这里从-6到6之间生成200个点(x值)
tensor = torch.linspace(-6, 6, 200)
tensor = Variable(tensor)
np_data = tensor.numpy()
print(np_data)

# 定义激活函数
y_relu = torch.relu(tensor).data.numpy()
y_sigmoid = torch.sigmoid(tensor).data.numpy()
y_tanh = torch.tanh(tensor).data.numpy()

plt.figure(1, figsize=(8, 6))
plt.subplot(221)
# 计算这些x值的激活值,并绘制曲线
plt.plot(np_data, y_relu, c='red', label='relu')
plt.legend(loc='best')

plt.subplot(222)
plt.plot(np_data, y_sigmoid, c='red', label='relu')
plt.legend(loc='best')

plt.subplot(223)
plt.plot(np_data, y_tanh, c='red', label='relu')
plt.legend(loc='best')

plt.show()

具体这些激活函数都是啥,有什么用,前面的文章已经介绍了,感兴趣可以查看。

损失函数

均方差损失函数

1
torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')

参数说明:

  • size_average: 弃用
  • reduce:弃用
  • reduction(string, optional):输出元素包含3种操作方式,即none、mean和sum。

交叉熵损失函数

1
torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')

参数说明:

  • weight(Tensor, optional):多分类任务中,手动给出每个类别权重的缩放量。如果给出,则其是一个大小等于类别个数的张量。
  • size_average:弃用
  • reduce:弃用
  • ignore_index(int, optional):指定被忽略且不对输入梯度做贡献的目标值。
  • reduction:输出元素包含3种操作方式,即none、mean和sum。

示例代码:

1
2
3
4
5
loss = torch.nn.CrossEntropyLoss()
input_data = torch.randn(3, 5, requires_grad=True)
target = torch.empty(3, dtype=torch.long).random_(5)
out_put = loss(input_data, target)
out_put.backward()

注意:CrossEntropyLoss不支持one-hot编码类型,输入的都是真实的target,所以如果输入的真实分类是one-hot编码的话需要自行转换,即将target ont-hot编码格式转换为每个样本的类别,再传给CrossEntropyLoss

PyTorch 实战

MNIST分类

首先,定义神经网络:自定义神经网络模型在Pytorch中需要继承torch.nn.Module,然后自己重写Forward方法完成前向计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from torch.utils.data import DataLoader
from torchvision import datasets
import torchvision.transforms as transform
from torch.autograd import Variable


# 创建神经网络模型
class NeuralNetwork(nn.Module):
# 初始化函数,接受自定义输入特征的维数,隐含层特征维数以及输出层特征维数
def __init__(self, _input_size, _hidden_size, _out_put):
super(NeuralNetwork, self).__init__()
# 从输入到隐藏层的线性处理
self.layer1 = nn.Linear(_input_size, _hidden_size)
# 从隐藏层输出层的线性处理
self.layer2 = nn.Linear(_hidden_size, _out_put)

def forward(self, x):
# 从输入层到隐藏层的线性计算
out = self.layer1(x)
# 隐藏层激活
out = torch.relu(out)
# 输出层,注意,输出层直接接Loss
out = self.layer2(out)
return out

if __name__ == "__main__":
input_size = 784 # mnist的像素为28*28
hidden_size = 500
num_classes = 10 # 输出为10个类别分别对应于0~9
net = NeuralNetwork(input_size, hidden_size, num_classes)
print(net)

网络结构打印的输出结果具体如下:

1
2
3
4
NeuralNetwork(
(layer1): Linear(in_features=784, out_features=500, bias=True)
(layer2): Linear(in_features=500, out_features=10, bias=True)
)

接下来使用Pytorch加载数据集,需要注意的是要将数据集转换成tensor张量,可以直接使用transform。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MNIST:

def __init__(self):
self.batch_size = 100
self.train_data_set = None
self.test_data_set = None
self.train_loader = None
self.test_loader = None

def init_data(self):
# MNIST data_set
self.train_data_set = datasets.MNIST(
root='/ml_dataset/pymnist', # 选择数据保存路径
train=True, # 选择训练集
transform=transform.ToTensor(), # 转换成Tensor变量
download=True # 从网络上下载图片
)

self.test_data_set = datasets.MNIST(
root='/ml_dataset/pymnist',
train=False, # 选择测试集
transform=transform.ToTensor(),
download=True
)

# 加载数据
self.train_loader = torch.utils.data.DataLoader(
dataset=self.train_data_set,
batch_size=self.batch_size,
shuffle=True # 将数据打乱
)

self.test_loader = torch.utils.data.DataLoader(
dataset=self.test_data_set,
batch_size=self.batch_size,
shuffle=True
)

# 数据打乱取小批次
print('批次的尺寸:', self.train_loader.batch_size)
print('load_train_data:', self.train_loader.dataset.data.shape)
print('load_train_labels:', self.train_loader.dataset.targets.shape)

if __name__ == "__main__":
mnist = MNIST()
mnist.init_data()

然后编写训练代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
learning_rate = 1e-1  # 学习率 0.1
num_epoch = 5 # 训练5轮
criterion = nn.CrossEntropyLoss() # 损失函数 交叉熵
# 优化器,使用随机梯度下降
optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate)
for epoch in range(num_epoch):
print(f'current epoch = {epoch}')
for i, (images, labels) in enumerate(mnist.train_loader):
# view类似于reshape操作,把图片转成一维
images_var = Variable(images.view(-1, 28*28))
labels_var = Variable(labels)

outputs = net.forward(images_var) # 将数据集传入网络做前向计算
loss = criterion(outputs, labels_var) # 计算Loss
optimizer.zero_grad() # 在做反向传播之前先清除下网络状态
loss.backward() # Loss反向传播
optimizer.step() # 更新参数

if i % 100 == 0:
print(f'current loss = {loss.item()}')
print('finished training')

初步学习pytorch确实感觉比Keras要自己做的操作多一些,比如构建网络,自己定义forward方法,然后这里计算损失,反向传播和更新参数等,更符合神经网络的一种学习流程。

最后预测准确率

1
2
3
4
5
6
7
8
9
10
11
12
total = 0
correct = 0
for images, labels in mnist.test_loader:
images = Variable(images.view(-1, 28*28))
outputs = net.forward(images)

# 使用max()函数,对softmax函数的输出值进行操作,求出预测值索引
_, predict = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predict == labels).sum()

print(f'Accuracy = {correct/total}')

最后的准确率大约在96%左右,比使用KNN算法得到的95%效果还要更好些。

这里有以下几个方法可能需要简单解释下:

images.view()类似于numpyreshape方法,用来改变tensor的形状,比如这里images的原始形状是[100, 28, 28],这里view之后变成了[100, 784],-1表示根据后面设置的列自适应调整。

然后这里引用一段关于view与reshape不同的地方:view()方法只适用于满足连续性条件的tensor,并且该操作不会开辟新的内存空间,只是产生了对原存储空间的一个新别称和引用,返回值是视图。

torch.max(input, dim)

  • inputsoftmax函数输出的一个tensor
  • dim是max函数索引的维度0/10是每列的最大值,1是每行的最大值
  • 函数会返回两个tensor,第一个tensor是每行/列(根据dim参数决定)的最大值;第二个tensor是每行/列最大值的索引。

在多分类任务中一般我们并不需要知道各类别的预测概率,所以返回值的第一个tensor对分类任务没有帮助,而第二个tensor包含了预测最大概率的索引,所以在实际使用中我们仅获取第二个tensor即可。

size()类似于numpyshape,返回tensor的形状,并且可以在括号中设置索引,表示返回该维的大小,如images.size()==>[100, 3, 32, 32],则images.size(0)==>100、images.size(2)==>32

sum()类似于numpysum(),求和,相应的有一个ndim参数,ndim=1计算的是行的和,ndim=0计算的是列的和。这里predict==lables返回的tensor,相同值的位置为1,不同值的位置为0,求和就表示预测对了多少个。

Cifar10分类

Cifar10数据集是彩色图像,同样也包含10个类别,图片尺寸为3*32*32。

首先定义神经网络,一个两层的网络(不算输出层),比上面的多一层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import torch
import numpy as np
from torch.utils.data import DataLoader
from torchvision import datasets
import matplotlib.pyplot as plt

from torchvision import transforms
from torch.autograd import Variable
import torch.nn as nn

class NeutralNetWork(nn.Module):

def __init__(self, _input_size, _hidden_size1, _hidden_size2, _num_classes):
super(NeutralNetWork, self).__init__()
self.layer1 = nn.Linear(_input_size, _hidden_size1)
self.layer2 = nn.Linear(_hidden_size1, _hidden_size2)
self.layer3 = nn.Linear(_hidden_size2, _num_classes)

def forward(self, x):
# 每层计算后进行一次激活
out = torch.relu(self.layer1(x))
out = torch.relu(self.layer2(out))
# 输出层输出后连接LOSS
out = self.layer3(out)
return out

if __name__ == "__main__":
# 这里的输入和输出是根据数据集而定的
input_size = 3072 # 32*32*3
hidden_size1 = 500
hidden_size2 = 200
num_classes = 10
net = NeutralNetWork(input_size, hidden_size1, hidden_size2, num_classes)
print(net)

加载Cifar10数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Cifar10:

def __init__(self):

self.batch_size = 100
self.train_data_set = None
self.test_data_set = None
self.train_loader = None
self.test_loader = None

def init_data(self):
self.train_data_set = datasets.CIFAR10(
root='/ml_dataset/pycifar',
train=True,
transform=transforms.ToTensor(), # 转换成Tensor变量
download=True
)
self.test_data_set = datasets.CIFAR10(
root='/ml_dataset/pycifar',
train=False,
transform=transforms.ToTensor(),
download=True
)

self.train_loader = torch.utils.data.DataLoader(
dataset=self.train_data_set,
batch_size=self.batch_size,
shuffle=True
)
self.test_loader = torch.utils.data.DataLoader(
dataset=self.test_data_set,
batch_size=self.batch_size,
shuffle=True
)

训练数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
num_epoch = 5
learning_rate = 1e-3 # 学习率0.001
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate)

for epoch in range(num_epoch):
print(f'current epoch = {epoch}')
for i, (images, labels) in enumerate(cifar10.train_loader):
images_var = Variable(images.view(images.size(0), -1))
labels_var = Variable(labels)
optimizer.zero_grad()
outputs = net(images_var)
loss = criterion(outputs, labels_var)
loss.backward()
optimizer.step()

if i % 100 == 0:
print(f'current loss = {loss.item()}')
print('Finished training')

测试数据

1
2
3
4
5
6
7
8
9
10
11
12
total = 0
correct = 0
for images, labels in cifar10.test_loader:

images = Variable(images.view(images.size(0), -1))
# 这里不指定forward方法也会默认执行forward方法
outputs = net(images)

_, predict = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predict == labels).sum()
print(f'Accuracy = {correct/total}')

我在自己的电脑上测试准确率只有20%,可以看出,浅层神经网络可以解决一部分简单的问题,但对于稍微复杂一些的彩色数据集表现并不理想,后面会进一步使用深度卷积神经网络实现Cifar10分类。