CS229讲义作业讲解:打通机器学习公式推导与代码落地
简介:这套CS229课程资料面向希望系统入门并深入机器学习的学习者,汇集吴恩达课程的全部讲义、四次作业及配套讲解,适合自学、复习和课程跟练。压缩包共47个文件、约9.22MB,以30份PDF讲义和复习笔记为主体,辅以7个ZIP作业数据包、6个MATLAB脚本和4个DAT数据文件,覆盖理论阅读、编程练习与结果验证的完整链路。目前已有1322人学习下载,具备较好的参考与复用基础。内容从线性回归、逻辑回归、生成学习算法延伸到支持向量机、学习理论、EM算法、因子分析、PCA、ICA与强化学习,复习笔记还涉及线性代数、概率论、凸优化、隐马尔可夫模型和高斯过程等先修或拓展主题;作业讲解补足推导思路、实现技巧和常见错误分析,PDF便于按主题检索,脚本与数据包可直接用于复现实验,适合边读讲义边完成作业、再对照解答查漏补缺。
1. 从「会调包」到「能推导」:cs229 这套讲义、作业与讲解补的是哪块短板
调包跑通一个模型不算难,难的是 loss 突然不降、准确率卡在 0.6 上不去的时候,你知道该看哪一行。cs229 这套讲义+作业+作业讲解的价值,就在于把「为什么这么写」和「代码里长什么样」接上:讲义负责把目标函数、梯度、Hessian 一步步推出来,作业负责逼你在白纸上重推一遍,讲解负责在你卡死的时候告诉你哪一步符号错了。它适合两类人——一类是已经会用框架但不清楚底层损失函数怎么来的工程师,另一类是要把机器学习基础补扎实、准备做研究或面试推导题的学生。不适合只想找个能跑的模型直接上线的人。
2. 资源分层:讲义、作业、讲解三件套怎么排学习顺序
很多人拿到这套东西的第一反应是从讲义第一页开始啃,啃到指数族分布就停了。问题不在难度,而在顺序——讲义是「知识线」,作业是「检验线」,讲解是「纠错线」,三条线要交错推进,不能串行。
2.1 讲义主线:五段式编排与前后依赖
把这套讲义按主题拆开,大致是这么一条链:监督学习(线性回归、逻辑回归、广义线性模型)→ 生成式学习(高斯判别分析、朴素贝叶斯)→ 核方法与支持向量机(含拉格朗日对偶、KKT)→ 学习理论(偏差方差权衡、一致收敛、VC 维)→ 无监督学习(K-means、混合高斯与 EM、因子分析、PCA、ICA)→ 强化学习与隐马尔可夫模型。
这条链的依赖关系比看上去紧。GLM 那一节如果没吃透指数族的自然参数,后面逻辑回归的多分类推广、softmax 的梯度推导都会变成背公式;生成式学习那一节如果没搞清「建模 $p(x|y)$ 再套贝叶斯」和「直接建模 $p(y|x)$」的区别,你会在 SVM 那一章反复纠结为什么判别式模型不需要假设数据分布。核方法那块更典型——对偶问题里的 $\alpha$ 到底代表什么,很多人是背下来的,不是推出来的。
我的建议是按「块」推进而不是按「页」:每块讲义先通读一遍抓结构,第二遍才动笔跟着推。第一遍的目标是知道这一块解决什么问题、输入输出是什么;第二遍才补数学细节。第一遍花的时间通常只有第二遍的三分之一,但它决定了你第二遍是在「补细节」还是在「重建体系」。
讲义里带编号的公式和附录里的矩阵求导结论要单独抄一遍。这不是仪式感,是因为作业里大量推导会直接引用这些中间结论,你翻回去找一次就断一次思路。我一般会把每块的公式抄成一页纸,作业时摊在旁边,比来回翻 PDF 省事得多。
2.2 作业梯度:四份 problem set 的知识点映射
这套材料里作业的难度是明显递进的,不是平均分配。按主题对过去大致是这样:
| 作业 | 覆盖讲义内容 | 主要考察 | 典型卡点 | 建议投入 |
|---|---|---|---|---|
| 第一份 | 线性回归、逻辑回归、GLM | 梯度推导、牛顿法、指数族变换 | 逻辑回归 Hessian 的矩阵形式 | 6~8 小时 |
| 第二份 | 生成式学习、朴素贝叶斯、SVM | 贝叶斯决策边界、拉格朗日对偶 | 对偶问题的互补松弛条件 | 8~10 小时 |
| 第三份 | 学习理论、正则化 | 偏差方差拆解、一致收敛界 | VC 维估计的不等式推导 | 8~12 小时 |
| 第四份 | 无监督、EM、PCA、强化学习 | E 步 M 步推导、特征值分解、值迭代 | EM 的对数似然单调性证明 | 10~14 小时 |
这张表最有用的地方是「典型卡点」那一列。你会发现卡住你的几乎从来不是概念,而是某个具体的矩阵维度对不上、某个求导的链式法则漏了一项。所以每份作业动笔前,先去看它对应讲义的哪几节,把那几节的公式页摊开,比硬着头皮空想快得多。
还有一个容易被忽略的点:作业里的编程题和推导题是配套的。编程题让你实现一个算法,推导题让你证明它为什么收敛或者为什么等价于某个形式。只做编程题会变成调参工,只做推导题会变成符号机器。两边都做,才能在看到一个不收敛的损失曲线时,知道是学习率的问题还是目标函数本身写错了。
2.3 讲解的正确用法:先卡四十分钟再看
讲解部分是这套资源里最容易被误用的。直接对着讲解抄推导,短期看进度很快,一周能刷完两份作业,但两周后换个数据集让你重推一遍,大概率还是推不出来——因为你练的是「抄写」不是「推导」。
我自己的用法是设一个硬性时间盒:每道推导题最多卡四十分钟。这四十分钟里允许翻讲义、允许查矩阵求导的结论,但不许看讲解。四十分钟到了还是没方向,就去看讲解的第一步,然后合上,自己接着往下推。只有连续两次在同一道题上卡超过四十分钟,才允许完整看一遍讲解。
这么做的好处是,你能精确知道自己断在哪一环。是没想起来用链式法则?是忘了把 $y$ 写成 one-hot?还是单纯矩阵转置写反了?这三种错误对应的补法完全不一样:第一种要回去补微积分,第二种要回去补问题建模,第三种只需要多做几道维度检查。把错误分类,比刷题数量重要。
提示:讲解里的推导步骤和讲义里的符号体系是统一的,如果发现某一步的符号对不上,先确认你看的是不是同一版讲义对应的讲解,版本错配是这类资源包里最常见的问题。
3. 从公式到可跑代码:线性回归与逻辑回归的落地对照
讲义上的推导和能跑的代码之间有一段距离,这段距离里藏着大部分工程问题:数值稳定性、停止条件、正则化项的位置。这章挑两个最基础的模型,把讲义公式和实现代码逐行对上。
3.1 正规方程与梯度下降:什么时候必须切到迭代解法
线性回归的闭式解来自对 $\frac{1}{2}\sum (h_\theta(x^{(i)}) - y^{(i)})^2$ 求导置零,得到 $\theta = (X^TX)^{-1}X^Ty$。这个公式在讲义里只有几行,但落到代码里有个坑:直接用inv求逆,在特征共线或者量纲差异大的时候会直接炸掉,或者给出一个看着像数字、实际毫无意义的解。
import numpy as np def normal_equation(X, y): """ X: (m, n) 设计矩阵,第一列已补全为 1 y: (m,) 目标值 返回 theta: (n,) """ # 用 pinv 代替 inv:设计矩阵接近奇异时给最小范数解,不会直接抛异常 theta = np.linalg.pinv(X.T @ X) @ X.T @ y return theta def gd_linear(X, y, lr=0.01, iters=5000, tol=1e-9): """ 批量梯度下降版本 lr: 学习率,特征未标准化时建议从 0.01 往下调 tol: 相邻两次损失的绝对差,低于它就提前停 """ m, n = X.shape theta = np.zeros(n) prev_loss = np.inf for i in range(iters): err = X @ theta - y # 残差向量 (m,) grad = (1.0 / m) * (X.T @ err) # 梯度 (n,) theta -= lr * grad loss = 0.5 / m * np.sum(err ** 2) if abs(prev_loss - loss) < tol: # 损失几乎不动了,提前收工 break prev_loss = loss return theta, losspinv内部走的是 SVD,代价比inv高,但对病态矩阵的容忍度高得多。判断要不要切到梯度下降,看两个量:特征维度 $n$ 和样本量 $m$。$n$ 在几千以内、$X^TX$ 的条件数还能接受时,闭式解又快又准;$n$ 上万之后求逆的 $O(n^3)$ 代价会明显拖慢,这时候迭代解法更划算。
梯度下降这边,学习率的选取和特征尺度强相关。如果某个特征的量纲是另外几个的几百倍,梯度方向会被拉得很偏,表现就是损失先降后震荡。标准做法是先做标准化再跑:
def standardize(X): """ 对除偏置列外的每一列做 z-score 标准化 返回标准化后的矩阵和 (mu, sigma),便于对测试集做同样变换 """ mu = X[:, 1:].mean(axis=0) sigma = X[:, 1:].std(axis=0) + 1e-12 # 防止除零 Xs = X.copy() Xs[:, 1:] = (X[:, 1:] - mu) / sigma return Xs, mu, sigma注意偏置列不参与标准化,否则 $x_0=1$ 会被压成别的值,截距项就失去意义了。这个细节在讲义里通常一句带过,但漏掉之后模型会莫名其妙地欠拟合。
3.2 逻辑回归牛顿法:先把 Hessian 推出来再写代码
逻辑回归的梯度是 $\nabla_\theta J = \frac{1}{m}X^T(h_\theta(X) - y)$,这个形式很多人记得住。但牛顿法要用到 Hessian,讲义里推出来是 $H = \frac{1}{m}X^T S X$,其中 $S$ 是以 $h_\theta(x^{(i)})(1 - h_\theta(x^{(i)}))$ 为对角元的对角阵。这个 $S$ 的物理含义是每个样本的预测方差——预测接近 0 或 1 的样本权重小,预测在 0.5 附近的样本权重大。理解了这层,Hessian 就不是死记的矩阵了。
import numpy as np def sigmoid(z): """分正负两支计算,避免 exp 溢出""" out = np.empty_like(z, dtype=float) pos = z >= 0 out[pos] = 1.0 / (1.0 + np.exp(-z[pos])) ez = np.exp(z[~pos]) out[~pos] = ez / (1.0 + ez) return out def newton_logistic(X, y, iters=25, tol=1e-8, reg=0.0): """ X: (m, n),第一列为 1 y: (m,),取值 0/1 reg: L2 正则系数,加在对角线上防止 Hessian 奇异 """ m, n = X.shape theta = np.zeros(n) for i in range(iters): h = sigmoid(X @ theta) g = (1.0 / m) * (X.T @ (h - y)) # 梯度 W = h * (1 - h) # 每个样本的方差 H = (1.0 / m) * (X.T @ (X * W[:, None])) # Hessian H += reg * np.eye(n) # 正则化,保证可逆 step = np.linalg.solve(H, g) # 解 H·d = g theta -= step if np.linalg.norm(step) < tol: # 步长足够小就停 break return thetaX * W[:, None]这一步是把每个样本的特征行乘以它的方差权重,等价于 $XS$,写法上比构造完整对角阵省内存。np.linalg.solve比显式求逆再乘快,数值上也更稳。牛顿法通常十几步就能收敛,比梯度下降快一个量级,代价是每步要算一次 $n\times n$ 矩阵的分解,特征维度特别高的时候不划算。
注意:牛顿法对初始点敏感,如果某次迭代后 Hessian 不再正定,说明步长跨过了最优点。工程上常见的兜底是加正则项,或者退化成「牛顿方向 + 线搜索」。
3.3 数值稳定与参数标定:几个容易被忽略的细节
reg这个参数不是可有可无的装饰。当正负样本完全可分时,逻辑回归的最优解在无穷远处,Hessian 会越来越接近奇异,牛顿法会在某一步报矩阵不可逆。加一个很小的reg(1e-6 到 1e-3 之间)既能让求解继续,又能起到一点防过拟合的作用。这个现象在讲义里属于「注意到即可」,但在代码里是必须处理的。
tol的设定要看损失的量级。交叉熵损失在样本量几百的时候通常落在 0.1~0.7 之间,1e-8是个合理的停止阈值;如果数据量到了几十万,损失本身就有量级差异,这时候更稳的做法是监控参数的相对变化而不是损失的绝对变化。
另外,sigmoid一定要分支计算。直接写1 / (1 + np.exp(-z)),当 $z$ 是负几百的时候exp会溢出成inf,结果是nan,而且报错位置往往在很下游的地方,排查起来非常难受。这个坑我在第一次实现 softmax 的时候踩过,血泪经验就是:任何带指数的地方,先想清楚输入范围。
4. 生成式模型与 EM:作业里卡人最多的两块
生成式学习这块的难点不在公式量,而在「什么时候该用它」。EM 这块的难点反过来——公式都认识,但 E 步到底在算什么,很多人是模糊的。
4.1 高斯判别分析和朴素贝叶斯的适用边界
高斯判别分析假设 $p(x|y)$ 是多元高斯,且两类共享同一个协方差矩阵。这个「共享」的假设很强,它带来的直接结果是两个类的决策边界是一条直线——和逻辑回归的形式一样,但两者的假设完全相反。逻辑回归假设 $p(y|x)$ 是 sigmoid 形式,对 $p(x|y)$ 不作要求;GDA 假设 $p(x|y)$ 是高斯,然后推出 $p(y|x)$ 是 sigmoid。讲义里会强调:GDA 的假设更强,所以在假设成立时它需要更少的样本就能达到同样的效果,但假设不成立时它的偏差会明显更大。
朴素贝叶斯走的是另一条路:假设特征在给定类别下条件独立。这个假设在文本分类上经常不成立(词和词之间明显相关),但实际效果往往还行,原因是分类只需要 $p(y|x)$ 的相对大小对,不需要概率本身准确。这一点在作业里会通过一个具体的文本分类任务让你体会——你会看到明明独立性假设被违反得很厉害,准确率依然不低。
判断用哪个,我一般看三条:样本量小且特征维度高,优先朴素贝叶斯;样本量中等、特征是连续值且大致呈钟形,可以试 GDA;样本量大或者明显不满足分布假设,回到逻辑回归这类判别式模型。这三条不是硬规则,但能帮你跳过大部分无意义的对比实验。
4.2 EM 的 E 步和 M 步:在混合高斯上具体展开
EM 解决的是含隐变量的最大似然问题。混合高斯的隐变量是「这个样本属于哪个簇」,E 步算的是在当前参数下,每个样本属于每个簇的后验概率,也就是响应度 $\gamma_{ik}$;M 步拿着这组响应度当软标签,重新估计每个簇的均值、协方差和混合权重。
import numpy as np def gmm_em(X, K, iters=200, seed=0, reg=1e-6): """ X: (m, n) 样本 K: 簇数量 reg: 加到协方差对角线上,防止奇异 返回: 混合权重 pi、均值 mu、协方差 Sigma、对数似然历史 """ rng = np.random.default_rng(seed) m, n = X.shape pi = np.full(K, 1.0 / K) mu = X[rng.choice(m, K, replace=False)] # 用真实样本点初始化,比随机数稳 Sigma = np.array([np.cov(X.T) + reg * np.eye(n) for _ in range(K)]) loglik_hist = [] for _ in range(iters): # ---------- E 步:算对数响应度 ---------- logp = np.zeros((m, K)) for k in range(K): diff = X - mu[k] inv = np.linalg.inv(Sigma[k]) _, logdet = np.linalg.slogdet(Sigma[k]) # 用 slogdet 避免行列式下溢 quad = np.einsum('ij,jk,ik->i', diff, inv, diff) # 马氏距离平方 logp[:, k] = (np.log(pi[k] + 1e-12) - 0.5 * (logdet + n * np.log(2 * np.pi)) - 0.5 * quad) # log-sum-exp 归一化,直接 exp 会下溢成 0 mx = logp.max(axis=1, keepdims=True) logZ = mx + np.log(np.exp(logp - mx).sum(axis=1, keepdims=True)) gamma = np.exp(logp - logZ) loglik_hist.append(logZ.sum()) # ---------- M 步:按响应度重估参数 ---------- Nk = gamma.sum(axis=0) + 1e-12 # 每个簇的有效样本数 pi = Nk / m mu = (gamma.T @ X) / Nk[:, None] for k in range(K): diff = X - mu[k] Sigma[k] = (diff.T * gamma[:, k]) @ diff / Nk[k] + reg * np.eye(n) return pi, mu, Sigma, loglik_hist几个关键点值得单独说。np.linalg.slogdet返回符号和对数行列式,直接用det在高维下会下溢成 0,然后log(0)就是负无穷。einsum那一行算的是 $(x-\mu_k)^T\Sigma_k^{-1}(x-\mu_k)$,写成循环会慢十倍不止。log-sum-exp 归一化是整个 E 步里最容易写错的地方——如果直接用gamma = np.exp(logp)再除,当某个样本对某个簇的概率极小时,分子分母会同时下溢,得到nan。
4.3 用对数似然曲线判断收敛和局部最优
EM 有一个很好的性质:每次迭代后对数似然单调不减。这个性质可以直接当验证手段用。把loglik_hist画出来,正常情况应该是一条单调上升、逐渐变平的曲线。如果中间出现下降,那一定是实现错了——常见的是 M 步里协方差没除对权重、或者 E 步的归一化漏了一项。
import numpy as np def check_loglik(hist, tol=1e-6): """检查 EM 是否单调收敛,并返回收敛所需的迭代数""" hist = np.asarray(hist) diffs = np.diff(hist) if (diffs < -1e-9).any(): idx = np.where(diffs < -1e-9)[0][0] raise ValueError(f"第 {idx} 次迭代对数似然下降,检查 M 步归一化") for i, d in enumerate(diffs): if abs(d) < tol: return i + 1 return len(hist)单调性保证了,但收敛到的可能是局部最优。混合高斯的目标函数是非凸的,不同的初始化会收敛到不同的解。工程上的做法是跑五到十次不同随机种子的初始化,取对数似然最高的那组参数。判断标准不是「损失最小」,而是「对数似然最大」,因为 EM 优化的就是似然。
提示:如果十次初始化里有几次收敛到明显更差的解,通常说明 K 设大了。可以先从 K=2、3 试起,用对数似然的拐点判断合理的簇数量,而不是一上来就按业务经验硬定。
5. 避坑与排查:五个真实翻车现场
这一章按「现象 → 原因 → 解决」记录几个在这套材料上最容易反复出现的问题。都是我或者身边的人真实踩过的,不是理论上的可能性。
现象一:梯度下降的损失曲线先降后震荡,永远不收敛。原因几乎都是特征没有标准化,某个特征量纲比其它大两三个数量级,导致梯度方向被主导。解决方法是先做 z-score 标准化,学习率再按标准化后的尺度重新调,通常从 0.1 开始试比从 0.001 开始快。判断方式很简单:打印每一维特征的均值和标准差,如果标准差之间差了两个数量级以上,先标准化再说。
现象二:逻辑回归跑出nan,且报错位置在损失计算那一行。原因是sigmoid里直接写了np.exp(-z),当某个样本的线性组合值是很小的负数时溢出。解决办法是按正负分支计算,或者用np.clip把输入限制在 [-500, 500]。更彻底的做法是直接用scipy.special.expit,它内部已经处理了溢出。这个错误的隐蔽之处在于:小数据集上跑不出来,数据量一大或者特征尺度一变就爆。
现象三:GDA 的协方差矩阵求逆报奇异。原因是某个类别的样本数少于特征维度,样本协方差矩阵秩不足。这是 GDA 的硬约束,不是实现 bug。解决办法有两个:降维之后再跑,或者往协方差对角线上加一个小量(1e-6 到 1e-4)做正则。后者本质上是在假设各特征有独立的小方差,牺牲一点无偏性换可解性。如果加了正则还是奇异,说明特征之间存在完全共线,得先做特征筛选。
现象四:EM 的对数似然不单调,中间出现下降。十有八九是 M 步里的协方差更新写错了权重。常见错误是用硬分配(把每个样本只归给响应度最大的簇)而不是软分配,或者更新均值时忘了除以 $N_k$。检查方法:在 M 步之后立刻重算一遍 E 步的对数似然,如果比上一轮低,就锁定是 M 步的问题。另一个可能是pi出现了 0,导致log(pi)是负无穷,加一个 1e-12 的平滑项能解决。
现象五:作业看讲解看懂了,合上就忘。原因是看讲解时大脑在做「识别」而不是「生成」。解决办法是隔天再做一次:把讲解关掉,只留题目,限时四十分钟重推一遍。能推出来才算过。这个方法的成本很高,但它是唯一能把「看懂了」变成「会了」的路径。讲解的正确位置是纠错工具,不是学习材料。
6. 用 SVM 对偶和学习理论给自己出题:怎么验证真的吃透了
判断有没有吃透,不看能不能复述结论,看能不能在参数被改动之后重新推一遍。我一般用三个自测动作,每个都对应讲义里一块硬骨头。
第一个是 SVM 对偶的手推。拿一个二维、五个样本的小数据集,手动写出拉格朗日函数,然后检查三件事:对偶问题的约束 $\sum \alpha_i y_i = 0$ 是否满足、支持向量对应的 $\alpha_i$ 是否大于零、非支持向量对应的 $\alpha_i$ 是否等于零。这三条如果都能对上,说明 KKT 条件你理解了;只要有一条对不上,回去看互补松弛条件那一段。我见过太多人能把对偶形式默写出来,但给一组具体数字就算不出 $\alpha$。
第二个是偏差方差的手动拆解。取一个一维回归问题,用不同阶数的多项式拟合同一批带噪声的数据,重复采样一百次,分别统计预测的均值和方差。下表是我常用的通过标准:
| 自测项 | 观察目标 | 通过标准 |
|---|---|---|
| 一阶多项式 | 偏差高、方差低 | 平均预测曲线明显偏离真实函数 |
| 五阶多项式 | 偏差低、方差高 | 单次预测波动大,方差随采样变化明显 |
| 最优阶数 | 测试误差最小 | 训练误差和测试误差的间隙处于中间位置 |
| 加正则后 | 方差下降 | 高阶模型的方差显著收敛,偏差略升 |
如果做出来的曲线不符合这个趋势,通常是采样次数不够或者噪声水平设错了。这个实验的价值在于把「偏差方差权衡」从一句话变成一组你能亲手复现的数字。
第三个是拿一份作业的推导题,把其中的样本量 $m$ 或者特征维度 $n$ 改成一个极端值(比如 $m < n$),看原来的结论还成不成立。学习理论里很多界都带 $O(\sqrt{\cdot})$ 的项,样本量一变,界的方向可能就翻了。能自己找出「这个结论在什么条件下失效」,比记住结论本身有用得多。
从那以后我每次刷这类课程材料,都会强制走一遍「关掉讲解重推一遍」的流程,哪怕只推一道题。因为真正让你在面试或者实际项目里不翻车的,从来不是看过多少页讲义,而是那些你合上资料还能白纸黑字写出来的推导。希望帮到你。
本文还有配套的精品资源,点击获取