ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

逻辑回归多分类实战:处理有序特征与类别不平衡的关键策略

逻辑回归多分类实战:处理有序特征与类别不平衡的关键策略

1. 先看清这个数据集再动手

Car Evaluation 这个分类任务,我前前后后跑过好几遍。六列特征、不到两千条样本,光看体量会以为是个入门练手题,实际上它把所有典型问题都藏在小体积里了:类别严重偏斜、特征全是有序分类变量、标签之间存在天然的等级关系。用逻辑回归做这个任务,最值钱的不是“跑出指标”,而是搞清楚每一步选择背后的原因——为什么这里不能直接给特征编号?为什么准确率很高模型却没什么用?为什么少数类永远学不好?这套思考方式,放到真实业务里照样能用。

所以这篇文章干脆完整记录一次从头到尾的实操,包括数据编码、模型训练、评估和分析,重点放在“为什么这么选”和“哪些地方容易翻车”。

1.1 六列输入特征到底在描述什么

这个分类任务的目标很直白:根据车辆的几个客观属性,判断这辆车“是否值得接受”。六个特征分别覆盖了购车和维护过程中的核心决策因素:

特征名含义取值
buying购买价格vhigh, high, med, low
maint维护费用vhigh, high, med, low
doors车门数量2, 3, 4, 5more
persons可载人数2, 4, more
lug_boot后备箱大小small, med, big
safety安全等级low, med, high

注意这些字段全部是离散的类别值,而且大多数带有顺序关系:价格最低是 low、其次是 med、high、vhigh;安全性从 low 到 med 再到 high。也就是说,这不是普通的无序分类变量,而是“有序分类变量”。这一步理解很关键,因为后面选择编码方式时,这个属性会影响模型结果。

目标字段是四个等级:unacc(不可接受)、acc(可接受)、good(好)、vgood(非常好)。表面看它是四分类问题,实际等级之间并不是独立的“颜色”关系,而是一个由低到高的序列——把 good 误判成 acc,和把 vgood 误判成 unacc,错误的严重程度完全不一样。这个标签间的序关系,会让分类器的误差分析变得更有意思。

1.2 级别分布才是第一个隐藏坑

任何拿到这个数据集的人,第一次看标签分布都会松一口气:样本量不大,类别也不复杂。但一旦统计每个类别的数量,问题就浮出来了。这份数据里 unacc 占了大约七成,acc 占两成左右,good 和 vgood 都只有百分之四上下。

拿这个分布直接训练逻辑回归,模型什么都不学,全预测 unacc 也能拿到大约 70% 的准确率。更麻烦的是,因为少数类样本太少,模型很容易把 acc、good、vgood 统统归入 unacc 这个大类里——毕竟只要把边界往“宽泛接受”的方向推一点,准确率并不会立刻掉太多。看到准确率 90% 以为自己做得很好,结果看混淆矩阵才发现少数类几乎全军覆没,这是这个项目里最常见的翻车方式。

因此,从加载数据开始就要有这样的意识:这个项目真正要解决的问题不是“四分类”,而是“在极度偏斜的类别分布下,怎么把少数类从多数类的阴影里捞出来”。

1.3 为什么值得专门跑一遍逻辑回归

有人会问,这种分类任务拿决策树或者随机森林来跑,效果不是更好吗?这确实是一个绕不开的点。但正因为数据特征全部是有序分类变量、样本量又小,结构并不复杂,逻辑回归在这里刚好能展现它的两面性:一方面它受特征编码方式影响很大,处理不好会明显拉低效果;另一方面,一旦把编码和类别不平衡处理到位,它能输出每个特征的系数,让你直接看出“安全性”和“价格”对判断结果的贡献方向。这种可解释性是树模型很难给的。

换句话说,这个项目很适合拿来练三个基本功:特征编码的选择、多分类逻辑回归的实现、类别不平衡下的评估方式。把这三件事想明白了,以后再遇到同类任务,就不会被数据和指标牵着走。

2. 逻辑回归做多分类的关键前提

逻辑回归在绝大多数人印象里是二分类模型,sigmoid 函数输出一个介于 0 和 1 之间的概率,以 0.5 为界划分正负类。但是 Car Evaluation 有四个类别,直接用逻辑回归会出现一个匹配问题:它默认的输出空间是“两个类别”,现在是“四个类别”,需要先做转换。

2.1 一对多和 Softmax 两条路怎么选

多分类逻辑回归常用两种做法。

第一种叫一对多(OvR),思路很朴素:把四分类拆成四个独立的二分类问题。训练时对每个类别训练一个分类器,比如“是否是 unacc”“是否是 acc”“是否是 good”“是否是 vgood”,预测时把样本分别送进四个分类器,取置信度最高的那个作为最终类别。优点是实现简单,每个二分类器独立训练,缺点是没有统一的概率尺度,四个分类器给出的分数未必可比较。

第二种叫多项式逻辑回归,也就是 Softmax 回归。它一次训练一个模型,输出一个长度为四的概率向量,四项之和恒为 1。公式可以写成:

P(y=k | x) = exp(w_k^T x) / Σ_j exp(w_j^T x)

这里 w_k 是第 k 个类别对应的权重向量,分母是对所有类别做归一化。相比 OvR,Softmax 的优势在于所有类别共享同一个概率框架,最后输出的每一项都可以直接解读成“模型认为属于某个类别的概率”,在后续做阈值调整和不确定性分析时更顺手。

在 scikit-learn 里,LogisticRegression 的 multi_class 参数默认是 auto,当检测到是多分类时会自动选择 multinomial,也就是 Softmax 方式。实际项目中我更倾向于显式指定 multi_class='multinomial',避免版本行为差异带来意外。除非样本量极大导致 Softmax 训练开销过高,否则这里没有理由用 OvR。

2.2 有序特征编码:小细节决定模型上限

特征编码是逻辑回归项目里最容易被低估的环节。六个特征全部是分类变量,必须先转成数值才能喂给线性模型。常见的做法有两种:Ordinal Encoding(有序编码)和 One-Hot Encoding(独热编码)。

有序编码的做法是对每个特征的有序取值直接映射为整数,比如 safety 的 low、med、high 分别映射成 0、1、2。好处是特征维度低、训练快,坏处也很明显——它隐含了一个强假设:从 low 到 med 的差距,和从 med 到 high 的差距,在线性空间里是相等的。换句话说,模型默认安全性每升一级,对结果的影响是等量线性的。这个假设在真实场景里基本不成立。

独热编码则把每个取值展开成独立的二值特征。比如 buying 有四种取值,就变成四列,每一列只能是 0 或 1。这样做的好处是不会强加距离假设,模型可以从数据中自由学习每个级别的独立影响;代价是特征维度膨胀,而且在样本量小的时候更容易稀疏。

我实际做过对比:在这个数据上直接用有序编码跑逻辑回归,效果往往不如独热编码稳定。原因不难理解,六个特征里的“等级”只是次序关系,不是数值关系,强行当成连续值处理,决策边界的位置就变得很别扭。所以在这个项目中,我建议对六个特征统一采用独热编码,让模型自己去估计每一档的权重。这也是我最终选择的做法。

2.3 分割数据时容易被忽略的分层问题

数据划分看似小事,翻车概率却很高。如果随机切分训练集和验证集,不控制类别比例,在 good 和 vgood 这类只有四五十条样本的类别上,很可能出现训练集有、验证集没有,或者反过来验证集中某个类别只有零星几条的情况。这时候评估结果会出现巨大的方差:换一次随机种子,指标就可能大幅波动。

正确的做法是使用分层抽样。在 scikit-learn 里就是 train_test_split 的 stratify 参数,按 y 的类别比例进行划分,保证训练集和验证集中每个类别的占比和原始数据基本一致。我习惯使用 80/20 的比例切分,同时设置一个固定的随机种子,保证实验可复现。这一个细节能在后面省掉很多自我怀疑的排查时间。

3. 实操全流程:从特征编码到模型训练

原理说得再多,终究要落到代码上。接下来这一段是完整的实操过程,每一步我都会说明当时是怎么考虑的。

3.1 数据装载与特征编码实现

读取数据时,约 1728 条样本,没有缺失值,六列特征全部是字符串型的类别值,目标列也是字符串。加载后先用独热编码转换特征部分。

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder from sklearn.linear_model import LogisticRegression from sklearn.compose import ColumnTransformer df = pd.read_csv("car_evaluation.csv") X = df.drop(columns=["class"]) y = df["class"] encoder = OneHotEncoder(handle_unknown="ignore") X_encoded = encoder.fit_transform(X)

这里 ColumnTransformer 其实可以更规范化,但因为所有特征都要做同样的转换,直接对整张表做 OneHotEncoder 也一样。转换后的矩阵是稀疏矩阵,后续喂给逻辑回归时不会有任何问题,逻辑回归本身就支持稀疏输入。

如果坚持用有序编码做对比实验,可以单独写一个映射表,把 low、med、high 这类值映射成整数。但我自己的结论是,在做逻辑回归时独热编码更稳妥。树模型则恰好相反,它们对有序编码更友好,因为树的分裂机制天然适应数值等级,这个差异也是后面模型对比时的重要解释变量。

3.2 模型配置与收敛问题

逻辑回归的超参数不多,但有几个地方必须注意。首先是 solver,也就是优化器的选择。在小规模数据上,lbfgs 是默认选项,适合大多数情况。其次是 max_iter,默认值是 100,但这个数据用了独热编码之后特征维度变大,训练时很容易出现收敛警告:ConvergenceWarning: Maximum number of iterations reached。我第一次跑就踩到了。

解决方案很简单,把 max_iter 调大,比如 1000,既能顺利收敛,训练时间也不会有明显增加。还有一个容易忽略的参数是 C,它是正则化强度的倒数,默认是 1.0。C 越小,正则化越强,模型越保守。在类别不平衡的场景下,适度调小 C 有时能降低模型对多数类的过度自信,我通常会拿 0.1 到 1.0 之间的几个值快速对比一遍。

model = LogisticRegression( max_iter=1000, multi_class="multinomial", solver="lbfgs", C=1.0, random_state=42, ) model.fit(X_train, y_train)

如果你是第一次跑,大概率会在这一步看到收敛警告。不用慌,它只是一个提示,不代表代码报错,但也不能无视——不收敛的模型权重没意义。

3.3 类别不平衡的三步调整

直接用上面的模型训练完,得到的准确率通常在 85% 到 90% 之间,看起来不错。但正因为样本分布偏斜,这个数字非常迷惑人。我按正确步骤处理类别不平衡时,用了三步:

第一步,先看分类报告和混淆矩阵,建立 baseline。这里会看到 unacc 的 F1 很高,而 acc、good、vgood 的 F1 很低,特别是 vgood 几乎预测不出来。

第二步,给模型加上 class_weight="balanced"。这个参数的原理是按类别频率反比调整权重,让少数类在计算损失时获得更高的惩罚权重。加上之后,多数类 unacc 会稍微让出一些边界,把更多样本判给少数类,少数类的召回率通常会明显提升。代价是整体准确率可能略微下降,这是正常现象。

第三步,结合业务场景选择最佳阈值。Softmax 输出的是四类概率,默认取概率最大的类别作为预测结果。但如果业务上更看重“不要漏掉好车”,就希望模型在拿不准时倾向于输出更高等级。这时可以自定义一个规则:比如当最高概率低于某个阈值且第二高概率接近时,选择等级更高的类别。这个调整不是必须做的,但它展示了逻辑回归输出概率的价值。

4. 评估要看到什么:混淆矩阵与逐类指标

模型训练完成并不代表工作结束,评估环节才是真正看出模型水平的地方。这个项目的评估难点同样藏在类别不平衡里。

4.1 先看整体准确率,再质疑它

先看整体准确率是最直观的操作。我随手跑了一次 baseline,准确率到了 88% 左右。很多人到这里就会觉得任务完成了,其实远远没有。

准确率的问题在于它把所有类别的错误一视同仁。在一个 unacc 占 70% 的数据集里,即使模型对 acc、good、vgood 三类完全没有区分能力,只要把所有样本都预测成 unacc,准确率也能到 70% 左右。稍微学一点边界,把一部分 acc 捞出来,准确率就奔着 90% 去了。但业务会满足于“能判断哪些车不可接受”吗?显然不是。我们真正关心的是能不能识别出省油、安全、价格合适的车,也就是少数类。

所以我评估模型从来不只盯准确率,而是先看分类报告和混淆矩阵,再综合 precision、recall、F1 来判断。

4.2 从混淆矩阵里读出错在哪

下面是一个带有典型特征的混淆矩阵示例,实际数字会随随机种子波动,但结构基本一致:

实际\预测unaccaccgoodvgood
unacc240500
acc304530
good4651
vgood2534

看这个矩阵能读出几件事。unacc 那一行几乎全对,说明多数类学得很好。acc 那一行里,有不少样本被误判成 unacc,说明模型对“可接受”和“不可接受”的边界划分偏保守。good 和 vgood 之间的混淆更明显,这两个类本身等级接近,特征差异很小,误判成相邻等级比误判成 unacc 更容易理解。

如果只看准确率,永远看不到这些信息。所以就算只做一分钟的评估,也要打印 classification_report,逐类看 precision 和 recall,才能知道模型真正擅长什么、放弃什么。

4.3 与树模型对比:是模型输了,还是目标订错了

我跑完逻辑回归之后,顺手用同样的数据跑了一棵决策树作为对照。决策树在这种表格数据上天然占优,它可以轻松捕捉类别特征之间的非线性交互,classification_report 的宏观表现通常比逻辑回归好一些,尤其是 good 和 vgood 的召回率会明显提高。

但这不能简单地说“逻辑回归不行”。逻辑回归的价值在解释性和概率输出上。比如训练完成后,可以取出每个特征的权重,发现 safety=high 对 vgood 预测有很强的正向贡献,buying=vhigh 对 unacc 预测有很强的正向贡献,这些结论完全符合业务直觉,也让模型结果能向非技术同事交代。

换句话说,如果目标是拿指标去打榜,树模型甚至集成模型当然是更好的选择;如果目标是理解“什么因素在驱动判断”,逻辑回归的输出更直观。做技术选型时,要先明确自己的核心诉求,再决定用哪个模型,而不是上来就默认树模型更好。

5. 踩过的坑和最后的经验

每次跑完一个项目,最有价值的东西都是那些翻车记录。这个任务看似简单,坑却不少,我挑几个印象最深的详细说说。

5.1 六个让我回头翻文档的坑

第一个坑是 max_iter 默认值不够。独热编码之后特征变多,逻辑回归在高维空间收敛变慢,默认 100 次迭代经常触发收敛警告。解决方案是显式调大 max_iter,同时关注警告信息,别眼一闭就当没看见。

第二个坑是滥用有序编码。把 low、med、high 直接映射成 0、1、2 看起来顺理成章,但对逻辑回归来说,这是在强加一个等距线性假设。我做过对比实验,有序编码在验证集上的表现明显弱于独热编码,而且稳定性差。如果你真想用有序编码,需要先验证这个假设在你的业务场景里是否成立。

第三个坑是切分数据没有分层。初始实验里我图省事,直接 stratify=None,结果换了几次随机种子,验证集里的 vgood 时多时少,指标波动大到根本没法判断模型改进是否有效。后来显式指定 stratify=y,实验方差立刻降下来。

第四个坑是只看准确率。baseline 跑完 88% 的准确率让我一度觉得任务完成了,直到打印混淆矩阵才意识到少数类的召回率低得可怜。准确率只是一个加权平均指标,它掩盖了类别内部的巨大差异。

第五个坑是 class_weight 和 C 同时调整时没有控制变量。我一开始同时改了 class_weight="balanced" 和 C=0.1,结果少数类指标确实涨了,但说不清楚到底是哪个操作起的作用。后来固定 C=1.0,只切换 class_weight,用 control variable 的方式做实验,结论才清晰可靠。

第六个坑是处理类别不平衡时用 SMOTE 这类合成采样方法。在这个数据集上,特征都是有序分类变量,SMOTE 会在样本之间做线性插值,很容易生成“车门数=3.5”这种不存在的取值,反而引入噪声。有朋友在这个数据上试 SMOTE 后跟我说效果变差了,我一点都不意外。对于小样本、全分类特征的数据,class_weight 是更稳妥的第一步。

5.2 给后来做同类任务的人的建议

经验归纳成几条,方便直接抄作业。

先跑一个最朴素的 baseline,包括默认参数和准确率指标,记录下这个数字,所有后续实验都要和它对比才有意义。然后用分类报告和混淆矩阵建立“逐类表现”的 baseline,不要只留一个准确率。

编码方式优先考虑 One-Hot,这对逻辑回归更友好。如果你的业务数据里确实有强顺序特征,并且你有足够的领域知识判断它近似等距,可以试有序编码做对比,但要用实验结果说话。

类别不平衡在这个数据上最重要的调整是 class_weight="balanced",先加它,再看指标变化,再考虑阈值调整。把这三个步骤拆开做,每一项改进的效果才能看得清楚。

最后一点,也是最想强调的:不要因为树模型在这个数据集上指标更好,就认定逻辑回归没有价值。模型是工具,不是信仰。逻辑回归的可解释性和概率输出,在很多需要向业务方解释结果、需要对人负责的场景里,是树模型很难替代的。先想清楚你要解决什么问题,再选工具,永远比先选工具再找问题要靠谱。

我自己跑完这个项目之后,最大的感受是:一个只有 1728 条样本的数据集,足够让人重新理解分类任务的复杂性。它不会教你调参技巧,但会逼你想清楚很多基础概念,而这恰恰是涨功夫最快的方式。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进