ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python重写吴恩达机器学习作业:从Matlab到numpy的完整实践

Python重写吴恩达机器学习作业:从Matlab到numpy的完整实践 简介面向吴恩达机器学习课程学习者这份双语言作业全集同时提供自写的Python版本与Matlab原版覆盖第一次至第八次全部编程练习包括线性回归、逻辑回归、神经网络、支持向量机与异常检测等内容适合需要对照两种语言理解算法实现细节的初学者和进阶者。整个资源包共323个文件以169个Matlab脚本、21个Jupyter Notebook笔记、57个数据文件和57个文本说明为主体另有16个PDF文档辅助阅读整体大小约71.86MB按章节组织便于检索。已有2837人学习下载。Python版本展示了数据预处理、特征工程、模型训练与评估的完整流程Matlab版本则保留课程原版风格两者相互对照可以直观感受矩阵运算、梯度下降和向量化实现的异同。Notebook中附有推导与可视化脚本中保留关键注释有助于深入理解代价函数与优化原理并迁移到实际项目中。 那段时间我正好在Coursera上刷吴恩达的机器学习课作业本身就是Matlab/Octave版本。刷到一半我突然想不如把整套作业用Python再写一遍——既能脱离课程自带的运行环境又能把每个算法从“填空”变成“手写”顺便看看自己是不是真搞懂了。于是就有了这套“自写Python版本Matlab原版对照”的作业复现项目。这篇文章就是我对整个过程的复盘记录了我怎么组织代码、怎么处理数据、踩了哪些坑以及最后怎么确认Python版本和Matlab原版的结果是一致的。如果你也在刷这门课或者想把手写算法练扎实这篇应该能帮上忙。1. 项目背景与整体设计思路1.1 为什么非得用Python重写一遍最开始我确实只是按照课程要求用Matlab做作业但做到ex3手写数字识别的时候我发现自己只是照着模板填了几行公式对数据是怎么流动的其实一知半解。课程给的框架已经把梯度下降循环、代价函数、预测函数都搭好了我要做的只是把空行补上。这种“填空式”学习最大的问题在于一旦脱离那个模板让我从零写一个逻辑回归我大概率会卡在维度转换上。所以我才决定用Python把作业全部重写。选Python的原因很实际现在做机器学习项目不管是快速验证还是上生产Python都是绕不开的语言。另一方面Python环境里没有课程提供的那些辅助函数所有矩阵运算都得自己用numpy搭这反倒逼着我把每个算法的输入输出理清楚。写完之后我发现很多当年在Matlab里“糊弄过去”的细节在Python翻写时会原形毕露。1.2 原版结构保留与Python重构的取舍原版作业是ex1到ex8每个练习对应一个主脚本和几个辅助函数数据都用.mat格式保存。我在重构时做了一个决定目录结构完全对齐原版Python脚本也按ex1、ex2这样编号数据直接读取原版.mat文件这样就能很方便地和Matlab原版做结果对比。实现上我严格控制依赖只用numpy、scipy、matplotlib这三个库。很多作业里的算法其实可以用sklearn一行搞定但我坚持不用因为一旦用现成库就失去了手写练习的意义。整个项目分两部分一部分是每个作业的入口脚本比如ex1.py、ex2.py负责加载数据、调用函数、输出结果另一部分是公共的工具函数模块里面放着sigmoid、代价函数、梯度计算这些会被多个练习复用的代码。这样既不会像Matlab原版那样把所有函数撒得到处都是又能保持一对一的对齐关系方便复查。2. 环境准备与数据读取2.1 Python运行环境搭建环境这块我建议用Anaconda直接建一个虚拟环境别把依赖装在系统Python里。我一开始偷懒直接在base环境里pip install结果装了个新版本的scipy和系统里的老numpy冲突一连串奇奇怪怪的报错。后来老老实实建环境conda create -n ml-course python3.10 conda activate ml-course pip install numpy scipy matplotlib版本上需要注意numpy建议1.20以上scipy建议1.6以上因为后面要用scipy.optimize.minimize代替Matlab里的fminunc低版本scipy对这个函数的支持没那么好。matplotlib版本随意能用就行。2.2 Matlab数据文件怎么读进Python原版作业的数据都是.mat文件Python读这种文件最常用的方式是用scipy.io.loadmat。这个函数会把.mat文件解析成一个Python字典key就是Matlab里的变量名。但有几个坑必须提前说。第一个坑是维度问题。Matlab里所有二维数组都是列优先存储的loadmat读进来之后如果直接打印shape经常会出现意外维度比如(M, 1)这种列向量。好在课程作业里的数据大多是矩阵不会太绕。第二个坑是loadmat返回的变量类型。有时候会读到numpy的ndarray有时候会读到结构化数组。我记得作业里有个.mat文件里存的是struct类型loadmat读出来直接是个numpy.void对象取字段还要用字段名去索引。我当时的处理方式是封装一个简单函数from scipy.io import loadmat def load_data(file_path, var_names): data loadmat(file_path) return [data[name] for name in var_names]调用的时候就明确传变量名比如load_data(ex3data1.mat, [X, y])返回两个ndarray。这样做虽然多打几个字但比直接用data[X]要清晰至少不会搞混变量名。还有个小技巧如果不想让维度多一层可以在loadmat里加squeeze_meTrue它会把单例维度挤掉。不过我在写作业时一般不用这个参数因为我要的就是矩阵形状挤掉之后有时候反而会变成奇怪的一维数组。2.3 文件结构和命名规范我的项目目录大概是这样的ml-course/ ├── ex1/ │ ├── ex1.py │ ├── ex1_multi.py │ └── ex1data1.mat ├── ex2/ │ ├── ex2.py │ ├── ex2_reg.py │ └── ex2data1.mat ├── ex3/ │ ├── ex3.py │ └── ex3data1.mat ├── common/ │ ├── __init__.py │ ├── cost_function.py │ ├── gradient_descent.py │ └── sigmoid.py └── requirements.txtex1.py对应原版的ex1.mex1_multi.py对应ex1_multi.m依此类推。公共函数放在common目录不同练习都从这里import。这么做最大的好处是校验的时候可以快速找到对应的Python文件再打开原版Matlab脚本一对一对比逻辑是否一致一目了然。3. 核心算法翻写的关键细节3.1 ex1线性回归从公式到向量化矩阵运算线性回归是整套作业里最基础的一节但也是我最推荐认真写的一节。因为后面的逻辑回归、神经网络本质上都是围绕矩阵运算展开的线性回归这里把矩阵流向搞清楚了后面就顺了。梯度下降的更新公式写出来很容易但真正实现时会发现一个选择用循环还是用矩阵乘法。课程在Matlab版本里是明确要求用矩阵运算的。我在Python版本里同样用矩阵运算def gradient_descent(X, y, theta, alpha, num_iters): m len(y) J_history np.zeros(num_iters) for i in range(num_iters): h X theta gradient (1 / m) * X.T (h - y) theta theta - alpha * gradient J_history[i] compute_cost(X, y, theta) return theta, J_history这里值得解释一下为什么梯度项能写成X.T (h - y)。X是(m, n)矩阵每一行是一个样本h - y是(m, 1)的误差向量X.T是(n, m)和误差向量做矩阵乘法得到(n, 1)正好是每个特征的梯度分量。这个推导我在纸上画了好几遍才真正看懂画完之后才算明白了“为什么是转置去乘误差”而不是反过来。还有一个细节是学习率的选择。课程给的alpha在Matlab里设的是0.01我在Python里也先用0.01但从成本曲线看下降得不够快。最后我试了0.01、0.03、0.1发现0.03在这个数据集上最合适下降曲线既平滑又能快速收敛。这说明学习率不是越大越好得结合数据规模来调。3.2 ex2逻辑回归与正则化优化器的替换逻辑回归这节最大的难点不在算法本身而在怎么用scipy.optimize.minimize替代Matlab的fminunc。原版作业里直接调用fminunc进行参数优化但在Python里没有直接对应的函数标准做法是把代价函数和梯度函数打包传给minimize。我当时写的优化调用代码是这样的from scipy.optimize import minimize def cost_function(theta, X, y, lambda_): m len(y) h sigmoid(X theta) epsilon 1e-5 cost (1 / m) * (-y.T np.log(h epsilon) - (1 - y).T np.log(1 - h epsilon)) reg_term (lambda_ / (2 * m)) * np.sum(theta[1:] ** 2) return cost reg_term def gradient(theta, X, y, lambda_): m len(y) h sigmoid(X theta) grad (1 / m) * X.T (h - y) grad[1:] (lambda_ / m) * theta[1:] return grad result minimize(funcost_function, x0theta, jacgradient, args(X, y, lambda_), methodTNC) theta_opt result.x这里面有三个容易踩的坑。第一个坑是log里面不能传0。Matlab的log函数对0会返回-inf但在数值计算里这会导致梯度变成nan。我一开始没加epsilon结果代价函数迭代几次就变成nan了。后面我加了1e-5这个数值足够小不会影响梯度精度又能避免log(0)。第二个坑是正则化项不对theta_0惩罚。课程里明确说了正则化只惩罚theta_1到theta_n不包括theta_0。所以我在计算reg_cost时用的是theta[1:]这个索引偏移如果错了优化的结果会差很多。第三个坑是minimize的method选择。我试过BFGS它在我这个数据集上收敛挺快但有时候会报“Desired error not necessarily achieved due to precision loss”。换成TNC之后这个问题基本就消失了。后来查资料才知道BFGS对线搜索步长的要求比较高代价函数和梯度如果有一点点精度损失就容易触发这个警告。TNC对这类问题的鲁棒性更好所以我最后的方案是统一用TNC。3.3 ex3与ex4从多分类到神经网络反向传播ex3的多分类手写数字识别核心是“一对多”策略。我在Python里做法是写一个循环对每个数字k训练一个逻辑回归分类器然后把所有分类器的参数堆叠起来。predict的时候用所有分类器分别预测概率取概率最大的那个类别作为最终结果。这里有个细节y的标签是1到10其中10代表数字0因为Matlab索引从1开始。在Python里做映射时容易忽略这个偏移导致预测结果对不上。ex4的反向传播是整套作业里最硬核的部分。我前后花了大约一个周末才把维度全部理清楚。反向传播的推导过程我不打算从零展开只说几个实现时最关键的决策。第一y标签要向量化。原版作业里y是10维向量每一位代表对应数字是否为当前样本的标签。在Python里我得先把y从(m, 1)转换成(m, 10)的one-hot矩阵。我用的是numpy的eye函数y_vec np.eye(num_labels)[y.flatten()] # y里的值范围是1-10对应到0-9索引时要小心第二权重初始化不能全设成0。全零初始化会让所有隐藏单元学到同样的特征网络失去意义。我采用课程推荐的随机初始化方法权重范围是[-epsilon_init, epsilon_init]epsilon_init取0.12。这个值是根据网络层大小估算的能保证激活值不会太大导致sigmoid饱和。第三梯度检查一定要做。刚开始写反向传播时我的代价函数在数值上下降但预测准确率一直上不去最后发现是delta层之间的误差传导写错了。后来我在前向传播和反向传播写完后先用课程自带的checkNNGradients函数做梯度检查数值梯度和我算的梯度逐步对比才定位到问题。做梯度检查时我习惯把lambda_设为0因为正则化会改变梯度公式干扰对比。确认梯度没问题后再加上lambda_这样排错效率会高很多。4. 实操中的常见问题与排查技巧4.1 我踩过的几个典型报错这里整理了一张速查表基本是我在翻写整套作业时遇到的高频问题。报错现象典型原因解决方案ValueError: shapes (97,2) and (3,1) not aligned给X加了一列1但theta初始维度是(n,1)没对齐检查X和theta的维度确保X是(m, n1)theta是(n1, 1)cost函数输出nanlog(0)或者学习率过大导致梯度震荡在sigmoid输出上加epsilon调小学习率scipy优化时警告precision loss代价函数和梯度函数计算不一致或者method不合适换methodTNC检查cost和grad用同一套thetaloadmat读出来的X维度不对Matlab数组列优先存储或者shape是(m,1)但预期是(m,)打印shape检查必要时用np.reshape调整神经网络预测准确率很低反向传播的误差项写错跑一遍数值梯度检查逐层对比我记得最深刻的是ex1里那个维度报错眼看X是97行2列theta却还是2行1列X theta算不了。原因是我在加偏置列时没有同步更新theta维度这个错很低级但排查起来需要打印每个中间变量的shape一处处对。4.2 怎么确认Python版和Matlab原版结果一致做完一版我不会急着写下一节而是先做结果对比。课程原版作业在运行时会打印不少关键信息比如ex1会打印theta值和最终代价ex2会打印训练准确率ex4会打印神经网络预测准确率。我的做法是把Matlab原版跑一遍把输出的theta、cost、准确率记录下来再用Python版本跑同样的数据逐项对比。数值上可以接受极小误差比如theta的差异在1e-5以内准确率完全相同。如果差异很大就可以判定这一节还没写对需要回头检查。这个方法非常有效因为它直接锚定了课程官方的最优解。如果你手头已经有别人写好的Python版本也可以直接对比不过我还是建议自己写完再看别人的不然很容易被“参考答案”带偏。4.3 调试技能从print到小样本测试我写这套作业时最常用的调试工具其实就一个print打印shape。矩阵运算出问题60%的情况是维度没对齐打印shape能快速定位是哪一层出了问题。比如在神经网络里我会在前向传播的每层都打印a和theta的shape在反向传播里打印delta的shape这样出了错就能立刻知道是哪一步的维度跟预期不匹配。还有一个非常有效的方法小样本测试。在跑全量数据之前先取前50个样本跑一遍看能不能正常完成一次前向、反向、参数更新。我几乎在每一节作业里都用这个方法能把调试时间压缩一半以上。等小样本跑通了再用全量数据训练心态会稳很多。5. 重写之后的体会与后续扩展5.1 这轮翻写让我真正理解了什么整套作业翻写完之后我最大的感受是之前用Matlab做作业只是在跟公式对齐用Python重写才是真的在跟数据对齐。线性回归里那个X.T (h - y)到底在算什么逻辑回归里正则化为什么只罚theta[1:]神经网络反向传播为什么是“从后往前传播误差”而不是“从前往后调整权重”——这些问题都是在Python版本里我被迫一个一个弄明白的。另一个体会是Python版代码写起来其实比Matlab原版更“累”但这种累是有价值的。Matlab原版自带很多矩阵操作的语法糖比如矩阵可以直接用做乘法而numpy里还要区分和这让代码多了一些约束但也让我对“这步到底是矩阵乘法还是逐元素乘法”想得更清楚。后面对比下来数学上两套实现的结果完全一致但在代码审查时Python版的向量化逻辑反而更显式、更好读。5.2 这套作业还能怎么继续玩作业做完不等于结束我后来的几个扩展方向如果你也在做类似的翻写可以一起参考。第一个扩展方向把ex4的手写网络固化下来换成PyTorch或TensorFlow重写。这样做的好处是能直观对比“手写反向传播”和“框架自动微分”的差异。我做完之后才发现框架帮你省掉的不只是公式推导还有大量形状变换的细节但如果不先手写一遍你根本不知道框架替你做了什么。第二个扩展方向把作业里训练好的模型用到其他数据集上比如sklearn自带的digits数据集或者Kaggle上的Titanic。作业里的数据集比较“干净”换到真实数据后会遇到缺失值、类别不平衡、特征尺度差异大等问题这些才更接近实际项目里的情况。第三个扩展方向把思维从“做作业”切换到“造工具”。我在写完ex1到ex4后顺手把逻辑回归、线性回归、神经网络这几个模型封装成了一个自己的小算法库。虽然这个库肯定不如sklearn这么完整但每次想用的时候知道它内部是怎么实现的这种底气是直接用现成库给不了的。5.3 给正在翻写作业的人一个建议最后想分享一个方法论层面的建议不要一上来就试图“完美复刻”。先跑通再优化。我一开始写ex2的时候总想着用最优雅的numpy写法结果在向量化上卡了两个小时。后来我改用最朴素的循环实现一遍确认逻辑没问题后再向量化反而更快。先求正确再求美观这个顺序在手写算法时几乎是铁律。如果你正在刷吴恩达这门课又恰好有想用Python重写一遍的念头我很推荐你动手试试。整套作业的规模不算大每个练习大概一到两天能完成但收获会比单纯看视频和填空大得多。翻写完一个练习后你会明显地感觉到对那部分内容的理解从“好像懂了”变成了“真的懂了”。本文还有配套的精品资源点击获取
返回列表