首页
/ ML-For-Beginners 实战:SVC 菜系分类模型转 ONNX,构建浏览器端食谱推荐 Web 应用

ML-For-Beginners 实战:SVC 菜系分类模型转 ONNX,构建浏览器端食谱推荐 Web 应用

2026-09-06 16:31:24作者:裘旻烁

本篇指南基于 ML-For-Beginners 课程第 4 章(Classification)的最后一课 “Applied”:使用课程中的清洗后菜系数据集训练一个 Scikit-learn SVC 分类模型,将其转换为 ONNX 格式,并借助 ONNX Runtime Web 在纯浏览器 JavaScript 环境中完成推理,最终交付一个可本地甚至离线运行的“食材 → 菜系”推荐小应用。读完本文,你将掌握 skl2onnx 模型转换的关键参数(FloatTensorTypenoclzipmap)、ONNX 模型检查方法,以及 onnxruntime-web 在浏览器中加载模型、构造 Tensor 并完成推理的完整流程。

1. 整体架构:从 Python 训练到 JavaScript 推理

该课程的定位是:把前面章节学到的分类技术(见 4-Classification/README.md)落地为一个可部署的推荐系统。构建应用级机器学习系统是把这些技术带进业务系统的重要一环——借助 ONNX,模型可以在 Web 应用内部直接运行,必要时还能在离线环境中工作。

课程先给出了一个对比锚点:在之前的 Web 应用课程(见 3-Web-App/1-Web-App/README.md)中,作者训练了一个关于 UFO 目击的回归模型,用 pickle 序列化后放进 Flask 应用里使用。那是一条完整的 Python 技术栈路线;而本课的出发点是:如果你的需求方是 JavaScript 应用呢?于是本课构建一个基于 JavaScript 的推理系统。整体链路分三步:

  1. 训练:用 cleaned_cuisines.csv 训练 SVC 分类模型;
  2. 转换:用 skl2onnx 把 Scikit-learn 模型导出为 model.onnx,并用 Netron 目检模型图;
  3. 部署:写一个 index.html,引入 onnxruntime-web,在浏览器中加载 ONNX 模型做推理。

本仓库在 4-Classification/4-Applied/solution/ 目录下提供了完整参考答案:notebook.ipynb(训练与转换全流程)、index.html(Web 应用成品)以及已转换好的 model.onnx(约 2.2 MB),可对照本文逐步复现。

2. 数据基础:380 维 one-hot 食材向量

整个应用的数据基础是 cleaned_cuisines.csv。该文件在本章前面的课程中已完成清洗(见 4-Classification/1-Introduction/README.md),从仓库中实际查看可以确认它的结构:

  • 382 列:第 1 列是行号(无列名),第 2 列是菜系标签 cuisine,其余 380 列是食材的 one-hot 列(almondangelicaaniseapple……直到 zucchini);
  • 3994 行样本,5 种菜系(chineseindianjapanesekoreanthai)各 799 条,类别完全均衡;
  • 每个样本是一行 0/1 向量,表示这道菜是否使用了每种食材。

配套的 ingredient_indexes.csv 是一张“食材 → 列索引”查找表:表头为 380 个食材名(按字母序),第二行是对应的 0~379 索引。这张表对后文构建 Web 界面至关重要——每个复选框的 value 属性就是食材在这张表中的索引。可以从表头直接验证文中示例:apple=4pear=247cherry=77fenugreek=126sake=302soy_sauce=327cumin=112,与文档给出的数值完全一致(从 0 开始计数,apple 是第 5 个食材,所以索引为 4)。

3. 训练 SVC 分类模型

下面按 4-Classification/4-Applied/README.md 的练习步骤完整走一遍训练流程。

3.1 安装依赖并载入数据

第一步安装 skl2onnx 并导入 pandas:

!pip install skl2onnx
import pandas as pd

skl2onnx 的作用是把 Scikit-learn 模型转换成 ONNX 格式。然后按前几课的方式用 read_csv() 读取数据(注意相对路径:若在 4-Applied 目录下运行,数据文件位于上一级 data 目录,即仓库中的 4-Classification/data/cleaned_cuisines.csv):

data = pd.read_csv('../data/cleaned_cuisines.csv')
data.head()

3.2 划分特征矩阵 X 与标签 y

去掉前两个无用的列(行号列和标签列),把剩余的 380 个 one-hot 食材列保存为 X,把 cuisine 列保存为 y

X = data.iloc[:,2:]
X.head()

y = data[['cuisine']]
y.head()

此时 X 的形状为 (3994, 380),y 为单列 DataFrame。

3.3 训练、预测与评估

课程选用线性核 SVC,因为其在这份数据上精度良好。完整代码如下:

from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score,precision_score,confusion_matrix,classification_report

# 1. 划分训练/测试集,测试集占 30%
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.3)

# 2. 构建 SVC 分类模型
model = SVC(kernel='linear', C=10, probability=True, random_state=0)
model.fit(X_train, y_train.values.ravel())

# 3. 用 predict() 在测试集上测试
y_pred = model.predict(X_test)

# 4. 打印分类报告检查模型质量
print(classification_report(y_test, y_pred))

几个参数值得注意:

  • kernel='linear':在 380 维稀疏 one-hot 特征上,线性核通常表现稳定且可解释;
  • C=10:正则化强度的倒数,控制对误分类的惩罚力度;
  • probability=True:启用概率估计(Platt 缩放),代价是训练略慢;
  • y_train.values.ravel():把单列 DataFrame 压平为一维数组,满足 SVC.fit 对标签维度的要求。

文档记录的分类报告如下(测试集 1199 条 = 3994 × 0.3):

              precision    recall  f1-score   support

     chinese       0.72      0.69      0.70       257
      indian       0.91      0.87      0.89       243
    japanese       0.79      0.77      0.78       239
      korean       0.83      0.79      0.81       236
        thai       0.72      0.84      0.78       224

    accuracy                           0.79      1199
   macro avg       0.79      0.79      0.79      1199
weighted avg       0.79      0.79      0.79      1199

从仓库中保存的参考答案 solution/notebook.ipynb 的运行输出来看,一次完整执行的 accuracy 约为 0.80。由于 train_test_split 未固定 random_state,每次随机划分会使各类 support 和指标在小幅范围内波动,这属于正常现象,不影响后续结论。

4. 用 skl2onnx 把模型转换为 ONNX

转换的核心是用正确的 Tensor 规格声明输入形状:数据集有 380 个 one-hot 食材分量,所以特征维度必须写成 380:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_type = [('float_input', FloatTensorType([None, 380]))]
options = {id(model): {'nocl': True, 'zipmap': False}}

然后执行转换并把序列化结果写入 model.onnx 文件:

onx = convert_sklearn(model, initial_types=initial_type, options=options)
with open("./model.onnx", "wb") as f:
    f.write(onx.SerializeToString())

三个关键细节:

  1. FloatTensorType([None, 380]):第一个维度 None 表示批大小动态(任意条样本一次送入),第二个维度 380 必须与特征数严格一致,否则后续推理时会因形状不匹配报错。输入名 float_input 是自定义的,后面 Web 端的 feeds 字典必须使用同一个名字(可以用 Netron 核对,见第 5 节)。
  2. nocl: Truenocl 控制是否把类别标签信息写进 ONNX 图内部。对分类模型关闭它可以减小模型体积
  3. zipmap: False:ONNX 分类器默认会用 ZipMap 节点把输出包装成“标签→概率”的字典张量。本课不需要这种结构,关掉后输出是普通张量,浏览器端读取更直接(后文的 results.label)。

运行完整 notebook 后,ONNX 模型即保存在工作目录的 model.onnx。仓库参考答案中的成品位于 4-Classification/4-Applied/solution/model.onnx,约 2.2 MB,可直接用于第 6 节的 Web 应用。

5. 用 Netron 检查模型

ONNX 文件是二进制图结构,在编辑器里几乎不可读。课程推荐用开源可视化工具 Netron 打开 model.onnx 做目检,确认模型被正确构建:可以看到 380 维的输入、SVC 分类器节点,以及输入名 float_input 是否与转换脚本中声明的一致。仓库文档中附有一张 Netron 的模型视图截图:

Netron 打开 model.onnx 的模型图可视化,显示输入与分类器节点

这一步的价值在于:它建立了“Python 端声明 → ONNX 图 → JavaScript 端 feeds 键名”三者之间的名字契约。如果 Web 端 feeds 的键名与图中输入名不一致,session.run 会直接失败。

6. 构建推荐 Web 应用

现在把模型直接放进 Web 应用。这种架构允许应用本地运行,甚至按需离线运行。做法:在与 model.onnx 同一个文件夹里创建 index.html。完整成品可对照 4-Classification/4-Applied/solution/index.html

6.1 页面骨架

<!DOCTYPE html>
<html>
    <header>
        <title>Cuisine Matcher</title>
    </header>
    <body>
        ...
    </body>
</html>

6.2 食材复选框:value 就是列索引

body 内加入若干复选框,每个代表一种食材:

<h1>Check your refrigerator. What can you create?</h1>
<div id="wrapper">
    <div class="boxCont">
        <input type="checkbox" value="4" class="checkbox">
        <label>apple</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="247" class="checkbox">
        <label>pear</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="77" class="checkbox">
        <label>cherry</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="126" class="checkbox">
        <label>fenugreek</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="302" class="checkbox">
        <label>sake</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="327" class="checkbox">
        <label>soy sauce</label>
    </div>

    <div class="boxCont">
        <input type="checkbox" value="112" class="checkbox">
        <label>cumin</label>
    </div>
</div>
<div style="padding-top:10px">
    <button onClick="startInference()">What kind of cuisine can you make?</button>
</div>

每个复选框都被赋予了 value,它反映该食材在数据集中的列索引(0 起)。例如 apple 在字母序列表中占第 5 列,所以 value="4"。要查任意食材的索引,直接查阅 ingredient_indexes.csv 即可。

6.3 引入 ONNX Runtime Web

在最终 </div> 之后加入脚本区,首先通过 CDN 引入 ONNX Runtime Web 运行库:

<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.9.0/dist/ort.min.js"></script>

ONNX Runtime 用于在广泛的硬件平台上运行 ONNX 模型,提供优化内核与调用 API;这里固定使用 1.9.0 版本,与仓库方案保持一致。

6.4 推理脚本:从复选框到 Tensor

<script>
    const ingredients = Array(380).fill(0);

    const checks = [...document.querySelectorAll('.checkbox')];

    checks.forEach(check => {
        check.addEventListener('change', function() {
            // toggle the state of the ingredient
            // based on the checkbox's value (1 or 0)
            ingredients[check.value] = check.checked ? 1 : 0;
        });
    });

    function testCheckboxes() {
        // validate if at least one checkbox is checked
        return checks.some(check => check.checked);
    }

    async function startInference() {

        let atLeastOneChecked = testCheckboxes()

        if (!atLeastOneChecked) {
            alert('Please select at least one ingredient.');
            return;
        }
        try {
            // create a new session and load the model.

            const session = await ort.InferenceSession.create('./model.onnx');

            const input = new ort.Tensor(new Float32Array(ingredients), [1, 380]);
            const feeds = { float_input: input };

            // feed inputs and run
            const results = await session.run(feeds);

            // read from results
            alert('You can enjoy ' + results.label.data[0] + ' cuisine today!')

        } catch (e) {
            console.log(`failed to inference ONNX model`);
            console.error(e);
        }
    }

</script>

这段代码中发生了五件事,逐条对应:

  1. 构造输入向量ingredients 是一个长度为 380、初值全 0 的数组,与训练时的 380 维 one-hot 特征一一对应;
  2. 监听复选框:页面加载时对每个 .checkbox 注册 change 事件,勾选与否把 ingredients[value] 置 1 或 0,即动态拼出模型的输入行;
  3. 合法性校验testCheckboxes() 检查至少有一个复选框被选中,否则弹框提示并中止;
  4. 触发推理:点击按钮调用 startInference(),内部按顺序执行——
    • ort.InferenceSession.create('./model.onnx') 异步创建会话并从本地加载模型文件(同目录);
    • new ort.Tensor(new Float32Array(ingredients), [1, 380]) 构造形状为 [1, 380] 的 Float32 Tensor,与 FloatTensorType([None, 380]) 的声明对齐;
    • feeds = { float_input: input }:键名 float_input 必须与转换时声明的输入名一致(可再用 Netron 核实);
    • session.run(feeds) 把输入送入模型并等待响应;
  5. 读取结果:由于转换时设置了 zipmap: False,输出就是普通张量,直接取 results.label.data[0] 得到预测菜系名(如 japanese),拼进提示语展示。

值得强调的设计点:整个推理发生在浏览器进程内,不需要任何后端服务;模型文件与页面同目录,通过 http-server 以静态文件方式加载即可,因此该应用天然支持离线使用。

7. 本地运行与测试

index.html 所在目录(其中已有 model.onnx)打开终端,确认全局安装了 http-server(可用 npm install -g http-server 安装),然后执行:

http-server

浏览器打开提示的 localhost 地址即可看到应用。勾选若干食材后点击按钮,页面会弹出类似 “You can enjoy japanese cuisine today!” 的推荐结果。仓库文档附带的运行效果截图如下:

食材推荐 Web 应用界面:复选框勾选食材后按钮触发推理并弹窗推荐菜系

至此,一个最小可用的“推荐”系统就完成了一个闭环:数据 → SVC 模型 → ONNX → 浏览器推理。

8. 挑战与作业:把它扩展成真正的推荐器

课程给出的挑战是:当前应用只有 7 个字段,过于简陋,请用 ingredient_indexes.csv 中全部 380 个食材及其索引继续扩充界面,探索“什么样的风味组合能做出某种菜系的招牌菜”。

正式作业见 4-Classification/4-Applied/assignment.md:基于本课学到的“JavaScript + ONNX Runtime + 转换后的 ONNX 模型”技术路线,构建一个全新的推荐系统——数据可以来自本课程,也可以来自其他来源(注明来源)。示例方向包括:按性格特征推荐宠物、按心情推荐音乐流派。评分标准要求同时提交 Web 应用和 notebook,且两者都需文档齐全、可以正常运行。

9. 关键文件速查

文件 说明
4-Classification/4-Applied/README.md 本课英文原文(本文的骨架来源)
4-Classification/4-Applied/solution/notebook.ipynb 训练 + ONNX 转换完整参考答案
4-Classification/4-Applied/solution/index.html 浏览器推理 Web 应用成品
4-Classification/4-Applied/solution/model.onnx 已转换的 ONNX 模型(约 2.2 MB)
4-Classification/data/cleaned_cuisines.csv 3994 × 382 清洗后菜系数据集(2 元数据列 + 380 one-hot 食材列)
4-Classification/data/ingredient_indexes.csv 380 个食材的列索引查找表

适用前提与限制:本流程要求输入特征为数值型且维度固定(380),因此天然适配 one-hot 食材向量这类特征;若更换数据集,需同步修改 FloatTensorType 的维度、JS 端 ingredients 数组长度与 Tensor 形状 [1, N] 三处。ONNX Runtime Web 在部分浏览器中首次加载模型时依赖 WebAssembly/WASM 支持,建议在现代浏览器中测试。

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