资讯详情 Python重写机器学习作业:从线性回归到神经网络的完整实现
📅 2026/10/12 3:03:18
简介对于想要系统掌握吴恩达机器学习课程作业的初学者和进阶学习者这份资源以 Python 与 Matlab 双版本代码为核心覆盖线性回归、逻辑回归、神经网络、SVM、聚类等经典算法的习题实现。压缩包共323个文件主体为169个m文件、Matlab运行源码与数据辅以21个ipynb交互式Python笔记、57个txt说明、57个mat数据文件及配套PDF文档整体大小约71.86MB结构清晰便于对照练习。资源已获得2858人学习下载适合正在参加Coursera课程或希望用中文笔记辅助理解算法细节的读者尤其适合需要同时体验Python生态与Matlab数值计算流程的实践者。通过逐题对比两种实现可直观感受数据预处理、模型训练、参数调优和结果可视化的差异有助于深入理解不同工具的适用场景并为后续选择技术栈提供参考。1. 机器学习作业Python版拿它替换Octave环境跑完整流程一门经典机器学习公开课的课后作业官方环境是Octave或Matlab但很多人日常主力环境是Python。于是常见场景就变成了算法原理听懂了作业却卡在矩阵库不兼容、绘图脚本报错、数据格式对不上这类纯工程问题上。这份资源把整套作业用Python全部重写了一遍同时保留Matlab原版供逐行对照。它能省掉你从零摸索格式转换的时间适合正在刷课的学生、想把作业改造成课程设计的人以及所有想用Python生态重新跑一遍经典算法作业的从业者。2. 线性回归作业代价函数、梯度下降与特征缩放的三个对齐点线性回归是整套作业里最容易在语言转换时翻车的部分因为题目里到处是矩阵运算而Python的numpy在维度处理上的习惯和Matlab不太一样。作业一包含单变量回归、多变量回归和正规方程三个环节重写时只要抓住三个对齐点数据维度、代价函数写法、特征缩放参数的保存与复用其余代码基本是流水账。2.1 数据载入与维度约定先让矩阵形状对齐作业一的数据是文本格式数值之间用逗号分隔。Matlab中用load或csvread就能读Python里等价的是numpy.loadtxt。真正的坑不在读文件而在矩阵形状Matlab矩阵默认二维Python的一维数组很容易丢掉维度信息。我的习惯是先把X和y的shape打印出来再进入下一步。import numpy as np import matplotlib.pyplot as plt data np.loadtxt(ex1data1.txt, delimiter,) m data.shape[0] # 样本数 X data[:, 0].reshape(m, 1) # 特征城市人口 y data[:, 1].reshape(m, 1) # 标签餐车利润 # 拼接一列全1用于拟合截距项 theta0 X_b np.hstack((np.ones((m, 1)), X)) print(X_b.shape, y.shape) # 打印维度确认形状 plt.scatter(X, y, markerx, cred) plt.xlabel(Population) plt.ylabel(Profit) plt.show()这里最容易被忽略的是reshape(m, 1)。data[:, 0]取出的是形状为(m,)的一维数组直接参与矩阵乘会在广播时产生意想不到的结果但不会报错这才是最麻烦的——程序能跑结果全是错的。把特征列和标签列都显式reshape成列向量后面所有矩阵运算的形状就都是(m,1)或者(2,1)和Matlab行为对齐。X_b的第一列全1对应截距项θ₀这是线性回归建模的标准做法。拼这一列时用到np.hstack它的作用与Matlab的[X ones(m,1)]一致。2.2 代价函数与梯度下降1/2m为什么可以向量化线性回归代价函数是J(θ) 1/(2m) Σ(h(x) - y)²。这里的1/2不是随意写的是为了让J对θ求导后梯度里分母的2被消掉得到梯度表达式 1/m · Xᵀ(Xθ - y)。如果用Python写循环版逻辑最容易懂但向量化版本更贴近Matlab原版习惯也更能暴露维度问题。def compute_cost(X, y, theta): m y.shape[0] h X theta # 线性假设函数 cost (1 / (2 * m)) * np.sum((h - y) ** 2) return cost def gradient_descent(X, y, theta, alpha, num_iters): m y.shape[0] cost_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 cost_history[i] compute_cost(X, y, theta) return theta, cost_historyh - y的形状是(m,1)X.T是(2,m)两者相乘得到(2,1)和theta形状一致这一步能对上说明维度没写错。alpha常见取值从0.01、0.03、0.1到0.3都可以试如果观察cost_history发现代价不降反升说明alpha太大如果降得太慢说明alpha太小。迭代次数设1500次左右通常够收敛但不要在提交前删掉cost_history画出来才能确认是否真的收敛。2.3 多变量回归与特征缩放把mu和sigma存下来再用作业一第二题有两个特征房屋面积和卧室数量。特征缩放的目的是让梯度下降更快收敛公式是x (x - mu) / sigma这里的mu是均值sigma是标准差。Python里直接用np.mean和np.std注意np.std默认ddof0和Matlab的std行为一致不需要额外参数。def feature_normalize(X): mu np.mean(X, axis0) sigma np.std(X, axis0) X_norm (X - mu) / sigma return X_norm, mu, sigma这个函数返回mu和sigma是有原因的后续预测新样本时必须用训练集统计出的同一组mu和sigma对输入特征做变换不能重新计算。这是新手最容易踩的坑——预测时直接拿原始值代入模型结果偏差很大。正规方程是作业一的最后一题。公式为θ (XᵀX)⁻¹Xᵀy用Python实现时优先用np.linalg.pinv求伪逆而不是np.linalg.inv。伪逆在矩阵不可逆或接近奇异时仍能给出数值稳定的结果而inv直接报错。正规方程不需要特征缩放也不需要选择学习率但数据量大时计算复杂度接近O(n³)工程上大规模场景仍然优先梯度下降。3. 逻辑回归作业用scipy逼近fminunc的优化器替换方案逻辑回归是整个课程里第一次引入优化器的作业。Matlab原版用fminunc一行代码就把参数迭代交给优化器Python生态里没有完全同名对应物最接近的是scipy.optimize.minimize。这章的核心不是重写代价函数而是把两类优化接口的参数语义对齐不然作业跑起来结果总是差一点。3.1 sigmoid与代价函数从exp到溢出保护逻辑回归的假设函数是sigmoid(hθ(x))代价函数带对数项。Matlab里直接写log可以Python里如果sigmoid输出恰好为0或1log(0)会得到-inf或nan整个代价函数直接就废了。所以第一件事是给对数参数加一个极小值保护。def sigmoid(z): z np.clip(z, -500, 500) # 防止exp溢出 return 1 / (1 np.exp(-z)) def cost_function(theta, X, y, lamb): m y.shape[0] theta theta.reshape(-1, 1) h sigmoid(X theta) eps 1e-10 cost (-1 / m) * (y.T np.log(h eps) (1 - y).T np.log(1 - h eps)) reg (lamb / (2 * m)) * np.sum(theta[1:] ** 2) return cost.item() regeps取1e-10足够小不影响代价函数的数值又能兜住log底部的极端情况。lambda是正则化系数作业里默认取1如果取0则和普通逻辑回归完全一致取100则会把参数压得很小决策边界趋近一条直线实际表现为欠拟合。theta[1:]表示正则化不惩罚截距项θ₀这是逻辑回归和神经网络作业里的通用约定。3.2 优化器替换minimize与fminunc的参数对照Matlab版本的调用形式是fminunc(costFunction, initial_theta, options)options里设置GradObj为on让代价函数同时返回代价和梯度并设置MaxIter为400。scipy.optimize.minimize的对应关系是GradObjon对应jacTrueMaxIter400对应options里的maxiter。from scipy.optimize import minimize result minimize(funcost_function, x0theta_init, methodTNC, jacTrue, args(X_b, y, lamb), options{maxiter: 400}) theta result.x.reshape(-1, 1)注意这里cost_function返回值第一个是代价标量第二个是梯度而且梯度要拉平成与theta_init相同形状的一维数组。如果只返回代价不返回梯度优化器会走数值差分速度慢且精度差。method可以选择TNC、BFGS或L-BFGS-B在作业这种小规模问题上结果差异不大我一般习惯用TNC它对无约束问题稳定且不强制要求梯度绝对光滑。如果出现迭代次数耗尽但cost没降的情况优先检查jac相关设置而不是怀疑优化器。3.3 决策边界网格、等高线与predict一致性逻辑回归最后一步是把结果可视化。题目要求画出决策边界常见做法是构造一个覆盖样本范围的网格逐个网格点计算sigmoid输出再用contour画出概率等于0.5的等高线。u np.linspace(x1_min, x1_max, 50) v np.linspace(x2_min, x2_max, 50) xx, yy np.meshgrid(u, v) grid np.c_[xx.ravel(), yy.ravel()] grid_b np.hstack((np.ones((grid.shape[0], 1)), grid)) prob sigmoid(grid_b theta).reshape(xx.shape) plt.contour(xx, yy, prob, levels[0.5], colorsg)这里的核心难点是reshape(xx.shape)。如果不做这一步prob是一维数组contour会按隐式顺序填充矩阵画出来是一团乱线。levels[0.5]表示只抽取概率等于0.5的等高线这条线就是线性决策边界。predict函数用的是同一个判断逻辑prob 0.5判为1否则为0所以决策边界和predict结果必须严格对应。4. 神经网络作业前向传播、参数展开与反向传播的维度核对课程后半段的神经网络作业是整套资源里含金量最高的。作业三偏前向传播和一对多分类作业四加入反向传播难度陡增。这里最大的敌人是维度Theta1、Theta2、a1、a2、z2、z3任何一个矩阵少一列多一列程序要么报错要么静默算出错误结果。4.1 作业三MAT文件载入与一对多分类作业三的数据是MAT格式不能用np.loadtxt直接读要用scipy.io.loadmat。数据集里是5000张20×20的手写数字灰度图展开成400维向量标签存在y字段里数字0在Matlab中对应标签10。from scipy.io import loadmat data loadmat(ex3data1.mat) X data[X] # 5000 x 400 y data[y] # 5000 x 1一对多逻辑回归可以复用第3章的代价函数和优化器只是要循环10次每次把当前数字作为正类、其余作为负类。这个思想本身不难难的是不要忘记把y做one-hot变换以及预测时取10个分类器输出中的最大概率对应的数字。写完后可以用随机抽两行样本的方式验证结果打印预测值和真实标签对比只要准确率在95%附近就说明逻辑回归部分没写错。4.2 作业四前向传播与维度检验作业四的网络结构是输入层400个单元、隐藏层25个单元、输出层10个单元。已提供的权重文件ex4weights.mat包含Theta1和Theta2前向传播的目标是用给定权重算出正确预测。def predict_nn(Theta1, Theta2, X): m X.shape[0] a1 np.hstack((np.ones((m, 1)), X)) # 5000 x 401 z2 a1 Theta1.T # 5000 x 25 a2 np.hstack((np.ones((m, 1)), sigmoid(z2))) # 5000 x 26 z3 a2 Theta2.T # 5000 x 10 a3 sigmoid(z3) return np.argmax(a3, axis1) 1 # 标签从1开始维度规律是固定的当前层的输出维度等于下一层单元数每次进入下一层前要补一列偏置单元。a1补一列是因为输入层要加偏置隐藏层同样要加。Theta1的维度是25×401它隐含地对应着输入特征401个加偏置后的值。判断前向传播是否写对最直接的办法是算一下准确率题目给出的权重本身就内置了97%以上的准确率如果明显偏低就是哪里维度写错了。4.3 参数展开与反向传播梯度从输出层传回输入层作业四最繁琐的是反向传播。由于优化器要求参数是一个向量Theta1和Theta2必须展开成一维向量传入代价函数在代价函数内部再按原始shape还原。这种做法叫参数展开是神经网络作业里连接优化器和网络的唯一桥梁。def nn_cost_function(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb): Theta1 nn_params[:hidden_layer_size * (input_layer_size 1)] \ .reshape(hidden_layer_size, input_layer_size 1) Theta2 nn_params[hidden_layer_size * (input_layer_size 1):] \ .reshape(num_labels, hidden_layer_size 1) 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) # one-hot编码 Y np.zeros((m, num_labels)) for i in range(m): Y[i, y[i] - 1] 1 cost (-1 / m) * np.sum(Y * np.log(a3 1e-10) (1 - Y) * np.log(1 - a3 1e-10)) # 反向传播 Delta1 np.zeros(Theta1.shape) Delta2 np.zeros(Theta2.shape) for t in range(m): a1_t np.hstack((1, X[t])) # 401 z2_t Theta1 a1_t # 25 a2_t np.hstack((1, sigmoid(z2_t))) # 26 z3_t Theta2 a2_t # 10 a3_t sigmoid(z3_t) delta3 a3_t - Y[t] # 10 delta2 (Theta2.T delta3) * sigmoid_gradient(np.hstack((1, z2_t))) delta2 delta2[1:] # 去掉偏置单元的误差 Delta1 delta2[:, np.newaxis] a1_t[np.newaxis, :] Delta2 delta3[:, np.newaxis] a2_t[np.newaxis, :] Theta1_grad Delta1 / m Theta2_grad Delta2 / m # 正则化梯度不惩罚偏置列 Theta1_grad[:, 1:] (lamb / m) * Theta1[:, 1:] Theta2_grad[:, 1:] (lamb / m) * Theta2[:, 1:] grad np.concatenate((Theta1_grad.ravel(), Theta2_grad.ravel())) return cost, grad这个函数里有几个关键约束。delta2计算时要用sigmoid_gradient激活函数是sigmoid导数形式是s * (1 - s)。delta2要从第二项开始取因为偏置单元的误差不需要回传到输入层否则梯度的维度会对不上。Delta1和Delta2的累加必须在整个训练集上循环完成最后除以m再叠加正则化梯度。反向传播的循环版本在5000个样本上运行不会太慢而且比向量化版本更容易读懂适合作为作业实现和调试基础。随机初始化也是神经网络特有的坑。不能像线性回归那样用零初始化如果所有参数相同每个隐藏单元会学到相同的特征。常见做法是随机初始化到一个很小的对称区间。def rand_initialize(L_in, L_out): eps 0.12 W np.random.rand(L_out, L_in 1) * 2 * eps - eps return Weps0.12是一个默认经验值接近 sqrt(6 / (L_in L_out))目的是让sigmoid的输入落在非饱和区梯度不至于消失或爆炸。如果换了网络结构一般按 sqrt(6/(L_inL_out)) 调整这个范围。5. 避坑实录重写机器学习作业最常见的五个翻车点这套作业重写过程中最耗费时间的往往不是算法本身而是写对了却得不到正确结果时的调试过程。下面五个坑基本覆盖了绝大多数新手遇到的问题每一条都是实际发生过且可以稳定复现的。5.1 现象代价函数第一轮正常第二轮开始出现NaN原因sigmoid激活值在迭代过程中逐渐逼近0或1log(0)产生-inf后续计算被污染成NaN。学习率设置过大也会导致梯度下降发散表现为代价在中间某轮突然跳到极大值再变NaN。解决办法有两步一是在代价函数里给log加eps保护如log(h 1e-10)二是把学习率调回较小值重新跑。如果用的是minimize优化器则优先检查梯度的返回形状优化器内部的线搜索被错误梯度误导时同样会出现NaN。5.2 现象Python算出的cost和Matlab版对不上始终差一个常数原因截距列被放在X的最后一列而原版放在第一列或者y被处理成了行向量导致矩阵乘后损失值差一个恒定量。这类问题不会报错但会让结果从第1轮开始就和标准答案对不上。解决方法是运行前统一打印X.shape、y.shape和theta.shape确认X的截距列在第一列、y是列向量。最好把代价函数单独调用一次在给定theta全为0的情况下输出一个已知的初始代价再和题目文档中的参考值比较。5.3 现象scipy优化器跑完400次迭代theta几乎没变原因代价函数返回的梯度始终保持为0或接近0常见于梯度表达式里把m误除了多次或者梯度被错误地写成了代价函数对theta偏导以外的内容。另一个可能原因是jacFalse导致优化器没有使用你提供的梯度而是走了数值差分。解决方法是先手动调用一次代价函数检查grad的形状和量级再用下一章的梯度检查方法验证解析梯度是否和目标函数一致。5.4 现象反向传播梯度数量级异常大训练过程中cost上下跳原因随机初始化范围过大导致sigmoid进入饱和区梯度在反向传播中累积放大或者特征没有归一化隐藏层输入的数值范围差异过大。解决方法是把随机初始化范围控制到0.12左右并在训练前对输入特征做标准化测试。作业四里输入本身是0-255灰度值虽然不归一化也能跑但把特征缩放到0-1区间会让训练稳定很多。5.5 现象决策边界画出来和数据分布完全对不上原因网格生成后没有reshape成二维contour函数按隐式顺序填充矩阵导致等高线错乱或者levels使用了概率值本身如levels[0.4,0.6]把多条概率线同时画出来视觉上看起来像杂乱曲线。解决方法是确认生成网格后做了reshape(xx.shape)并只保留levels[0.5]的单条边界线。画之前先用predict函数对几个网格点做预测验证逻辑正确再画图。6. 收尾技巧梯度检查的两种姿势与数值验证反向传播是整份作业里最容易写错且最不容易察觉的部分。错误可能藏在某一层的delta计算里、正则化梯度的拼接里或者参数展开的reshape顺序里。这些错误不会报错只会让模型准确率停留在90%以下看起来很接近正确但永远差一截。梯度检查是我用来锁定这一类问题的固定手段。数值梯度的原理很简单对代价函数的某个参数θᵢ用中心差分近似导数。def gradient_check(theta, cost_func, eps1e-4): num_grad np.zeros(theta.shape) for i in range(theta.shape[0]): theta_plus theta.copy() theta_plus[i] eps theta_minus theta.copy() theta_minus[i] - eps num_grad[i] (cost_func(theta_plus)[0] - cost_func(theta_minus)[0]) / (2 * eps) _, analytic_grad cost_func(theta) denom np.linalg.norm(num_grad) np.linalg.norm(analytic_grad) diff np.linalg.norm(num_grad - analytic_grad) / denom return diff第一种姿势是全量扫描适合线性回归和逻辑回归这种参数只有几个或几十个的模型。第二种姿势是随机抽样适合参数数量庞大的神经网络作业从300多个参数里随机抽20到30个维度做梯度检查速度很快误差量级足够说明问题。相对误差在1e-9附近说明实现几乎肯定正确在1e-5到1e-4之间说明可能哪里有小瑕疵超过1e-3基本可以判定梯度实现有误。还有一个细节梯度检查必须在优化器运行之前做而且是在小批量样本上做全量样本会让每个维度的两次代价计算变得非常慢。我以前总觉得自己写的反向传播不可能错直到有一次梯度检查暴露了问题——我在正则化梯度时不小心把偏置列也加了惩罚项相对误差一直在1e-2级别模型准确率卡在88%上不去。从那以后我每写完一个向量化实现都会强制跑一遍梯度检查不是为了交差是为了确认自己写的代码不是玄学。希望帮到你。本文还有配套的精品资源点击获取