首页
/ ML-For-Beginners 分类实战:用 Scikit-learn 逻辑回归构建多类别菜系识别分类器

ML-For-Beginners 分类实战:用 Scikit-learn 逻辑回归构建多类别菜系识别分类器

2026-09-06 15:45:27作者:彭桢灵Jeremy

本文基于 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 个食材特征列(如 almondanisetomatoyogurtwhite_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 官方分类器对比图:KNeighbors、SVC、GaussianProcess、DecisionTree、RandomForest、MLP、AdaBoost、GaussianNB 与 QDA 的决策边界可视化

图源为 Scikit-learn 官方文档的分类器并排对比示例(plot_classifier_comparison)。

哪种分类器更合适? 一种朴素做法是"多试几个,挑结果好的"。Scikit-learn 官方文档中就提供了这种并排对比:在一份人造数据集上比较 KNeighbors、两种配置的 SVC、GaussianProcessClassifier、DecisionTreeClassifier、RandomForestClassifier、MLPClassifier、AdaBoostClassifier、GaussianNB 与 QuadraticDiscriminantAnalysis,并可视化结果。业界还有 AutoML 类产品直接在云端替你跑完这轮对比。

Microsoft 算法 Cheat Sheet 中多类别分类部分

图源为 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 选项仅由 lbfgssagsaganewton-cg 四个 solver 支持。

两个概念值得展开:

  • scheme(方案):可以是 ovr(one-vs-rest)或 multinomial。逻辑回归本质上是为二分类设计的,这些方案正是让它能够扩展到多类别任务的机制。
  • solver(求解器/优化算法):官方定义为"在优化问题中使用的算法",即求解模型参数的数值优化方法。

Scikit-learn 文档中各 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 相互印证。

练习二:训练多类别逻辑回归模型

既然是多类别情形,需要同时选定 schemesolver。本课使用 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)供不同技术栈的读者参考;该讲义亦提供了多语言翻译版本(如 阿拉伯语版)。

小结

本课完成了一条完整的多类别分类流水线:

  1. 数据:均衡、干净的 5 类 × 799 样本 × 380 维 0/1 食材特征(cleaned_cuisines.csv);
  2. 选型:基于数据规模与本地训练约束,排除神经网络与不匹配的方案,锁定多类别逻辑回归;
  3. 配置multi_class='ovr' + solver='liblinear',并理解二者各自的语义(分类方案 vs. 优化算法)及兼容矩阵;
  4. 评估:80% 以上整体精度、predict_proba 的单样本概率解释、逐类 classification_report 诊断;
  5. 深化:通过 solver 对比作业把"调库"提升到"懂原理"。

这是第 4 章四节分类课中的第二节;下一课(3-Classifiers-2)将引入 SVC 等更强的分类器,与本课基线形成对照。

登录后查看全文
热门项目推荐
相关项目推荐