ML-For-Beginners 分类实战作业:用 ONNX + 原生 JavaScript 构建可部署的推荐系统
本文围绕 ML-For-Beginners 分类模块第 4 课(4-Classification/4-Applied)的结课作业展开:在完成"训练 SVC 分类器 → 转换为 ONNX → 用 Onnx Runtime Web 在浏览器中做推理"的完整链路之后,作业要求你独立构建一个新的推荐系统(宠物推荐、音乐流派推荐等均可)。本文将把该作业所依赖的完整技术栈从数据到前端逐层拆解,并给出作业的评分标准与扩展方向,读完后你将能够:用 Scikit-learn 训练多分类模型并用 skl2onnx 正确配置张量规格完成 ONNX 转换,使用 Netron 校验模型结构,最后用一个纯 HTML + JavaScript 页面加载 model.onnx 完成离线推理,产出一个可直接交付、可自评的推荐系统。
作业背景:从"做菜推荐"到自建推荐系统
该作业的上下文是分类模块的实战课。课程以仓库中的美食数据集(cuisine dataset)为基础,先用 Scikit-learn 的 SVC 模型对"根据 380 种食材判断菜系(中式/印度/日式/韩式/泰式)"这一问题做多分类训练,再把模型转成 ONNX 格式,最后用一个基于 JavaScript 的 Web 应用加载该模型做推理。课程原文见 4-Applied 课程文档,配套空模板笔记本在 4-Applied/notebook.ipynb,完整可运行答案在 solution/notebook.ipynb 与 solution/index.html。
在此基础之上,结课作业(英文版 assignment.md)的要求是:
根据本课程的练习,你已经知道如何使用 Onnx Runtime 和一个转换好的 Onnx 模型构建基于 JavaScript 的 Web 应用。请使用本课程中的数据,或从其他来源获取的数据(请务必注明出处)尝试构建一个新的推荐系统。你可以做一个基于各种性格特征的宠物推荐系统,或者基于个人心情推荐音乐流派的系统。请发挥创意!
这意味着作业交付的不是单页脚本,而是一个端到端可运行的系统:数据准备、模型训练与评估、ONNX 转换、前端推理界面、以及配套文档。下文先把支撑这套作业的技术链路完整过一遍——这是你自己选题时可以直接复用的"骨架"。
第一步:用 SVC 训练菜系分类模型
作业要求的新推荐系统虽然主题可以自选,但技术骨架与课程中的菜系分类器完全一致。数据集 cleaned_cuisines.csv 共 3995 行数据(含表头 3996 行),382 列:第一列为空索引,第二列 cuisine 是标签(chinese/indian/japanese/korean/thai,每类各 799 条),其余 380 列是食材特征,取值 0/1,表示该菜谱是否使用该食材。
训练流程(与 solution/notebook.ipynb 中的代码一致):
- 安装转换工具并读取数据:
!pip install skl2onnx
import pandas as pd
data = pd.read_csv('../data/cleaned_cuisines.csv')
data.head()
- 去掉前两列无用的列,特征存为
X,标签存为y:
X = data.iloc[:,2:]
X.head()
y = data[['cuisine']]
y.head()
- 训练 SVC 分类器(线性核,
C=10,开启probability以便概率输出):
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
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.3)
model = SVC(kernel='linear', C=10, probability=True,random_state=0)
model.fit(X_train,y_train.values.ravel())
- 预测并输出分类报告:
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
这套"读 CSV → 切片取特征/标签 → 划分训练/测试集 → 训练 → 分类报告"的流程,在作业中换一套数据(宠物性格、音乐心情等)同样适用:只需把特征列换成你的属性向量,把标签列换成你的推荐类别。
第二步:用 skl2onnx 转换模型,注意张量规格与两个关键选项
转换 ONNX 时最容易踩的坑是输入张量维度声明错误。本数据集有 380 种食材,所以必须在 FloatTensorType 中写明 380,第一个维度 None 表示批大小可变:
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}}
随后生成 ONNX 对象并写盘为 model.onnx:
onx = convert_sklearn(model, initial_types=initial_type, options=options)
with open("./model.onnx", "wb") as f:
f.write(onx.SerializeToString())
这里有两个必须理解的转换选项(options 以 id(model) 为键,作用于整个转换器):
nocl: True:不在模型内部嵌入类别(class label)信息,从而减小模型体积。对分类模型来说,这通常正是你想要的——类别表可以由前端或数据侧自己维护(例如菜系名称的固定顺序),不必打包进模型文件。zipmap: False:禁用 ZipMap 算子。默认情况下 skl2onnx 会在输出端插入 ZipMap,把"分数数组"压缩成"字典列表";关闭后模型输出退化为纯张量,前端拿到的是label、scores这样的张量名,取值更直接。
注意 ('float_input', ...) 中自定义的输入名 float_input——它会在前端推理时作为 feed 的键出现,两边必须严格一致。仓库中预置的转换产物 solution/model.onnx 就是按上述规格生成的,可以直接用于前端联调。
第三步:用 Netron 校验模型结构
ONNX 模型在编辑器里只是二进制文件,课程建议使用免费可视化工具 Netron 打开 model.onnx 检查结构:应能看到 380 维输入 float_input 与线性分类器节点。Netron 在这里的实际作用有三点:确认输入节点名称(前端 feeds 的键必须与之一致)、确认输入维度声明为 380、确认输出张量名(本例前端读取的是 label 输出)。
第四步:构建纯 JavaScript 推理页面
接下来是作业评分标准里"Web 应用"这一半的实体部分。思路是:在存放 model.onnx 的同一目录创建 index.html,用 Onnx Runtime Web 在浏览器中加载模型,实现"打开冰箱、勾选现有食材、推荐菜系"的交互。完整参考实现见 solution/index.html,关键片段如下。
页面主体是一组复选框,每个复选框的 value 是食材在特征矩阵中的列索引(从 0 开始计)。例如 apple 是字母序中第 5 个特征,故 value="4";soy sauce 对应 327。任意食材的索引可查 ingredient_indexes.csv——该文件第一行按字母序列出 380 种食材,第二行是 0–379 的对应索引:
<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>
通过 CDN 引入 Onnx Runtime Web(用于在浏览器中运行 ONNX 模型,支持多种硬件平台与优化):
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.9.0/dist/ort.min.js"></script>
然后是核心推理脚本,可拆成四个职责清晰的环节:
<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 的数组,代表"该食材不存在";勾选某食材后按复选框value将对应位置置 1,取消勾选再置回 0。这与训练时X的 0/1 编码严格对齐。- 通过
querySelectorAll('.checkbox')收集所有复选框并绑定change事件,实现状态同步。 testCheckboxes()校验至少勾选了一项,避免空输入触发推理。startInference()是按钮回调,内部完成:异步创建推理会话并加载./model.onnx(ort.InferenceSession.create);把ingredients包装成Float32Array并声明形状[1, 380](1 个样本、380 个特征,与转换时的[None, 380]呼应);以{ float_input: input }作为 feeds——键名必须与训练转换阶段的输入名一致,可用 Netron 核对;session.run(feeds)执行推理后,从输出张量results.label中读取预测类别。因为转换时设置了zipmap: False,输出就是裸张量而非字典,直接取results.label.data[0]即可。
第五步:本地运行并测试应用
在 index.html 所在目录打开终端,确认已全局安装 http-server,然后执行:
http-server
浏览器访问打开的 localhost 地址即可使用应用:勾选不同食材组合,观察模型给出的菜系推荐。
需要说明的适用前提:推理在浏览器本地完成,模型文件与页面同目录,因此该架构可以离线运行;但页面本身仍需一个静态服务器(http-server)来托管,避免 file:// 协议下跨域加载 model.onnx 的问题。
作业要求与评分标准
回到 assignment.md 本体。作业有两层要求:
指令(Instructions):基于本课程练习所学,用本课程的数据或外部数据(外部数据必须注明出处)构建一个新的推荐系统。示例方向:
- 基于性格特征维度的宠物推荐系统;
- 基于个人心情的音乐流派推荐系统;
- 鼓励自行发挥创意。
评分标准(Rubric):
| 标准 | 优秀(Exemplary) | 合格(Adequate) | 需改进(Needs Improvement) |
|---|---|---|---|
| 交付物 | Web 应用与笔记本(notebook)均完整呈现,文档完善且可正常运行 | 两者中有一个缺失或存在缺陷 | 两者均缺失或均存在缺陷 |
对照评分表可以明确交付边界:作业不是只交一个 .onnx 或只交一个 .html,而是**"训练/评估模型 + 前端推理"两部分都要有,且各自文档完善、可运行**。用本课程骨架做自检清单:
- 笔记本侧:数据读取与清洗、特征/标签划分(如
X = data.iloc[:,2:]、y = data[['cuisine']]的写法)、训练/测试集划分、模型训练、classification_report评估、skl2onnx转换(张量维度正确、nocl/zipmap选项有解释); - Web 应用侧:
index.html与model.onnx同目录、特征索引与数据列一一对应、feeds 键名与模型输入名一致、空输入校验、异常捕获(try/catch中打印失败原因); - 创意侧:换一套主题数据后,上述两半仍然成立——这正是"推荐系统"的通用形态:把"输入特征向量 → 输出推荐类别"的模型装进一个零后端依赖的浏览器页面。
课程延伸挑战:从 7 个复选框到完整食材库
课程正文另设有一个挑战(Challenge):当前 Web 应用只放了 7 个复选框,属于极简版本。课程建议利用 ingredient_indexes.csv 中的完整食材列表与索引把应用扩展开——例如动态生成全部 380 个复选框,或按搜索/分组展示,然后探索"哪些食材组合最能锁定某个国家菜系"。做这道挑战时的工程要点:
- 索引表只有两行(食材名 + 索引号),可以直接解析后在前端用
value=索引批量渲染复选框,与现有ingredients[check.value]的写入逻辑天然兼容; - 若把 380 个复选框全部平铺会很难用,建议配合筛选或分类(蔬果/香料/酒类等)分组;
- 模型端无需任何改动——特征向量维度始终是 380,变化的只是用户能触达的勾选范围。
小结
这篇作业的技术主线可以概括为一条链:Pandas 读入 0/1 特征数据 → SVC 训练与 classification_report 评估(约 0.79 准确率)→ skl2onnx 按 [None, 380] 规格转换并用 nocl/zipmap 选项瘦身 → Netron 校验输入名与维度 → Onnx Runtime Web 在纯 HTML 页面中加载 model.onnx 完成本地推理。作业要求你沿这条链换一套主题数据(宠物、音乐等)重走一遍,并交付"文档完善且可运行的 Web 应用 + 笔记本"。掌握这条链之后,任何"特征向量 → 推荐类别"的轻量级推荐系统都可以不依赖后端、离线运行地落到浏览器里。
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 StartedRust0627
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

