目录
一、选题
源 码 免 费 下 载 链 接 如 下:
Python课程设计基于卷积神经网络的手写数字识别系统源码.zip-Python文档类资源-CSDN下载Python课程设计基于卷积神经网络的手写数字识别系统源码.zipPython课程设计基于卷积神经网更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/chengxuyuanlaow/86762748利用numpy完成手写数字数据集的识别,完成多分类问题,搭建神经网络,并且完成模型的训练以及性能评估,可视化数据
二、用到的知识
- sklearn 数据集的提取分割
- yaml配置文件使用
- numpy实现各个神经层
- 参数初值选择
- 梯度下降方法选择
- sklearn 分类模型评估
- matplotlib数据可视化
- 设计模式
- Markdown写报告
- 防脱发技术
代码框架如下所示:
. ├── save_params.pkl 训练好的模型 ├── params.yaml 神经网络搭建参数配置 ├── tools.py 卷积和池化过程用到的转换 ├── im2col 将图像卷积转化为矩阵相乘的形式 └── col2im 反向传播将矩阵相乘形式转化为原图像 ├── optimizer.py 参数更新方法 ├── OptimizerBase类 参数更新方法基类,必须重写覆盖虚函数才能构造类 └── update 对参数进行更新,子类的update同作用 ├── AdaGrad类 AdaGrad方法 └── SGD类 随机梯度下降法 ├── network.py 主程序,将所有其它模块连接起来 └── NeuralNetwork类 神经网络总类 ├── test 模型测试 ├── loadAndTest 加载已经训练好的模型然后测试 ├── runNetWork 训练模型 ├── predict 所有网络层前向传播 ├── update 所有网络层反向传播并更新参数 ├── getParams 获取yaml配置文件参数 ├── saveParams 保存训练好的参数模型 ├── getData 获取训练集和测试集 ├── getLayer 根据配置文件生成神经网络 └── getTrainData 随机抽取训练数据 ├── method.py 参数初始化方法 ├── ParamsInitBase类 参数初始化基类 ├── getParams 获取卷积层初始化参数,子类同 └── getAffineParams 获取Affine层初始化参数,子类同 ├── He 类 He方法初始化参数 └── Xavier类 Xavier方法初始化参数 ├── layer.py 神经网络层 ├── LayerBase类 神经网络层基类 ├── forward 前向传播,子类同 ├── backward 反向传播,子类同 ├── update 参数更新,子类同 ├── saveParams 参数保存,子类同 └── loadParams 参数加载,子类同 ├── AffineLayer类 └── ininParams 参数初始化 ├── ReluLayer类 ├── SoftMaxLayer类 ├── SoftMaxLossLayer类 └── cross_entropy_error 计算交叉熵 ├── ConvolutionLayer类 └── initParams 参数初始化 ├── PoolingLayer类 ├── DropOutLayer类 └── LayerFactory类 └── produce 根据神经网络层的名称和参数来构造类 ├── drawer.py 结果绘制 └── Drawer类 数据可视化类 ├── record 记录数据 └── plot 绘制数据 ├── dataset.py 获取数据集并且split好 └── DataPreparer类 数据准备类 ├── getData 加载数据并且划分测试集和训练集 └── get 外部获取数据接口 ├── analyser.py 模型分析 └── Analyser类 模型分析类 └── analyse 分析函数
三、具体流程和代码
1、参数配置文件的加载
with open(params_path) as params_file: self.params = yaml.load((params_file))
这里使用了yaml文件作为配置文件,原因是yaml文件结构比较简单简洁,可以清楚地表示出层次结构,通过参数文件的配置,可以不用修改源代码就可以配置出不同的神经网络,具体见下面layer生成的分析
2、加载数据集,分割训练集和测试集
使用sklearn 来获取数据集,并且进行分割
# 加载数据 digits_data, digits_target = load_digits( return_X_y=True) # 划分训练集和测试集 self.x_train, self.x_test, self.y_train, self.y_test = \ train_test_split(digits_data, digits_target, test_size=1-train_size_, train_size=train_size_, random_state=3, shuffle=True)
3、layer的生成
def produce(self, layer: str, params_set) -> object: if params_set is None: params_set = {} return eval(layer)(**params_set)
这里利用eval动态生成构造信息,这样的话就可以根据配置文件的信息来动态生成类,也就是说网络选择哪些层,层的顺序是什么,只要符合一定的顺序(这个规则在yaml文件有写),就可以构造网络并且顺利运行,所得到的层使用OrderDict按顺序存储
-
训练数据的提取
由于训练集数据并不多,如果仅仅利用这些数据训练的话,一定不够,所以采用了每次随机提取一个batch的数据的方案来进行训练,这样可以缓解数据集不足的问题
-
参数初始化方法
- 由于使用Relu函数作为激活函数,故参数初始化统一使用He方法,Xavier方法可选
-
神经网络的前向传播
根据 ConvolutionLayer -> ReluLayer -> PoolingLayer -> ConvolutionLayer -> ReluLayer -> PoolingLayer -> ConvolutionLayer -> ReluLayer -> DropOutLayer -> AffineLayer -> ReluLayer -> AffineLayer -> SoftMaxLossLayer 的顺序搭建网络,然后依次调用forward函数即可
-
AffineLayer的前向传播
-
由于传进的维度可能不是二维,所以首先要保存原来的维度信息,然后resize成二维矩阵
-
根据矩阵的乘法以及加法来计算前向传播值
-
-
ConvolutionLayer的前向传播
-
将图像卷积转化为易于矩阵相乘的形式
-
用相同与AffineLayer前向传播的方式计算
-
-
ReluLayer的前向传播
- mask标记出小于零的项,然后使这些项等于0
-
PoolingLayer的前向传播
-
利用im2col将4个数放在同一行(2*2池化核的情况下)
-
计算每一行的最大值,保留最大值,其它全部删去,传播给下一层
-
保存最大值的索引,方便反向传播
-
-
DropOutLayer的前向传播
-
训练模式下,删去部分神经元,即将参数设置为0,传播给下一层
-
测试模式下,将每一个神经元参数下降一定比例
-
-
SoftMaxLossLayer的前向传播
-
利用SoftMax层进行计算,得到前向传播的最终结果
-
对SoftMax层的计算结果利用交叉熵方法求loss
-
4、神经网络的反向传播
-
AffineLayer的反向传播
-
根据矩阵乘法求取梯度(这里不具体分析原因,需要用到矩阵论的知识)
-
矩阵乘法求取反向传播值,并且将值resize回原来的形状
-
-
ConvolutionLayer的反向传播
-
用同于计算AffineLayer反向传播的方法计算
-
将结果用col2im还原为原来图片的形状
-
-
ReluLayer的反向传播
- 使原来小于零的项反向传播值也为0
-
PoolingLayer的反向传播
-
使前向传播最大值处的梯度值保留,其余处的梯度值变为0
-
将矩阵使用col2im转化回原来的形式
-
-
DropOutLayer的反向传播
- 删除部分的神经元反向传播值为0
-
SoftMaxLossLayer的前向传播
- 公式 y-t 来进行计算
神经网络的参数更新
- 通过反向传播之后,我们获得了AffineLayer和ConvolutionLayer的梯度值,于是可以根据这些梯度值对参数进行更新,更新方法有两种可选:
-
随机梯度下降法,学习率恒定
-
AdaGrad:学习率会根据之前梯度的平方和来逐渐削弱
-
-
模型测试
x_test = self.x_test.reshape(-1, 1, 8, 8) x_predict = self.predict(x_test) predict_y = np.argmax(x_predict, axis=1)
-
参数保存
-
通过调用每一层的saveParams()函数,可以对每一层的参数按顺序保存,然后利用pickle保存模型参数为pkl格式
params_list = [] for layer in self.order_layer.values(): params_list.append(layer.saveParams()) with open(self.params['save_params_path'], 'wb') as f: pickle.dump(params_list, f)
- 参数加载
- 在已经保存参数模型之后,可以调用loadAndTest()函数对参数进行加载。思路是先通过正常方式构建网络,然后将W,b两个参数通过调用网络的initParams()函数进行构造
- 模型评估
-
使用sklearn的metrics板块对分类模型进行评估
print(metrics.classification_report(target, predict_y)) print(metrics.accuracy_score(target, predict_y)) print(metrics.confusion_matrix(target, predict_y))
-
通过不同的测试集和不同的数据集,正确率在 97.2 - 99.4 不等
- 由数据可以知道,准确率只有1,5 不到1.00,根据混淆矩阵,可以知道1被错误识别为5,5被错误识别为6(这个也可以通过召回率来得到,通过混淆矩阵可以知道准确率和召回率是怎样计算的)
- loss曲线绘制
-
利用matplitlib中的绘制散点图函数,即
plt.scatter(x, self.data_set) plt.title("handwriting digits classification") plt.xlabel('num') plt.ylabel('loss') plt.legend('loss')
-
由图可以看出,第一次迭代时梯度下降最快,后续下降越来越慢最终loss稳定在 10 * e-4 左右