news 2026/10/12 12:51:31

周志华《机器学习》习题Python实现:从KNN到AdaBoost的西瓜书代码实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
周志华《机器学习》习题Python实现:从KNN到AdaBoost的西瓜书代码实战

简介:这份资源是周志华《机器学习》(西瓜书)课程习题的代码实现合集,面向正在学习机器学习课程、需要完成课后作业或希望动手复现经典算法的高校学生与自学者。内容按章节组织,覆盖线性模型、决策树、神经网络、支持向量机、贝叶斯分类、集成学习、聚类与降维等核心主题,每章配有习题说明与对应实现脚本,便于对照理论逐题练习。压缩包共90个文件,以22个Python脚本和10个Markdown笔记为主,另含34张图片、若干数据文件与配置项,整体约11.74MB,结构清晰、便于按模块检索。目前已有934人学习下载,适合希望借助可运行代码理解算法细节、巩固课程知识点的读者参考使用。

1. 从一份西瓜书作业代码包说起:周志华《机器学习》习题的 Python 落地

如果你正在啃周志华老师的《机器学习》,大概率经历过这种场景:公式推导看懂了,手推一遍也能跟上,但真让你从零写一个 KMeans 或者 AdaBoost,脑子里立刻一片空白。这份MachineLearning_Zhouzhihua_ProblemSets-master就是冲着这个痛点来的——它把西瓜书第 2 到第 10 章的核心习题,用 Python 逐个实现了一遍,覆盖模型评估、线性模型、决策树、神经网络、支持向量机、贝叶斯分类、集成学习、聚类、降维与度量学习。每个章节目录下都有对应的习题说明 Markdown 和可运行的.py脚本,数据文件用的是西瓜数据集 2.0、3.0 系列。适合谁?正在做机器学习课程设计、准备期末复习、或者想拿西瓜书当主线系统刷一遍算法实现的人。不适合谁?想直接调 sklearn 一行出结果的人——这里很多地方是手写实现,目的就是让你看清每一步在算什么。

2. 环境搭建与数据准备:把西瓜数据集跑起来

2.1 目录结构与依赖清单

拿到压缩包解压后,先别急着跑代码。花两分钟把目录结构看清楚,能省掉后面一半的报错排查时间。整个仓库按章节组织,每一章是一个独立文件夹,命名格式是chX--章节名,比如ch3--线性模型、ch8--集成学习。每个章节文件夹里通常包含三类东西:习题说明文档(chX习题.md)、Python 实现脚本(如3.3.py、8.3-AdaBoost.py)、以及image文件夹存放习题相关的插图。

数据文件集中在ch4--决策树目录下,包括watermelon3_0_Ch.csv、watermelon2_0_Ch.txt、watermelon3_0a_Ch.txt、watermelon4_0_Ch.txt等。这些就是西瓜书里反复出现的那几个经典数据集,分别对应不同的属性数量和样本分布。ch10--降维与度量学习下还有一个ex7faces.mat,是用于 PCA 降维实验的人脸数据。

依赖方面,这份代码主要用到numpy、pandas、matplotlib,部分章节可能涉及scipy。没有requirements.txt,所以需要手动确认环境。常见做法是建一个干净的虚拟环境,然后按需安装。

# 创建虚拟环境(以 conda 为例) conda create -n watermelon python=3.9 conda activate watermelon # 安装核心依赖 pip install numpy pandas matplotlib scipy

这里 Python 版本建议 3.8 到 3.10 之间。太新的版本(比如 3.12)可能在部分老代码上遇到兼容性问题,主要是某些 numpy 旧 API 的弃用警告会变成报错。如果你不想折腾版本,直接用 3.9 是最稳的选择。

2.2 数据加载的两种方式与编码坑

西瓜数据集有两种格式:.csv和.txt。CSV 文件用 pandas 读很直接,但要注意编码。原始文件大概率是 UTF-8,但如果你在 Windows 上解压后直接用默认编码读,可能会遇到UnicodeDecodeError。

import pandas as pd # 读取 CSV 格式的西瓜数据集 # encoding='utf-8' 显式指定,避免 Windows 默认 GBK 导致乱码 df = pd.read_csv('ch4--决策树/watermelon3_0_Ch.csv', encoding='utf-8') # 查看前几行,确认列名和数据类型 print(df.head()) print(df.dtypes)

逻辑说明:read_csv的encoding参数是关键。很多人在 Windows 上跑报编码错误,就是因为系统默认用 GBK 去解码 UTF-8 文件。显式写utf-8能规避这个问题。如果仍然报错,可以尝试utf-8-sig,它能处理带 BOM 头的文件。

对于.txt格式的数据,通常是制表符或空格分隔,读取时需要指定sep参数:

# 读取 TXT 格式,sep='\t' 表示制表符分隔 # 如果实际是空格分隔,改成 sep='\s+' 用正则匹配 df_txt = pd.read_csv('ch4--决策树/watermelon2_0_Ch.txt', sep='\t', encoding='utf-8') print(df_txt.head())

参数说明:sep的值取决于文件实际的分隔符。不确定的时候,先用open()读前几行看看原始内容,比盲目试参数快得多。watermelon2_0_Ch.txt和watermelon3_0_Ch.csv的区别在于属性数量——2.0 版本只有两个特征,适合可视化决策边界;3.0 版本特征更多,适合完整的决策树构建。

提示:如果数据文件路径包含中文,在部分 IDE 里可能出问题。建议把整个项目放在纯英文路径下,比如D:/ml_projects/watermelon/,能避免很多玄学报错。

3. 核心算法实现拆解:从 KNN 到 AdaBoost 的代码逻辑

3.1 KNN 与 PCA:降维与分类的配合

ch10--降维与度量学习下的10.1-KNN.py和10.6-PCA.py是两个很适合入门的脚本。KNN 的核心逻辑是计算样本间距离,然后取最近的 k 个邻居投票。这份代码里大概率是手写距离计算,而不是直接调 sklearn。

import numpy as np def euclidean_distance(x1, x2): """计算两个样本之间的欧氏距离""" return np.sqrt(np.sum((x1 - x2) ** 2)) def knn_predict(X_train, y_train, x_test, k=3): """ 手写 KNN 预测 X_train: 训练集特征,shape (n_samples, n_features) y_train: 训练集标签 x_test: 单个测试样本 k: 邻居数量 """ # 计算测试样本到所有训练样本的距离 distances = [euclidean_distance(x_test, x) for x in X_train] # 按距离升序排序,取前 k 个的索引 k_indices = np.argsort(distances)[:k] # 获取这 k 个邻居的标签 k_labels = [y_train[i] for i in k_indices] # 投票:返回出现次数最多的标签 from collections import Counter return Counter(k_labels).most_common(1)[0][0]

逻辑说明:euclidean_distance是纯 numpy 实现,没有用scipy.spatial.distance,目的是让你看清距离公式怎么落到代码上。np.argsort返回的是索引数组,取前 k 个就得到了最近的邻居。Counter投票是最直观的多分类决策方式。

参数说明:k的取值直接影响结果。k 太小(比如 1),模型对噪声敏感,容易过拟合;k 太大,决策边界会过于平滑,可能欠拟合。西瓜书里提到,k 通常取奇数以避免平票,实际项目中常用交叉验证来选。这份代码默认 k=3,你可以手动改成 5 或 7 对比效果。

PCA 脚本10.6-PCA.py配合ex7faces.mat使用,逻辑是先对数据去中心化,然后计算协方差矩阵的特征值和特征向量,取前 d 个最大特征值对应的向量作为投影矩阵。

def pca(X, n_components): """ 手写 PCA 降维 X: 输入数据,shape (n_samples, n_features) n_components: 保留的主成分数量 """ # 去中心化:每个特征减去该特征的均值 X_centered = X - np.mean(X, axis=0) # 计算协方差矩阵 cov_matrix = np.cov(X_centered, rowvar=False) # 特征值分解 eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) # 按特征值从大到小排序,取前 n_components 个 sorted_idx = np.argsort(eigenvalues)[::-1] top_idx = sorted_idx[:n_components] # 投影矩阵 projection_matrix = eigenvectors[:, top_idx] # 降维后的数据 return X_centered @ projection_matrix

逻辑说明:np.cov计算协方差矩阵时,rowvar=False表示每一列是一个特征,每一行是一个样本。np.linalg.eigh用于对称矩阵的特征分解,比eig更稳定。排序后取最大的几个特征值对应的特征向量,就是主成分方向。

参数说明:n_components决定了降维后的维度。选多少?常见做法是看累计方差贡献率,一般保留到 85% 到 95% 之间。这份代码里没有自动选维度的逻辑,需要你手动指定。跑ex7faces.mat的时候,可以先设n_components=50看看效果,再调整。

3.2 集成学习:AdaBoost 与 Bagging 的手写实现

ch8--集成学习下的8.3-AdaBoost.py和8.5-StumpBagging.py是这份代码包里比较有分量的部分。AdaBoost 的核心是串行训练弱分类器,每一轮根据上一轮的误差调整样本权重。

def adaboost_train(X, y, n_estimators=10): """ AdaBoost 训练过程 X: 特征矩阵 y: 标签,取值为 +1 或 -1 n_estimators: 弱分类器数量 """ n_samples = X.shape[0] # 初始化样本权重,均匀分布 weights = np.ones(n_samples) / n_samples classifiers = [] alphas = [] for _ in range(n_estimators): # 训练一个弱分类器(这里用决策树桩) stump = DecisionStump() stump.fit(X, y, weights) # 计算加权误差 predictions = stump.predict(X) error = np.sum(weights * (predictions != y)) / np.sum(weights) # 计算该分类器的权重 alpha = 0.5 * np.log((1 - error) / (error + 1e-10)) # 更新样本权重:分错的样本权重增大 weights = weights * np.exp(-alpha * y * predictions) weights = weights / np.sum(weights) # 归一化 classifiers.append(stump) alphas.append(alpha) return classifiers, alphas

逻辑说明:AdaBoost 的关键在于样本权重的迭代更新。每一轮训练后,被分错的样本权重会增大,下一轮的弱分类器会更关注这些难分样本。alpha是弱分类器的投票权重,误差越小,权重越大。1e-10是为了防止除零。

参数说明:n_estimators控制弱分类器的数量。太少会欠拟合,太多可能过拟合,而且计算量线性增长。西瓜书里的习题通常用 10 到 50 个基学习器就够了。DecisionStump是决策树桩,只分裂一次,是最简单的弱分类器。

Bagging 脚本8.5-StumpBagging.py的逻辑不同,它是并行训练多个基学习器,每个学习器用自助采样(bootstrap)得到的子集训练,最后投票。

def bagging_train(X, y, n_estimators=10, sample_ratio=0.8): """ Bagging 训练过程 sample_ratio: 每个基学习器使用的样本比例 """ n_samples = X.shape[0] classifiers = [] for _ in range(n_estimators): # 自助采样:有放回地抽取样本 indices = np.random.choice(n_samples, size=int(n_samples * sample_ratio), replace=True) X_bootstrap = X[indices] y_bootstrap = y[indices] # 训练基学习器 stump = DecisionStump() stump.fit(X_bootstrap, y_bootstrap) classifiers.append(stump) return classifiers

逻辑说明:np.random.choice的replace=True实现了有放回采样,这是 Bagging 的核心。每个基学习器看到的训练集不同,增加了多样性。预测时所有基学习器投票,多数胜出。

参数说明:sample_ratio控制每个基学习器的训练集大小。常见做法是取 0.6 到 0.8。取 1.0 的话,每个学习器看到的样本数和原始一样,但因为有放回采样,仍然有约 36.8% 的样本没被选中,这部分就是“袋外样本”,可以用来做验证。

3.3 支持向量机:mySVM 模块的结构

ch6--支持向量机下有一个mySVM文件夹,说明这部分是模块化实现的,而不是单文件脚本。SVM 的手写实现通常包含核函数、对偶问题求解、KKT 条件判断等。这份代码大概率是用简化的 SMO 算法来求解对偶问题。

def kernel_transform(X, kernel_type='linear', gamma=1.0): """ 核函数计算 kernel_type: 'linear' 或 'rbf' gamma: RBF 核的参数 """ if kernel_type == 'linear': return X @ X.T elif kernel_type == 'rbf': # RBF 核:exp(-gamma * ||x_i - x_j||^2) sq_dists = np.sum(X**2, axis=1).reshape(-1, 1) + \ np.sum(X**2, axis=1) - 2 * X @ X.T return np.exp(-gamma * sq_dists)

逻辑说明:线性核直接算内积,RBF 核算的是高斯距离。sq_dists的计算用了广播机制,避免显式循环,效率更高。gamma控制 RBF 核的“宽度”,值越大,单个样本的影响范围越小,容易过拟合。

参数说明:gamma的取值对 RBF 核影响很大。常见做法是设为1/n_features,或者用网格搜索。这份代码里默认gamma=1.0,在小数据集上可能偏大,你可以试着调小到 0.1 或 0.5 对比决策边界的变化。

注意:手写 SVM 的收敛速度通常比 sklearn 慢很多,尤其是在样本量大的时候。这份代码的目的是教学,不是生产。如果你要跑大规模数据,还是用sklearn.svm.SVC。

4. 避坑与排查:跑这份代码时最容易翻车的五个地方

4.1 路径问题导致文件找不到

现象:运行脚本时报FileNotFoundError,提示找不到watermelon3_0_Ch.csv或ex7faces.mat。

原因:代码里用的多半是相对路径,比如'../ch4--决策树/watermelon3_0_Ch.csv'。如果你不在脚本所在目录下运行,或者项目移动过位置,相对路径就失效了。

解决:要么cd到脚本所在目录再运行,要么把路径改成绝对路径。更稳妥的做法是用os.path.dirname(__file__)动态获取脚本所在目录,然后拼接数据路径。

import os # 获取当前脚本所在目录 script_dir = os.path.dirname(os.path.abspath(__file__)) # 拼接数据文件路径 data_path = os.path.join(script_dir, '..', 'ch4--决策树', 'watermelon3_0_Ch.csv') df = pd.read_csv(data_path, encoding='utf-8')

4.2 numpy 版本不兼容导致 API 报错

现象:运行时报AttributeError: module 'numpy' has no attribute 'float'或类似错误。

原因:numpy 1.24 之后弃用了np.float、np.int等别名,而这份代码可能是在更早的版本下写的。

解决:要么降级 numpy 到 1.23 以下,要么把代码里的np.float改成float,np.int改成int。全局搜索替换就行。

# 降级 numpy(不推荐,可能影响其他包) pip install numpy==1.23.5 # 或者手动改代码,把 np.float 替换为 float

4.3 matplotlib 中文显示乱码

现象:画图时标题、坐标轴标签的中文变成方框或乱码。

原因:matplotlib 默认字体不支持中文。

解决:在画图前设置中文字体。Windows 上用SimHei,Mac 上用Arial Unicode MS。

import matplotlib.pyplot as plt # Windows 系统 plt.rcParams['font.sans-serif'] = ['SimHei'] # Mac 系统 # plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题

4.4 数据标签格式不一致导致算法报错

现象:AdaBoost 或 SVM 脚本运行时报标签类型错误,或者结果完全不对。

原因:这些算法要求标签是 +1 和 -1,但西瓜数据集里的标签可能是 0 和 1,或者是字符串“是”和“否”。

解决:在训练前做标签映射。先确认原始标签的取值,再统一转换。

# 查看标签的唯一值 print(df['好瓜'].unique()) # 映射为 +1 和 -1 label_map = {'是': 1, '否': -1} y = df['好瓜'].map(label_map).values

4.5 随机种子未固定导致结果不可复现

现象:每次运行 Bagging 或 KNN 脚本,结果都不一样,没法对比。

原因:np.random.choice或np.random.seed没有固定。

解决:在脚本开头设置随机种子。

np.random.seed(42) # 42 是习惯用法,任意整数都行

提示:固定随机种子后,每次运行的结果就一致了。但要注意,如果你改了采样比例或基学习器数量,结果仍然会变,这是正常的。

5. 进阶用法:用这份代码做课程设计的三个具体技巧

5.1 把单文件脚本改造成可配置的实验框架

这份代码里的脚本大多是“写死”的——数据路径、参数、输出都在代码里。如果你要做课程设计,需要跑多组对比实验,手动改参数太慢。一个实用的技巧是把关键参数抽到配置文件里,用argparse或yaml管理。

import argparse parser = argparse.ArgumentParser() parser.add_argument('--k', type=int, default=3, help='KNN 的邻居数量') parser.add_argument('--n_estimators', type=int, default=10, help='集成学习的基学习器数量') parser.add_argument('--dataset', type=str, default='watermelon3_0_Ch.csv', help='数据集文件名') args = parser.parse_args() # 用 args.k 替代硬编码的 k

这样你可以在命令行里快速切换参数,跑完一组实验后直接改参数再跑,不用动代码。对于课程设计里的“对比不同 k 值对 KNN 准确率的影响”这类题目,效率提升很明显。

5.2 用交叉验证替代单次划分

很多脚本里用的是简单的训练集/测试集划分,但西瓜书第 2 章专门讲了交叉验证。你可以把ch2--模型评估与选择里的思路用起来,给 KNN 或决策树加上 k 折交叉验证。

from sklearn.model_selection import KFold def cross_validate(X, y, model_fn, n_splits=5): """ k 折交叉验证 model_fn: 返回一个训练好的模型的函数 """ kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) scores = [] for train_idx, test_idx in kf.split(X): X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] model = model_fn(X_train, y_train) score = evaluate(model, X_test, y_test) scores.append(score) return np.mean(scores), np.std(scores)

逻辑说明:KFold把数据分成 k 份,每次用其中一份做测试,其余做训练。最后取平均准确率和标准差。标准差能反映模型在不同数据划分下的稳定性。

参数说明:n_splits通常取 5 或 10。数据量小的时候取 5,数据量大的时候取 10。shuffle=True保证每次划分前打乱数据,避免原始顺序的影响。

5.3 可视化决策边界:让结果更直观

课程设计里如果能画出决策边界,展示效果会好很多。对于二维特征的数据集(比如watermelon3_0a_Ch.txt),可以用 matplotlib 的contourf画出分类区域。

import numpy as np import matplotlib.pyplot as plt def plot_decision_boundary(model, X, y): """绘制决策边界""" x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02)) # 对网格上的每个点预测 Z = np.array([model.predict(np.array([x, y])) for x, y in zip(xx.ravel(), yy.ravel())]) Z = Z.reshape(xx.shape) # 画等高线填充 plt.contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.coolwarm) # 画散点 plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm, edgecolors='k') plt.show()

逻辑说明:np.meshgrid生成网格点,然后对每个点预测类别,最后用contourf填充颜色。alpha=0.3让背景半透明,散点更清晰。

参数说明:网格步长0.02控制边界的精细度。步长越小,边界越平滑,但计算量越大。对于小数据集,0.02 足够了;如果数据范围很大,可以适当放大到 0.1。

注意:这个可视化只适用于二维特征。如果你的数据集超过两个特征,需要先用 PCA 降到二维再画,但降维后的边界和原始空间不完全等价,解释时要谨慎。

从那以后我每次拿到一份别人的代码包,都强制先跑通一个最小脚本,确认环境和数据没问题,再逐个章节推进。这份西瓜书作业代码包的价值不在于代码写得多优雅,而在于它把书里的公式和实际能跑的 Python 代码之间的那道墙给拆了。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 12:41:39

Kubernetes LoadBalancer原理与MetalLB/BGP实战部署

1. 项目概述:为什么K8s集群里LoadBalancer不是“开箱即用”的魔法按钮?在某次给某高校实验室做容器化教学支持时,一位刚接触云原生的导师直接问我:“K8s文档里写的LoadBalancer类型Service,我yaml一写,服务…

作者头像 李华
网站建设 2026/10/12 12:40:39

2022版RoLabelImg在Windows上旋转框标注实战:安装、避坑与DOTA导出

简介:2022-RoLabelImg 是一款面向计算机视觉与机器学习研发者的图像标注工具,尤其适合从事目标检测、自动驾驶等方向的研究人员与开发者使用。该版本针对 Windows 环境做了专门优化,修复了以往安装与运行中可能出现的兼容性故障,解…

作者头像 李华
网站建设 2026/10/12 12:37:07

灰色马尔科夫预测:小样本波动序列的MATLAB实现与实战

我最早被灰色预测“种草”,是因为手头一个非常别扭的建模任务:数据只有十几个样本点,而且波动明显——折线图画出来像锯齿。线性回归不敢用,样本量太小,噪声还大;ARIMA要差分、要定阶,十几期数据…

作者头像 李华
网站建设 2026/10/12 12:35:58

多棱锥全攻略:从概念公式到三视图与模型制作

如果你跟我一样,早年学立体几何是从正方体、长方体入门的,第一次见到多棱锥的时候多少会有点不习惯:好好的平面图形,怎么底部还是个多边形,上面却收成一个尖?但多棱锥恰恰是立体几何里最“能打”的基本形体…

作者头像 李华
网站建设 2026/10/12 12:35:52

PLM零部件管理蓝图设计:从主数据到BOM的避坑指南

简介:这份PPT方案面向PLM项目中的零部件管理模块,适合制造业信息化顾问、研发数据管理工程师及企业主数据治理人员参考。内容围绕GPLM一期蓝图设计展开,覆盖零部件主数据属性规则、分类规则与编码规则,以及对象查询、创建、查重、…

作者头像 李华
网站建设 2026/10/12 12:35:30

LDPC-CPM联合仿真:相位连续性与稀疏编码协同设计

简介:本资源是一套面向通信工程专业高年级本科生及研究生的LDPC码与连续相位调制(CPM)联合仿真教学实践包,聚焦无线通信系统中高可靠、高频谱效率编码调制技术的建模与性能验证。资源包含96个文件,以50个MATLAB源码&am…

作者头像 李华