ML-For-Beginners 实战课:用 ONNX 把菜品分类模型做成「冰箱食材推荐」Web 应用
2026/9/8 23:04:28 网站建设 项目流程

ML-For-Beginners 实战课:用 ONNX 把菜品分类模型做成「冰箱食材推荐」Web 应用

【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners

本教程对应开源课程 ML-For-Beginners「4-Classification」模块的第 4 课(4-Applied),核心目标是用此前分类课程中学到的技术训练一个菜系分类模型,将其导出为标准 ONNX 模型,再用纯 JavaScript 构建一个「查看冰箱剩余食材、推荐可做菜系」的轻量 Web 应用。读完本文你将掌握:如何用skl2onnx把 Scikit-learn 模型转换为 ONNX、如何用 Netron 可视化校验模型结构、以及如何用onnxruntime-web在浏览器中完成端到端推理。

为什么要构建「应用型」机器学习系统

将模型投入真实业务场景是机器学习落地中不可或缺的一环。ONNX(Open Neural Network Exchange)提供了一种开放、跨框架的模型格式,其 Web 运行时使得模型可以被直接放进浏览器执行——这也意味着应用可以在本地甚至离线环境中运行。

课程在此前 3-Web-App/1-Web-App/README.md 一课中曾训练过一个关于 UFO 目击的回归模型,将其用pickle序列化后在 Flask 应用中使用。但那套方案本质是一个全栈 Python 服务;如果业务要求是纯 JavaScript 前端(例如静态站点或移动端 WebView),就需要一条"模型训练一次、到处推理"的新路径。本课正是这条路径的最小可行实现:

  1. 在 Python 中用清洗后的菜系数据训练一个分类模型;
  2. skl2onnx把它转换为model.onnx
  3. 在浏览器中用onnxruntime-web加载模型做推理。

预备知识:训练数据的形态

本课沿用整个分类系列使用的"菜系"数据集。核心数据文件位于 4-Classification/data/cleaned_cuisines.csv,其结构为:

  • 第 1 列:行索引(无实际意义);
  • 第 2 列:cuisine标签列,取值为chinese/indian/japanese/korean/thai
  • 其余共380 列:每一列是一种食材(almondapplesoy_sauce……),单元格值为 0/1,表示该菜谱是否用到该食材。

也就是说,每个样本是一个 380 维的 0/1 向量,任务是根据"使用了哪些食材"预测所属菜系。这里"380"是后续 ONNX 转换与前端代码中反复出现的关键常数,务必记住它的由来。

训练一个菜系分类模型

第 1 步:读取数据并拆分特征/标签

在 Jupyter 笔记本中逐步执行(完整代码见 4-Classification/4-Applied/solution/notebook.ipynb)。首先安装并导入依赖:

!pip install skl2onnx import pandas as pd

skl2onnx用于把 Scikit-learn 模型转换为 ONNX 格式。随后读取 CSV,并剔除前两列(索引列与标签列)得到特征矩阵X

data = pd.read_csv('../data/cleaned_cuisines.csv') data.head() X = data.iloc[:,2:] X.head()

cuisine列单独取出作为标签y

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

第 2 步:进入训练流程

选用此前课程验证过、精度良好的支持向量机分类器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

按 7:3 划分训练集与测试集:

X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.3)

创建并拟合模型(注意三个关键超参数:kernel='linear'线性核、正则化强度C=10probability=True使模型可输出概率,这为后续保留概率输出节点做准备;random_state=0保证结果可复现):

model = SVC(kernel='linear', C=10, probability=True,random_state=0) model.fit(X_train,y_train.values.ravel())

其中y_train.values.ravel()把 DataFrame 形状的标签压平为一维数组,以满足fit的输入要求。

调用predict()并输出分类报告评估模型质量:

y_pred = model.predict(X_test) print(classification_report(y_test,y_pred))

参考输出如下,整体准确率约 0.79:

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

将 Scikit-learn 模型转换为 ONNX

转换的关键是声明正确的输入张量形状。数据集中共有 380 种食材,因此特征维度为 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}}

各参数含义:

  • float_input:为模型输入节点起的名字,必须记住——稍后在前端feeds中要以同名键喂入数据;
  • FloatTensorType([None, 380]):形状为[batch, 380]的浮点张量,None表示批大小可变,推理时每批只有一行([1, 380]);
  • options:以id(model)为键的转换选项字典。zipmap=False表示不生成 ZipMap 输出(分类模型默认会附带一个把类别映射到概率的字典列表,浏览器端并不需要,移除可简化输出结构);nocl=True表示不把类别信息内嵌进模型,从而显著缩小模型体积——这正是前端results.label直接以字符串类别出现的替代方案的前提。

执行转换并写入文件model.onnx

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

运行完整个笔记本,当前目录下即生成model.onnx。仓库中的成品示例保存在 4-Classification/4-Applied/solution/model.onnx。

用 Netron 可视化校验模型

ONNX 模型本质是二进制 protobuf,在 VS Code 中无法直接"看到"其结构。行业研究者常用免费工具 Netron 打开model.onnx,直观确认模型是否构建正确。

从上方截图可以清晰看到本模型的完整计算图:

  • 输入节点float_input,维度标注?×380
  • 中间黑色核心节点SVMClassifier
  • 两个输出:label(预测的菜系类别)与probabilities(维度?×5,对应 5 个菜系的概率分布)。

Netron 的价值不止于"看一眼":转换后输入节点名、输出节点名、张量形状都会在此如实呈现,你可以据此核对前端代码中feeds的键名(应为float_input)以及输出字段(labelprobabilities)。

模型就绪后,就可以用它构建一个实用性场景:打开冰箱、勾选剩余食材,让模型告诉你这些食材最可能做出哪个国家的菜。

构建浏览器端的推荐 Web 应用

在保存model.onnx的同一目录下新建index.html(仓库内成品见 4-Classification/4-Applied/solution/index.html)。整套页面只有一个文件,不依赖任何构建工具。

页面骨架与食材复选框

先搭建 HTML 骨架:

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

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 的含义:每个value是食材在数据集中对应列索引(从 0 开始计数)。以apple为例,它在按字母排序的食材列表中位于第 5 列,因此值是4;对照 ingredient_indexes.csv 可以查到:文件首行为按字母序排列的 380 种食材,第二行即依次为0,1,2,…,379的索引。例如从上面输出可确认apple索引为 4、cherry为 77、cumin为 112、fenugreek为 126、pear为 247、sake为 302、soy_sauce为 327——与 HTML 中写死的 value 完全一致。想扩充更多食材时,只需从该 CSV 查索引并复制一组 checkbox 即可。

引入 onnxruntime-web

在最后一个闭合</div>之后加入脚本块。首先从 CDN 引入 Onnx Runtime:

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

Onnx Runtime 用于跨各类硬件平台运行 ONNX 模型,内置推理优化,并提供统一的调用 API。浏览器端加载该脚本后,全局对象ort即可用。

编写推理逻辑

<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. 构造 380 维输入向量const ingredients = Array(380).fill(0);先建立长度为 380 的全 0 数组;每个复选框监听change事件,勾选时把ingredients[check.value]置 1,取消勾选时置回 0,即把"我有哪些食材"编码成一个 0/1 稀疏向量,语义上与训练数据中每行 380 个 0/1 特征完全对齐。
  2. 收集复选框并判断勾选状态[...document.querySelectorAll('.checkbox')]用展开运算符把 NodeList 转为数组;testCheckboxes()通过checks.some(check => check.checked)校验是否至少勾选了一项,未勾选时弹出提示并提前返回。
  3. 触发推理:点击按钮调用async function startInference()。推理例程分为四步:
    • await ort.InferenceSession.create('./model.onnx'):异步创建推理会话并加载模型;
    • new ort.Tensor(new Float32Array(ingredients), [1, 380]):把普通数组包装成形状[1, 380]Float32Array张量——1 表示批大小为 1;
    • const feeds = { float_input: input };:构造 feeds,键名float_input必须与训练阶段initial_type中声明的输入名完全一致(可用 Netron 复核此名称);
    • const results = await session.run(feeds);:把 feeds 送入模型并等待输出。
  4. 读取结果results.label.data[0]取出预测类别并弹窗展示;results.probabilities则为各菜系概率(5 维),可供需要置信度展示的进阶版本使用。

提示:由于推理时需要从同目录加载model.onnx(涉及异步 fetch 与 WebAssembly 资源),请勿直接双击file://打开页面,务必通过本地 HTTP 服务访问,以避免跨域限制导致加载失败。

本地运行与效果验证

在存放index.html的目录打开终端,确保全局安装过 http-server 后执行:

http-server

终端会输出本地地址(形如http://127.0.0.1:8080),浏览器打开即可看到应用。勾选若干食材(例如fenugreekcumin)并点击 "What kind of cuisine can you make?",模型会返回最匹配的菜系:

至此,你已经完成了一个"真实可用"的推荐 Web 应用:模型训练、格式转换、结构校验、浏览器端推理全链路打通。

挑战:把系统扩展成真正的推荐器

当前应用的食材复选框仍很有限。可继续基于 ingredient_indexes.csv 扩充更多食材及其索引,探索哪些食材组合会触发特定国家菜系的推荐。更进一步的扩展方向包括:

  • 展示probabilities输出,给出 Top-3 候选菜系及置信度;
  • 按"国家/地区食材特征"分组建复选框,做成筛选器;
  • 参考同一课程的 assignment.md,用本课掌握的 "训练 → 转 ONNX → onnxruntime-web 推理" 方法论,改造出宠物推荐器、按心情推荐音乐流派等全新推荐器(注意评价标准要求同时交付带注释的 Web 应用与 notebook,二者缺一不可)。

延伸阅读

本课只是触及了食材推荐系统的皮毛,推荐系统是 ML 应用中范例极其丰富的领域。可进一步阅读推荐引擎的构建方法(如协同过滤、基于内容的推荐、混合推荐等经典主题),深入理解其数据形态、相似度度量与冷启动等核心问题。

关联资源一览(均位于当前仓库内,可直接打开对照):

  • 本课英文原始文档:4-Classification/4-Applied/README.md
  • 完整可运行笔记本:4-Classification/4-Applied/solution/notebook.ipynb
  • 前端成品页面:4-Classification/4-Applied/solution/index.html
  • 已导出的 ONNX 模型:4-Classification/4-Applied/solution/model.onnx
  • 训练数据:4-Classification/data/cleaned_cuisines.csv
  • 食材-索引对照表:4-Classification/data/ingredient_indexes.csv
  • 前置课(Flask + pickle 回归应用):3-Web-App/1-Web-App/README.md

【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询