统计学习方法课件与代码:从PPT到numpy实现的教学资源 简介《统计学习方法》第2版配套课件PPT与随书代码面向机器学习初学者与进阶学习者系统覆盖监督学习、无监督学习及经典算法是兼顾理论推导与代码实践的完整资料。压缩包共60个文件包含21个章节PPT、26个Notebook、11个Python脚本及目录说明等。PPT按章节精炼梳理假设空间、经验风险最小化、SVM、EM算法、隐马尔可夫模型等核心概念代码则覆盖感知机、k近邻、决策树、朴素贝叶斯、逻辑回归、Adaboost、条件随机场等主流算法的Python实现便于逐章对照学习。另外课件覆盖第1章至第22章附录还包含梯度下降、牛顿法、拉格朗日对偶、反向传播等Notebook供深入理解数学原理。整套资料约55.36MB按章节组织索引清晰既适合自学也方便教师备课调整内容。已有213人学习浏览借助可运行的代码和可编辑的PPT读者能快速打通理论到实践的全流程提升复现经典算法与开展实验的能力。1. 统计学习方法课件与代码一套能直接讲课、也能直接跑算法的学习资源《统计学习方法》第2版的课件PPT和随书代码拆开看就两样东西一套能编辑的PPT课件一套跟着章节走的可运行代码。备考算法岗、准备面试或者要给这门课备课这套资源都能省掉大量从公式到代码的翻译时间。PPT把每章的模型、策略、算法三要素理清了公式推导和流程图都在代码用纯numpy实现感知机、k近邻、决策树、朴素贝叶斯、SVM、隐马尔可夫没有sklearn封装的黑匣子每一行都读得懂。适合三类人学生对照推导讲师改课件备课开发快速看算法底层实现。它不追求性能追求的是和书上对上号。2. 资源结构拆解PPT课件和随书代码是怎么组织的2.1 课件PPT每章都覆盖了模型、策略、算法三要素这套PPT不是把书的截图贴进去就完事而是按《统计学习方法》的讲述节奏做的。全书十二章PPT基本对应每一章做得最细的是感知机、决策树、支持向量机、隐马尔可夫这几章。每一章内部都沿用“模型、策略、算法”这套骨架来讲先给模型定义再讲为什么用这个损失函数最后落到梯度下降或动态规划这种可执行步骤上。这种组织方式的直接好处是你要在课件里找某个推导小节按三要素定位比按页码翻书快得多。我翻的时候特别留意了公式部分。书里一些推导很长比如支持向量机的对偶问题、隐马尔可夫的前向后向算法PPT把它们拆成一页一步的推导过程每页只推一小段适合课堂上翻页讲。对于要改课件的人来说这些公式和图示都可编辑可以把例题数字换掉把示意图重画把某几页拖到别章复用。相比网上那种导出来的图片版讲义这个才是能落地的教学资源。PPT里还配了一些用代码跑出来的实验图比如感知机迭代过程中决策边界的变化、kd树切分二维平面的效果。课件讲到这类图的时候展示的是真实运行结果而不是手绘示意图这点对理解“书上的收敛性在数据上长什么样”帮助很大。2.2 随书代码按章节目录组织numpy 为主、零网络依赖代码部分拆开后的目录结构大致是这样statistical_learning_code/ ├── ch02_perceptron/ │ ├── perceptron.py │ └── data.py ├── ch03_knn/ │ ├── kd_tree.py │ └── knn.py ├── ch04_naive_bayes/ │ └── naive_bayes.py ├── ch05_decision_tree/ │ └── decision_tree.py ├── ch06_svm/ │ └── svm_smo.py ├── ch10_hmm/ │ ├── forward_backward.py │ └── viterbi.py ├── data/ │ ├── perceptron_data.txt │ └── ... └── requirements.txt这套代码最大的特点是零网络依赖不需要联网下载数据集数据文件放在 data 目录里每个算法脚本直接读本地文件跑完用 matplotlib 画图。对初学者的意义在于你不必先搭一套数据管道才能跑通算法环境配好就能动手验证书上的结论。data.py 这个文件挺有意思它不读外部数据而是在脚本里用随机种子生成一组线性可分样本再写到文本文件。这么做是为了保证每次跑出来的分类图都是同一批点方便对照书里的例题。如果把随机种子改掉跑出来的图会变但收敛性结论不变这也是一个可以用来验证“感知机对任意线性可分数据都收敛”的小实验。代码风格是典型的教学代码函数粒度不细但参数名和书里的记号高度一致。比如感知机里用 lr 表示学习率SVM里用 C、tol 直接对应书里的惩罚参数和容忍度。这种命名在工程上看不算规范但做教学资源反而合适读完书再看代码基本不用查变量的含义。2.3 从文件清单看资源全貌拿到压缩包之后第一件事看 requirements.txt 和每个章节目录下有没有说明文件。一般随书代码会给出依赖清单这份代码在依赖上很克制核心就 numpy、matplotlib、pandas 三个。用表格做一个整体索引文件/目录类型说明ch02 ~ ch12 各章代码Python 脚本每章对应一个或多个算法实现data/数据文件实验用的本地数据集多为文本格式requirements.txt依赖清单第三方库范围课件PPTOffice 文档按章组织公式图示可编辑还要注意一点代码并不是十二个章节全都有。感知机、k近邻、朴素贝叶斯、决策树、SVM、AdaBoost、EM、隐马尔可夫这几章因为算法可手写实现代码覆盖最全纯理论章节可能只有练习模板甚至没有代码。用的时候别期待每章都是一个完整工程重点盯那几章经典模型就行。整套资源值得下载下来按章节当辅助讲义用PPT负责讲清楚推导代码负责验证推导。建议按“先PPT后代码”的顺序使用每一章先用PPT把模型、策略、算法过一遍再打开对应脚本看实现最后跑一次看输出图。面试前突击时上午过两章PPT下午跑两章代码一周能把核心章节串完。3. 核心代码走读从感知机到 HMM 的关键实现3.1 感知机原始形式与可视化感知机是最适合入门的一段代码因为整个模型就一个线性超平面更新规则也最简单。随书代码里的原始形式实现长这样import numpy as np def perceptron(X, y, lr0.1, max_iter500): 感知机原始形式对误分类点做梯度下降 w np.zeros(X.shape[1]) b 0.0 for epoch in range(max_iter): updated 0 for i in range(len(X)): # 误分类条件y * (w·x b) 0 if y[i] * (np.dot(w, X[i]) b) 0: w lr * y[i] * X[i] b lr * y[i] updated 1 if updated 0: break return w, b, epoch 1这段对应书里第2章感知机学习算法的原始形式。逻辑说明每一轮遍历全部样本一旦发现误分类点就更新 w 和 b更新方向是让这个点在超平面上的函数间隔变大一点学习率 lr 控制每次挪动的步长如果某一轮一整轮都没有误分类点说明当前参数已经能把训练集分开提前停机。参数说明lr 是学习率太小收敛慢太大会在最优解附近来回震荡max_iter 是最大迭代轮数而不是总更新次数因为感知机保证在线性可分数据上有限轮收敛max_iter 只是兜底防止数据不可分时死循环。跑完可以用 matplotlib 把样本点和决策边界画出来x 轴取两个端点用-(w[0] * x b) / w[1]算直线的坐标。3.2 k 近邻kd 树的平衡构建k近邻的实现分两步建树和搜索。很多简化版只会暴力计算欧氏距离但随书代码里一般会给出 kd 树版本毕竟书上花了篇幅讲这个。class KDNode: def __init__(self, point, leftNone, rightNone, axis0): self.point point self.left left self.right right self.axis axis def build_kd_tree(data, depth0): if len(data) 0: return None axis depth % data.shape[1] # 轮换切分维度 data data[data[:, axis].argsort()] # 按当前维排序 mid len(data) // 2 return KDNode( pointdata[mid], leftbuild_kd_tree(data[:mid], depth 1), rightbuild_kd_tree(data[mid 1:], depth 1), axisaxis )这里用 numpy 的 argsort 按切分轴排序取中位数作为当前子树的根节点左右递归建树。选择 axis 时用深度对特征维度数取模这样不同层轮流用不同维度切分保证树平衡mid 取中间值的下标一半数据进左子树一半进右子树。实际实现里还要注意两点kd 树构建本身不涉及目标值 y所以要把 point 和 label 一起封装进节点或者单独维护一个 label 数组搜索最近邻时会维护一个候选点列表和当前最优距离回溯往上走检查兄弟子树能否剪枝这是整个k近邻里最绕的部分建议单步调试着看。树建好之后搜索的复杂度从线性降到对数级数据量一大优势很明显。3.3 朴素贝叶斯先验和条件概率怎么统计朴素贝叶斯这一章的代码核心不是模型结构而是统计频率。用书上的例子最直观判断一个样本属于哪个类分别计算每个类别下的先验概率再乘上每个特征的条件概率取最大者。class NaiveBayes: def __init__(self): self.prior {} self.cond_prob {} def fit(self, X, y): n_samples, n_features X.shape classes np.unique(y) for c in classes: class_mask y c self.prior[c] class_mask.sum() / n_samples self.cond_prob[c] [] for f in range(n_features): # 统计当前类别下第f个特征的取值分布 values, counts np.unique(X[class_mask, f], return_countsTrue) self.cond_prob[c].append(dict(zip(values, counts / class_mask.sum()))) return self def predict(self, X): results [] for x in X: scores {} for c, prior in self.prior.items(): # 用对数概率累加防止连乘下溢 score np.log(prior) for f, val in enumerate(x): prob self.cond_prob[c][f].get(val, 1e-6) score np.log(prob) scores[c] score results.append(max(scores, keyscores.get)) return np.array(results)fit 阶段遍历每个类别先算 P(Yc)再对每个特征统计 P(X_f value | Y c)用字典保存取值到概率的映射。predict 阶段对每个新样本把先验和特征条件概率相乘工程上取 log 变成连加避免几十个小概率连乘直接下溢成 0。这里的参数没有加平滑系数但如果你用书里“有估计”的例子建议在 fit 里加拉普拉斯平滑把计数分子加1、分母加类别数否则测试集里出现训练集没见过的特征取值时get 的兜底值 1e-6 虽然不报错但会把整个后验概率拉得很小。这是个很经典的坑书里解释了原因代码里却不写需要自己动手补。3.4 支持向量机简化版 SMO 迭代第7章支持向量机的代码是所有章节里最重的随书代码一般会给出简化版 SMOdef select_j_rand(i, n): j i while j i: j np.random.randint(0, n) return j def smo_simple(X, y, C1.0, tol0.001, max_passes50): m, n X.shape alpha np.zeros(m) b 0.0 passes 0 while passes max_passes: changed 0 for i in range(m): # 计算当前 alpha[i] 对应的误差 fi np.sum(alpha * y * np.dot(X, X[i])) b ei fi - y[i] if (y[i] * ei -tol and alpha[i] C) or (y[i] * ei tol and alpha[i] 0): j select_j_rand(i, m) fj np.sum(alpha * y * np.dot(X, X[j])) b ej fj - y[j] ai_old, aj_old alpha[i], alpha[j] # 计算上下界并裁剪 aj if y[i] ! y[j]: L max(0, aj_old - ai_old) H min(C, C aj_old - ai_old) else: L max(0, aj_old ai_old - C) H min(C, aj_old ai_old) if L H: continue eta 2.0 * np.dot(X[i], X[j]) - np.dot(X[i], X[i]) - np.dot(X[j], X[j]) if eta 0: continue alpha[j] aj_old - y[j] * (ei - ej) / eta alpha[j] min(H, max(L, alpha[j])) if abs(alpha[j] - aj_old) 1e-5: continue alpha[i] ai_old y[i] * y[j] * (aj_old - alpha[j]) # 更新阈值 b b1 b - ei - y[i] * (alpha[i] - ai_old) * np.dot(X[i], X[i]) - y[j] * (alpha[j] - aj_old) * np.dot(X[i], X[j]) b2 b - ej - y[i] * (alpha[i] - ai_old) * np.dot(X[i], X[j]) - y[j] * (alpha[j] - aj_old) * np.dot(X[j], X[j]) b (b1 b2) / 2 changed 1 if changed 0: passes 1 else: passes 0 return alpha, b这段 SMO 是入门级实现只做了随机选择 j 的简版没有缓存误差值也没有像完整版那样优先遍历支持向量。参数说明C 是惩罚系数控制硬间隔和软间隔之间的权衡tol 是误差容忍度判断 KKT 条件是否满足max_passes 是最大“整轮无变化”次数和 max_iter 不一样它是用来判断优化是否停滞的。运行时你会发现它比 sklearn 的 SVC 慢很多尤其样本量过百之后因为内层循环每次都重新算整个核矩阵的线性组合没有做缓存。想提速的话把np.dot(X, X[i])的结果先算成核矩阵 K 缓存起来这是最直接的优化点。3.5 隐马尔可夫模型维特比解码第10章隐马尔可夫模型的代码一般包含前向算法、后向算法和维特比算法。维特比是最能体现动态规划的一段def viterbi(A, B, pi, O): T len(O) N A.shape[0] delta np.zeros((T, N)) psi np.zeros((T, N), dtypeint) # 初始化t1 时刻 delta[0] pi * B[:, O[0]] psi[0] 0 # 递推t2 到 T for t in range(1, T): for j in range(N): probs delta[t - 1] * A[:, j] * B[j, O[t]] delta[t, j] np.max(probs) psi[t, j] np.argmax(probs) # 回溯最优路径 best_last np.argmax(delta[-1]) path [best_last] for t in range(T - 1, 0, -1): path.insert(0, psi[t, path[0]]) return path, np.max(delta[-1])这里 A 是状态转移矩阵B 是观测概率矩阵pi 是初始状态分布O 是观测序列。逻辑说明delta[t, j] 表示到时刻 t 状态为 j 的所有路径中的最大概率psi[t, j] 记录这个最大概率是从上一时刻哪个状态转移来的整个递推过程就是沿着时间轴逐步剪枝最后从最后一个时刻概率最大的状态往回回溯就得到最优状态序列。注意 B[j, O[t]] 的取法O[t] 需要是整数编码后的观测索引如果观测本身是字符串要先做映射。初始化的 psi[0] 没有实际意义回溯时也不会用到它。把这段代码跑通之后前向算法和后向算法就只是把 max 换成 sum理解起来会顺很多。4. 运行环境与跑通步骤把PPT和代码落地到本地4.1 依赖安装与环境准备这套代码以 numpy 和 matplotlib 为主不需要 GPU 也不需要深度学习框架。我建议用 Python 3.8 到 3.11 之间的版本因为部分脚本可能用了老式 numpy 写法。创建虚拟环境python -m venv statlearn_env source statlearn_env/bin/activate # Windows 用 statlearn_env\Scripts\activate pip install numpy matplotlib pandas如果解压后有 requirements.txt直接pip install -r requirements.txt也行。版本选择上建议装 numpy 1.x 而不是最新的 2.x老代码在 numpy 2.x 上容易碰到 API 不兼容的问题后面避坑章节会细说。装完先做一个快速自检python -c import numpy, matplotlib; print(numpy.__version__)如果 import 阶段就报警告说明版本不对先降版本再继续不要带着警告往下跑否则最后报错时很难判断是环境问题还是代码问题。4.2 三个最值得先跑的脚本我一般拿到代码先跑三个脚本最快验证环境和理解代码风格cd ch02_perceptron python perceptron.py然后是 k 近邻cd ch03_knn python knn.py最后跑隐马尔可夫cd ch10_hmm python viterbi.py跑的结果长相是这样感知机会打印训练轮数和学习到的 w、b然后弹出一张二维分类图knn 会打印预测标签和准确率有的版本直接跑一个简单的分类任务并把最近邻连线画出来viterbi 则打印给定观测序列对应的最优隐藏状态序列。跑完之后建议在代码里故意改一个参数比如把感知机的 lr 从 0.1 改成 1.0观察收敛轮数的变化这是理解学习率最直接的方式。如果你是刚上手建议在朴素贝叶斯的 fit 里加断点或者直接打印 self.cond_prob。打印出来的结构是“类别 - 特征下标 - 取值 - 概率”的嵌套字典你能直观看到书里的条件概率表长什么样。这一步比闷头看推导有用得多。调试时用python -i进入交互模式跑完 fit 之后可以手动喂几个样本 predict看会不会返回预期类别。4.3 PPT 的编辑方式与字体公式处理PPT 是可编辑的直接用 Office 或 WPS 打开就能改。注意三件事。第一字体课件里可能会用某些特定中文字体如果你的电脑没装打开会自动替换成默认字体排版可能乱建议先用“替换字体”功能批量改成微软雅黑或思源黑体。第二公式如果是 Word 公式对象或文本可以直接改如果是图片就只能整张替换。打开后选中公式看一下能不能进入公式编辑器就能判断。第三配色如果想换一套配色用 PPT 的“设计 — 变体”统一改主题色不要一页页手动改。我见过有人试图把整套PPT的每页背景图抠出来换模板结果改到第三章就放弃了因为每页的元素位置都差一点。更省力的做法是保留原版式只改配色和文本内容。注意代码和数据文件建议统一用 UTF-8 编码。Windows 下如果用记事本另存过再打开可能乱码推荐直接用 VS Code 或编辑器批量确认编码。5. 踩坑记录跑这套代码最容易翻车的几个点这套代码整体能跑但毕竟有些年头在新版本 Python 和 numpy 环境下会遇到几个常见问题。每条都是实际跑的时候大概率撞上的按“现象 → 原因 → 解决”写清楚。5.1 现象一np.float / np.int 直接报 AttributeError现象运行某个脚本时报module numpy has no attribute float脚本直接中断。原因代码里用了老式 numpy 写法比如np.float、np.int这些在 numpy 1.20 以后被移除了。Python 3.9 以上安装的 numpy 基本都是新版本所以一跑就炸。解决全局替换np.float为float、np.int为int或者np.float64。更省事的是直接装 numpy 1.19.5和 Python 3.8 配套但如果你已经在用 Python 3.11还是手动改代码更靠谱。我一般用 IDE 的全局替换改完把全章节脚本跑一遍做回归确认没有遗漏。5.2 现象二matplotlib 中文全部变方块现象画图时标题和图例的中文全部变成方框坐标轴上的负号也变成奇怪的小方块。原因matplotlib 默认字体里不包含中文字形也没有正确配置 unicode 负号。解决在画图脚本开头加配置import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 或 Microsoft YaHei plt.rcParams[axes.unicode_minus] False第三行尤其关键不然坐标轴上的负号也会变成方块。如果你用的 Linux 服务器没有 SimHei可以用fc-list :langzh查一下系统有哪些中文字体换成对应的字体名比如 Noto Sans CJK SC。5.3 现象三数据文件路径找不到现象在项目根目录跑某个脚本报FileNotFoundError: data/xxx.txt。原因脚本里用了相对路径而相对路径是相对于当前工作目录的。你在根目录下执行 ch03_knn/knn.py脚本里写的data/knn_data.txt会从根目录/data 找但实际数据可能在 ch03_knn 子目录下路径对不上。解决先确认 data 目录在哪一层然后以脚本所在目录为基准改成绝对路径或者用 pathlib 定位from pathlib import Path DATA_DIR Path(__file__).resolve().parent.parent / data这样不管在哪个目录下执行都不会跑偏。这个改动很简单但每次换环境都要重新确认一遍所以我建议拿到代码第一件事就是把所有 data 路径统一改掉。5.4 现象四print 输出与 Python2 风格混搭现象个别脚本直接语法报错报错位置在 print 那一行或者输出里出现uxxx这样的字符串前缀。原因随书代码在多次转载过程中混入了 Python2 残留写法比如print w, b少了括号在 Python3 下直接 SyntaxError。解决遇到语法错误先看是不是 print 没加括号顺手改成函数式再检查字典遍历用的iteritems()是否要换成items()。这类问题通常集中在老代码里改动不大但需要一个一个脚本过一遍。开个全局搜索把所有print开头的行扫一遍基本能定位到。5.5 现象五SVM 脚本跑起来极慢现象样本量才一百来个svm_smo.py 要跑几秒甚至更久样本量上千时直接等得失去耐心。原因简化版 SMO 每次计算 f_i 时都重新算一遍np.dot(X, X[i])没有缓存核矩阵时间复杂度被拉高了。算法本身的正确性没问题但实现上不是工程优化过的版本。解决如果不改算法就只用小样本集跑书里的例题数据量完全够验证如果确实想跑大样本把核矩阵 K 提前算好缓存下来内层循环直接查表。另一个思路是限制 max_passes 和 tol让优化提前收敛牺牲一点精度换速度。6. 二次开发思路把随书代码变成你自己的算法工具箱6.1 统一接口fit / predict / score书里的代码是教学导向的函数命名和参数风格并不统一。想二次开发第一步是做接口统一。我动手时会定义一个公共基类让每个算法都有 fit 和 predictclass BaseModel: def fit(self, X, y): raise NotImplementedError def predict(self, X): raise NotImplementedError def score(self, X, y): pred self.predict(X) return (pred y).mean()然后把感知机、朴素贝叶斯、决策树这些代码往里套。这个改造的收益在于后面无论接网格搜索还是交叉验证都只需要一份通用代码不用每个模型单独写一套评估逻辑。6.2 可视化把训练过程画出来随书代码虽然有图但大多是最终结果。二次开发最常加的东西是训练过程的中间状态输出。比如感知机把每一轮的 w 画成一条线叠加成动图SVM 把不满足 KKT 条件的点在迭代中高亮。这块加上之后对教学和面试讲稿的帮助很大因为面试官更关心“你怎么知道它收敛了”而不是“模型结果是多少”。6.3 扩展方向接标准评价体系再往下可以给模型补充概率输出或者决策函数输出。朴素贝叶斯本身是概率模型把 scores 归一成概率很自然感知机可以把np.dot(w, x) b暴露成 decision_function。接上这些之后就能用标准工具画 ROC 曲线把这套教学代码往工程评价体系靠。最后说点个人习惯。以前我拿到教学代码总想直接优化性能上来就换向量化写法结果改完发现自己根本没法对照书里公式理解算法了。后来我给自己定了个规矩先原样跑通再在接口层面做增量改造每改一步都要能对着书里某个公式说清“这行代码对应哪一步推导”。从那以后我每次拿到这类随书代码都会强制走一遍这个流程——跑通基线、记下输出长相、再动刀。希望帮到你。本文还有配套的精品资源点击获取