卷积神经网络(手写数字识别)
何为卷积
首先在理解CNN前,需要先理解什么是卷积
卷积是一种数学的运算,以一个图片为例,它具有3个颜色通道,假如他的尺寸是4x4,那么在数学上我们可以将他表示为一个size = 3x4x4的张量,而我们规定卷积运算是输入张量与卷积核之间的运算,例如我们设置一个3x3x3的卷积核,并且约定步长为1
那么卷积运算可以这么理解:
对卷积核的每个通道,它将会分别和输入张量的对应通道做卷积,然后将所得的矩阵求和,对于单个通道与单个卷积核对应通道,具体的运算方法如图
值得注意的是,卷积核的通道数必须和输入张量的通道数保持一致
但是卷积操作其实是可以改变输入张量的通道数的,我们可以人为的设计多个卷积核,一个卷积核经过卷积将会得到输出张量的一个通道,n个卷积核将会得到n个通道
事实上卷积过程中我们还可以人为设置卷积的步长stride,当我们不去设置它时,他默认是1,即对一个区域卷积后,卷积核会向后移动一个单位长度,如果已经到了最右端,他将会向下移动.
如果我们设置了stride=2,那么卷积核将会两个单位长度的移动
在设计卷积层的过程中,我们常常会进行padding操作,要理解padding,首先我们要仔细观察一下卷积的过程,我们会发现处在边缘地带的区块,被卷积核提取信息的次数明显会少于中间的区块,事实上,边缘区域也很有可能包含了关键的信息,如果我们直接卷积,无形之中就让边缘区域变得不那么重要了
那么怎么解决这个问题呢,我们需要进行padding
我们可以在输入张量每一层的边缘补充0,将例如原来3x3的矩阵扩展为4x4,甚至5x5那么那些包含了信息的区域,就能够同等的被提取信息.
卷积神经网络的架构(简单的手写数字识别)
下面我就一个简单的卷积神经网络(AlexNet)谈谈我对架构的理解:
接下来先是一些基本的操作的概念.
如何分类
首先我们就MNIST数据集而言,输入的是一张张灰度图,然后输出的是属于哪个类别,这是一个明显的分类问题,对于分类问题我们通常有几个类别就输出一个几维向量,这个向量的每一个位置的元素表示该输入属于这一类别的概率,例如分类是不是猫,假如网络输出了(0.3,0.7)这样一个向量,如果我们约定零号位置是该输入是猫的概率,那么一号位置我们约定是不是猫的概率,这两个概率之和必须为一,更多类别时也是同理,我们一般取概率最大的位置对应的类别为分类结果.
什么是池化
池化(pooling)操作一般常用的分为两种,一种是max_pooling,一种是avg_pooling,即最大值池化,和平均池化,不过不管是哪种池化,我们首先需要定义单次池化的范围大小,一般我们都是对正方形区域进行池化,例如我们设置一个2x2的区域,如果使用最大值池化,那么这个区域里四个数值,我们只保留数值最大的,类似的平均值池化保留四个位置的平均值.进行池化后,矩阵变小,我们适当的舍弃了一些作用不大信息,这有助于我们减少训练成本,同时又尽可能保证了训练的结果,事实上,大多数时候max_pooling能取得比avg_pooling更好的结果.
具体的操作流程
因此首先我们要将输入的灰度图转换为可以处理的张量,然后利用卷积操作提取信息,之后我们对卷积后的结果进行一次池化,进一步精炼信息,然后我们进行非线性化,这便是一轮处理.同样的操作我们可以设置多轮,有助于网络更好的学习.最后我们需要设置一个全连接层,也就是一般的bp神经网络,输出一个十维的向量,放到分类器中,从而得到分类结果和相应的loss值,最后调用后向传播算法,经过多轮训练,使网络收敛.
具体的pytorch实现(对MNIST的手写数字识别)
第一步:加载MNIST数据集
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
batch_size = 64 # 设置批量大小
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# 根据MNIST的均值和标准差对数据集做一些处理
train_dataset = datasets.MNIST('../data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST('../data', train=False, transform=transform, download=True)
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)
这里由于MNIST数据集在pytorch中已经内置了,所以我们直接使用对应的模块加载即可
第二步:定义网络的模块
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
self.pooling = torch.nn.MaxPool2d(kernel_size=2)
self.fc1 = torch.nn.Linear(320, 50) # 全连接层
self.fc2 = torch.nn.Linear(50, 10)
self.ReLU = torch.nn.ReLU()
def forward(self, x):
batch_size = x.size(0)
x = self.ReLU(self.pooling(self.conv1(x))) # 先卷积,再池化,再非线性化
x = self.ReLU(self.pooling(self.conv2(x)))
x = x.view(batch_size, -1) # 展成一维向量,便于后续全连接层处理
x = self.ReLU(self.fc1(x))
x = self.fc2(x) # 使用crossEntropyLoss时最后一层不能激活
return x
值得注意的是,由于我们做的是多分类任务,最后一层不能激活,因为后续的分类器crossEntropyLoss已经自带了激活功能.
第三步:实现网络,分类器,优化器
net = Net()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")###
net.to(device)###
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.5)
没有GPU的,可以把带###号的GPU的操作去掉
第四步:训练模块
def train(epoch):
running_loss = 0.0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device) ### 计算的张量迁移到显卡
y_pred = net(data)
loss = criterion(y_pred, target)
optimizer.zero_grad()
loss.backward() # 反向传播
optimizer.step()
running_loss += loss.item() # 采用了MINI_BATCH 损失要累加起来
if batch_idx % 300 == 299:
print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0.0
# 每三百次计算一次平均损失
这里其实就是一般的训练流程了,输入,前向传播,计算损失,反向传播,修改参数
第五步:测试模块
def Test():
correct = 0
total = 0
with torch.no_grad(): # 测试集无需计算梯度,只是做测试,不对网络进行优化
for data, target in test_loader:
data, target = data.to(device), target.to(device) ###
output = net(data)
_, predicted = torch.max(output.data, 1) # 找到预测值最大的类别
total += target.size(0)
correct += (predicted == target).sum().item() # 统计预测正确的次数
print('Accuracy of the network on the test images: %d %% [%d/%d]' % (100 * correct / total, correct, total))
这里是在测试集上进行测试,统计正确率
第六步:开始训练,测试
if __name__ == '__main__':
for epoch in range(0, 10):
train(epoch)
Test()
在本地训练结果如下:
[1, 300] loss: 1.036
[1, 600] loss: 0.221
[1, 900] loss: 0.154
Accuracy of the network on the test images: 96 % [9635/10000]
[2, 300] loss: 0.114
[2, 600] loss: 0.102
[2, 900] loss: 0.091
Accuracy of the network on the test images: 97 % [9728/10000]
[3, 300] loss: 0.076
[3, 600] loss: 0.072
[3, 900] loss: 0.073
Accuracy of the network on the test images: 98 % [9825/10000]
[4, 300] loss: 0.062
[4, 600] loss: 0.063
[4, 900] loss: 0.057
Accuracy of the network on the test images: 98 % [9842/10000]
[5, 300] loss: 0.052
[5, 600] loss: 0.051
[5, 900] loss: 0.049
Accuracy of the network on the test images: 98 % [9862/10000]
[6, 300] loss: 0.046
[6, 600] loss: 0.045
[6, 900] loss: 0.044
Accuracy of the network on the test images: 98 % [9874/10000]
[7, 300] loss: 0.045
[7, 600] loss: 0.039
[7, 900] loss: 0.039
Accuracy of the network on the test images: 98 % [9874/10000]
[8, 300] loss: 0.037
[8, 600] loss: 0.036
[8, 900] loss: 0.038
Accuracy of the network on the test images: 98 % [9875/10000]
[9, 300] loss: 0.036
[9, 600] loss: 0.033
[9, 900] loss: 0.033
Accuracy of the network on the test images: 98 % [9863/10000]
[10, 300] loss: 0.031
[10, 600] loss: 0.030
[10, 900] loss: 0.032
Accuracy of the network on the test images: 98 % [9893/10000]
在MNIST数据集上取得了98%的正确率.
总结
自此我们成功利用简单的卷积神经网络对实现了手写数字识别,事实上这只是最简单的卷积神经网络,后续还有 GoogleNet , ResNet等网络.以后我将会逐个复现.
本人只是一个深度学习的初学者,感谢您愿意阅读我的博文,如果文章有错误,或者有可以改进的地方,欢迎批评指正.再次感谢!
8 % [9893/10000]
在MNIST数据集上取得了98%的正确率.
## 总结
自此我们成功利用简单的卷积神经网络对实现了手写数字识别,事实上这只是最简单的卷积神经网络,后续还有 GoogleNet , ResNet等网络.以后我将会逐个复现.
本人只是一个深度学习的初学者,感谢您愿意阅读我的博文,如果文章有错误,或者有可以改进的地方,欢迎批评指正.再次感谢!