在上一章中,我们介绍了构建对话应用程序的核心步骤。我们从一个基础的聊天机器人开始,然后逐步添加了更复杂的组件,例如记忆、非参数化知识和外部工具。借助LangChain的预构建组件以及Streamlit的UI渲染,这一切都变得相对简单。尽管对话应用程序通常被视为生成式AI和LLM的“舒适区”,但这些模型实际上涵盖了更广泛的应用领域。
在本章中,我们将讨论如何使用LLM来增强推荐系统,结合嵌入和生成模型。我们将学习如何使用LangChain作为框架,创建一个利用最先进的LLM的推荐系统应用程序。
在本章中,我们将涵盖以下主题:
- 推荐系统的定义及其演变
- LLM如何影响这一研究领域
- 使用LangChain构建推荐系统
针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料
分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程
等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓
👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈
技术要求
要完成本书中的任务,您需要以下内容:
- 一个Hugging Face账号和用户访问令牌。
- 一个OpenAI账号和用户访问令牌。
- Python版本3.7.1或更高版本。
请确保安装了以下Python包:langchain
、python-dotenv
、huggingface_hub
、streamlit
、lancedb
、openai
和tiktoken
。这些包可以通过在终端中使用pip install
命令轻松安装。
本章的代码可以在本书的GitHub仓库中找到,地址是:github.com/PacktPublis…
推荐系统简介
推荐系统是一种计算机程序,旨在为电子商务网站和社交网络等数字平台的用户推荐物品。它利用大型数据集来建立用户喜好和兴趣的模型,然后向个人用户推荐类似的物品。
根据使用的方法和数据,推荐系统可以分为不同的类型。以下是一些常见的类型:
-
协同过滤:这种类型的推荐系统使用具有类似偏好的其他用户的评分或反馈。它假设过去喜欢某些物品的用户将来会喜欢类似的物品。例如,如果用户A和用户B都喜欢电影X和Y,那么如果用户B也喜欢电影Z,算法可能会将电影Z推荐给用户A。
协同过滤可以进一步分为两种子类型:基于用户的协同过滤和基于物品的协同过滤:
- 基于用户的协同过滤查找与目标用户相似的用户,并推荐他们喜欢的物品。
- 基于物品的协同过滤查找与目标用户喜欢的物品相似的物品,并推荐这些物品。
-
基于内容的过滤:这种类型的推荐系统使用物品本身的特征或属性,推荐与目标用户之前喜欢或互动过的物品相似的物品。它假设喜欢某个物品特定特征的用户会喜欢其他具有相似特征的物品。与基于物品的协同过滤的主要区别在于,后者使用用户行为模式进行推荐,而基于内容的过滤则使用物品本身的信息。例如,如果用户A喜欢电影X,X是一部由演员Y主演的喜剧片,那么算法可能会推荐电影Z,Z也是一部由演员Y主演的喜剧片。
-
混合过滤:这种类型的推荐系统结合了协同过滤和基于内容的过滤方法,以克服它们的一些局限性,并提供更准确和多样化的推荐。例如,YouTube使用混合过滤,根据观看了类似视频的其他用户的评分和观看次数以及视频本身的特征和类别推荐视频。
-
基于知识的过滤:这种类型的推荐系统使用有关领域和用户需求或偏好的显式知识或规则,推荐满足特定标准或约束的物品。它不依赖于其他用户的评分或反馈,而是基于用户的输入或查询。例如,如果用户A想购买一台具有特定规格和预算的笔记本电脑,算法可能会推荐一台符合这些标准的笔记本电脑。基于知识的推荐系统在评分历史很少或没有可用时,或者物品复杂且可定制时效果很好。
在上述框架内,可以使用各种机器学习技术,我们将在下一节中讨论这些技术。
现有的推荐系统
现代推荐系统使用机器学习(ML)技术来基于以下数据更好地预测用户的偏好:
- 用户行为数据:关于用户与产品交互的见解。这些数据可以从用户评分、点击和购买记录等因素中获取。
- 用户人口统计数据:指用户的个人信息,包括年龄、教育背景、收入水平和地理位置等详细信息。
- 产品属性数据:涉及产品特征的信息,例如书籍的类型、电影的演员阵容或食品中的特定菜系。
截至目前,一些最流行的ML技术包括K近邻(K-nearest neighbors)、降维(dimensionality reduction)和神经网络。让我们详细了解这些方法。
K近邻算法
K近邻(KNN)是一种可以用于分类和回归问题的机器学习算法。它通过找到距离新数据点最近的k个数据点(k是用户在初始化算法之前设置的),并使用它们的标签或值来进行预测。KNN基于一个假设,即相似的数据点可能具有相似的标签或值。
KNN可以应用于推荐系统中的协同过滤,包括基于用户的协同过滤和基于物品的协同过滤:
- 基于用户的KNN 是一种协同过滤方法,它使用具有类似品味或偏好的其他用户的评分或反馈来推荐物品。例如,假设我们有三位用户:Alice、Bob 和 Charlie。他们都在网上购买书籍并对其进行评分。Alice 和 Bob 都喜欢(高评分)《哈利·波特》系列和《霍比特人》这本书。系统会发现这个模式,并认为 Alice 和 Bob 的品味相似。如果 Bob 还喜欢《权力的游戏》这本书,而 Alice 尚未阅读,系统就会向 Alice 推荐《权力的游戏》。这是因为系统假设既然 Alice 和 Bob 有相似的品味,那么 Alice 也可能喜欢《权力的游戏》。
- 基于物品的KNN 是另一种协同过滤方法,它使用物品的属性或特征来向目标用户推荐类似的物品。例如,考虑同样的用户及其对书籍的评分。系统注意到《哈利·波特》系列和《霍比特人》都得到了 Alice 和 Bob 的喜欢,所以系统认为这两本书是相似的。如果 Charlie 读过并喜欢《哈利·波特》,系统就会向 Charlie 推荐《霍比特人》。这是因为系统假设既然《哈利·波特》和《霍比特人》相似(都被相同的用户喜欢),Charlie 也可能喜欢《霍比特人》。
KNN是一种在推荐系统中广受欢迎的技术,但它也存在一些缺陷:
- 可扩展性:当处理大数据集时,KNN可能变得计算密集且缓慢,因为它需要计算所有物品或用户之间的距离。
- 冷启动问题:对于新物品或新用户,KNN面临挑战,因为它们缺乏足够的交互历史数据,难以找到有意义的邻居。
- 数据稀疏性:在数据稀疏的情况下,KNN性能可能会下降,因为缺少足够的邻居信息来做出准确的预测。
- 特征相关性:KNN将所有特征视为等同,并假设所有特征在相似性计算中同等重要。这在某些场景下可能并不成立,有些特征可能比其他特征更重要。
- K值选择:选择适当的K值(邻居数量)可能是主观的,并且会影响推荐的质量。K值太小可能导致噪声,K值太大可能导致推荐过于宽泛。
一般来说,KNN适用于小数据集、噪声较少的场景(以免异常值、缺失值等影响距离度量)和动态数据(KNN是一种基于实例的方法,不需要重新训练,可以快速适应变化)。
此外,在推荐系统领域还广泛使用了其他技术,如矩阵分解。
矩阵分解
矩阵分解是一种用于推荐系统的技术,用于基于历史数据分析和预测用户偏好或行为。它通过将一个大的矩阵分解为两个或多个较小的矩阵,揭示出数据模式中的潜在特征,从而解决所谓的“维度灾难”。
定义
维度灾难指的是处理高维数据时出现的挑战。这导致复杂性增加、数据稀疏以及由于数据需求指数增长和潜在的过拟合而导致的分析和建模困难。
在推荐系统的背景下,这种技术用于预测用户-物品交互矩阵中缺失的值,该矩阵表示用户与各种物品(如电影、产品或书籍)的交互情况。
例如,假设你有一个矩阵,其中行代表用户,列代表电影,单元格包含评分(从1到5分)。然而,并非所有用户都对所有电影进行了评分,导致矩阵中存在许多缺失条目:
电影1 | 电影2 | 电影3 | 电影4 | |
---|---|---|---|---|
用户1 | 4 | - | 5 | - |
用户2 | - | 3 | - | 2 |
用户3 | 5 | 4 | - | 3 |
表7.1:带有缺失数据的数据集示例
矩阵分解旨在将此矩阵分解为两个矩阵:一个是用户矩阵,另一个是电影矩阵,维度数量减少(即潜在因素)。这些潜在因素可能代表像类型偏好或特定电影特征等属性。通过将这些矩阵相乘,你可以预测缺失的评分,并推荐用户可能喜欢的电影。
矩阵分解有不同的算法,包括:
- 奇异值分解(SVD) 将一个矩阵分解为三个单独的矩阵,其中中间矩阵包含代表数据中不同成分重要性的奇异值。它广泛用于数据压缩、降维和推荐系统中的协同过滤。
- 主成分分析(PCA) 是一种通过将数据转换到与主成分对齐的新坐标系中来降低数据维度的技术。这些成分捕捉了数据中最重要的变异性,从而实现高效的分析和可视化。
- 非负矩阵分解(NMF) 将矩阵分解为两个具有非负值的矩阵。它通常用于主题建模、图像处理和特征提取,其中成分代表非负属性。
在推荐系统的背景下,可能最流行的技术是SVD(得益于其可解释性、灵活性以及处理缺失值和性能的能力)。以下是如何在Python中使用numpy模块应用SVD的示例:
import numpy as np
# 你的用户-电影评分矩阵(用实际数据替换)
user_movie_matrix = np.array([
[4, 0, 5, 0],
[0, 3, 0, 2],
[5, 4, 0, 3]
])
# 应用SVD
U, s, V = np.linalg.svd(user_movie_matrix, full_matrices=False)
# 潜在因子的数量(可以根据需要选择)
num_latent_factors = 2
# 使用选定的潜在因子重构原始矩阵
reconstructed_matrix = U[:, :num_latent_factors] @ np.diag(s[:num_latent_factors]) @ V[:num_latent_factors, :]
# 将负值替换为0
reconstructed_matrix = np.maximum(reconstructed_matrix, 0)
print("重构矩阵:")
print(reconstructed_matrix)
以下是输出结果:
重构矩阵:
[[4.2972542 0. 4.71897811 0. ]
[1.08572801 2.27604748 0. 1.64449028]
[4.44777253 4.36821972 0.52207171 3.18082082]]
在这个示例中,U矩阵包含用户相关的信息,s矩阵包含奇异值,V矩阵包含电影相关的信息。通过选择一定数量的潜在因子(num_latent_factors
),可以在降维的情况下重构原始矩阵,同时在np.linalg.svd
函数中设置full_matrices=False
参数以确保分解的矩阵被截断为与选定的潜在因子数量一致的维度。
这些预测的评分可以用于向用户推荐具有较高预测评分的电影。矩阵分解使推荐系统能够发现用户偏好中的隐藏模式,并基于这些模式进行个性化推荐。
矩阵分解在推荐系统中广泛使用,尤其是在处理包含大量用户和物品的大型数据集时,因为它能够高效地捕捉潜在因子;或者当你希望基于潜在因子提供个性化推荐时,因为它为每个用户和物品学习了独特的潜在表示。然而,它也存在一些缺陷(与KNN的技术类似):
- 冷启动问题:与KNN类似,矩阵分解在处理交互历史有限或不存在的新物品或用户时面临挑战。由于它依赖于历史数据,无法为新物品或用户提供有效的推荐。
- 数据稀疏性:随着用户和物品数量的增加,用户-物品交互矩阵变得越来越稀疏,从而导致准确预测缺失值的挑战。
- 可扩展性:对于大型数据集,执行矩阵分解可能计算代价高且耗时。
- 上下文限制:矩阵分解通常仅考虑用户-物品交互,忽略了时间、地点或其他用户属性等上下文信息。
因此,近年来人们探索了神经网络(NN)作为替代方案,以缓解这些缺陷。
针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料
分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程
等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓
👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈
神经网络
神经网络(NN)用于推荐系统,以通过从数据中学习复杂模式来提高推荐的准确性和个性化。以下是神经网络在此上下文中常见的应用方式:
- 使用神经网络的协同过滤:神经网络可以通过将用户和物品嵌入到连续的向量空间来建模用户-物品交互。这些嵌入捕捉用户偏好和物品特征的潜在特征。神经协同过滤模型将这些嵌入与神经网络架构相结合,以预测用户与物品之间的评分或交互。
- 基于内容的推荐:在基于内容的推荐系统中,神经网络可以学习物品内容的表示,例如文本、图像或音频。这些表示捕捉物品特征和用户偏好。像卷积神经网络(CNN)和递归神经网络(RNN)这样的神经网络用于处理和学习物品内容,从而实现个性化的基于内容的推荐。
- 序列模型:在用户交互具有时间序列的场景中,例如点击流或浏览历史,RNN或其变体(如长短期记忆网络LSTM)可以捕捉用户行为中的时间依赖性,并进行序列推荐。
- 自编码器和变分自编码器(VAEs) 可用于学习用户和物品的低维表示。
定义
自编码器是一种用于无监督学习和降维的神经网络架构。它们由编码器和解码器组成。编码器将输入数据映射到低维潜在空间表示,而解码器则尝试从编码表示中重构原始输入数据。
变分自编码器(VAEs)是传统自编码器的扩展,它引入了概率元素。VAEs不仅学习将输入数据编码到潜在空间,还使用概率方法对该潜在空间的分布进行建模。这使得可以从学习到的潜在空间中生成新的数据样本。VAEs用于生成任务,如图像合成、异常检测和数据插补。
在自编码器和VAEs中,核心思想是学习输入数据在潜在空间中的压缩和有意义的表示,这对包括特征提取、数据生成和降维在内的各种任务都有用。
这些表示可以用于通过在潜在空间中识别相似的用户和物品来进行推荐。事实上,NN独特的架构特点使其能够应用以下技术:
- 边信息集成:NN可以结合其他用户和物品属性,如人口统计信息、位置或社交关系,通过从多种数据源学习来改进推荐。
- 深度强化学习:在某些场景中,可以使用深度强化学习来优化推荐系统,从用户反馈中学习,并建议能够最大化长期奖励的行动。
NN提供了灵活性和捕捉数据中复杂模式的能力,使其非常适合推荐系统。然而,为了达到最佳性能,它们也需要精心设计、训练和调整。NN还带来了一些挑战,包括以下几点:
- 复杂性增加:NN,特别是深度神经网络(DNN),由于其分层结构,可能变得非常复杂。随着隐藏层和神经元的增加,模型学习复杂模式的能力也会增强。
- 训练需求:NN是重型模型,其训练需要特殊的硬件要求,包括GPU,这可能非常昂贵。
- 潜在的过拟合:过拟合发生在ANN在训练数据上表现异常好,但在未见过的数据上无法推广时。
选择合适的架构、处理大型数据集和调整超参数对于有效使用NN进行推荐至关重要。
尽管近年来取得了相关的进展,但上述技术仍存在一些缺陷,主要是它们具有任务特定性。例如,一个基于评分预测的推荐系统无法处理需要推荐与用户口味相符的前k个物品的任务。实际上,如果我们将这种限制扩展到其他“LLM之前”的AI解决方案,我们可能会发现一些相似之处:实际上,正是这种任务特定的情况正在被LLM(以及更广泛的Large Foundation Models)所革命化,它们高度泛化并且能够根据用户的提示和指令适应各种任务。因此,关于LLM在多大程度上能够增强现有推荐模型的研究正在广泛展开。在接下来的部分中,我们将参考最近的论文和博客,讨论这些新方法背后的理论。
LLMs 如何改变推荐系统
在前几章中,我们了解了如何通过三种主要方式定制LLM:预训练、微调和提示。根据Wenqi Fan等人撰写的论文《大语言模型时代的推荐系统》(Recommender systems in the Era of Large Language Models (LLMs)),这些技术也可以用来将LLM定制为推荐系统:
- 预训练:为推荐系统预训练LLM是使LLM能够获取广泛的世界知识和用户偏好的重要步骤,并能够以零样本或少量样本适应不同的推荐任务。
**注意** 一个推荐系统LLM的示例是P5,由Shijie Gang等人在论文《推荐作为语言处理(RLP):统一的预训练、个性化提示与预测范式》(Recommendation as Language Processing (RLP): A Unified Pretrain, Personalized Prompt & Predict Paradigm (P5))中提出。 P5是一个使用大语言模型(LLM)构建推荐系统的统一文本到文本范式。它包含三个步骤:
1. **预训练**:基于T5架构的基础语言模型在大规模网络语料库上进行预训练,并在推荐任务上进行微调。
1. **个性化提示**:根据用户的行为数据和上下文特征,为每个用户生成个性化提示。
1. **预测**:将个性化提示输入到预训练的语言模型中生成推荐。
P5基于这样的理念,即LLM可以编码广泛的世界知识和用户偏好,并可以通过零样本或少量样本适应不同的推荐任务。
-
微调:从头开始训练一个LLM是一项非常计算密集的活动。一个替代且影响较小的方法是通过微调来定制一个用于推荐系统的LLM。
更具体地说,论文的作者回顾了两种主要的微调LLM的策略:
- 全模型微调:根据特定任务的推荐数据集更改整个模型的权重。
- 参数高效微调:仅更改一小部分权重或开发可训练的适配器以适应特定任务。
-
提示:将LLM定制为推荐系统的第三种也是“最轻量”的方法是提示。根据作者的说法,有三种主要的提示LLM的技术:
- 常规提示:通过设计文本模板或提供一些输入输出示例,将下游任务统一为语言生成任务。
- 上下文学习:使LLM能够基于上下文信息学习新任务,而无需进行微调。
- 思维链:通过提供多个示例来描述思维链作为提示中的示例,从而增强LLM的推理能力。作者还讨论了每种技术的优势和挑战,并提供了一些采用这些技术的现有方法的示例。
无论是哪种类型,提示都是测试通用LLM能否处理推荐系统任务的最快方式。
LLM在推荐系统领域的应用正在引起研究界的兴趣,并且如上所述,已经有了一些有趣的结果。在下一节中,我们将使用提示方法并利用LangChain作为AI编排器的能力,来实现我们自己的推荐应用程序。
实现一个基于LLM的推荐系统
现在我们已经讨论了一些关于推荐系统的理论以及LLM如何增强它们的最新研究,让我们开始构建我们的推荐应用程序——一个名为MovieHarbor的电影推荐系统。目标是使其尽可能通用,也就是说,我们希望我们的应用程序能够通过对话界面处理各种推荐任务。我们将模拟所谓的“冷启动”场景,即用户与推荐系统的第一次交互,此时我们没有用户的偏好历史。我们将利用一个包含文本描述的电影数据库来实现这一目的。
为此,我们将使用Kaggle上的Movie recommendation data数据集,该数据集可以在 此处 获取。
使用包含每部电影文本描述的数据集的原因是,这样我们可以获得文本的嵌入。让我们开始构建我们的MovieHarbor应用程序。
数据预处理
为了将LLM应用于我们的数据集,我们首先需要对数据进行预处理。初始数据集包含了多个列,但我们感兴趣的列如下:
- Genres:电影的适用类型列表。
- Title:电影标题。
- Overview:剧情的文本描述。
- Vote_average:给定电影的评分,范围为1到10。
- Vote_count:给定电影的评分次数。
我不会在这里列出整个代码(您可以在本书的GitHub仓库中找到完整代码,地址是 此处),但我会分享数据预处理的主要步骤:
首先,我们将genres
列转换为numpy数组,这比数据集中原始的字典格式更容易处理:
import pandas as pd
import ast
# 将字典的字符串表示转换为实际的字典
md['genres'] = md['genres'].apply(ast.literal_eval)
# 转换 'genres' 列
md['genres'] = md['genres'].apply(lambda x: [genre['name'] for genre in x])
接下来,我们将vote_average
和vote_count
列合并为一个列,这个列是根据投票数量的加权评分。我还将行限制在95百分位数的投票数量以内,以便去除最低投票数以防止结果偏斜:
# 计算加权评分 (IMDb 公式)
def calculate_weighted_rate(vote_average, vote_count, min_vote_count=10):
return (vote_count / (vote_count + min_vote_count)) * vote_average + (min_vote_count / (vote_count + min_vote_count)) * 5.0
# 防止结果偏斜的最低投票数
vote_counts = md[md['vote_count'].notnull()]['vote_count'].astype('int')
min_vote_count = vote_counts.quantile(0.95)
# 创建新列 'weighted_rate'
md['weighted_rate'] = md.apply(lambda row: calculate_weighted_rate(row['vote_average'], row['vote_count'], min_vote_count), axis=1)
接下来,我们创建一个名为combined_info
的新列,在该列中我们将合并所有要提供给LLM的上下文元素。这些元素包括电影标题、简介、类型和评分:
md_final['combined_info'] = md_final.apply(lambda row: f"Title: {row['title']}. Overview: {row['overview']} Genres: {', '.join(row['genres'])}. Rating: {row['weighted_rate']}", axis=1).astype(str)
我们将电影的combined_info
进行分词,以便在嵌入时获得更好的结果:
import pandas as pd
import tiktoken
import os
import openai
openai.api_key = os.environ["OPENAI_API_KEY"]
from openai.embeddings_utils import get_embedding
embedding_encoding = "cl100k_base" # text-embedding-ada-002 的编码
max_tokens = 8000 # text-embedding-ada-002 的最大值是8191
encoding = tiktoken.get_encoding(embedding_encoding)
# 忽略过长的文本
md_final["n_tokens"] = md_final.combined_info.apply(lambda x: len(encoding.encode(x)))
md_final = md_final[md_final.n_tokens <= max_tokens]
定义
cl100k_base是OpenAI的嵌入API使用的一个分词器的名称。分词器是一种工具,它将文本字符串分割为称为token的单元,然后这些token可以被神经网络处理。不同的分词器有不同的规则和词汇来决定如何分割文本以及使用哪些token。cl100k_base分词器基于字节对编码(BPE)算法,它从大型语料库中学习子词单元的词汇。cl100k_base分词器具有10万个token的词汇量,这些token大多是常见的单词和单词片段,但也包括一些标点符号、格式和控制的特殊token。它可以处理多种语言和领域的文本,并且每个输入最多可以编码8191个token。
我们使用text-embedding-ada-002嵌入文本:
md_final["embedding"] = md_final.overview.apply(lambda x: get_embedding(x, engine=embedding_model))
在更改了一些列的名称并删除不必要的列后,最终的数据集如下所示:
让我们来看一下随机一行文本:
md['text'][0]
输出结果如下:
'Title: GoldenEye. Overview: James Bond must unmask the mysterious head of the Janus Syndicate and prevent the leader from utilizing the GoldenEye weapons system to inflict devastating revenge on Britain. Genres: Adventure, Action, Thriller. Rating: 6.173464373464373'
我们将进行的最后一次修改是更改一些命名约定和数据类型,如下所示:
md_final.rename(columns={'embedding': 'vector'}, inplace=True)
md_final.rename(columns={'combined_info': 'text'}, inplace=True)
md_final.to_pickle('movies.pkl')
现在我们有了最终的数据集,需要将其存储在一个向量数据库中。为此,我们将使用LanceDB,这是一个开源的、带有持久存储的向量搜索数据库,它极大地简化了嵌入的检索、过滤和管理,并且还与LangChain提供了原生集成。您可以通过以下命令轻松安装LanceDB:
pip install lancedb
然后,我们可以将数据存储在LanceDB中:
import lancedb
uri = "data/sample-lancedb"
db = lancedb.connect(uri)
table = db.create_table("movies", md)
现在我们已经准备好了所有的基础数据和工具,可以开始处理这些嵌入并构建我们的推荐系统了。我们将从冷启动场景中的一个简单任务开始,并逐步增加LangChain组件的复杂性。之后,我们还将尝试基于内容的场景,以通过各种任务挑战我们的LLM。
针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料
分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程
等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓
👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈
在冷启动场景中构建一个QA推荐聊天机器人
在前面的章节中,我们了解了冷启动场景——即在没有用户背景信息的情况下首次与用户互动——是推荐系统经常遇到的问题。我们对用户的信息越少,越难以将推荐与他们的偏好相匹配。
在本节中,我们将使用LangChain和OpenAI的LLM模拟一个冷启动场景,采用以下高级架构:
在之前的部分中,我们已经将嵌入保存到LanceDB中。现在,我们将构建一个LangChain的RetrievalQA检索器,这是一个专为基于索引的问答而设计的链组件。在我们的例子中,我们将使用向量存储作为我们的索引检索器。该链的想法是根据用户的查询,返回最相似的前k部电影,使用余弦相似度作为距离度量(这是默认的度量方式)。
让我们开始构建这个链:
我们仅使用电影的简介作为信息输入:
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import LanceDB
import os
os.environ["OPENAI_API_KEY"]
embeddings = OpenAIEmbeddings()
docsearch = LanceDB(connection=table, embedding=embeddings)
query = "I'm looking for an animated action movie. What could you suggest to me?"
docs = docsearch.similarity_search(query)
docs
以下是对应的输出(我将显示输出的一个截断版本,仅展示四个文档来源中的第一个):
[Document(page_content='Title: Hitman: Agent 47. Overview: An assassin teams up with a woman to help her find her father and uncover the mysteries of her ancestry. Genres: Action, Crime, Thriller. Rating: 5.365800865800866', metadata={'genres': array(['Action', 'Crime', 'Thriller'], dtype=object), 'title': 'Hitman: Agent 47', 'overview': 'An assassin teams up with a woman to help her find her father and uncover the mysteries of her ancestry.', 'weighted_rate': 5.365800865800866, 'n_tokens': 52, 'vector': array([-0.00566491, -0.01658553, […]
如您所见,每个文档旁边都显示了所有的变量作为元数据,并且距离也被作为得分报告。距离越小,用户查询与电影文本嵌入之间的接近程度越大。
一旦我们收集到最相似的文档,我们就想要生成一个对话式的响应。为此,除了使用嵌入模型外,我们还将使用OpenAI的完成模型GPT-3,并将其与RetrievalQA结合:
qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(), return_source_documents=True)
query = "I'm looking for an animated action movie. What could you suggest to me?"
result = qa({"query": query})
result['result']
让我们看看输出结果:
' I would suggest Transformers. It is an animated action movie with genres of Adventure, Science Fiction, and Action, and a rating of 6.'
由于我们设置了return_source_documents=True
参数,我们还可以检索文档来源:
result['source_documents'][0]
以下是输出结果:
Document(page_content='Title: Hitman: Agent 47. Overview: An assassin teams up with a woman to help her find her father and uncover the mysteries of her ancestry. Genres: Action, Crime, Thriller. Rating: 5.365800865800866', metadata={'genres': array(['Action', 'Crime', 'Thriller'], dtype=object), 'title': 'Hitman: Agent 47', 'overview': 'An assassin teams up with a woman to help her find her father and uncover the mysteries of her ancestry.', 'weighted_rate': 5.365800865800866, 'n_tokens': 52, 'vector': array([-0.00566491, -0.01658553, -0.02255735, ..., -0.01242317, -0.01303058, -0.00709073], dtype=float32), '_distance': 0.42414575815200806})
注意,第一个报告的文档并不是模型推荐的文档。这可能是因为评分较低,而《变形金刚》则是第三个结果。这是一个很好的例子,展示了LLM如何在相似度之外考虑多个因素来向用户推荐电影。
模型能够生成对话式的答案,但它仍然只使用了可用信息的一部分——文本概述。如果我们希望我们的MovieHarbor系统也利用其他变量,该怎么办?我们可以通过两种方式来解决这个任务:
-
“过滤器”方式:这种方法包括在我们的检索器中添加一些过滤器作为kwargs,这些可能是应用程序在响应用户之前所需的。例如,这些问题可能与电影的类型有关。
例如,假设我们只想提供类型被标记为喜剧的电影结果。你可以使用以下代码来实现:
df_filtered = md[md['genres'].apply(lambda x: 'Comedy' in x)] qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(search_kwargs={'data': df_filtered}), return_source_documents=True) query = "I'm looking for a movie with animals and an adventurous plot." result = qa({"query": query})
过滤器也可以在元数据级别操作,例如在以下示例中,我们只想过滤评分高于7的结果:
qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(search_kwargs={'filter': {weighted_rate__gt:7}}), return_source_documents=True)
-
“代理”方式:这可能是解决问题最创新的方式。使我们的链成为代理意味着将检索器转换为工具,代理在需要时可以利用这些工具,包括其他变量。通过这样做,用户只需使用自然语言提供他们的偏好,代理就可以在需要时检索最有希望的推荐。
让我们看看如何通过代码实现这一点,特别要求一个动作电影(因此过滤类型变量):
from langchain.agents.agent_toolkits import create_retriever_tool from langchain.agents.agent_toolkits import create_conversational_retrieval_agent from langchain.chat_models import ChatOpenAI llm = ChatOpenAI(temperature=0) retriever = docsearch.as_retriever(return_source_documents=True) tool = create_retriever_tool( retriever, "movies", "Searches and returns recommendations about movies." ) tools = [tool] agent_executor = create_conversational_retrieval_agent(llm, tools, verbose=True) result = agent_executor({"input": "suggest me some action movies"})
让我们看看思维链和输出(始终基于根据余弦相似度得出的最相似的四部电影)的结果:
> Entering new AgentExecutor chain... Invoking: `movies` with `{'genre': 'action'}` [Document(page_content='The action continues from [REC], […] Here are some action movies that you might enjoy: 1. [REC]² - The action continues from [REC], with a medical officer and a SWAT team sent into a sealed-off apartment to control the situation. It is a thriller/horror movie. 2. The Boondock Saints - Twin brothers Conner and Murphy take swift retribution into their own hands to rid Boston of criminals. It is an action/thriller/crime movie. 3. The Gamers - Four clueless players are sent on a quest to rescue a princess and must navigate dangerous forests, ancient ruins, and more. It is an action/comedy/thriller/foreign movie. 4. Atlas Shrugged Part III: Who is John Galt? - In a collapsing economy, one man has the answer while others try to control or save him. It is a drama/science fiction/mystery movie. Please note that these recommendations are based on the genre "action" and may vary in terms of availability and personal preferences. > Finished chain.
最后,我们可能还希望使我们的应用程序更加符合其作为推荐系统的目标。为此,我们需要进行一些提示工程。
注意
使用LangChain的预构建组件(如RetrievalQA链)的优势之一是它们自带预配置的、精心策划的提示模板。在覆盖现有提示之前,最好先检查它,这样你就可以看到组件已经预期的变量(在{}中)。
要探索现有的提示,你可以运行以下代码:
print(qa.combine_documents_chain.llm_chain.prompt.template)
输出结果如下:
Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}
Question: {question}
Helpful Answer:
例如,假设我们希望系统为每个用户的请求返回三个建议,并简要描述剧情和用户可能喜欢它的原因。以下是一个可能符合此目标的示例提示:
from langchain.prompts import PromptTemplate
template = """You are a movie recommender system that help users to find movies that match their preferences.
Use the following pieces of context to answer the question at the end.
For each question, suggest three movies, with a short description of the plot and the reason why the user might like it.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}
Question: {question}
Your response:"""
PROMPT = PromptTemplate(
template=template, input_variables=["context", "question"])
现在我们需要将其传递到我们的链中:
PROMPT = PromptTemplate(
template=template, input_variables=["context", "question"])
chain_type_kwargs = {"prompt": PROMPT}
qa = RetrievalQA.from_chain_type(llm=OpenAI(),
chain_type="stuff",
retriever=docsearch.as_retriever(),
return_source_documents=True,
chain_type_kwargs=chain_type_kwargs)
query = "I'm looking for a funny action movie, any suggestion?"
result = qa({'query':query})
print(result['result'])
得到以下输出:
1. A Good Day to Die Hard: An action-packed comedy directed by John Moore, this movie follows Iconoclastic, take-no-prisoners cop John McClane as he travels to Moscow to help his wayward son Jack. With the Russian underworld in pursuit, and battling a countdown to war, the two McClanes discover that their opposing methods make them unstoppable heroes.
2. The Hidden: An alien is on the run in America and uses the bodies of anyone in its way as a hiding place. With lots of innocent people dying in the chase, this action-packed horror movie is sure to keep you laughing.
3. District B13: Set in the ghettos of Paris in 2010, this action-packed science fiction movie follows an undercover cop and ex-thug as they try to infiltrate a gang in order to defuse a neutron bomb. A thrilling comedy that will keep you laughing.
我们还可以在提示中加入我们希望设置为欢迎页面的对话预备问题收集到的信息。例如,在让用户输入自然语言问题之前,我们可能希望询问他们的年龄、性别和喜欢的电影类型。为此,我们可以在提示中插入一个部分,其中包含用户共享的信息,然后将此提示部分与我们将传递给链的最终提示结合。下面是一个示例(为简单起见,我们将直接设置变量,而不向用户询问):
from langchain.prompts import PromptTemplate
template_prefix = """You are a movie recommender system that help users to find movies that match their preferences.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}"""
user_info = """This is what we know about the user, and you can use this information to better tune your research:
Age: {age}
Gender: {gender}"""
template_suffix = """Question: {question}
Your response:"""
user_info = user_info.format(age=18, gender='female')
COMBINED_PROMPT = template_prefix + '\n' + user_info + '\n' + template_suffix
print(COMBINED_PROMPT)
输出结果如下:
You are a movie recommender system that help users to find movies that match their preferences.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}
This is what we know about the user, and you can use this information to better tune your research:
Age: 18
Gender: female
Question: {question}
Your response:
现在让我们格式化提示并传递到我们的链中:
PROMPT = PromptTemplate(
template=COMBINED_PROMPT, input_variables=["context", "question"])
chain_type_kwargs = {"prompt": PROMPT}
qa = RetrievalQA.from_chain_type(llm=OpenAI(),
chain_type="stuff",
retriever=docsearch.as_retriever(),
return_source_documents=True,
chain_type_kwargs=chain_type_kwargs)
result = qa({'query': query})
result['result']
我们得到以下输出:
' Sure, I can suggest some action movies for you. Here are a few examples: A Good Day to Die Hard, Goldfinger, Ong Bak 2, and The Raid 2. All of these movies have high ratings and feature thrilling action elements. I hope you find something that you enjoy!'
如您所见,系统考虑了用户提供的信息。当我们构建MovieHarbor的前端时,我们将使这些信息成为动态的作为向用户提出的预备问题。
构建基于内容的系统
在上一节中,我们讨论了冷启动场景,在这种情况下系统对用户一无所知。有时,推荐系统已经掌握了一些关于用户的背景信息,将这些知识嵌入我们的应用程序中是非常有用的。举个例子,假设我们有一个用户数据库,系统已经存储了所有注册用户的信息(如年龄、性别、国家等)以及用户已经观看的电影及其评分。
为了实现这一点,我们需要设置一个自定义提示,以便能够从源中检索这些信息。为了简单起见,我们将创建一个包含用户信息的样本数据集,该数据集只包含两个记录,对应两个用户。每个用户将展示以下变量:用户名、年龄、性别,以及一个包含已观看电影及其评分的字典。
以下是表示这一高级架构的示意图:
让我们分解这个架构,并检查每个步骤,以构建最终的基于内容的系统聊天机器人,从可用的用户数据开始:
正如之前讨论的那样,我们现在对用户的偏好有了一些了解。更具体地说,假设我们有一个包含用户属性(姓名、年龄、性别)以及他们对一些电影的评分(评分从1到10不等)的数据集。以下是用于创建该数据集的代码:
import pandas as pd
data = {
"username": ["Alice", "Bob"],
"age": [25, 32],
"gender": ["F", "M"],
"movies": [
[("Transformers: The Last Knight", 7), ("Pokémon: Spell of the Unknown", 5)],
[("Bon Cop Bad Cop 2", 8), ("Goon: Last of the Enforcers", 9)]
]
}
# 将 "movies" 列转换为字典
for i, row_movies in enumerate(data["movies"]):
movie_dict = {}
for movie, rating in row_movies:
movie_dict[movie] = rating
data["movies"][i] = movie_dict
# 创建一个 pandas DataFrame
df = pd.DataFrame(data)
df.head()
得到以下输出:
图7.4:样本用户数据集
现在我们要做的是应用与冷启动提示相同的逻辑,并使用变量进行格式化。不同之处在于,这里我们不再要求用户提供这些变量的值,而是直接从用户数据集中收集这些信息。因此,我们首先定义提示块:
template_prefix = """You are a movie recommender system that help users to find movies that match their preferences.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}"""
user_info = """This is what we know about the user, and you can use this information to better tune your research:
Age: {age}
Gender: {gender}
Movies already seen alongside with rating: {movies}"""
template_suffix= """Question: {question}
Your response:"""
然后我们按如下方式格式化user_info
块(假设与系统交互的用户是Alice):
age = df.loc[df['username']=='Alice']['age'][0]
gender = df.loc[df['username']=='Alice']['gender'][0]
movies = ''
# 遍历字典并输出电影名称和评分
for movie, rating in df['movies'][0].items():
output_string = f"Movie: {movie}, Rating: {rating}" + "\n"
movies += output_string
user_info = user_info.format(age=age, gender=gender, movies=movies)
COMBINED_PROMPT = template_prefix +'\n'+ user_info +'\n'+ template_suffix
print(COMBINED_PROMPT)
以下是输出结果:
You are a movie recommender system that help users to find movies that match their preferences.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}
This is what we know about the user, and you can use this information to better tune your research:
Age: 25
Gender: F
Movies already seen alongside with rating: Movie: Transformers: The Last Knight, Rating: 7
Movie: Pokémon: Spell of the Unknown, Rating: 5
Question: {question}
Your response:
现在让我们在链中使用这个提示:
PROMPT = PromptTemplate(
template=COMBINED_PROMPT, input_variables=["context", "question"])
chain_type_kwargs = {"prompt": PROMPT}
qa = RetrievalQA.from_chain_type(llm=OpenAI(),
chain_type="stuff",
retriever=docsearch.as_retriever(),
return_source_documents=True,
chain_type_kwargs=chain_type_kwargs)
query = "Can you suggest me some action movie based on my background?"
result = qa({'query': query})
result['result']
我们得到以下输出:
" Based on your age, gender, and the movies you've already seen, I would suggest the following action movies: The Raid 2 (Action, Crime, Thriller; Rating: 6.71), Ong Bak 2 (Adventure, Action, Thriller; Rating: 5.24), Hitman: Agent 47 (Action, Crime, Thriller; Rating: 5.37), and Kingsman: The Secret Service (Crime, Comedy, Action, Adventure; Rating: 7.43)."
如您所见,模型现在能够根据用户过去偏好的信息(作为上下文从模型的元提示中检索)向Alice推荐一系列电影。
请注意,在这个场景中,我们使用了一个简单的pandas数据框作为数据集。在生产环境中,存储与任务相关的变量(如推荐任务)的最佳实践是使用特征存储。特征存储是旨在支持机器学习工作流的数据系统,它们允许数据团队存储、管理和访问用于训练和部署机器学习模型的特征。
此外,LangChain还提供了与一些最流行的特征存储的原生集成:
- Feast:这是一个开源的机器学习特征存储。它允许团队定义、管理、发现和服务特征。Feast支持批处理和流数据源,并与各种数据处理和存储系统集成。Feast使用BigQuery进行离线特征存储,并使用BigTable或Redis进行在线特征服务。
- Tecton:这是一个托管的特征平台,提供了构建、部署和使用机器学习特征的完整解决方案。Tecton允许用户在代码中定义特征,对其进行版本控制,并按照最佳实践将其部署到生产中。此外,它还与现有数据基础设施和机器学习平台(如SageMaker和Kubeflow)集成,并使用Spark进行特征转换,使用DynamoDB进行在线特征服务。
- Featureform:这是一个虚拟特征存储,将现有的数据基础设施转变为特征存储。Featureform允许用户使用标准特征定义和Python SDK创建、存储和访问特征。它编排和管理特征工程和物化所需的数据管道,并兼容多种数据系统,如Snowflake、Redis、Spark和Cassandra。
- AzureML Managed Feature Store:这是一个新的工作空间类型,允许用户发现、创建和操作化特征。此服务与现有的数据存储、特征管道和机器学习平台(如Azure Databricks和Kubeflow)集成。此外,它使用SQL、PySpark、SnowPark或Python进行特征转换,并使用Parquet/S3或Cosmos DB进行特征存储。
您可以在LangChain的博客上关于LangChain与特征存储的集成信息。
使用Streamlit开发前端
现在我们已经了解了基于LLM的推荐系统的逻辑,是时候为我们的MovieHarbor提供一个GUI了。为此,我们将再次利用Streamlit,并假设冷启动场景。和往常一样,您可以在本书的GitHub仓库中找到完整的Python代码:Building-LLM-Powered-Applications。
和第六章的Globebotter应用程序一样,在这种情况下,您也需要创建一个.py文件,通过streamlit run file.py
在终端中运行。在我们的例子中,该文件将被命名为movieharbor.py
。
现在让我们总结一下构建前端应用程序的关键步骤:
-
配置应用程序网页:
import streamlit as st st.set_page_config(page_title="MovieHarbor", page_icon="") st.header(' Welcome to MovieHarbor, your favourite movie recommender')
-
导入凭证并建立与LanceDB的连接:
load_dotenv() openai_api_key = os.environ['OPENAI_API_KEY'] embeddings = OpenAIEmbeddings() uri = "data/sample-lancedb" db = lancedb.connect(uri) table = db.open_table('movies') docsearch = LanceDB(connection=table, embedding=embeddings) # 导入电影数据集 md = pd.read_pickle('movies.pkl')
-
创建小部件供用户定义他们的特征和电影偏好:
# 为用户输入创建侧边栏 st.sidebar.title("Movie Recommendation System") st.sidebar.markdown("Please enter your details and preferences below:") # 询问用户年龄、性别和最喜欢的电影类型 age = st.sidebar.slider("What is your age?", 1, 100, 25) gender = st.sidebar.radio("What is your gender?", ("Male", "Female", "Other")) genre = st.sidebar.selectbox("What is your favourite movie genre?", md.explode('genres')["genres"].unique()) # 根据用户输入过滤电影 df_filtered = md[md['genres'].apply(lambda x: genre in x)]
-
定义参数化的提示块:
template_prefix = """You are a movie recommender system that helps users to find movies that match their preferences. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. {context}""" user_info = """This is what we know about the user, and you can use this information to better tune your research: Age: {age} Gender: {gender}""" template_suffix = """Question: {question} Your response:""" user_info = user_info.format(age=age, gender=gender) COMBINED_PROMPT = template_prefix +'\n'+ user_info +'\n'+ template_suffix print(COMBINED_PROMPT)
-
设置RetrievalQA链:
# 设置链 qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=docsearch.as_retriever(search_kwargs={'data': df_filtered}), return_source_documents=True)
-
为用户插入搜索栏:
query = st.text_input('Enter your question:', placeholder='What action movies do you suggest?') if query: result = qa({"query": query}) st.write(result['result'])
就这样!您可以通过在终端中运行streamlit run movieharbor.py
来查看最终结果。运行结果如下所示:
通过这些步骤,您现在已经创建了一个完整的、基于内容的电影推荐系统的前端应用程序,用户可以通过简单的界面输入他们的偏好,并获取个性化的电影推荐。
所以,你可以看到,仅仅通过几行代码,我们就能够为MovieHarbor搭建一个网页应用程序。基于这个模板,你可以使用Streamlit的组件来自定义布局,并将其调整为基于内容的场景。此外,你还可以根据自己的需求定制提示,使推荐系统按照你的偏好进行操作。
总结
在本章中,我们探讨了LLM如何改变我们处理推荐系统任务的方式。我们从分析构建推荐应用程序的当前策略和算法开始,区分了各种场景(协同过滤、基于内容、冷启动等)以及不同的技术(KNN、矩阵分解和神经网络)。
接着,我们进入了一个新的、正在兴起的研究领域,即如何将LLM的强大功能应用于这个领域,并探讨了最近几个月内进行的各种实验。
利用这些知识,我们构建了一个由LLM驱动的电影推荐应用程序,使用LangChain作为AI编排器,Streamlit作为前端,展示了LLM如何凭借其推理能力和泛化能力革新这一领域。这只是一个示例,说明了LLM不仅能开启新的前沿,还能增强现有的研究领域。
针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料
分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程
等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓
👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈