吴恩达机器学习作业Python实战:逐周拆解核心算法与踩坑记录
简介这是一份面向机器学习入门者与吴恩达课程学习者的作业代码资源同时提供自写的Python实现与Matlab原版覆盖线性回归、逻辑回归、神经网络、支持向量机、异常检测、推荐系统等经典实验。压缩包共323个文件约71.86MB以169个m文件、57个txt说明、57个mat数据文件、21个ipynb笔记为主另有16个pdf和少量图片、Markdown文档便于对照阅读和运行调试。全包按课程练习模块清晰组织既适合验证理论也适合练习将算法从Matlab迁移到Python。目前已有2837人学习下载对于希望同时掌握两种主流机器学习工具、深入理解算法细节的读者是一份实用的动手参考资料。 一个老话题但每次都有新同学翻出来问吴恩达的机器学习课程作业到底该怎么练原版是Matlab/Octave网上虽然有各路参考实现但很多要么只贴代码不讲思路要么年代久远运行起来一堆报错。这篇把我自己重新用Python实现这套作业的完整过程写出来包含每一周的作业拆解、关键知识点、自写代码的思路以及踩过的坑。这不仅仅是给你一份能跑的代码更多是想聊聊每一步背后的原理和选型考虑。另外也顺手整理了Matlab原版留存的那些经典实验比如Octave自带数据集的那些练习毕竟很多学校考试和面试题依然直接参考这套作业。1. 这套作业到底在练什么先对还没入门的同学说清楚Coursera上吴恩达的机器学习课程Machine Learning不是Deep Learning Specialization是很多人的第一门ML课。它最大的特点不是理论深度而是“用最少的数学把整条链路走通”。整套课程一共9到10周作业不同年份略有差异覆盖了线性回归、逻辑回归、神经网络前向反向传播、正则化、偏差方差诊断、支持向量机、K-Means聚类、PCA降维、异常检测、协同过滤推荐系统。这些内容放在今天看依然是工业界最常用的基础组件。原版作业是Matlab/Octave脚本每个练习对应一个主入口脚本比如ex1.m、ex2.m里面调用了你自己要补全的函数。课程要求你补充的部分都是核心算法比如梯度下降的迭代步骤、代价函数的计算、反向传播的梯度计算等。为什么要用Python重写一遍原因很简单现在大多数公司和实验室的生产环境用的都是Python学完想真正用起来还是得切到Python生态。Octave虽然免费但调试体验、可视化、生态工具链和Python差距太大。用Python把同一套算法重写一次相当于把课程里的公式和伪代码亲手翻译成工程代码理解深度完全不一样。我用的是Python 3.8 NumPy Pandas SciPy Matplotlib核心算法全部手工实现没有调用sklearn。这样最大程度保留和Matlab原版的对应关系。后面每一周我分别说。2. 环境准备与工具链选型2.1 版本选择与依赖说明如果你是第一次搭环境别追求最新版本。推荐直接用Anaconda创建虚拟环境Python版本选3.8或3.9即可不用上3.11、3.12这种特别新的版本因为部分旧库可能存在兼容问题虽然现在大部分库都跟上了但没必要冒这个险。conda create -n ml_class python3.9 conda activate ml_class pip install numpy pandas scipy matplotlib jupyter这套组合足够跑完所有Python版作业。额外补充一句如果你之后想对比sklearn实现再单独装scikit-learn但初期不装反而有助于逼自己手写。2.2 为什么保留Matlab原版我保留了原版Matlab/Octave代码的思路而不是完全扔掉主要原因有两个。第一课程讲解里的图表、数值结果、交互演示全是基于Matlab生态的。比如ex1里的fi是plot如果不看原版很难理解课程视频里那张回归曲线的坐标轴是怎么配出来的。第二Matlab原版作业的官方评分结构非常清晰每个函数都有对应的测试用例submit作业时会跑用Python重写时可以直接对照这些测试用例来验证自己的实现是否正确。我实际的做法是把官方发布的ex1到ex8的官方包和Octave版放在一个reference目录里Python实现放在另一个python目录里。每写完一个函数就回去看Matlab的库函数版本是怎么写的对照一下边界条件。3. 逐周作业的Python实现拆解3.1 第1周线性回归单变量与多变量ex1是整套课程中最简单但最关键的切入。核心是梯度下降和正规方程。需要注意课程里的梯度下降分批量梯度下降Batch GD和随机梯度下降Stochastic GD两种作业要求实现的是批量梯度下降。单变量线性回归的假设函数def hypothesis(X, theta): return X theta代价函数Jdef compute_cost(X, y, theta): m len(y) h X theta return (1 / (2 * m)) * np.sum((h - y) ** 2)梯度下降更新公式def gradient_descent(X, y, theta, alpha, num_iters): m len(y) J_history [] for i in range(num_iters): theta theta - (alpha / m) * (X.T (X theta - y)) J_history.append(compute_cost(X, y, theta)) return theta, J_history这部分我把向量化实现放在第一优先级。刚入门时很容易写一个for循环逐步累加梯度但向量化不仅代码简洁更重要的是在矩阵运算层面效率高很多。整个课程里凡是能用矩阵乘法表达的不要去用循环。多变量线性回归的核心差别是特征缩放Feature Scaling。ex1后半部分要求对特征做均值归一化Mean Normalization我练习时用了StandardScaler的思路但自己动手实现了一遍def feature_normalize(X): mu np.mean(X, axis0) sigma np.std(X, axis0) X_norm (X - mu) / sigma return X_norm, mu, sigma这里有个坑Matlab的std函数默认是除以n-1样本标准差而NumPy的np.std默认除以n总体标准差。如果直接用np.std和原版结果会有细微偏差。我自己重写时选了除以n-1来对齐Matlabsigma np.std(X, axis0, ddof1)这个问题很典型真正去复现的时候才能遇到也提醒我跨语言复现代码对“默认值”特别敏感一个参数的差异可能导致后面代价函数曲线对不上。3.2 第2周逻辑回归与正则化ex2是分类问题。逻辑回归的假设函数是Sigmoid函数def sigmoid(z): return 1 / (1 np.exp(-z))但要注意数值稳定性。如果z是一个很大的负数np.exp(-z)会溢出反之如果z很大np.exp(-z)变成0结果接近1这没问题。用一个稳定的写法def sigmoid(z): z np.clip(z, -500, 500) return 1 / (1 np.exp(-z))逻辑回归的代价函数和梯度def cost_function_reg(theta, X, y, lambda_): m len(y) h sigmoid(X theta) reg (lambda_ / (2 * m)) * np.sum(theta[1:] ** 2) J (-1 / m) * np.sum(y * np.log(h 1e-8) (1 - y) * np.log(1 - h 1e-8)) reg return J注意两点第一正则化不惩罚theta[0]偏置项第二log里面加一个1e-8防止log(0)出现NaN。这在Python里比Matlab更常见因为Python浮点数在h取到1或0时输出的inf/nan不会自动处理。ex2最实用的是用scipy.optimize.minimize来替代手动梯度下降因为课程也讲了Matlab的fminunc函数。对应Python写法from scipy.optimize import minimize result minimize(funcost_function_reg, x0theta, args(X, y, lambda_), methodBFGS) theta result.x这里method选BFGS就够用不需要上L-BFGS-B因为没有边界约束。我自己测试下来BFGS收敛速度比写好的梯度下降快很多也更稳定非常适合用来验证手写梯度是否正确。3.3 第3~4周神经网络的前向传播与反向传播ex3和ex4是整套作业中比较重的两块。ex3要你实现前向传播预测手写数字ex4要你实现带正则化的反向传播计算梯度。前向传播相对简单def predict_nn(Theta1, Theta2, X): m X.shape[0] a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T a3 sigmoid(z3) return np.argmax(a3, axis1) 1 # 注意标签1~10反向传播才是真正的难点。很多同学卡在这里我觉得核心原因是课程视频里的一堆累加符号和局部梯度符号让人头晕。我重新梳理了实现步骤前向传播保存每一层的输入a和线性输出zz和a都要存因为求梯度时要用。计算输出层误差delta_3 a3 - y这里y是one-hot编码。反向传播到隐藏层delta_2 delta_3 * Theta2[:, 1:] .T * sigmoidGradient(z2)。注意Theta2去掉偏置列。累加梯度Theta1_grad delta_2.T a1Theta2_grad delta_3.T a2。最后除以样本数m再加上正则化梯度仍然不惩罚偏置项。sigmoidGradient的公式是def sigmoid_gradient(z): return sigmoid(z) * (1 - sigmoid(z))这里有个经验哪怕你完全相信公式也建议用课程提供的numerical gradient方法梯度检查gradient checking验证一遍。就是用数值差分近似梯度和反向传播算出来的梯度对比。这个方法能帮你一眼看出是UFLDL式的小错误还是整个计算图错了。3.4 第5~7周偏差方差、SVM与聚类降维ex5是最容易被忽视但实际收益极高的一周。它要求你画出学习曲线Learning Curves观察训练误差和交叉验证误差随训练集大小的变化来判断模型是偏差Bias问题还是方差Variance问题。用Python画出学习曲线的核心代码def learning_curve(X_train, y_train, X_val, y_val, lambda_): m len(X_train) errors_train [] errors_val [] for i in range(1, m 1): theta train_linear_reg(X_train[:i], y_train[:i], lambda_) errors_train.append(linear_reg_cost(theta, X_train[:i], y_train[:i], 0)) errors_val.append(linear_reg_cost(theta, X_val, y_val, 0)) return errors_train, errors_val这条曲线的作用我举例说明如果训练误差和验证误差都很高且曲线基本平行这就是高偏差欠拟合需要增加特征或降低正则化系数如果训练误差很低、验证误差高则可能是高方差过拟合需要增加样本或增大正则化系数。ex6的SVM部分Pyhton里虽然有sklearn的SVC可以一条命令搞定但我建议还是用课程里的线性核和高斯核概念结合一个简单的手写核函数来实现。高斯核的核心是计算两两样本之间的相似度def gaussian_kernel(x1, x2, sigma): return np.exp(-np.sum((x1 - x2) ** 2) / (2 * sigma ** 2))ex7的K-Means和PCA是最贴近数据可视化的一周。K-Means实现起来就是两步交替分配样本到最近的簇中心重新计算簇中心。PCA在Python里可以用np.linalg.svd实现这也是我当时熟悉SVD的好机会。SVD实现的PCA核心代码如下def pca(X): m, n X.shape Sigma (1 / m) * (X.T X) U, S, V np.linalg.svd(Sigma) return U, S, V然后通过U的列向量构造降维矩阵U_reduce把数据投影到低维空间。3.5 第8周推荐系统与协同过滤ex8是我个人觉得整套作业里最“工业感”的一周。它包含两个独立内容一是异常检测Anomaly Detection二是协同过滤Collaborative Filtering推荐系统。异常检测部分比较简单主要是计算高斯分布参数mu和sigma^2然后根据阀值epsilon判定哪些样本异常。注意在官方作业里epsilon是通过F1分数在交叉验证集上调出来的这个思想比算法本身更重要。协同过滤部分我认为是全课程代码量最大的部分。它的代价函数需要对用户特征矩阵X和物品特征矩阵Theta同时求梯度也就是两个梯度更新。核心实现def cofi_cost_func(params, Y, R, num_users, num_movies, num_features, lambda_): X params[:num_movies * num_features].reshape(num_movies, num_features) Theta params[num_movies * num_features:].reshape(num_users, num_features) error (X Theta.T - Y) * R J (1 / 2) * np.sum(error ** 2) J (lambda_ / 2) * (np.sum(Theta ** 2) np.sum(X ** 2)) X_grad error Theta Theta_grad error.T X X_grad lambda_ * X Theta_grad lambda_ * Theta grad np.concatenate([X_grad.ravel(), Theta_grad.ravel()]) return J, grad这里有个关键点R是一个0/1矩阵R[i][j]1表示用户j对电影i有评分。我们只计算有评分的样本的误差所以要用R把无评分的位置清零不能让无评分的地方反向传播出梯度。4. 常见问题与排查技巧实录4.1 矩阵维度不匹配最常见这个话题几乎每周作业都有人问。其实方法很简单在调试时每算完一个矩阵乘法就打印shape从输入一直追到最后。特别是神经网络那周a1的维度是(m, 401)加了一列1Theta1的维度是(25, 401)a2是(m, 26)Theta2是(10, 26)最后a3是(m, 10)。只要中间任何一步维度对不上马上打印出来看就能发现是哪里少了一列偏置或者多转置了一次。4.2 代价函数出现NaN或inf最常见的原因是log(0)。我在ex2逻辑回归里加了1e-8的平滑项另外也可以用np.clip对h值做截断。这个问题的根源是数值稳定性不是公式错误。4.3 梯度下降不收敛如果你的代价函数曲线随着迭代次数增加反而上升大概率是学习率alpha太大。课程建议尝试0.01、0.03、0.1、0.3、1.0这些值。我自己的经验是线性回归的数据如果做了特征缩放0.1基本是比较稳妥的选择如果没有做缩放收敛会非常慢甚至发散。4.4 PCA重构结果不对PCA降维后重构回去很多人发现重构数据和原数据差距大。常见原因是忘了对数据进行均值中心化或者忘了用原始数据的mu和sigma对新数据做同样的标准化。PCA对数据的中心化极其敏感所以务必保证训练集、验证集、测试集都使用同一组mu和sigma。4.5 反向传播梯度与数值梯度对不上如果数值梯度和反向传播梯度不一致先检查是不是漏了正则化梯度的偏置项——正则化项通常不惩罚theta_0。如果确认这点后还是对不上再检查是否在某个隐藏层忘记加dropout或非线性激活这个课程中没提但如果你自己加了额外结构就需要注意。5. 实操心得与扩展建议跑完全部8周Python版之后我觉得最大的收获不是会调参而是建立了一种“从公式到矩阵运算”的翻译能力。看到一篇论文里的损失函数我能很快在大脑中过一遍对应的向量化实现然后直接在Python里写出来。这种能力是单纯调sklearn库练不出来的。给几个具体的建议不要一开始就打开别人的Python版答案先自己照着Matlab原版把关键函数补全然后提交到Coursera的评分系统验证。虽然现在Coursera有时对老课程提交不太方便但Octave本地跑一遍再和官方给的参考数值对比也能判断对错。每做完一周把代价函数的变化曲线画出来贴在笔记里。这个是别的教程不会告诉你的一个习惯。后来我复习时看一眼曲线就能回忆起这一周的核心思想。把每一周作业的Python版都整理成独立的小项目带readme和运行入口。因为这套作业本身就像一个小型开源的算法库留着以后面试前复习用非常顺手。有一点特别提醒课程里的SVM部分用的是libsvm的线性核和高斯核但你如果用sklearn复现注意默认参数中的gamma和C不同需要手动调整。这在面试里也经常被问到sklearn SVC里的gamma和课程里的sigma是什么关系。简单说就是gamma 1 / (2 * sigma^2)换算清楚就能对齐。最后再分享一个小技巧。在跑PCA那周的练习时我用Python画了特征脸Eigenface可视化把U矩阵每一列作为权重映射到原图的像素空间输出一张相似度最高的图。这个做法本质上就是生成式AI里的线性隐空间思想。把课程作业往这个方向延伸一下你会觉得一切都是相通的。这套作业我前后零散地刷了三遍。第一遍纯粹为了交作业第二遍用Python重写第三遍是在准备面试时快速翻阅核心实现。每一次都有新发现也是我强烈建议你亲自动手重写的原因。本文还有配套的精品资源点击获取