打开这个数据集的时候,脑子里突然闪过一个问题:class 那一列里的 2 和 4,分别对应的是"良性"和"恶性"——但这两个数字背后,坐着的是谁?
699个女人
威斯康星乳腺癌数据集(Wisconsin Breast Cancer Dataset),算是机器学习领域的"Hello World"。699个样本,每个样本有10个细胞特征:clump_thickness(结块厚度)、uniform_cell_size(细胞大小均匀性)、uniform_cell_shape(细胞形状均匀性)、marginal_adhesion(边缘黏附力)、single_epithelial_size(单上皮细胞大小)、bare_nuclei(裸核)、bland_chromatin(淡染色质)、normal_nucleoli(正常核仁)、mitoses(核分裂)。
这些特征听起来抽象,却是病理医生在显微镜下实实在在看过的东西。
他们在数细胞层数,量细胞直径,看细胞边缘是光滑还是锯齿状,观察细胞核被染成什么颜色。每一个1到10的数字,都是人眼与人脑的判断——是经验的积累,是训练的痕迹。
数据里的每一行,曾经是一个女人坐在诊室里,等待活检结果。
细胞的语言
病理医生看细胞,和我们看人很像。
看形状——圆润的倾向于良性,畸形的倾向于恶性。看边界——边界清晰的通常良性,黏连在一起的往往恶性。看颜色——染色均匀的相对正常,染色质浓集发黑的令人担忧。
这不是玄学。是几千例病例训练出来的直觉。
而机器学习做的事情,简单来说就是:绕过医生的经验,用算法把这些"直觉"数字化、规则化。SVM(Support Vector Machine,支持向量机)在高维空间里找到一条最优分割线,把良性样本和恶性样本分开。KNN(K-Nearest Neighbors,K近邻)则是"物以类聚"——看一个未知样本的5个最近邻居是什么类别,由多数投票决定它属于哪一类。
KNN 选 K=5,是因为经验发现这个数值通常效果较好。太少容易受噪声影响,太多又会模糊边界。
SVM 的核函数(kernel)把细胞特征映射到高维空间,在那个空间里,线性不可分的数据变得可以分开。
10折交叉验证
直接用全部数据训练,然后测试,会发生什么?
模型可能"背住"了训练数据,而不是真的学会了规律。就像考试前背答案,遇到新题就傻眼。
10折交叉验证(10-fold cross validation)解决的是这个问题。把数据随机分成10份,轮流让9份当训练集、1份当测试集,做10次实验,最后取平均准确率。
结果是:KNN 96.6%,SVM 96.0%。看起来差不多,但实际上——
KNN 的标准差是 2.9%,SVM 是 3.3%。KNN 更稳定。
这意味着什么?KNN 在10次实验里波动较小,SVM 则有时表现更好、有时更差。对于实际部署来说,稳定比偶尔的高分更重要。
Precision、Recall、与生命的重量
但准确率(accuracy)不是唯一的指标。在医疗场景下,误诊和漏诊的代价是不同的。
Precision(精确率):预测为恶性的样本里,真正是恶性的比例。
Recall(召回率):所有恶性样本里,被正确识别出来的比例。
F1-Score是这两者的调和平均。
SVM 报告里的 Class 2(良性):
- Precision = 1.00:预测良性的,几乎全对
- Recall = 0.95:100个良性里,有5个被误判为恶性
SVM 报告里的 Class 4(恶性):
- Precision = 0.90:预测恶性的,10个里有1个其实是良性
- Recall = 1.00:所有恶性都被找到了
对于癌症筛查,Recall 更关键。漏诊一个癌症患者,比误诊一个良性肿瘤更危险——后者只需要进一步检查,前者可能延误治疗时机。
3%的准确率误差,在699个样本里意味着约21个人。21个女人,坐在诊室里,拿到了一张写有"建议进一步检查"的报告单。
技术可以给出概率,但无法替她们承担等待的重量。
算法从未见过真正的细胞。它只是在699个女人的经验里,找到了一个参考点。
附录
数据预处理
import numpy as np
import pandas as pd
from sklearn import preprocessing, model_selection
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.metrics import classification_report, accuracy_score
# 载入数据集
names = ['id', 'clump_thickness', 'uniform_cell_size', 'uniform_cell_shape',
'marginal_adhesion', 'single_epithelial_size', 'bare_nuclei',
'bland_chromatin', 'normal_nucleoli', 'mitoses', 'class']
df = pd.read_csv('data.csv', names=names)
# 替换缺失数据
df.replace('?', -99999, inplace=True)
# 删除无关特征
df.drop(['id'], axis=1, inplace=True)
数据划分
X = np.array(df.drop(['class'], 1))
y = np.array(df['class'])
X_train, X_test, y_train, y_test = model_selection.train_test_split(
X, y, test_size=0.2)
模型训练与交叉验证
seed = 8
scoring = 'accuracy'
models = []
models.append(('KNN', KNeighborsClassifier(n_neighbors = 5)))
# scikit-learn 在 0.22 版本中对 SVC 的默认参数进行了更改
# gamma 参数的默认值从 'auto' 改为 'scale'
# models.append(('SVM', SVC()))
models.append(('SVM', SVC(gamma='auto')))
results = []
names = []
for name, model in models:
# kfold = model_selection.KFold(n_splits=10, random_state = seed)
# 指定 seed 需要令参数 shuffle = True
kfold = model_selection.KFold(n_splits=10, shuffle = True, random_state = seed)
cv_results = model_selection.cross_val_score(model, X_train, y_train, cv=kfold, scoring=scoring)
results.append(cv_results)
names.append(name)
msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
print(msg)
输出:
KNN: 0.966039 (0.029270)
SVM: 0.960649 (0.032726)
模型预测与评估
for name, model in models:
model.fit(X_train, y_train)
predictions = model.predict(X_test)
print(name)
print(accuracy_score(y_test, predictions))
print(classification_report(y_test, predictions))
SVM 输出:
0.9642857142857143
precision recall f1-score support
2 1.00 0.95 0.97 95
4 0.90 1.00 0.95 45
accuracy 0.96 140
macro avg 0.95 0.97 0.96 140
weighted avg 0.97 0.96 0.96 140