
简介这是吴恩达机器学习课程的MATLAB编程作业合集面向正在啃理论但动手困难的学习者也适合用MATLAB实现经典算法的数据科学入门者。压缩包约14.64MB共109个文件以79个.m脚本为主配合mat数据文件与txt说明另含md文档方便边读边跑、对照理解。已有935人学习下载属于课程作业中少见的完整沉淀。内容覆盖线性回归、逻辑回归、前向神经网络和BP神经网络四大实验每个实验都涉及建模、训练、评估与可视化能帮你把梯度下降、Sigmoid、损失函数、反向传播等概念落到代码层面。通过阅读这些代码还能掌握MATLAB的矩阵操作、fitlm等函数用法以及plot调试技巧适合作为课后复习、作业参考或教学示例使用。1. 从吴恩达作业到 MATLAB 复现这套代码到底在训练什么如果你下载过吴恩达机器学习课程的 MATLAB 编程作业包会发现里面除了 ex1 到 ex4 的主脚本还有一批反复出现的loadjson.m、savejson.m、saveubjson.m文件——它们不是作业本身而是配套的数据序列化工具用来在 MATLAB 与外部环境之间交换训练数据。整个作业包的核心是通过四个递进实验让你亲手把线性回归、逻辑回归、前向神经网络和 BP 反向传播从公式变成可运行的向量化代码。对 5 年以上的工程师来说这套作业的价值不在语法而在“用矩阵运算替代 for 循环”以及“梯度检查”这一整套调试方法论至今仍在使用。适合的人群是准备系统复习机器学习基础、想把理论公式落成 MATLAB 实现、或者在 TensorFlow/PyTorch 之外想理解底层数值优化过程的从业者。2. 线性回归与逻辑回归代价函数、梯度与向量化实现2.1 从正规方程到梯度下降为什么作业里两者都要写ex1 的线性回归表面上是拟合h(x) θᵀx但真正的训练点在于用正规方程一步求出最优参数再用梯度下降验证同一个解。你会在ex1.m里看到两种路径的对比这对应了实际生产中“数据量小直接闭式解、数据量大走迭代优化”的选型逻辑。MATLAB 里用fitlm三行代码就能完成拟合但作业要求的手写实现能让你看清每一步在算什么。核心代价函数function J computeCost(X, y, theta) m length(y); h X * theta; % 假设函数矩阵乘法一次算出所有样本的预测值 J 1/(2*m) * sum((h - y).^2); % 均方误差除以2是为了求导时消掉系数 end这段代码里X * theta是向量化实现的关键X是 m×n 的特征矩阵theta是 n×1 的参数向量矩阵乘法本身替代了内层的求和循环。sum((h-y).^2)计算所有样本误差平方之和1/(2*m)是 MSE 的归一化。注意如果不除以m梯度下降的学习率 α 就需要跟着样本量缩放否则大样本下梯度会爆炸。对应的梯度下降更新函数function [theta, J_history] gradientDescent(X, y, theta, alpha, num_iters) m length(y); J_history zeros(num_iters, 1); for iter 1:num_iters h X * theta; theta theta - alpha/m * (X * (h - y)); % 向量化梯度更新 J_history(iter) computeCost(X, y, theta); end end这里的核心是X * (h - y)它一次完成了所有特征的梯度累加X是 n×m(h - y)是 m×1乘积结果就是 n×1 的梯度向量。alpha是学习率作业里通常建议从 0.01 开始试如果J_history震荡不下降就把alpha缩小十倍。2.2 特征归一化的边界条件ex1 里有一个容易被忽略的细节featureNormalize.m对特征做均值归零和标准差缩放。在房价预测数据里面积几百到几千和卧室数量1 到 5量纲差异极大如果不归一化代价函数的等高线会呈狭长椭圆形梯度下降会走锯齿路径收敛极慢。function [X_norm, mu, sigma] featureNormalize(X) mu mean(X); % 每个特征的均值1×n 向量 sigma std(X); % 每个特征的标准差 X_norm (X - mu) ./ sigma; % 广播运算每列减去均值再除以标准差 end注意./是逐元素除法不是矩阵除法。mu和sigma必须保存下来测试新样本时要用训练集的均值标准差做同样的变换不能用测试集的。这是线上推理时最容易出错的地方——归一化参数只应该从训练集统计。2.3 逻辑回归的正则化与多分类视角ex2 把问题切到了分类用 Sigmoid 函数把线性输出映射到 (0,1) 区间作为正类的概率。代价函数变成function [J, grad] costFunctionReg(theta, X, y, lambda) m length(y); h sigmoid(X * theta); theta_reg theta(2:end); % 正则化项不惩罚 θ0偏置项 J 1/m * sum(-y .* log(h) - (1-y) .* log(1-h)) lambda/(2*m) * sum(theta_reg.^2); grad 1/m * X * (h - y); grad(2:end) lambda/m * theta_reg; % 梯度中同样加入正则化项 end这里的lambda是正则化强度。λ 太小会过拟合决策边界贴着训练样本走λ 太大则欠拟合决策边界退化成一条直线。作业里 ex2_reg 的部分就是让你观察这个权衡。优化器用fminunc而不是手写梯度下降这是因为fminunc内部的拟牛顿法BFGS对学习率不敏感收敛更快——这也是实际项目里优先选成熟优化器的原因。options optimset(GradObj, on, MaxIter, 400); [theta, cost] fminunc((t)(costFunctionReg(t, X, y, lambda)), initial_theta, options);(t)是匿名函数把t作为唯一入参传给代价函数GradObj, on告诉优化器你同时提供了梯度函数这样fminunc会走更高效的梯度下降路径。如果你只给代价不给梯度fminunc会用有限差分近似梯度慢一个数量级。ex2 还要求绘制决策边界做法是从 0 到 1 生成网格点用训练好的 θ 计算每个网格点的预测概率再用contour画概率等于 0.5 的等高线。这个技巧在论文里的效果图制作中仍然常用。3. 前向神经网络的参数结构与矩阵运算3.1 为什么需要隐藏层从线性边界到非线性边界逻辑回归的问题在于决策边界只能是线性的或经过特征工程后的多项式曲面。ex3 引出手写数字识别0-9这是一个 10 分类问题像素级别的特征线性不可分需要隐藏层做非线性变换。作业里给出的网络结构是 400-25-10输入层 400 个节点20×20 像素展开隐藏层 25 个节点输出层 10 个节点。function p predict(Theta1, Theta2, X) m size(X, 1); a1 [ones(m, 1) X]; % 输入层加偏置变成 m×401 z2 a1 * Theta1; % Theta1 是 25×401转置后做矩阵乘法 a2 sigmoid(z2); a2 [ones(m, 1) a2]; % 隐藏层加偏置变成 m×26 z3 a2 * Theta2; % Theta2 是 10×26 a3 sigmoid(z3); [~, p] max(a3, [], 2); % 取每行最大值所在的列索引即预测类别 end这段代码的核心是[ones(m,1) X]把偏置项拼进特征矩阵——在神经网络里偏置相当于输入恒为 1 的额外节点。Theta1转置的原因是作业里存储参数用的是“层-节点”维度每行是一个目标节点而矩阵乘法要求特征维度对齐。max(a3, [], 2)的[]表示沿行方向取值2指定维度——如果写成max(a3)默认沿列方向会得到每列的最大值维度就错了。3.2 参数展开与维度对齐的坑ex3 给出了预训练好的Theta1、Theta2你不需要训练只需要实现前向传播做预测。但这恰恰引出了 ex4 最重要的工程问题fminunc要求优化变量是一个向量而神经网络的参数是多个矩阵所以必须做展开unrolltheta_vec [Theta1(:); Theta2(:)]; % 把两个矩阵拉直拼接成列向量 % 训练后再 reshape 回原维度 Theta1 reshape(theta_vec(1:25*401), 25, 401); Theta2 reshape(theta_vec(25*4011:end), 10, 26);(:)是 MATLAB 里最容易被忽视的向量化操作它按列把矩阵展开成向量。这里必须严格记录每段参数的边界否则 reshape 回去的时候维度错位前向传播的结果全是 NaN。常见做法是把维度信息存成常量input_layer_size 400; hidden_layer_size 25; num_labels 10;然后用Theta1 reshape(nn_params(1:hidden_layer_size * (input_layer_size 1)), hidden_layer_size, (input_layer_size 1))这类带注释的代码做切分。实际项目里我会把这个参数切分过程单独封装成一个函数避免在训练脚本里散落魔法数字。3.3 预测准确率的验证口径ex3 最后用训练集做回判准确率通常能到 97% 以上。这里隐藏着一个统计陷阱用训练集评估模型表现会偏高真正的模型能力需要用验证集或测试集来衡量。作业没有划分测试集是因为数据量本身小5000 张但在真实项目里你应该把数据按 60/20/20 划分训练、验证、测试三份。另一个容易踩的坑是max(a3, [], 2)返回的类别索引是 1 到 10而数字 0 被映射到了索引 10——作业的y向量里 0 被替换成了 10这是 MATLAB 索引从 1 开始导致的映射偏移。如果你用 Python 复现注意把 10 映射回 0。4. BP 反向传播误差传播、梯度检查与训练细节4.1 从链式法则到误差项的具体含义ex4 要求你从零实现反向传播。前向传播逐层计算z和a反向传播逐层计算误差项δ。第 L 层的误差项定义为代价函数对该层加权输入的偏导数实际计算时从输出层往输入层反向推导% 前向传播缓存中间结果 a1 [ones(m,1) X]; z2 a1 * Theta1; a2 sigmoid(z2); a2 [ones(m,1) a2]; z3 a2 * Theta2; a3 sigmoid(z3); % 输出层误差预测值减真实值one-hot 编码下正好等于 a3 - y delta3 a3 - y_encoded; % 隐藏层误差反向传播Theta2 去掉偏置列 delta2 (delta3 * Theta2(:, 2:end)) .* sigmoidGradient(z2);delta3 a3 - y_encoded是一个美妙的结果当输出层使用 Sigmoid 激活且代价函数是交叉熵时输出层误差恰好等于预测减真实值。sigmoidGradient的公式是g(z) g(z) .* (1 - g(z))也就是激活值乘以其补数。Theta2(:, 2:end)去掉偏置列是因为偏置节点没有“上一层输入”不需要参与误差回传。写不出 sigmoidGradient 的时候最容易犯的错误是把它当成常数 1这会让梯度方向走偏训练发散。手动验证一下z0 时g(0)0.5梯度 0.25这是最大值z 很大或很小时梯度趋近于 0这就是梯度消失的雏形——深度学习里用 ReLU 替代 Sigmoid 正是因为它的导数在正区间恒为 1不会产生梯度消失。4.2 梯度检查唯一可靠的调错手段BP 实现完成后肉眼检查数值几乎不可能发现错误。作业提供了一个checkNNGradients.m脚本用数值梯度对比解析梯度% 数值梯度让每个参数 ± epsilon计算代价函数变化率 numgrad zeros(size(theta)); perturb zeros(size(theta)); epsilon 1e-4; for p 1:numel(theta) perturb(p) epsilon; loss1 J(theta - perturb); loss2 J(theta perturb); numgrad(p) (loss2 - loss1) / (2*epsilon); perturb(p) 0; end % 对比 numgrad 与 backprop 计算的 grad差别应 1e-9epsilon取 1e-4 是常用的折中——太小会引入浮点误差太大会让斜率近似失真。数值梯度本身很慢numel(theta)是 10285全量检查要跑两万多次代价函数作业里是用一个 3 层小网络做的快速验证。实际项目里我只会检查一小部分参数比如前 10 个确认误差在1e-9量级就停因为数值梯度只是验证反向传播的梯度方向是否正确的工具。4.3 随机初始化 vs 全零初始化如果所有参数初始化为 0BP 会让同一层的所有神经元学到完全相同的特征——权重对称问题。作业里的randInitializeWeights.m使用对称区间随机化function W randInitializeWeights(L_in, L_out) epsilon_init sqrt(6) / sqrt(L_in L_out); % 以 Xavier 初始化的简化版 W rand(L_out, 1 L_in) * 2 * epsilon_init - epsilon_init; endsqrt(6)/(L_inL_out)的取值来自 Xavier 初始化的推导保证每层输入输出方差大致相等避免激活值出现饱和。rand(L_out, 1L_in)生成 [0,1] 均匀分布*2*epsilon - epsilon平移到 [-ε, ε]。这里的偏置列1L_in 多出的那一列同样随机初始化不能设成 0——虽然偏置全零不会导致对称问题但在 ex4 的框架下统一处理更省事。4.4 超参数会话记录参考超参数作业取值调整方向说明隐藏层节点数25大→更强拟合小→抗过拟合ex4 可以用 50 对比测试正则化 λ1大→更平滑的边界从 0.01 到 10 做网格搜索最大迭代50大→更充分收敛注意代价函数是否平台期学习率不显式设置fmincg 内部—手写梯度下降时取 0.01~1作业里的fmincg是共轭梯度法变体比fminunc更适合小批量高维参数场景。你可以在ex4.m中把options optimset(MaxIter, 50)改成 200 观察代价下降曲线但小心别过拟合训练集。5. JSON 序列化、训练中断与常见报错的排查路径5.1 loadjson/savejson 到底在作业里扮演什么角色作业包里反复出现的loadjson.m、savejson.m、saveubjson.m是用于在不同编程语言之间交换数据的序列化工具。最初的课程作业从 Octave 迁移到 MATLAB 时需要把训练好的参数或预处理后的数据导出成 JSON 格式便于 Python 或 JavaScript 端做可视化验证。savejson的常见用法savejson(theta, theta, theta.json); % 保存单个变量 % 或者保存整个结构体 data.theta1 Theta1; data.theta2 Theta2; savejson(, data, model.json); % 空字符串表示保存整个结构体saveubjson是二进制版 JSON体积更小但可读性差日常调试不推荐。如果你的项目需要保存模型供他方调用优先用savejson搭配结构体如果只在本机 MATLAB 环境内使用直接save model.mat Theta1 Theta2更高效。5.2 训练中断的断点恢复思路fmincg不支持中间结果保存一旦训练中断所有迭代进度丢失。常见做法是手动保存每个 epoch 的参数save checkpoint.mat Theta1 Theta2 cost_history更符合工程习惯的是包装成带保存回调的训练循环每 10 次迭代检查一次代价函数下降幅度低于阈值就提前停止并保存。实际操作中另一个高频坑是代价函数出现 NaN——通常来自log(0)或sigmoid(z)在 z 极大时的下溢。应对方法是在 Sigmoid 实现里加截断function g sigmoid(z) g 1.0 ./ (1.0 exp(-z)); g(g 0 | g 1) eps; % 用 eps 代替 0 和 1避免 log 爆炸 end5.3 高频报错对照表报错信息成因处理手段Error using * Inner matrix dimensionsX 与 theta 维度不匹配检查是否漏加偏置列ones(m,1)NaN in cost functionexp(-z) 溢出或学习率过大给 sigmoid 加截断减小 alphafminunc stopped because it exceeded...迭代数不够调大MaxIter观察代价是否继续下降Out of memory直接loadjson超大文件改用流式解析或转成.mat遇到维度报错时我习惯先在脚本开头打印size(X)、size(theta)目测对齐再往下跑。数值类报错NaN/Inf则优先检查归一化——X存在极端离群值时梯度可能溢出这时把featureNormalize的均值置零改为中位数置零能提升鲁棒性。6. 从作业到项目复用的三个技巧完成 ex1 到 ex4 后把散在各脚本里的核心逻辑收拢成可复用的模块很有价值。第一个技巧是封装一个统一的预测接口把predict、featureNormalize、sigmod用到的所有预处理参数放在一个结构体里实现“训练/预测一条龙”。第二个技巧是保存处理链信息——包括归一化用的mu、sigma、网络结构维度和正则化参数加载模型时一并恢复避免预测阶段遗忘预处理步骤function pred predictWithModel(model, X_new) X_norm (X_new - model.mu) ./ model.sigma; pred predict(model.Theta1, model.Theta2, X_norm); end这里model是包含mu、sigma、Theta1、Theta2的结构体预测前必须先做同样的归一化变换。第三个技巧是代价函数的模块化组合设计将costFunction、sigmoidGradient、randInitializeWeights拆成独立文件换数据集时只改入口脚本和结构体配置。比如把input_layer_size、hidden_layer_size、num_labels、lambda汇总进一个配置脚本训练和预测都引用它这样整个流程的可维护性会高很多。这套从作业里长出来的组织方式放进任何后续的 MATLAB 或 Python 项目里都适用结构体管理配置、函数接口单一、预处理与模型参数同存。遇到新数据时改动只发生在配置层算法层不用动。本文还有配套的精品资源点击获取