简介:这份MATLAB实现资源以BP神经网络为核心,面向希望掌握手写数字识别完整流程的机器学习初学者与MATLAB开发者。压缩包共5027个文件,含5000张0-9手写数字BMP样本(每类500张)及7个M脚本,分别用于数据预处理、网络构建、训练与评估,另有20个INI配置文件,包体仅6.15MB,轻量易下载。目前已有3522人学习使用。资源涵盖从图片灰度化、二值化到反向传播训练和Softmax输出层评估的完整链路,可帮助读者理解梯度下降、损失函数与网络调参细节,并直接运行代码观察识别效果,适合课程作业、算法复现及入门深度学习实践。
1. BP神经网络手写数字识别:能在MATLAB里跑通的完整工程
如果你手头有MNIST手写数字数据集,却不想从零推导BP公式、不想踩矩阵维度不匹配的坑,这套MATLAB工程就是为你准备的。它把BP神经网络的完整链路——数据读入、归一化、前向传播、反向传播、权值更新、准确率评估——集成在可运行代码里:输入层784个神经元、隐藏层100个、输出层10个,用sigmoid做隐藏层激活、softmax做输出、交叉熵做损失。适合两类人:想验证自己反向传播推导的研究者,以及需要快速搭识别基线做课程实验的同学。区别于深度学习框架的黑匣子,这里每行矩阵运算都展开着,单步调试就能看清梯度从输出层一层层往回传的过程。
2. 项目架构与文件布局:MATLAB BP工程从IDX原始数据到训练管线的完整链路
先讲清楚这套工程的文件是怎么组织的。整个工程不是一个大而全的脚本,而是按"数据准备 → 网络定义 → 训练 → 评估"四个阶段拆成了若干个独立函数。每个函数短小、职责单一,调参时只改一处,定位问题时只看一个文件。这个拆分思路比把所有逻辑塞进一个main.m要实用得多,尤其当你需要把训练好的模型复用到其他实验里的时候——直接调用训练完的权重做预测,不用把几千行脚本整个跑一遍。
2.1 目录结构与数据准备模块
工程目录结构如下:
mnist_bp/ ├── main.m # 主入口:加载数据、初始化、训练、测试 ├── load_mnist.m # 解析MNIST的IDX二进制格式 ├── initialize_network.m # 初始化两层权值和偏置 ├── train_bp.m # 批量梯度下降训练主循环 ├── predict.m # 单样本前向预测 ├── evaluate.m # 测试集准确率统计 ├── sigmoid.m # sigmoid激活函数 ├── sigmoid_deriv.m # sigmoid导数,反向传播用 ├── softmax.m # softmax归一化输出 ├── onehot_encode.m # 标签转one-hot矩阵 └── mnist_data/ # 原始数据集目录数据准备是整个工程的第一个坑位。load_mnist.m 负责解析MNIST的IDX格式,IDX文件头的构成是:4字节magic number、4字节样本数、4字节行数、4字节列数,随后是unsigned byte类型的像素值。这里有个关键点:IDX是big-endian存储,而MATLAB默认按本机字节序读取,通常是little-endian,直接fread读出来的前四个值全是反的,样本数会变成一个天文数字。
function [images, labels] = load_mnist(image_file, label_file) % 读取图像文件(IDX格式,big-endian) fid = fopen(image_file, 'rb'); magic = fread(fid, 1, 'uint32', 0, 'ieee-be'); num_images = fread(fid, 1, 'uint32', 0, 'ieee-be'); rows = fread(fid, 1, 'uint32', 0, 'ieee-be'); cols = fread(fid, 1, 'uint32', 0, 'ieee-be'); images = fread(fid, num_images * rows * cols, 'uint8'); images = reshape(images, rows * cols, num_images)'; fclose(fid); % 读取标签文件 fid = fopen(label_file, 'rb'); magic = fread(fid, 1, 'uint32', 0, 'ieee-be'); num_labels = fread(fid, 1, 'uint32', 0, 'ieee-be'); labels = fread(fid, num_labels, 'uint8'); fclose(fid); end逻辑说明:fread的第四个参数0表示从当前位置顺序读,第五个参数'ieee-be'指定大端字节序,这一行是整个解析函数最容易被忽略的地方。读出的images是uint8类型,范围在0到255之间,训练前必须转成double并除以255归一化,否则后面矩阵乘法会溢出或收敛极慢。
参数说明:image_file传train-images.idx3-ubyte路径,label_file传train-labels.idx1-ubyte路径。函数返回的images是num_images行乘以784列的矩阵,每行代表一张28x28像素展平后的灰度向量;labels是对应的真实数字,是取值0到9的uint8列向量。写代码时建议用assert(num_images == 60000)这类检查兜底,防止文件路径给错时读出一堆垃圾数据还不知情。
2.2 网络初始化与标签编码设计
initialize_network.m 定义三层网络:输入层784个神经元,隐藏层100个,输出层10个。权值初始化我用的是均匀分布小随机数,范围控制在[-0.05, 0.05]。这个范围很重要——如果初始值过大,sigmoid在训练初期就进入饱和区,梯度几乎为零,网络从第一步开始就不动了;如果全是零,所有神经元完全对称,反向传播更新出的梯度相同,相当于退化成单神经元。
function [W1, b1, W2, b2] = initialize_network(input_size, hidden_size, output_size) % 权值均匀随机初始化,范围[-0.05, 0.05] W1 = rand(input_size, hidden_size) * 0.1 - 0.05; b1 = zeros(1, hidden_size); W2 = rand(hidden_size, output_size) * 0.1 - 0.05; b2 = zeros(1, output_size); end逻辑说明:输入层784固定,对应MNIST的28x28;隐藏层100是可以随意改的第一个调参旋钮,改到50会欠拟合,改到200会明显变慢;输出层10对应0到9十个类别。b1和b2初始化为0不会造成对称破裂问题,因为W1、W2的随机器已经打破了对称性。
标签编码这里有个容易出错的设计。网络输出的是10维概率,而原始标签是0到9的数字,直接拿来算交叉熵会出维度错误,所以需要one-hot编码:
function y = onehot_encode(labels, num_classes) n = length(labels); y = zeros(n, num_classes); idx = sub2ind(size(y), (1:n)', labels + 1); y(idx) = 1; end逻辑说明:sub2ind把二维行列索引转成线性索引,核心是labels + 1这个偏移。MNIST的标签0对应one-hot的第一列,标签9对应第十列,所以一定要加1。如果这里忘了加1,MATLAB会直接报索引0非法,或者把所有数字整体左移一位导致准确率永远上不去。返回的y是n行10列的稀疏矩阵,每一行只有一个位置是1,其余全是0。
2.3 隐藏层尺寸与激活函数的选择依据
为什么隐藏层选100而不是784或者20?这是经验值和复杂度折中的结果。784个输入本身就是高维的,如果隐藏层也设784,两层之间的参数量是784乘以784约61万个权值,一次性梯度计算的内存开销在MATLAB里非常可观,而且这个规模的数据集上很容易过拟合。设成100的话,参数量降到784乘以100加上100乘以10,约7.9万个,训练速度和效果都处在合理区间。如果你拿到的不是MNIST而是尺寸更大的灰度图,按"输入维度的一半到两倍之间"去试隐藏层大小是比较通用的起点。
激活函数的选择逻辑是围绕sigmoid的导数展开的:sigmoid在0附近导数最大约0.25,输入绝对值超过4就进入饱和区,导数趋近于零。这就是为什么反向传播要用链式法则把每一层的梯度逐层相乘——任何一层饱和,整条梯度链就会断掉。后续章节里我会讲怎么替换成ReLU来缓解这个问题,以及换完之后哪些地方要跟着改。
3. 核心代码拆解:一次MATLAB BP训练的前向与反向完整生命周期
这一章我把训练过程中最关键的三段代码拆开讲。理解这三段,就等于理解了整个BP神经网络的精髓,剩下的都是围绕它们的封装和美化。建议把train_bp.m打开,对照下面的讲解逐行看,看完整个梯度流动就清楚了。
3.1 前向传播:从784维输入到10维概率输出
前向传播的目的很直接:给定一张展平后的图像x,经过两层线性变换加非线性激活,得到10个类别的概率分布。predict.m里就是这个过程:
function [prob, a2] = predict(W1, b1, W2, b2, x) % 隐藏层计算:线性变换 + sigmoid z2 = x * W1 + b1; a2 = sigmoid(z2); % 输出层计算:线性变换 + softmax z3 = a2 * W2 + b2; prob = softmax(z3); end逻辑说明:x是1行784列的向量,和W1(784x100)做矩阵乘法得到1行100列的z2,b1用MATLAB的隐式广播自动加到每一列。sigmoid把z2压到0到1之间,得到隐藏层激活值a2。第二层同理,a2乘以W2(100x10)得到z3,再经过softmax归一化成概率。返回的prob里10个值加起来等于1,其中最大的那个值的索引减1就是预测的数字。
softmax的实现有个经典细节值得单独说:
function y = softmax(z) % 减去每行最大值,防止指数溢出 z = z - max(z, [], 2); exp_z = exp(z); y = exp_z ./ sum(exp_z, 2); end逻辑说明:z3里的值如果有某个特别大,比如超过700,exp(z)在MATLAB里会直接变成Inf,整个结果就被污染成NaN,后续所有梯度更新全部失效。先减掉每行的最大值再取指数,数值上完全等价,但永远不会溢出。这是跑BP神经网络时最容易忽略的数值稳定性问题,很多"玄学"的NaN都是从这里来的。参数上,max的第二个参数2表示按行取最大值,sum同理,保证的是对每个样本单独归一化。
3.2 反向传播:交叉熵损失下的梯度回传
反向传播是整套代码的核心。这里采用交叉熵损失配合softmax输出层,两者的梯度有一个非常漂亮的简化:输出层的误差项delta3直接等于A3减去真实标签Y,不需要额外的导数项。这个简化来自softmax和交叉熵联合求导后的形式,代码看起来像魔法,实际是数学上的抵消。
function [W1, b1, W2, b2, loss_history] = train_bp(X, Y, W1, b1, W2, b2, opts) learning_rate = opts.learning_rate; epochs = opts.epochs; n = size(X, 1); loss_history = zeros(epochs, 1); for epoch = 1:epochs % 前向传播:一次性计算全部样本 Z2 = X * W1 + b1; A2 = sigmoid(Z2); Z3 = A2 * W2 + b2; A3 = softmax(Z3); % 交叉熵损失,加1e-12防止log(0) loss = -mean(sum(Y .* log(A3 + 1e-12), 2)); loss_history(epoch) = loss; % 反向传播:输出层误差(softmax+交叉熵联合简化) delta3 = A3 - Y; % 反向传播:隐藏层误差,sigmoid_deriv是a2.*(1-a2) delta2 = (delta3 * W2') .* sigmoid_deriv(Z2); % 梯度计算 grad_W2 = A2' * delta3; grad_b2 = sum(delta3, 1); grad_W1 = X' * delta2; grad_b1 = sum(delta2, 1); % 参数更新:梯度下降 W2 = W2 - learning_rate * grad_W2; b2 = b2 - learning_rate * grad_b2; W1 = W1 - learning_rate * grad_W1; b1 = b1 - learning_rate * grad_b1; fprintf('Epoch %d/%d, Loss: %.4f\n', epoch, epochs, loss); end end逻辑说明:这里整个训练过程是用全量样本做的批量梯度下降,X是60000行784列的大矩阵,前向传播一次矩阵乘法就把全部样本跑完了。delta3 = A3 - Y来自softmax与交叉熵的联合求导,如果换成均方误差,这个等式不成立,必须乘上额外的导数项,这是新手最容易抄错的地方——网上很多代码直接把这就简写了,换损失函数时忘记了它只对交叉熵成立。
梯度更新的维度要仔细核对:grad_W2是A2的转置(100行n列)乘以delta3(n行10列),结果是100行10列,和W2的shape一致;grad_W1同理是784行100列。偏置的梯度是对样本维求和,得到一个1行10列的向量,这也是MATLAB里广播机制和手写循环最容易对不上的地方。
参数说明:opts是一个结构体,建议字段包含learning_rate(默认0.5)、epochs(默认5)。如果你想改成小批量训练,需要自己加一个batch_size字段,用循环对数据切片,在每个切片内做同样的前向和反向计算,最后累加梯度取平均。学习率0.5对这个网络结构偏大但能收敛,训练速度快;追求稳定就把learning_rate降到0.1。
3.3 梯度检查:给反向传播上一道保险
反向传播写完之后第一件事不是跑全量训练,而是做梯度检查。这个环节能省下大量排查时间——很多脑补"看起来没问题"的代码,一上梯度检查就原形毕露。做法是用数值微分近似梯度,和BP算出来的解析梯度对比。
function check_gradients(X, Y, W1, b1, W2, b2) % 只取256个样本做梯度检查,全量样本太耗时 X = X(1:256, :); Y = Y(1:256, :); eps = 1e-6; % 解析梯度:直接复用BP公式,要检查的部分 Z2 = X * W1 + b1; A2 = sigmoid(Z2); Z3 = A2 * W2 + b2; A3 = softmax(Z3); delta3 = A3 - Y; delta2 = (delta3 * W2') .* sigmoid_deriv(Z2); grad_W1 = X' * delta2; % 对W1的随机一个元素算数值梯度 i = randi(size(W1, 1)); j = randi(size(W1, 2)); Wp = W1; Wp(i, j) = Wp(i, j) + eps; Wm = W1; Wm(i, j) = Wm(i, j) - eps; lp = compute_loss(X, Y, Wp, b1, W2, b2); lm = compute_loss(X, Y, Wm, b1, W2, b2); num_grad = (lp - lm) / (2 * eps); fprintf('数值梯度: %.6f, 解析梯度: %.6f\n', num_grad, grad_W1(i, j)); end function loss = compute_loss(X, Y, W1, b1, W2, b2) Z2 = X * W1 + b1; A2 = sigmoid(Z2); Z3 = A2 * W2 + b2; A3 = softmax(Z3); loss = -mean(sum(Y .* log(A3 + 1e-12), 2)); end逻辑说明:数值梯度的思想是用导数的定义式,对某个权值加一个小量再减一个小量,分别计算损失,用中心差分近似梯度。如果解析梯度(BP算出来的)和数值梯度相差超过1e-6量级,说明反向传播公式写错了。常见错误包括:漏乘sigmoid导数、delta3的符号反了、参数更新方向写成了加号。注意只随机抽查几个元素即可,全部参数逐个算数值梯度在工程上不现实。
提示:梯度检查务必在投入完整训练前跑,而且建议用单精度或小矩阵做。一旦训练已经开始,loss的趋势很容易掩盖公式错误,让你误以为只是"学习率没调好"。
4. 训练与评估:主脚本跑通手写数字识别全流程
代码拆完之后,这一章讲怎么把整个工程串起来跑,以及跑完之后怎么判断结果是不是正常。整个流程在MATLAB里敲一个main,等待几十秒,就能看到测试准确率打印出来。
4.1 主脚本main.m:五步跑完整训练流程
main.m是工程的唯一入口,按顺序做五件事:加载数据、预处理、初始化网络、训练、评估。
%% 主程序入口 clc; clear; close all; % 第1步:加载MNIST训练集和测试集 [train_x, train_y] = load_mnist('mnist_data/train-images.idx3-ubyte', ... 'mnist_data/train-labels.idx1-ubyte'); [test_x, test_y] = load_mnist('mnist_data/t10k-images.idx3-ubyte', ... 'mnist_data/t10k-labels.idx1-ubyte'); % 第2步:归一化并one-hot化标签 train_x = double(train_x) / 255.0; test_x = double(test_x) / 255.0; train_y_oh = onehot_encode(train_y, 10); % 第3步:初始化784-100-10网络 [W1, b1, W2, b2] = initialize_network(784, 100, 10); % 第4步:训练5个epoch opts.learning_rate = 0.5; opts.epochs = 5; [W1, b1, W2, b2, loss_history] = train_bp(train_x, train_y_oh, ... W1, b1, W2, b2, opts); % 第5步:测试集评估 acc = evaluate(W1, b1, W2, b2, test_x, test_y); fprintf('测试集准确率: %.2f%%\n', acc * 100); % 可视化损失下降曲线 figure; plot(1:opts.epochs, loss_history, '-o'); xlabel('Epoch'); ylabel('Loss'); title('BP Training Loss'); grid on;参数说明:load_mnist的路径要和实际数据存放位置对应,相对路径或绝对路径都行,但注意MATLAB的工作目录必须切到mnist_bp文件夹下面,否则会报文件不存在。归一化必须在转double之后进行,uint8直接除255在MATLAB里会被取整,结果大部分是0,模型直接废掉。opts.learning_rate在0.3到0.7之间对这个网络结构都比较稳定。
训练过程的输出类似这样:每个epoch打印一行loss,能看到loss从最初的2.3左右单调下降到0.1附近。如果loss在某个epoch出现反弹,多半是学习率偏大,把0.5改成0.1再试。跑完之后evaluate函数统计测试集预测正确的比例,默认参数下通常能到92%到94%之间。
4.2 结果评估与学习率调参对照
evaluate.m的实现就是对测试集做一次批量前向传播,取概率最大的索引和真值比对:
function acc = evaluate(W1, b1, W2, b2, test_x, test_y) % 测试集批量前向传播 Z2 = test_x * W1 + b1; A2 = sigmoid(Z2); Z3 = A2 * W2 + b2; A3 = softmax(Z3); [~, pred] = max(A3, [], 2); acc = mean(pred - 1 == test_y); end逻辑说明:max的第二个返回值是每行最大值的列索引,pred取值范围是1到10,所以和test_y比较前要减1。mean对布尔向量求平均,得到的就是准确率。这里整个测试过程也是全量矩阵运算,10000张图一次性前向,比for循环逐张预测快两个数量级以上。
如果准确率低于90%,优先检查两个地方:一是数据归一化是否生效,二是one-hot编码的列偏移。学习率对收敛的影响可以做一个快速对比实验:
| 学习率 | 5轮后loss | 典型现象 |
|---|---|---|
| 0.05 | 0.35 | 收敛太慢,5轮不够 |
| 0.5 | 0.08 | 正常收敛,准确率92%+ |
| 2.0 | 震荡 | loss反复横跳,准确率上不去 |
这个观察是通用的:学习率每差一个数量级,收敛速度差好几倍;但过大之后网络在损失曲面上的更新步长会跨过极小值点,在代价函数底部来回震荡。遇到震荡,最直接的后悔药就是把学习率降回0.5,或者改成每10个epoch衰减一次学习率的策略。
4.3 抽样可视化:别让准确率骗了你
整体准确率只能说明模型"大体上"在学,但看不到细节。我一般会再跑一段可视化代码,随机抽几张测试图,把原图和预测结果并排打出来:
% 随机抽5张测试图,展示预测结果 figure; for k = 1:5 idx = randi(size(test_x, 1)); [prob, ~] = predict(W1, b1, W2, b2, test_x(idx, :)); [~, pred] = max(prob); subplot(1, 5, k); imshow(reshape(test_x(idx, :), 28, 28)); title(sprintf('True:%d Pred:%d', test_y(idx), pred - 1)); end逻辑说明:这里用reshape把784维向量还原成28x28矩阵,imshow显示灰度图。title里同时打印真实标签和预测标签,一眼就能看出网络在哪些数字上犯糊涂。如果发现9经常被认成4、3经常被认成8,说明隐藏层特征没学够,需要增加hidden_size或训练轮数;如果全部预测都对,再考虑要不要收一收网络容量防止过拟合。
5. 避坑指南:MATLAB跑BP网络时我踩过的5个坑
这一章写的都是从实际运行里攒下来的血泪经验,每条对应一个真实调试场景——前两条是数据层面的问题,后三条是网络和资源层面的。按"现象 → 原因 → 解决"的格式写,照方抓药能省不少时间。
5.1 训练误差不下降,一直卡在2.3附近
现象:loss从第一次迭代开始就纹丝不动,打印出来的值始终在2.3左右。
原因:2.3接近log(10),说明输出层的概率分布几乎是均匀的,网络完全没有学到任何东西。最常见的原因是数据没归一化,uint8的像素值0到255直接和全0.05量级的初始权重相乘,sigmoid的输入全部落在饱和区,梯度为零。另一个可疑点是训练前忘了做one-hot,Y还是0到9的数字向量,和10列的输出对不上,产生的错误虽然不报维度问题,但loss计算出来就是乱的。
解决:检查train_x是否做了double转换和除以255。在main.m里加一行min(train_x(:))和max(train_x(:))的打印,确认像素范围真的在0到1之间。另外在进入训练循环前打印一次初始loss,如果初始值就不是log(10)附近,说明数据或标签已经有问题了,别急着调学习率。
5.2 准确率有90%以上,但实际上全在预测同一个数字
现象:评估打印的准确率看起来还行,但抽几张图看预测结果,发现所有输入都输出同一个数字,准确率来源于测试集里恰好那个数字占的比例高。
原因:这是典型的类别不均衡叠加梯度方向跑偏。输出层某个神经元的偏置b2被初始化成偏大的值,softmax的输出被它主导,梯度更新没有把偏置拉回来。还有一个隐蔽来源:onehot_encode的labels + 1写成了labels,导致标签整体偏移,网络学到的是错位的对应关系,测试时却因为同样的偏移"碰巧"对了。
解决:检查b2初始化是否真的为全零,检查onehot的偏移。别只看整体准确率,把每个类别的单独准确率打印出来,比如用histcounts统计pred和test_y的分布。我习惯在评估后随机挑10张测试图,把原图、真实标签、预测标签并排打印出来,这一步基本能暴露所有"假准确率"。
5.3 训练集loss正常下降,测试集准确率却越来越低
现象:训练集的loss漂亮地一路下降,测试准确率却停滞甚至下滑。
原因:网络过拟合了。隐藏层神经元数量过大,或者epoch数太多,网络记住了训练集的具体像素分布而不是数字的通用模式。MNIST本身是相对干净的数据集,但隐藏层拉到300以上时过拟合现象就会显现;epoch超过20后,训练集loss能降到0.01以下,测试集却开始回落。
解决:两个思路——减少网络容量,把隐藏层从100降到64;或者加早停,每训练一个epoch都算一次测试准确率,连续两个epoch不再上升就停止。最简单有效的还是控制epoch数量,5到10轮之间通常是最合适的区间。如果想进一步提升泛化,可以在loss里加一个L2正则项:loss = loss + lambda * sum(W1(:).^2) / 2,lambda取1e-4量级。
5.4 MATLAB报错:矩阵维度必须一致
现象:运行到某一阶段直接报Matrix dimensions must agree,stack trace指向train_bp内部某一行。
原因:最常见的两种——X的列数不是784,比如reshape时行列写反了,导致X变成60000行60000列;或者Y的列数不是10,比如onehot_encode传入的num_classes参数写错成784。还有一种是train_x在归一化后仍保持了uint8类型,后续矩阵乘法结果类型异常,在某个比较操作时报错。
解决:在main.m里加断言,用assert(size(train_x, 2) == 784, '输入维度不对')和assert(size(train_y_oh, 2) == 10, '标签维度不对')。这类维度错误MATLAB指向的往往不是根源,而是第一个维度不匹配的乘法位置,所以最有效的排查方式是往前查所有reshape和拼接操作,逐个用size()打印当前矩阵维度。
5.5 内存不足,训练中途Out of memory
现象:训练到第二或第三个epoch时MATLAB抛出Out of memory,尤其是在配置不高的机器上。
原因:60000张784维图像转成double后是60000乘以784乘以8字节,约等于376MB。前向传播里Z2、A2、Z3、A3四个中间矩阵全保存在工作区,加上梯度矩阵和原始数据,总内存占用可能超过2GB,老机器一下就爆了。
解决:把train_x转成single类型,内存直接减半,准确率几乎没有损失——MNIST这种任务对精度本就不敏感。更进一步可以用小批量训练,每次只喂2000个样本,内存占用直接降一个数量级,代价是训练循环里多一层for。特别提醒,single类型下保存的权重在后续可视化或部署时要转回double,否则imshow这类函数会报类型错误。
6. 二次开发:把784维BP网络改造成你需要的识别模型
工程跑通只是第一步,真正的价值在于改。这一章给三个最常用的改造方向,每个都可以在半小时内完成。
6.1 把sigmoid换成ReLU加速收敛
把sigmoid.m和sigmoid_deriv.m替换成ReLU版本,初始化改成He初始化:
function y = relu(z) y = max(0, z); end function d = relu_deriv(z) d = double(z > 0); endtrain_bp里把sigmoid(Z2)换成relu(Z2),sigmoid_deriv(Z2)换成relu_deriv(Z2),初始化范围从均匀分布换成randn * sqrt(2 / input_size),学习率从0.5调到0.05。ReLU的收敛速度通常比sigmoid快两到三倍,但要记住学习率必须调小,否则大量神经元会一起死在零输出,loss再也不动。
6.2 复用代码到自定义数据集
如果换成32x32的灰度字符数据集,只需要把input_size改成1024,再把load_mnist换成自己的图像读入函数,训练、评估代码一行都不用动。最容易翻车的地方是标签偏移——如果数据集标签从1开始而不是从0开始,onehot_encode里的labels + 1要改成labels;如果输出类别不是10个,output_size和onehot_encode的num_classes要同步改。
6.3 可视化隐藏层权值
把W1的每一列reshape成28x28,用imagesc就能直观看到网络学到的东西,这一步最适合用来验证网络是否真的在学特征而不是死记硬背像素位置。从那以后,我每次在MATLAB里改网络结构,都强制先跑一遍梯度检查、再比较改动前后的准确率、最后保存本次权重快照,翻车了直接load回来重试。希望帮到你,少走我走过的这些弯路。
本文还有配套的精品资源,点击获取