发布于 2026-01-06 0 阅读
0

用 JavaScript 构建一个基于内容的推荐引擎

用 JavaScript 构建一个基于内容的推荐引擎

机器学习一直都在我的关注范围内,但我从未真正下定决心认真学习。直到最近,我才开始着手实践。我一直是个学习狂,而且也没什么其他安排,于是决定尝试一下机器学习。我给自己设定了一个任务:创建一个推荐引擎。我们每天都会接触到这类工具,比如社交媒体、网上购物等等。我使用了一个简单的网络数据集,其中包含20张图片,以及通过Google Vision API请求得到的结果。我的目标是:当用户选择一张图片时,从该数据集中推荐其他图片。

我意识到 Python 可能是完成这项任务的更好语言选择,但我非常了解 Javascript,不想给自己增加额外的负担,去用一种我并不完全熟悉的语言来拼凑引擎。

根据维基百科的定义,基于内容的推荐引擎是:

“推荐系统(有时用平台或引擎等同义词代替“系统”)是信息过滤系统的一个子类,旨在预测用户对某个项目的‘评分’或‘偏好’。”

推荐引擎是一种主动过滤系统,它根据已知的用户信息,为用户提供个性化的信息。在我们的案例中,这些信息包括用户最初选择的图像以及从 Google Vision 返回的数据。

最好在本文结尾,我们将能够根据用户最初选择的图片向其推荐更多图片。

利弊分析

在详细介绍具体方法之前,我们先来谈谈原因。这种类型的发动机之所以如此受欢迎是有原因的,但同时也有一些不宜使用它的原因。

优点

  • 与其他方法不同,基于内容的过滤不需要其他用户的数据,因为推荐内容是针对特定用户的。这避免了数据有限时出现的冷启动问题。
  • 该模型能够捕捉用户的特定兴趣,因此可以推荐一些其他用户可能不感兴趣的小众商品。

缺点

  • 该模型只能根据用户已有的兴趣进行推荐。这使得推荐范围局限于已知兴趣,从而阻碍了用户兴趣的拓展。
  • 你必须依赖标签的准确性。
  • 没有考虑到用户的特殊喜好。他们可能喜欢某样东西,但仅限于非常特定的情况下。

基于内容的推荐引擎是如何工作的?

基于内容的推荐引擎利用用户提供的数据(在本例中为用户选择的图片)进行工作。基于这些数据,我们可以向用户提供建议。

在本例中,我们的脚本将按以下步骤进行:

  1. 训练
    • 将数据格式化为可用状态
    • 计算 TF-IDF 并从格式化的文档中创建向量
    • 计算相似文档
  2. 利用训练数据,根据用户选择的图片进行推荐。

在开始编写推荐引擎之前,我们需要讨论几个关键概念。具体来说,我们将如何决定推荐哪些数据?

词频 (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
    },
    [...]
  ]
}


Enter fullscreen mode Exit fullscreen mode

1.a 格式设置

在本练习中,我们只关注对象的顶层键(1.jpg)以及description数组中每个对象的描述。但我们希望将所有描述放在一个字符串中。这样便于后续处理。

我们希望数据以对象数组的形式存储,如下所示:



const formattedData = [
  {
    id: '1.jpg',
    content: 'flower flowering plant plant petal geraniaceae melastome family geranium wildflower geraniales perennial plant' 
  }
]


Enter fullscreen mode Exit fullscreen mode

为了格式化数据,我们将使用以下函数进行处理。这将返回一个包含所有训练引擎所需数据的数组。我们使用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;
};


Enter fullscreen mode Exit fullscreen mode

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' 
}


Enter fullscreen mode Exit fullscreen mode

IDF 的计算稍微复杂一些。公式如下:

替代文字

在 JavaScript 中,这是通过以下方式实现的:



var idf = Math.log((this.documents.length) / docsWithTerm );


Enter fullscreen mode Exit fullscreen mode

我们只需要上述数值(TF 和 IDF)即可计算 TF-IDF。它就是 TF 乘以 IDF。



const tdidf = tf * idf;


Enter fullscreen mode Exit fullscreen mode

下一步是计算文档的 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);
  }


Enter fullscreen mode Exit fullscreen mode

现在我们有了一个对象数组,其中包含图像的 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;


Enter fullscreen mode Exit fullscreen mode

该方法实际上getCosineSimilarity执行了许多操作。

它计算点积,此运算接受两个向量并返回一个标量数。它实际上是将两个向量的每个分量相乘,然后将结果相加。



a = [1.7836, 3]
b = [4, 0.5945]

a.b = 1.7836 * 4 + 3 *0.5945 = 8.9176


Enter fullscreen mode Exit fullscreen mode

计算出点积后,我们只需要将每个文档的向量值转换为标量值。这可以通过将每个值与其自身相乘并求和后开平方根来实现。getLength以下方法执行此计算。



const getLength = () => {
  let l = 0;

  this.getComponents().forEach(k => {
    l += this.vector[k] * this.vector[k];
  });

  return Math.sqrt(l);
}


Enter fullscreen mode Exit fullscreen mode

实际的余弦相似度公式如下所示:

替代文字

在 JavaScript 中,它看起来像这样:



const getCosineSimilarity = (vector) => {
  return this.getDotProduct(vector) / (this.getLength() * vector.getLength());
}


Enter fullscreen mode Exit fullscreen mode

训练结束了!

查看

2. 获取我们的建议

现在训练阶段已经完成,我们可以直接从训练数据中请求推荐的图像。



const getSimilarDocuments = (id, trainedData) => {
  let similarDocuments = trainedData[id];

  if (similarDocuments === undefined) {
    return [];
  }

  return similarDocuments;
};


Enter fullscreen mode Exit fullscreen mode

这将返回一个对象数组,其中包含推荐的图像及其余弦相似度得分。



// e.g
[ { id: '14.jpg', score: 0.341705472305971 },
{ id: '9.jpg', score: 0.3092133517794513 },
{ id: '1.jpg', score: 0.3075994367748345 } ]

Enter fullscreen mode Exit fullscreen mode




包起来

希望你能跟上我的讲解。我从这次练习中学到了很多,它真的激发了我对机器学习的兴趣。

文章来源:https://dev.to/jimatjibba/build-a-content-based-recommendation-engine-in-js-2lpi