简介这套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 的矩阵形式68 小时第二份生成式学习、朴素贝叶斯、SVM贝叶斯决策边界、拉格朗日对偶对偶问题的互补松弛条件810 小时第三份学习理论、正则化偏差方差拆解、一致收敛界VC 维估计的不等式推导812 小时第四份无监督、EM、PCA、强化学习E 步 M 步推导、特征值分解、值迭代EM 的对数似然单调性证明1014 小时这张表最有用的地方是「典型卡点」那一列。你会发现卡住你的几乎从来不是概念而是某个具体的矩阵维度对不上、某个求导的链式法则漏了一项。所以每份作业动笔前先去看它对应讲义的哪几节把那几节的公式页摊开比硬着头皮空想快得多。还有一个容易被忽略的点作业里的编程题和推导题是配套的。编程题让你实现一个算法推导题让你证明它为什么收敛或者为什么等价于某个形式。只做编程题会变成调参工只做推导题会变成符号机器。两边都做才能在看到一个不收敛的损失曲线时知道是学习率的问题还是目标函数本身写错了。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, lr0.01, iters5000, tol1e-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(axis0) sigma X[:, 1:].std(axis0) 1e-12 # 防止除零 Xs X.copy() Xs[:, 1:] (X[:, 1:] - mu) / sigma return Xs, mu, sigma注意偏置列不参与标准化否则 $x_01$ 会被压成别的值截距项就失去意义了。这个细节在讲义里通常一句带过但漏掉之后模型会莫名其妙地欠拟合。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, dtypefloat) 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, iters25, tol1e-8, reg0.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 会越来越接近奇异牛顿法会在某一步报矩阵不可逆。加一个很小的reg1e-6 到 1e-3 之间既能让求解继续又能起到一点防过拟合的作用。这个现象在讲义里属于「注意到即可」但在代码里是必须处理的。tol的设定要看损失的量级。交叉熵损失在样本量几百的时候通常落在 0.10.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, iters200, seed0, reg1e-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, replaceFalse)] # 用真实样本点初始化比随机数稳 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(axis1, keepdimsTrue) logZ mx np.log(np.exp(logp - mx).sum(axis1, keepdimsTrue)) gamma np.exp(logp - logZ) loglik_hist.append(logZ.sum()) # ---------- M 步按响应度重估参数 ---------- Nk gamma.sum(axis0) 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, tol1e-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 设大了。可以先从 K2、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})$ 的项样本量一变界的方向可能就翻了。能自己找出「这个结论在什么条件下失效」比记住结论本身有用得多。从那以后我每次刷这类课程材料都会强制走一遍「关掉讲解重推一遍」的流程哪怕只推一道题。因为真正让你在面试或者实际项目里不翻车的从来不是看过多少页讲义而是那些你合上资料还能白纸黑字写出来的推导。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?