前言:
各位读者好,这里是手写BP神经网络的第三个部分:总结。一定会有读者抱有一些疑惑:我已经通过Part2的上下的两个部分理解了甚至已经实现了模型,为什么还需要这一篇呢?至今为止读者掌握的是组成一台庞大的机器的所有的零件的制作方式,也同时掌握了制作这台机器的图纸。但是,这并不意味着读者可以依靠这些就加工出任何零件:可能不同品种的零件需要的是对于机器做一些细微的调整。如果说回BP神经网络就是:什么样的网络层才能收敛?不同的损失函数分别收敛的条件是什么?上限的准确率又是多少?这些问题都会在本章完成解析。这个部分作者做了比较完整的实验进行说明,也会在这一博文之中叙述出来。那么在开始本章之前,作者先贴上上一个Part的传送门。阅读本文之前建议先阅读上一组文章。
手写感知器的反向传播算法 Part2:基于理论推导的代码实践:如何实现自己的第一个神经网络完成手写体识别?(上)-CSDN博客
预备:
预备Part1:说明实验背景:
由于本文更像是一个实验,所以这里作者先介绍实验的背景。
1)数据集一共620*12张图片,作者选择了6000张作为训练集,1440张作为测试集。
2)如同上一篇文章中所介绍的那样,本实验网络结构等可以调整。
3)learning rate作者设定为1e-2(关于这个部分作者后续会专门说明)
4)可以改变的是:损失函数的类别(ReLU&Sigmoid)&网络的层数结构&不同的trick
预备Part2:怎么进行实验?
(1)为什么会有这个问题出现?
关于如何进行实验这个问题,读者可能也会有些疑问:修改网络结构,训练就行了呗。但是实际上,体验过的读者都会发现,用自己的训练极慢无比。究其原因是因为其不可能用上cuda加速。而有无cuda加速将带来整整10倍的速度差异。那么这里作者将给出在pytorch上怎么进行类似训练的代码。这个部分不仅教会了读者如何用pytorch完成这个任务(这也是机器学习的基本任务之一),还给了读者自己写代码时如何用pytorch验证自己的任务是否出现了问题的方法
(2)具体怎么做?(理论分析)
想要完成一个严格的实验,那么需要的就是能够严格进行对照。这也就意味着对于感知器的初始化权重两者必须一样。手写的BP神经网络的权重想要定为一个值是比较容易的:在前面的initialize函数中修改即可,这里主要给出的就是,怎么在一个成型的pytorch程序之中让每一层都使用给定的权重。那么下面作者就给出对应的代码
(3)具体怎么做?(代码分析)
1.手写BP神经网络中的initialize函数:
def initialize(self):
n1 = np.loadtxt("matrix1.txt", delimiter=',', dtype=float)
n2 = np.loadtxt("matrix2.txt", delimiter=',', dtype=float)
n3 = np.loadtxt("matrix3.txt", delimiter=',', dtype=float)
n = [n1,n2,n3]
b1 = np.loadtxt("bias1.txt", delimiter=',', dtype=float)
b2 = np.loadtxt("bias2.txt", delimiter=',', dtype=float)
b3 = np.loadtxt("bias3.txt", delimiter=',', dtype=float)
b = [b1,b2,b3]
for i in range(layer_num + 1):
l = self.neuron_sit[i]
w = self.neuron_sit[i + 1]
copy(self.weight_para[:, :, i], n[i])
copy(self.bias_para[:, i], b[i])
# 每一个层只有需要的参数才非0,,这样安排便于检查
这里的每一个txt文件就是保存的初始化的权重。
2.pytorch代码之中的关键部分:
model = Linear_model.linear_model()
n1 = np.loadtxt("matrix1.txt", delimiter=',', dtype=float)
n2 = np.loadtxt("matrix2.txt", delimiter=',', dtype=float)
n3 = np.loadtxt("matrix3.txt", delimiter=',', dtype=float)
b1 = np.loadtxt("bias1.txt", delimiter=',', dtype=float)
b2 = np.loadtxt("bias2.txt", delimiter=',', dtype=float)
b3 = np.loadtxt("bias3.txt", delimiter=',', dtype=float)
model.linear1.weight.data = torch.tensor(n1)
model.linear1.bias.data = torch.tensor(b1)
model.linear2.weight.data = torch.tensor(n2)
model.linear2.bias.data = torch.tensor(b2)
model.linear3.weight.data = torch.tensor(n3)
model.linear3.bias.data = torch.tensor(b3)
np.savetxt("matrix1.txt",np.array(model.linear1.weight.detach()),delimiter=',')
np.savetxt("matrix2.txt",np.array(model.linear2.weight.detach()),delimiter=',')
np.savetxt("matrix3.txt",np.array(model.linear3.weight.detach()),delimiter=',')
np.savetxt("bias1.txt",np.array(model.linear1.bias.detach()),delimiter=',')
np.savetxt("bias2.txt",np.array(model.linear2.bias.detach()),delimiter=',')
np.savetxt("bias3.txt",np.array(model.linear3.bias.detach()),delimiter=',')
这其中np.loadtxt的效果是使用一个权重供手写的BP神经网络使用;np.savetxt保存了模型中的参数矩阵到本地供下次读取使用。关于完整的pytorch代码,作者会在附录中给出。
至此,开始实验:
正式实验:
1.可能的影响因素:
(1)激活函数选取:sigmoid函数和ReLU函数
(2)网络层数&神经元个数
(3)一些其余的trick:是否有kaiming normalization?是否有对于图片的标准化操作?
2.实验过程:
2.1:实验参数:神经元个数:784,500,250,12;激活函数:sigmoid函数
经调用torch和本机的GPU加速后作者尝试发现,100个epoch无法收敛。运行结果如下:
这也就是说,两层的感知机对于这个图片是完全无法收敛的。
2.2:加上了Kaiming正则化:
既然sigmoid函数根本无法收敛,那么是否使用一些别的trick提升一下正确率会好一些呢?于是作者使用了一个机制:Kaiming Normalization。效果如下:
可以看出来,略微好了一些,但是仍然不是很令人满意。
2.3修改损失函数:
作者决定使用ReLU损失函数进行激活,结果发现,效果变得相当显著。
从此,作者已经成功地完成了一个实验。
2.4:对照实验:到底哪个因素对于这个模型的收敛起到的作用最大?
分析一:对于这个实验,Sigmoid函数的表现是远远不够理想的。只有15左右的准确率,也就是说仅仅略微高于随机猜测。因此后续的实验暂时不测试sigmoid函数了。
分析二:那么需要考虑的要素变成了以下的两个因素:
1.Kaiming Normalization
2.transforms中的图像预处理,也就是transforms.normalize(mean = 0.5,std=0.5)
下面使用控制变量法完成实验:
从这一组实验成果中可以看出,Kaiming Normalization起到的作用是较小的。但是使用了也可以使得正确率上升10%。但是transforms.normalize一定是必须的。如果没有了这个操作模型的正确率会下降50%以上。
2.5:总结:
根据上面的结论,已经可以下结论了。下面一定要实现的是将sigmoid函数换成ReLU函数;以及完成Normalize的操作。
至此,实验部分完毕,但是仍然困扰作者的一点是:为什么?
3.分析实验背后的原理:
为什么sigmoid函数不能用呢?
因为sigmoid函数太容易出现梯度消失的情况了。一旦梯度传播,由于逐层相乘,对于两层的感知器第一层只能每次改变1e-7这个量级,实在不令人满意。那么是否只使用一层感知器就能解决问题呢?
一层感知器的效果还是有的,但是30%的正确率实在不够看,于是作者不采用这种思路。
4.正式实验的结论:
(1)如果使用sigmoid函数,那么网络最多一层。但是如果在这种情况之下sigmoid函数效果极差。
(2)ReLU函数可以适用于大多数情况,而且效果要远好于sigmoid函数。
(3)图片的normalization的效果是非常重要的,建议不论是什么实验都做一下。
(4)Kaiming norm起到的是一个添砖加瓦的作用,可以作为Bonus使用。
总结:
这个实验自然不是完成整项作业的必备内容。老师需求的是一个能跑出来的网络结构。因此只是为了完成作业前两篇博文已经足够完成了。但是本文是作者在自己完成作业的时候探究到的结果。这里更新出来供给很多其他的有可能和我面对一样问题的朋友提供帮助。作者这里也是有些奇怪的:因为作者自己在写这个作业的时候发现,助教老师也不知道我是什么问题。他只能告诉我,可能是我的网络结构出了问题,但是却并不能告诉我是因为我使用了两层隐层的结构,却使用了sigmoid函数作为激活函数。从此可以看出,实际上很多这样的问题仍然未被解决 。因此作者出了这一期博客来解答大家可能有的疑惑。
附录:pytorch代码:
1.train部分:
import numpy as np
import torch
import torchvision.datasets
from matplotlib import pyplot as plt
from torch import nn, optim
from torch.utils.data import DataLoader
import Linear_model
batch_size = 16
epochs = 30
# import Model
from torchvision import transforms
if torch.cuda.is_available(): # 如果有GPU就用,没有就用CPU
device = torch.device('cuda:0')
else:
device = torch.device('cpu')
transform_train = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
# 对每个通道的像素进行标准化,给出每个通道的均值和方差
])
# transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
train_dataset = torchvision.datasets.ImageFolder(root='train_2022',transform=transform_train)
val_dataset = torchvision.datasets.ImageFolder(root='val_2022',transform=transform_train)
# train_dataset = torchvision.datasets.ImageFolder(root='train',transform=transform_train)
# val_dataset = torchvision.datasets.ImageFolder(root='val',transform=transform_train)
# 确认数据集情形
train_num = len(train_dataset)
val_num = len(val_dataset)
print('train_num:', train_num, 'val_num:', val_num)
# 下面打印出不同的类别名对应的在模型中的序号
class_dict = train_dataset.class_to_idx
print(class_dict)
# 将类别名称保存在列表中
class_names = list(class_dict.keys())
print(class_names)
train_loader = DataLoader(dataset=train_dataset, # 接收训练集
batch_size=batch_size, # 训练时每个step处理16张图
shuffle=True, # 打乱每个batch
num_workers=0) # 加载数据时的线程数量,windows环境下只能=0
# 构造验证集
val_loader = DataLoader(dataset=val_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=0)
model = Linear_model.linear_model()
# n1 = np.loadtxt("matrix1.txt", delimiter=',', dtype=float)
# n2 = np.loadtxt("matrix2.txt", delimiter=',', dtype=float)
# n3 = np.loadtxt("matrix3.txt", delimiter=',', dtype=float)
# b1 = np.loadtxt("bias1.txt", delimiter=',', dtype=float)
# b2 = np.loadtxt("bias2.txt", delimiter=',', dtype=float)
# b3 = np.loadtxt("bias3.txt", delimiter=',', dtype=float)
# model.linear1.weight.data = torch.tensor(n1)
# model.linear1.bias.data = torch.tensor(b1)
# model.linear2.weight.data = torch.tensor(n2)
# model.linear2.bias.data = torch.tensor(b2)
# model.linear3.weight.data = torch.tensor(n3)
# model.linear3.bias.data = torch.tensor(b3)
# np.savetxt("matrix1.txt",np.array(model.linear1.weight.detach()),delimiter=',')
# np.savetxt("matrix2.txt",np.array(model.linear2.weight.detach()),delimiter=',')
# np.savetxt("matrix3.txt",np.array(model.linear3.weight.detach()),delimiter=',')
# np.savetxt("bias1.txt",np.array(model.linear1.bias.detach()),delimiter=',')
# np.savetxt("bias2.txt",np.array(model.linear2.bias.detach()),delimiter=',')
# np.savetxt("bias3.txt",np.array(model.linear3.bias.detach()),delimiter=',')
model = model.to(device)
loss_funtion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
def plot(acc, loss, mode='train', best_acc_=None):
plt.figure(figsize=(10, 4))
plt.suptitle('%s_curve' % mode)
plt.subplots_adjust(wspace=0.2, hspace=0.2)
epoch = len(acc)
plt.subplot(1, 2, 1)
plt.plot(np.arange(epoch), loss, label='loss')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(loc='upper left')
plt.subplot(1, 2, 2)
plt.plot(np.arange(epoch), acc, label='acc')
if best_acc_ is not None:
plt.scatter(best_acc_[0], best_acc_[1], c='r')
plt.xlabel('epoch')
plt.ylabel('acc')
plt.legend(loc='upper left')
plt.savefig('my_own_model_deep_%s.jpg' % mode, bbox_inches='tight')
plt.show()
# 5.正式开始运行模型
# 保存最好的一次计算结果
best_acc = 0.0
loss_ultra = np.zeros(epochs, dtype=float)
# 计算每一个epoch的loss值形成一个数组
acc_ultra = np.zeros(epochs, dtype=float)
# 计算每一个epoch的accuracy形成一个数组
best_acc_ = np.zeros((2, 1), dtype=float)
# 下面正式开始对于网络的训练
for epoch in range(epochs):
print('-' * 30, '\n', 'epoch:', epoch)
# 将模型设置为训练模型, dropout层和BN层只在训练时起作用
model.train()
# 计算训练一个epoch的总损失
running_loss = 0.0
step_num = 0
step = loss = 0
# train_loader的最大的好处就是自动的分成了batch。在这个基础之上,只要用enumerate进行遍历,那么就可以轻易的每一个batch的标注类别和图片对应的数字矩阵。
for step, data in enumerate(train_loader):
# 从遍历的结果中读出图片和label,并且这里将其挂上cuda
image,label = data
image = image.to(device)
label = label.to(device)
# 清理一次梯度防止影响后续
image = image[:,0,:,:].reshape(image.shape[0],-1)
optimizer.zero_grad()
result = model(image)
loss = loss_funtion(result,label)
loss.backward()
optimizer.step()
# 统计总的训练的loss
running_loss += loss.item()
step_num = step
print(f'step:{step} loss:{loss}')
# 5.2:验证
model.eval()
acc = 0
with torch.no_grad():
for data in val_loader:
val_image, val_label = data
val_image = val_image.to(device)
val_label = val_label.to(device)
val_image = val_image[:, 0, :, :].reshape(val_image.shape[0], -1)
outputs = model(val_image)
predict_y = torch.max(outputs, dim=1)[1]
mid_res = predict_y-val_label
for i in mid_res:
if i == 0:
acc +=1
acc_test = acc / val_num
loss_test = running_loss / step_num
loss_ultra[epoch] = loss_test
acc_ultra[epoch] = acc_test
print(f'total_train_loss:{loss_test}, total_test_acc:{acc_test}')
# 6.保存模型
if acc_test > best_acc:
best_acc = acc_test
best_acc_[0,0] = epoch
best_acc_[1,0] = acc_test
print(f"the best result is altered to {best_acc}")
torch.save(model.state_dict(), 'my_model_linear.pth')
plot(acc_ultra, loss_ultra, mode='test', best_acc_=best_acc_)
2.Model部分:
from torch import nn
import torch
class linear_model(nn.Module):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.linear1 = nn.Linear(64*64,500)
# self.linear1 = nn.Linear(784, 5000)
# self.linear5 = nn.Linear(5000,1000)
# self.linear4 = nn.Linear(1000,500)
self.linear2 = nn.Linear(500,250)
self.linear3 = nn.Linear(250,10)
# self.sigmoid = nn.Sigmoid()
self.sigmoid = nn.ReLU()
# for m in self.modules():
# if isinstance(m, nn.Linear):
# # isinstance函数是判断是否一样,这里的意思是对于这个神经网络的结构,若抽出的层是conv2d那么就对其中的参数进行凯明初始化
# nn.init.kaiming_normal_(m.weight, mode='fan_out')
def forward(self,x):
# x = x.to(torch.float64)
x = self.linear1(x)
x = self.sigmoid(x)
# x = self.linear5(x)
# x = self.sigmoid(x)
# x = self.linear4(x)
# x = self.sigmoid(x)
x = self.linear2(x)
x = self.sigmoid(x)
x = self.linear3(x)
return x