# hand_writing **Repository Path**: cfz2024/hand_writing ## Basic Information - **Project Name**: hand_writing - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-06-26 - **Last Updated**: 2026-06-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 手写数字识别 - 从零实现的神经网络 > 不使用任何深度学习框架(PyTorch/TensorFlow),仅用 NumPy 从零实现神经网络的全部核心组件。 ## 项目简介 本项目实现了一个完整的神经网络框架,用于识别 MNIST 手写数字数据集(0~9)。所有核心算法——前向传播、反向传播、梯度下降、激活函数、损失函数——全部手动实现,帮助你深入理解深度学习的底层原理。 ### 最终效果 - 测试集准确率:**94.43%**(20 epochs) - 训练耗时:约 33 秒 - 支持导出 ONNX 模型,可跨平台部署 --- ## 文件结构 ``` hand_writing/ ├── main.py # 主程序:训练流程 + 评估 + 导出 ├── predict.py # 测试脚本:输入图片 → 输出识别结果 ├── network.py # 神经网络核心类(组装、前向、反向、更新) ├── layers.py # 全连接层(Dense Layer) ├── activations.py # 激活函数(Sigmoid, ReLU, Softmax) ├── loss.py # 损失函数(交叉熵、MSE) ├── optimizer.py # 优化器(SGD、Adam) ├── data_loader.py # MNIST 数据加载与预处理 ├── utils.py # 工具函数(混淆矩阵、训练曲线、可视化) ├── onnx_exporter.py # ONNX 模型导出 ├── .gitignore # Git 忽略规则 └── README.md # 本文档 ``` --- ## 环境要求 - Python 3.8+ - numpy(必须) - onnx(可选,用于导出 ONNX 模型) - onnxruntime(可选,用于验证 ONNX 模型) - matplotlib(可选,用于绘制训练曲线和可视化) - Pillow(可选,用于 predict.py 处理图片) ```bash # 激活虚拟环境(已有 YOLO 环境)或者任意有这些包的环境 source ~/yolo11-env-py39/bin/activate # 或手动安装依赖 pip install numpy onnx onnxruntime matplotlib Pillow ``` --- ## 快速开始 ### 1. 训练模型 ```bash source ~/yolo11-env-py39/bin/activate python main.py ``` 训练完成后会自动: - 下载 MNIST 数据集到 `./mnist_data/` - 保存模型权重到 `./output/model_weights.pkl` - 导出 ONNX 模型到 `./output/model.onnx` - 绘制训练曲线到 `./output/training_curves.png` ### 2. 测试预测 ```bash # 随机测试 10 个 MNIST 样本 python predict.py --random 10 # 预测单张图片 python predict.py --image test.png # 预测并可视化概率分布 python predict.py --image test.png --visualize # 批量预测目录中的所有图片 python predict.py --batch ./my_digits/ # 查看完整帮助 python predict.py --help ``` --- ## 核心原理详解 ### 网络结构 ``` 输入层 (784个神经元) ↓ 全连接层 1: Dense(784 → 128) + ReLU 激活 ↓ 全连接层 2: Dense(128 → 64) + ReLU 激活 ↓ 输出层 3: Dense(64 → 10) + Softmax 激活 ↓ 输出 (10个概率值,对应数字 0~9) ``` **为什么是 784?** MNIST 图片是 28×28 像素的灰度图,展平后就是 784 维向量。 **为什么输出是 10?** 我们要识别 0~9 共 10 个数字,每个数字对应一个输出神经元。 --- ## 各模块详解 ### 1. activations.py — 激活函数 激活函数决定了神经元是否被"激活",引入非线性,让网络能学习复杂模式。 #### Sigmoid ``` 公式:σ(x) = 1 / (1 + e^(-x)) 导数:σ'(x) = σ(x) × (1 - σ(x)) 输出范围:(0, 1) ``` - 将任意实数压缩到 0~1 之间 - 适合二分类输出层 - **缺点**:深层网络会出现梯度消失问题(输出接近 0 或 1 时导数趋近于 0) #### ReLU(Rectified Linear Unit) ``` 公式:f(x) = max(0, x) 导数:f'(x) = 1 (x>0), 0 (x≤0) 输出范围:[0, +∞) ``` - 计算简单,收敛快 - **优点**:正区间导数恒为 1,不会梯度消失 - **缺点**:负区间导数为 0,可能导致"神经元死亡" - **当前项目中用于隐藏层** #### Softmax ``` 公式:f(x_i) = e^(x_i) / Σ e^(x_j) ``` - 将一组数字转换为概率分布(所有输出之和 = 1) - **当前项目中用于输出层**,输出每个数字的概率 - 实现时减去最大值防止数值溢出 --- ### 2. layers.py — 全连接层(Dense Layer) 全连接层是神经网络最基本的构建块,每个输入神经元与每个输出神经元都有连接。 #### 前向传播 ``` Z = X · W + b ``` - `X`: 输入矩阵,shape = (batch_size, input_size) - `W`: 权重矩阵,shape = (input_size, output_size) - `b`: 偏置向量,shape = (1, output_size) - `Z`: 输出矩阵,shape = (batch_size, output_size) **通俗理解**:每个输出 = 所有输入的加权和 + 偏置。权重决定了每个输入的重要程度。 #### 反向传播 ``` dW = X^T · dZ / batch_size (权重梯度) db = mean(dZ) (偏置梯度) dX = dZ · W^T (传给前一层的梯度) ``` - `dZ`: 来自上层的梯度 - `dW`: 损失对权重的梯度,用于更新 W - `db`: 损失对偏置的梯度,用于更新 b - `dX`: 损失对输入的梯度,继续向前传播(链式法则) **通俗理解**:根据"误差有多大"来调整每个连接的权重。误差大就调多一点,误差小就调少一点。 #### 权重初始化 | 方法 | 公式 | 适用场景 | |------|------|----------| | He 初始化 | W ~ N(0, √(2/n_in)) | ReLU 激活函数 | | Xavier 初始化 | W ~ N(0, √(1/n_in)) | Sigmoid/Softmax 激活函数 | | Random | W ~ N(0, 0.01) | 简单场景 | **为什么不能全初始化为 0?** 因为所有神经元会学到完全相同的东西,网络等效于单个神经元。 **为什么用 He/Xavier?** 它们根据输入维度自动调整初始权重的范围,保证前向传播时信号不会逐层放大或缩小。 --- ### 3. loss.py — 损失函数 损失函数衡量模型预测值与真实值之间的差距。训练的目标就是最小化损失。 #### 交叉熵损失(Cross-Entropy Loss) ``` L = -1/m × Σ Σ y_i × log(ŷ_i) ``` - `y`: 真实标签(one-hot 编码),如数字 3 → [0,0,0,1,0,0,0,0,0,0] - `ŷ`: 预测概率(Softmax 输出),如 [0.01,0.02,0.01,0.85,0.03,...] - `m`: 样本数量 **通俗理解**:如果真实标签是 3,而模型预测 3 的概率是 0.85,损失就很小;如果预测 3 的概率只有 0.01,损失就很大。 #### Softmax + 交叉熵的组合导数 ``` dL/dz = ŷ - y ``` 这是一个非常优雅的简化。Softmax 和交叉熵配合使用时,梯度直接等于"预测值 - 真实值",计算非常高效。 #### MSE 损失(均方误差,可选对比) ``` L = 1/m × Σ (y - ŷ)² ``` 通常用于回归任务,在分类任务中效果不如交叉熵。 --- ### 4. optimizer.py — 优化器 优化器决定了如何根据梯度来更新参数。 #### SGD(随机梯度下降) ``` W = W - learning_rate × dW ``` - `learning_rate`(学习率):控制每次更新的步长 - **太大**:参数来回震荡,损失不收敛 - **太小**:收敛太慢,训练时间长 - **经验值**:0.001 ~ 0.1 #### SGD + Momentum(动量) ``` v = momentum × v - learning_rate × dW W = W + v ``` - `momentum`(动量因子):利用历史梯度的"惯性" - **作用**:加速收敛,减少震荡,帮助跳出局部最小值 - **经验值**:0.9 **通俗理解**:想象一个球从山上滚下来。没有动量时,球只看当前坡度;有动量时,球还保留了之前的滚动惯性,能冲过小坑继续前进。 #### Adam(自适应矩估计) ``` m = β1 × m + (1-β1) × dW # 一阶矩(均值) v = β2 × v + (1-β2) × dW² # 二阶矩(方差) W = W - lr × m̂ / (√v̂ + ε) ``` - `β1=0.9, β2=0.999`: 指数衰减率 - `ε=1e-8`: 防止除零 - **优点**:自动调整每个参数的学习率,对超参数不敏感 --- ### 5. network.py — 神经网络核心类 `NeuralNetwork` 类负责组装所有组件,协调整个训练流程。 #### 前向传播流程 ``` 输入 X → 第1层 Dense: Z1 = X·W1 + b1 → ReLU 激活: A1 = max(0, Z1) → 第2层 Dense: Z2 = A1·W2 + b2 → ReLU 激活: A2 = max(0, Z2) → 第3层 Dense: Z3 = A2·W3 + b3 → Softmax 激活: A3 = softmax(Z3) → 输出 ŷ = A3 ``` #### 反向传播流程 ``` 损失梯度 dL/dŷ → Softmax+交叉熵导数: dZ3 = ŷ - y → 第3层 Dense 反向: dW3, db3, dA2 = backward(dZ3) → ReLU 反向: dZ2 = dA2 × (Z2 > 0) → 第2层 Dense 反向: dW2, db2, dA1 = backward(dZ2) → ReLU 反向: dZ1 = dA1 × (Z1 > 0) → 第1层 Dense 反向: dW1, db1 = backward(dZ1) ``` #### 参数更新 ``` W1 = W1 - lr × dW1 b1 = b1 - lr × db1 W2 = W2 - lr × dW2 b2 = b2 - lr × db2 W3 = W3 - lr × dW3 b3 = b3 - lr × db3 ``` --- ### 6. data_loader.py — 数据加载 #### MNIST 数据集 - 训练集:60,000 张 28×28 灰度手写数字图片 - 测试集:10,000 张 - 标签:0~9 的数字 - 格式:IDX 格式(二进制),需要解压和解析 #### 数据预处理 ``` 原始像素 (0~255) → 归一化到 (0~1) → 展平为 784 维向量 ``` - **归一化**:除以 255.0,将像素值缩放到 [0, 1] - **展平**:28×28 的二维图片 → 784 维的一维向量 - **One-hot 编码**:标签 3 → [0,0,0,1,0,0,0,0,0,0] #### Mini-batch 训练 不是一次用全部数据训练,而是分成小批次(如 128 个样本一批)。 - `batch_size`(批次大小): - **太小**(如 16):梯度估计噪声大,训练不稳定 - **太大**(如 2048):内存占用大,泛化能力可能下降 - **经验值**:32 ~ 256 - `shuffle`(打乱):每个 epoch 开始前随机打乱数据顺序,防止模型学到数据顺序的规律 --- ### 7. onnx_exporter.py — ONNX 导出 ONNX(Open Neural Network Exchange)是开放的模型交换格式。 #### 导出过程 将我们的 numpy 权重转换为 ONNX 计算图: ``` Input(784) → MatMul(X, W1) → Add(Z1, b1) → Relu(A1) → MatMul(A1, W2) → Add(Z2, b2) → Relu(A2) → MatMul(A2, W3) → Add(Z3, b3) → Softmax(ŷ) → Output(10) ``` #### 导出后用途 - 用 [Netron](https://netron.app/) 可视化网络结构 - 用 ONNX Runtime 推理(跨语言:Python/C++/Java/C#) - 部署到 TensorRT、OpenVINO、CoreML 等推理引擎 --- ### 8. utils.py — 工具函数 | 函数 | 作用 | |------|------| | `accuracy()` | 计算分类准确率 | | `confusion_matrix()` | 计算混淆矩阵,展示每个类别的预测情况 | | `plot_training_curves()` | 绘制训练损失和准确率曲线 | | `visualize_predictions()` | 可视化预测结果(图片 + 真实/预测标签) | | `format_time()` | 格式化时间显示 | --- ## 训练参数说明 在 `main.py` 的 `train()` 函数中可以调整以下参数: ```python train( network, X_train, y_train_onehot, X_test, y_test, epochs=20, # 训练轮数 batch_size=128, # 批次大小 learning_rate=0.1, # 学习率 momentum=0.9 # 动量因子 ) ``` | 参数 | 默认值 | 说明 | 调参建议 | |------|--------|------|----------| | `epochs` | 20 | 整个数据集训练多少轮 | 太少欠拟合,太多过拟合。观察测试准确率不再提升时停止 | | `batch_size` | 128 | 每次更新用多少样本计算梯度 | 32~256 都可以,越大越快但可能泛化差 | | `learning_rate` | 0.1 | 参数更新的步长 | 最重要的超参数。太大震荡,太小慢。可配合学习率衰减 | | `momentum` | 0.9 | 动量因子,利用历史梯度 | 0.9 是常用值,可尝试 0.95 或 0.99 | --- ## 网络结构参数说明 在 `main.py` 的 `build_network()` 中可以修改网络结构: ```python # 第一层: 784 → 128, ReLU layer1 = Dense(784, 128, init_method='he') network.add_layer(layer1, activation=ReLU()) # 第二层: 128 → 64, ReLU layer2 = Dense(128, 64, init_method='he') network.add_layer(layer2, activation=ReLU()) # 输出层: 64 → 10, Softmax layer3 = Dense(64, 10, init_method='xavier') network.add_layer(layer3, activation=Softmax()) ``` | 参数 | 说明 | |------|------| | `Dense(input_size, output_size)` | 全连接层,input_size 是输入维度,output_size 是输出维度/神经元数量 | | `init_method='he'` | He 初始化,适合 ReLU。改为 `'xavier'` 适合 Sigmoid,`'random'` 是简单随机 | | `activation=ReLU()` | 激活函数。隐藏层推荐 ReLU,输出层用 Softmax | **可以尝试的修改**: - 增加隐藏层:添加更多 `Dense + ReLU` - 改变神经元数量:如 `Dense(784, 256)` 或 `Dense(784, 64)` - 更换激活函数:如把 ReLU 换成 Sigmoid - 添加 Dropout(需要自己实现) --- ## 训练输出解读 ``` Epoch 1/20 | Loss: 1.5239 | Train Acc: 0.6008 | Test Acc: 0.8204 | Time: 1.8s ``` | 指标 | 含义 | |------|------| | `Epoch 1/20` | 当前是第 1 轮,共 20 轮 | | `Loss: 1.5239` | 训练损失值,越小越好。刚开始大,随着训练逐渐减小 | | `Train Acc: 0.6008` | 训练集准确率,本批次平均 | | `Test Acc: 0.8204` | 测试集准确率,泛化能力的体现 | | `Time: 1.8s` | 本轮训练耗时 | **判断训练状态**: - Train Acc 和 Test Acc 都在提升 → 正常训练中 - Train Acc 很高但 Test Acc 不动 → **过拟合**,需要正则化 - 两者都很低 → **欠拟合**,可能需要更多层或更大网络 - Loss 突然变大 → 学习率可能太大 --- ## 常见问题 ### Q: 为什么不用 PyTorch/TensorFlow? A: 本项目的目的是深入理解神经网络的底层原理。用框架虽然方便,但很多细节被封装了。手写一遍后,你会真正理解前向传播、反向传播、梯度下降等核心概念。 ### Q: 94.43% 的准确率是不是太低了? A: 对于纯全连接网络,94% 已经不错。如果用卷积神经网络(CNN),MNIST 可以达到 99.5%+。全连接层没有利用像素的空间关系,所以有上限。 ### Q: 如何提升准确率? A: 几个方向: 1. 增加网络深度/宽度 2. 使用学习率衰减(lr_scheduler) 3. 添加正则化(Dropout、L2 正则化) 4. 数据增强(旋转、平移、缩放) 5. 换用卷积神经网络(CNN) ### Q: 为什么预测自己的图片不准? A: 因为 MNIST 的图片特征是白字黑底、笔画较细、居中、无噪点。你自己的图片可能风格完全不同。解决方案是使用数据增强或在你的图片上微调。 --- ## 扩展学习 1. **实现卷积层(Conv2D)**:学习卷积神经网络 2. **实现 Dropout 层**:防止过拟合 3. **实现 Batch Normalization**:加速训练 4. **实现学习率调度器**:训练过程中动态调整学习率 5. **用 CNN 重新训练**:在 MNIST 上达到 99%+ 准确率 --- ## 许可证 MIT License