# 卷积神经网络手写数字识别 **Repository Path**: XianMengxi/NeturalNetwork ## Basic Information - **Project Name**: 卷积神经网络手写数字识别 - **Description**: 华中科技大学 2021 人工智能与自动化学院 Python课程设计 课设 手写数字识别卷积神经网络 - **Primary Language**: Python - **License**: ISC - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 27 - **Forks**: 4 - **Created**: 2021-12-26 - **Last Updated**: 2025-06-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 手写数字识别神经网络 ## 作者信息 - 程序地址: ## 程序依赖 - pyyaml - numpy - sklearn - matplotlib - abc ## 选题 - 利用numpy完成手写数字数据集的识别,完成多分类问题,搭建神经网络,并且完成模型的训练以及性能评估,可视化数据 ## 用到的知识 1. sklearn 数据集的提取分割 2. yaml配置文件使用 3. numpy实现各个神经层 4. 参数初值选择 5. 梯度下降方法选择 6. sklearn 分类模型评估 7. matplotlib数据可视化 8. 设计模式 9. Markdown写报告 10. 防脱发技术 ## 框架 - 整体框架 ![框架](./pic/1.png) - 代码框架 ```txt . ├── 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. 参数配置文件的加载 ```python with open(params_path) as params_file: self.params = yaml.load((params_file)) ``` 这里使用了yaml文件作为配置文件,原因是yaml文件结构比较简单简洁,可以清楚地表示出层次结构,通过参数文件的配置,可以不用修改源代码就可以配置出不同的神经网络,具体见下面layer生成的分析 2. 加载数据集,分割训练集和测试集 使用sklearn 来获取数据集,并且进行分割 ```python # 加载数据 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的生成 ```python def produce(self, layer: str, params_set) -> object: if params_set is None: params_set = {} return eval(layer)(**params_set) ``` 这里利用eval动态生成构造信息,这样的话就可以根据配置文件的信息来动态生成类,也就是说网络选择哪些层,层的顺序是什么,只要符合一定的顺序(这个规则在yaml文件有写),就可以构造网络并且顺利运行,所得到的层使用OrderDict按顺序存储 4. 训练数据的提取 由于训练集数据并不多,如果仅仅利用这些数据训练的话,一定不够,所以采用了每次随机提取一个batch的数据的方案来进行训练,这样可以缓解数据集不足的问题 5. 参数初始化方法 - 由于使用Relu函数作为激活函数,故参数初始化统一使用He方法,Xavier方法可选 6. 神经网络的前向传播 根据 ConvolutionLayer -> ReluLayer -> PoolingLayer -> ConvolutionLayer -> ReluLayer -> PoolingLayer -> ConvolutionLayer -> ReluLayer -> DropOutLayer -> AffineLayer -> ReluLayer -> AffineLayer -> SoftMaxLossLayer 的顺序搭建网络,然后依次调用forward函数即可 - AffineLayer的前向传播 1. 由于传进的维度可能不是二维,所以首先要保存原来的维度信息,然后resize成二维矩阵 2. 根据矩阵的乘法以及加法来计算前向传播值 - ConvolutionLayer的前向传播 1. 将图像卷积转化为易于矩阵相乘的形式 2. 用相同与AffineLayer前向传播的方式计算 - ReluLayer的前向传播 1. mask标记出小于零的项,然后使这些项等于0 - PoolingLayer的前向传播 1. 利用im2col将4个数放在同一行(2*2池化核的情况下) 2. 计算每一行的最大值,保留最大值,其它全部删去,传播给下一层 3. 保存最大值的索引,方便反向传播 - DropOutLayer的前向传播 1. 训练模式下,删去部分神经元,即将参数设置为0,传播给下一层 2. 测试模式下,将每一个神经元参数下降一定比例 - SoftMaxLossLayer的前向传播 1. 利用SoftMax层进行计算,得到前向传播的最终结果 2. 对SoftMax层的计算结果利用交叉熵方法求loss 7. 神经网络的反向传播 - AffineLayer的反向传播 1. 根据矩阵乘法求取梯度(这里不具体分析原因,需要用到矩阵论的知识) 2. 矩阵乘法求取反向传播值,并且将值resize回原来的形状 - ConvolutionLayer的反向传播 1. 用同于计算AffineLayer反向传播的方法计算 2. 将结果用col2im还原为原来图片的形状 - ReluLayer的反向传播 1. 使原来小于零的项反向传播值也为0 - PoolingLayer的反向传播 1. 使前向传播最大值处的梯度值保留,其余处的梯度值变为0 2. 将矩阵使用col2im转化回原来的形式 - DropOutLayer的反向传播 1. 删除部分的神经元反向传播值为0 - SoftMaxLossLayer的前向传播 1. 公式 y-t 来进行计算 8. 神经网络的参数更新 - 通过反向传播之后,我们获得了AffineLayer和ConvolutionLayer的梯度值,于是可以根据这些梯度值对参数进行更新,更新方法有两种可选: 1. 随机梯度下降法,学习率恒定 2. AdaGrad:学习率会根据之前梯度的平方和来逐渐削弱 9. 模型测试 ```python x_test = self.x_test.reshape(-1, 1, 8, 8) x_predict = self.predict(x_test) predict_y = np.argmax(x_predict, axis=1) ``` 10. 参数保存 - 通过调用每一层的saveParams()函数,可以对每一层的参数按顺序保存,然后利用pickle保存模型参数为pkl格式 ```python 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) ``` 11. 参数加载 - 在已经保存参数模型之后,可以调用loadAndTest()函数对参数进行加载。思路是先通过正常方式构建网络,然后将W,b两个参数通过调用网络的initParams()函数进行构造 12. 模型评估 - 使用sklearn的metrics板块对分类模型进行评估 ```python 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 不等 ![测试结果](./pic/hao.png) - 由数据可以知道,准确率只有1,5 不到1.00,根据混淆矩阵,可以知道1被错误识别为5,5被错误识别为6(这个也可以通过召回率来得到,通过混淆矩阵可以知道准确率和召回率是怎样计算的) 13. loss曲线绘制 - 利用matplitlib中的绘制散点图函数,即 ```python plt.scatter(x, self.data_set) plt.title("handwriting digits classification") plt.xlabel('num') plt.ylabel('loss') plt.legend('loss') ``` - 由图可以看出,第一次迭代时梯度下降最快,后续下降越来越慢最终loss稳定在 10 * e-4 左右 ![loss](./pic/loss.png) ## 编程规范 - 命名规范: 1. py文件名称统一全部小写 2. 类名称统一首字母大写,大小写结合的形式 3. 变量名称用 _ 来进行连接,全部小写,尽量做到见名知意 4. 函数名首字母小写,大小写结合的形式 5. 部分参考(抄)书本中的代码的命名没改 - 编程思想规范 1. 尽量使每一个类负责一个独立的功能,每个类之间没有关联,然后有一个主要的类将所有的类连接起来 ## 设计模式思想 1. 活用继承,比如设置了一个LayerBase()类,类内有多个抽象函数(虚函数)构成纯虚类,这样的话,每一个layer继承这个类之后必须强制重载虚函数才能构造类,这用到了设计模式中的可扩展思想(多态不太明显) 2. 使用工厂设计模式,比如LayerFactory(),通过传进类名称和参数可以动态构造类,这可以极大地减少代码量,使代码更加简洁,同时,如果有需要新的层的话,只需要在配置文件中修改,而不需要改动程序中的任何内容 ## 感悟 - 我之前有学习过机器学习,但对深度学习不是很了解,而且对于底层实现,基本没有写过(都是在调库)。所以,这对于我来说本身就是一次挑战。 - 过程中,由于[深度学习入门:基于Python的原理与实现] 这本书确实比较浅显,加上有代码作为参考,很多代码其实在参考代码上改动一下就能用(但也需要理解其中的思想)。 - 我的目标一开始便是搭建可以根据配置文件修改网络的神经网络,加上我之前主要用的是c++,也有了解过一点设计模式的内容。于是就打算将这些设计思想用到python上。但实践过程发现,python并不是很适合用虚基类,因为其本身就不需要指定类的类型,这就导致了多态的优势不是很明显,用了虚继承相当于没用,不过工厂设计模式优势还是可见的 - 学习过程之中,我一步步地走完了整个搭建卷积神经网络的流程,虽然在搭建卷积层的时候出现了不少问题,比如对维度的敏感性不够,或者是对reshape轴的变化不是很理解(就是因为这个reshape, gpuarray不能使用导致我放弃了加入pycuda加速程序),过程中感悟到的思想特别是反向传播思想令我收益匪浅 ## 参考 - [1] [深度学习入门:基于Python的原理与实现]