LeNet由Yann Lecun 创建,属于提出最早的一类神经网络,有着良好的学习和识别能力。LeNet-5,具有一个输入层,两个卷积层,两个池化层,3个全连接层(其中最后一个全连接层为输出层)。LeNet网络结构图如下:
第一层卷积层 | 输入是32 * 32 * 1的原始图像矩阵。使用6个大小为5 * 5的卷积核,步长为1,对输入层进行卷积运算,不进行填充操作;输出特征图尺寸为32-5+1=28,因此产生6个大小为28*28的特征矩阵。这么做够防止原图像输入的信息掉到卷积核边界之外 |
---|---|
第二层池化层 | 也称为下采样层,输入为上层输出,28 * 28 * 6的特征矩阵, 池化核采用2 * 2,步长大小为2,经池化后得到6个14*14的特征矩阵,作为下一层神经元的输入 |
第三层卷积层 | 输入是14 *14 * 6的特征矩阵。使用16个大小为5 * 5的卷积核,步长为1,对输入层进行卷积运算,不进行填充操作;输出特征图尺寸为14-5+1=10,因此产生16个大小为10 *10的特征矩阵 |
第四层池化层 | 池化方式与第二层池化相同, 池化核采用2 * 2,步长大小为2,经池化后得到16个5 * 5的特征矩阵 |
第五层全连接层 | 在论文中称为卷积层,继续用5 * 5的卷积核对上层的输出进行卷积,卷积核数量增加至120。这样C5层的输出图片大小为5-5+1=1。最终输出120个1*1的特征图。这里实际上是与S4全连接了,但仍将其标为卷积层,原因是如果LeNet-5的输入图片尺寸变大,其他保持不变,那该层特征图的维数也会大于1 * 1 |
第六层全连接层 | 第六层有84个单元(之所以选这个数字的原因来自于输出层的设计),与C5层全相连。有10164个可训练参数。如同经典神经网络,第六层层计算输入向量和权重向量之间的点积,再加上一个偏置。然后将其传递给sigmoid函数产生单元i的一个状态。最后,输出层由欧式径向基函数(Euclidean Radial Basis Function)单元组成,每类一个单元,每个有84个输入 |
第七层输出层 | 包含10个神经元,用于手写数字集的输出 |
网络搭建简介:
第一层卷积层 | 第一层卷积层采用1633的卷积核,步长为1,填充也为1,激活函数采用 ReLU激活函数 |
---|---|
第二层池化层 | 采用平均池化的池化方式,池化核为2*2,步长为2 |
第三层卷积层 | 32个3*3的卷积核,步长为1,填充也为1,激活函数采用 ReLU激活函数 |
第四层池化层 | 采用平均池化的池化方式,池化核为2*2,步长为2 |
第五层全连接层 | 256个神经节点,激活函数采用 ReLU激活函数 |
第六层全连接 | 128个神经节点,激活函数采用 ReLU激活函数 |
第七层输出层 | 输出层,10个神经节点,激活函数采用 ReLU激活函数 |
通过nnnn.Sequential(),nn.Conv2d(),nn.ReLU(),nn.AvgPool2d()等函数定义了一个拥有两个卷积层和三个全连接层的神经网络分类器,并在forward()函数定义前传播过程,用myconvnet = MyConvNet()得到搭建的网络模型。
实现代码:
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__() # 对继承自父类Module的属性进行初始化
# 定义第一个卷积层,16个3*3的卷积核,池化层为平均池化
self.conv1 = nn.Sequential(nn.Conv2d(in_channels=1, # 输入图像的通道数
out_channels=16, # 卷积核的数量
kernel_size=3, # 卷积核的大小
stride=1, # 步长
padding=1, # 填充的数量
), # 经过卷积后的尺寸变化:(1*28*28) -> (16*28*28)
nn.ReLU(), # ReLU激活函数
nn.AvgPool2d(kernel_size=2, # 池化窗口的大小
stride=2, # 步长
), # 经过池化后的尺寸变化:(16*28*28) -> (16*14*14)
)
# 定义第二个卷积层,32个3*3的卷积核,池化层为平均池化
self.conv2 = nn.Sequential(nn.Conv2d(in_channels=16, # 输入图像的通道数
out_channels=32, # 卷积核的数量
kernel_size=3, # 卷积核的大小
stride=1, # 步长
padding=0, # 填充的数量
), # 经过卷积后的尺寸变化:(16*14*14) -> (32*12*12)
nn.ReLU(), # ReLU激活函数
nn.AvgPool2d(kernel_size=2, # 池化窗口的大小
stride=2, # 步长
), # 经过池化后的尺寸变化:(32*12*12) -> (32*6*6)
)
# 定义全连接层
self.classifier = nn.Sequential(nn.Linear(32*6*6, 256), # 全连接层的输入为32*6*6=1152,输出为256
nn.ReLU(),
nn.Linear(256, 128), # 全连接层的输入为256,输出为128
nn.ReLU(),
nn.Linear(128, 10) # 全连接层的输入为128,输出为10
)
# 定义网络结构的前向传播路径
def forward(self, x):
x = self.conv1(x) # 将数据集x输入给第一个卷积层
x = self.conv2(x) # 将第一个卷积层的输出给到第二个卷积层
x = x.view(x.size(0), -1) # 将第二个卷积层的输出展开成一维张量x
# 四维张量x对应的维度为(batch_size,channels,x,y),其中x.size(0)对应batch_size
output = self.classifier(x) # 将展开的一维张量x给到全连接层和分类器
return output
Fashion-MNIST数据集包含了10个类别的图像,分别是:t-shirt(T恤),trouser(牛仔裤),pullover(套衫),dress(裙子),coat(外套),sandal(凉鞋),shirt(衬衫),sneaker(运动鞋),bag(包),ankle boot(短靴)。
数据处理简介:分为训练数据和测试数据;首先使用FashionMNIST数据集,准备训练数据集,导入了训练数据集,然后使用Data. DataLoader()函数将其定义为数据加载器,每个batch中会包含64个样本,需要注意的是参数shuffle=False,表示加载器中每个batch使用的样本都是固定的,这样有利于在训练模型时根据迭代的次数将其切分为训练集和验证集:封装后便于后面训练模型使用。
实现代码:
训练数据准备:
def train_data_process():
# 加载FashionMNIST数据集
train_data = FashionMNIST(root="D:\深度学习数据 勿删\FashionMNIST", # 数据路径
train=True, # 只使用训练数据集
transform=transforms.ToTensor(), # 把PIL.Image或者numpy.array数据类型转变为torch.FloatTensor类型
# 尺寸为Channel * Height * Width,数值范围缩小为[0.0, 1.0]
download=False, # 若本身没有下载相应的数据集,则选择True
)
train_loader = Data.DataLoader(dataset=train_data, # 传入的数据集
batch_size=64, # 每个Batch中含有的样本数量
shuffle=False, # 不对数据集重新排序
num_workers=2, # 加载数据所开启的进程数量
)
print("The number of batch in train_loader:", len(train_loader)) # 一共有938个batch,每个batch含有64个训练样本
# 获得一个Batch的数据
for step, (b_x, b_y) in enumerate(train_loader):
if step > 0:
break
batch_x = b_x.squeeze().numpy() # 将四维张量移除第1维,并转换成Numpy数组
batch_y = b_y.numpy() # 将张量转换成Numpy数组
class_label = train_data.classes # 训练集的标签
class_label[0] = "T-shirt"
# 可视化一个Batch的图像
plt.figure(figsize=(12, 5))
for ii in np.arange(len(batch_y)):
plt.subplot(4, 16, ii+1)
plt.imshow(batch_x[ii, :, :], cmap=plt.cm.gray)
plt.title(class_label[batch_y[ii]], size=9)
plt.axis("off")
plt.subplots_adjust(wspace=0.05)
plt.show()
return train_loader, class_label
def test_data_process():
test_data = FashionMNIST(root="./data/FashionMNIST", # 数据路径
train=False, # 不使用训练数据集
download=False, # 如果前面数据已经下载,这里不再需要重复下载
)
test_data_x = test_data.data.type(torch.FloatTensor) / 255.0 # 将数值范围缩小为[0.0, 1.0]
test_data_x = torch.unsqueeze(test_data_x, dim=1) # 为测试数据test_data_x添加一个维度,即通道数
test_data_y = test_data.targets # 测试集的标签
print("test_data_x.shape:", test_data_x.shape)
print("test_data_y.shape:", test_data_y.shape)
return test_data_x, test_data_y
定义网络训练方式
train_ model0函 数通过train_ batch_ num确定用于训练的batch数量,并且在每轮的迭代中,如果step < train batch_ num,则进入训练模式,否则进人验证模式。在模型的训练和验证过程中,分别输出当前的损失函数的大小和对应的识别精度,并将它们保存在列表汇总中,最后组成数据表格train_ process输出。 为了保存模型最高精度下的训练参数,使用copy deepcopy()函数将模型最优的参数保存在best_ model wts中,最终将所有的训练结果使用model.load_ state_ dict(best model_wts)将最优的参数赋值给最终的模型。
def train_model(model, traindataloader, train_rate, criterion, optimizer, num_epochs=25):
'''
:param model: 网络模型
:param traindataloader: 训练数据集,会切分为训练集和验证集
:param train_rate: 训练集batch_size的百分比
:param criterion: 损失函数
:param optimizer: 优化方法
:param num_epochs: 训练的轮数
'''
batch_num = len(traindataloader) # batch数量
train_batch_num = round(batch_num * train_rate) # 将80%的batch用于训练,round()函数四舍五入
best_model_wts = copy.deepcopy(model.state_dict()) # 复制当前模型的参数
# 初始化参数
best_acc = 0.0 # 最高准确度
train_loss_all = [] # 训练集损失函数列表
train_acc_all = [] # 训练集准确度列表
val_loss_all = [] # 验证集损失函数列表
val_acc_all = [] # 验证集准确度列表
since = time.time() # 当前时间
# 进行迭代训练模型
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# 初始化参数
train_loss = 0.0 # 训练集损失函数
train_corrects = 0 # 训练集准确度
train_num = 0 # 训练集样本数量
val_loss = 0.0 # 验证集损失函数
val_corrects = 0 # 验证集准确度
val_num = 0 # 验证集样本数量
# 对每一个mini-batch进行训练和计算
for step, (b_x, b_y) in enumerate(traindataloader):
if step < train_batch_num: # 使用数据集的80%用于训练
model.train() # 设置模型为训练模式,启用Batch Normalization和Dropout
output = model(b_x) # 前向传播过程,输入为一个batch,输出为一个batch中对应的预测
pre_lab = torch.argmax(output, 1) # 查找每一行中最大值对应的行标
loss = criterion(output, b_y) # 计算每一个batch的损失函数
optimizer.zero_grad() # 将梯度初始化为0
loss.backward() # 反向传播计算
optimizer.step() # 根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值的作用
train_loss += loss.item() * b_x.size(0) # 对损失函数进行累加
train_corrects += torch.sum(pre_lab == b_y.data) # 如果预测正确,则准确度train_corrects加1
train_num += b_x.size(0) # 当前用于训练的样本数量
else: # 使用数据集的20%用于验证
model.eval() # 设置模型为评估模式,不启用Batch Normalization和Dropout
output = model(b_x) # 前向传播过程,输入为一个batch,输出为一个batch中对应的预测
pre_lab = torch.argmax(output, 1) # 查找每一行中最大值对应的行标
loss = criterion(output, b_y) # 计算每一个batch中64个样本的平均损失函数
val_loss += loss.item() * b_x.size(0) # 将验证集中每一个batch的损失函数进行累加
val_corrects += torch.sum(pre_lab == b_y.data) # 如果预测正确,则准确度val_corrects加1
val_num += b_x.size(0) # 当前用于验证的样本数量
# 计算并保存每一次迭代的成本函数和准确率
train_loss_all.append(train_loss / train_num) # 计算并保存训练集的成本函数
train_acc_all.append(train_corrects.double().item() / train_num) # 计算并保存训练集的准确率
val_loss_all.append(val_loss / val_num) # 计算并保存验证集的成本函数
val_acc_all.append(val_corrects.double().item() / val_num) # 计算并保存验证集的准确率
print('{} Train Loss: {:.4f} Train Acc: {:.4f}'.format(epoch, train_loss_all[-1], train_acc_all[-1]))
print('{} Val Loss: {:.4f} Val Acc: {:.4f}'.format(epoch, val_loss_all[-1], val_acc_all[-1]))
# 寻找最高准确度
if val_acc_all[-1] > best_acc:
best_acc = val_acc_all[-1] # 保存当前的最高准确度
best_model_wts = copy.deepcopy(model.state_dict()) # 保存当前最高准确度下的模型参数
time_use = time.time() - since # 计算耗费时间
print("Train and val complete in {:.0f}m {:.0f}s".format(time_use // 60, time_use % 60))
# 选择最优参数
model.load_state_dict(best_model_wts) # 加载最高准确度下的模型参数
train_process = pd.DataFrame(data={"epoch": range(num_epochs),
"train_loss_all": train_loss_all,
"val_loss_all": val_loss_all,
"train_acc_all": train_acc_all,
"val_acc_all": val_acc_all}
) # 将每一代的损失函数和准确度保存为DataFrame格式
# 显示每一次迭代后的训练集和验证集的损失函数和准确率
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")
plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")
plt.legend()
plt.xlabel("epoch")
plt.ylabel("Loss")
plt.subplot(1, 2, 2)
plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")
plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
plt.xlabel("epoch")
plt.ylabel("acc")
plt.legend()
plt.show()
return model, train_process
训练和测试网络模型
def train_model_process(myconvnet):
optimizer = torch.optim.Adam(myconvnet.parameters(), lr=0.0003) # 使用Adam优化器,学习率为0.0003
criterion = nn.CrossEntropyLoss() # 损失函数为交叉熵函数
train_loader, class_label = train_data_process() # 加载训练集
test_data_x, test_data_y = test_data_process() # 加载测试集
myconvnet, train_process = train_model(myconvnet, train_loader, 0.8, criterion, optimizer, num_epochs=25) # 进行模型训练
# 对测试集进行预测
myconvnet.eval() # 设置模型为评估模式,不启用Batch Normalization和Dropout
output = myconvnet(test_data_x) # 前向传播过程,输入为测试数据集,输出为对每个样本的预测
pre_lab = torch.argmax(output, 1) # 查找每一行中最大值对应的行标
acc = accuracy_score(test_data_y, pre_lab) # 计算分类准确率
print("val_acc:", acc)
# 计算混淆矩阵并可视化
conf_mat = confusion_matrix(test_data_y, pre_lab)
df_cm = pd.DataFrame(conf_mat, index=class_label, columns=class_label)
heatmap = sns.heatmap(df_cm, annot=True, fmt="d", cmap="YlGnBu")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right')
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right')
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
训练过程可视化过程
使用混淆矩阵表示测试样本的预测结果,便于观察由矩阵图可以看出,容易出错的是T-shirt
mian
if __name__ == '__main__':
convnet = ConvNet()
train_model_process(convnet)