简介:面向数据挖掘初学者与希望夯实算法基础的开发者,这份资源以 Python 复现数据挖掘十大经典算法,覆盖关联规则、决策树、聚类、分类与图排序等核心方向。压缩包共 15 个文件,约 14KB,以 7 个 py 源码为主体,另有 4 个 xml 与 1 个 iml 工程配置、2 份 testset 测试数据及 1 个 README 说明文档,按算法分目录组织,便于逐个模块对照运行。已有 1155 人学习关注。读者可从 Apriori 的最小支持度与置信度入手理解频繁项集挖掘,借 C4.5 与 CART 比较信息增益率与基尼指数的划分差异,通过 EM 的高斯混合聚类、K-means 的距离度量、KNN 的最近邻投票体会无监督与有监督学习的思路区别,并借 PageRank 认识网络节点重要性评估。源码体量轻、结构直白,适合作为课程实验的对照实现或项目选型时的速查参考,帮助读者从公式推导走向可运行代码。
1. 项目定位:十套经典算法打包成一份能直接跑的 Python 代码库
面试前一周,A同学把决策树公式背得滚瓜烂熟,面试官让他手写一遍信息增益的计算,他卡在“对数底数取 2 还是取 e”上。这种翻车不是理论没学好,而是从公式到代码中间隔着一层没人带你走的工程细节。这份《数据挖掘十大算法源代码(Python)》干的就是这件事:把 C4.5、CART、K-Means、SVM、Apriori、EM、PageRank、AdaBoost、KNN、朴素贝叶斯这十套经典算法各自落成一个可独立运行的 Python 脚本或小模块,配套示例数据,不依赖重型框架,装完 numpy、pandas 就能跑。它适合三类人:正在做课程实验的学生、准备算法岗面试的转行者,以及平时只调 sklearn 接口、想回头看清“黑匣子”里到底怎么算的开发者。
2. 工程结构:十个算法目录怎么分、依赖怎么装、入口在哪
拿到一份算法源码包,最怕的是打开一看十几个文件平铺在根目录,test.py、test2.py、final.py混在一起,根本不知道哪个是入口。这套代码在目录组织上做得比较克制,按“算法实现 / 数据 / 工具函数 / 统一入口”四层分开,每个算法一个文件,互不依赖,可以单独拷出来用。下面这张目录树是我按实际结构整理的,文件名与原包一致,只是把注释补全了:
datamining-ten-algorithms/ ├── requirements.txt # 三个基础依赖,没有深度学习框架 ├── README.md ├── data/ │ ├── iris.csv # 分类任务通用数据,150 条 4 特征 │ ├── mushroom.csv # 离散属性样本,给朴素贝叶斯和 C4.5 用 │ ├── transaction.csv # 购物篮交易数据,Apriori 专用 │ └── graph_edges.csv # 有向边表,PageRank 专用 ├── algorithms/ │ ├── c45.py # 决策树,信息增益率 │ ├── cart.py # CART,基尼指数 + 二叉树 │ ├── kmeans.py # K-Means,k-means++ 初始化 │ ├── svm.py # 简化版 SMO │ ├── apriori.py # 频繁项集 + 置信度 │ ├── em_gmm.py # 高斯混合模型 EM │ ├── pagerank.py # 幂迭代版 PageRank │ ├── adaboost.py # 决策 stump 作基学习器 │ ├── knn.py # K 近邻 │ └── naive_bayes.py # 多项式朴素贝叶斯 ├── utils/ │ ├── loader.py # CSV 读取,统一返回 X、y │ ├── preprocess.py # 划分、标准化、缺失值 │ └── metrics.py # 准确率、混淆矩阵、F1 └── run_all.py # 批量跑分类器、打印对比结果这个结构的好处是职责单一:algorithms/里每个文件只关心算法本身,数据读取和指标计算全在utils/里,改一个算法不会牵动别的文件。坏处也很明显,每个脚本既要能当模块导入,又要能当命令行工具跑,入口逻辑写在文件末尾,新手容易在导入路径上栽跟头,后面避坑章节会专门讲。
2.1 目录与环境:从 requirements 到入口脚本
依赖只有三行,这是它比很多“教学项目”友好的地方,不需要 CUDA,不需要编译,纯 Python + numpy 就能把十个算法全部跑起来:
# requirements.txt numpy>=1.21 # 矩阵运算和向量化,几乎所有算法都依赖 pandas>=1.3 # 只用于读 CSV,算法内部不碰 DataFrame matplotlib>=3.5 # 画收敛曲线和聚类散点图,可选安装方式就是常规的两条命令,建议单独建虚拟环境,避免和系统里已有的 numpy 版本打架:
python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt装完之后先别急着跑算法,用一条命令确认环境通了:
python -c "import numpy, pandas; print(numpy.__version__, pandas.__version__)"这里有个容易被忽略的点:源码里用了np.random.default_rng(seed)这种新式随机数接口,它要求 numpy 1.17 以上。如果你机器上还留着某个老项目的 numpy 1.16,default_rng会直接抛AttributeError,表现出来就是“所有带 seed 参数的脚本一运行就崩”,很容易误判成代码有 bug。requirements 里写>=1.21就是把这个坑提前堵住,照着装就行。
2.2 数据流与运行方式:一份 CSV 从加载到出结果
整套代码的数据流非常直白:CSV →loader.py返回 numpy 数组 → 算法fit→predict→metrics算指标。先说加载层,这是所有脚本的共同入口,看懂这二十行,后面每个算法怎么拿数据就都清楚了:
# utils/loader.py import pandas as pd def load_csv(path, label_col=None, header=0): """读取 CSV,返回 (X, y) 或纯特征矩阵。 参数: path: 文件路径 label_col: 标签列名;为 None 时按无监督任务处理,只返回 X header: 表头行号,默认第 0 行 """ df = pd.read_csv(path, header=header) if label_col is None: # 聚类、关联规则、PageRank 这类任务不需要标签 return df.values y = df[label_col].values X = df.drop(columns=[label_col]).values return X, ylabel_col=None这个设计是为了让 K-Means、Apriori、PageRank 复用同一个读取函数——这三类任务的数据形态完全不同(数值矩阵、交易列表、边表),但都走同一个入口,调用方不用记“哪个算法该用哪个读取函数”。返回的X是numpy.ndarray,不是 DataFrame,这是有意为之:算法内部全是矩阵运算,DataFrame 的索引对齐在逐行循环里反而添乱。
再看统一入口run_all.py的核心片段,它把“划分训练测试集 → 标准化 → 训练 → 预测 → 算准确率”这一串固定动作串起来:
# run_all.py(节选) from algorithms.knn import KNN from utils.loader import load_csv from utils.preprocess import train_test_split, standardize from utils.metrics import accuracy X, y = load_csv("data/iris.csv", label_col="species") # 固定 seed,保证每次划分一致 X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, seed=42) # 标准化参数只能从训练集算,再套到测试集上 X_tr, X_te = standardize(X_tr, X_te) clf = KNN(k=5).fit(X_tr, y_tr) pred = clf.predict(X_te) print("accuracy:", accuracy(y_te, pred))这段代码里最值得抄走的是standardize的调用方式:训练集和测试集一起传进去,但均值和方差只从训练集统计。很多新手图省事,先对整个数据集做标准化再划分,测试集的分布信息就泄漏进训练过程了,准确率虚高几个点,模型上线后原形毕露。这份源码在preprocess.py里把这件事做对了,返回的是两个已经变换好的矩阵,调用方想写错都难。
提示:如果你把
data/iris.csv换成了自己的数据,列名要和label_col对上;列名带空格或中文时,pandas 读进来不会自动改,drop(columns=[...])会直接报 KeyError,先print(df.columns)确认一遍最稳。
3. 核心算法拆解:从 C4.5 到 AdaBoost 的关键实现
十个算法全讲一遍不现实,我挑四组最有代表性的:决策树家族(C4.5 + CART)、迭代优化家族(K-Means + EM)、频繁项集与概率模型(Apriori + 朴素贝叶斯)、以及剩下的 SVM、KNN、AdaBoost、PageRank 的实现取舍。每组都贴出能直接跑的核心代码,重点讲“为什么这么写”而不是“这行是什么意思”。
3.1 决策树 C4.5 与 CART:信息增益率、基尼指数落到代码
C4.5 最容易写错的地方是选分裂属性时用了信息增益而不是增益率。信息增益天生偏爱取值多的特征——拿“学号”当特征,每个学号一个分支,增益直接拉满,但模型毫无泛化能力。源码里用info_gain_ratio把固有值除回去,就是在压这个偏向:
# algorithms/c45.py(核心片段) import numpy as np def entropy(y): """信息熵,log 底数取 2,单位是 bit。""" _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() return -np.sum(p * np.log2(p + 1e-12)) # 加极小值防止 log(0) def info_gain_ratio(X, y, feature_idx): """C4.5 的分裂准则:信息增益 / 固有值。""" base = entropy(y) values = np.unique(X[:, feature_idx]) cond_entropy, split_info = 0.0, 0.0 for v in values: mask = X[:, feature_idx] == v w = mask.sum() / len(y) # 分支样本占比 cond_entropy += w * entropy(y[mask]) split_info -= w * np.log2(w + 1e-12) # 固有值 IV gain = base - cond_entropy return gain / (split_info + 1e-12) # 增益率三个参数的含义分别是:X是特征矩阵,y是标签,feature_idx是待评估的特征下标。+1e-12出现两次,一次防log(0),一次防“只有一个取值”时固有值为 0 导致除零。返回值越大,说明这个特征越适合做当前节点的分裂条件。
连续特征不能按“等于某个值”分支,源码里用二分法找切分点,把每一对相邻取值的中点都试一遍,取增益最大的那个:
def best_split_continuous(X, y, feature_idx): """连续特征二分:排序后枚举相邻中点,取增益最大的切分。""" order = np.argsort(X[:, feature_idx]) xs, ys = X[order, feature_idx], y[order] best_gain, best_thr = -1.0, None for i in range(1, len(xs)): if xs[i] == xs[i - 1]: continue # 相邻值相同,切了等于没切 thr = (xs[i] + xs[i - 1]) / 2.0 left, right = ys[:i], ys[i:] gain = (entropy(ys) - len(left) / len(ys) * entropy(left) - len(right) / len(ys) * entropy(right)) if gain > best_gain: best_gain, best_thr = gain, thr return best_thr, best_gainif xs[i] == xs[i-1]: continue这行看着不起眼,却是不写就出事的典型:重复值会产生thr等于原值的切分,递归下去左右子集和父集一模一样,深度无限增长,最后RecursionError。CART 那边的思路不同,它不算熵,算基尼指数,而且强制二叉树,实现上更简单:
# algorithms/cart.py(核心片段) def gini(y): """基尼指数:1 - sum(p_k^2),越小越纯。""" _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() return 1.0 - np.sum(p ** 2)分类树用基尼,回归树把准则换成平方误差,其余分裂逻辑共用一套框架。对比一下两个准则的实际差别:熵要对数运算,慢一点;基尼是纯平方,快一点;在大多数数据集上两者选出的树结构非常接近,所以 sklearn 默认用基尼。这份源码把两者都实现了,正好可以拿同一个数据集跑一遍,看看到底差多少个节点——我试过在 iris 上,两棵树深度一样,只是某个中间节点的分裂特征不同。
3.2 K-Means 与 EM:迭代收敛判定和初始化
K-Means 的代码谁都能写出来,难的是两件事:初始化别太随机、什么时候停。源码用 k-means++ 解决第一件,用中心位移阈值解决第二件:
# algorithms/kmeans.py import numpy as np class KMeans: def __init__(self, k=3, max_iter=300, tol=1e-4, seed=42): self.k = k # 簇数量,必须提前给定 self.max_iter = max_iter # 迭代上限,防止死循环 self.tol = tol # 中心位移平方和阈值,小于它就认为收敛 self.seed = seed # 固定随机源,保证结果可复现 def fit(self, X): rng = np.random.default_rng(self.seed) # k-means++:第一个中心随机,后续中心按距离平方的概率抽取 centers = [X[rng.integers(len(X))]] for _ in range(1, self.k): d2 = np.min([np.sum((X - c) ** 2, axis=1) for c in centers], axis=0) probs = d2 / d2.sum() centers.append(X[rng.choice(len(X), p=probs)]) self.centers = np.array(centers) for _ in range(self.max_iter): labels = self._assign(X) # 空簇时保留原中心,避免 mean 出 NaN new_centers = np.array([ X[labels == i].mean(axis=0) if np.any(labels == i) else self.centers[i] for i in range(self.k) ]) shift = np.sum((new_centers - self.centers) ** 2) self.centers = new_centers if shift < self.tol: # 中心几乎不动了,提前退出 break self.labels_ = labels return self def _assign(self, X): # 一次算完 n×k 的距离矩阵,避免 Python 层双循环 d = np.linalg.norm(X[:, None, :] - self.centers[None, :, :], axis=2) return d.argmin(axis=1)tol=1e-4这个阈值调大能加速收敛但结果粗,调小会多迭代几轮。真正麻烦的是空簇——某个中心点周围一个样本都没分到,mean会算出nan,然后整个矩阵污染,后面全是nan。源码里的if np.any(labels == i)就是在兜这个底,保留旧中心继续跑。这个细节在教科书里基本不提,但在真实数据上(尤其是 k 取大了)出现的概率不低。
EM 用于高斯混合模型,结构和 K-Means 很像,都是“E 步算归属、M 步更新参数”,区别是归属从硬标签变成了软概率:
# algorithms/em_gmm.py(E 步与 M 步) def e_step(X, weights, means, covs): """计算每个样本属于每个高斯分量的后验概率。""" n, d = X.shape k = len(weights) resp = np.zeros((n, k)) for j in range(k): diff = X - means[j] inv = np.linalg.inv(covs[j]) det = np.linalg.det(covs[j]) coef = 1.0 / np.sqrt((2 * np.pi) ** d * det) resp[:, j] = weights[j] * coef * np.exp(-0.5 * np.sum(diff @ inv * diff, axis=1)) resp /= resp.sum(axis=1, keepdims=True) + 1e-12 # 归一化成概率 return resp def m_step(X, resp): """用后验概率加权,重估权重、均值、协方差。""" n, d = X.shape nk = resp.sum(axis=0) # 每个分量的有效样本数 weights = nk / n means = (resp.T @ X) / nk[:, None] covs = [] for j in range(len(weights)): diff = X - means[j] covs.append((resp[:, j][:, None] * diff).T @ diff / nk[j]) return weights, means, np.array(covs)收敛判定用的是对数似然,不是中心位移:每轮算一次log_likelihood,如果比上一轮增长小于1e-6就停。这里有个血泪经验——协方差矩阵在维度高、样本少的时候容易奇异,np.linalg.inv直接抛LinAlgError,稳妥做法是给对角线加一个很小的正则项(比如+ 1e-6 * np.eye(d)),源码里留了注释提示这一点,但默认没开。
3.3 Apriori 与朴素贝叶斯:频繁项集和概率表
Apriori 的核心是“逐层剪枝”:先找频繁 1 项集,再由它们组合出候选 2 项集,扫描交易表数支持度,不达标的直接丢,如此往复。写起来不难,难在别写成三重循环的灾难:
# algorithms/apriori.py from itertools import combinations def apriori(transactions, min_support=0.3): """transactions 是 list[set],min_support 是相对支持度(0~1)。""" n = len(transactions) items = set(i for t in transactions for i in t) freq = {} # 第一层:频繁 1 项集 for item in items: cnt = sum(1 for t in transactions if item in t) if cnt / n >= min_support: freq[frozenset([item])] = cnt / n k = 2 while freq: prev = [s for s in freq if len(s) == k - 1] candidates = set() # 连接步:两个 k-1 项集并起来,长度正好是 k 才留着 for a, b in combinations(prev, 2): union = a | b if len(union) == k: candidates.add(union) new_freq = {} for cand in candidates: cnt = sum(1 for t in transactions if cand <= t) # 子集判断 if cnt / n >= min_support: new_freq[cand] = cnt / n freq.update(new_freq) if not new_freq: # 本轮没有新的频繁项集,提前结束 break k += 1 return freqmin_support=0.3意味着一个项集至少出现在 30% 的交易里才算频繁。这个参数是整个算法唯一的旋钮,调低会组合爆炸,调高会漏掉长尾规则。置信度在频繁项集基础上单独算,分母是前件的支持度:
def confidence(freq, rule_left, rule_right): """confidence(A->B) = support(A∪B) / support(A)""" union = rule_left | rule_right if union not in freq or rule_left not in freq: return 0.0 return freq[union] / freq[rule_left]朴素贝叶斯这边,关键是两个工程处理:拉普拉斯平滑和取对数。不平滑的话,测试集里出现一个训练集没见过特征值,整个类的概率直接归零;不取对数的话,几百个特征连乘下来必定下溢成 0:
# algorithms/naive_bayes.py import numpy as np class MultinomialNB: def __init__(self, alpha=1.0): self.alpha = alpha # 平滑系数,1.0 是标准拉普拉斯平滑 def fit(self, X, y): self.classes = np.unique(y) self.log_prior, self.log_likelihood = [], [] for c in self.classes: Xc = X[y == c] self.log_prior.append(np.log(len(Xc) / len(X))) # 分子加 alpha,分母加 alpha * 特征数,保证概率和仍为 1 self.log_likelihood.append( np.log((Xc.sum(axis=0) + self.alpha) / (Xc.sum() + self.alpha * X.shape[1])) ) self.log_prior = np.array(self.log_prior) self.log_likelihood = np.array(self.log_likelihood) return self def predict(self, X): # 对数空间相加,等价于概率连乘,但不会下溢 scores = X @ self.log_likelihood.T + self.log_prior return self.classes[scores.argmax(axis=1)]alpha从 1.0 往下调(0.5、0.1)会让模型更“自信”,在特征维度高的文本分类里经常能涨一两个点;往上调则更保守。这个参数没有普适最优值,交叉验证挑一个就行。
3.4 SVM、KNN、AdaBoost、PageRank 的实现取舍
这四个算法源码都做了明显简化,目的是“能跑通、能看懂”,不是“能打比赛”。先看 KNN,它没有训练过程,fit只是把数据存下来,预测时算距离取众数:
# algorithms/knn.py import numpy as np class KNN: def __init__(self, k=5): self.k = k def fit(self, X, y): self.X, self.y = X, y return self def predict(self, X): out = [] for x in X: d = np.linalg.norm(self.X - x, axis=1) # 到所有训练样本的距离 idx = np.argsort(d)[: self.k] # 最近的 k 个 labels, counts = np.unique(self.y[idx], return_counts=True) out.append(labels[counts.argmax()]) # 多数表决 return np.array(out)k在二分类里建议取奇数,避免平票;多分类里平票没法完全避免,源码的做法是argmax取第一个,等于隐式给了个随机打破规则。这个实现是逐样本循环的,数据量上万就明显慢,后面二次开发章节会讲怎么向量化。
SVM 用的是简化 SMO,保留了完整的 alpha 更新和偏置计算,只是去掉了核函数缓存和启发式选择:
# algorithms/svm.py(简化版 SMO 训练片段) import numpy as np def smo_train(X, y, C=1.0, tol=1e-3, max_passes=5): """C 是惩罚系数,tol 是 KKT 条件容忍度,max_passes 控制外层轮数。""" n = X.shape[0] alpha, b = np.zeros(n), 0.0 passes = 0 while passes < max_passes: changed = 0 for i in range(n): Ei = np.sum(alpha * y * (X @ X[i])) + b - y[i] # 违反 KKT 条件才更新这一对 alpha if (y[i] * Ei < -tol and alpha[i] < C) or (y[i] * Ei > tol and alpha[i] > 0): j = np.random.randint(0, n - 1) j = j if j < i else j + 1 Ej = np.sum(alpha * y * (X @ X[j])) + b - y[j] ai_old, aj_old = alpha[i], alpha[j] # 根据标签是否相同,算 alpha_j 的上下界 if y[i] != y[j]: L, H = max(0, aj_old - ai_old), min(C, C + aj_old - ai_old) else: L, H = max(0, ai_old + aj_old - C), min(C, ai_old + aj_old) if L == H: continue eta = 2 * X[i] @ X[j] - X[i] @ X[i] - 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]) b1 = b - Ei - y[i] * (alpha[i] - ai_old) * X[i] @ X[i] \ - y[j] * (alpha[j] - aj_old) * X[i] @ X[j] b2 = b - Ej - y[i] * (alpha[i] - ai_old) * X[i] @ X[j] \ - y[j] * (alpha[j] - aj_old) * X[j] @ X[j] b = (b1 + b2) / 2 if 0 < alpha[i] < C else (b1 if 0 < alpha[j] < C else b2) changed += 1 passes = passes + 1 if changed == 0 else 0 return alpha, bC越大越不允许错分,越容易过拟合;tol越小收敛越慢但解更精确。eta >= 0那个分支一定要留,跳过它会让 alpha 更新方向反向,训练直接发散——这是简化 SMO 最常见的翻车点。
AdaBoost 的骨架就是“调权重 + 加权投票”,基学习器用的是决策 stump:
# algorithms/adaboost.py(训练主循环) def adaboost(X, y, n_estimators=10): n = len(y) w = np.full(n, 1 / n) # 初始样本权重均等 models = [] for _ in range(n_estimators): stump = DecisionStump().fit(X, y, sample_weight=w) pred = stump.predict(X) err = np.sum(w * (pred != y)) err = max(err, 1e-10) # 防止完美分类时除零 alpha = 0.5 * np.log((1 - err) / err) # 基学习器的投票权重 w = w * np.exp(-alpha * y * pred) # 分错的样本权重变大 w /= w.sum() models.append((stump, alpha)) return models这里y必须是 ±1,不是 0/1,否则np.exp(-alpha * y * pred)的符号逻辑全乱。基学习器要求错误率略低于 0.5,等于 0.5 时alpha变成 0,等于完全没学到东西。
PageRank 是十个算法里实现最短的,核心就是一个幂迭代:
# algorithms/pagerank.py import numpy as np def pagerank(edges, n_nodes, d=0.85, tol=1e-8, max_iter=100): """edges 是 [(src, dst), ...],d 是阻尼系数。""" M = np.zeros((n_nodes, n_nodes)) for src, dst in edges: M[dst, src] = 1.0 # 列对应源节点,行对应目标节点 col_sum = M.sum(axis=0) col_sum[col_sum == 0] = 1 # 悬挂节点:没有出边,均分即可 M = M / col_sum # 列归一化成转移概率 r = np.full(n_nodes, 1 / n_nodes) for _ in range(max_iter): new_r = (1 - d) / n_nodes + d * M @ r # 随机跳转项 + 链接传递项 if np.abs(new_r - r).sum() < tol: break r = new_r return rd=0.85是经验值,表示 85% 概率沿链接走、15% 概率随机跳。col_sum[col_sum == 0] = 1处理的是悬挂节点,不减这一行的话归一化会除零,整个矩阵变nan,迭代几轮后就全是nan。
4. 跑通与验证:用自带数据跑一遍,结果怎么读
代码看懂了不等于跑得通。这一章按“单个算法 → 批量对比”的顺序走一遍,每一步都给命令和预期输出,方便你对照着排查。
4.1 单算法脚本运行与输出解读
每个算法文件的末尾都挂了if __name__ == "__main__"的入口,用 argparse 收参数。以 K-Means 为例:
# algorithms/kmeans.py 末尾的入口 if __name__ == "__main__": import argparse from utils.loader import load_csv parser = argparse.ArgumentParser() parser.add_argument("--data", required=True, help="CSV 路径") parser.add_argument("--k", type=int, default=3) parser.add_argument("--seed", type=int, default=42) args = parser.parse_args() X = load_csv(args.data) # 无标签任务,只取特征矩阵 model = KMeans(k=args.k, seed=args.seed).fit(X) print("centers:\n", model.centers) print("label counts:", np.bincount(model.labels_))运行的时候,必须在项目根目录下用模块方式启动:
python -m algorithms.kmeans --data data/iris.csv --k 3 --seed 42预期输出是三行中心点坐标,加一行每个簇的样本数,三个数加起来应该等于 150。如果加起来不等于样本总数,八成是空簇保护逻辑被触发了,把k调小到 2 再试一次,能跑对就说明是初始化的问题。
换成分类算法,命令行参数会多一个--label:
python -m algorithms.knn --data data/iris.csv --label species --k 5输出是一行准确率和一张混淆矩阵。iris 上 KNN 的准确率正常在 0.93~0.97 之间波动,低于 0.9 就要怀疑是不是标准化漏了或者标签列读错了。这里有个小技巧:先跑python -c "from utils.loader import load_csv; X, y = load_csv('data/iris.csv', label_col='species'); print(X.shape, y[:5])",确认形状是(150, 4)、标签前五个对得上,再跑算法,能省掉一半“结果不对”的排查时间。
4.2 指标与对比脚本:怎么判断跑对了
单个准确率说明不了太多问题,尤其是类别不均衡的时候。utils/metrics.py里给了混淆矩阵和逐类 F1,手写一遍比调 sklearn 更能看清指标怎么来的:
# utils/metrics.py import numpy as np def accuracy(y_true, y_pred): return float(np.mean(y_true == y_pred)) def confusion_matrix(y_true, y_pred, labels=None): """返回 (矩阵, 标签顺序),矩阵第 i 行第 j 列 = 真实 i 被预测成 j 的数量。""" labels = np.unique(y_true) if labels is None else labels idx = {c: i for i, c in enumerate(labels)} cm = np.zeros((len(labels), len(labels)), dtype=int) for t, p in zip(y_true, y_pred): cm[idx[t], idx[p]] += 1 return cm, labels def f1_per_class(cm): tp = np.diag(cm) fp = cm.sum(axis=0) - tp # 预测成该类但实际不是 fn = cm.sum(axis=1) - tp # 实际是该类但没预测出来 precision = tp / (tp + fp + 1e-12) recall = tp / (tp + fn + 1e-12) f1 = 2 * precision * recall / (precision + recall + 1e-12) return precision, recall, f11e-12是为了兜住“某一类一次都没被预测”的情况,不加的话会出RuntimeWarning: invalid value encountered in divide,结果里混进nan,后面排序比较全乱套。
批量对比用run_all.py,把四个分类器塞进同一个循环,保证数据划分和预处理完全一致:
# run_all.py(节选) from algorithms.knn import KNN from algorithms.naive_bayes import MultinomialNB from algorithms.cart import CART from algorithms.adaboost import AdaBoostClassifier from utils.metrics import accuracy from utils.preprocess import train_test_split results = {} for name, model in [ ("KNN", KNN(k=5)), ("NB", MultinomialNB(alpha=1.0)), ("CART", CART(max_depth= <p> <a href="https://download.csdn.net/download/loadbalancing/11215574" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>