ML-For-Beginners 分类实战:用 Scikit-learn 逻辑回归构建多类别菜系识别分类器
本文基于 ML-For-Beginners 课程的「4-Classification / 2-Classifiers-1」讲义,讲解如何使用清洗、均衡后的菜系数据集(cleaned_cuisines.csv),在 Scikit-learn 中完成从数据加载、特征/标签拆分、训练/测试划分,到多类别逻辑回归(multi_class='ovr' + solver='liblinear')建模与评估的完整流程。学完本篇,你将掌握多类别分类任务的分类器选型思路(scheme 与 solver 的搭配规则)、单样本概率预测的可解释性验证方法,以及用 classification_report 输出的 precision / recall / F1 进行逐类诊断的能力。
任务背景与数据准备
本课时属于课程第 4 章「Classification」四节课中的第二节。上一节(Introduction to classification)已经完成了数据清洗与类别均衡,产出了 cleaned_cuisines.csv。本节的目标是:使用多种分类器,根据一组食材预测某道菜品属于哪个国家菜系(chinese / indian / japanese / korean / thai),并在此过程中理解算法在分类任务中的多种用法。
数据文件位于 cleaned_cuisines.csv,按讲义要求放在四节课程共享的 4-Classification/data/ 目录下。从文件实际内容可以确认其结构:
- 共 3995 行样本,每个菜系恰好 799 条,五个类别完全均衡(这正是上一课做 rebalancing 的结果);
- 共 382 列:一个无名列(
Unnamed: 0)、一个标签列(cuisine)、以及 380 个食材特征列(如almond、anise、tomato、yogurt、white_wine……); - 特征均为 0/1 编码,表示该食材是否出现在菜谱中,每条菜谱平均约 6.8 个非零特征。
每个单元格形如:
| Unnamed: 0 | cuisine | almond | angelica | anise | anise_seed | apple | apple_brandy | ... | yogurt | zucchini | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | indian | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 |
| 1 | 1 | indian | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 |
加载数据与构造训练矩阵
所有代码在课时的 notebook.ipynb 中执行,可运行答案见 solution/notebook.ipynb。
第一步,用 Pandas 读入数据:
import pandas as pd
cuisines_df = pd.read_csv("../data/cleaned_cuisines.csv")
cuisines_df.head()
注:该相对路径以课时文件夹为基准;答案版 notebook 位于
solution/子目录,因此内部写的是"../../data/cleaned_cuisines.csv"。
第二步,导入本课要用到的 Scikit-learn 模块:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, precision_score, confusion_matrix, classification_report, precision_recall_curve
from sklearn.svm import SVC
import numpy as np
这里一次性引入了本课及后续课时会用到的工具:LogisticRegression(本次建模主角)、SVC(下一课时将对比的核方法分类器)、cross_val_score(交叉验证)以及一批评估指标函数。
第三步,把 cuisine 列单独取出作为标签(y):
cuisines_label_df = cuisines_df['cuisine']
cuisines_label_df.head()
输出为:
0 indian
1 indian
2 indian
3 indian
4 indian
Name: cuisine, dtype: object
第四步,用 drop() 丢弃 Unnamed: 0(无意义的原始索引列)与 cuisine 标签列,其余全部作为可训练特征(X):
cuisines_feature_df = cuisines_df.drop(['Unnamed: 0', 'cuisine'], axis=1)
cuisines_feature_df.head()
此时特征矩阵是 3995 × 380 的 0/1 稀疏矩阵,可以开始训练模型了。
选择分类器:从"试一遍"到"讲道理"
数据就绪后,需要决定用哪种算法。Scikit-learn 把分类归入 Supervised Learning(监督学习)大类,其中包含分类技术的方法非常多,初见时容易眼花缭乱,主要包括:
- Linear Models(线性模型)
- Support Vector Machines(支持向量机)
- Stochastic Gradient Descent(随机梯度下降)
- Nearest Neighbors(近邻)
- Gaussian Processes(高斯过程)
- Decision Trees(决策树)
- Ensemble methods(集成方法,含投票分类器 Voting Classifier)
- Multiclass and multioutput algorithms(多类别、多输出算法,涵盖 multiclass 与 multilabel 分类)
神经网络(Neural Networks)也可以用于分类,但超出了本课时的范围。
图源为 Scikit-learn 官方文档的分类器并排对比示例(plot_classifier_comparison)。
哪种分类器更合适? 一种朴素做法是"多试几个,挑结果好的"。Scikit-learn 官方文档中就提供了这种并排对比:在一份人造数据集上比较 KNeighbors、两种配置的 SVC、GaussianProcessClassifier、DecisionTreeClassifier、RandomForestClassifier、MLPClassifier、AdaBoostClassifier、GaussianNB 与 QuadraticDiscriminantAnalysis,并可视化结果。业界还有 AutoML 类产品直接在云端替你跑完这轮对比。
图源为 Microsoft ML Algorithm Cheat Sheet 中多类别分类(Multiclass)选项片段。讲义建议:把这张 Cheat Sheet 下载打印出来贴在墙上。
更讲道理的做法是结合本任务的约束条件逐项排除:
- 神经网络太重:我们的数据集虽然干净但规模有限,且训练是在本地 notebook 中完成的,神经网络对这个任务而言过于 heavyweight。
- 不用二分类器,排除 one-vs-all 的某些用法:任务不是二分类,所以纯粹面向二分类的方案被排除。
- 决策树或逻辑回归可行:决策树可能有效,多类别数据也可以用逻辑回归。
- 多类别 Boosted Decision Trees 解决的是另一类问题:多类别增强决策树更适合非参数任务(例如构建排名的任务),对本任务帮助不大。
逻辑回归的两个关键参数:multi_class 与 solver
最终本课选用 Scikit-learn 的 LogisticRegression。但 Scikit-learn 中配置逻辑回归的方式很多,需要重点关注两个参数——multi_class(scheme,方案)与 solver(优化算法),且并非所有 solver 都能搭配所有 multi_class 取值。
按照官方文档的描述,在多类别情形下,训练算法的行为是:
- 当
multi_class设为ovr时,使用 one-vs-rest(OvR,一对余)方案; - 当
multi_class设为multinomial时,使用交叉熵损失(cross-entropy loss)。目前multinomial选项仅由lbfgs、sag、saga、newton-cg四个 solver 支持。
两个概念值得展开:
- scheme(方案):可以是
ovr(one-vs-rest)或multinomial。逻辑回归本质上是为二分类设计的,这些方案正是让它能够扩展到多类别任务的机制。 - solver(求解器/优化算法):官方定义为"在优化问题中使用的算法",即求解模型参数的数值优化方法。
图源为 Scikit-learn 文档中的 solver 对照表。本课数据是 380 维的 0/1 特征矩阵(非负、稀疏),
liblinear这类支持稀疏矩阵且收敛快的线性求解器与之天然匹配。
练习一:划分训练集与测试集
由于上一课时刚学过逻辑回归,本课以它作为第一个训练试验。调用 train_test_split() 把数据切分为训练组与测试组:
X_train, X_test, y_train, y_test = train_test_split(cuisines_feature_df, cuisines_label_df, test_size=0.3)
test_size=0.3 表示 30% 的数据留作测试。对应到本数据集(3995 行),测试集约 1199 行、训练集约 2797 行——这正好与后面分类报告 support 列总计 1199 相互印证。
练习二:训练多类别逻辑回归模型
既然是多类别情形,需要同时选定 scheme 与 solver。本课使用 multi_class='ovr' 搭配 liblinear solver 训练:
lr = LogisticRegression(multi_class='ovr', solver='liblinear')
model = lr.fit(X_train, np.ravel(y_train))
accuracy = model.score(X_test, y_test)
print("Accuracy is {}".format(accuracy))
要点说明:
np.ravel(y_train):标签是 Pandas Series,用ravel()将其展平为一维数组再交给fit(),避免形状警告;- 讲义建议动手实验:换一个 solver(例如默认的
lbfgs)对比精度变化; - 运行结果:测试集精度超过 80%。
单样本验证:让模型"展示工作过程"
随机看一行测试数据(第 50 行),可以直观地看到模型面对的是什么:
print(f'ingredients: {X_test.iloc[50][X_test.iloc[50]!=0].keys()}')
print(f'cuisine: {y_test.iloc[50]}')
输出:
ingredients: Index(['cilantro', 'onion', 'pea', 'potato', 'tomato', 'vegetable_oil'], dtype='object')
cuisine: indian
✅ 换一个行号再试一次,观察结果如何变化。
查看预测概率分布
再深入一步,用 predict_proba 检查这一预测的可信度。注意先把该行的特征向量重塑为二维行向量(分类器要求 (n_samples, n_features) 形状):
test = X_test.iloc[50].values.reshape(-1, 1).T
proba = model.predict_proba(test)
classes = model.classes_
resultdf = pd.DataFrame(data=proba, columns=classes)
topPrediction = resultdf.T.sort_values(by=[0], ascending=[False])
topPrediction.head()
按概率从高到低排序后的结果:
| 0 | |
|---|---|
| indian | 0.715851 |
| chinese | 0.229475 |
| japanese | 0.029763 |
| korean | 0.017277 |
| thai | 0.007634 |
模型的最佳猜测是 indian,且给出了相当高的概率(约 0.72)。✅ 思考题:你能解释为什么模型相当确定这是印度菜吗?(提示:cilantro + onion + tomato + vegetable_oil 等组合在印度菜系样本中的出现模式。)
打印分类报告
与回归课时一样,打印完整的 classification_report 获得逐类细节:
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
| precision | recall | f1-score | support | |
|---|---|---|---|---|
| chinese | 0.73 | 0.71 | 0.72 | 229 |
| indian | 0.91 | 0.93 | 0.92 | 254 |
| japanese | 0.70 | 0.75 | 0.72 | 220 |
| korean | 0.86 | 0.76 | 0.81 | 242 |
| thai | 0.79 | 0.85 | 0.82 | 254 |
| accuracy | 0.80 | 1199 | ||
| macro avg | 0.80 | 0.80 | 0.80 | 1199 |
| weighted avg | 0.80 | 0.80 | 0.80 | 1199 |
几个值得注意的读表点:
- 每个类别的 support 之和为 1199,与
test_size=0.3划分出的测试集规模一致; - indian 类表现最好(precision 0.91 / recall 0.93),与前面单样本的高概率预测相呼应;
- japanese 类最弱(precision 0.70),说明模型容易把日料误判成其他菜系——在下一课时换成 SVM 等非线性方法后,可以观察这一项是否改善;
- 由于数据是均衡的,macro avg 与 weighted avg 相同(均为 0.80)。
挑战与课后任务
🚀 Challenge:本课你已经用清洗后的数据构建了一个"根据食材序列预测国家菜系"的机器学习模型。花些时间通读 Scikit-learn 提供的众多分类选项,并深入理解 solver 的概念——搞清楚它背后到底发生了什么(对应哪类数值优化问题、对稀疏数据的支持等)。
课后作业(Study the solvers):见 assignment.md。要求从本课涉及的 solver 中任选两个,用自己的话撰写对比:它们各自解决什么类型的问题?如何适配不同的数据结构(稠密/稀疏)?为什么会在两者之间做取舍?评分标准(Rubric):
| 标准 | 优秀(Exemplary) | 合格(Adequate) | 待改进(Needs Improvement) |
|---|---|---|---|
| 文档与对比质量 | 提交 .doc 文档,包含两段文字,各对一个 solver 进行深入、有思考的对比 | 文档只写了一段 | 作业未完成 |
此外,讲义提供了课前/课后测验(quiz)链接、逻辑回归数学推导的延伸阅读材料,以及本课的 R 语言版本(solution/R/lesson_11-R.ipynb)与 Julia 版本(solution/Julia/README.md)供不同技术栈的读者参考;该讲义亦提供了多语言翻译版本(如 阿拉伯语版)。
小结
本课完成了一条完整的多类别分类流水线:
- 数据:均衡、干净的 5 类 × 799 样本 × 380 维 0/1 食材特征(cleaned_cuisines.csv);
- 选型:基于数据规模与本地训练约束,排除神经网络与不匹配的方案,锁定多类别逻辑回归;
- 配置:
multi_class='ovr'+solver='liblinear',并理解二者各自的语义(分类方案 vs. 优化算法)及兼容矩阵; - 评估:80% 以上整体精度、
predict_proba的单样本概率解释、逐类classification_report诊断; - 深化:通过 solver 对比作业把"调库"提升到"懂原理"。
这是第 4 章四节分类课中的第二节;下一课(3-Classifiers-2)将引入 SVC 等更强的分类器,与本课基线形成对照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


