☰
从KNN到SVM:《机器学习实战》算法复现与调参避坑手册
2026/10/13 5:51:09 网站建设 项目流程

简介:《机器学习实战》(Machine Learning in Action)中文版 PDF,是机器学习入门与实战的经典读物,面向初学者、算法工程师及数据科学从业者。书中以大量可运行示例,带领读者围绕分类、回归、聚类、关联分析等任务正确选用算法,并完成从数据处理、模型训练到结果评估的完整流程。资源以 1 个 PDF 文件打包,体积 16.14MB,含完整目录与章节结构,便于按需跳转;目前已有 646 人学习/下载。内容涵盖 k-近邻、决策树、朴素贝叶斯、Logistic 回归、支持向量机、随机森林与 AdaBoost、回归与树回归、K-means 聚类、Apriori 与 FP-growth 关联分析、PCA/SVD 降维、MapReduce 与推荐系统等 16 大主题,既有原理拆解也有可运行代码与调优思路,适合边学边练,帮助读者系统掌握机器学习实战方法论。

1. 机器学习实战是什么:一本PDF里的完整训练场

很多人把《机器学习实战》当成入门读物,翻了两章就放在收藏夹里吃灰。其实这本PDF最大的价值不在理论讲解,而在于它把KNN、朴素贝叶斯、决策树、Logistic回归、SVM、Adaboost、K-means、FP-growth、Apriori、PCA、SVD这些经典算法全部用可运行的Python代码串了一遍。换句话说,它不是让你“看懂”机器学习,而是让你“跑通”机器学习——从数据处理到模型评估,每一步都有现成的实现路径。适合两类人:一类是刚学完理论基础、想验证算法怎么落地的初学者;另一类是做课程设计或期末复习、需要快速把某个算法改到自己的数据集上的学生或工程师。这本书解决的核心问题,就是“这个算法的代码到底长什么样、参数怎么调、数据从哪下手”。

2. 把数据处理与KNN、决策树跑通:机器学习实战的第一条主线

2.1 特征矩阵与标签怎么对齐:先看数据再写代码

书里的每个算法都遵循同一个套路:数据是特征矩阵加标签,特征是二维数组,标签是一维数组。但实际使用时,最先翻车的往往是数据格式。比如kNN那一章用的是datingTestSet2.txt,每行数据是“飞行里程、游戏时间、冰淇淋公升数、是否喜欢”,前三维是特征,最后一列是标签。常见的错误是把标签也当成特征喂进模型,导致分类结果莫名其妙地偏掉。

我一般会先做一个数据检查,确认特征列和标签列的数量、类型和缺失情况:

import numpy as np def load_data(filepath): data = np.loadtxt(filepath, dtype=float, delimiter='\t') features = data[:, :-1] # 所有行、除最后一列外的列 labels = data[:, -1] # 最后一列是标签 return features, labels features, labels = load_data('datingTestSet2.txt') print(features.shape, labels.shape) print(features[:2], labels[:2])

这段代码的作用是:用np.loadtxt读取制表符分隔的数据,切片操作data[:, :-1]取前三维作为特征矩阵,data[:, -1]取最后一列作为标签。delimiter='\t'表示按Tab分隔,如果你的数据是逗号分隔就要改成',',这是书中代码和自定义数据之间最常见的差异点。

另一个容易被忽略的点是数据归一化。kNN依赖距离计算,如果“飞行里程”数值是“冰淇淋公升数”的上千倍,距离会被大数值特征主导,小数值特征直接失效。书中给出的是min-max归一化:

def auto_norm(data_set): min_vals = data_set.min(0) # 每个特征的最小值 max_vals = data_set.max(0) # 每个特征的最大值 ranges = max_vals - min_vals # 极差 norm_data = (data_set - min_vals) / ranges return norm_data, ranges, min_vals

这里min(0)表示按列取最小值,(data_set - min_vals) / ranges把每个特征缩放到[0,1]区间。注意返回的ranges和min_vals要保存下来,后面用测试数据做预测时,必须用训练数据算出的极差和最小值去归一化,而不能重新计算,否则测试数据的归一化结果和训练数据不在同一个尺度上。

2.2 KNN分类与决策树构建:从伪代码到可复现实现

KNN的核心逻辑很简单:计算待分类点与所有已知样本的距离,取前K个最近样本,投票决定类别。书中的代码体现的是“纯Python+NumPy写算法”的思路,习惯用Scikit-learn的人会觉得它绕,但恰恰是这种底层写法让人看清算法的每一步。

def classify_knn(in_x, data_set, labels, k): data_size = data_set.shape[0] # 样本数量 diff_mat = np.tile(in_x, (data_size, 1)) - data_set # 把待测点复制成矩阵 sq_diff = diff_mat ** 2 distances = sq_diff.sum(axis=1) ** 0.5 # 欧氏距离 sorted_idx = distances.argsort() # 按距离从小到大排序 class_count = {} for i in range(k): vote_label = labels[sorted_idx[i]] class_count[vote_label] = class_count.get(vote_label, 0) + 1 return max(class_count, key=class_count.get)

np.tile的作用是把一维的待测点in_x在行方向复制data_size次,和训练集对齐做矩阵减法。argsort()返回索引排序,而不是排序后的数值,这便于直接通过索引找到对应标签。class_count.get(vote_label, 0)实现计数器的累加,是Python里很典型的统计写法。K值的选择书中默认是3,实际使用建议用交叉验证试K=1到20,看哪个K的准确率稳定。

决策树部分稍微复杂,核心是特征选择和信息增益。书中用ID3算法,每次选择信息增益最大的特征进行划分。信息增益的计算很容易写错,关键是熵的定义要先搞清楚:

from math import log def calc_shannon_ent(data_set): num_entries = len(data_set) label_counts = {} for feat_vec in data_set: current_label = feat_vec[-1] label_counts[current_label] = label_counts.get(current_label, 0) + 1 ent = 0.0 for key in label_counts: prob = float(label_counts[key]) / num_entries ent -= prob * log(prob, 2) # 以2为底的对数 return ent

这个函数对数据集最后一列的标签做频率统计,然后按信息熵公式求和。注意log(prob, 2)中的底数,换底数会改变熵的绝对值,但不会改变特征选择的相对顺序。书中生成决策树的代码用字典嵌套表示树结构,{'no surfacing': {0: 'no', 1: 'yes'}},这种结构可以直接用Matplotlib画树状图,也可以在预测时递归遍历判断分支。

3. 监督学习怎么选型:Logistic回归、朴素贝叶斯与SVM的适用边界

3.1 线性可分与概率输出:什么时候选Logistic回归

Logistic回归在书里被用来解决二分类问题,比如根据病马的各项指标判断马是否死亡。它的本质是线性回归加一个Sigmoid函数把输出压缩到0到1之间,输出值可以当作概率来理解。

def sigmoid(in_x): return 1.0 / (1 + np.exp(-in_x)) def grad_ascent(data_mat, class_labels): data_matrix = np.mat(data_mat) label_mat = np.mat(class_labels).transpose() m, n = np.shape(data_matrix) alpha = 0.001 # 学习率 max_cycles = 500 # 最大迭代次数 weights = np.ones((n, 1)) for k in range(max_cycles): h = sigmoid(data_matrix * weights) error = label_mat - h weights = weights + alpha * data_matrix.transpose() * error return weights

这个梯度上升法的更新公式在逻辑上比较绕:error是预测概率与真实标签的差值,data_matrix.transpose() * error是计算每个特征的梯度方向,weights沿梯度方向更新。alpha控制步长,设置太大会震荡不收敛,太小则收敛缓慢。数百次迭代后权重趋于稳定,训练好的权重就可以用来做预测。书中代码用矩阵乘法实现,性能一般,但胜在直观。

适用场景上,Logistic回归适用于特征与标签呈近似线性关系的问题,而且需要输出概率的场景,比如评分卡、风控模型。如果数据本身线性不可分,需要加多项式特征或换核方法,这时候就该考虑SVM了。

3.2 文本与独立特征场景:朴素贝叶斯的参数设定

朴素贝叶斯在书中用垃圾邮件分类作为例子。它的核心是条件独立性假设:所有特征在给定类别下相互独立。这个假设在实际数据里几乎不成立,但文本分类任务里效果出奇的好。书里用词向量表示文本,每个位置对应字典里的一个词,取值为0或1。

def train_nb(train_matrix, train_category): num_train_docs = len(train_matrix) p_abusive = sum(train_category) / float(num_train_docs) # 垃圾邮件占比 p0_num = np.ones(len(train_matrix[0])) p1_num = np.ones(len(train_matrix[0])) p0_denom = 2.0 p1_denom = 2.0 for i in range(num_train_docs): if train_category[i] == 1: p1_num += train_matrix[i] p1_denom += sum(train_matrix[i]) else: p0_num += train_matrix[i] p0_denom += sum(train_matrix[i]) p1_vect = np.log(p1_num / p1_denom) p0_vect = np.log(p0_num / p0_denom) return p0_vect, p1_vect, p_abusive

这里有两个关键处理。第一,所有词频初始化为1、分母初始化为2,这是拉普拉斯平滑,避免某个词在训练集中没出现过导致概率为0。第二,最终概率取对数,因为大量小于1的浮点数连乘会下溢成0,取对数把乘法变成加法,数值稳定性好得多。预测时用贝叶斯公式算出每条文本属于垃圾邮件的对数概率,比较两个类别的大小即可。

注意朴素贝叶斯对特征独立性要求较高,如果特征之间强相关,比如两列特征本身就是按相同规则生成的,模型会重复计算概率,导致分类过于自信。现实里的文本特征经过分词后仍然有关联,但实践证明影响可控。

3.3 SVM与Adaboost:小样本与弱分类器集成的选择

书中SVM部分用的是简化版序列最小优化算法,篇幅较长,核心思想是找到最大间隔超平面,支持向量是离超平面最近的那些样本点。SVM在小样本、高维数据上有天然优势,尤其是引入核函数之后能处理非线性边界。但书中代码对数值精度比较敏感,Python 3下容易出现收敛慢的问题,复现时建议直接使用现成的sklearn.svm.SVC验证效果,再回头对照书中的原理实现。

Adaboost的思路是串行训练多个弱分类器,每个新分类器更关注上一轮被分错的样本。书中用单层决策树作为弱分类器,通过调整样本权重实现“重点关注错分样本”。如果数据集类别不平衡,Adaboost的优势是能硬生生地把准确率拉上来,但代价是容易过拟合,在噪声较多的数据上要控制迭代次数。

4. 复现避坑:机器学习实战中五个常见的翻车现场

4.1 现象:运行书中代码直接报错,map返回的不是列表

书中大量代码基于Python 2编写,很多地方使用map()函数直接当作列表使用。比如map(float, line.split('\t'))在Python 2中返回列表,在Python 3中返回map对象,如果直接做下标访问或类型转换就会报错。原因:这本书出版时Python 2仍是主流,Python 3的map是惰性求值的迭代器,行为不一致。解决:遇到map调用改为列表推导式[float(x) for x in line.split('\t')],或者在外层加list()转换。

4.2 现象:手写数字识别准确率忽高忽低,同样的代码换数据就差一大截

原因:归一化和训练集/测试集的划分顺序不对,也就是数据泄露。在做kNN或者Logistic回归之前,如果先用全量数据计算了归一化的极差,测试数据的信息已经参与了训练过程,模型评估结果虚高。解决:先切分训练集和测试集,再在训练集上计算归一化参数,把参数应用到测试集上。

4.3 现象:读取中文文本数据时出现乱码或UnicodeDecodeError

原因:书中示例都是英文文本,中文数据集在读取时没有显式指定编码格式。解决:用open时指定编码,路径和文件名也尽量避免中文字符。推荐统一使用encoding='utf-8'读取,如果文件是GBK编码就改成gbk。

with open('news_data.txt', 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines()

4.4 现象:模型在训练集上准确率接近100%,在测试集上却很糟糕

原因:决策树和Adaboost在未限制深度或迭代次数时容易过拟合,模型把训练数据里的噪声也学进去了。解决:决策树设置最大深度或剪枝参数,Adaboost控制迭代次数,并通过交叉验证观察训练集和验证集的准确率差距。书中的决策树代码没有内置剪枝逻辑,使用时要自己加深度限制。

4.5 现象:使用SVD压缩数据后,矩阵重建结果看起来完全不对

原因:SVD分解时选择了过少的奇异值数量,保留的信息量不足以代表原始数据。解决:根据奇异值能量比来决定保留个数,通常保留能量占比90%以上的成分。计算方式是取奇异值平方之和,与总和的比值超过0.9就认为信息保留足够。

5. 无监督与降维:K-means、FP-growth和PCA在真实场景怎么配合

5.1 K-means的k怎么定:手肘法与轮廓系数的实际操作

书中的K-means实现是经典的随机初始化加迭代更新。算法本身不难,但实际项目里最让人头疼的是K值的选择。K值定小了,聚类结果合并了不相关的簇;定大了,簇之间边界模糊,业务上没法解释。

def dist_eclud(vec_a, vec_b): return np.sqrt(sum((vec_a - vec_b) ** 2)) def k_means(data_set, k): m = data_set.shape[0] cluster_assment = np.zeros((m, 2)) centroids = init_centroids(data_set, k) cluster_changed = True while cluster_changed: cluster_changed = False for i in range(m): min_dist = float('inf') min_index = -1 for j in range(k): dist = dist_eclud(centroids[j], data_set[i]) if dist < min_dist: min_dist = dist min_index = j if cluster_assment[i, 0] != min_index: cluster_changed = True cluster_assment[i] = min_index, min_dist for cent in range(k): pts_in_cluster = data_set[np.nonzero(cluster_assment[:, 0] == cent)] centroids[cent] = np.mean(pts_in_cluster, axis=0) return centroids, cluster_assment

这段代码中,cluster_assment第一列存簇编号,第二列存距离。np.nonzero筛选属于当前簇的所有样本,np.mean计算新的质心。实际操作时,我会对K从2到10分别计算总误差平方和(SSE),画出手肘图,选取曲率最大处的K值;同时用轮廓系数验证,系数接近1说明聚类效果好,接近0说明簇之间有重叠。

5.2 FP-growth做关联分析:支持度与置信度的参数配合

FP-growth比Apriori高效的地方在于它只需要扫描数据库两次,第一次统计频繁项,第二次构建FP树,不需要像Apriori那样反复生成候选集和扫描数据。书中用FP-growth挖掘新闻网站点击流中的频繁项集,参数主要是最小支持度。

最小支持度的设定没有万能值。如果设置过高,频繁项集太少,发现不了有价值的关联;设置过低,频繁项集爆炸,计算机内存吃紧,产出的规则大多是无意义组合。常见做法是先设一个较高的支持度观察频繁项数量,再逐步降低,直到频繁项数量达到业务可分析的范围。置信度用于过滤规则,支持度用于过滤项集,两个参数要分开调,先定支持度再定置信度,否则很难判断是哪个环节导致规则质量差。

5.3 PCA与SVD降维:保留多少主成分

PCA在书里用来对半导体数据进行降维,SVD用来做协同过滤推荐。两者的共同问题是“留多少维度”。PCA的处理方式是看主成分方差占比,一般要求累计方差贡献率达到85%到95%。SVD则看奇异值能量比。

def pca(data_mat, top_n_feat=9999999): mean_vals = np.mean(data_mat, axis=0) mean_removed = data_mat - mean_vals cov_mat = np.cov(mean_removed, rowvar=False) eig_vals, eig_vects = np.linalg.eig(np.mat(cov_mat)) eig_val_ind = np.argsort(eig_vals) eig_val_ind = eig_val_ind[:-(top_n_feat + 1):-1] red_eig_vects = eig_vects[:, eig_val_ind] low_data_mat = mean_removed * red_eig_vects return low_data_mat, red_eig_vects

np.cov(..., rowvar=False)表示每列是一个特征,求解协方差矩阵后通过特征值分解得到主成分方向。argsort排序后从大到小取前top_n_feat个特征向量。降维之后的数据可以用重构公式还原到原空间,如果还原误差很大,说明保留的维度不够。

6. 一个值得长期保留的调试习惯:用最小复现路径把参数一遍过

这本书的代码读通之后,最实用的做法是把每个算法都压缩成一个最小复现脚本,只有三样东西:固定的输入数据、一个核心函数、一组可以打印输出的中间变量。不要一上来就接完整业务数据,先用书里自带的小数据集跑通,再换自己的数据。

比如用kNN判断手写数字时,我的调试流程是这样的:先用十张图片跑通识别流程,确认特征提取和距离计算的输出数值合理,再扩大到全部训练集。一旦发现准确率下降,优先检查归一化参数是否沿用了训练集的统计值,而不是急着调K值。用Logistic回归时,把每次迭代的损失值打印出来,如果曲线在前几十轮没有明显下降,说明学习率太小或特征尺度差异过大,先归一化再训练。

我自己的一个习惯是:每个算法跑完后,把最佳参数、数据来源、运行结果三样信息记录在一个Markdown表格里。书的章节很多,算法之间容易串,记录能帮你快速知道哪个参数组合在自己数据集上验证过。曾经有个课程设计需要对比三种分类器,我因为没有记录上次SVM的核函数参数,重新调了两天才找到最优组合。从那以后,我每次做实验都强制走一遍“最小复现→参数记录→换数据集验证”的流程,不知道省了多少回头路。希望这本书的实战内容也能成为你手里的那套工具,帮你在做机器学习作业、期末设计或者实际项目时少走弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询