ML-For-Beginners 实战:SVC 菜系分类模型转 ONNX,构建浏览器端食谱推荐 Web 应用
本篇指南基于 ML-For-Beginners 课程第 4 章(Classification)的最后一课 “Applied”:使用课程中的清洗后菜系数据集训练一个 Scikit-learn SVC 分类模型,将其转换为 ONNX 格式,并借助 ONNX Runtime Web 在纯浏览器 JavaScript 环境中完成推理,最终交付一个可本地甚至离线运行的“食材 → 菜系”推荐小应用。读完本文,你将掌握 skl2onnx 模型转换的关键参数(FloatTensorType、nocl、zipmap)、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 的推理系统。整体链路分三步:
- 训练:用
cleaned_cuisines.csv训练 SVC 分类模型; - 转换:用
skl2onnx把 Scikit-learn 模型导出为model.onnx,并用 Netron 目检模型图; - 部署:写一个
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 列(almond、angelica、anise、apple……直到zucchini); - 共 3994 行样本,5 种菜系(
chinese、indian、japanese、korean、thai)各 799 条,类别完全均衡; - 每个样本是一行 0/1 向量,表示这道菜是否使用了每种食材。
配套的 ingredient_indexes.csv 是一张“食材 → 列索引”查找表:表头为 380 个食材名(按字母序),第二行是对应的 0~379 索引。这张表对后文构建 Web 界面至关重要——每个复选框的 value 属性就是食材在这张表中的索引。可以从表头直接验证文中示例:apple=4、pear=247、cherry=77、fenugreek=126、sake=302、soy_sauce=327、cumin=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())
三个关键细节:
FloatTensorType([None, 380]):第一个维度None表示批大小动态(任意条样本一次送入),第二个维度 380 必须与特征数严格一致,否则后续推理时会因形状不匹配报错。输入名float_input是自定义的,后面 Web 端的feeds字典必须使用同一个名字(可以用 Netron 核对,见第 5 节)。nocl: True:nocl控制是否把类别标签信息写进 ONNX 图内部。对分类模型关闭它可以减小模型体积。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 的模型视图截图:
这一步的价值在于:它建立了“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>
这段代码中发生了五件事,逐条对应:
- 构造输入向量:
ingredients是一个长度为 380、初值全 0 的数组,与训练时的 380 维 one-hot 特征一一对应; - 监听复选框:页面加载时对每个
.checkbox注册change事件,勾选与否把ingredients[value]置 1 或 0,即动态拼出模型的输入行; - 合法性校验:
testCheckboxes()检查至少有一个复选框被选中,否则弹框提示并中止; - 触发推理:点击按钮调用
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)把输入送入模型并等待响应;
- 读取结果:由于转换时设置了
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!” 的推荐结果。仓库文档附带的运行效果截图如下:
至此,一个最小可用的“推荐”系统就完成了一个闭环:数据 → 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 支持,建议在现代浏览器中测试。
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 StartedRust0624
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

