零样本学习(Zero-Shot Learning,简称ZSL)是机器学习中一个非常有意思的方向:模型可以识别训练阶段从未见过的类别。传统监督学习需要为每个类别准备大量标注样本,而零样本学习只需要类别的语义描述,就能让模型完成分类判断。很多人以为这类技术只能用Python实现,其实Node.js生态现在已经有了可用的方案,本文就来详细讲讲如何在Node.js中实现零样本学习。

一、零样本学习的核心原理
要理解零样本学习,关键是搞清楚它与传统监督学习的区别。传统分类模型训练完成后,输出层是固定的,比如训练时只有猫和狗两类,那模型永远只能输出这两个结果。而零样本学习的思路是:把类别本身也变成一种可以被模型理解的输入,而不是固定的输出节点。
具体做法通常分三步。第一步,用一个通用的语义编码模型把类别名称或类别描述转换成语义向量,比如把"一张开心的照片"、"一张悲伤的照片"这样的标签文本编码成高维向量。第二步,把待分类的输入(文本、图像等)也用相应模型编码到同一个语义空间中。第三步,计算输入向量与各个类别向量的相似度,通常用余弦相似度,相似度最高的类别就是预测结果。
这种架构的妙处在于,类别列表可以随时动态增删,完全不需要重新训练模型。你上午告诉模型今天要区分"正面情绪"和"负面情绪",下午改成区分"科技新闻"和"娱乐新闻",模型照样能工作,这就是零样本的能力来源。
二、Node.js环境下的技术选型
在Node.js中做零样本学习,目前最主流的路线是使用TRANSFORMERS.JS这个库,它是HUGGING FACE TRANSFORMERS的JavaScript版本,底层基于ONNX RUNTIME运行时,可以直接在NODE环境和浏览器中加载预训练模型。它支持文本嵌入、图像嵌入以及零样本分类的Pipeline,正好覆盖了ZSL的核心需求。
先做环境准备。确保NODE版本在18以上,然后初始化项目并安装依赖:
mkdir zsl-demo && cd zsl-demo npm init -y npm install @xenova/transformers
安装完成后,还需要注意一个问题:模型文件默认会从远程仓库下载并缓存到本地,第一次运行时会有较长的等待时间。如果在国内网络环境下载缓慢,可以配置镜像地址或者提前手动下载模型放到缓存目录中。常用的文本零样本分类模型是XENOVA/BART-LARGE-MNLI,中文场景可以换成支持多语言的模型,比如PARAPHRASE-MULTILINGUAL模型来做语义嵌入。
三、实现文本零样本分类
下面直接上手写第一个例子:文本零样本分类。我们使用PIPELINE API,它会自动帮我们完成模型加载、分词、推理、后处理的全流程。
const { pipeline } = require('@xenova/transformers');
async function main() {
// 创建零样本分类器,指定模型
const classifier = await pipeline(
'zero-shot-classification',
'Xenova/bart-large-mnli'
);
// 待分类的文本和候选标签
const text = 'The new smartphone has a great camera and excellent battery life.';
const labels = ['technology', 'sports', 'politics', 'entertainment'];
const result = await classifier(text, labels);
console.log(result);
// 输出每个标签的得分,得分最高的即为预测类别
}
main();
运行后控制台会输出每个标签对应的概率分数,比如TECHNOLOGY可能得到0.87,其余标签分数很低。这个分数来自模型在自然语言推理任务上的判断,本质上是判断"这段文本与这个标签描述是否相容"。
如果是中文文本,建议的做法是换用多语言嵌入模型,自己计算余弦相似度。思路是:分别对文本和标签做嵌入,然后算相似度。下面给出一个手动实现的版本,让你看清零样本分类的内部逻辑:
const { pipeline } = require('@xenova/transformers');
// 计算两个向量的余弦相似度
function cosineSimilarity(a, b) {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
async function main() {
const extractor = await pipeline(
'feature-extraction',
'Xenova/multilingual-e5-small'
);
const text = '这款手机的拍照效果非常出色,续航也很给力。';
const labels = ['科技数码', '体育竞技', '美食烹饪', '旅游出行'];
// 对文本做嵌入,并做均值池化得到句向量
const textEmbedding = await extractor(text, { pooling: 'mean', normalize: true });
const scores = [];
for (const label of labels) {
const labelEmbedding = await extractor(label, { pooling: 'mean', normalize: true });
scores.push({
label,
score: cosineSimilarity(textEmbedding.data, labelEmbedding.data)
});
}
scores.sort((x, y) => y.score - x.score);
console.log(scores);
}
main();
这段代码就是零样本学习最本质的形态:把文本和标签都映射到同一个语义空间,然后比距离。理解了这一点,你完全可以脱离PIPELINE,根据自己的业务需求定制整个流程,比如给标签加上更详细的描述文本,通常能明显提升分类效果。
四、图像零样本识别与工程化建议
图像场景的零样本学习同样可以落地,经典方案是CLIP模型,它把图像和文本编码到同一个语义空间。TRANSFORMERS.JS提供了对应的PIPELINE:
const { pipeline } = require('@xenova/transformers');
async function main() {
const classifier = await pipeline(
'zero-shot-image-classification',
'Xenova/clip-vit-base-patch16'
);
const result = await classifier('./photo.jpg', [
'a photo of a cat',
'a photo of a dog',
'a photo of a car'
]);
console.log(result);
}
main();
模型会输出图像与每条文本描述的匹配分数。值得注意的是,标签描述的写法会影响结果,"A PHOTO OF A CAT"这种格式与CLIP训练时的数据格式一致,效果通常比只写"CAT"更好,这也是使用预训练模型时的一个小技巧。
最后谈谈工程化方面的注意事项。第一,模型加载非常耗时且占内存,BART-LARGE级别的模型推理在纯CPU上单次可能需要几百毫秒,务必在服务启动时加载一次并全局复用,千万不要每次请求都加载。第二,NODE是单线程事件循环,模型推理是同步阻塞的,建议通过WORKER THREADS把推理放到独立线程,避免阻塞HTTP请求处理。第三,如果并发量上来,可以考虑用消息队列做异步推理,或者直接把推理服务独立部署。第四,零样本分类的准确率天然不如专门训练的模型,它更适合冷启动阶段、类别频繁变化的业务场景,等积累了一定标注数据后,再切换到微调过的专用模型才是更合理的演进路径。