用随机的共享的卷积核得到像素点的加权和从而提取到某种特定的特征,然后用反向传播来优化卷积核参数就可以自动的提取特征,是CNN特征提取的基石。
基础知识
全连接的权值数:4x4x4=64(4个神经元,每个神经元都有4x4个不同的权值,这里先不考虑偏置值)
局部连接:一个神经元只于图片中的部分像素点有关系,即一个神经元连接部分像素点。
只局部连接不权值共享的权值数:4x4=16(4个神经元,每个神经元都有4个不同的权值)
权值共享:是其中一个神经元的是4个权值,所谓权值共享,就是其他神经元的权值也使用这四个值。
局部连接和权值共享的权值数: 4(每个神经元都是相同的4个权值)
上图包含输入层总共8层网络,分别为:
输入层(INPUT)、卷积层(Convolutions,C1)、池化层(Subsampling,S2)、卷积层(C3)、池化层(S4)、卷积层(C5)、全连接层(F6)、输出层(径向基层)
输入层(INPUT):
输入的手写体是32x32像素的图片,在论文里说输入像素的值背景层(白色)的corresp值为-0.1,前景层(黑色)的corresp值为 1.175。这使得平均输入大约为0,而方差大约为1,从而加速了学习,要求手写体应该在中心,即20x20以内。
卷积层(Convolutions,C1): 通过卷积运算,可以使原信号特征增强,并且降低噪音
特征平面的概念:
我们从上图可以看到有6个特征平面(这里不应该称为卷积核,卷积核是滑动窗口,通过卷积核提取特征的结果叫特征平面,特征平面的个数与卷积核的个数一致),得到的每个特征平面使用的一个5x5的卷积核(这里说明窗口滑动的权值就是卷积核的内容,这里需要注意的是特征平面有6个说明有6个不同的卷积核,因此每个特征平面所使用的权值都是一样的,这样就得到了特征平面)。
那么特征平面有多少神经元呢?32x32通过一个5x5的卷积核运算,根据局部连接和平滑,需要每次移动1,因此从左移动到右时是28,因此特征平面是28x28的,即每个特征平面有28x28个神经元。6个特征平面对应6个不同的卷积核或者6个滤波器,每个滤波器的参数值也就是权值都是一样的,这样的平面有6个,即卷积层有6个特征平面。
现在我们计算一下该层总共有多少个连接,有多少个待训练的权值呢?
连接数,首先每个卷积核是5x5的,每个特征平面有28x28的神经元(每个神经元对应一个偏置值),总共有6个特征平面,因此连接数为:(5x5+1)x28x28x6 = 122304。
权值数,首先每个特征平面神经元共用一套权值,而每套权值取决于卷积核的大小,因此权值数为:(5x5+1)x6 = 156个
池化层(Subsampling,S2): 降低数据维度,只改变H、W,不改变C
池化层又叫下采样层,目的是压缩数据,降低数据维度,池化和卷积有明显的区别,这里采样2x2的选择框进行压缩,如何压缩呢,通过选择框的数据求和再取平均值然后在乘上一个权值和加上一个偏置值,组成一个新的图片,每个特征平面采样的权值和偏置值都是一样的,因此每个特征平面对应的采样层只两个待训练的参数。如下图4x4的图片经过采样后还剩2x2,直接压缩了4倍。本层具有激活函数,为sigmod函数,而卷积层没有激活函数。
S2层有12个可训练参数和5880个连接
卷积层(C3):
这一层也是卷积层,和C2不同的是这一层有16个特征平面,那么16个特征平面是如何和上一层池化层是如何对应的呢?这里的16个特征平面是这样对应的,每个特征平面对应的卷积核,和池化层的多个平面进行卷积。这里把C3的卷积层特征平面编号即0,1,2,…,15,把池化层S2也编号为0,1,2,3,4,5.这两层具体如何对应呢?如下图
上面说了,C3层和S2的对应关系和前面不一样,主要体现在C3的每一个特征平面是对应多个池化层的采样数据,如上图,横向的数表示卷积层C3的特征平面,纵向表示池化层的6个采样平面,我们以卷积层C3的第0号特征平面为例,它对应了池化层的前三个采样平面即0,1,2,三个平面使用的是三个卷积核(每个采样平面是卷积核相同,权值相等,大小为5x5),既然对应三个池化层平面,那么也就是说有5x5x3个连接到卷积层特征平面的一个神经元,因为池化层所有的样本均为14x14的,而卷积窗口为5x5的,因此卷积特征平面为10x10(大家可按照第一个卷积计算求的)。只是这里的卷积操作要更复杂,他不是所有的都是特征平面对应三个池化层平面,而是变化的,从上图我们可以清楚的看到前6个特征平面对应池化层的三个平面即0,1,2,3,4,5 , 而6~14每张特征平面对应4个卷积层,此时每个特征平面的一个神经元的连接数为5x5x4,最后一个特征平面是对应池化层所有的样本平面,这里大家好好理解。我们来计算一下连接数和待训练权值个数:
连接数: (5x5x3+1)x10x10x6+(5x5x4+1)x10x10x9+(5x5x6+1)x10x10 = 45600+90900+15100=151600
权值数: (5x5x3+1)x6 + (5x5x4+1)x9 + 5x5x6+1 = 456 + 909+151 = 1516
这一层为什么要这样做呢?为什么不和前面的一样进行卷积呢?Lecon的论文说,主要是为了打破对称性,提取深层特征,因为特征不是对称的,因此需要打破这种对称,以提取到更重要的特征,这样设计的目的就是这个原因
池化层(S4):
这一层采样和前面的采样是一样的,使用的采样窗口为2x2的,对C3层进行采样,得到16个采样平面,此时的采样平面为5x5的,这里不细讲了,另外本层存在激活函数,为sigmod函数。
卷积层(C5):
这一层还是卷积层,且这一层的特征平面有120个,每个特征平面是5x5的,而上一层的池化层S2只有16个平面且每个平面为5x5,本层使用的卷积核为5x5,因此和池化层正好匹配,那么怎么连接呢?很简单就是这里每个特征平面连接池化层的所有的采样层。这里称呼特征平面已经不合适了,因为每个卷积核只对应一个神经元了,因此本层只有120个神经元并列排列,每个神经元连接池化层的所有层。C5层的每个神经元的连接数为5x5x16+1,因此总共的连接数为:(5x5x16+1)x120=48120,而这一层的权值和连接数一样,因此也有48120个待训练权值。结合下面的图看:
全连接层(F6):
这一层其实就是BP网络的隐层,且为全连接层,即这一层有84个神经元,每一个神经元都和上一次的120个神经元相连接,那么连接数为(120+1)x84 = 10164,因为权值不共享,隐层权值数也是10164,至于为什么隐层是84个神经元稍后解释,本层的输出有激活函数,激活函数为双曲正切函数
模型代码
import torch.nn as nn
import torch.nn.functional as F
#pytorch tensor的通道排序[batch,channel,height,width]
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()#继承父类的构造函数
self.conv1 = nn.Conv2d(3, 16, 5)#输入通道3,输出通道16--卷积核的个数,卷积核5x5
self.pool1 = nn.MaxPool2d(2, 2)#下采样,池化核大小为2x2,步距为2
self.conv2 = nn.Conv2d(16, 32, 5)#输入通道16,输出通道32--卷积核的个数,卷积核5x5
self.pool2 = nn.MaxPool2d(2, 2)#下采样,池化核大小为2x2,步距为2
self.fc1 = nn.Linear(32*5*5, 120)#全连接输入为一维向量,需要把我们得到的矩阵展成一维向量。然后连接120个节点
self.fc2 = nn.Linear(120, 84)#第2个全连接,输入为120个节点,输出为84个节点
self.fc3 = nn.Linear(84, 10)#第3个全连接,输入为84个节点,输出为10个节点--根据训练集的类别确定的
def forward(self, x): # x--代表我们训练的数据 #input(3, 32, 32)
x = F.relu(self.conv1(x)) # output(16, 28, 28)
x = self.pool1(x) # output(16, 14, 14)
x = F.relu(self.conv2(x)) # output(32, 10, 10)
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5) batch= -1表示自动推理,channel为32*5*5,把矩阵展成一维数据
x = F.relu(self.fc1(x)) # output(120)
x = F.relu(self.fc2(x)) # output(84)
x = self.fc3(x) # output(10) 最后不用加入softmax函数,因为交叉熵损失函数内部存在softmax了
return x
训练数据
import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
def main():
############################################数据预处理########################################################
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
#把PIL图像或者numpy数组(H x W x C)、范围为0-255 转换成 tensor (C x H x W)、范围为0--1
#使用均值和标准差来标准化这个tensor
# input[channel] = (input[channel] - mean[channel]) / std[channel]
##############下载导入训练集###############
# 50000张训练图片
# 第一次使用时要将download设置为True才会自动去下载数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
#把我们下载好的数据集逐批次的导入,打乱顺序
train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
shuffle=True, num_workers=0)
###############下载导入测试集#########################
# 10000张验证图片
# 第一次使用时要将download设置为False才会自动去下载数据集
val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=False, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader)#把val_loader变成可以迭代的迭代器
val_image, val_label = val_data_iter.next()#通过 .next()就可以获得一批数据,数据中包含了测试图像和所对应的标签
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
#############################################实例化模型########################################################
net = LeNet()#实例化模型
#########################################损失函数和优化器######################################################
loss_function = nn.CrossEntropyLoss()#定义交叉熵损失函数
optimizer = optim.Adam(net.parameters(), lr=0.001)#使用Adam优化器
###############################################开始训练########################################################
for epoch in range(5): #整个训练集训练的次数
running_loss = 0.0#累加训练中的损失
for step, data in enumerate(train_loader, start=0):#遍历训练集样本,不仅能返回这一批数据,而且还能返回相对应的索引
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data#得到数据之后,分离出输入的图像以及所对应的标签
# zero the parameter gradients
optimizer.zero_grad()#如果不清除历史梯度,就会对计算过的历史梯度进行累加
# forward + backward + optimize
outputs = net(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if step % 500 == 499: # print every 500 mini-batches
with torch.no_grad():#在测试或者预测过程中都加上这句话!!!!!
outputs = net(val_image) # [batch, 10]
predict_y = torch.max(outputs, dim=1)[1]
# dim=1是在输出的10个节点中寻找最大值,[1]是知道最大值所在的索引,不需要知道最大值是多少
accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)
#torch.eq(predict_y, val_label)预测标签类别和真实的标签类别进行比较,相同为true,不同为false
#torch.eq(predict_y, val_label).sum()然后再求和,代表在本次测试中预测对多少样本,数据为tensor格式
#torch.eq(predict_y, val_label).sum().item()通过 .item 转换成标量数值
print('[%d, %5d] train_loss: %.3f test_accuracy: %.3f' %
(epoch + 1, step + 1, running_loss / 500, accuracy))
running_loss = 0.0
print('Finished Training')
save_path = './Lenet.pth'#模型的保存路径
torch.save(net.state_dict(), save_path)#模型的所有数据,保存在设置路径下。
if __name__ == '__main__':
main()
预测数据
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
def main():
transform = transforms.Compose(
[transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
net = LeNet()
net.load_state_dict(torch.load('Lenet.pth'))#载入保存的权重文件
im = Image.open('1.jpg')
im = transform(im) # [C, H, W]
im = torch.unsqueeze(im, dim=0) # [N, C, H, W]
with torch.no_grad():
outputs = net(im)
predict = torch.max(outputs, dim=1)[1].data.numpy()
print(classes[int(predict)])
if __name__ == '__main__':
main()