用 JavaScript 构建一个基于内容的推荐引擎
机器学习一直都在我的关注范围内,但我从未真正下定决心认真学习。直到最近,我才开始着手实践。我一直是个学习狂,而且也没什么其他安排,于是决定尝试一下机器学习。我给自己设定了一个任务:创建一个推荐引擎。我们每天都会接触到这类工具,比如社交媒体、网上购物等等。我使用了一个简单的网络数据集,其中包含20张图片,以及通过Google Vision API请求得到的结果。我的目标是:当用户选择一张图片时,从该数据集中推荐其他图片。
我意识到 Python 可能是完成这项任务的更好语言选择,但我非常了解 Javascript,不想给自己增加额外的负担,去用一种我并不完全熟悉的语言来拼凑引擎。
根据维基百科的定义,基于内容的推荐引擎是:
“推荐系统(有时用平台或引擎等同义词代替“系统”)是信息过滤系统的一个子类,旨在预测用户对某个项目的‘评分’或‘偏好’。”
推荐引擎是一种主动过滤系统,它根据已知的用户信息,为用户提供个性化的信息。在我们的案例中,这些信息包括用户最初选择的图像以及从 Google Vision 返回的数据。
最好在本文结尾,我们将能够根据用户最初选择的图片向其推荐更多图片。
利弊分析
在详细介绍具体方法之前,我们先来谈谈原因。这种类型的发动机之所以如此受欢迎是有原因的,但同时也有一些不宜使用它的原因。
优点
- 与其他方法不同,基于内容的过滤不需要其他用户的数据,因为推荐内容是针对特定用户的。这避免了数据有限时出现的冷启动问题。
- 该模型能够捕捉用户的特定兴趣,因此可以推荐一些其他用户可能不感兴趣的小众商品。
缺点
- 该模型只能根据用户已有的兴趣进行推荐。这使得推荐范围局限于已知兴趣,从而阻碍了用户兴趣的拓展。
- 你必须依赖标签的准确性。
- 没有考虑到用户的特殊喜好。他们可能喜欢某样东西,但仅限于非常特定的情况下。
基于内容的推荐引擎是如何工作的?
基于内容的推荐引擎利用用户提供的数据(在本例中为用户选择的图片)进行工作。基于这些数据,我们可以向用户提供建议。
在本例中,我们的脚本将按以下步骤进行:
- 训练
- 将数据格式化为可用状态
- 计算 TF-IDF 并从格式化的文档中创建向量
- 计算相似文档
- 利用训练数据,根据用户选择的图片进行推荐。
在开始编写推荐引擎之前,我们需要讨论几个关键概念。具体来说,我们将如何决定推荐哪些数据?
词频 (TF) 和逆文档频率 (IDF) 的概念用于确定词项的相对重要性。由此,我们可以利用余弦相似度来决定推荐方向。本文将对此进行详细讨论。
TF 指的是一个词在文档中出现的频率。IDF 指的是一个词在整个文档语料库中出现的频率。它反映了词的稀有程度,有助于提升稀有词的得分。TD-IDF 之所以被使用,是因为它不仅考虑了单个词,还考虑了词在整个文档语料库中的重要性。该模型结合了词在文档中的重要性(局部重要性)和词在整个语料库中的重要性(全局重要性)。
余弦相似度是一种用于衡量文档相似度的指标,它不受文档大小的限制。从数学角度来说,它是在测量两个向量之间的余弦夹角。在我们的语境中,这两个向量是对象,其中键是词项,值是TF-IDF值。该值也称为向量的模。
1. 培训
“训练”引擎的第一步是将数据格式化为易于使用和管理的结构。从 Google Cloud Vision 返回的标签数据大致如下所示:
{
"1.jpg": [
{
"locations": [],
"properties": [],
"mid": "/m/0c9ph5",
"locale": "",
"description": "Flower",
"score": 0.9955990314483643,
"confidence": 0,
"topicality": 0.9955990314483643,
"boundingPoly": null
},
{
"locations": [],
"properties": [],
"mid": "/m/04sjm",
"locale": "",
"description": "Flowering plant",
"score": 0.9854584336280823,
"confidence": 0,
"topicality": 0.9854584336280823,
"boundingPoly": null
},
[...]
]
}
1.a 格式设置
在本练习中,我们只关注对象的顶层键(1.jpg)以及description数组中每个对象的描述。但我们希望将所有描述放在一个字符串中。这样便于后续处理。
我们希望数据以对象数组的形式存储,如下所示:
const formattedData = [
{
id: '1.jpg',
content: 'flower flowering plant plant petal geraniaceae melastome family geranium wildflower geraniales perennial plant'
}
]
为了格式化数据,我们将使用以下函数进行处理。这将返回一个包含所有训练引擎所需数据的数组。我们使用Object.entries它是为了更方便地进行迭代。MDN 指出:
Object.entries() 方法返回给定对象自身可枚举的字符串键属性 [key, value] 对的数组……
Object.entries然后,我们遍历通过提取必要属性创建的数组,并将它们添加到另一个desc数组中。最后,我们将该数组的内容合并desc,并将其写入content属性。这个formatted数组就是我们的语料库。
const formatData = data => {
let formatted = [];
for (const [key, labels] of Object.entries(data)) {
let tmpObj = {};
const desc = labels.map(l => {
return l.description.toLowerCase();
});
tmpObj = {
id: key,
content: desc.join(" ")
};
formatted.push(tmpObj);
}
return formatted;
};
1.b TF-IDF 和向量
如上所述,TF 只是术语在文档中出现的次数。
例如:
// In the data set below the TF of plant is 3
{
id: '1.jpg',
content: 'flower flowering plant plant petal geraniaceae melastome family geranium wildflower geraniales perennial plant'
}
IDF 的计算稍微复杂一些。公式如下:
在 JavaScript 中,这是通过以下方式实现的:
var idf = Math.log((this.documents.length) / docsWithTerm );
我们只需要上述数值(TF 和 IDF)即可计算 TF-IDF。它就是 TF 乘以 IDF。
const tdidf = tf * idf;
下一步是计算文档的 TF-IDF 值,并创建一个向量,其中词项作为键,TF-IDF 值(向量)作为值。我们借助naturalnpmvector-object包来简化这一过程。该包tfidf.addDocument会对我们的content属性进行标记化处理。该tfidf.listTerms方法会列出处理后的文档,并返回一个包含 TD、IDF 和 TD-IDF 的对象数组。不过,我们目前只关注 TF-IDF 值。
/**
* Generates the TF-IDF of each term in the document
* Create a Vector with the term as the key and the TF-IDF as the value
* @example - example vector
* {
* flowers: 1.2345
* }
*/
const createVectorsFromDocs = processedDocs => {
const tfidf = new TfIdf();
processedDocs.forEach(processedDocument => {
tfidf.addDocument(processedDocument.content);
});
const documentVectors = [];
for (let i = 0; i < processedDocs.length; i += 1) {
const processedDocument = processedDocs[i];
const obj = {};
const items = tfidf.listTerms(i);
for (let j = 0; j < items.length; j += 1) {
const item = items[j];
obj[item.term] = item.tfidf;
}
const documentVector = {
id: processedDocument.id,
vector: new Vector(obj)
};
documentVectors.push(documentVector);
}
现在我们有了一个对象数组,其中包含图像的 ID(1.jpg)作为 id,以及我们的向量。下一步是计算文档之间的相似度。
1.c 用余弦相似度和点积计算相似度
“训练”阶段的最后一步是计算文档之间的相似度。我们vector-object再次使用该软件包来计算余弦相似度。计算完成后,我们将结果放入一个数组中,该数组包含图像 ID 和所有训练中推荐的图像。最后,我们对数组进行排序,使余弦相似度最高的图像排在第一位。
/**
* Calculates the similarities between 2 vectors
* getCosineSimilarity creates the dotproduct and the
* length of the 2 vectors to calculate the cosine
* similarity
*/
const calcSimilarities = docVectors => {
// number of results that you want to return.
const MAX_SIMILAR = 20;
// min cosine similarity score that should be returned.
const MIN_SCORE = 0.2;
const data = {};
for (let i = 0; i < docVectors.length; i += 1) {
const documentVector = docVectors[i];
const { id } = documentVector;
data[id] = [];
}
for (let i = 0; i < docVectors.length; i += 1) {
for (let j = 0; j < i; j += 1) {
const idi = docVectors[i].id;
const vi = docVectors[i].vector;
const idj = docVectors[j].id;
const vj = docVectors[j].vector;
const similarity = vi.getCosineSimilarity(vj);
if (similarity > MIN_SCORE) {
data[idi].push({ id: idj, score: similarity });
data[idj].push({ id: idi, score: similarity });
}
}
}
// finally sort the similar documents by descending order
Object.keys(data).forEach(id => {
data[id].sort((a, b) => b.score - a.score);
if (data[id].length > MAX_SIMILAR) {
data[id] = data[id].slice(0, MAX_SIMILAR);
}
});
return data;
该方法实际上getCosineSimilarity执行了许多操作。
它计算点积,此运算接受两个向量并返回一个标量数。它实际上是将两个向量的每个分量相乘,然后将结果相加。
a = [1.7836, 3]
b = [4, 0.5945]
a.b = 1.7836 * 4 + 3 *0.5945 = 8.9176
计算出点积后,我们只需要将每个文档的向量值转换为标量值。这可以通过将每个值与其自身相乘并求和后开平方根来实现。getLength以下方法执行此计算。
const getLength = () => {
let l = 0;
this.getComponents().forEach(k => {
l += this.vector[k] * this.vector[k];
});
return Math.sqrt(l);
}
实际的余弦相似度公式如下所示:
在 JavaScript 中,它看起来像这样:
const getCosineSimilarity = (vector) => {
return this.getDotProduct(vector) / (this.getLength() * vector.getLength());
}
训练结束了!
2. 获取我们的建议
现在训练阶段已经完成,我们可以直接从训练数据中请求推荐的图像。
const getSimilarDocuments = (id, trainedData) => {
let similarDocuments = trainedData[id];
if (similarDocuments === undefined) {
return [];
}
return similarDocuments;
};
这将返回一个对象数组,其中包含推荐的图像及其余弦相似度得分。
// e.g
[ { id: '14.jpg', score: 0.341705472305971 },
{ id: '9.jpg', score: 0.3092133517794513 },
{ id: '1.jpg', score: 0.3075994367748345 } ]
包起来
希望你能跟上我的讲解。我从这次练习中学到了很多,它真的激发了我对机器学习的兴趣。
文章来源:https://dev.to/jimatjibba/build-a-content-based-recommendation-engine-in-js-2lpi



