简介:周志华《机器学习》代码练习.zip 是针对《机器学习》一书的算法复现代码包,适合正在研读该教材、希望将理论落到实践的机器学习初学者。压缩包共 16 个文件,其中 15 个 Python 脚本覆盖线性模型、逻辑回归、LDA、决策树、集成学习、SVM 等经典算法的实现与对比实验,另有 1 个 Markdown 说明文档辅助阅读。整个压缩包仅 20KB,轻量易下载,便于按章节对照调试。资源目前已吸引 508 人学习使用。借助这些代码,读者可以直观理解每个算法的训练、预测与评估流程,并通过 LogisticRegression_CV、bias_variance_regression、ReducedSVM 等练习体会交叉验证、偏差方差权衡和降维思路,是巩固机器学习原理、快速上手实验的实用资料。
1. 收到“周志华《机器学习》代码练习.zip”之后:先分清它到底是什么
拿到“周志华《机器学习》代码练习.zip”的人,绝大多数不是来欣赏代码审美的,而是被期末复习、课程设计或面试突击逼到了同一个路口:想快速把西瓜书里的算法变成能运行的实验。这个包转手多年,内容大多是数据集加按章节整理的 Python 脚本,质量参差不齐。这篇笔记不按某个具体版本写,而是把这类“代码练习 zip”最常见的样子讲清楚,给你一条能直接照做的流程:先验 zip 完整性,再配 Python 环境,跑通线性回归和决策树两个实验,最后把结果写进实验报告。按这条线走一遍,大约四十分钟就能出图和数值;就算你手里的包结构不大一样,这套顺序也照样能帮你把问题定位出来。
2. 解压与跑通环境:先把 Python 版本和依赖这两道坎迈过去
2.1 先验货再解压:10 秒看清 zip 是真完整还是假方便
网上流传的“周志华《机器学习》代码练习.zip”,来源多半是网盘、群文件或者课程 FTP,转手好几次之后,文件是不是完整只有解压时才知道。真正让人翻车的往往不是代码本身,而是拿到一个看似正常、一解压就报错的 zip。最省事的检查方法是在命令行里跑两个命令:file看一眼真实格式,unzip -t做完整测试。下面这段在 mac 和 Linux 上通用,Windows 上可以使用 PowerShell 里的Expand-Archive或 7-Zip 的7z t。
file 周志华_机器学习_代码练习.zip unzip -t 周志华_机器学习_代码练习.zip | tail -20unzip -t会逐个读取 zip 里的压缩条目,逐条打印校验结果,最后给出汇总。如果看到invalid zip archive: could not find eocd这样的错误,说明压缩包末尾的 Central Directory 记录丢了,本质是文件被截断,这种情况再怎么换解压软件都救不回来,重新下载源头文件才是唯一出路。如果校验过程一路ok,再解压也不迟,这一步能帮你排除掉一大堆后续的“代码跑不通”的冤枉路。
另一种容易吓到人的情况是解压时弹密码框。有些学习练习包在打包时设置了加密标志位,但实际内容并没有被加密,这叫做 zip 伪加密。它在课程资料里时不时能碰到,不算罕见。遇到弹密码框时别急着去找“zip 密码移除”工具,先用 7-Zip 打开,看看能不能直接列出文件列表并能预览内容。能预览就说明只是标志位在作怪,直接解压即可;真的被加密的包,只能联系发布者要密码。
2.2 Python 版本别追新:3.8 到 3.12 是安全区间
机器学习练习代码强依赖 numpy、pandas、scikit-learn 这三个库,它们对 Python 版本很敏感。下载的代码往往是几年前写的,你用最新的 Python 3.13 去跑,遇到的第一个错误多半是某个第三方包还没有提供对应版本的预编译包;反过来,版本太老又装不上新版 scikit-learn。我一般建议在 3.8 到 3.12 之间选,机器上正好有 3.10 或 3.11 就用它,这是最不容易踩坑的地带。
环境隔离是第二步。不要直接往系统 Python 里装依赖,否则你机器上其他项目装的 pandas 1.5、sklearn 0.24 可能把这份练习代码的运行环境搅成一锅粥。常见做法是建一个 venv 虚拟环境,把所有依赖装在里面,跑完删除也不心疼。以下是创建环境和安装依赖的标准流程:
python -m venv ml_env # Windows 激活 ml_env\Scripts\activate # Linux/macOS 激活 source ml_env/bin/activate pip install --upgrade pip setuptools wheel pip install numpy pandas matplotlib scikit-learn jupytervenv 不是玄学,是让你能安心折腾的后悔药。zip 里如果带了requirements.txt,我建议把里面的版本号当作参考而不是圣旨,里面锁定的版本可能太旧,先直接安装上面四个核心库,跑通了再回头对齐其他小依赖。依赖版本不是越高越好,比如 numpy 1.24 之后移除了np.float、np.int这类旧别名,很多老代码会在导入阶段直接报错,这点在后面的排查章节还会展开。
| 库 | 用途 | 需要注意的版本细节 |
|---|---|---|
| numpy | 矩阵运算基础 | 1.24 后移除np.float、np.int别名 |
| pandas | 读取 CSV 和处理表格 | 1.x 与 2.x 对 dtype 的处理有差异 |
| scikit-learn | 训练、评估、对拍 | 1.0 后不少参数改名或移除 |
| matplotlib | 画图 | 中文字体需要单独配置 |
| jupyter | 交互式实验 | 需要和 venv 绑定激活 |
2.3 用一条最小脚本确认环境不黑匣
环境装完先别急着跑主程序,写一个最小的探测脚本,确认 Python 解释器、numpy、scikit-learn 之间没打架。很多安装失败的场景,表面是“代码运行不了”,实际是虚拟环境没激活就启动了 Jupyter,导致 Jupyter 内核还在用全局环境。
import sys import numpy as np import sklearn print("python:", sys.version.split()[0]) print("numpy:", np.__version__) print("sklearn:", sklearn.__version__)运行结果里出现三个版本号,说明环境基本是通的。接着把工作目录切到解压目录,再运行主程序;如果第一句报错是ModuleNotFoundError,十有八九是 venv 没激活,或者 pip 安装到了别的环境。这一步能过滤掉至少一半的入门问题,后面所有实验都会省心不少。把这个脚本存成env_check.py放进练习包的根目录,以后换机器也能复用。
3. 把西瓜数据集和代码对上:数据、特征和输出目录
3.1 西瓜数据集 3.0 的常见形态
周志华老师书里反复出现的“西瓜数据集 3.0”,几乎每个代码练习 zip 都会带一份。典型内容是一张 CSV 表格,17 行样本,前面若干列是属性,最后一列是标签“好瓜”。分类属性有色泽、根蒂、敲声、纹理、脐部、触感,连续属性有密度、含糖量。这一份数据是整个包的锚点,先把它读进来,后面所有算法都拿它练手。
import pandas as pd df = pd.read_csv('data/watermelon_3_0.csv', encoding='utf-8') print(df.shape) # 常见结果是 (17, 9) print(df.head(3)) print(df['好瓜'].value_counts())这段代码做的事很简单:读取文件、打印行列数、看前几行、统计标签分布。参数上唯一需要灵活处理的是encoding,这个 CSV 在不同版本的练习包里可能是 utf-8 也可能是 GBK。读取时报UnicodeDecodeError就换成encoding='gbk';如果你看到注释乱码但数据列正常,用encoding='utf-8-sig'往往能消掉 BOM 造成的干扰。
拿到数据后先别急着训练。17 个样本对机器学习来说很少,跑练习没问题,但别指望靠它得到泛化结论。这本是一个用来理解算法原理的教学数据,它的作用是把某个算法的计算过程完整走一遍,让你看见每一步的数字变化。
3.2 离散特征编码是“机器学习中的数据处理”第一步
很多初学者拿到练习 zip,第一反应是直接df.dropna()、然后fit,结果 sklearn 报could not convert string to float。这就是“机器学习中的数据处理”要解决的事:线性模型和神经网络只能吃数值,必须把色泽、根蒂这类离散文本转成数值编码。常见做法是构造一个映射字典,用 pandas 的map方法原地替换。
# 把书里出现的类别文本映射成整数 mapping = { '色泽': {'青绿': 0, '乌黑': 1, '浅白': 2}, '根蒂': {'蜷缩': 0, '稍蜷': 1, '硬挺': 2}, '敲声': {'浊响': 0, '沉闷': 1, '清脆': 2}, '纹理': {'清晰': 0, '稍糊': 1, '模糊': 2}, '脐部': {'凹陷': 0, '稍凹': 1, '平坦': 2}, '触感': {'硬滑': 0, '软粘': 1}, } for col, mp in mapping.items(): df[col] = df[col].map(mp)不要凭感觉自定义映射顺序,最好按书中属性值出现的顺序排列,避免后续对照教材计算结果时对不上。决策树自己实现的版本可以直接吃字符串,但为了统一接口、方便和 sklearn 对拍,建议也提前做一次数值映射,反正数值编码不影响信息熵的计算结果。
这里也不要忽略标签列。标签“好瓜”通常映射成 1、“坏瓜”映射成 0,训练时二分类问题默认正类就是 1。有的练习包会直接用中文标签字符串,sklearn 能接受字符串标签,但你自己手写损失函数时就会遇到判断类型的问题,统一成 0/1 最省事。
3.3 常见代码结构与输出目录
解压后的目录结构没有统一标准,但大多数练习包会按章节或算法名组织。常见的结构长这样:
├── data/ │ ├── watermelon_3_0.csv │ └── watermelon_3_0a.csv ├── ch4_decision_tree/ │ ├── decision_tree.py │ └── run_experiment.py ├── ch5_neural_net/ │ └── bp.py ├── out/ └── requirements.txt如果你的包没有 README,先看看根目录下有没有data/和按算法分层的文件夹。找到入口脚本后,第一件事是确认脚本里读数据用的相对路径和你的实际目录结构一致。很多练习代码写死data/watermelon_3_0.csv,你从别的路径启动就会FileNotFoundError。
输出目录是另一个容易被忽视的点。练习代码通常会把预测结果、训练曲线、树结构图直接保存到当前目录,跑上几次就会留下十几个同名图片文件,把目录搞得一团糟。我一般会在项目根目录建一个out/,保存所有结果,方便写报告时统一整理。下面这段代码用pathlib建目录并保存文件,比手写os.makedirs更简洁:
from pathlib import Path out_dir = Path('out') out_dir.mkdir(exist_ok=True) # 保存决策树图,dpi 设成 300 保证打印清晰 plt.savefig(out_dir / 'sklearn_tree.png', dpi=300, bbox_inches='tight') # 保存预测结果,固定小数位数方便后续对比 np.savetxt(out_dir / 'pred.txt', y_pred, fmt='%.4f')这里要提醒一句:mkdir(exist_ok=True)只是保证目录存在,并不会清空旧文件。如果你要让实验结果可复现,应该在脚本开头加一段清理逻辑,或者让输出文件名带时间戳,否则容易把上一次的旧图当成新结果,这也是很多课程设计答辩翻车的常见原因。
4. 跑通两个典型实验:线性回归与决策树的最小可运行版本
4.1 线性回归实验:用最小二乘跑出 w 和 MSE
西瓜书线性回归一章最有直觉的实验是:用“密度”预测“含糖量”,画出一条拟合直线。很多练习 zip 里给的是梯度下降版本,代码很长,绕来绕去,其实原理和下面这个最小二乘实现完全一致。先用 numpy 的lstsq直接求解析解,能立刻看到w的值和误差,然后再去对照梯度下降的迭代过程,这样理解更深。
import numpy as np import pandas as pd df = pd.read_csv('data/watermelon_3_0.csv', encoding='utf-8') x = df['密度'].values.astype(float) y = df['含糖量'].values.astype(float) # 设计矩阵:在特征左侧加一列全 1,才能拟合截距 X = np.vstack([np.ones_like(x), x]).T w = np.linalg.lstsq(X, y, rcond=None)[0] pred = X @ w mse = np.mean((pred - y) ** 2) print("w0(截距):", w[0], "w1(斜率):", w[1], "MSE:", mse)这段代码的逻辑链是:将一维特征扩展成设计矩阵,左侧加一列1表示截距项;lstsq求解超定方程组的最小二乘解,返回的第一个元素就是系数向量;最后用矩阵乘法得到预测值,再算均方误差MSE。最关键的参数要数rcond=None,它用来处理设计矩阵奇异时的警告,建议保留。如果你忘了加全 1 那一列,拟合直线会被强制过原点,误差会大很多,而且画出来的图和书里的明显对不上。
如果你的练习包里是梯度下降版本,需要关心三个参数:learning_rate、epochs和batch_size。常见初始值分别是 0.01、1000 和全量样本。跑完以后必须把损失曲线打印出来,看到单调下降才能证明过程收敛;如果损失曲线剧烈震荡或直接变成 NaN,多半是学习率太大,降一个数量级再试。
4.2 决策树实验:从信息熵到最优划分
决策树实验一般要求手写 ID3 的信息增益划分。核心分两步,先算经验熵,再按属性加权计算信息增益,选增益最大的属性做当前划分。以下代码按西瓜书的记号实现,适用于离散特征:
import numpy as np import pandas as pd def entropy(y): _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() return -np.sum(p * np.log2(p)) def info_gain(df, attr, label): base = entropy(df[label]) total = len(df) weighted = 0.0 for v in df[attr].unique(): sub = df[df[attr] == v] weighted += len(sub) / total * entropy(sub[label]) return base - weightedentropy用np.unique统计每个类别的数量,换算成概率后用公式-sum(p * log2(p))计算信息熵。info_gain先算父节点的熵,再按每个属性值的样本占比加权求和得到子节点熵,两者相减就是信息增益。参数上唯一要注意的是,这里假设属性列已经完成了 3.2 的数值映射,且标签列是 0/1。如果你是拿原始字符串跑的,np.unique一样能处理,只是后续代码容易在类型判断上出问题。
接下来用贪婪搜索选出根节点的最优划分特征:
label = '好瓜' best_attr = max( (c for c in df.columns if c != label), key=lambda c: info_gain(df, c, label) ) print("第一次分裂应选:", best_attr)对西瓜数据集 3.0,这个结果应该是“纹理”。如果你跑出来的不是纹理,优先检查离散特征编码有没有对齐,其次检查数据是不是被意外排序过。递归构建子树时,每次划分后要剔除已经用过的属性,否则树会无限长下去;连续属性则需要另外实现二分法找划分点,不能直接套这个代码。
4.3 用 sklearn 对拍,确认结果没有偏
手写模型最大的问题是没有标准答案。决策树自己写的熵和划分逻辑,只要差一个符号,树就完全偏掉。常见做法是训练一棵 sklearn 的决策树做参照,把树画出来,结构接近就能说明你手写部分基本正确。
from sklearn.tree import DecisionTreeClassifier from sklearn import tree as sk_tree import matplotlib.pyplot as plt feature_cols = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖量'] X = df[feature_cols] y = df['好瓜'].map({'坏瓜': 0, '好瓜': 1}) clf = DecisionTreeClassifier(max_depth=3, random_state=42) clf.fit(X, y) plt.figure(figsize=(10, 6)) sk_tree.plot_tree(clf, feature_names=feature_cols, class_names=['坏瓜', '好瓜'], filled=True) plt.savefig('out/sklearn_tree.png', dpi=300)sklearn 的决策树默认用基尼系数而不是信息熵,所以它和你的 ID3 实现不会完全一致,但树的第一层划分特征和整体分类逻辑应该能互相印证。max_depth=3是为了防止 17 个样本被分到底造成过拟合,random_state=42保证同一份代码在别人机器上重跑结果一致。如果你发现手写树的准确率比 sklearn 差很多,先别怀疑模型,回看特征编码和数据读取部分,八成是数据处理阶段漏了列。
5. 避坑与排查:解压失败、中文乱码、Python 2 残留
5.1 zip 是真坏还是伪加密
现象:双击解压弹密码框,输什么都不对;或者命令行解压时报invalid zip archive: could not find eocd。
原因:两种情况要区分开。第一种是伪加密,打包者把 zip 头部的通用位标志改成了加密状态,解压工具以为有密码,实际数据根本没加密,这种情况多出现在防止网页直接抓取链接的资料包里。第二种是文件传输中被截断,zip 末尾的 Central Directory 丢了,解压工具连结尾都找不到,自然报could not find eocd。
解决:先用 7-Zip 打开这个 zip,能列出文件列表并能预览内容的,就是伪加密,直接解压即可,不需要任何“zip 密码移除”工具。如果 7-Zip 里也报错,或者解压中途提示某个条目 CRC 失败,说明文件字节已经不完整,重新从原始来源下载。这里有一条实操经验:下载完先看文件大小,和发布页面标注的大小对不上,就别浪费时间尝试修复了。文件被截断不是换个软件能解决的。
5.2 中文乱码:从 CSV 到图题全是方块
现象:read_csv报UnicodeDecodeError;CSV 读进来了但打印出来是乱码;matplotlib 画图时标题、图例里的中文全显示成方块。
原因:文件编码不匹配是主因。CSV 可能是 GBK 编码,你默认用 utf-8 打开就报错;还有一种情况是文件带 BOM,要用utf-8-sig才能正常解析。matplotlib 的中文方块是另一回事,默认字体里没有中文字形,需要手动指定字体。
解决:读取文件时按顺序尝试encoding='utf-8'、encoding='gbk'、encoding='utf-8-sig',找到能正常打印的那一个。matplotlib 这边,在画图脚本开头加上字体设置:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = FalseSimHei是 Windows 上的黑体,Arial Unicode MS是 mac 上常见的可用字体,把两个都写上可以跨平台兜底。axes.unicode_minus设为 False 是为了解决负号显示成方块的问题,画损失曲线时一定会遇到。如果设置完还是方块,多半是系统压根没装中文字体,直接用plt.rcParams['font.sans-serif']查看当前可用字体列表再调整。
5.3 Python 2 时代的代码残留
现象:运行到一半报NameError: name 'xrange' is not defined,或者AttributeError: 'dict' object has no attribute 'iteritems',再或者看到print "xxx"这种没有括号的写法。
原因:练习 zip 的原始代码写于 Python 2 时期,被后人转存时没做完整迁移。Python 3 里xrange并入了range,字典的iteritems改成了items,print变成了函数。这种报错通常在脚本前几行就会出现,因为 Python 解释器在编译整个文件时就会识别语法差异。
解决:先做全局替换,把明显的老写法批量改掉。可以用 Python 3 自带的 2to3 工具做一次自动迁移:
2to3 -w -n ch4_decision_tree/*.py-w表示直接写回原文件,-n表示不生成备份文件。这个工具能处理大部分常见替换,但不是万能的,跑完以后还要再看一遍。下面是几个高频出现的替换对照,遇到报错直接手动改:
| 老写法 | 新写法 | 说明 |
|---|---|---|
xrange(n) | range(n) | Python 3 的 range 已经是惰性对象 |
dict.iteritems() | dict.items() | items 返回视图,效果相同 |
print "xxx" | print("xxx") | print 变成函数 |
np.float/np.int | float/np.float64 | numpy 1.24 后别名移除 |
list.sort(cmp=...) | list.sort(key=...) | 比较函数改为 key 函数 |
改完以后重新运行,如果报错行变到更靠后的位置,说明迁移方向是对的。还有一种隐性坑是代码里用了long类型,在 Python 3 里直接删掉即可,int 会自动扩容。
5.4 scikit-learn 版本差异导致的“参数不存在”
现象:调用模型时报TypeError: DecisionTreeClassifier() got an unexpected keyword argument 'presort',或者RandomForestClassifier提示某个参数已移除。
原因:练习包代码是在旧版 scikit-learn 上写的。presort这类参数在 sklearn 0.24 之后就删掉了,新版本里连提示都不给。
解决:先确认当前环境里的版本,再看代码里用了哪些过时参数。命令行执行pip show scikit-learn或者python -m pip freeze | grep scikit-learn,拿到版本号后,把报错涉及的过时参数直接删掉。比如presort=True删掉就行,新版决策树会根据数据量自动决定是否预排序,你显式指定反而会报错。如果删掉后结果差异很大,说明这个参数在旧版本里确实参与了计算,那就需要把 sklearn 降到代码要求的版本,而不是迁就代码本身。我的建议是优先升级代码,而不是降级库,因为旧版本库可能又和新的 numpy 冲突,连环依赖会让人崩溃。
6. 进阶:把练习变成一份能交的机器学习实验报告
6.1 用 sklearn 对拍,让“手工实现”有参照系
跑通练习只是第一步,真正拉开差距的是能不能证明你的实现是对的。常见做法是把训练集切出一小部分做测试,分别用手写模型和 sklearn 模型预测,对比准确率或均方误差。指标接近,说明你的手写逻辑没有结构性错误;差距过大,就要回去查数据处理和特征编码。
from sklearn.metrics import accuracy_score # hand_pred 是你手写模型在测试集上的预测结果 # sk_pred 是同一份数据上 sklearn 模型的预测结果 acc_hand = accuracy_score(y_test, hand_pred) acc_sk = accuracy_score(y_test, sk_pred) print(f"手写精度: {acc_hand:.4f} sklearn精度: {acc_sk:.4f}")两个结果不应该完全一样,因为手写 ID3 用的是信息熵,sklearn 默认用基尼系数,但差距如果超过 0.1,大概率是特征没对齐。对拍时要注意,测试集必须完全一致,不能用同一个随机种子跑两次却忘记固定切分,否则对拍没有任何意义。把这条对拍代码加进你的实验脚本,每次运行自动打印两个精度,这是我认为最值得养成的习惯。
6.2 实验报告的最小骨架
一份能交差、能拿去答辩的实验报告,不需要花哨,但必须有这五块:实验目标、数据集说明、模型与参数、结果分析、改进方向。实验目标写一句话,比如“验证 ID3 决策树在西瓜数据集上的划分逻辑”;数据集说明写出 17 个样本、8 个属性、二分类标签;模型与参数列出手写模型和 sklearn 对照模型的参数表;结果分析放精度、MSE、树结构图;改进方向写两三条,比如“连续属性采用二分法划分”“增加预剪枝控制深度”。
我第一次交机器学习期末大作业时,就是从一个类似的 zip 包开始改的,最初只会改参数乱跑,后来才意识到真正加分的是把数据处理、模型选择、结果数值和失败记录写清楚。现在我会把每次实验的输出统一放进out目录,在 README 底部追加一条命令行记录,三天后回头审阅还能完整复现。这种做法在期末复习和面试聊项目时都非常受用,因为它向对方证明了一件事:你不是在跑别人的代码,而是在做自己的实验。希望你能把这份练习包变成真正属于自己的东西,希望帮到你。
本文还有配套的精品资源,点击获取