文章目录
- 一、回顾:
- 二、基础概念
- 三、CNN流程
- 四、pytorch细节实现
- 五、完整代码
- 六、课后作业
一、回顾:
在普通的神经网络中,我们把二维特征变为batch_size=N的一维特征(由于在pytorch中只能对二维矩阵进行处理,所以在forward方法内对x又进行了一次处理,所以我们把1×28×28的矩阵(784个像素值)转变为N×784的矩阵)。
但是由于这种处理方式,直接的维度变换使图像的空间特征消失了,所以出现了CNN,通过卷积层的变化他可以保留图片的空间特征,进而提高结果的准确率。
二、基础概念
(1)简单理解下什么卷积?
取图片的一小部分我们会得到一些像素点,由于图像是彩色的所以我们可以得到右面代表RGB三原色的三层结构,当我们对图像"进行遍历"的时候,拿一块长方体(占取部分原图像的channel、weight、height)在图像中移动,而这个过程就是在做卷积。
(2)单层卷积的运算过程:
彩色图片有3层channel,也就是每层都有不同的像素值,那么我们先将每一层拆分来看是如何计算的:
左边灰色的部分是一层像素值,黄色的是卷积核(filter),蓝色的是一次卷积的结果。红色框住的部分和黄色的部分做数乘(对应位置相乘并相加)得到卷积结果的左上角值。
依次移动卷积核的位置得到下一个数:
直至得到如下的输出:
这里解释一下为什么Output是3*3的矩阵,因为按照上面的移动方法,会使得卷积核的中心只能在绿色的框内进行运动,也就是中心移动9次,那么可以推断出最后计算的次数为9,形状和绿框相同=输出的矩阵。
(2)三层卷积的运算过程:
三层输入则需要三层的卷积核,框住的位置相乘后得到输出的左上角
立体计算图就是这样:
他们的维度的直观表示如下:
我们可以知道当以有一个卷积核的时候可以得到一层的output,那么当有多个卷积核就可以得到多层的output,再将它们连接起来即可,如下:
因此我们可以得出:卷积核的channel和input的channel一样,输出的channel和卷积核的个数有关(与输入的channel个数无关),也就是下图的表示。
(3)在做卷积时候的技巧——padding
当有 5x5 的输入时通过 3x3 的卷积核会得到3x3的输出。
如果想用3x3的卷积核得到 5x5 的输出,那么就需要扩大对input的扫描范围,使卷积核的中心从input的第一个位置进行移动即在外围加入一圈0。如果想用 5x5 的卷积核得到 5x5 的输出,那么就需要在input中添加两圈0。添加0的圈数有一个规律:卷积核的n/2圈。如果能自己根据题意想明白最好。
可以将这个过程想象成将input的每一个数通过了函数y=w×x,这里的input相当于x,kernel(卷积核)相当于w,那么我们可以得到output矩阵的计算过程代码:
import torch
input = [3, 4, 6, 5, 7,
2, 4, 6, 8, 2,
1, 6, 7, 8, 4,
9, 7, 4, 6, 2,
3, 7, 5, 4, 1
]
# 将input转为tensor数据
input = torch.Tensor(input).view(1, 1, 5, 5)
# 参数依次表示batchsize,channel,width,height
# 设置卷积核
conv_layer = torch.nn.Conv2d(1, 1, kernel_size=3, padding=1, bias=False)
# 参数依次表示:输入通道数,输出通道数,卷积核的大小,padding添加的圈数,无需偏置量
kernel = torch.Tensor([1, 2, 3, 4, 5, 6, 7, 8, 9]).view(1, 1, 3, 3)
conv_layer.weight.data = kernel.data
output = conv_layer(input)
print(output)
输出:
tensor([[[[ 91., 168., 224., 215., 127.],
[114., 211., 295., 262., 149.],
[192., 259., 282., 214., 122.],
[194., 251., 253., 169., 86.],
[ 96., 112., 110., 68., 31.]]]], grad_fn=<ThnnConv2DBackward>)
(4)在做卷积时候的技巧——stride
stride表示每次卷积核移动的距离,当tride=2时卷积核的中心只会经过下图蓝色的四个位置,所以output变为2×2.
对上面的代码进行修改,将paddig=1改为stride=2其余不变得到新的output
输出:
tensor([[[[211., 262.],
[251., 169.]]]], grad_fn=<ThnnConv2DBackward>)
(4)下采样——max booling
只在一个通道内进行操作(不hi改变通道的数目),默认stride=2,可以获得四个区域,并取得四个区域的最大值拼的新的矩阵使得图像的大小变为原来的一半。
实现如下:
maxbooling_layer = torch.nn.MaxPool2d(kernel_size=2)
三、CNN流程
首先要确定输入输出的维度,为了达到输出的维度需要明白中间层的维度是如何变换的
实现步骤如下:
代码实现如下:
import torch
import torch.nn.functional as F
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(2)
# 设置flatten后的线性变化层
self.linear = torch.nn.Linear(320, 10)
def forward(self, x):
# flatten data (n,1,28,28) -> (n,784)
batch_size = x.size(0)
# 卷积->激活->池化->卷积->激活->池化—>线性变化
x = self.pooling(F.relu(self.conv1(x)))
x = self.pooling(F.relu(self.conv2(x)))
x = x.view(batch_size, -1)
x = self.linear(x)
return x
x = Net()
四、pytorch细节实现
大部分与这篇没有用CNN的方法差不多,详见
(1)数据加载同上篇文章,详见链接
import torch
from torchvision import transforms # 对数据进行原始处理
from torchvision import datasets
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import torch.optim as optim
transform = transforms.Compose([
transforms.ToTensor(), # 转变为1×28×28的tensor
transforms.Normalize((0.1307,), (0.3081,)) # 控制像素值在[0,1]
])
batch_size = 64
train_dataset = datasets.MNIST(root='E:/datasets_download/dataset/mnist/', train=True, download=True,
transform=transform)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = datasets.MNIST(root='E:/datasets_download/dataset/mnist/', train=False, download=True,
transform=transform)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
(2)创建连接网络
(3)设置loss、optimizer
同上篇文章:
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5) # momentum=0.5加上冲量(惯性大速度快)优化计算
(4)迭代更新
这里添加了使用GPU的运算
五、完整代码
import torch
from torchvision import transforms # 对数据进行原始处理
from torchvision import datasets
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import torch.optim as optim
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(2)
# 设置flatten后的线性变化层
self.linear = torch.nn.Linear(320, 10)
def forward(self, x):
# flatten data (n,1,28,28) -> (n,784)
batch_size = x.size(0)
# 卷积->激活->池化->卷积->激活->池化—>线性变化
x = self.pooling(F.relu(self.conv1(x)))
x = self.pooling(F.relu(self.conv2(x)))
x = x.view(batch_size, -1)
x = self.linear(x)
return x
model = Net()
transform = transforms.Compose([
transforms.ToTensor(), # 转变为1×28×28的tensor
transforms.Normalize((0.1307,), (0.3081,)) # 控制像素值在[0,1]
])
batch_size = 64
train_dataset = datasets.MNIST(root='E:/datasets_download/dataset/mnist/', train=True, download=True,
transform=transform)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = datasets.MNIST(root='E:/datasets_download/dataset/mnist/', train=False, download=True,
transform=transform)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5) # momentum=0.5加上冲量(惯性大速度快)优化计算
# 用cuda进行计算,"cuda:0"表示用第一块显卡运算
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将model将要用到的weight等送入duda
model.to(device)
def train(epoch):
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):
inputs, target = data
inputs, target = inputs.to(device), target.to(device)
optimizer.zero_grad()
# forward+backward+update
outputs = model(inputs)
loss = criterion(outputs, target)
loss.backward()
optimizer.step()
running_loss += loss.item()
if batch_idx % 300 == 299:
print('[%d, %5d] loss %.3lf' % (epoch + 1, batch_idx + 1, running_loss / 2000))
running_loss = 0
def test():
correct = 0
total = 0
with torch.no_grad(): # 由于只需要forward不需要计算梯度
for data in test_loader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, dim=1) # 取每一行最大的预测值,_接收最大值的下标
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy on test set: %d %%' % (100 * correct / total))
if __name__ == '__main__':
for epoch in range(10):
train(epoch)
test()
运行时间3分钟,比直接使用cpu快了2分钟(上次试验结果请详见链接结尾处),准确率也得到了提升
[1, 300] loss 0.102
[1, 600] loss 0.031
[1, 900] loss 0.020
Accuracy on test set: 96 %
[2, 300] loss 0.018
[2, 600] loss 0.014
[2, 900] loss 0.013
Accuracy on test set: 97 %
[3, 300] loss 0.012
[3, 600] loss 0.011
[3, 900] loss 0.011
Accuracy on test set: 98 %
[4, 300] loss 0.010
[4, 600] loss 0.010
[4, 900] loss 0.009
Accuracy on test set: 98 %
[5, 300] loss 0.009
[5, 600] loss 0.009
[5, 900] loss 0.008
Accuracy on test set: 98 %
[6, 300] loss 0.007
[6, 600] loss 0.008
[6, 900] loss 0.008
Accuracy on test set: 98 %
[7, 300] loss 0.007
[7, 600] loss 0.007
[7, 900] loss 0.007
Accuracy on test set: 98 %
[8, 300] loss 0.006
[8, 600] loss 0.007
[8, 900] loss 0.007
Accuracy on test set: 98 %
[9, 300] loss 0.006
[9, 600] loss 0.006
[9, 900] loss 0.006
Accuracy on test set: 98 %
[10, 300] loss 0.006
[10, 600] loss 0.006
[10, 900] loss 0.005
Accuracy on test set: 98 %
六、课后作业
改进:卷积层为三层、激活层变为三层、池化层变为三层,线性变换为三层,具体见下图:
这里直接贴出代码:
只对Net()函数做修改:将卷积核的5×5改为3×3,池化层不变,那么最后的线性层是40->10
import torch
from torchvision import transforms # 对数据进行原始处理
from torchvision import datasets
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import torch.optim as optim
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
# 设置卷积核
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=3)
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=3)
self.conv3 = torch.nn.Conv2d(20, 40, kernel_size=3)
# 设置池化层
self.pooling = torch.nn.MaxPool2d(2)
# 设置flatten后的线性变化层
self.linear = torch.nn.Linear(40, 10)
def forward(self, x):
# flatten data (n,1,28,28) -> (n,784)
batch_size = x.size(0)
# 卷积->激活->池化 ->卷积->激活->池化 —>卷积->激活->池化 ->线性变化
x = self.pooling(F.relu(self.conv1(x)))
x = self.pooling(F.relu(self.conv2(x)))
x = self.pooling(F.relu(self.conv3(x)))
x = x.view(batch_size, -1)
x = self.linear(x)
return x
运行结果如下:
[1, 300] loss 0.217
[1, 600] loss 0.055
[1, 900] loss 0.038
Accuracy on test set: 93 %
[2, 300] loss 0.032
[2, 600] loss 0.027
[2, 900] loss 0.025
Accuracy on test set: 95 %
[3, 300] loss 0.022
[3, 600] loss 0.022
[3, 900] loss 0.020
Accuracy on test set: 96 %
[4, 300] loss 0.018
[4, 600] loss 0.018
[4, 900] loss 0.016
Accuracy on test set: 96 %
[5, 300] loss 0.016
[5, 600] loss 0.015
[5, 900] loss 0.015
Accuracy on test set: 96 %
[6, 300] loss 0.013
[6, 600] loss 0.014
[6, 900] loss 0.014
Accuracy on test set: 96 %
[7, 300] loss 0.012
[7, 600] loss 0.012
[7, 900] loss 0.013
Accuracy on test set: 97 %
[8, 300] loss 0.011
[8, 600] loss 0.012
[8, 900] loss 0.011
Accuracy on test set: 97 %
[9, 300] loss 0.010
[9, 600] loss 0.011
[9, 900] loss 0.010
Accuracy on test set: 97 %
[10, 300] loss 0.010
[10, 600] loss 0.010
[10, 900] loss 0.009
Accuracy on test set: 97 %
但最后的结果不如两层5×5的卷积核好。
我又将卷积核个数做了一些修改,但是结果变得更差了,此处不贴代码了。