1. 从回归到分类:为什么"预测数值"和"预测类别"是两个世界
上一篇笔记里,我把机器学习的整体框架捋了一遍,重点是线性回归和最小二乘法——用一条直线去拟合数据,预测房价、温度这种连续数值。今天这篇笔记2,要跨过一道关键的门槛:分类问题。
先说说我自己的理解。回归和分类,表面上看都是"根据输入预测输出",但骨子里逻辑完全不同。回归的输出是连续实数,比如明天气温22.5度,可以是22.5、22.6、22.7,无限接近都算合理;分类的输出是离散类别标签,比如一封邮件是垃圾邮件还是正常邮件,只有"是"和"不是"两种答案,不存在"0.7封垃圾邮件"这种中间态。你可能觉得这没啥大不了,但一旦开始动手写代码,你会发现这两种任务的损失函数、评估方式、甚至训练策略都大不一样。
这个区别在数学上的体现也很直接。回归任务的损失函数通常用均方误差(MSE),预测值和真实值差得越远,惩罚越大;而分类任务更常用交叉熵这类基于概率分布的损失,评判的是"你给出的概率分布和真实标签之间的差异"。换句话说,回归关心"差多少",分类关心"错没错"以及"错得多离谱"。
这篇笔记里,我会用一个非常经典的三分类数据集作为主线,依次聊聊数据怎么准备、两个最常用的入门算法(K近邻和逻辑回归)的原理与实现、以及评估分类模型时必看的几个指标。整个过程会附带完整的 Python 代码和我在实际跑实验时踩过的坑,希望能当作一份可以直接照着操作的 roadmap。
1.1 分类问题长什么样
分类问题按照类别数量分成两类:二分类和多分类。二分类是"是/否"问题,比如信用卡交易是否欺诈、病人是否患病;多分类则是"哪一个"的问题,比如手写数字识别里判断是 0 到 9 中的哪个数字,图像分类里判断这是猫、狗还是鸟。
还有一个值得放在这里的知识点:多分类任务里,类别之间可能是互斥的(一个样本只能属于一个类别),也可能是非互斥的(一个样本可以同时属于多个类别,比如一张图片里既有猫又有狗)。前者直接用单模型多分类输出就行,后者往往拆成多个二分类任务来处理,这叫多标签分类,是另一个话题。第一次接触分类的时候,先聚焦在互斥的多分类上就够了。
无论哪种分类任务,核心思想都是:学习一个从特征空间到类别标签的映射函数。特征就是你能观测到的数据属性,比如花的萼片长度、花瓣宽度,或者用户的年龄、消费金额。类别标签则是这件事物所属的种类。算法要做的,就是找出特征组合和类别之间的对应规律,然后用这个规律去预测没见过的新样本。
1.2 用鸢尾花数据集做主线,为什么选它
我做笔记的时候,总会挑一个足够简单、足够标准的数据集来贯穿全篇,这样讲解算法的时候,重点放在算法本身而不是数据清洗上。经典的三分类数据集——鸢尾花数据集做主线就很合适。它只有 150 个样本、4 个特征(萼片长、萼片宽、花瓣长、花瓣宽)、3 个类别(三种鸢尾花),规模小,跑起来快,可视化也直观,非常适合初学者反复折腾。
我用的数据加载代码很简单,一行就搞定:
import pandas as pd from sklearn.datasets import load_iris data = load_iris() df = pd.DataFrame(data.data, columns=data.feature_names) df['target'] = data.target print(df.head()) print(df['target'].value_counts())运行结果会告诉你,每个类别恰好 50 个样本,非常均衡。这样我们后续看准确率的时候,基本不会被类别不平衡干扰,理解起来更干净。等你把流程走通了,再换成那种 99% 都是负类的业务数据去体会不平衡问题的棘手,那又是另一座山。
我在第一次跑这个数据集的时候,犯过一个很低级的错误:训练之前没有打乱数据就直接切分,导致训练集和测试集里的类别分布不均匀,模型准确率低得吓人。还好当时多看了一眼输出,才意识到是顺序切分惹的祸。这一课让我养成了一个习惯:拿到数据,先 visualize 和检查分布,再谈建模。
2. 动手前必须先理清:数据集划分与特征标准化
好,有了数据,下一步不是直接丢给模型,而是先做两件看似琐碎但决定成败的事:划分训练集和测试集,以及对特征做标准化。这两个步骤如果处理不好,后面的所有结果都不可信。
2.1 训练集、验证集、测试集的分工
刚学机器学习的时候,我总以为"拿一部分数据训练,剩下拿去测试"就够了。后来才意识到,标准的做法其实更讲究。数据一般要分成三份:
- 训练集(Training Set):用来拟合模型参数,让模型学会特征到标签的映射。
- 验证集(Validation Set):用来调超参数和做模型选择,比如决定 K近邻里的 K 值、逻辑回归的正则化强度。
- 测试集(Test Set):模型完全定稿之后,才拿出来做最终评估,模拟"没见过的新数据"。
验证集和测试集不能混着用。原因很直白:如果你拿着测试集反复调参,模型的超参数其实已经"记住了"测试集中的信息,再用它评估就相当于开卷考试,分数虚高。这就是所谓的 Data Leakage(数据泄漏),初学者特别容易犯。
实际落地的时候,如果数据量不大,我经常用交叉验证(Cross-Validation)来替代固定的验证集。k-fold 交叉验证的思路是把训练数据切成 k 份,轮流拿其中一份当验证集,其余 k-1 份训练,最后把 k 次的结果取平均。这样每个样本都有机会当"验证集",超参数评估更稳定。常用 k 值是 5 或 10。sklearn 里一个cross_val_score就能搞定,非常方便:
from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier model = KNeighborsClassifier(n_neighbors=5) scores = cross_val_score(model, X_train, y_train, cv=5) print("CV accuracy:", scores.mean(), "+/-", scores.std())2.2 为什么标准化不能省
标准化是分类项目里另一个高频坑。K近邻、逻辑回归这类基于距离或梯度的算法,对特征尺度非常敏感。假设一个特征的单位是"厘米",范围是 0 到 10,另一个特征的单位是"平方米",范围是 100 到 10000,直接计算欧氏距离时,小尺度的特征几乎被大尺度的特征淹没,模型本质上只看了那个大尺度特征,这显然不是我们想要的效果。
解决办法是让所有特征都在同一个量纲下比较。最常用的手段是 Z-score 标准化:
[ z = \frac{x - \mu}{\sigma} ]
其中 (\mu) 是特征均值,(\sigma) 是标准差。处理之后,每个特征的均值是 0,标准差是 1,数值范围大致落在 -3 到 3 之间。sklearn 里用StandardScaler一行调用:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)注意一个关键细节:fit_transform只能用在训练集上,测试集只做transform。因为标准化的均值和标准差应该完全从训练数据计算,测试集只是被"平移缩放"一下而已。如果你对全部数据一起做fit,测试集的信息就提前泄漏到训练过程里了,这又是一个数据泄漏,只是藏得更深。
我后来在项目里养成了一个习惯:把"缩放器"和模型一起放进一个 Pipeline 里,这样训练和预测的时候缩放操作会自动执行,不会再因为手滑搞错训练集测试集的统计量:
from sklearn.pipeline import Pipeline pipe = Pipeline([ ('scaler', StandardScaler()), ('clf', KNeighborsClassifier(n_neighbors=5)) ]) pipe.fit(X_train, y_train)这段代码跑通之后,整个流程就顺了。接下来可以放心地进入算法本身。
3. K近邻:最不需要"训练"的分类算法
K近邻(K-Nearest Neighbors,简称 KNN)可以说是机器学习里最直观的分类算法,特别适合作为第一个深入研究的分类器。它的核心思想用一句话就能概括:物以类聚,人以群分。一个新样本进来,看看离它最近的 K 个训练样本都是什么类别,哪个类别票数最多,就把它归为哪一类。
3.1 KNN 的工作原理与几何直觉
KNN 是一个基于实例的学习方法(Instance-Based Learning)。它不像逻辑回归那样有一个明确的函数表达式和训练过程,而是把训练数据"存起来",等到预测时再去计算距离。所以严格来说,KNN 的"训练"只是把数据加载进内存,复杂度接近 O(1),真正的计算开销发生在预测阶段——每个新样本都要和所有训练样本算一遍距离。
距离度量最常用的是欧氏距离,也就是多维空间里的直线距离:
[ d(x, y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} ]
当特征做了标准化之后,这个距离的意义才合理。如果不做标准化,之前说的那个"大特征压倒小特征"的问题会让 KNN 的距离几乎失真,分类结果自然也就是一团糟。我看到过很多初学者直接用原始特征调 KNN,最后发现准确率上不去,还以为是自己代码写错了,其实问题出在数据预处理上。
K 值的选择对模型影响很大。K 太小(比如 K=1),模型对噪声和离群点非常敏感,决策边界会变得曲折复杂,容易过拟合;K 太大,决策边界过于平滑,会把不同类别的细节都磨掉,导致欠拟合。实际操作中,我一般会在 K=1 到 K=20 之间逐个尝试,配合交叉验证观察准确率的变化,选择一个"拐点"附近的值。如果不确定,K 取奇数通常是保险的选择,能避免投票打平的情况(但二分类打成平手其实很少见,不用太纠结)。
3.2 用 sklearn 实现 KNN 分类
鸢尾花数据集上用 sklearn 实现 KNN 非常简洁,核心代码就这几行:
from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report # 切分数据 X_train, X_test, y_train, y_test = train_test_split( df.iloc[:, :-1], df['target'], test_size=0.2, random_state=42 ) # 标准化 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 训练 + 预测 knn = KNeighborsClassifier(n_neighbors=5) knn.fit(X_train_scaled, y_train) y_pred = knn.predict(X_test_scaled) # 评估 print(classification_report(y_test, y_pred))跑完之后看一下classification_report,你会看到每个类别的精确率(precision)、召回率(recall)和 F1 值。在鸢尾花这个数据集上,K=5 配合标准化,测试集准确率通常能到 0.9 以上,效果非常可观。
如果你想手动实现一个简单的 KNN 来加深理解,核心伪代码也不复杂:
def knn_predict(X_train, y_train, x_new, k): # 计算 x_new 到所有训练样本的距离 distances = [euclidean_distance(x, x_new) for x in X_train] # 取距离最近的 k 个样本的下标 k_indices = np.argsort(distances)[:k] # 返回出现次数最多的标签 k_labels = [y_train[i] for i in k_indices] return Counter(k_labels).most_common(1)[0][0]手动实现一遍,你对 KNN 的记忆会深刻很多。尤其是"预测才计算距离"这个特性,只有自己写一次才有体感。
3.3 KNN 的优缺点与适用场景
KNN 最大的优点就是简单、可解释性强,几乎不需要调参(选个 K 就行),而且天然支持多分类。缺点是计算复杂度高,训练样本量大时预测很慢;对高维数据不太友好,因为维度一高,样本分布会变得稀疏,"近邻"这个概念本身就开始失去意义;另外它对特征的尺度极度敏感,特征标准化几乎是强制要求。
所以你问我什么时候用 KNN,我的回答是:数据量不大、特征维度不高、需要快速出个基线结果的场景。它非常适合当"第一个分类模型"——跑通流程、验证数据可分性、做交叉验证找感觉,然后再逐步切换到更复杂的模型。我在实际项目里,遇到简单的几何分布数据,KNN 的表现甚至能和一些复杂的深度模型打平,别小看它。
4. 逻辑回归:从线性到概率的那一步
提到逻辑回归,很多初学者会困惑:名字里带"回归",怎么大家都在用它做分类?这个历史遗留问题一度让我也很懵。简单说,逻辑回归本质上还是在线性回归的基础上,套了一层"压缩函数",把连续输出映射到 0 到 1 之间的概率值,然后基于概率做分类决策。换句话说,它是一根"披着回归外衣的分类器"。
4.1 sigmoid 函数:把直线"掰弯"
线性回归的输出是:
[ z = w^T x + b ]
这个 (z) 的取值可以从负无穷到正无穷。为了让输出变成"概率",我们引入 sigmoid 函数(也叫 logistic 函数):
[ \sigma(z) = \frac{1}{1 + e^{-z}} ]
它的形状是一条 S 形曲线,输入任何实数,输出都落在 (0, 1) 之间。当 (z=0) 时,输出是 0.5,这是分类决策的天然分界线:概率大于 0.5 判为正类,小于 0.5 判为负类。
sigmoid 函数神奇的地方在于,它不仅仅是一个"压缩"操作,还保证了输出可以被解释为某种概率,而且整个函数是平滑可导的,方便我们用梯度下降来优化参数。我在学习的时候把它类比成"把一根无限长的直线揉成一个弹力球再放进 0 到 1 的盒子里",虽然不精确,但确实帮助我建立了直觉。
4.2 从二分类到多分类的扩展
逻辑回归原生解决的是二分类问题。那多分类怎么办?最常用的策略有两种:
- 一对一(One-vs-One, OvO):每两个类别之间训练一个二分类器,预测时让所有分类器投票,得票最多的类别胜出。
- 一对多(One-vs-Rest, OvR):每次把一个类别当作正类、其余所有类别当作负类,训练 K 个分类器,预测时选择输出概率最高的那一个。
sklearn 里的LogisticRegression对于多分类问题,默认用的是一种叫 multinomial 的 softmax 回归方式,它直接用 softmax 函数把输出变成一组类别概率分布。softmax 函数的公式是:
[ P(y=k|x) = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}} ]
它把 K 个类别的原始分数转换成总和为 1 的概率。这个思路可以看作 sigmoid 在多分类上的推广,不用真的去训练多个二分类器,而是一次性建模所有类别的概率,通常效果更好,也更简洁。
from sklearn.linear_model import LogisticRegression logreg = LogisticRegression(max_iter=200, multi_class='multinomial') logreg.fit(X_train_scaled, y_train) y_pred_lr = logreg.predict(X_test_scaled) print(classification_report(y_test, y_pred_lr))需要注意一个参数:max_iter。逻辑回归用迭代优化算法求解参数,默认的最大迭代次数在某些数据集上可能不够用,控制台会蹦出 ConvergenceWarning 警告。如果你看到这个警告,直接把迭代次数调大(比如 200、500),或者换用求解器solver='lbfgs',一般就能解决。这个坑我踩过一次,当时还以为模型出了问题,折腾了半天才发现只是迭代次数不够。
4.3 逻辑回归的可解释性与正则化
相比 KNN 的"黑箱投票",逻辑回归最大的优势是可解释性。模型训练完,你会得到每个特征对应的权重系数 (w_i)。权重的绝对大小代表该特征对分类结果的影响程度,权重为正表示该特征值越大,正类概率越高;权重为负则相反。这在不少业务场景下非常受用——我不光要预测,还要知道"为什么"。
举个例子,训练完逻辑回归后,你可以直接打印每个特征的系数:
for feature, coef in zip(data.feature_names, logreg.coef_[0]): print(f"{feature}: {coef:.3f}")如果发现花瓣长度这个特征的系数是 3.6,而萼片宽度只有 -0.2,那就可以初步判断:花瓣长度是区分这种花类的关键指标,萼片宽度影响不大。这种分析能力是很多复杂模型给不了的。
正则化也是逻辑回归里必须理解的概念。模型为了在训练集上尽可能准确,可能会把权重系数调得很大,导致泛化能力下降。正则化就是在损失函数里加上权重的"惩罚项",让权重尽量小。sklearn 中常见的参数是penalty='l2'(默认)和penalty='l1'。L2 正则化会让权重整体变小但不强制为 0,适合大多数场景;L1 正则化会让部分权重直接变成 0,相当于自动做特征选择。我一般先默认用 L2,如果特征特别多、怀疑有冗余,再试试 L1 看效果。
5. 模型效果怎么算数:混淆矩阵背后的信息量
分类模型训练完了,测试集准确率 0.93,是不是就万事大吉了?不一定。准确率(Accuracy)只是分类模型评估里最基础、最容易受"骗"的指标,尤其是类别不平衡的时候。这时候,必须看更细粒度的指标:精确率、召回率、F1 值和混淆矩阵。
5.1 混淆矩阵里四个格子,每一格都有含义
混淆矩阵是一个 2x2(多分类就是 KxK)的表格,行代表真实标签,列代表预测标签。以二分类为例,四个格子分别是:
- TP(True Positive):真实是正类,预测也是正类。
- TN(True Negative):真实是负类,预测也是负类。
- FP(False Positive):真实是负类,预测却成了正类(误报)。
- FN(False Negative):真实是正类,预测却成了负类(漏报)。
每次看到这四个格子我都会提醒自己:准确率只是"对着的那部分占总数的比例",而这四个格子才真正告诉你模型在哪些地方犯错、犯的是什么性质的错。有的场景怕误报,比如垃圾邮件过滤——把正常邮件误判为垃圾邮件,用户可能连重要邮件都收不到;有的场景怕漏报,比如癌症筛查——把病人误判为健康,后果更严重。混淆矩阵能帮你针对性地看问题。
5.2 precision、recall、F1 解读
从这四个格子可以派生出几个经典指标:
- 精确率(Precision):预测为正类的样本中,有多少是真正类?公式是 TP / (TP + FP)。它回答的问题是"我说你是正类,到底准不准"。
- 召回率(Recall):真实的正类样本中,有多少被成功找出来了?公式是 TP / (TP + FN)。它回答的问题是"正类样本,我有没有漏掉"。
- F1 值:精确率和召回率的调和平均,兼顾两者。公式是 2 * Precision * Recall / (Precision + Recall)。
直观理解:精确率越高,模型"宁缺毋滥";召回率越高,模型"宁可错杀一千,不可放过一个"。F1 高,说明模型在两个目标之间取得了较好的平衡。
sklearn 里一行代码就能输出所有指标:
from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(y_test, y_pred) print(cm) print(classification_report(y_test, y_pred))多分类问题中,classification_report会为每个类别单独计算精确率、召回率和 F1,并把它们平均。这就比单纯报告一个准确率有信息量得多。
5.3 对 KNN 和逻辑回归的结果做对比
用一个简单的表格把 KNN 和逻辑回归在这个数据集上的表现放在一起看:
| 模型 | 测试集准确率 | 可解释性 | 训练耗时 | 预测耗时 | 适合场景 |
|---|---|---|---|---|---|
| KNN(K=5) | 约 0.93 | 较强 | 几乎为 0 | 较慢(需算距离) | 小样本、低维、快速基线 |
| 逻辑回归 | 约 0.93 | 强(有权重系数) | 快 | 极快 | 需要解释、特征较多、业务场景 |
你可能注意到两者的准确率差不多。在小规模标准数据集上,这很正常。真正让你做选择的不是准确率,而是项目的具体约束:需要解释特征的影响,就用逻辑回归;数据分布极度非线性、但样本少特征低维,KNN 反而更省心。这就是为什么我会建议初学者两个算法都实现一遍,然后遇到新问题再去对比选型。
6. 笔记2阶段的常见误区与排雷经验
学到这里,我在学习分类的过程中踩过不少坑,也看小伙伴踩过不少坑。把它们集中写在这一节,算是一份"避坑清单",能帮你省下大把调试时间。
6.1 对全部数据做标准化,测试集信息泄漏
这是最隐蔽也最常见的坑。很多初学者(包括我)上手时习惯写:
scaler.fit(X_all) X_all_scaled = scaler.transform(X_all)然后才去切分训练集和测试集。这会让测试集的数据分布信息进入"缩放器",进而在训练时被模型间接"看到"。严格来说,scaler只能从训练集上学习均值和方差。写完代码之后,用train_test_split先切分,再对训练集fit_transform、对测试集只transform,这个顺序不能乱。
6.2 KNN 不标准化,准确率上不去
KNN 基于距离计算,特征尺度不同会导致距离失真。我试过用原始特征直接跑 KNN,准确率在 0.85 左右徘徊;做了标准化之后,直接跳到 0.93 以上。如果你发现 KNN 的表现怎么调都不对劲,先用 StandardScaler 处理特征再试一次,这一条往往立竿见影。
6.3 逻辑回归不收敛,先看 max_iter
训练逻辑回归时,控制台出现 ConvergenceWarning,很多人的第一反应是换算法。其实这大概率只是迭代次数不够。把max_iter从默认值调到 200 或更大,或者把solver换为lbfgs,警告就会消失。如果调大之后还是不收敛,再回头检查特征是否标准化、数据里是否有缺失值或极端离群点。
6.4 只看准确率,忽略类别不平衡
在鸢尾花这种均衡数据上,准确率有一定参考价值。但真实项目里,比如 99% 的样本都是负类的欺诈检测,一个全预测负类的模型也能有 99% 准确率,却毫无用处。这时候必须看精确率、召回率和混淆矩阵。我还见过有人为了提升准确率,给正负样本强行过采样导致过拟合,这些都是后续进阶的内容。作为笔记2,现阶段要养成的习惯是:每次评估模型,至少打印一次 classification_report。
6.5 不设 random_state,复现困难
无论是train_test_split还是交叉验证,都建议固定随机种子,比如random_state=42。不固定的话,每次运行代码切分的数据都不一样,模型结果会有波动,自己都觉得不稳定。固定种子之后,实验结果才可复现,调试和对比才有意义。这一点在学习阶段尤其重要,它会帮你建立"实验可重复"的好习惯。
我自己在写笔记2的时候,最大的收获倒不是会调包用 sklearn,而是真正理解了"数据预处理 → 模型选择 → 评估反馈"这个闭环。每一个环节出问题,最终都会反映在指标上,但指标不会告诉你错在哪一步,你得自己一层层回溯。这也是我坚持在笔记里记录报错和异常的原因——下次遇到类似问题,翻笔记能直接找到答案。
7. 扩展思考:下一步该学什么
写到这里,笔记2的主体内容已经完整覆盖了分类问题的入门链路。但我还是想多聊几句,因为学习机器学习最怕的就是"只会调包、不懂原理"。笔记2阶段学完 KNN 和逻辑回归,下面有几条路值得接着走。
7.1 从线性到非线性:决策树与随机森林
逻辑回归是线性模型,KNN 虽然能刻画非线性边界,但受限于距离计算。决策树则是另一种思路:通过一系列"如果特征值大于多少,就走左边"的规则,把特征空间逐步切分。单棵决策树容易过拟合,但把很多棵树集成起来(随机森林、梯度提升树),往往能在分类任务上达到非常强力的表现。我在实际项目里,随机森林经常作为比逻辑回归更"能打"的默认选择。
如果你手头已经跑通了今天的代码,下一步可以尝试用DecisionTreeClassifier和RandomForestClassifier替换 KNN 和逻辑回归,看看同一个数据集上的准确率和可解释性有什么变化,这会让你对模型的多样性有直观感受。
7.2 特征工程:提升模型上限的关键
同样的数据,特征工程做得好不好,模型差距可能巨大。比如,原始特征只有花瓣长度和宽度,但两个特征的比值(花瓣长宽比)可能是一个更强的分类信号。这类领域知识驱动的特征构造,在机器学习项目里非常重要。此外,特征选择(删掉冗余特征)、特征组合(多项式特征)也是提升模型表现的有效手段。
7.3 模型解释与业务落地
如果你在真实业务场景里做分类,报告准确率只是开始。业务方会问:"哪些特征导致了这个用户被标记为高风险?""为什么这批样本被误判了?"这时候逻辑回归的系数、决策树的结构都能派上用场。而像 SHAP、LIME 这类模型解释工具,可以把复杂模型的行为拆解成特征贡献度,是进阶必学的技能。
学习建议:别急着上深度学习框架,先把经典模型吃透。KNN 和逻辑回归虽然简单,但它们是理解"损失函数、优化、评估、泛化"这些核心概念的绝佳载体。把这个闭环体验完整了,后面学 SVM、神经网络都会顺畅很多。
这篇笔记写到这里,我自己也相当于把分类入门重新梳理了一遍。下一步我打算继续更新笔记3,主题是决策树和集成学习,顺便对比一下它们在鸢尾花和另一个模拟数据集上的表现差异。如果你也正在这个阶段摸索,可以按照这篇文章的顺序先把 KNN 和逻辑回归的代码跑通,再回来一起讨论。