☰
Python重写机器学习作业:从线性回归到神经网络的完整实现
2026/10/12 3:03:15 网站建设 项目流程

简介:对于想要系统掌握吴恩达机器学习课程作业的初学者和进阶学习者,这份资源以 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, marker='x', c='red') 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_history

h - 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默认ddof=0,和Matlab的std行为一致,不需要额外参数。

def feature_normalize(X): mu = np.mean(X, axis=0) sigma = np.std(X, axis=0) 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或1,log(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() + reg

eps取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的对应关系是:GradObj=on对应jac=True,MaxIter=400对应options里的'maxiter'。

from scipy.optimize import minimize result = minimize(fun=cost_function, x0=theta_init, method='TNC', jac=True, 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], colors='g')

这里的核心难点是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, axis=1) + 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 W

eps=0.12是一个默认经验值,接近 sqrt(6 / (L_in + L_out)),目的是让sigmoid的输入落在非饱和区,梯度不至于消失或爆炸。如果换了网络结构,一般按 sqrt(6/(L_in+L_out)) 调整这个范围。

5. 避坑实录:重写机器学习作业最常见的五个翻车点

这套作业重写过程中,最耗费时间的往往不是算法本身,而是写对了却得不到正确结果时的调试过程。下面五个坑基本覆盖了绝大多数新手遇到的问题,每一条都是实际发生过且可以稳定复现的。

5.1 现象:代价函数第一轮正常,第二轮开始出现NaN

原因:sigmoid激活值在迭代过程中逐渐逼近0或1,log(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偏导以外的内容。另一个可能原因是jac=False导致优化器没有使用你提供的梯度,而是走了数值差分。解决方法是先手动调用一次代价函数,检查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, eps=1e-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%上不去。从那以后,我每写完一个向量化实现都会强制跑一遍梯度检查,不是为了交差,是为了确认自己写的代码不是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询