如果你现在正在“吴恩达机器学习课后第一次编程作业”里打转觉得 Octave 环境装不明白、梯度下降写着写着就报维度不对、甚至想不通这个线性回归到底在干嘛那很正常。这不是你一个人遇到的问题基本上每一个自称“学过机器学习”的人都在这份作业面前卡过壳。我当时做这份作业的时候也是踩着坑过来的而且算是踩得比较彻底的那种环境装了三遍、ex1.m 跑一遍报一遍错、submit 永远交不上最后才发现问题出在哪。所以这篇文章想好好聊聊这份第一次编程作业它到底想让你学会什么哪些地方最容易把人劝退以及如果你正卡在某个步骤应该怎么一步步走出来。我要说的是这份作业的价值不在“跑通”本身而在于它逼你完成一次从数学公式到可运行代码的完整翻译。这是很多自学的人最缺的一步——课能看懂公式能推导但一打开编辑器就不知道从哪里下手。这份作业就是来治这个病的。1. 这道作业卡住所有人的地方其实不在算法上先说结论第一次编程作业的算法知识并不难就是线性回归加梯度下降吴恩达在课程里已经讲得非常细了。真正把绝大多数人挡在门外的是三件事环境装不明白、Octave/MATLAB 语法不熟、submit 自动评分机制摸不透。1.1 为什么课程要选 Octave/MATLAB而不是 Python很多新手一上来就问能不能用 Python 做能网上有大把 Python 版实现而且写得也很好。但我的建议是第一次作业尽量还是老老实实按课程原版来用 Octave 或者 MATLAB。原因很直接。吴恩达这门课的设计宗旨是“让你专注于算法本身而不是工程细节”。Python 生态里光是装 NumPy、pandas、matplotlib、Jupyter 就能劝退一批人而且版本兼容问题一层套一层。Octave 则是为了教学专门优化过的工具打开就能跑语法上和 MATLAB 几乎一致这份作业的所有测试脚本、submit 工具都是基于 MATLAB/Octave 设计的你不需要额外折腾任何东西。更重要的是课程最后的 submit 评分系统只认 Octave/MATLAB 的脚本格式。如果你用 Python 重写作业能做出来但没办法走官方评分通道等于你白白放弃了自动判分这个验证机制。第一次做这份作业的时候最需要的就是即时反馈——写完了提交一下马上知道对不对。1.2 环境安装中最容易被忽略的版本坑安装 Octave 这件事本身不难但有个坑很典型别装太老的版本也别装那种“精简版”“绿色版”。我当时用的是 Windows 系统第一次随便下了个 4.2 版本的 Octave结果跑作业里的 plot 命令时图形窗口总是不稳定后来发现是版本太老对高分屏支持不好。建议直接上 Octave 6.x 或更新的版本官方下载页选那个 64 位的 .exe 安装包就行。安装路径不要带中文、不要带空格比如直接装到 D:\Octave后面很多莫名其妙的问题都能避免。装完之后打开 Octave CLI 或者 GUI第一件事不是写代码而是在命令行里敲一句 version确认当前版本号。如果版本号正常再敲一下 pkg list看看有没有什么报错。这一步能帮你在正式开工前就排除掉一大半环境问题。2. 一个很容易忽略的准备工作搞清楚这个作业的整体流程很多同学拿到作业包第一反应就是打开 ex1.m 直接点运行然后被一堆报错砸晕。实际上这份作业的包结构设计得很有教学意图花五分钟把流程看清楚比你瞎折腾半小时有效得多。2.1 作业包里到底有哪些文件它们各自干什么解压作业包之后你大概会看到这些文件ex1.m主脚本所有任务都从这里驱动你不需要改这个文件但需要理解它的每一段在做什么ex1data1.txt单变量线性回归的数据集第一列是城市人口第二列是快餐车利润ex1data2.txt多变量线性回归的数据集第一列是房屋面积第二列是卧室数量第三列是房价plotData.m一个需要你自己补全的函数负责把数据可视化computeCost.m需要补全的代价函数gradientDescent.m需要补全的梯度下降函数featureNormalize.m多变量回归部分需要的特征归一化函数normalEqn.m正规方程求解函数submit.m 和 submitWeb.m作业提交入口登录课程网站账号后可以自动评分。我第一次打开这个包的时候其实有点懵因为我一直在找“作业题”结果发现没有一题一题的任务列表只有一堆分散的函数文件。后来才反应过来这门课的设计理念就是让你以“完善一个项目”的方式去写代码。每个函数都是一个独立模块ex1.m 是总指挥你补全模块它帮你串起来跑。2.2 先通读 ex1.m再动笔写函数这是一个非常重要的实操习惯。ex1.m 开头的注释部分完整描述了作业要求包括你要实现哪几个函数、每个函数应该返回什么类型的数据、可视化应该画成什么样。不看这些直接去写函数很容易出现函数名对不上、变量维度不对、返回参数顺序错乱这些问题。我把 ex1.m 的核心流程给你捋一下以单变量部分为例用 load 读取 ex1data1.txt 数据把数据切分成 X特征和 y标签在 X 左边加一列全 1作为截距项设置学习率 alpha 0.01 和迭代次数 iterations 1500初始化 theta 为全零向量调用 plotData 画散点图调用 computeCost 计算初始代价验证结果是否为 32.07 左右调用 gradientDescent 训练模型得到优化后的 theta绘制线性拟合结果并预测 3.5 万人口和 7 万人口城市的利润。看到这个流程你就明白了你其实是在写几个被主脚本调用的“零件”不是写整套程序。这也是这份作业最有价值的地方——训练你模块化思考问题的能力。工程上真正的代码全都是这种组织方式。3. 从数学公式到代码手把手拆解单变量线性回归的每个函数现在进入正题每个函数到底怎么写为什么这么写有哪些坑在里面。3.1 plotData.m别小看可视化这里有一半人的挂科点plotData.m 要完成的很简单把训练集数据里的点画出来x 轴是城市人口y 轴是利润。核心就是一段 plot 命令plot(x, y, rx, MarkerSize, 10); xlabel(Population of City in 10,000s); ylabel(Profit in $10,000s);但这个函数里有几个细节值得注意第一个是 x 和 y 的来源。ex1.m 在调用 plotData 之前已经把数据切分好了第 1 列作为 X第 2 列作为 y。然后还会给 X 加一列全 1。这里很多同学第一次会搞混plotData 里接收到的 X 已经是加了截距项的版本所以如果你在 plotData 里直接用 X 的原始第一列画图画出来的其实是截距项那一列散点图会变成一条垂直线严重误导你对数据的判断。正确的做法是在 plotData 里取 X 的第二列也就是原始特征列plot(X(:, 2), y, rx, MarkerSize, 10);第二个是图形窗口的显示问题。在 Octave 里如果直接跑 ex1.m图形窗口可能会一闪而过或者根本不显示。这时候需要在脚本开头加上 figure; 打开一个新的图形窗口或者在命令行里执行 graphics_toolkit(qt) 指定图形后端。ex1.m 里其实已经有 figure 的调用但如果你是自己写脚本测试 plotData记得自己加。第三个坑比较隐蔽如果你在 Windows 上Octave 的图形窗口有时候会显示中文乱码因为默认字体对中文支持不好。解决办法是在绘图命令后加一句set(gca, FontName, Arial);3.2 computeCost.m矩阵运算一行搞定但维度思维很重要computeCost 的任务是计算给定 theta 下的代价函数 J。公式是J(theta) 1/(2m) * sum((h(x^i) - y^i)^2)其中 h(x) X * theta。新手最容易卡在“为什么要用矩阵乘法而不是 for 循环”。这里要建立起一个概念在 Octave/MATLAB 里矩阵运算是矢量化的效率远超 for 循环而且代码更简洁。正确的实现是function J computeCost(X, y, theta) m length(y); predictions X * theta; errors predictions - y; J 1 / (2 * m) * sum(errors .^ 2); end几个容易出错的细节predictions 是 X 乘以 theta这一步用的是矩阵乘法不是点乘 .。因为 X 是 m×n 维theta 是 n×1 维两者是标准的矩阵乘法errors .^ 2 用的是逐元素平方所以必须是 .^ 而不是 ^。如果你写成 errors^2Octave 会尝试做矩阵乘法大概率会报维度错误sum 是对向量所有元素求和。也可以写成 errors * errors效果一样但可读性差一些不推荐新手用。写完这个函数之后ex1.m 里会有一个验证点用初始 theta zeros(2, 1) 去计算代价正确结果是大约 32.07。这个数字就是你的“第一个里程碑”。如果你的结果不是 32.07大概率是公式写错了或者 X 和 y 的维度关系不对。我见过一个比较常见的错法是有人把实现写成了J 1 / (2 * m) * sum((theta * X - y) .^ 2);这个写法维度上也能通但代码可读性很差后面调试起来非常痛苦。这里我想强调一个习惯代码的正确性不只是“能跑”还要“好读”。机器学习项目里代价函数这种核心模块如果写得绕来绕去后面调试梯度下降的时候理不清。3.3 gradientDescent.m梯度下降的全部秘密都在同步更新里接下来是重头戏 gradientDescent。公式是theta_j : theta_j - alpha/m * sum((h(x^i) - y^i) * x_j^i)用矩阵形式写就是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 predictions X * theta; errors predictions - y; theta theta - (alpha / m) * (X * errors); J_history(iter) computeCost(X, y, theta); end end这段代码里有几个地方需要特别注意。第一个是 theta 同步更新。你可能在课程里学过所有 theta_j 要同时更新不能先更新 theta_0 再用更新后的 theta_0 去算 theta_1。在矩阵形式里这个约束天然就被满足了——因为 theta 的更新只用到了更新前的 theta 来计算 errors一次性算出所有方向上的偏导再统一更新。但如果你想写成逐特征更新的形式就必须用临时变量存旧值。第一次写代码的时候建议仔细想想这一层逻辑。第二个是 X * errors 这一步的几何含义。X 的维度是 n×merrors 是 m×1乘出来刚好是 n×1也就是每个特征对应的梯度方向。这里不需要你自己去推导维度但建议你在纸上画一下理解这一步到底算的是什么。第三个是 J_history 的存储。这个向量记录了每一轮迭代后的代价后面画图时要用它来看收敛情况。这里有个细节每次迭代都要重新调用 computeCost 函数会增加运行时间但这是课程特意要求的——让你直观看到代价在下降。如果你自己实现梯度下降做研究不一定要每轮都算代价可以每隔几十轮算一次。我当时第一次跑这个函数的时候结果稍微有点偏差后来发现是忘记加 (alpha / m) 前面的括号了导致更新步长变成 alpha 除以整个后面那一大坨整个梯度下降直接发散。这种低级错误主要靠一个习惯来避免不要一行写完所有内容把每一步拆开、添加注释。% 1. 用当前 theta 计算预测值 predictions X * theta; % 2. 计算预测误差 errors predictions - y; % 3. 累加梯度并更新 theta theta theta - (alpha / m) * (X * errors);3.4 可视化拟合结果和预测这一步比你想的重要ex1.m 在 gradientDescent 跑完之后会画出拟合直线并用学到的 theta 预测两个新数据点。这里有个非常值得做的事情画出 J_history 随迭代次数变化的曲线确认代价在稳定下降。这是判断梯度下降是否正常工作最直观的方法。figure; plot(1:numel(J_history), J_history, -b, LineWidth, 2); xlabel(Number of iterations); ylabel(Cost J);如果这条曲线是下降的而且最后趋于平稳说明学习率和迭代次数选得没问题。如果曲线震荡或者一路上升那是学习率太大。如果下降非常缓慢说明学习率太小或者需要做特征缩放。新数据点的预测也一样简单关键是记得给输入特征加截距项predict1 [1, 3.5] * theta; predict2 [1, 7] * theta;这里我很想强调一点你的预测结果不只是一个数字它是一个可以让你理解“模型学到了什么”的窗口。比如 theta 算出来之后theta(2) 的含义是“每增加 1 万人口利润增加多少”。把 theta 的实际数值打印出来看一眼比单纯看拟合线更能加深对线性回归的理解。4. submit 才是第一道真正的门槛常见的提交失败原因全解当你把所有函数都写完、ex1.m 本地跑通之后接下来就是 submit。这一步理论上不难但它是一个“心态考验区”因为很多人的代码在本地完全没问题一提交就各种报错。4.1 submit 的登录机制打开 Octave在作业目录下输入 submit程序会提示你输入邮箱地址和提交密码。这个密码不是你的课程登录密码而是课程网站“Assignments”页面里给出的一个单独密钥。很多同学在这个环节吃亏因为不确定到底填什么、怎么填就一直卡着。正确的流程是登录 Coursera 课程页面如果你在用的话进入相对应的作业页面找到你的登录密码复制到 Octave 里。如果你用的是非 Coursera 的课程镜像资源submit 逻辑可能不完全一样建议以课程本身的说明为准。我第一次做的时候因为这个密码问题折腾了很久。后来发现其实只需要注意大小写和末尾的空格就没问题。这种事说起来很小但真的很打击士气。4.2 常见提交报错的定位思路面试中有个经典的问题“你在做作业的时候遇到最棘手的 bug 是什么”很多人说的都是 submit 的报错。submit 报错主要有几类第一类是文件名不对。作业要求你补全 computeCost.m但你把文件命名成了 ComputeCost.m或者保存到了错误的目录。Octave 在调用函数时按文件名匹配大小写不一致会导致找不到函数。第二类是函数返回值的数量和顺序不对。比如 computeCost 应该是返回一个标量 J但你多返回了一个东西submit 脚本就会提示“error: computeCost: operator *: nonconformant arguments”。这种报错往往指的是你的函数内部逻辑溢出而不是 submit 本身的问题。第三类是内置函数名冲突。我在论坛里见过有人把自己的一个变量命名成了 sum结果后面所有 sum 调用全报错。Octave 对变量名和函数名不做严格区分这种遮蔽问题非常隐蔽。4.3 submit 通过不代表万事大吉提交后依然要自查submit 全过只说明你的输出值满足期望但你的代码风格、是否用了循环、可读性如何它管不着的。比如作业要求用矩阵形式写 computeCost但你用了 for 循环结果能算对submit 也能过但这不符合课程的教学意图。遇到这种情况建议还是改写成向量化形式因为后续作业的规模更大靠 for 循环根本跑不动。5. 多变量线性回归特征归一化才是重头戏作业的第二部分是多变量线性回归数据是房屋面积和卧室数量预测房价。这部分和单变量的核心区别就在于多了一个特征并且两个特征的量纲差异巨大——面积动辄几百上千卧室数量只有个位数。如果不做归一化梯度下降会非常慢甚至不收敛。5.1 featureNormalize.m归一化的正确公式归一化的标准步骤是对每个特征减去均值除以标准差。在 Octave 里写function [X_norm, mu, sigma] featureNormalize(X) mu mean(X); sigma std(X); X_norm (X - mu) ./ sigma; end这里的坑主要在两个地方一是用 std 还是用 range。有的教材里用 max - min 作为分母吴恩达的课程里明确用了标准差。你可以回想一下课程的思路因为后续正规方程的推导与此相关。二是广播机制。X 是 m×nmu 是 1×nX - mu 在 Octave 里会自动做广播即每一行都减去同一个均值向量不需要手动复制。但如果你是从 Python 转过来的注意这里的写法逻辑和 numpy 是不同的别把两边的习惯搞混。做完归一化之后预测新数据时必须使用训练集的 mu 和 sigma。这是很多人会记错的点如果你对新数据自己做了一个归一化结果会错。一定要用训练集算好的 mu 和 sigma 去归一化新数据再丢进模型预测。5.2 学习率的选择用代价下降曲线验证你的判断多变量回归的学习率需要自己试。你可以试着跑 alpha 0.01、0.03、0.1 等几种情况每轮迭代都记录下代价然后画出来对比。看曲线的下降速度不是越小越好也不是越快越好。核心追求是收敛快且稳定。一个很实用的操作是写一个小脚本循环跑多种学习率把不同 alpha 下的 J_history 画在同一张图里标注不同的颜色。这样一张图就能直观看出哪一种学习率“既快又稳”。我第一次做的时候直接用了 0.01结果发现收敛很慢画出来的代价曲线下降得像蜗牛。后来换成 0.3下降速度立竿见影。重要教训不要拿到参数就用先观察再判断。5.3 正规方程部分为什么它不用归一化也能算作业的第三部分是正规方程。公式一行就够function [theta] normalEqn(X, y) theta pinv(X * X) * X * y; end或者用反斜杠运算符写更简洁theta X \ y;这部分的重点是理解为什么正规方程不需要归一化因为它的计算本质上是一次性矩阵求逆没有“逐步下降”的过程所以量纲差异不会带来数值不稳定的问题在特征数量不大的情况下。那为什么不总是用正规方程因为当特征数量非常大时矩阵求逆的复杂度会变得难以接受。作业里会给一个对比用正规方程预测一个 1650 平方英尺、3 个卧室的房价应该和前面梯度下降得到的结果非常接近。如果你的结果差得特别远说明前面归一化或者预测步骤有 bug。6. 我踩过的坑和给你的验收清单代码能跑只是起点最后这部分我想分享几个特别具体的实操经验和踩过的坑希望能帮你少走弯路的。6.1 环境相关第一是 Octave 每次启动都要重新跑脚本。很多人做完 ex1.m 之后关掉 Octave第二天打开直接敲 submit发现报错说函数找不到。这是因为你现在的工作目录不是作业目录。用 cd 切过去或者直接在命令行里跑 pwd 看当前路径。第二是文件保存的编码问题。Windows 上如果默认用记事本编辑 .m 文件容易出现编码不兼容。建议用 VS Code 或者 Notepad 打开把编码设置为 UTF-8。第三是 Octave 里的 m 文件里如果写了中文注释在某些系统上会因为编码问题报错或显示乱码。如果你非要写中文注释保存的时候确认编码格式或者干脆都用英文注释。6.2 代码实现相关第一个经验写好一个函数立刻单独验证不要等所有函数都写完再一起调。比如你写完 computeCost就用 ex1.m 里给定的初始 theta 算一算看是不是 32.07。如果不对马上停下来修正。这样定位 bug 的成本最低。第二个经验用 disp 或 fprintf 打印中间变量是调试机器学习代码最基础但最有效的手段。不要觉得打印日志土。我后来做深度学习调模型的时候依然靠打印 loss、梯度范数、参数分布来定位问题。第三个经验理解每一个变量的维度。做完第一次作业之后你最好能闭着眼睛写出 X 是 m×n、theta 是 n×1、h 是 m×1、errors 是 m×1、梯度是 n×1。这一步想通了后续逻辑回归、神经网络作业里的维度问题都难不倒你。6.3 验收清单做完整个第一次作业下面这些点如果你都能达到说明你掌握得比较扎实[ ] ex1.m 能从头到尾无报错运行[ ] computeCost 用初始 theta 计算结果为 32.07[ ] 代价曲线随迭代次数单调下降并趋于稳定[ ] 用梯度下降得到的 theta 和正规方程得到的 theta 高度一致[ ] 归一化后的特征均值为 0、标准差为 1[ ] submit 全部通过[ ] 合上代码文档能自己独立写出大致的矩阵实现。这份作业其实没有多难难的是它要求你同时调动数学理解、编程实现和工具使用三种能力。很多人在这一步就放弃了觉得“机器学习不适合我”。但实际上跨过这个坎之后后面的逻辑回归、神经网络你都会觉得顺畅很多因为套路都是一样的分析维度、写好函数、画图验证、提交检查。我在做第一次作业的时候最深刻的体会是机器学习的学习曲线不是爬山而是过台阶。每一个台阶上去的时候都费劲但上去了之后回头看一切都变得理所当然。第一次作业就是这个台阶的基石搞清楚它是值得的。