一 LeNet
Lenet 是一系列网络的合称,包括 Lenet1 - Lenet5,由 Yann LeCun 等人在 1990 年《Handwritten Digit Recognition with a Back-Propagation Network》中提出,是卷积神经网络的 HelloWorld。
LeNet-5是一个较简单的卷积神经网络。下图显示了其结构:输入的二维图像,先经过两次卷积层到池化层,再经过全连接层,最后使用softmax分类作为输出层。
LeNet-5 这个网络虽然很小,但是它包含了深度学习的基本模块:卷积层,池化层,全连接层。是其他深度学习模型的基础, 这里我们对LeNet-5进行深入分析。同时,通过实例分析,加深对与卷积层和池化层的理解。
LeNet-5共有7层,不包含输入,每层都包含可训练参数;每个层有多个Feature Map,每个FeatureMap通过一种卷积滤波器提取输入的一种特征,然后每个FeatureMap有多个神经元。
各层参数详解:
1、INPUT层-输入层
首先是数据 INPUT 层,输入图像的尺寸统一归一化为32*32。
注意:本层不算LeNet-5的网络结构,传统上,不将输入层视为网络层次结构之一。
2、C1层-卷积层
输入图片:32*32
卷积核大小:5*5
卷积核种类:6
输出featuremap大小:28*28 (32-5+1)=28
神经元数量:28286
可训练参数:(55+1) * 6(每个滤波器55=25个unit参数和一个bias参数,一共6个滤波器)
连接数:(55+1)62828=122304
**详细说明:**对输入图像进行第一次卷积运算(使用 6 个大小为 55 的卷积核),得到6个C1特征图(6个大小为2828的 feature maps, 32-5+1=28)。我们再来看看需要多少个参数,卷积核的大小为55,总共就有6(55+1)=156个参数,其中+1是表示一个核有一个bias。对于卷积层C1,C1内的每个像素都与输入图像中的55个像素和1个bias有连接,所以总共有1562828=122304个连接(connection)。有122304个连接,但是我们只需要学习156个参数,主要是通过权值共享实现的。
3、S2层-池化层(下采样层)
输入:28*28
采样区域:2*2
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid
采样种类:6
输出featureMap大小:14*14(28/2)
神经元数量:14146
可训练参数:2*6(和的权+偏置)
连接数:(22+1)61414
S2中每个特征图的大小是C1中特征图大小的1/4。
**详细说明:**第一次卷积之后紧接着就是池化运算,使用 22核 进行池化,于是得到了S2,6个1414的 特征图(28/2=14)。S2这个pooling层是对C1中的2*2区域内的像素求和乘以一个权值系数再加上一个偏置,然后将这个结果再做一次映射。于是每个池化核有两个训练参数,所以共有2x6=12个训练参数,但是有5x14x14x6=5880个连接。
4、C3层-卷积层
输入:S2中所有6个或者几个特征map组合
卷积核大小:5*5
卷积核种类:16
输出featureMap大小:10*10 (14-5+1)=10
C3中的每个特征map是连接到S2中的所有6个或者几个特征map的,表示本层的特征map是上一层提取到的特征map的不同组合。
存在的一个方式是:C3的前6个特征图以S2中3个相邻的特征图子集为输入。接下来6个特征图以S2中4个相邻特征图子集为输入。然后的3个以不相邻的4个特征图子集为输入。最后一个将S2中所有特征图为输入。则:可训练参数:6*(355+1)+6*(455+1)+3*(455+1)+1*(655+1)=1516
连接数:10101516=151600
**详细说明:**第一次池化之后是第二次卷积,第二次卷积的输出是C3,16个10x10的特征图,卷积核大小是 55. 我们知道S2 有6个 1414 的特征图,怎么从6 个特征图得到 16个特征图了? 这里是通过对S2 的特征图特殊组合计算得到的16个特征图。具体如下:
C3的前6个feature map(对应上图第一个红框的6列)与S2层相连的3个feature map相连接(上图第一个红框),后面6个feature map与S2层相连的4个feature map相连接(上图第二个红框),后面3个feature map与S2层部分不相连的4个feature map相连接,最后一个与S2层的所有feature map相连。卷积核大小依然为55,所以总共有6(355+1)+6*(455+1)+3*(455+1)+1*(655+1)=1516个参数。而图像大小为10*10,所以共有151600个连接。
C3与S2中前3个图相连的卷积结构如下图所示:
上图对应的参数为 355+1,一共进行6次卷积得到6个特征图,所以有6*(355+1)参数。 为什么采用上述这样的组合了?论文中说有两个原因:1)减少参数,2)这种不对称的组合连接的方式有利于提取多种组合特征。
5、S4层-池化层(下采样层)
输入:10*10
采样区域:2*2
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid
采样种类:16
输出featureMap大小:5*5(10/2)
神经元数量:5516=400
可训练参数:2*16=32(和的权+偏置)
连接数:16*(2*2+1)55=2000
S4中每个特征图的大小是C3中特征图大小的1/4
**详细说明:**S4是pooling层,窗口大小仍然是2*2,共计16个feature map,C3层的16个10x10的图分别进行以2x2为单位的池化得到16个5x5的特征图。这一层有2x16共32个训练参数,5x5x5x16=2000个连接。连接的方式与S2层类似。
6、C5层-卷积层
输入:S4层的全部16个单元特征map(与s4全相连)
卷积核大小:5*5
卷积核种类:120
输出featureMap大小:1*1(5-5+1)
可训练参数/连接:120*(1655+1)=48120
**详细说明:**C5层是一个卷积层。由于S4层的16个图的大小为5x5,与卷积核的大小相同,所以卷积后形成的图的大小为1x1。这里形成120个卷积结果。每个都与上一层的16个图相连。
所以共有(5x5x16+1)x120 = 48120个参数,同样有48120个连接。C5层的网络结构如下:
7、F6层-[全连接层]
输入:c5 120维向量
计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过sigmoid函数输出。
可训练参数:84*(120+1)=10164
**详细说明:**6层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164。ASCII编码图如下:
F6层的连接方式如下:
8、Output层-全连接层
Output层也是全连接层,共有10个节点,分别代表数字0到9,且如果节点i的值为0,则网络识别的结果是数字i。采用的是径向基函数(RBF)的网络连接方式。假设x是上一层的输入,y是RBF的输出,则RBF输出的计算方式是:
上式w_ij 的值由i的比特图编码确定,i从0到9,j取值从0到7*12-1。RBF输出的值越接近于0,则越接近于i,即越接近于i的ASCII编码图,表示当前网络输入的识别结果是字符i。该层有84x10=840个参数和连接。
上图是LeNet-5识别数字3的过程。
总结
- LeNet-5是一种用于手写体字符识别的非常高效的卷积神经网络。
- 卷积神经网络能够很好的利用图像的结构信息。
- 卷积层的参数较少,这也是由卷积层的主要特性即局部连接和共享权重所决定。
import torch from torch import nn import torch.nn.functional as F # input:32*32*1 # Conv1:Kernel_size=5*5,Channel=16,Stride=1,padding=0 # Pooling1:Kernel_size=2*2,Stride=2 # Conv2:kernel_size=5*5,Channel=32,Stride=1,padding=0 # Pooling2:kernel_Size=2*2,stride=2 # Fc1:input=32*10*10 ouput=120 # Fc2:input=120 ouput=84 # Dc3:input=84 output=10 # Dc4:input=84 output=2 class MyLeNet(nn.Module): def __init__(self): super(MyLeNet, self).__init__() self.conv1 = nn.Conv2d(3, 16, 5) self.pool1 = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, 5) self.pool2 = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(32 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) self.fc4 = nn.Linear(10, 2) def forward(self, x): x = F.relu(self.conv1(x)) # input[3,32,32] 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] 展平后节点的个数 x = F.relu(self.fc1(x)) # output[120] x = F.relu(self.fc2(x)) # output[84] x = self.fc3(x) # output[10] x = self.fc4(x) # output[2] return x if __name__ == '__main__': x = torch.rand([1, 3, 32, 32]) model = MyLeNet() y = model(x)
训练代码
import torch from torch import nn from net import MyLeNet from torch.optim import lr_scheduler import os from torchvision import transforms from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader import matplotlib.pyplot as plt # 解决中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' ROOT_TRAIN = r'D:/other/ClassicalModel/data/CatAndDog/train' ROOT_TEST = r'D:/other/ClassicalModel/data/CatAndDog/val' # 将图像的像素值归一化到【-1, 1】之间 normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) train_transform = transforms.Compose([ transforms.Resize((32, 32)),#重调尺寸 transforms.RandomVerticalFlip(),#随即裁剪 transforms.ToTensor(), normalize]) val_transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), normalize]) train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform) val_dataset = ImageFolder(ROOT_TEST, transform=val_transform) train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True) val_dataloader = DataLoader(val_dataset, batch_size=32, shuffle=True) # 使用GPU加速 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model = MyLeNet().to(device) # 定义一个损失函数 loss_fn = nn.CrossEntropyLoss() # 定义一个优化器 optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 学习率每隔10轮变为原来的0.5 lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5) # 定义训练函数 def train(dataloader, model, loss_fn, optimizer): loss, current, n = 0.0, 0.0, 0 for batch, (x, y) in enumerate(dataloader): image, y = x.to(device), y.to(device) output = model(image) cur_loss = loss_fn(output, y) _, pred = torch.max(output, axis=1) cur_acc = torch.sum(y==pred) / output.shape[0] # 反向传播 optimizer.zero_grad() cur_loss.backward() optimizer.step() loss += cur_loss.item() current += cur_acc.item() n = n+1 train_loss = loss / n train_acc = current / n print('train_loss' + str(train_loss)) print('train_acc' + str(train_acc)) return train_loss, train_acc # 定义一个验证函数 def val(dataloader, model, loss_fn): # 将模型转化为验证模型 model.eval() loss, current, n = 0.0, 0.0, 0 with torch.no_grad(): for batch, (x, y) in enumerate(dataloader): image, y = x.to(device), y.to(device) output = model(image) cur_loss = loss_fn(output, y) _, pred = torch.max(output, axis=1) cur_acc = torch.sum(y == pred) / output.shape[0] loss += cur_loss.item() current += cur_acc.item() n = n + 1 val_loss = loss / n val_acc = current / n print('val_loss' + str(val_loss)) print('val_acc' + str(val_acc)) return val_loss, val_acc # # 定义画图函数 # def matplot_loss(train_loss, val_loss): # plt.plot(train_loss, label='train_loss') # plt.plot(val_loss, label='val_loss') # plt.legend(loc='best') # plt.ylabel('loss') # plt.xlabel('epoch') # plt.title("训练集和验证集loss值对比图") # plt.show() # # def matplot_acc(train_acc, val_acc): # plt.plot(train_acc, label='train_acc') # plt.plot(val_acc, label='val_acc') # plt.legend(loc='best') # plt.ylabel('acc') # plt.xlabel('epoch') # plt.title("训练集和验证集acc值对比图") # plt.show() # 开始训练 loss_train = [] acc_train = [] loss_val = [] acc_val = [] epochs = 20 min_acc = 0 for epoch in range(epochs): lr_scheduler.step() print(f"epoch{epoch+1}\n-----------") train_loss, train_acc = train(train_dataloader, model, loss_fn, optimizer) val_loss, val_acc = val(val_dataloader, model, loss_fn) loss_train.append(train_loss) acc_train.append(train_acc) loss_val.append(val_loss) acc_val.append(val_acc) # 保存最好的模型权重 if val_acc >min_acc: folder = 'save_model' if not os.path.exists(folder): os.mkdir('save_model') min_acc = val_acc print(f"save best model, 第{epoch+1}轮") torch.save(model.state_dict(), 'save_model/best_model.pth') # 保存最后一轮的权重文件 if epoch == epoch-1: torch.save(model.state_dict(), 'save_model/last_model.pth') # matplot_loss(loss_train, loss_val) # matplot_acc(acc_train, acc_val) print('Done!')
验证代码
import torch from net import MyLeNet from torch.autograd import Variable from torchvision import datasets, transforms from torchvision.transforms import ToTensor from torchvision.transforms import ToPILImage from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader ROOT_TRAIN = r'D:/other/ClassicalModel/AlexNet/data/train' ROOT_TEST = r'D:/other/ClassicalModel/AlexNet/data/train' # 将图像的像素值归一化到【-1, 1】之间 normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) train_transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.RandomVerticalFlip(), transforms.ToTensor(), normalize]) val_transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), ]) train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform) val_dataset = ImageFolder(ROOT_TEST, transform=val_transform) train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True) val_dataloader = DataLoader(val_dataset, batch_size=32, shuffle=True) device = 'cuda' if torch.cuda.is_available() else 'cpu' model = MyLeNet().to(device) # 加载模型 model.load_state_dict(torch.load("D:/other/ClassicalModel/LeNet/save_model/best_model.pth")) # 获取预测结果 classes = [ "cat", "dog", ] # 把张量转化为照片格式 show = ToPILImage() # 进入到验证阶段 model.eval() for i in range(10): x, y = val_dataset[i][0], val_dataset[i][1] show(x).show() x = Variable(torch.unsqueeze(x, dim=0).float(), requires_grad=True).to(device) x = torch.tensor(x).to(device) with torch.no_grad(): pred = model(x) predicted, actual = classes[torch.argmax(pred[0])], classes[y] print(f'predicted:"{predicted}", Actual:"{actual}"')
特点:
1.相比MLP,LeNet使用了相对更少的参数,获得了更好的结果。
2.设计了maxpool来提取特征