大型语言模型和向量数据库用于新闻推荐
使用大型语言模型和向量数据库进行新闻推荐的技术应用
使用Sentence Transformers和Qdrant将LLMs用于生产环境
大型语言模型(LLMs)最近的生成式人工智能工具(如Chat-GPT,Bard等)在机器学习社区引起了全球关注。其中这些解决方案的核心思想之一是计算非结构化数据(如文本和图片)的数值表示,并找到这些表示之间的相似性。
然而,将所有这些概念应用于生产环境还面临着自己的机器学习工程挑战:
- 如何快速生成这些表示?
- 如何将它们存储在适当的数据库中?
- 如何在生产环境中快速计算相似性?
在本文中,我将介绍两个开源解决方案,旨在解决这些问题:
- Sentence Transformers [1]:基于文本信息的嵌入生成技术;
- Qdrant:一种能够存储嵌入并提供易于查询的向量数据库。
这些工具被应用于NPR [2]的新闻门户推荐数据集(可在Kaggle上公开获取),该数据集的构建旨在支持学术界发展推荐算法。在文章的最后,您将了解如何:
- 使用Sentence Transformers生成新闻嵌入
- 使用Qdrant存储嵌入
- 查询嵌入以推荐新闻文章
本文所有代码都可以在Github上找到。
1. 使用Sentence Transformers生成嵌入
首先,我们需要找到一种将输入数据转换为向量(我们称之为嵌入)的方法(如果您想深入了解嵌入概念,我推荐阅读 Boykis 的文章《什么是嵌入?》 [3])。
所以让我们看一下我们可以使用NPR数据集处理的数据类型:
import pandas as pddf = pd.read_parquet("articles.parquet")df.tail()
我们可以看到NPR有一些有趣的文本数据,如文章的标题和正文内容。我们可以将它们用于嵌入生成过程,如下图所示:
因此,一旦我们确定了输入数据的文本特征,我们需要建立一个嵌入模型生成我们的数值表示。幸运的是,有一些网站(如HuggingFace)提供了适用于特定语言或任务的预训练模型。在我们的示例中,我们可以使用neuralmind/bert-base-portuguese-cased模型,它是针对以下任务在巴西葡萄牙语中进行训练的:
- 命名实体识别
- 句子文本相似度
- 文本推理
就代码而言,这是我们如何将嵌入生成过程转化为汉语:
from sentence_transformers import SentenceTransformermodel_name = "neuralmind/bert-base-portuguese-cased"encoder = SentenceTransformer(model_name_or_path=model_name)title = """巴拉圭人将在本周日(30日)投票选举新总统"""sentence = titlesentence_embedding = encoder.encode(sentence)print (sentence_embedding)# 输出: np.array([-0.2875876, 0.0356041, 0.31462672, 0.06252239, ...])
所以,给定一个示例输入数据,我们可以将标题和标签内容连接成一个单独的文本,并将其传递给编码器以生成文本嵌入。
我们可以对 NPR 数据集中的所有其他文章应用相同的过程:
def generate_item_sentence(item: pd.Series, text_columns=["title"]) -> str: return ' '.join([item[column] for column in text_columns])df["sentence"] = df.apply(generate_item_sentence, axis=1)df["sentence_embedding"] = df["sentence"].apply(encoder.encode)
注意:请记住,根据您的计算机处理能力,此过程可能需要更长时间。
一旦我们对所有新闻文章进行了嵌入,让我们定义一种存储它们的策略。
2. 存储嵌入
由于生成嵌入可能是一个昂贵的过程,我们可以使用向量数据库来存储这些嵌入,并根据不同的策略执行查询。
有几种向量数据库软件可以完成这个任务,但本文将使用 Qdrant,它是一个开源解决方案,提供了适用于流行编程语言(如Python、Go和Typescript)的 API。要更好地比较这些向量数据库,请参阅这篇 文章 [4]。
设置 Qdrant
为了处理所有 Qdrant 操作,我们需要创建一个指向向量数据库的客户端对象。Qdrant 允许您创建一个免费版本的服务,以测试与数据库的远程连接,但为了简单起见,我将在本地创建并持久化数据库:
from qdrant_client import QdrantClientclient = QdrantClient(path="./qdrant_data")
一旦建立了这个连接,我们就可以在数据库中创建一个集合来存储新闻文章的嵌入:
from qdrant_client import modelsfrom qdrant_client.http.models import Distance, VectorParamsclient.create_collection( collection_name = "news-articles", vectors_config = models.VectorParams( size = encoder.get_sentence_embedding_dimension(), distance = models.Distance.COSINE, ),)print (client.get_collections())# 输出: CollectionsResponse(collections=[CollectionDescription(name='news-articles')])
请注意,向量配置参数用于创建集合。这些参数告诉 Qdrant 向量的某些属性,例如它们的大小和在比较向量时要使用的距离度量(我将使用余弦相似度,但您也可以使用其他策略,如内积或欧氏距离)。
生成向量点
最后,在向数据库填充数据之前,我们需要创建适当的对象进行上传。在 Qdrant 中,可以使用 PointStruct 类存储向量,您可以用它来定义以下属性:
- id:向量的ID(在 NPR 的情况下,是新闻ID)
- vector:表示向量的一维数组(由嵌入模型生成)
- payload:包含任何其他相关元数据的字典,稍后可以用于查询集合中的向量(在 NPR 的情况下,是文章的标题、正文和标签)
from qdrant_client.http.models import PointStructmetadata_columns = df.drop(["newsId", "sentence", "sentence_embedding"], axis=1).columnsdef create_vector_point(item:pd.Series) -> PointStruct: """将向量转换为 PointStruct""" return PointStruct( id = item["newsId"], vector = item["sentence_embedding"].tolist(), payload = { field: item[field] for field in metadata_columns if (str(item[field]) not in ['None', 'nan']) } )points = df.apply(create_vector_point, axis=1).tolist()
上传向量
最后,在将所有项目转换为点结构之后,我们可以将它们分批上传到数据库:
CHUNK_SIZE = 500n_chunks = np.ceil(len(points)/CHUNK_SIZE)for i, points_chunk in enumerate(np.array_split(points, n_chunks)): client.upsert( collection_name="news-articles", wait=True, points=points_chunk.tolist() )
3. 查询向量
现在,集合终于被向量填充,我们可以开始查询数据库了。查询数据库的方式有很多种,但我认为有两种非常有用的输入方式:
- 输入文本
- 输入向量ID
3.1 使用输入向量查询向量
假设我们构建了这个向量数据库用于搜索引擎。在这种情况下,我们期望用户输入的是一个文本输入,我们需要返回最相关的项目。
由于向量数据库的所有操作都是基于…向量完成的,所以我们首先需要将用户的文本输入转换为向量,以便我们可以找到基于该输入的相似项。回想一下,我们使用了句子转换器将文本数据编码为嵌入向量,因此我们可以使用完全相同的编码器为用户的文本输入生成一个数值表示。
假设NPR中包含新闻文章,假设用户输入的是”Donald Trump”,以了解美国选举:
query_text = "Donald Trump"query_vector = encoder.encode(query_text).tolist()print (query_vector)# 输出: [-0.048, -0.120, 0.695, ...]
计算出输入查询向量后,我们可以在集合中搜索最接近的向量,并定义我们希望从这些向量中获得哪些输出,例如它们的newsId、标题和主题:
from qdrant_client.models import Filterfrom qdrant_client.http import modelsclient.search( collection_name="news-articles", query_vector=query_vector, with_payload=["newsId", "title", "topics"], query_filter=None)
注意:默认情况下,Qdrant使用近似最近邻方法快速扫描嵌入向量,但是您也可以进行全面扫描,并找到最接近的邻居,只要记住这是一种更昂贵的操作。
运行此操作后,生成的输出标题如下(为了更好的理解已翻译为英文):
- 输入句子: Donald Trump
- 输出 1: 巴拉圭人将在周日 (30) 进行投票,选择新总统
- 输出 2: 选民表示拜登和特朗普不应参加2024年的选举,路透社/Ipsos民意调查显示
- 输出 3: 作家指控特朗普在1990年代对她进行性虐待
- 输出 4: 前副总统迈克·彭斯给出证词,可能会给前总统特朗普带来麻烦
除了提供与特朗普本人有关的新闻之外,嵌入模型还成功表示了与总统选举相关的话题。请注意,在第一个输出中,除了总统选举之外,并没有直接提到输入词汇”Donald Trump”。
另外,我省略了一个query_filter参数。如果您想指定输出必须满足某个给定条件,这是一个非常有用的工具。例如,在新闻门户网站中,仅过滤最近的文章非常重要(例如过去7天及以后的文章)。因此,您可以查询满足最低发布时间戳的新闻文章。
注意:在新闻推荐的背景下,有许多值得关注的方面,例如公平性和多样性。这是一个讨论的开放性主题,但是如果您对这个领域感兴趣,请查看归一化研讨会(NORMalize Workshop)的文章。
3.2 使用输入向量ID查询向量
最后,我们可以要求向量数据库“推荐”与某些期望的向量ID接近但与不希望的向量ID相距较远的项目。期望的和不希望的ID分别称为正例和负例,并且它们被视为推荐的种子。
例如,假设我们有以下正例ID:
seed_id = '8bc22460-532c-449b-ad71-28dd86790ca2'#标题(翻译):“了解为什么乔·拜登于本周二启动连任竞选”
然后,我们可以请求类似于此示例的项目:
client.recommend( collection_name="news-articles", positive=[seed_id], negative=None, with_payload=["newsId", "title", "topics"])
运行此操作后,以下是翻译的输出标题:
- 输入项目:了解为什么乔·拜登于本周二启动连任竞选
- 输出 1:拜登宣布将参加连任竞选
- 输出 2:美国:导致拜登参加连任竞选的 4 个原因
- 输出 3:选民表示拜登和特朗普不应参加 2024 年选举,路透社/益普索民调显示
- 输出 4:拜登顾问的失言引发对大选后可能出现的第二个政府的疑虑
结论
本文演示了如何结合LLMs和向量数据库进行推荐。特别地,使用句子转换器从NPR数据集中的文本新闻文章生成数值表示(嵌入)。一旦计算出这些嵌入,它们可以填充像Qdrant这样的向量数据库,以便基于几种策略查询向量。
本文的示例后可以进行许多改进,例如:
- 测试其他嵌入模型
- 测试其他距离度量方法
- 测试其他向量数据库
- 使用像Go这样的编译型编程语言以获得更好的性能
- 创建一个用于提供推荐的API
换句话说,可以提出许多想法来改进基于LLMs的推荐的机器学习工程。因此,如果您想分享关于这些改进的想法,请随时给我发送留言:)
关于我
我是一名在巴西媒体科技公司Globo担任高级数据科学家的人。作为公司推荐团队的一员,我周围有一支非常出色和才华横溢的团队,他们付出了很多努力,通过像G1、GE、Globoplay和许多其他数字产品向数百万用户提供个性化内容。没有他们的不可或缺的知识,本文是无法实现的。
参考资料
[1] N. reimers and I. Gurevych, Sentence-BERT: 句子嵌入使用Siamese BERT网络(2019),计算语言学协会。
[2] J. Pinho, J. Silva and L. Figueiredo, NPR: 新闻门户推荐数据集(2023),推荐系统 ACM 会议
[3] V. Boykis, 嵌入是什么?,个人博客
[4] M. Ali, 前 5 名向量数据库(2023),DataCamp 博客