AI入门:神经网络实战----卷积神经网络
前言
我们使用的MNIST数据集中的数据大体是这样的:有效的像素只占据28 * 28个像素中的很小一部分。另外,即使是有效的像素之间,它们也不一定有联系,一般来说,一个像素点只会跟它附近的像素有联系,与比较远的像素没有联系。
上一节,我们使用全连接把输入层与第二层相连接,单单这一层的连接数就有784 * 256 = 20万个。这个数据是比较大的,事实上也是非常浪费的。如果我们能够把这些有效像素提取出来,则我们减少大量计算,同时提高准确率。再进一步,其实我们可能只需要知道几个点,就可以判断这个图像表示什么数字,而没有必要了解所有点的情况,这样就可以进一步提高运行速度和准确率。
什么是卷积,为什么要卷积
卷积是一个数学概念,表示两个多项式A(x)和B(x)的每个元素相乘之后再累加的结果。把卷积操作应用到神经网络中就是卷积神经网络。以下图为例,原始图片上3 * 3的数据,与3 * 3的卷积核发生卷积操作之后,只生成了一个数据。这样相当于把9个数据变成一个数据了,极大的减少了数据量。换一种思路就是我们可以通过卷积操作提取到原始图片中的关键特征。不过这样就会产生一个问题:怎么保证提取到的特征是真正需要的特征呢?
关于这个问题,其实没有好的方法,目前通用的解决方案是换不同的卷积核,多卷积几次。这样可以保证把需要的特征都提取出来。这种方案的问题是可能会重复提取特征。从下图可以看到,如果把原始图片中的数据看做是一个3 * 3的矩阵,卷积核也看做是一个3 * 3的矩阵,这个卷积操作其实就是矩阵的乘法运算,最后再把生成的矩阵中的所有元素都累加得到结果。这个过程在GPU中计算特别快,另外,由于卷积神经网络中有80%的工作都是在做卷积操作,因此强烈建议在GPU中训练卷积神经网络。
经过卷积操作之后,我们可以把图片的特征压缩到比较小的程度,但是我们可以进一步通过池化操作减少数据量。所谓池化就是2 * 2的元素中进行四选一,或者更进一步,使用3 * 3的元素中进行九选一。池化有最大池化和平均池化两种方式,我们一般选用最大池化。也就是在4个元素中挑选最大的那个。通过池化可以减少75%的数据量。
通过上述卷积和池化操作之后,我们发现一张图片被转化成很多张小图片了。这些小图片中存放的就是提取出来的特征。对于单次计算而言,计算量急剧下降。但是由于有很多小图片,所以总体的计算量是增加很多的。
卷积相关函数
卷积的函数:nn.Conv2d(input, output, kernel_size, padding=0, stride = 0)
input:输入的图片的channel数。这里输入的是MNIST数据集,是灰度图,所以是channel数为1。
output:输出的channel数。这个数据是程序员确定的,建议是2^n。
kernel_size:卷积核的大小,一定要是奇数,这里选择5。注意,卷积核的长和宽是相等的。 padding:卷积核移动的时候,原图片的像素可能不够用,所以要补充一些像素,这些像素的值都是0,这样就不会对原来的结果产生影响。padding的默认值为0。
stride默认为1,也就是卷积核每次移动一个像素。
其它:bias=True,表示使用bias。
最大池化的函数:F.max_pool2d(input, (2, 2))
input就是卷积 (并且激活) 后的数据,(2, 2)表示从2*2的数据中挑选最大的那个元素。
值得注意的是,卷积神经网络提取特征的能力很强,而且特征容易被重复提取。所以相当于神经网络被反复训练,这样就会导致这个神经网络对训练数据的预测效果很好,但对测试数据的预测效果不好。根本原因是这个神经网络已经记住了训练数据,而不是“识别”出来。专业术语称之为“过拟合”。为了解决过拟合的问题,现在主要有L1、L2正则化、dropout等三种方式。其中效果最好的是dropout。
dropout的函数:input = F.dropout(input, p, training)。
p是参数的保留率,一般是0.5~0.8之间的一个数。dropout的输出结果与输入数据的形状相同。
编写卷积神经网络,计算图片尺寸、参数量
我们可以看到,卷积操作时,图片是被当作一个矩阵看待的,而不像全连接层时作为一维数据看待。由于图片是需要考虑通道数的 (灰度图的通道数为1,彩色图为3)。所以,在做卷积操作时,一定要保证输入数据的格式为: (BATCH_SIZE, channels, width, height)。MNIST数据集中的数据是被处理成(28, 28)的,所以在做卷积操作之前要变换一下张量的形状。
当卷积完成之后,我们还需要进行两次全连接操作。全连接的输入数据是一维的,所以在卷积完成后需要再改变一下形状。
下面我们计算一下卷积前后特征图片的尺寸。在卷积之前,图片的尺寸是(28, 28)。如果我们的卷积核的尺寸是(5, 5),每次移动一步。则卷积之后的图片的宽度为:28 - 5 + 1 = 24。用这种方式进行卷积的话,经过多次卷积,图片尺寸会变得越来越小。那么有什么办法让卷积得到的图片不变小 (与原始图片保持一样的尺寸) 呢?这时我们就需要在图片四周填充一些没有信息的空白像素。我们发现卷积之后图片的宽度小了4,所以我们应该在图片两侧各填充2个像素。这个操作的专业术语称之为:padding。
卷积之后图片的宽度的计算公式:width2 = (width + 2 * padding - kernel_size)/stride + 1。反过来,如果我们知道卷积前后的图片的尺寸,也可以推导出计算padding的公式:padding = ((width2 -1) * stride - width + kernel_size) / 2。
下面是卷积的代码:
首先在__init__()中定义两个卷积层,然后是三个全连接层,最后输出预测结果。
这里输入的是MNIST数据集,是黑白图片,所以是channel数为1。所以第一层的Conv2d()中第一个参数是1。第二个参数6是程序员指定的,表示输出的channel数,第三个参数5表示卷积核的大小。这里使用padding = 2,所以卷积之后的图片的宽度 = 高度 = (28 + 2 * 2 - 5) / 1 + 1 = 28,也就是卷积前后图片的尺寸保持不变。这个卷积层的参数量是:1 * 6 * 5 * 5 = 150个。紧接着这个卷积层,后面有一个(2, 2)的池化层,使得图片的尺寸变为(14, 14)。
第二层的Conv2d()参数也是类似。第一个参数6就是上一层卷积产生的6层的图片,第二个参数16是程序员指定的,表示输出的channel数,第三个参数5表示卷积核的大小。这里使用padding = 0,所以卷积之后的图片的宽度 = 高度 = (14 + 2 * 0 - 5) / 1 + 1 = 10,也就是卷积之后图片的尺寸变为(10, 10)。这个卷积层的参数量是:6 * 5 * 5 * 16 = 2400个。紧接着这个卷积层,后面有一个(2, 2)的池化层,使得图片的尺寸变为(5, 5)。
在完成上述卷积、池化操作后,我们要把这些图片输入到三层全连接层。这时我们一共有16张图片,每张图片的尺寸是(5, 5),所以一共有16 * 5 * 5 = 400个特征。第一个全连接层的神经元一共有128个,所以这两层之间的参数有16 * 5 * 5 * 128 = 51200个 (只考虑w,不考虑b)。
第二层全连接的参数数量为:128 * 64 = 8192个。
第三层全连接的参数数量为:64 * 10 = 640 个。
我们可以看到,总的参数量为:150 + 2400 + 51200 + 8192 + 640 = 62582个,其中卷积层与第一个全连接层之间的参数占据了81.8%,(只考虑w,不考虑b)。
在定义完各个层的参数之后,就是编写forward()函数:forward()中各层的代码是非常简单的,就是一层一层累加上去即可。
这里需要注意三点:
① 卷积、激活、池化、dropout这四个操作的次序不要弄乱;
② 卷积完成后要先把图片数据转成一维的。这时一定要明确的指出各个参数,不能让PyTorch自动计算。
③ 全连接层的最后的输出不需要激活,也不需要softmax。
定义完模型后要实例化一个模型的变量,接着指定优化器、学习率、以及损失函数的计算公式。这些与上一节的全连接的神经网络一样。
卷积神经网络的训练和测试
卷积神经网络的训练和测试与上一节的全连接的神经网络一样。这里就不重复写了。
在训练的时候,我们发现如果给定一个比较大的batch_size,则有可能导致占用内存很大。如果使用的是CPU进行训练,且内存占用率超过总的内存大小,则系统直接死机。如果使用的是GPU,则报错 (但不死机)。用CPU训练全连接的神经网络速度是比较快的,但是训练卷积神经网络就非常慢了,使用GPU可以大大加速训练过程。当然,使用CPU和GPU得到的结果是一样的。
经过5个epochs的计算,卷积神经网络的准确率可以达到97%,比全连接的神经网络高约2%。增加epochs到20,最终可以达到98%以上的准确率。
总结
这一节里,我们提出了使用全连接神经网络的一些问题:大量无效数据/计算,准确率比较低。为此我们尝试用卷积神经网络解决这些问题。我们讨论了卷积、池化操作,也介绍了dropout。并分别计算了每一层的参数量,经过每一层操作之后的图片的尺寸等。
这里我们埋下一个伏笔:卷积层与全连接层之间的参数量非常大,占总参数量的81.8%。参数量大意味着训练和测试的计算量大,速度降低。我们是否可以降低这两层之间的参数量,以提高计算速度?
import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.autograd import Variable
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
BATCH_SIZE = 4
#加载torchvision包内内置的MNIST数据集
#这里涉及到transform:将图片转化成torchtensor
train_dataset = datasets.MNIST(root='./', #保存/读取数据集的路径
train=True, #是否用于训练
transform=transforms.ToTensor(),
download=False) #是否要下载
test_dataset = datasets.MNIST(root='./',
train=False, #测试集,不用于训练
transform=transforms.ToTensor())
#train_loader和test_loader用于加载小批次数据,
#即将MNIST数据集中的data分成每组batch_size的小块,
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=BATCH_SIZE,
shuffle=True)#是否随机读取
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=BATCH_SIZE,
shuffle=False)#是否随机读取
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
#卷积层。各参数的意思:输入的图片的channel数、输出的channel数、kernel_size和padding
#其它:步长stride默认为1,bias=True
#卷积之前,图片的尺寸是28 * 28,
#卷积之后:图片的宽度是(28 + 2 * 2 - 5)/ 1 + 1 = 28。即卷积前后图片尺寸不变
#卷积之后有(2, 2)的池化,尺寸变为(14, 14)
self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
#这个卷积之前的图片尺寸是(14, 14)。
#卷积之后:(14 + 2 * 0 - 5)/1 + 1 = 10
#卷积之后有(2, 2)的池化,尺寸变为(5, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
#nn.Linear()就是全连接层
#卷积层的输出channel的数量为16,图片的尺寸为5 * 5。 所以这个全连接层的输入就是16 * 5 * 5。
#这里的输出是程序员指定的,这里是128
self.fc1 = nn.Linear(16 * 5 * 5, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 10)#MNIST数据集中的数据一共有10种可能,所以最后输出10
def forward(self, x):
#print(x.shape) #torch.Size([4, 1, 28, 28])
#卷积、tanh激活、池化,卷积核尺寸(2, 2) -> (1, 1)
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.dropout(x, p = 0.5, training = self.training)
x = F.max_pool2d(F.relu(self.conv2(x)), (2, 2))
x = F.dropout(x, p = 0.5, training = self.training)
#-1表示batch_size,其余就是16 * 5 * 5
#卷积之后,把每一张图片都变成一维的数据,也就是Flatten()
x = x.reshape(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.dropout(x, p = 0.5, training = self.training)
x = F.relu(self.fc2(x))
x = F.dropout(x, p = 0.5, training = self.training)
x = self.fc3(x)
return x
# def num_flat_features(self, x):
# size = x.size()[1:]
# num_features = 1
# for s in size:
# num_features *= s
# return num_features
model = LeNet5()
optimizer = optim.SGD(model.parameters(), lr = 0.1, momentum=0.5)
criterion = nn.CrossEntropyLoss()
# for (data, target) in train_loader:
# for i in range(4):
# plt.figure()
# plt.imshow(data[i].numpy()[0])
# break
# plt.show()
def train(epoch):
#形成训练期间的网络
#因为dropout()在训练时只使用部分神经元,但测试时使用所有神经元
#也就是说训练时的网络与测试时的网络不是同一个网络
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data = Variable(data)
target = Variable(target)
optimizer.zero_grad()#PyTorch下,梯度是累加的,所以要清零
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
#每隔100次,计算一次损失
if batch_idx %100 ==0:
print(loss.item())
Loss.append(loss.item())
print('Train Epoch:{}[{}/{}({:.0f}%]\tLoss:{:.6f}'.format(epoch,
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
return loss.data
def test():
model.eval() #使用测试时的网络
test_loss = 0
correct = 0
for data, target in test_loader:
data = Variable(data)
target = Variable(target)
#print(data) #4组数据
#print(target)#tensor([6, 5, 2, 5])
output = model(data)
#sum up batch loss
test_loss += criterion(output, target).item()
#get the index of the max log-probability
pred = output.data.max(1, keepdim = True)[1] #得到10个数中的一个数,后面的[1]表示index
#print(output.data.max(1, keepdim = True)) #torch.return_types.max(values=tensor([[0.1735],[0.1452],[0.1544],[0.1555]]),
#print(pred)#indices=tensor([[7], [7], [7], [7]]))
#pred是否等于target的值
correct += pred.eq(target.data.view_as(pred)).cpu().sum()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss:{:.4f}, Accuracy:{}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
Loss = []
for epoch in range(5):
loss = train(epoch)
Loss.append(loss)
test()
print("完成")
还没有评论,来说两句吧...