如何检测 LLM 中的幻觉
原文:
towardsdatascience.com/real-time-llm-hallucination-detection-9a68bb292698
教导聊天机器人说“我不知道”
·发表于数据科学前沿 ·10 分钟阅读·2023 年 12 月 31 日
–
伊夫琳·哈特维尔是谁?
伊夫琳·哈特维尔是一位美国作家、演讲者和生活教练…
伊夫琳·哈特维尔是一位加拿大芭蕾舞演员以及创始艺术总监…
伊夫琳·哈特维尔是一位美国演员,以其在…中的角色而闻名
不,伊夫琳·哈特维尔并不是一个拥有多个虚假身份的诈骗犯,她过着一种伪装的三重生活,拥有各种职业。实际上,她根本不存在,但模型并没有告诉我它不知道,而是开始编造事实。我们正面临 LLM 幻觉的问题。
长篇详细的输出可能看起来非常可信,即使是虚构的。这是否意味着我们不能信任聊天机器人,每次都需要手动核实输出的真实性?幸运的是,通过正确的保护措施,可能会有方法使聊天机器人不太容易编造信息。
text-davinci-003 在虚构人物上的提示完成。图片由作者提供。
对于上述输出,我设置了较高的温度 0.7。我允许 LLM 更改句子的结构,以避免每次生成相同的文本。输出之间的差异应仅为语义上的,而非事实上的。
这个简单的想法引入了一种基于样本的幻觉检测机制。如果 LLM 对相同的提示产生的输出相互矛盾,那么这些输出很可能是幻觉。如果它们相互包含,这意味着信息是事实的。[2]
对于这种类型的评估,我们只需要 LLM 的文本输出。这被称为黑箱评估。此外,由于我们不需要任何外部知识,这也被称为零资源。[5]
句子嵌入的余弦距离
让我们从一种非常基本的相似性度量方法开始。我们将计算嵌入句子对应对的成对余弦相似性。我们对其进行归一化,因为我们只需要关注向量的方向,而不是大小。下面的函数以原始生成的句子 output 和一个包含 3 个样本输出的列表 sampled_passages 作为输入。所有的完成都在文章开头的图片中找到。
为生成嵌入,我使用了轻量级模型 all-MiniLM-L6-v2。嵌入一个句子将其转换为向量表示。
output = "Evelyn Hartwell is a Canadian dancer, actor, and choreographer."
output_embeddings= model.encode(output)
array([ 6.09108340e-03, -8.73148292e-02, -5.30637987e-02, -4.41815751e-03,
1.45469820e-02, 4.20340300e-02, 1.99541822e-02, -7.29453489e-02,
…
-4.08893749e-02, -5.41420840e-02, 2.05906332e-02, 9.94611382e-02,
-2.24501686e-03, 2.29083393e-02, 7.80007839e-02, -9.53456461e-02],
dtype=float32)
我们为 LLM 的每个输出生成嵌入;然后,使用 pairwise_cos_sim 函数计算成对余弦相似性。我们将原始响应与每个新样本响应进行比较,然后计算平均值。
from sentence_transformers.util import pairwise_cos_sim
from sentence_transformers import SentenceTransformer
def get_cos_sim(output,sampled_passages):
model = SentenceTransformer('all-MiniLM-L6-v2')
sentence_embeddings = model.encode(output).reshape(1, -1)
sample1_embeddings = model.encode(sampled_passages[0]).reshape(1, -1)
sample2_embeddings = model.encode(sampled_passages[1]).reshape(1, -1)
sample3_embeddings = model.encode(sampled_passages[2]).reshape(1, -1)
cos_sim_with_sample1 = pairwise_cos_sim(
sentence_embeddings, sample1_embeddings
)
cos_sim_with_sample2 = pairwise_cos_sim(
sentence_embeddings, sample2_embeddings
)
cos_sim_with_sample3 = pairwise_cos_sim(
sentence_embeddings, sample3_embeddings
)
cos_sim_mean = (cos_sim_with_sample1 + cos_sim_with_sample2 + cos_sim_with_sample3) / 3
cos_sim_mean = cos_sim_mean.item()
return round(cos_sim_mean,2)
这是函数如何在二维笛卡尔空间中使用一对非常简单的向量的直观解释。A 和 B 是原始向量,而 Â 和 B̂ 是归一化后的向量。
成对余弦相似性计算。图片由作者提供。
从上面的图片中,我们可以看到向量之间的角度大约是 30⁰,所以它们彼此接近。余弦值大约是 0.87。余弦值越接近 1,向量之间就越接近。
cos_sim_score = get_cos_sim(output, [sample1,sample2,sample3])
我们的嵌入输出的 cos_sim_score 平均值为 0.52。
为了理解如何解读这个数字,让我们将其与一些有效输出的余弦相似性分数进行比较,这些输出涉及关于现有人物的信息。
作者提供的图片 — text-davinci-003 在尼古拉斯·凯奇上的提示完成
在这种情况下,成对余弦相似性分数是 0.93。这看起来很有前景,特别是作为一种非常快速的方法来评估输出之间的相似性。
余弦相似性计算的持续时间。图片由作者提供。
SelfCheckGPT- BERTScore
BERTScore 基于我们之前实现的成对余弦相似性思想。
[1]
用于计算上下文嵌入的默认分词器是 RobertaTokenizer。上下文嵌入不同于静态嵌入,因为它们考虑了词汇周围的上下文。例如,“bat” 这个词会根据上下文是指“飞行的哺乳动物”还是“棒球棒”而对应不同的标记值。
def get_bertscore(output, sampled_passages):
# spacy sentence tokenization
sentences = [sent.text.strip() for sent in nlp(output).sents]
selfcheck_bertscore = SelfCheckBERTScore(rescale_with_baseline=True)
sent_scores_bertscore = selfcheck_bertscore.predict(
sentences = sentences, # list of sentences
sampled_passages = sampled_passages, # list of sampled passages
)
df = pd.DataFrame({
'Sentence Number': range(1, len(sent_scores_bertscore) + 1),
'Hallucination Score': sent_scores_bertscore
})
return df
让我们深入了解一下自检 _bert 评分.predict函数。我们没有将完整的原始输出作为参数传递,而是将其拆分为单独的句子。
['Evelyn Hartwell is an American author, speaker, and life coach.',
'She is best known for her book, The Miracle of You: How to Live an Extraordinary Life, which was published in 2007.',
'She is a motivational speaker and has been featured on TV, radio, and in many magazines.',
'She has authored several books, including How to Make an Impact and The Power of Choice.']
这个步骤很重要,因为selfcheck_bertscore.predict*函数计算了每个原始响应中的句子与样本中的每个句子的 BERTScore。首先,它创建一个数组,其中行数等于原始输出中的句子数,列数等于样本的数量。
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
用于计算候选句子和参考句子之间 BERTScore 的模型是具有 17 层的 RoBERTa large。我们的原始输出有 4 个句子,我将其称为 r1、r2、r3 和 r4。第一个样本有两个句子:c1 和 c2。我们计算原始输出中每个句子与第一个样本中每个句子的 F1 BERTScore。然后,我们进行基于基线张量 b = **tensor([0.8315, 0.8315, 0.8312])**的基础重标定。基线b是通过使用来自 Common Crawl 单语数据集的 100 万对随机句子计算的。他们计算了每对句子的 BERTScore 并取其平均值。这代表了一个下界,因为随机对的语义重叠很少。[1]
F1 BERTScore。图片来源:作者。
我们保留原始响应中每个句子与每个抽样样本中最相似句子的 BERTScore。逻辑是,如果某条信息出现在从相同提示生成的多个样本中,那么该信息很可能是事实。如果某个声明仅出现在一个样本中,而在相同提示的其他样本中没有出现,则更可能是虚构的。
让我们为第一个样本添加数组中的最大相似度:
bertscore_array
array([[0.43343216, 0\. , 0\. ],
[0.12838356, 0\. , 0\. ],
[0.2571277 , 0\. , 0\. ],
[0.21805632, 0\. , 0\. ]])
现在我们对另外两个样本重复这个过程:
array([[0.43343216, 0.34562832, 0.65371764],
[0.12838356, 0.28202596, 0.2576825 ],
[0.2571277 , 0.48610589, 0.2253703 ],
[0.21805632, 0.34698656, 0.28309497]])
然后我们计算每一行的平均值,从而得到原始响应中每个句子与每个后续样本的相似度评分。
array([0.47759271, 0.22269734, 0.32286796, 0.28271262])
每个句子的幻觉评分通过从 1 中减去上述每个值来获得。
伊夫琳·哈特维尔的幻觉评分。图片来源:作者。
将结果与尼古拉斯·凯奇的答案进行比较。
尼古拉斯·凯奇的幻觉评分。图片来源:作者。
看起来很合理;有效输出的幻觉评分较低,而虚构输出的幻觉评分较高。不幸的是,计算 BERTScore 的过程非常耗时,这使得它不适合实时幻觉检测。
BERTScore 计算的持续时间。图片来源:作者。
SelfCheckGPT-NLI
自然语言推断(NLI)涉及确定一个假设是否从给定的前提中逻辑推导出来,或与之矛盾。关系被分类为蕴含、矛盾或中立。对于 SelfCheck-NLI,我们使用经过 MNLI 数据集微调的 DeBERTa-v3-large 模型来执行 NLI。
NLI 流程图 [5]
以下是一些前提—假设对及其标签的示例。
上下文—假设对的示例 [4]
def get_self_check_nli(output, sampled_passages):
# spacy sentence tokenization
sentences = [sent.text.strip() for sent in nlp(output).sents]
selfcheck_nli = SelfCheckNLI(device=mps_device) # set device to 'cuda' if GPU is available
sent_scores_nli = selfcheck_nli.predict(
sentences = sentences, # list of sentences
sampled_passages = sampled_passages, # list of sampled passages
)
df = pd.DataFrame({
'Sentence Number': range(1, len(sent_scores_nli) + 1),
'Probability of Contradiction': sent_scores_nli
})
return df
在 selfcheck_nli.predict 函数中,将原始响应中的每个句子与三个样本中的每一个配对。
logits = model(**inputs).logits # neutral is already removed
probs = torch.softmax(logits, dim=-1)
prob_ = probs[0][1].item() # prob(contradiction)
相对于第一个句子和每个样本的矛盾概率。图像由作者提供。
现在我们对每个四个句子重复这个过程。
Evelyn Hartwell 的 SelfCheck-NLI。图像由作者提供。
我们可以看到模型输出了一个极高的矛盾概率。现在我们与实际输出进行比较。
Nicolas Cage 的 SelfCheck-NLI。图像由作者提供。
模型表现得非常好!不幸的是,NLI 检查时间有点长。
NLI 计算的持续时间。图像由作者提供。
SelfCheckGPT-Prompt
更新的方法已经开始使用 LLMs 自身来评估生成的文本。我们将输出和三个样本一起发送到 gpt-3.5-turbo,而不是使用公式来计算评分。模型将决定原始输出与生成的其他三个样本的相符程度。 [3]
def llm_evaluate(sentences,sampled_passages):
prompt = f"""You will be provided with a text passage \
and your task is to rate the consistency of that text to \
that of the provided context. Your answer must be only \
a number between 0.0 and 1.0 rounded to the nearest two \
decimal places where 0.0 represents no consistency and \
1.0 represents perfect consistency and similarity. \n\n \
Text passage: {sentences}. \n\n \
Context: {sampled_passages[0]} \n\n \
{sampled_passages[1]} \n\n \
{sampled_passages[2]}."""
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": ""},
{"role": "user", "content": prompt}
]
)
return completion.choices[0].message.content
对于 Evelyn Hartwell,返回的自相似度评分是0。与此同时,与 Nicolas Cage 相关的输出评分是0.95。获取评分所需的时间也相当短。
LLM 自相似度评分计算的持续时间。图像由作者提供。
这似乎是我们案例的最佳解决方案,因为我们从源论文的比较分析中也预期到了这一点 [2]。SelfCheckGPTPrompt 显著优于所有其他方法,NLI 是表现第二好的方法。
幻觉检测评估结果 [6]
评估数据集是通过使用 WikiBio 数据集和 GPT-3 生成合成的维基百科文章创建的。为了避免模糊的概念,从最长的文章的前 20%中随机抽取了 238 个文章主题。GPT-3 被提示以维基百科风格为每个概念生成首段。
评估数据集创建 [5]
接下来,这些生成的段落被人工注释以评估其准确性。根据预定义的指南,每个句子被标记为主要不准确、次要不准确或准确。总共标注了 1908 个句子,其中约 40%为主要不准确,33%为次要不准确,27%为准确。
为了评估标注者的一致性,200 个句子有双重标注。如果标注者达成一致,则使用该标签;否则,选择最坏的标签。通过 Cohen’s kappa 测量的标注者间一致性在选择准确、轻微不准确和主要不准确之间为 0.595,而在将轻微/主要不准确合并为一个标签时为 0.748。
评估指标 AUC-PR 指的是精确度-召回曲线下的面积,这是用于评估分类模型的指标。
实时幻觉检测
作为最终应用,我们来构建一个 Streamlit 应用以实现实时幻觉检测。如前所述,最佳指标是 LLM 自相似性分数。我们将使用 0.5 的阈值来决定是显示生成的输出还是免责声明。
import streamlit as st
import utils
import pandas as pd
# Streamlit app layout
st.title('Anti-Hallucination Chatbot')
# Text input
user_input = st.text_input("Enter your text:")
if user_input:
prompt = user_input
output, sampled_passages = utils.get_output_and_samples(prompt)
# LLM score
self_similarity_score = utils.llm_evaluate(output,sampled_passages)
# Display the output
st.write("**LLM output:**")
if float(self_similarity_score) > 0.5:
st.write(output)
else:
st.write("I'm sorry, but I don't have the specific information required to answer your question accurately. ")
现在,我们可以可视化最终结果。
Streamlit 应用演示。图片由作者提供。
结论
结果非常令人鼓舞!聊天机器人中的幻觉检测一直是一个长期讨论的质量问题。
这里所述技术之所以令人兴奋,是因为采用了使用 LLM 评估其他 LLM 输出的新颖方法。具体做法是生成多个对同一提示的响应并比较其一致性。
仍然需要做更多的工作,但与其依赖人工评估或手工制定规则,不如让模型本身捕捉不一致性,这似乎是一个不错的方向。
. . .
如果你喜欢这篇文章,加入 文本生成 ——我们的新闻通讯每周发布两篇文章,提供有关生成 AI 和大语言模型的最新见解。
你可以在 GitHub上找到这个项目的完整代码。
你也可以在 LinkedIn上找到我。
. . .
参考文献:
-
[广覆盖挑战语料库用于
通过推断进行句子理解](https://aclanthology.org/N18-1101)
-
drive.google.com/file/d/13LUBPUm4y1nlKigZxXHn7Cl2lw5KuGbc/view
-
drive.google.com/file/d/1EzQ3MdmrF0gM-83UV2OQ6_QR1RuvhJ9h/view
现实世界的问题以及数据如何帮助我们解决这些问题
·
关注 发表在 数据科学前沿 ·发送至 通讯 ·阅读时间 3 分钟 ·2023 年 11 月 23 日
–
在不断关注新工具和前沿模型的喧嚣中,很容易忽视一个基本的真理:利用数据的真正价值在于其带来切实的积极变化。无论是在复杂的商业决策还是我们日常的生活中,数据驱动的解决方案只有在现实世界中产生实际影响时才算真正有效。
为了帮助你更有效地将点连接起来,并激励你尝试新的方法,我们收集了一些最近最受欢迎的文章,这些文章具有强烈的问题解决角度。它们涵盖了从战略到极具个人化的广泛应用场景,但都分享了对数据在我们生活中作用的务实和注重细节的观点。祝阅读愉快!
-
通过生存分析探索时间事件在生存分析的一个易于理解的介绍中,奥利维亚·塔努维贾贾涵盖了一些基本概念和技术,并展示了这种方法如何应用于从医疗领域到维护预测和客户分析等各种问题。
-
**决策树如何从数据中知道下一个最佳提问是什么?**一些机器学习从业者可能认为二分类任务很基础,但即使近年来出现了更复杂的技术,它们的有用性依然保持不变。为了帮助你入门,古尔金德·考尔最近分享了一个适合初学者的决策树入门指南;该指南详细解释了决策树如何在一个训练来预测给定鱼类是金枪鱼还是鲑鱼的模型中运作。
-
我的生活统计:我追踪了一年的习惯,这就是我学到的在他的 TDS 首秀中,保罗·布拉斯科·伊·罗卡展示了一个为期一年的项目,这个项目处于数据分析和所谓的量化自我之间的交汇点。保罗已经跟踪了他 332 天的日常活动,并展示了即使是看似微不足道的数据,也可以从中提取出有意义的见解。
-
客户终生价值建模方法:精华与陷阱对于行业数据科学家而言,计算客户终生价值是一个常见的目标——而且这个目标在你深入挖掘业务操作时会变得越来越复杂。Katherine Munro对 CLV 的全面实用指南提供了对此话题的急需清晰度,并概述了各种建模选项,包括它们各自的优势和局限性。
-
改进 Strava 训练日志如果你是马拉松跑者——即使你不是——你也不想错过barrysmyth的最新深度探讨,他带我们完成了下载、分析和可视化 Strava 训练日志的整个过程。这篇文章特别有帮助,因为它专注于从“这里有很多跑步数据!”到“这是如何利用数据跑得更好”。
无论你是否计划在这个周末切火鸡,我们希望你能找到一些空闲时间去探索我们作者最近探讨的其他几个迷人话题:
-
E-值是否是 p-hacking 的答案?Hennie de Harder继续剖析数据操控的棘手话题。
-
对于加速数据分析师求职过程的实用方法,可以参考Natassha Selvaraj的详细路线图。
-
检索增强生成后的下一步是什么?Gadi Singer分享了对解释性检索中心生成(RCG)模型的全面概述及其克服 RAG 缺陷的潜力。
-
如果你的团队考虑转向无服务器技术,Sheen Brisals对主要概念和注意事项的详细解析是绝对必读的。
-
了解如何在无法随机化处理时估计因果效应:Matteo Courthoud最新的解释文稿在将理论和实际元素结合方面做得非常出色。
感谢你支持我们作者的工作!如果你喜欢在 TDS 上阅读的文章,考虑 成为 Medium 会员 — 这将解锁我们整个档案(以及 Medium 上的所有其他帖子)。
直到下一个变量,
TDS 编辑
RecList 2.0:开源系统化测试 ML 模型
一个新的 RecList 以提供更多灵活性和更好的评估支持
·
关注 发布于 Towards Data Science ·7 分钟阅读·2023 年 8 月 8 日
–
介绍
评估是一个复杂的事项。管理撰写评估管道中涉及的不同组件往往很困难,你需要在某个地方拥有模型,需要加载它,然后获取测试,运行测试等等。
然后呢?嗯,你需要将结果保存到某个地方,也许还需要在线记录输出,以便你可以跟踪它们。
由于这总是一个艰难的过程,我们最近尝试提供一种更结构化的测试方法。在这篇博客文章中,我们介绍并展示了如何使用 RecList beta,我们的开源评估包;RecList 是一种通用的即插即用方法,用于扩展测试,具有易于扩展的自定义用例接口。RecList 是一个自由开放的开源项目,您可以在 GitHub 上自由获取。
RecList 允许你将代码的评估部分分离出来,并封装在一个类中,这个类会自动处理其他几个方面的事情(例如,存储和日志记录)。
RecList 提供了一种简单的方法来系统化测试,并在你训练自己的模型后保存所有需要的信息。
我们在几年前开始开发 RecList,并且 RecList 的 alpha 版本在一年前左右发布。从那时起,RecList 已经收获了超过 400 个 GitHub stars。
我们已经使用 RecList 并进行了压力测试,在 2022 年的 CIKM 上举办了 RecSys 挑战,目前正在准备 2023 年的 KDD 挑战。RecList 使我们能够系统化所有参与者的评估。我们的想法是,一旦每个人都提供相同的 RecList,比较不同的评估将变得简单。我们的经验总结出现在我们的 Nature Machine Intelligence 评论文章中。
RecList 最初是在一篇学术论文中介绍的,但我们也有一个在 Towards Data Science 出版的概述,你可以在这里阅读:
使用 RecList 对推荐系统进行行为测试
towardsdatascience.com
Chia, P. J., Tagliabue, J., Bianchi, F., He, C., & Ko, B. (2022 年 4 月)。超越 nDCG:使用 RecList 对推荐系统进行行为测试。见 Web Conference 2022 附录(第 99–104 页)。
虽然我们最初设计 RecList 是为了推荐系统的测试,但没有什么能阻止你将 RecList 用于测试其他机器学习模型。那么,为什么会有一篇新的博客文章呢?好吧,在开发了第一个版本后,我们意识到它需要一些更新。
我们学到了什么:重新思考 API
通常只有在你构建了某样东西之后,你才会意识到如何改进它。
对于那些使用了 RecList 1.0 的用户,我们对 RecList API 进行了重大更新。最初,我们对代码结构和输入/输出对有更严格的约束。
事实上,当我们实现 RecList 时,我们的目标是提供一个更通用的 API,用于评估推荐系统,并提供了几种开箱即用的功能。然而,为了做到这一点,我们不得不创建多个抽象接口,用户需要实现这些接口。
例如,原始的 RecList 1.0 要求用户将自己的模型和数据集包装到预定义的抽象类中(即 RecModel 和 RecDataset)。这使我们能够实现一组由这些抽象连接的通用行为。然而,我们很快意识到,这可能会经常使流程复杂化,并需要额外的工作,这可能让一些人不喜欢。
在 RecList 2.0 中,我们决定让这些约束成为可选的:我们使测试更加灵活。用户定义自己的评估用例,将其包装在一个便捷的装饰器中,并且他们已经实现了元数据存储和日志记录。用户可以将测试接口分享给其他人,他们可以运行相同的实验。
总结:我们意识到在构建需要其他人使用的软件时,灵活性是多么重要。
RecList 2.0 实践
现在,让我们探索一个如何使用 RecList 来编写和运行评估流程的简单用例。我们将使用非常简单的模型,这些模型随机输出数字,以减少机器学习项目中涉及的复杂性。
一个简单的用例
让我们创建一个非常简单的用例和一个非常简单的数据集。假设我们有一个整数目标序列,每个都有一个关联的类别。我们只是生成一些随机数据。
n = 10000
target = [randint(0, 1) for _ in range(n)]
metadata = {"categories": [choice(["red", "blue", "yellow"])
for _ in range(n)]}
我们的非常简单的数据集应该看起来像这样:
>>> target
[0, 1, 0, 1, 1, 0]
>>> metadata
{"categories" : ["red", "blue", "yellow", "blue", "yellow", "yellow"]}
一个简单的模型
现在假设我们有一个 DummyModel,它会随机输出整数。当然,正如我们所说,这不是一个“好”的模型,但它是一个很好的抽象,可以用来查看整个评估流程。
class DummyModel:
def __init__(self, n):
self.n = n
def predict(self):
from random import randint
return [randint(0, 1) for _ in range(self.n)]
simple_model = DummyModel(n)
# let's run some predictions
predictions = simple_model.predict()
现在,我们如何运行评估?
一个简单的 RecList
RecList 是一个 Python 类,继承了我们 RecList 抽象类的功能。RecList 实现了 RecTests,这是一些简单的抽象,允许你系统化评估。例如,这可能是一个可能的准确性测试。
@rec_test(test_type="Accuracy", display_type=CHART_TYPE.SCALAR)
def accuracy(self):
"""
Compute the accuracy
"""
from sklearn.metrics import accuracy_score
return accuracy_score(self.target, self.predictions)
我们正在采用 sklearn 准确性指标,并将其包装到另一个方法中。这与简单的准确性函数有何不同?好吧,装饰器允许我们引入一些额外的功能:例如,rectest 现在会自动将信息存储到本地文件夹中。此外,定义一种图表类型使我们能够为这些结果创建一些可视化。
如果我们想要更复杂的测试会怎样?例如,我们想要查看在不同类别中我们的准确性有多稳定(例如,计算红色物体的准确性是否高于黄色物体的准确性?)
@rec_test(test_type="SlicedAccuracy", display_type=CHART_TYPE.SCALAR)
def sliced_accuracy_deviation(self):
"""
Compute the accuracy by slice
"""
from reclist.metrics.standard_metrics import accuracy_per_slice
return accuracy_per_slice(
self.target, self.predictions, self.metadata["categories"])
现在让我们看看一个完整的 RecList 示例!
class BasicRecList(RecList):
def __init__(self, target, metadata, predictions, model_name, **kwargs):
super().__init__(model_name, **kwargs)
self.target = target
self.metadata = metadata
self.predictions = predictions
@rec_test(test_type="SlicedAccuracy", display_type=CHART_TYPE.SCALAR)
def sliced_accuracy_deviation(self):
"""
Compute the accuracy by slice
"""
from reclist.metrics.standard_metrics import accuracy_per_slice
return accuracy_per_slice(
self.target, self.predictions, self.metadata["categories"]
)
@rec_test(test_type="Accuracy", display_type=CHART_TYPE.SCALAR)
def accuracy(self):
"""
Compute the accuracy
"""
from sklearn.metrics import accuracy_score
return accuracy_score(self.target, self.predictions)
@rec_test(test_type="AccuracyByCountry", display_type=CHART_TYPE.BARS)
def accuracy_by_country(self):
"""
Compute the accuracy by country
"""
# TODO: note that is a static test,
# used to showcase the bin display
from random import randint
return {"US": randint(0, 100),
"CA": randint(0, 100),
"FR": randint(0, 100)}
几行代码即可将我们需要的内容集中在一个地方。我们可以重用这段代码用于新的模型,或添加测试并重新运行过去的模型。
只要你的指标返回了一些值,你就可以以任何你喜欢的方式实现它们。例如,这个 BasicRecList 在特定的上下文中评估特定的模型。但没有什么能阻止你生成更多模型特定的 reclists(例如,GPT-RecList)或数据集特定的 reclists(例如,IMDB-Reclist)。如果你想查看 RecList 上深度模型的示例,可以 查看这个 Colab。
运行并获取输出
让我们运行 RecList。我们需要目标数据、元数据和预测。我们还可以指定一个日志记录器和一个元数据存储。
rlist = BasicRecList(
target=target,
metadata=metadata,
predictions=predictions,
model_name="myRandomModel",
)
# run reclist
rlist(verbose=True)
这个过程的输出是什么?我们将在命令行中看到以下结果:对于每个测试,我们都有一个实际的得分。
指标也会自动绘制。例如,AccuracyByCountry 应该显示如下内容:
RecTest 生成的图示示例。
除此之外,RecList 还会保存一个 JSON 文件,其中包含我们刚刚运行的实验的所有信息:
{
"metadata": {
"model_name": "myRandomModel",
"reclist": "BasicRecList",
"tests": [
"sliced_accuracy",
"accuracy",
"accuracy_by_country"
]
},
"data": [
{
"name": "SlicedAccuracy",
"description": "Compute the accuracy by slice",
"result": 0.00107123176804103,
"display_type": "CHART_TYPE.SCALAR"
},
...
}
好的一点是,只需几行额外的代码,大部分日志记录工作就会自动处理!
使用在线日志记录器和元数据存储
默认情况下,RecList 运行器将使用以下日志记录器和元数据设置。
logger=LOGGER.LOCAL,
metadata_store= METADATA_STORE.LOCAL,
然而,没有什么阻止我们使用在线和云解决方案。例如,我们将 CometML 和 Neptune API 封装起来,以便你可以直接在评估管道中使用它们。我们还提供 S3 数据存储的支持。
例如,向 BasicRecList 添加几个参数将允许我们在 Neptune 上记录信息(我们对 Comet.ml 也提供类似支持)!
rlist = BasicRecList(
target=target,
model_name="myRandomModel",
predictions=predictions,
metadata=metadata,
logger=LOGGER.NEPTUNE,
metadata_store= METADATA_STORE.LOCAL,
NEPTUNE_KEY=os.environ["NEPTUNE_KEY"],
NEPTUNE_PROJECT_NAME=os.environ["NEPTUNE_PROJECT_NAME"],
)
# run reclist
rlist(verbose=True)
以类似的方式,添加以下内容:
bucket=os.environ["S3_BUCKET"]
将允许我们使用 S3 桶来存储元数据(当然,你还需要设置一些环境密钥)。
结论
就这些!我们创建 RecList 是为了使推荐系统的评估更加系统化和有序。我们希望这一大规模的 API 重构能帮助人们构建更可靠的评估管道!
致谢
在 2022 年 6 月至 12 月期间,我们的 beta 版开发得到了 Comet、Neptune、Gantry 的出色支持,并在 Unnati Patel 的帮助下完成。
推荐系统:基于矩阵分解的协同过滤
原文:
towardsdatascience.com/recommendation-system-with-matrix-factorization-ebc4736869e4
通过矩阵分解解释推荐
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 4 月 26 日
–
图片来源:freestocks 于 Unsplash
Netflix 是一个受欢迎的在线流媒体平台,为其订阅者提供了各种电影、纪录片和电视节目。为了提升用户体验,Netflix 开发了一个复杂的推荐系统,根据你的观看历史、评分和偏好来推荐电影。
推荐系统使用复杂的算法分析大量数据,以预测用户最可能喜欢的内容。Netflix 的推荐系统是其成功的关键因素之一,并为流媒体行业设立了标准,全球拥有超过 2 亿用户。以下是 Netflix 如何通过个性化实现 80%观看时间的来源 link。
那么,什么是推荐系统?
推荐系统是一种无监督学习,通过信息过滤向用户推荐产品或内容,基于他们的偏好、兴趣和行为。这些系统广泛应用于电子商务和在线流媒体设置,以及其他应用程序中,以帮助发现可能感兴趣的新产品和内容。
推荐系统经过训练,以了解用户 和 产品的偏好、过去的决策和特征,使用关于用户-产品互动的数据收集。
推荐系统有两种类型,如下:
基于内容的过滤
推荐是基于用户或物品属性作为算法的输入。然后,使用共享属性空间的内容来创建用户和物品档案。
例如,蜘蛛侠:英雄无归 和 蚁人和黄蜂女:量子狂潮 有类似的属性,因为这两部电影都属于动作/冒险类型。不仅如此,它们都是漫威的一部分。因此,如果爱丽丝观看了蜘蛛侠电影,基于内容的推荐系统可能会推荐具有类似属性的电影,比如动作/漫威电影。
协同过滤
基于多个具有类似历史互动的用户。这种方法的关键思想是利用协作概念来生成新的推荐。
例如,爱丽丝和鲍勃对特定电影类型有类似兴趣。协同过滤推荐系统可能会向爱丽丝推荐鲍勃之前看过但爱丽丝没看过的项目,因为他们的偏好非常相似。鲍勃也适用相同的情况。
更多关于协同过滤的信息
推荐系统模型类型范围广泛,如下图所示,但今天本文将重点讨论协同过滤 (CF) 与矩阵分解。
推荐系统类型 - 作者插图
基于矩阵分解的协同过滤
简言之,矩阵分解是一种数学过程,将复杂的矩阵转换为低维空间。在推荐系统中,最流行的矩阵分解技术包括奇异值分解 (SVD)、非负矩阵分解 (NMF) 和概率矩阵分解。
以下是矩阵分解概念如何预测用户-电影评分的示意图
作者插图
阶段 1: 矩阵分解会随机初始化数字,并设置因子数量 (K)。在此示例中,我们将 K 设为 5。
-
用户矩阵(绿色框)表示每个用户与特征之间的关联
-
项目矩阵(橙色框)表示每个项目与特征之间的关联
在这里,例如,我们创建了 5 个特征 (k=5) 来表示 m_1 电影的特性:喜剧 为 2.10,恐怖 为 0.88,动作 为 0.04,家长指南为 0.02,适合家庭观看为 0.04。用户矩阵的情况也是类似的。用户矩阵表示用户的特征,如喜欢的演员或导演、最喜欢的电影制作等。
阶段 2: 评分预测是通过 用户矩阵 和 项目矩阵 的点积计算的。
其中 R 为真实评分,P 为用户矩阵,Q 为项目矩阵,结果 R’ 为预测评分。
作者插图
在更好的数学符号表示中,预测评分 R’ 可以在以下方程中表示:
阶段 3: 使用平方误差来计算真实评分与预测评分之间的差异
一旦这些步骤到位,我们可以使用随机梯度下降来优化我们的参数。它将计算此值的导数。
在每次迭代中,优化器将通过使用点积计算每部电影和每个用户之间的匹配,然后将其与用户给出的实际评分进行比较。然后,它将计算此值的导数,并通过将其乘以学习率⍺来更新权重。随着我们多次重复这一过程,损失将得到改善,从而提供更好的推荐。
广泛用于推荐系统的矩阵分解模型之一是奇异值分解(SVD)。SVD 本身具有广泛的应用,包括图像压缩和信号处理中的噪声减少。此外,SVD 通常用于推荐系统,因为它擅长解决大型用户-项目矩阵中固有的稀疏性问题。
本文还将提供SVD 实现的概述,使用 Surprise 包。
所以让我们开始实现吧!!
实现内容
-
数据导入
-
数据预处理
-
实现#1:从头开始用 Python 实现矩阵分解
-
实现#2:使用 Surprise 包的矩阵分解
完整的矩阵分解实现笔记本可以在这里找到。
数据导入
由于我们正在开发一个类似于 Netflix 的推荐系统,但可能没有访问其大数据的权限,我们将使用来自MovieLens的优秀数据集进行实践[1]已获许可。此外,你可以阅读并查看他们的README文件以了解使用许可和其他细节。该数据集包含数百万部电影、用户和用户过去的互动评分。
提取 zip 文件后,将会提供 4 个 csv 文件,如下所示:
数据快照 - 作者提供的图片
数据预处理
顺便提一下,协同过滤有 用户冷启动 的问题。冷启动问题指的是系统或算法无法对没有先前信息的新用户、项目或实体做出准确的预测或推荐。这可能发生在对新用户或项目几乎没有历史数据时,使得系统难以了解他们的偏好或特征。
冷启动问题是推荐系统中的一个常见挑战,系统需要为与用户交互历史有限或没有交互历史的用户提供个性化推荐。
在这个阶段,我们将选择与至少 2000 部电影互动过的用户以及被 1000 用户评分的电影(这可以是减少数据规模和空数据的一种好方法。此外,我的 RAM 无法处理大规模表格)
我的 RAM 状况 - 来源:KC Green 的 2013 年网页漫画
实际上,你也可以使用 MovieLens 提供的 100k 评分的小子集。我只是想尽可能优化我的计算机资源,减少空数据。
数据预处理后的输出 - 作者图片
按照惯例,我们将数据分为两组:训练集和测试集——通过使用 train_test_split 方法。
尽管我们需要的信息存在,但它的呈现方式并不利于人类理解。不过,我创建了一个表格,以一种更容易理解的格式呈现相同的数据。
原始数据 - 作者图片
实现 #1:从头开始用 Python 实现矩阵分解
这里是实现矩阵分解与梯度下降的 Python 代码片段。matrix_factorization
函数返回两个矩阵:nP (用户矩阵) 和 nQ (项目矩阵)。
然后,将训练数据集拟合到模型中,这里我设置了 n_factor K = 5。接下来,可以通过 使用点积方法将 nP 与 nQ 的转置相乘 来计算预测,如下代码片段所示。
结果是,矩阵分解产生了最终的预测
训练集中的新预测评分 - 作者图片
测试集上的预测
以下代码片段利用给定的 nP (用户矩阵) 和 nQ (电影矩阵) 对测试集进行预测
测试集的评分和预测评分输出 - 作者图片
评估预测性能
尽管推荐系统有多种评估指标,如 Precision@K、Recall@K、MAP@K 等,但在这个练习中,我将使用一个基本的准确度指标,即 RMSE。我可能会在后续的文章中更详细地描述其他评估指标。
结果显示,测试集上的 RMSE 为0.829,即使在超参数调整之前也相当不错。显然,我们可以调整一些参数,如学习率、n_factor、epochs 步数,以获得更好的结果。
实现 #2: 使用 Surprise 包的矩阵分解
在这一部分,我们选择了名为surprise package的 Python 库。一个surprise package是用于构建和评估推荐系统的 Python 库。它提供了一个简单易用的接口,用于加载和处理数据集,以及实现和评估不同的推荐算法。
数据导入和模型训练
Top-N 推荐生成器
对于 UserId: 231832
,以下是前 10 个电影推荐列表:
m_912, m_260, m_1198, m_110, m_60069, m_1172, m_919, m_2324, m_1204, m_3095
前 10 推荐结果 - 图片由作者提供
总结
在现代娱乐如 Netflix 中使用矩阵分解有助于理解用户偏好。这些信息随后用于向最终用户推荐最相关的项目/产品/电影。
这是我创建的矩阵分解示例的总结,以备将来需要向我的孙子们解释时使用……
图片由作者提供
参考
[1] Maxwell Harper 和 Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. https://doi.org/10.1145/2827872
## Collaborative Filtering Deep Dive
使用 Kaggle Notebooks 探索和运行机器学习代码 | 无附加数据源
## Recommender Systems in Python 101
使用 Kaggle Notebooks 探索和运行机器学习代码 | 使用来自 CI&T 的文章分享和阅读数据……
现在的网络上信息量过大,这点可能无需多言。搜索引擎帮助我们获取…
使用 TensorFlow 推荐系统的隐式反馈推荐系统
推荐系统
当客户没有明确告诉你他们想要什么时
·发表于 Towards Data Science ·11 分钟阅读·2023 年 11 月 5 日
–
图片由Noom Peerapong提供,来源于Unsplash
提供推荐实际上并没有那么难。你只需要检查你的客户如何评分你的产品,例如使用 1 到 5 颗星,然后在其基础上训练一个回归模型,对吗?
一个典型的数据集,你会希望拥有的。图片由作者提供。
好吧,如果我们没有任何数值的用户或电影特征,我们可能需要处理嵌入,但我们已经在我之前的文章中见过如何做到这一点。
学习在 TensorFlow 中构建一个简单的矩阵分解推荐系统
towardsdatascience.com
在本文中我们也需要嵌入,因此我建议在继续之前阅读上面的文章。
隐式反馈
然而,有时候我们没有显式的用户反馈,例如星级、点赞或点踩,或类似的反馈。这在零售中很常见,我们知道哪个客户购买了哪个商品,但不知道他们是否真的喜欢它。我们从客户那里得到的唯一信息是关于他们对该产品兴趣的隐式信号。
如果他们购买了(观看、消费……)这个产品,他们就表现出兴趣。如果没有,他们可能不感兴趣,但也可能只是还不知道。我们无法确定。
这听起来像是我们可以将其视为分类问题。感兴趣 = 1,不感兴趣 = 0。然而,这里有一个小问题,我们不能确定 0(不感兴趣)是否真的是零。也可能是客户只是从未有机会购买,但实际上是想要的。
让我们回到电影,假设我们没有任何评分。我们只知道哪个用户观看了哪个电影。
比如说,Alice 观看了 Gaußzilla 和 The Markov Chainsaw Massacre。图片由作者提供。
从这里我们可以有至少两种方法继续。
-
只需将所有缺失值视为零,然后训练一个二分类器。
-
使用成对损失函数来确保用户与他们观看过的电影之间的相似性高于同一用户与他们没有观看过的电影之间的相似性。
将所有缺失值视为零
这是最简单的解决方案。从上面的不完整表格,你可以创建以下数据集:
注意: A = Alice, B = Bob, C = Charlie, G = Gauß, E = Euler, M = Markov
图片由作者提供。
你可以将观看列解释为用户是否对电影感兴趣的标签。从这个表格中,你可以推断出,例如,用户A喜欢电影G,但不喜欢电影E,这在数据上是一个大胆的声明。也许A还不知道E。或者更糟的是,它实际上在A的观看列表中,但没有时间观看。
这个方法在技术方面的问题是,模型学会对几乎任何(用户,电影)输入都返回 0,因为大多数观看值通常为零。想象一下你有一个包含 1,000,000 个用户和 100,000 部电影的数据集。平均每个用户观看多少部不同的电影?也许是 1000 部?那你1%的观看标签是 1。因此,你有一个严重不平衡的数据集,这本身并不是坏事。然而,由于我们人工创建这些零值,可能会导致性能较差。
一个计算问题是这个数据集变得非常庞大。1,000,000 个用户乘以 100,000 部电影意味着你有一个包含 100,000,000,000 行的数据集。并且通常,你的数据库中还有更多的电影和项目。在这种情况下,你不会将所有的零目标行都放入数据集中,而是进行子采样,也称为负采样。例如,如果你有 1,000,000,000 行目标为 1 的数据(= 发生的交易),你也可以子采样 1,000,000,000 个负样本(= 从未发生的交易)。这样你就有了一个可以训练的良好数据集。
这种方法有效,但通常不是最优的,因为你把问题变得比实际需要的更复杂。你不需要完美预测观看标签。你只想对每个用户对电影进行排序,即你想能够说“用户A更喜欢电影G而不是电影E”。第二种方法正好满足这个需求。
使用成对损失函数
在这种方法中,我们不会告诉模型用户是否喜欢某个特定的电影。我们更谨慎地表述:
如果用户 A 观看了电影 G,但没有观看电影 E,我们仅仅说 A 对 G 的兴趣大于对 E 的兴趣。
这使我们能够解决一个更简单的目标。现在,让我们开始一些公式,以便更好地理解这种直觉如何转化为算法。
我们将再次训练一个处理嵌入的模型。假设我们有用户A、电影G和电影E的嵌入。如果A观看了G,但没有观看E,我们仅希望得到
图片由作者提供。
其中的e是嵌入,·是点积。这意味着对于用户A来说,电影G在某种程度上是更好的,优于电影E。但这比说“A喜欢G但不喜欢E”要温和,因为后者是二元分类的情况。
训练这样的模型听起来比训练二元分类器复杂得多,但有几个库可以帮助我们。我将展示如何使用TensorFlow Recommenders,因为这是我知道的最灵活的库。另一个值得一提的是implicit,它易于使用但不够灵活。
使用 TensorFlow Recommenders 训练
我们现在将看到将之前描述的逻辑放入代码中是多么简单。为了开诚相见,我遵循了官方 TFRS 网站的指南。我只是试图让它更简洁。
准备和数据生成
首先,让我们做一个
pip install tensorflow tensorflow-recommenders tensorflow-datasets
然后我们可以通过以下方式加载一些数据
import tensorflow_datasets as tfds
import tensorflow_recommenders as tfrs
import tensorflow as tf
ratings = (
tfds.load("movielens/100k-ratings", split="train")
.map(lambda x: {
"movie_title": x["movie_title"],
"user_id": x["user_id"],
})
.shuffle(10000)
)
ratings_df = tfds.as_dataframe(ratings)
ratings
是一个 TensorFlow 数据集,处理起来总是有点繁琐。然而,对于大数据集的内存高效训练,你必须使用它。但对于我们的小示例,我尽量停留在友好的数据框世界中,因此将数据集转换为数据框ratings_df
。数据如下:
图片由作者提供。
模型定义
我们将构建一个有两个部分的模型:
-
一个用户模型
-
一个电影模型
这些模型应分别接收用户或电影,并将其转换为嵌入,即一组浮点数。
embedding_dimension = 32
user_model = tf.keras.Sequential([
tf.keras.layers.StringLookup(vocabulary=ratings_df["user_id"].unique()),
tf.keras.layers.Embedding(ratings_df["user_id"].nunique() + 1, embedding_dimension)
])
movie_model = tf.keras.Sequential([
tf.keras.layers.StringLookup(vocabulary=ratings_df["movie_title"].unique()),
tf.keras.layers.Embedding(ratings_df["movie_title"].nunique() + 1, embedding_dimension)
])
使用这两个组件,我们可以这样定义完整的模型:
class MovielensModel(tfrs.Model):
def __init__(self, user_model, movie_model, task):
super().__init__()
self.movie_model = movie_model
self.user_model = user_model
self.task = task
def compute_loss(self, features, training=False):
user_embeddings = self.user_model(features["user_id"])
positive_movie_embeddings = self.movie_model(features["movie_title"])
return self.task(user_embeddings, positive_movie_embeddings)
我们可以看到模型由user_model
和movie_model
组成。我们将这个task
属性设置为检索任务,这正是我们所需要的实现。还有另一种任务类型,叫做排名任务,当你有明确的反馈如评分时可以使用。本文不会进一步探讨这个。
你还可以看到一些损失值被计算出来。输入是一个名为features
的字典,它应该看起来像这样:
features = {
"user_id": ["A", "B", "C"],
"movie_title": ["G", "E", "M"],
}
它包含了一些用户 ID 以及一系列电影标题。在这个例子中,用户A观看了G,用户B观看了E,用户C观看了M。我们这里只有正例,即过去发生的电影会话。
用户和电影被转化为嵌入,然后计算一些损失。我稍后会详细说明,但请放心,它正在做我们希望它做的事。
模型的架构如同我在其他文章中描述的那样:
图片由作者提供。
训练模型
最后,我们可以使用一个很好的 TFRS 预测类,但为了使其正常工作,我们需要一个独特的电影列表作为 TensorFlow 数据集。
# a TensorFlow dataset
unique_movies = tf.data.Dataset.from_tensor_slices(ratings_df["movie_title"].unique())
使用这个数据集,我们可以定义之前提到的任务:
task = tfrs.tasks.Retrieval()
现在,我们可以训练模型了!
model = MovielensModel(user_model, movie_model, task)
model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))
model.fit(ratings.batch(10000).cache(), epochs=5)
你最终会看到类似这样的结果:
图片由作者提供。
很难给loss
赋予具体的意义,但越小越好。我会进一步详细说明,但让我们先用我们的模型预测一些电影!
预测时间
首先,你需要定义一个叫做索引的东西。然后你可以使用这个索引来获取预测结果。
index = tfrs.layers.factorized_top_k.BruteForce(model.user_model)
index.index_from_dataset(
tf.data.Dataset.zip((unique_movies.batch(100), unique_movies.batch(100).map(model.movie_model)))
)
注意: 你不再使用model
了。你只是用它来调整user_model
和movie_model
的参数,现在直接使用这两个子模型。此时你基本上可以抛弃 TFRS 模型model
。
现在,你可以将用户传递给index
。索引将会
-
将这个用户 ID 转换为嵌入 —— 这是因为你传递了它给
user_model
—— 然后 -
计算每部电影的嵌入 —— 这是通过执行
index_from_dataset
函数实现的 —— 然后 -
输出与用户嵌入最接近的电影标题。
注意: 它在后台进行的是精确的最近邻搜索,这可能会很慢。它还支持使用ScaNN进行近似最近邻搜索。你可以通过输入ScaNN
代替BruteForce
来使用它。
它的工作原理如下:
_, titles = index(tf.constant(["99"]))
print(f"Recommendations for user 99: {titles[0, :3]}")
# Output:
# Recommendations for user 99: [b'Sunset Park (1996)' b'Happy Gilmore (1996)' b'High School High (1996)']
很好!你现在已经准备好使用这个模型了。
收尾工作
还有一些我承诺要深入探讨的内容:这个task
和它输出的损失。内部发生的事情文档(尚未)非常好,但我查看了源代码以了解发生了什么。你可以在这里找到我提到的源代码。
我将通过一个小例子批次来解释。
进入模型的批量。图片由作者提供。
它进入模型,然后使用user_model
和movie_model
创建每个用户和电影的嵌入。这些嵌入进入检索任务对象。
在task
中,所有用户嵌入与所有电影嵌入进行相乘(点积)。这可以通过简单的矩阵乘法完成,其中电影矩阵首先转置。假设我们使用二维嵌入来节省空间。
图片由作者提供。
矩阵乘积是
图片由作者提供。
现在,推理过程如下:从数据中我们知道
-
爱丽丝观看了高斯,
-
鲍勃观看了欧拉,并且
-
查理观看了马尔可夫。
这就是为什么我们希望在单元格中的对应数字(A,G),(B,E),(C,M)——这就是主对角线——具有最高的数字。在这个小例子中,我们差得很远。
为了量化这一点,他们进行另一步:TFRS 的作者进行逐行 softmax。
经过逐行 softmax 处理。注意每行的和是 1。图片由作者提供。
现在,观察到的是:如果前一个矩阵中主对角线上的元素远高于其他数字,那么“softmax 处理过”的矩阵接近于单位矩阵。
最优的单位矩阵。图片由作者提供。
这是因为如果你对一个数组进行 softmax 处理,如果一个数字远高于其他数字,这个数字将接近 1。所以其他数字必须接近零。可以尝试一下:
x = np.array([1, 2, 10])
np.exp(x) / np.exp(x).sum() # softmax
# Output:
# array([1.23353201e-04, 3.35308764e-04, 9.99541338e-01])
所以,损失来自于将上面的矩阵与单位矩阵进行比较。具体来说,使用的是分类交叉熵损失。但是不是平均值,而是总和,见此处。这就是为什么损失值总是这么高的原因。批量越大,损失会越大。所以不要对损失突然变得非常低感到困惑,仅仅因为你把批量大小从 10,000 改成 1,000 或类似的数值。
结论
在这篇文章中,我们学习了如何利用隐式反馈数据构建推荐系统。为此,我们使用了 TensorFlow Recommenders,因为它扩展性强且非常表达清晰:你可以使用任何子模型——只要它输出一个嵌入——然后将它们组合在一起,使用 tfrs.Model
类进行联合训练。
经过训练后,你可以使用一个方便的类来进行实际的预测。如果你使用 ScaNN,这应该会非常快,但如果你需要更强大的搜索功能,你可以使用像 Qdrant 这样的专用向量数据库。你将训练模型中的用户和电影嵌入提供给它,它会为你进行搜索。
我们还查看了库的内部结构,以了解要最小化的损失来自哪里,因此这个库不再是纯粹的魔法。
如果你想了解如何评估隐式反馈推荐系统的质量,请参考我的另一篇文章:
离线评估推荐系统可能会很棘手
towardsdatascience.com
我希望你今天学到了一些新的、有趣的和有价值的东西。感谢你的阅读!
如果你有任何问题,可以在 LinkedIn上写信给我!
如果你想更深入地了解算法的世界,可以尝试我的新出版物**《全面了解算法》**!我还在寻找作者!
从直观的解释到深入的分析,算法通过示例、代码和精彩的内容变得生动起来…
medium.com](https://medium.com/all-about-algorithms?source=post_page-----8ba36a976c57--------------------------------)
重新创建 Andrej Karpathy 的周末项目 — 电影搜索引擎
使用 OpenAI 嵌入和向量数据库构建电影推荐系统
·
关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 11 月 7 日
–
最终电影推荐演示 的风格化截图(图片由作者提供)
在 2023 年 4 月,Andrej Karpathy,OpenAI 的创始成员之一及前特斯拉人工智能总监,分享了这个有趣的周末项目,一个 电影搜索和推荐引擎:
用户界面非常简单,主要有两个关键功能。首先,你有一个搜索框,可以通过电影标题进行搜索。然后点击任何电影,你会得到一份该电影的 40 部最相似电影的推荐列表。
演示网站:awesome-movies.life/
尽管很受欢迎,Karpathy 不幸地没有公开分享该项目的源代码。
原推文下的 评论截图(截图由作者提供)
所以,拿上一些爆米花,让我们自己动手重建吧!
前提条件
该项目基于四个主要组件:
-
OpenAI 嵌入模型生成嵌入
-
使用 Weaviate 向量数据库 来存储嵌入,数据通过 Python 脚本填充
-
前端:HTML、CSS、Js
-
后端:NodeJs
因此,要跟随本教程,你需要以下内容:
-
Python 用于数据处理和填充向量数据库
-
Docker 和 Docker-Compose 用于在本地运行向量数据库。
-
Node.js 和 npm 用于在本地运行应用程序。
-
OpenAI API 密钥 用于访问 OpenAI 嵌入模型
实现一个电影搜索引擎
本节分析 Karpathy 的周末黑客活动,并旨在以一些小变化重建它。要构建一个简单的电影搜索引擎,请按照以下步骤操作:
-
准备工作:电影数据集
-
步骤 1:生成和存储嵌入
-
步骤 2:搜索电影
-
步骤 3:获取类似的电影推荐
-
步骤 4:运行演示
完整代码是开源的,你可以在 GitHub 上找到它。
准备工作:电影数据集
Karpathy 的项目索引了自 1970 年以来的 11,762 部电影,包括来自 Wikipedia 的情节和摘要。
若想在不手动抓取 Wikipedia 的情况下实现类似功能,你可以使用以下两个来自 Kaggle 的数据集:
-
48,000+ 电影数据集(许可:CC0: 公共领域)包括
'id'
、'name'
、'PosterLink'
、'Genres'
、'Actors'
、'Director'
、'Description'
、'Keywords'
和'DatePublished'
列。 -
Wikipedia 电影情节(许可:CC BY-SA 4.0),用于
'plot'
列。
两个数据集在电影标题和发行年份上合并,然后筛选出 1970 年后发行的电影。你可以在[add_data.py](https://github.com/weaviate-tutorials/awesome-moviate/blob/main/add_data.py)
文件中找到详细的预处理步骤。结果数据框包含大约 35,000 部电影,其中约 8,500 部电影有情节描述,数据框如下所示:
预处理后的电影数据框(截图由作者提供)
步骤 1:生成并存储嵌入
这个演示项目的核心是电影数据对象的嵌入,这些嵌入主要用于通过情节相似度推荐电影。在 Karpathy 的项目中,为电影摘要和情节生成了向量嵌入。生成向量嵌入有两个选项:
-
术语频率-逆文档频率(TF-IDF),这是一种简单的二元组,应该用于单个词的使用。
-
[text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
OpenAI 的嵌入模型,应该用于语义相似度。
此外,相似度是基于每部电影的维基百科摘要和情节计算的,具有两种相似度排序选择:
-
k-最近邻(kNN)使用余弦相似度
-
支持向量机
Karpathy 建议将[text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
和 kNN 结合使用,以获得一个好的快速默认设置。
最后但同样重要的是,如这条臭名昭著的回应所述,向量嵌入存储在np.array
中:
原推文下的评论的截图(截图由作者提供)
在这个项目中,我们还将使用来自 OpenAI 的[text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
嵌入模型,但将向量嵌入存储在向量数据库中。
也就是说,我们将使用Weaviate*,一个开源的向量数据库。虽然我可以争论说,向量数据库比将嵌入存储在np.array
中要快得多,因为它们使用向量索引,但坦率地说:在这个规模(数千个)下,你不会注意到速度上的任何差异。我使用向量数据库的主要原因是 Weaviate 有许多方便的内置功能可以立即使用,比如使用嵌入模型的自动向量化。
首先,如[add_data.py](https://github.com/weaviate-tutorials/awesome-moviate/blob/main/add_data.py)
文件所示,你需要设置你的 Weaviate 客户端,它连接到本地的 Weaviate 数据库实例,如下所示。此外,你将定义你的 OpenAI API 密钥,以启用集成 OpenAI 模块的使用。
# pip weaviate-client
import weaviate
import os
openai_key = os.environ.get("OPENAI_API_KEY", "")
# Setting up client
client = weaviate.Client(
url = "http://localhost:8080",
additional_headers={
"X-OpenAI-Api-Key": openai_key,
})
接下来,你将定义一个名为Movies
的数据集合来存储电影数据对象,这类似于在关系数据库中创建一个表。在这一步中,你定义text2vec-openai
模块作为向量化器,它允许在导入和查询时自动进行数据向量化,并在模块设置中定义使用[text-embedding-ada-002](https://platform.openai.com/docs/guides/embeddings/what-are-embeddings)
嵌入模型。此外,你可以定义余弦距离作为相似性度量。
movie_class_schema = {
"class": "Movies",
"description": "A collection of movies since 1970.",
"vectorizer": "text2vec-openai",
"moduleConfig": {
"text2vec-openai": {
"vectorizeClassName": False,
"model": "ada",
"modelVersion": "002",
"type": "text"
},
},
"vectorIndexConfig": {"distance" : "cosine"},
}
接下来,你定义电影数据对象的属性以及为哪些属性生成向量嵌入。在下面的简化代码片段中,你可以看到,对于属性movie_id
和title
,没有生成向量嵌入,因为向量化模块的设置为"skip" : True
。这是因为,我们只希望为description
和plot
生成向量嵌入。
movie_class_schema["properties"] = [
{
"name": "movie_id",
"dataType": ["number"],
"description": "The id of the movie",
"moduleConfig": {
"text2vec-openai": {
"skip" : True,
"vectorizePropertyName" : False
}
}
},
{
"name": "title",
"dataType": ["text"],
"description": "The name of the movie",
"moduleConfig": {
"text2vec-openai": {
"skip" : True,
"vectorizePropertyName" : False
}
}
},
# shortened for brevity ...
{
"name": "description",
"dataType": ["text"],
"description": "overview of the movie",
},
{
"name": "Plot",
"dataType": ["text"],
"description": "Plot of the movie from Wikipedia",
},
]
# Create class
client.schema.create_class(movie_class_schema)
最后,你定义一个批处理过程来填充向量数据库:
# Configure batch process - for faster imports
client.batch.configure(batch_size=10)
# Importing the data
for i in range(len(df)):
item = df.iloc[i]
movie_object = {
'movie_id':float(item['id']),
'title': str(item['Name']).lower(),
# shortened for brevity ...
'description':str(item['Description']),
'plot': str(item['Plot']),
}
client.batch.add_data_object(movie_object, "Movies")
第二步:搜索电影
在 Karpathy 的项目中,搜索栏是一个简单的基于关键字的搜索,尝试逐字匹配你的确切查询与电影标题。当一些人表示他们希望搜索能够进行语义搜索时,Karpathy 同意这可能是项目的一个很好的扩展:
原始推文下的评论截图(作者截图)
在这个项目中,你将在[queries.js](https://github.com/weaviate-tutorials/awesome-moviate/blob/main/queries.js)
文件中启用三种类型的搜索:
每个搜索都会返回num_movies = 20
部电影,属性为['title', 'poster_link', 'genres', 'year', 'director', 'movie_id']
。
要启用基于关键字的搜索,你可以使用.withBm25()
搜索查询,涉及的属性为['title', 'director', 'genres', 'actors', 'keywords', 'description', 'plot']
。你可以通过指定'title³'
来给属性'title'
更大的权重。
async function get_keyword_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withBm25({query: text,
properties: ['title³', 'director', 'genres', 'actors', 'keywords', 'description', 'plot'],
})
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
})
return data;
}
要启用语义搜索,你可以使用.withNearText()
搜索查询。这将自动将搜索查询向量化,并检索其在向量空间中最接近的电影。
async function get_semantic_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withNearText({concepts: [text]})
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
});
return data;
}
要启用混合搜索,你可以使用.withHybrid()
搜索查询。alpha : 0.5
表示关键字搜索和语义搜索的权重相等。
async function get_hybrid_results(text) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'poster_link', 'genres', 'year', 'director', 'movie_id'])
.withHybrid({query: text, alpha: 0.5})
.withLimit(num_movies)
.do()
.then(info => {
return info
})
.catch(err => {
console.error(err)
});
return data;
}
第三步:获取类似电影推荐
要获取类似的电影推荐,你可以执行 .withNearObject()
搜索查询,如 [queries.js](https://github.com/weaviate-tutorials/awesome-moviate/blob/main/queries.js)
文件中所示。通过传递电影的 id
,查询返回向量空间中与给定电影最接近的 num_movies = 20
部分电影。
async function get_recommended_movies(mov_id) {
let data = await client.graphql
.get()
.withClassName('Movies')
.withFields(['title', 'genres', 'year', 'poster_link', 'movie_id'])
.withNearObject({id: mov_id})
.withLimit(20)
.do()
.then(info => {
return info;
})
.catch(err => {
console.error(err)
});
return data;
}
第 4 步:运行演示
最后,将一切包装成一个具有 2000 年代标志性 GeoCities 美学的网页应用(我不打算让你厌倦前端的内容),然后就完成了!你已准备好!
要在本地运行演示,克隆 GitHub 仓库。
git clone git@github.com:weaviate-tutorials/awesome-moviate.git
导航到演示的目录并设置一个虚拟环境。
python -m venv .venv
source .venv/bin/activate
确保 在你的虚拟环境中设置环境变量 $OPENAI_API_KEY。 此外,在目录中运行以下命令,以安装虚拟环境中所需的所有依赖项。
pip install -r requirements.txt
接下来,在 docker-compose.yml
文件中设置你的 OPENAI_API_KEY,并运行以下命令通过 Docker 本地运行 Weaviate。
docker compose up -d
一旦你的 Weaviate 实例启动并运行,运行 add_data.py
文件以填充你的向量数据库。
python add_data.py
在你运行应用程序之前,安装所有必需的节点模块。
npm install
最后,运行以下命令在本地启动你的电影搜索引擎应用程序。
npm run start
现在,导航到 localhost:3000/
并开始尝试你的应用程序。
摘要
本文重新创建了 Andrej Karpathy 的有趣周末项目,即电影搜索引擎/推荐系统。下面,你可以看到完成的 实时演示 的简短视频:
演示网址 awesome-moviate.weaviate.io/
与原始项目相比,该项目使用向量数据库来存储嵌入。此外,搜索功能扩展到了语义和混合搜索。
如果你进行尝试,你会注意到它并不完美,但正如 Karpathy 所说:
“它运行得~还不错,哈哈,需要再调试一下。”
你可以在 GitHub 上找到项目的开源代码,并根据需要进行修改。一些进一步改进的建议包括尝试向量化不同的属性、调整关键词搜索与语义搜索之间的权重,或用开源替代品替换嵌入模型。
喜欢这个故事吗?
免费订阅 以便在我发布新故事时收到通知。
[## 每当 Leonie Monigatti 发布时获取电子邮件。
每当 Leonie Monigatti 发布新内容时,会收到一封电子邮件。通过注册,如果你还没有 Medium 账户,你将创建一个…
在 LinkedIn、Twitter 和 Kaggle 上找到我!
免责声明
-
这个项目并非原创想法:该项目的灵感来源于Andrej Karpathy 的周末项目,实现基于一个旧的电影搜索引擎演示项目。
从基础到高层次讲解和可视化的递归神经网络
机器翻译的应用
·发表于 Towards Data Science ·阅读时间 23 分钟·2023 年 6 月 22 日
–
递归神经网络(RNNs)是可以顺序操作的神经网络。虽然它们不像几年前那样流行,但它们在深度学习的发展中代表了一个重要的进步,并且是前馈网络的自然扩展。
在这篇文章中,我们将涵盖以下内容:
-
从前馈网络到递归网络的过渡
-
多层递归网络
-
长短期记忆网络(LSTM)
-
顺序输出(‘文本输出’)
-
双向性
-
自回归生成
-
机器翻译的应用(对 Google Translate 2016 模型架构的高层次理解)
本文的目的是不仅解释 RNN 的工作原理(已有许多文章对此进行了说明),还通过插图探索其设计选择和高层次直观逻辑。希望这篇文章不仅能为你对这一技术话题的理解提供独特的价值,还能更广泛地提升对深度学习设计灵活性的理解。
递归神经网络(1985)的设计基于两个观察,即理想模型(如人类阅读文本)如何处理顺序信息:
-
它应该跟踪“已学习”的信息,以便能够将新信息与之前看到的信息相关联。为了理解句子“the quick brown fox jumped over the lazy dog”,我需要跟踪词‘quick’和‘brown’,以便后续理解这些词适用于‘fox’。如果我没有在‘短期记忆’中保留这些信息,我将无法理解信息的顺序意义。当我在‘lazy dog’处完成句子时,我会将这个名词与之前遇到的‘quick brown fox’联系起来阅读。
-
尽管后来的信息总是会在早期信息的背景下被阅读,我们希望以类似的方式处理每一个词(标记),无论它的位置如何。我们不应该出于某种原因将第三个位置的词系统地转换成与第一个位置的词不同,即使我们可能会根据后者来阅读前者。请注意,之前提出的方法——将所有标记的嵌入并排堆叠并同时呈现给模型——不具备这一特性,因为没有保证第一个词的嵌入与第三个词的嵌入遵循相同的规则。这种通用特性也称为位置不变性。
循环神经网络的核心由循环层组成。一个循环层,像前馈层一样,是一组可学习的数学变换。事实证明,我们可以通过多层感知机来大致理解循环层。
循环层的‘短期记忆’被称为它的隐藏状态。这是一个向量——只是一组数字——它传达了网络迄今为止学到的重要信息。然后,对于标准化文本中的每个标记,我们将新信息融入隐藏状态。我们使用两个 MLP 来完成这项工作:一个 MLP 转换当前的嵌入,另一个转换当前的隐藏状态。这两个 MLP 的输出相加,形成更新后的隐藏状态,或称为‘更新后的短期记忆’。
然后我们对下一个标记重复这一过程——嵌入被传递到一个 MLP 中,而更新后的隐藏状态被传递到另一个 MLP 中;两个 MLP 的输出相加。这对序列中的每个标记重复:一个 MLP 将输入转换为可以融入短期记忆(隐藏状态)的形式,而另一个准备更新短期记忆(隐藏状态)。这满足了我们的第一个要求——我们希望在旧信息的背景下读取新信息。此外,这两个 MLP在每个时间步都是相同的。也就是说,我们使用相同的规则来合并当前隐藏状态与新信息。这满足了我们的第二个要求——我们必须对每个时间步使用相同的规则。
这两种 MLP 通常仅实现为单层深度:也就是说,它们只是一个大的逻辑回归堆栈。例如,下面的图示展示了 MLP A 的架构是如何工作的,假设每个嵌入长度为八个数字,隐藏状态也由八个数字组成。这是一种简单但有效的转换,将嵌入向量映射到适合与隐藏状态合并的向量。
当我们完成将最后一个标记融入隐藏状态后,循环层的任务也就完成了。它生成了一个向量——一个数字列表——代表了通过按顺序读取标记所累积的信息。然后,我们可以将这个向量传递通过第三个 MLP,MLP 学习“当前记忆状态”与预测任务之间的关系(在这个案例中,是股票价格是上涨还是下跌)。
更新权重的机制过于复杂,无法在本书中详细讨论,但其逻辑类似于反向传播算法。额外的复杂性在于追踪每个参数对其自身输出重复作用的累积效应(因此模型具有“递归”特性),这可以通过一种称为“时间反向传播”的修改算法在数学上解决。
递归神经网络是一种相当直观的方法来处理序列数据建模。它是线性回归模型复杂排列的另一种情况,但它非常强大:它允许我们系统地处理如语言这样的困难序列学习问题。
为了方便图示和简洁性,你通常会看到递归层被简单地表示为一个块,而不是作为一个扩展的单元,顺序处理一系列输入。
这是用于文本的最简单的递归神经网络变种:标准化的输入标记被映射到嵌入中,然后输入到循环层;循环层的输出(“最新的记忆状态”)由 MLP 处理,并映射到预测目标。
循环网络的复杂变种
循环层使得网络能够处理序列问题。然而,我们当前的递归神经网络模型存在一些问题。为了理解递归神经网络如何在实际应用中用于建模复杂问题,我们需要添加一些附加功能。
其中一个问题是深度不足:一个递归层只是简单地遍历一次文本,因此仅获得了内容的表面级、粗略的阅读。考虑哲学家伊曼纽尔·康德的句子“幸福不是理性的理想,而是想象的理想”。要真正理解这句话的深度,我们不能仅仅遍历一次文字。相反,我们需要读过这些文字,然后——这是关键的一步——我们阅读我们的思考。我们评估对句子的即时解释是否合理,并可能对其进行修改以使其更有深度。我们甚至可能阅读我们对我们思考的思考。这一切发生得非常快,通常在我们没有意识的情况下,但这是一个使我们能够从文本内容中提取多个层次深度的过程。
相应地,我们可以添加多个递归层来增加理解的深度。当第一个递归层获取文本的表面级信息时,第二个递归层则读取第一个递归层的“思考”。然后,将第二层的双重信息“最近记忆状态”作为输入传递给最终决策的 MLP。或者,我们可以添加多于两个递归层。
为了具体说明这种堆叠机制如何工作,请参阅下图:我们不仅将每个隐藏状态传递给下一个递归层进行更新,还将该输入状态提供给下一个递归层。虽然第一个递归层的第一个输入是嵌入,第一个递归层的第二个输入是“第一个递归层对第一个输入的思考”。
几乎所有用于现实世界语言建模问题的递归神经网络都使用递归层的堆栈而不是单个递归层,因为这样可以增加理解深度和语言推理。对于大堆栈的递归层,我们通常使用递归残差连接。回忆一下残差连接的概念,其中早期版本的信息被添加到后来的信息版本中。类似地,我们可以在每层的隐藏状态之间放置残差连接,使得各层可以参考不同的“思维深度”。
虽然递归模型在短小简单的句子如“美联储宣布经济衰退”中表现良好,但金融文档和新闻文章通常远比几个词要长。对于较长的序列,标准递归模型会遇到持续的长期记忆丧失问题:序列中较早的单词的信号或重要性常常被后面的单词稀释和掩盖。由于每个时间步都会对隐藏状态产生影响,它会部分破坏早期信息。因此,在序列结束时,大部分起始处的信息变得不可恢复。递归模型的关注/记忆窗口很窄。如果我们希望创建一个能够以类似人类的理解和深度来查看和分析文档的模型,我们需要解决这个记忆问题。
长短期记忆(LSTM)(1997)层是一个更复杂的递归层。其具体机制过于复杂,无法在本书中准确或完全讨论,但我们可以大致理解为试图将“长期记忆”与“短期记忆”分开。两个组件在“读取”序列时都很重要:我们需要长期记忆来追踪跨越时间的大量信息,同时也需要短期记忆来专注于特定的、本地化的信息。因此,LSTM 层不仅存储单一的隐藏状态,还使用“细胞状态”(代表“长期记忆”)。
每一步中,输入与隐藏状态以与标准递归层相同的方式结合。然而,之后会有三个步骤:
-
长期记忆清除。长期记忆是宝贵的;它保存了我们会持久保留的信息。当前的短期记忆状态用于确定长期记忆中哪些部分不再需要,并“剪切”这些部分以腾出空间给新的记忆。
-
长期记忆更新。现在长期记忆中的空间已被清除,短期记忆被用来更新(添加到)长期记忆,从而将新信息记录到长期记忆中。
-
短期记忆通知。此时,长期记忆状态已经根据当前时间步完全更新。因为我们希望长期记忆能够指导短期记忆的功能,长期记忆帮助剪切和修改短期记忆。理想情况下,长期记忆对什么是重要的、什么是不重要的有更大的监督。
因此,短期记忆和长期记忆——请记住,这两者都是数字列表——在每个时间步与彼此及输入进行交互,以一种允许仔细阅读而不会发生灾难性遗忘的方式读取输入序列。这个三步过程在下图中以图形方式描述。+
表示信息添加,而 x
表示信息移除或清理。(加法和乘法是实现这些思想的数学运算。例如,假设当前隐藏状态的值为 10。如果我将其乘以 0.1,它变成 1——因此,我已经‘减少’了隐藏状态中的信息。)
使用带有残差连接的 LSTM 堆叠,我们可以构建强大的语言理解模型,这些模型能够阅读(如果你愿意,也可以说是‘理解’)段落甚至整篇文章。除了用于金融分析以浏览大量金融和新闻报告外,这些模型还可以用于预测社交媒体帖子和消息中的潜在自杀或恐怖分子,推荐客户可能会购买的新产品(根据他们以前的产品评论),以及检测在线平台上的有毒或骚扰性评论和帖子。
这样的应用迫使我们批判性地思考其材料的哲学意义。政府对检测潜在恐怖分子有强烈的兴趣,而最近大屠杀的枪手往往有令人担忧的公共社交媒体记录——但悲剧在于,他们在海量的互联网信息中未被发现。语言模型,如你所见,纯粹是数学性的:它们试图找到最能建模输入文本和输出文本之间关系的权重和偏置。但在这些权重和偏置有意义的程度上,它们可以以有效且极其迅速的方式‘阅读’信息——比人类读者更快,甚至可能更有效。这些模型可能使政府能够在潜在恐怖分子行动之前检测、追踪和阻止他们。当然,这可能会以隐私为代价。此外,我们已经看到,尽管语言模型能够机械地追踪数据中的模式和关系,但它们实际上只是能够犯错的数学算法。如何调和模型对个体的错误标记为潜在恐怖分子的情况呢?
社交媒体平台在用户和政府的压力下,想要减少在线论坛中的骚扰和有害言论。从概念上看,这可能看似一个简单的任务:将社交媒体评论的语料库标记为有害或无害,然后训练语言模型预测特定文本样本的有害程度。然而,直接的问题是,数字话语因为依赖于迅速变化的参考(如表情包)、内部笑话、隐晦的讽刺以及必需的背景知识而极具挑战性。然而,更有趣的哲学问题是,是否可以且是否应该真正训练一个数学模型(一个‘客观’模型)来预测一个看似‘主观’的目标,如有害言论。毕竟,对一个人来说有害的东西,对另一个人可能并无害。
随着我们进入处理越来越个人化数据的模型——语言是我们沟通和吸收几乎所有知识的媒介——我们发现思考和致力于回答这些问题的重要性不断增加。如果你对这一研究方向感兴趣,你可能需要了解对齐、陪审学习、宪法 AI、RLHF 和价值多元化。
神经机器翻译
概念:多输出递归模型,双向性,注意力机制
机器翻译是一项令人惊叹的技术:它使得那些以前无法进行有效沟通的个体能够自由对话。一个讲印地语的人可以通过点击‘翻译此页面’按钮阅读用西班牙语写的网站,反之亦然。一个讲英语的人观看俄语电影时,可以启用实时翻译字幕。一个在法国的中国游客可以通过获取菜单的照片翻译来点餐。从非常字面意义上讲,机器翻译将语言和文化融合在一起。
在深度学习兴起之前,机器翻译的主流方法是基于查找表。例如,在中文中,‘I’翻译为‘我’,‘drive’翻译为‘开’,而‘car’翻译为‘车’。因此,‘I drive car’会被逐字翻译为‘我开车’。然而,任何双语者都知道这种系统的缺陷。许多拼写相同的词具有不同的含义。一种语言中的多个词可能在另一种语言中被翻译为一个词。此外,不同的语言有不同的语法结构,因此翻译后的词本身需要重新排列。英语中的冠词在像西班牙语和法语这样的有性别语言中有多种不同的上下文依赖翻译。许多试图通过巧妙的语言解决方案来调和这些问题的方法已经被提出,但其效果仅限于短小简单的句子。
深度学习则为我们提供了构建更深层次理解语言的模型的机会——甚至可能接近人类理解语言的方式——从而更有效地执行翻译这一重要任务。在这一部分,我们将介绍语言深度建模的多个额外想法,并最终深入探索 Google 翻译的工作原理。
文本输出递归模型
目前,构建可行的递归模型的最大障碍是无法输出文本。之前讨论的递归模型可以‘读取’但不能‘写入’——输出的结果是一个单一的数字(或一组数字,向量)。为了解决这个问题,我们需要赋予语言模型输出整个文本序列的能力。
幸运的是,我们不需要做太多工作。回顾之前介绍的递归层堆叠概念:我们不是仅在递归层处理完整个序列后收集‘记忆状态’,而是在每个时间步收集‘记忆状态’。因此,为了输出一个序列,我们可以在每个时间步收集一个记忆状态的输出。然后,我们将每个记忆状态传递给指定的 MLP,它预测给定记忆状态下输出词汇中的哪个词(标记为‘MLP C’)。概率最高的词被选为输出。
为了绝对清楚地了解每个记忆状态是如何转化为输出预测的,请考虑以下图示的进展。
在第一个图中,第一个输出的隐藏状态(这是在层读取了第一个词‘the’后得到的隐藏状态)被传递到 MLP C。MLP C 输出一个输出词汇的概率分布;即,它为输出词汇中的每个词提供一个概率,指示该词在该时间点作为翻译被选择的可能性。这是一个前馈网络:我们本质上是在对隐藏状态进行逻辑回归,以确定给定词的可能性。理想情况下,概率最大的词应该是‘les’,因为这是‘the’的法语翻译。
下一个隐藏状态是在递归层读取了‘the’和‘machines’之后得到的,再次传递到 MLP C。这次,概率最高的词应该是‘machine’(这是‘machines’在法语中的复数形式)。
在最后一个时间步中最可能选择的词应该是‘gagnent’,它是‘win’在特定时态中的翻译。模型应该选择‘gagnent’,而不是‘gagner’或其他不同的时态,基于它之前阅读的信息。这就是使用深度学习模型进行翻译的优势所在:能够掌握贯穿整句话的语法规则。
实际上,我们通常希望将多个递归层堆叠在一起,而不仅仅是一个递归层。这允许我们发展多层次的理解,首先是‘理解’输入文本的含义,然后以输出语言重新表达输入文本的‘意义’。
双向性
请注意,递归层是顺序进行的。当它读取文本“the machines win”时,它首先读取“the”,然后是“machines”,再是“win”。虽然最后一个词“win”是根据前面的词“the”和“machines”来读取的,但这种反向关系并不成立:第一个词“the”不是在后面的词“machines”和“win”上下文中读取的。这是一个问题,因为语言通常是为了预期我们将要说的内容而被说出的。在像法语这样的性别语言中,像“the”这样的冠词可以有许多不同的形式——“la”用于阴性物体,“le”用于阳性物体,而“les”用于复数物体。我们不知道要翻译哪个版本的“the”。当然,一旦我们阅读了句子的其余部分——“the machines”——我们就知道对象是复数的,应该使用“les”。这是文本的早期部分受到后期部分影响的一个例子。更一般地说,当我们重新阅读一个句子时——我们常常在不自觉中这样做——我们是在以句子的开始部分为背景来阅读开始部分。尽管语言是按顺序读取的,但它往往需要以‘非顺序’的方式解释(即,不严格从开始到结束的单向顺序)。
为了解决这个问题,我们可以使用双向性——这是一种对递归模型的简单修改,使得层能够‘前向’和‘后向’读取。双向递归层实际上是两个不同的递归层。一个层向前读取时间,而另一个层向后读取。两个层都读取完成后,它们在每个时间步的输出会被加在一起。
双向性使模型能够以一种方式读取文本,使得过去的内容在未来的上下文中被读取,同时也在过去的上下文中读取未来的内容(递归层的默认功能)。请注意,双向递归层在每个时间步的输出是由整个序列而不仅仅是所有之前的时间步所提供的信息。例如,在一个 10 时间步的序列中,时间步 t = 3 是由一个已经读取了序列 [t = 0] → [t = 1] → [t = 2] → [t = 3] 以及 另一个已经读取了序列 [t = 9] → [t = 8] → [t = 7] → [t = 6] → [t = 5] → [t = 4] → [t = 3] 的“记忆状态”所提供的信息。
这个简单的修改使得语言理解的深度显著丰富。
自回归生成
我们当前的翻译模型工作模型是一个大型(双向)递归层的堆栈。然而,存在一个问题:当我们将一些文本 A 翻译成其他文本 B 时,我们不仅仅是根据 A 来写 B,还要根据 B 自身 来写 B。
我们不能直接将复杂的俄语句子“Грузовик внезапно остановился потому что дорогу переходила курица”翻译成英语“ The truck suddenly stopped because a chicken was crossing the road”,因为直接从俄语逐字翻译会得到“Truck suddenly stopped because road was crossed by chicken”。在俄语中,宾语位于名词之后,但在英语中保留这种形式虽然可读但不流畅,也不是“最佳”的。因此,关键思想是:为了获得易于理解和使用的翻译,我们不仅需要确保翻译忠实于原文,还需要“忠实于自身”(自洽)。
为了实现这一点,我们需要一种不同的文本生成方法,称为 自回归生成。这使得模型能够不仅根据原始文本,还根据模型已经翻译过的内容来翻译每个单词。自回归生成不仅是神经翻译模型的主流范式,也是各种现代文本生成模型,包括高级聊天机器人和内容生成器的主流。
我们从一个“编码器”模型开始。在这种情况下,编码器模型可以表示为一个递归层的堆栈。编码器读取输入序列并生成一个输出,即编码后的表示。这一串数字表示输入文本序列的“本质”以定量形式呈现——如果你愿意,可以称其为“普遍/真实意义”。编码器的目标是将输入序列提炼成这个基本的意义包。
一旦获得了这个编码表示,我们就开始解码的任务。解码器的结构与编码器类似——我们可以将其视为另一个接受序列并生成输出的递归层堆栈。在这种情况下,解码器接受编码表示(即编码器的输出)和一个特殊的‘开始标记’(表示为)。开始标记表示句子的开头。解码器的任务是预测给定句子中的下一个词;在这种情况下,它给定了一个‘零词句子’,因此必须预测第一个词。在这种情况下,没有以前的翻译内容,因此解码器完全依赖于编码表示:它预测第一个词‘The’。
接下来是关键的自回归步骤:我们将解码器的先前输出重新输入到解码器中。现在我们有了一个‘单词句子’(开始标记后跟词‘The’)。两个标记都被传入解码器,以及编码表示——和之前一样,由编码器输出——现在解码器预测下一个词“truck”。
这个标记随后被视作另一个输入。在这里,我们可以更清楚地意识到为什么自回归生成是文本生成的一个有用算法框架:给定当前工作的句子是“The truck”,这限制了我们如何完成它。在这种情况下,下一个词很可能是动词或副词,这是我们作为语法结构“知道”的。另一方面,如果解码器仅能访问原始俄语文本,它将无法有效地限制可能性的集合。在这种情况下,解码器能够参考先前翻译的内容和原始俄语句子的含义,从而正确预测下一个词为“突然”。
这个自回归生成过程继续进行:
最后,为了结束一个句子,解码器模型预测一个指定的‘结束标记’(表示为)。在这种情况下,解码器将‘匹配’当前翻译句子与编码表示,以确定翻译是否令人满意并停止句子生成过程。
2016 Google Translate
到现在为止,我们已经覆盖了很多内容。现在,我们掌握了开发对 Google 翻译模型的较全面理解所需的大部分要素。我不需要多说这种模型的重要性:即使粗略,一个准确且易于使用的神经机器翻译系统也能打破许多语言障碍。对我们来说,这个特定的模型有助于将我们讨论的许多概念统一到一个连贯的应用中。
这些信息摘自 2016 年的Google 神经机器翻译论文,该论文介绍了 Google 的深度学习机器翻译系统。尽管在这些年里使用的模型几乎肯定已经发生了变化,但该系统仍然为神经机器翻译系统提供了有趣的案例研究。为了清楚起见,我们将该系统称为“Google 翻译”,并承认它可能不是最新的。
Google 翻译使用了编码器-解码器自回归模型。也就是说,该模型由编码器组件和解码器组件组成;解码器是自回归的(回顾前述:它在接受其他信息的同时,还接受之前生成的输出作为输入,这里是编码器的输出)。
编码器是由七层长短期记忆(LSTM)层组成的堆叠。第一层是双向的(因此技术上有 8 层,因为双向层“算作两个”),这使得它能够捕捉输入文本中双向的重要模式(底部图像,左侧)。此外,该架构在每一层之间采用了残差连接(底部图像,右侧)。回顾之前的讨论,残差连接在递归神经网络中可以通过将输入添加到递归层的输出上来实现,从而使递归层学习到对输入施加的最佳差异。
解码器也是由八层 LSTM 组成的堆叠。它以自回归的方式接收之前生成的序列,从开始标记</s>
开始。然而,Google 神经机器翻译架构使用了自回归生成和注意力机制。
注意力分数是为每个原始文本单词计算的(由编码器中的隐藏状态表示,编码器迭代地转换文本但仍保持位置表示)。我们可以把注意力看作是解码器和编码器之间的对话。解码器说:“我已经生成了[sentence],我想预测下一个翻译词。原始句子中的哪些词与下一个翻译词最相关?”编码器回答:“让我看看你在想什么,我会将其与我对原始输入中每个词的了解进行匹配……啊,你应该关注[word A],但不要过多关注[word B]和[word C],它们与预测下一个特定词的关系较小。”解码器感谢编码器:“我会考虑这些信息来确定如何生成,以便确实关注[word A]。”关于注意力的信息被发送到每个 LSTM 层,使得这种注意力信息在生成的所有层级中都可知。
这代表了谷歌神经机器翻译系统的主要部分。该模型在一个大型翻译任务数据集上进行训练:给定英文输入,预测西班牙文输出。模型学习读取的最佳方式(即编码器中的参数)、关注输入的最佳方式(即注意力计算)以及将关注的输入与西班牙文输出关联的最佳方式(即解码器中的参数)。
后续工作扩展了神经机器翻译系统的多语言能力,在这种能力下,一个模型可以用于在多个语言对之间进行翻译。这不仅从实际角度来看是必要的——为每对语言训练和存储一个模型是不可行的——而且也已显示出能改善任何两对语言之间的翻译。此外,GNMT 论文提供了训练的细节——这是一个受硬件限制的非常深的架构——以及实际部署——大型模型不仅训练慢,预测也慢,但谷歌翻译用户不希望等待超过几秒钟来翻译文本。
虽然 GNMT 系统确实是计算语言理解的一个里程碑,但仅仅几年后,一种在某些方面极简化的新方法将彻底改变语言建模——并完全摆脱曾经常见的递归层,我们曾如此费力地去理解。请关注关于 Transformers 的第二篇文章!
感谢阅读!
在这篇文章中,我们对递归神经网络进行了深入的调查:它们的设计逻辑、更多复杂特性和应用。
一些关键点:
-
RNN(循环神经网络)是前馈网络的自然扩展,可以用于机器翻译。RNN 旨在跟踪迄今为止学习到的信息,并将新信息与先前见过的信息相关联,类似于人类处理顺序信息的方式。
-
RNN 使用递归层,其中隐藏状态代表短期记忆。
-
可以堆叠递归层,以增加网络对理解和推理的深度。
-
长短期记忆(LSTM)网络是更复杂的递归层类型,它将长期记忆与短期记忆分开。LSTM 具有清除、更新和通知长期记忆和短期记忆的机制。
-
RNN 在金融分析、社交媒体分析、推荐系统和语言翻译等多个领域都有应用。
-
RNN 在现实世界应用中的使用引发了关于隐私、模型错误以及在标记有毒或有害内容时的主观性等哲学和伦理问题。
-
神经机器翻译是 RNN 的一个强大应用,它使不同语言之间的翻译成为可能,促进了沟通和文化交流。
所有照片由作者提供。
递归化学反应
使用 RDKit 对化学结构进行算法分析
·
关注 发表在Towards Data Science ·8 分钟阅读·2023 年 3 月 7 日
–
图片来源于Manuel Darío Fuentes Hernández来自Pixabay
RDKit 是一个用 C++ 编写的开源化学信息学工具包,也可以在 Java、Python 和 KNIME 中使用。它提供了广泛的化学信息学功能,例如读取和写入分子、处理原子、键和环、生成 2D 或 3D 坐标、搜索子结构、应用化学转换以及计算指纹和描述符。RDKit 还为 PostgreSQL 提供了高性能的 cartridge 数据库。RDKit 理解 SMARTS 语言(用于描述分子模式),以及 SMIRKS 语言(用于应用反应转换),这两者都以 SMILES 为基础,SMILES 是用于输入和表示分子及反应的著名线性符号。
RDKit 可以应用反应转换,并结合 Python 的递归功能,支持如本文所述的特殊用例。特别是,我们将递归地应用化学反应,以检查输入的分子结构是否为肽,即氨基酸的线性序列。我发现递归应用反应是算法分析化学结构的一个好方法,例如,通过反复分割结构、去除定义明确的片段并分类剩余基团,当不再能应用更多的转换时。
· 介绍
· 构建模块:氨基酸
· 识别肽键
· 打破肽键
· 结论
介绍
下图是一个由四个氨基酸组成的线性寡肽,即精氨酸、丙氨酸、苏氨酸和蛋氨酸,最近在我分析的数据集中发现。我从事工业化学工作,看到我们的数据库中有这样的结构感到惊讶,因为我们与生物分子无关,肽也不常见。进一步挖掘发现,我们通过将化学名称解释为化学名称到结构的算法错误地生成了这些结构。除了全名,氨基酸还可以用一个或三个字符表示,这些字符有时出现在文本中,并被错误地解释为化学结构。这种数据质量问题可能会影响公开可用的数据集,因此我认为找到检测这些情况并在其意外出现时加以消除的方法可能会很有用。
一小段含有四个氨基酸的肽。图像来源:作者。
解决这个问题有很多方法,我认为所有线性肽序列都可以通过复杂的 SMARTS 查询来匹配。开发这个方法对我来说似乎很具有挑战性,因此我认为通过将问题拆分成更小的问题来解决可能更为可行。在化学术语中,这意味着我们需要通过逐一水解肽键,将初始分子分解成更小的片段,直到没有更多的键可以断裂。如果所有获得的片段都是氨基酸,那么起始结构必须是肽。这样做还可以确定精确的氨基酸序列。我们需要的是一种递归应用化学反应的方法。如果你有兴趣了解如何使用 RDKit 实现这一点,并在过程中发现这个丰富的化学信息学库的一些功能,请继续阅读。
本文的代码可以在我的 GitHub 博客 仓库 中找到。我们将使用 RDKit 版本 2022.9.4 和 Python 3.9.13。该仓库还包括了提供的要求 文件 中的所有依赖项。
基础单元:氨基酸
组成蛋白质的氨基酸共有 20 种。此外,还有两种额外的氨基酸,这些氨基酸在某些物种中由通常解释为终止密码子的密码子编码。所有这些都是α-氨基酸,即氨基直接连接到α-碳上,即连接到羧基的碳原子。为了方便起见,我为这 22 种氨基酸创建了 SMILES,这些 SMILES 可以在 仓库 中找到。
第一步是读取 22 种氨基酸,并同时导入所有必要的 RDKit 模块。
在倒数第二行中,微笑表情被转换为本地的 RDKit Mol 对象。最后一行可能看起来有些神秘,但它的作用只是将氨基酸名称包含在分子中,具体可以通过
print(Chem.MolToMolBlock(amino_acids['mol'].iloc[0]))
打印出来
L-丙氨酸 Mol 块,其中包含了化学名称。图片由作者提供。
氨基酸可以使用 matplotlib 在定制的网格中进行可视化,如下所示。
最后一行将下方的图像保存为 PNG 文件,你可以在仓库中找到这个文件以及本文中所有其他 图像。
出现在遗传密码中的 22 种氨基酸。图片来源于作者。
RDKit 可以无缝地在 jupyter notebooks 中使用,其中 Draw 模块允许通过Draw.MolsToGridImage()
函数轻松可视化分子结构。但我发现使用 matplotlib 可以提供更多灵活性,特别是如果遵循这些出色的 建议 来调整图表组件。所有氨基酸共享以下对映体骨架。
α氨基酸骨架。图片来源于作者。
在这一阶段,我们可以引入一个有用的 RDKit 功能,即所谓的 R 基团分解。在下面的代码中,我们定义了氨基酸骨架核心,其 smiles 为 [*:1]C@HC(O)=O,具有两个明确的 R 基团标签。使用两个 R 基团标签的原因是 L-脯氨酸中的呋喃环。通过明确设置 R 基团分解,我们将其约束为仅匹配明确指定的 R 基团。
其余的代码创建了必要的输入数组,包含分子和图例,用于生成图像,使用的是与之前相同的实用函数。如果你仔细观察,你会注意到甘氨酸未能被分解为 R 基团。原因是它不是手性分子,而用于分解的核心结构是手性的。如果我们从核心中移除手性中心,甘氨酸的分解会成功,但 R 基团分解会失去一些特异性,这可能是不希望的。
氨基酸 R 基团分解。图片来源于作者。
R 基团分解在氨基酸骨架需要进一步处理的情况下很有用。本文不再对此深入探讨。
识别肽键
在我们尝试断裂肽键之前,我们可以检查是否能够识别这些肽键。我们使用一个由苏氨酸、蛋氨酸和精氨酸形成的线性三肽作为例子。肽键被定义为一种结构模式,用来定位原始分子中的匹配原子和键。
这产生了下面的图像。
苏氨酸、精氨酸、蛋氨酸三肽,突出显示了两个肽键。图片来源于作者。
使用 rdkit.Chem.Draw.rdMolDraw2D
,肽键被很好地定向。子结构搜索返回了一个原子索引的元组,其中的元组被展平,并用于在展平列表中找到所有原子对之间的键索引。在分子结构被保存为 PNG 图像之前,使用浅灰色突出显示了肽链中原子和键的索引。这里的关键消息是,通过使用 RDKit,可以控制到原子和键的级别,这些原子和键实质上形成了适合任何想象中的算法的图形。
打破肽键
打破肽键需要使用 SMIRKS 定义一个反应。之前定义的肽模式用于子结构匹配,成为 SMIRKS 反应的反应物部分。产品只是水解产物,其中两个氨基酸使用点符号分隔。苏氨酸、精氨酸、蛋氨酸三肽有两个肽键,因此可以应用两次反应,导致两个反应结果,这两个结果都包括一个氨基酸和一个二肽。
上述代码生成了一个 PNG 图像,其中包含两行的两个反应可能性。
苏氨酸、精氨酸、蛋氨酸三肽的水解。图片由作者提供。
如果我们想将一个肽完全分解成其组成的氨基酸,我们需要反复应用反应,直到不能再打破更多肽键。在每个阶段不必列举所有可能的反应。我们可以简单地应用一次反应,然后分别对两个反应物进行水解。RDKit 允许通过将 maxProducts
参数 设置为 1 来控制产品数量。什么使得起始结构成为肽?如果在应用所有可能的肽水解反应后我们只产生了一种或多种已知的氨基酸,那么起始结构就是一个肽。相反,如果在某个时点无法应用任何肽水解反应,并且结构不是已知氨基酸之一,那么起始结构就不是肽。
上述代码使用了两个实用函数,一个用于检查两个结构是否通过彼此为子结构的方式在化学上等效,另一个用于检查结构是否可以在结构列表中找到。递归肽键水解在第三个也是最后一个函数中实现。
使用这个递归函数,我们检查一组九个示例分子是否是肽。
该算法正确地将前 8 个结构分类为肽,将最后两个分类为非肽。值得注意的是,我们使用了氨基酸本身就是一个肽的约定,这严格来说可能并不准确,但这对于应用递归来说是方便的。
通过递归水解检查结构是否为肽。图像由作者提供。
通过将反应物结果添加到图中,可以增强算法,例如使用NetworkX并通过在每个节点上绘制结构来可视化反应进程。叶子节点将是可以进一步分析的氨基酸,以获取肽中氨基酸的确切序列。可能性无穷无尽;RDKit 已经完成了它的部分工作,然后可以依赖 Python 的表现力完成其余部分。
结论
RDKit 是一个丰富的化学信息学库。现在可以通过 pip 轻松部署,并打开了在化学应用中使用 Python 及其数据分析和数据科学生态系统的可能性。RDKit 的文档虽说并非最好,但现在有很多教程和博客可以参考。该库不断发展,新增了许多功能。希望这篇文章对展示 RDKit 的一些功能和潜力有所帮助。
通过大型语言模型重新定义对话式人工智能
实现对话式人工智能以提供统一用户体验的指南
·
关注 发表在 Towards Data Science · 21 分钟阅读 · 2023 年 9 月 28 日
–
来源: rawpixel.com
对话式人工智能是大型语言模型(LLMs)的一种应用,因其在多个行业和用例中的可扩展性而引发了大量关注。尽管对话式系统已经存在了几十年,但 LLMs 带来了大规模应用所需的质量提升。在本文中,我们将使用图 1 所示的心理模型来剖析对话式人工智能应用(参见 用整体心理模型构建 AI 产品以了解心理模型)。在考虑了对话式人工智能系统的市场机会和商业价值之后,我们将解释需要设置的额外“机制”,包括数据、LLM 微调和对话设计,以使对话不仅成为可能,而且有用和愉悦。
图 1:AI 系统的心理模型(参见 用整体心理模型构建 AI 产品)
1. 机会、价值和局限
传统的用户体验(UX)设计围绕着大量的人工 UX 元素、滑动、点击和触碰展开,这要求每个新应用都有一个学习曲线。通过使用对话式人工智能,我们可以摆脱这些繁琐的操作,代之以自然流畅对话的优雅体验,这样我们可以忘记不同应用、窗口和设备之间的过渡。我们使用语言——我们通用且熟悉的沟通协议——与不同的虚拟助手(VAs)互动并完成任务。
对话式用户界面(UI)并不是全新的热门事物。交互式语音响应系统(IVRs)和聊天机器人自 1990 年代以来就已经存在,而自然语言处理(NLP)的重大进展也一直伴随着语音和聊天界面的希望和发展浪潮。然而,在大型语言模型(LLMs)出现之前,大多数系统都是以符号范式实现的,依赖于规则、关键字和对话模式。它们还局限于特定的、预定义的“能力”领域,用户如果超出这些领域很快就会陷入困境。总的来说,这些系统充满了潜在的失败点,在经历了几次令人沮丧的尝试后,许多用户再也没有回到这些系统中。下图展示了一个对话示例。一个希望为特定音乐会订票的用户耐心地经过了一系列详细的询问流程,结果在最后发现音乐会已经售罄。
图 2:糟糕对话流程的示例
作为一种赋能技术,LLMs 可以将对话界面提升到新的质量和用户满意度水平。对话系统现在能够展示更广泛的世界知识、语言能力和对话能力。利用预训练模型,它们也可以在更短的时间内开发完成,因为编写规则、关键词和对话流程的繁琐工作现在被 LLM 的统计知识所取代。让我们来看看两个对话式 AI 可以大规模提供价值的突出的应用场景:
-
客户支持,更一般来说,是那些由大量用户使用的应用,这些用户经常提出类似的请求。在这里,提供客户支持的公司在信息上相对于用户具有明显的优势,可以利用这一点创造出更直观和愉悦的用户体验。以重新预订航班为例。对于我这样一个经常乘坐飞机的人来说,这是一年中会发生 1 到 2 次的事情。在这段时间内,我往往会忘记过程的细节,更不用说特定航空公司的用户界面了。相比之下,航空公司的客户支持将重新预订请求置于其操作的核心。与其通过复杂的图形界面展示重新预订过程,不如将其逻辑“隐藏”在联系支持的客户面前,他们可以使用自然语言作为通道来完成重新预订。当然,仍然会有一些较少见的请求。例如,想象一下一个突发的情绪波动使得一位商务客户决定将她心爱的狗作为超额行李添加到已预订的航班中。这些更具个性化的请求可以交给人工客服处理,或通过连接到虚拟助手的内部知识管理系统来解决。
-
知识管理依赖于大量的数据。对于许多现代公司而言,他们在运营、迭代和学习过程中积累的内部知识是一项核心资产和差异化因素——前提是这些知识以高效的方式存储、管理和访问。虽然公司拥有隐藏在协作工具、内部维基、知识库等中的大量数据,但他们常常未能将其转化为可操作的知识。随着员工离职、新员工加入,以及你永远无法完成三个月前开始的文档页面,有价值的知识会受到熵的影响。找到内部数据迷宫中的出路并获取特定业务情境所需的信息变得越来越困难。这导致了知识工作者的巨大效率损失。为了解决这个问题,我们可以通过在内部数据源上增强 LLM 的语义搜索功能来应对。LLM 允许使用自然语言问题而不是复杂的正式查询来对数据库进行提问。这样,用户可以专注于他们的信息需求,而不是知识库的结构或查询语言(如 SQL)的语法。由于这些系统是基于文本的,它们在丰富的语义空间中处理数据,在“幕后”进行有意义的连接。
除了这些主要的应用领域,还有许多其他应用,例如远程医疗、心理健康助手和教育聊天机器人,它们可以以更快、更高效的方式优化用户体验并为用户带来价值。
2. 数据
大型语言模型(LLMs)最初并不是为了进行流畅的小谈话或更深入的对话而训练的。相反,它们在每一步推理中学习生成下一个标记,最终形成连贯的文本。这种低级别的目标与人类对话的挑战不同。对人类来说,对话非常直观,但当你想教会机器做到这一点时,它就变得极其复杂和微妙。例如,让我们来看一下意图的基本概念。当我们使用语言时,我们是为了特定的目的,这就是我们的沟通意图——可能是传达信息、社交或要求别人做某事。前两种目的对 LLM 来说相对直接(只要它在数据中见过所需的信息),而后者则更具挑战性。LLM 不仅需要以连贯的方式组合和组织相关信息,还需要在正式性、创造力、幽默感等软性标准方面设定正确的情感基调。这是对话设计(参见第五部分)的挑战,与创建微调数据的任务紧密相关。
从传统语言生成转向识别和响应特定的交流意图是提升对话系统可用性和接受度的重要步骤。与所有微调工作一样,这从编制适当的数据集开始。
微调数据应尽可能接近(未来的)实际数据分布。首先,它应该是对话(对话)数据。其次,如果你的虚拟助手将专注于特定领域,你应该尝试组装反映必要领域知识的微调数据。第三,如果你的应用程序中有典型的流程和请求会频繁出现,例如客户支持中的情况,请尽量将这些的多样化示例纳入你的训练数据中。下表显示了来自3K Conversations Dataset for ChatBot的对话微调数据示例,该数据集在 Kaggle 上免费提供:
表 1:来自3K Conversations Dataset for ChatBot的对话微调数据示例
手动创建对话数据可能是一项昂贵的工作——众包和使用 LLMs 来帮助生成数据是扩展的两种方式。一旦对话数据收集完成,就需要对对话进行评估和注释。这使你能够向模型展示正面和负面的示例,并推动其捕捉到“正确”对话的特征。评估可以通过绝对分数或不同选项之间的排名来进行。后一种方法能够提供更准确的微调数据,因为人类通常更擅长对多个选项进行排名,而不是单独评估它们。
在数据就绪后,你可以对模型进行微调,并为其增添额外的功能。在下一节中,我们将探讨微调、从记忆和语义搜索中集成额外信息,以及将代理连接到你的对话系统以使其能够执行特定任务。
3. 组装对话系统
一个典型的对话系统由一个协调和组织系统组件和能力的对话代理构建,例如 LLM、内存和外部数据源。对话 AI 系统的开发是一个高度实验性和经验性的任务,你的开发人员将不断在优化数据、改进微调策略、尝试额外组件和增强功能以及测试结果之间反复进行。非技术团队成员,包括产品经理和用户体验设计师,也将持续测试产品。根据他们的客户发现活动,他们能够很好地预测未来用户的对话风格和内容,并应积极贡献这一知识。
3.1 教授你的 LLM 对话技能
对于微调,你需要你的微调数据(参见第二部分)和一个预训练的 LLM。LLM 已经对语言和世界有很多了解,我们的挑战是教会它们对话的原则。在微调中,目标输出是文本,模型将被优化以生成尽可能与目标相似的文本。对于监督微调,你首先需要清晰定义你希望模型执行的对话 AI 任务,收集数据,然后运行和迭代微调过程。
随着对 LLM 的炒作,各种微调方法应运而生。对于一个相对传统的对话微调示例,你可以参考 LaMDA 模型的描述。[1] LaMDA 通过两个步骤进行微调。首先,使用对话数据教会模型对话技能(“生成”微调)。然后,使用评估数据时由注释员产生的标签来训练分类器,这些分类器可以评估模型在所需属性(包括合理性、具体性、趣味性和安全性)上的输出。这些分类器随后用于引导模型的行为朝向这些属性。
图 3:LaMDA 通过两个步骤进行微调
此外,事实基础性——将其输出建立在可靠的外部信息上的能力——是 LLM 的一个重要属性。为了确保事实基础性并最小化幻觉,LaMDA 通过一个数据集进行了微调,该数据集涉及在需要外部知识时调用外部信息检索系统。因此,模型学会了在用户提出需要新知识的查询时,首先检索事实信息。
另一种流行的微调技术是来自人类反馈的强化学习(RLHF)[2]。RLHF “重定向”了大型语言模型(LLM)的学习过程,从简单但人为的下一个词预测任务转向在特定交流情境中学习人类偏好。这些人类偏好直接编码在训练数据中。在标注过程中,人类会收到提示,或者写下期望的回应,或者对一系列现有回应进行排序。然后,LLM 的行为被优化以反映人类的偏好。
3.2 添加外部数据和语义搜索
除了编译对话用于微调模型之外,你可能还想用可以在对话中利用的专门数据来增强你的系统。例如,你的系统可能需要访问外部数据,如专利或科学论文,或内部数据,如客户档案或你的技术文档。这通常通过语义搜索(也称为检索增强生成,或 RAG)[3] 完成。额外的数据以语义嵌入的形式保存在数据库中(参见这篇文章以了解嵌入的解释和进一步参考)。当用户请求到来时,它会被预处理并转换为语义嵌入。语义搜索然后识别与请求最相关的文档,并将其作为提示的上下文。通过将额外数据与语义搜索结合,你可以减少幻觉并提供更有用、基于事实的回应。通过不断更新嵌入数据库,你还可以保持系统的知识和回应的最新状态,而不需要不断重新运行微调过程。
3.3 记忆与上下文意识
想象一下你去参加一个派对,遇到了彼得,一位律师。你很兴奋,开始介绍你正在计划构建的法律聊天机器人。彼得看起来很感兴趣,朝你倾斜身体,嗯嗯作声并点头。在某个时刻,你想知道他是否愿意使用你的应用程序。你听到的不是一个能弥补你口才的有信息性的声明,而是:“嗯……这个应用程序来做什么的?”
人类之间的未书面沟通契约假定我们在倾听对话伙伴,并在我们共同创建的上下文基础上构建自己的言语行为。在社交环境中,这种共同理解的出现标志着一次富有成效、充实的对话。在更平凡的场景中,比如预订餐厅桌位或购买火车票,这是完成任务并向用户提供期望价值的绝对必要条件。这要求您的助理了解当前对话的历史,也包括过去对话的历史——例如,它不应该在用户每次发起对话时重复询问用户的姓名和其他个人信息。
维护上下文意识的挑战之一是共指消解,即理解代词指代的对象。人类在解释语言时直观地使用了很多上下文线索——例如,您可以问一个小孩:“请把红色盒子里的绿球拿出来给我,”孩子会知道您指的是球,而不是盒子。对于虚拟助手来说,这项任务可能相当具有挑战性,如以下对话所示:
助理:谢谢,我现在将为您预订航班。您是否还想为航班订餐?
用户:嗯……我可以稍后决定是否需要吗?
助理:抱歉,这个航班不能更改或取消。
在这里,助理未能识别用户的代词it指的不是航班,而是餐食,因此需要进行另一次迭代以纠正这一误解。
3.4 附加保护措施
即使是最好的 LLM,也会偶尔出现行为不当和幻觉。在许多情况下,幻觉只是简单的准确性问题——而且,您需要接受没有任何 AI 是 100% 准确的。与其他 AI 系统相比,用户与 AI 之间的“距离”相对较小。简单的准确性问题很快会转变为被认为是有害的、歧视性的或一般性的有害内容。此外,由于 LLM 对隐私没有固有的理解,它们还可能泄露诸如个人身份信息(PII)之类的敏感数据。您可以通过使用额外的保护措施来抵制这些行为。工具如 Guardrails AI、Rebuff、NeMo Guardrails 和 Microsoft Guidance 允许您通过对 LLM 输出制定额外要求并阻止不良输出来降低系统的风险。
对话 AI 中可能有多种架构。以下示例展示了如何通过对话代理将微调的 LLM、外部数据和记忆集成在一起,代理还负责提示构建和保护措施。
图 4:包含微调 LLM、语义搜索数据库和记忆组件的对话 AI 系统示意图
4. 用户体验与对话设计
对话界面的魅力在于它们在不同应用程序中保持的简单性和一致性。如果用户界面的未来是所有应用看起来或多或少都一样,那么 UX 设计师的工作就注定要失败了吗?绝对不是——对话是一门艺术,需要教给你的 LLM,以便它能够进行对用户有帮助、自然且舒适的对话。良好的对话设计是在结合我们对人类心理学、语言学和 UX 设计的知识时产生的。接下来,我们将首先考虑在构建对话系统时的两个基本选择,即你是否会使用语音和/或聊天,以及你系统的更大背景。然后,我们将看看对话本身,并了解如何设计你的助手的个性,同时教会它进行有帮助和合作的对话。
4.1 语音与聊天
对话界面可以通过聊天或语音实现。简而言之,语音更快,而聊天允许用户保持隐私,并受益于丰富的 UI 功能。让我们深入了解这两种选项,因为这是构建对话应用时你将面临的第一个也是最重要的决策之一。
要在这两种选择之间做出决定,首先要考虑你的应用将使用的物理环境。例如,为什么几乎所有汽车中的对话系统,如 Nuance Communications 提供的那些,都是基于语音的?因为司机的双手已经忙碌,他们不能不断在方向盘和键盘之间切换。这同样适用于其他活动,如烹饪,在这些活动中,用户希望在使用你的应用时保持活动的流畅。汽车和厨房通常是私人环境,因此用户可以享受语音交互的乐趣,而无需担心隐私问题或打扰他人。相比之下,如果你的应用将在办公室、图书馆或火车站等公共场所使用,语音可能不是你的首选。
在了解了物理环境后,考虑情感方面。语音可以有意地传达语调、情绪和个性——这在你的背景中是否增添了价值?如果你为休闲目的构建应用,语音可能会增加趣味性,而一个用于心理健康的助手可以更具同情心,并允许潜在的困扰用户有更大的表达范围。相比之下,如果你的应用将帮助用户在专业环境中,如交易或客户服务,基于文本的更匿名交互可能有助于做出更客观的决策,并免去设计过于情感化体验的麻烦。
下一步,考虑功能性。基于文本的界面允许你通过其他媒体(如图片)和图形用户界面元素(如按钮)来丰富对话。例如,在一个电子商务助手中,通过展示产品图片和结构化描述来推荐产品的应用程序将比通过语音描述产品并可能提供其标识符的应用程序更具用户友好性。
最后,让我们谈谈构建语音用户界面的额外设计和开发挑战:
-
在用户输入可以通过 LLM 和自然语言处理(NLP)处理之前,还有一个额外的语音识别步骤。
-
语音是一种更具个人化和情感化的沟通媒介——因此,为虚拟助手设计一致、合适且愉快的个性化要求更高,你需要考虑“语音设计”的额外因素,如音色、重音、语调和语速。
-
用户期望你的语音对话速度与人类对话速度相同。为了通过语音提供自然的互动,你需要比聊天更短的延迟。在人类对话中,转折之间的典型间隔为 200 毫秒——这种快速响应是可能的,因为我们在听对方讲话时开始构建我们的发言。你的语音助手需要达到这种流畅度。相比之下,对于聊天机器人,你需要与几秒钟的时间跨度竞争,一些开发者甚至引入额外的延迟,使对话感觉像是人类之间的输入聊天。
-
语音沟通是线性的、一发即成的事业——如果用户没有听懂你说的话,你将进入一个冗长且容易出错的澄清循环。因此,你的发言需要尽可能简洁、清晰和信息丰富。
如果你选择语音解决方案,确保你不仅清楚了解相对于聊天的优势,还具备应对这些额外挑战的技能和资源。
4.2 你的对话 AI 将生活在哪里?
现在,让我们考虑你可以集成对话 AI 的更大背景。我们都熟悉公司网站上的聊天机器人——当我们打开商业网站时,屏幕右侧弹出的那些小部件。就我个人而言,更多时候,我的直觉反应是寻找“关闭”按钮。这是为什么呢?通过最初尝试“与这些机器人对话”,我了解到它们无法满足更具体的信息需求,最终我仍然需要浏览网站。故事的寓意?不要因为聊天机器人很酷和时尚而构建它——而是因为你确定它能为用户创造额外的价值。
除了公司网站上有争议的小部件,还有几个令人兴奋的背景,可以集成那些随着 LLMs 变得可能的更通用的聊天机器人:
-
副驾驶:这些助手在特定的流程和任务中为你提供指导和建议,比如用于编程的 GitHub CoPilot。通常,副驾驶是“绑定”到特定应用程序(或一小套相关应用程序)的。
-
合成人物(也称数字人物):这些生物在数字世界中“模仿”真实人类。它们看起来、行动和说话都像人类,因此也需要丰富的对话能力。合成人物通常用于沉浸式应用,如游戏、增强现实和虚拟现实。
-
数字双胞胎:数字双胞胎是现实世界过程和物体的数字“副本”,例如工厂、汽车或引擎。它们用于模拟、分析和优化真实物体的设计和行为。与数字双胞胎的自然语言交互使得对数据和模型的访问更加顺畅和多样化。
-
数据库:如今,任何主题的数据都是可用的,无论是投资建议、代码片段还是教育材料。通常难的是找到用户在特定情况下需要的非常具体的数据。图形界面的数据库要么过于粗糙,要么布满了无尽的搜索和过滤小部件。诸如 SQL 和 GraphQL 等多功能查询语言仅对具备相应技能的用户开放。对话式解决方案允许用户以自然语言查询数据,而处理请求的 LLM 会自动将其转换为相应的查询语言(参见这篇文章以了解 Text2SQL 的解释)。
4.3 在你的助手上印刻个性
作为人类,我们天生倾向于拟人化,即在看到某些与人类略微相似的事物时附加额外的人类特征。语言是人类最独特和迷人的能力之一,对话产品将自动与人类相关联。人们会想象屏幕或设备背后有一个人——而且最好不要让这个特定的人物依赖于用户的想象,而是赋予其与您的产品和品牌一致的个性。这个过程被称为“角色设计”。
角色设计的第一步是理解你希望角色展示的特质。理想情况下,这应该在训练数据层面上完成——例如,在使用 RLHF 时,你可以要求标注者根据有用性、礼貌性、趣味性等特质对数据进行排名,从而使模型偏向于所需的特征。这些特征可以与您的品牌属性相匹配,以创建一个一致的形象,通过产品体验不断地促进品牌建设。
除了一般特征外,你还应该考虑你的虚拟助手如何处理“幸福路径”之外的特定情况。例如,它将如何回应超出其范围的用户请求,回答关于自身的问题,以及处理辱骂或粗俗的语言?
重要的是要制定明确的内部指南,以供数据标注员和对话设计师使用。这将使你能够以有目的的方式设计你的角色,并在团队内及随着应用程序经过多个迭代和优化的过程中保持一致性。
4.4 使用“合作原则”使对话更有帮助
你是否曾经有过和砖墙对话的感觉,即便你实际上是在和一个人交谈?有时,我们发现我们的对话伙伴根本不愿意将对话引向成功。幸运的是,在大多数情况下,事情会更顺利,人们会直观地遵循由语言哲学家保罗·格赖斯提出的“合作原则”。根据这一原则,成功沟通的人遵循四个准则,即量、质量、相关性和方式。
量的准则
量的准则要求说话者提供信息,并使他们的贡献尽可能有信息量。在虚拟助手的角度,这也意味着积极推动对话。例如,请考虑这个来自电子商务时尚应用的片段:
助理:你在寻找什么类型的衣物?
用户:我在找一件橙色的连衣裙。
助理:不要:对不起,我们目前没有橙色的连衣裙。
做:对不起,我们没有橙色的连衣裙,但我们有这件很棒且非常舒适的黄色连衣裙:…
用户希望从你的应用中找到合适的物品。由于没有符合确切描述的物品而停止对话会使成功的可能性丧失。然而,如果你的应用能提出替代物品的建议,它会显得更有帮助,并留住成功互动的可能性。
尤其是在语音交互中,找到提供用户可能需要的所有信息与不使他们被不必要的信息淹没之间的正确平衡非常重要,以免干扰对话。
质量的准则
质量的准则要求说话者要真实,不说他们认为是虚假的或缺乏充分证据的事物。在这里涉及大量主观性和个人信念——因此,这一准则不能直接投射到大型语言模型上。正如我们所见,大型语言模型倾向于产生虚假信息,这种风险在对话进入不熟悉的领域时尤其高。可以使用额外的保护措施来避免误导性陈述,并将对话引回正轨。
相关性准则
相关原则要求我们要——确切地说,要相关。作为前提,虚拟助理必须理解用户的真实意图。以下是一个违反这一原则的航空公司助理的例子:
用户:我需要一张明天早上从柏林到苏黎世的航班。
助理:好的,我找到了一些明天早上从柏林到苏黎世的航班。航班 BZ36294 在早上 6:30 从柏林起飞,8 点到达苏黎世。航班 BZ96294 在早上 8 点从柏林起飞,9:30 到达。航班 BZ96294 在早上 8 点从柏林起飞,9:30 到达苏黎世。航班 BZ95614 在早上 11 点从柏林起飞,12:30 到达苏黎世。此外,还有一个中转航班的可能性,即你将在 11:45 离开柏林,3 点到达苏黎世。
用户:对不起……你能再重复一遍吗?
助理用大量可能不相关的详细信息让用户感到不知所措。通过将这些信息分解并引导用户缩小搜索范围,对话会变得更易于理解:
用户:我需要一张明天早上从柏林到苏黎世的航班。
助理:好的,我找到了一些明天早上从柏林出发的航班,分别是 6:30、8 点和 11 点。你想选择哪个时间出发?
用户:我需要在早上 9 点前到达苏黎世。
助理:好的,你可以选择航班 BZ36294。它在 6:30 出发,8 点到达。我要为你购买机票吗?
用户:是的,谢谢。
表述原则
最后,表述原则指出我们的言语行为应当清晰、简洁、有序,避免歧义和模糊表达。你的虚拟助理应避免技术或内部术语,倾向于使用简单、普遍易懂的表述。
虽然格赖斯的原则适用于所有对话,无论具体领域如何,但没有专门针对对话进行训练的 LLMs 往往无法满足这些原则。因此,在编制训练数据时,重要的是要有足够的对话样本,以便模型能够学习这些原则。
对话设计领域发展迅速。无论你是已经在构建人工智能产品还是考虑在人工智能领域的职业道路,我鼓励你深入探讨这个话题(参见[5]和[6]中的优秀介绍)。随着人工智能逐渐成为一种商品,良好的设计和可辩护的数据策略将成为人工智能产品的重要差异化因素。
摘要
让我们总结一下文章的要点。此外,图 5 提供了一个“备忘单”,其中包含主要内容,你可以下载作为参考。
-
大型语言模型(LLMs)提升了对话型人工智能的质量和可扩展性:大型语言模型(LLMs)显著提高了各个行业和应用场景中对话型人工智能应用的质量和可扩展性。
-
对话型人工智能可以为处理大量类似用户请求(例如客服)或需要访问大量非结构化数据(例如知识管理)的应用增加很多价值。
-
数据:为对话任务微调 LLMs 需要高质量的对话数据,这些数据应尽可能贴近现实世界的互动。众包和 LLM 生成的数据可以是扩大数据收集的宝贵资源。
-
组建系统:开发对话型 AI 系统是一个迭代和实验的过程,涉及对数据、微调策略和组件集成的持续优化。
-
教授对话技巧给 LLMs:微调 LLMs 涉及训练它们识别和响应特定的交流意图和情境。
-
使用语义搜索添加外部数据:通过使用语义搜索整合外部和内部数据源,可以通过提供更多上下文相关的信息来增强 AI 的响应。
-
记忆和上下文意识:有效的对话系统必须保持上下文意识,包括跟踪当前对话和过去互动的历史,以提供有意义和连贯的回应。
-
设置保护措施:为了确保负责任的行为,对话型 AI 系统应采用保护措施,以防止不准确、虚假信息和隐私泄露。
-
人物设定:为你的对话助手设计一个一致的人物设定对创建连贯且具有品牌特色的用户体验至关重要。人物特征应与产品和品牌属性相一致。
-
语音与聊天:选择语音还是聊天界面取决于物理环境、情感背景、功能和设计挑战等因素。在决定对话型 AI 的界面时,请考虑这些因素。
-
在各种环境中的集成:对话型 AI 可以在不同的环境中集成,包括副驾驶、合成人物、数字双胞胎和数据库,每种情况都有特定的应用场景和需求。
-
观察合作原则:遵循对话中的数量、质量、相关性和方式原则可以使与对话型 AI 的互动更加有帮助和用户友好。
图 5:对话型 AI 的关键要点和最佳实践
参考文献
[1] Heng-Tze Chen 等. 2022. LaMDA: 迈向安全、扎根且高质量的对话模型。
[2] OpenAI. 2022. ChatGPT: 优化对话模型。2022 年 1 月 13 日检索。
[3] Patrick Lewis 等. 2020. 检索增强生成用于知识密集型 NLP 任务。
[4] Paul Grice. 1989. 《言语的方式研究》。
[5] Cathy Pearl. 2016. 《语音用户界面设计》。
[6] Michael Cohen 等. 2004. 《语音用户界面设计》。
注意:除非另有说明,所有图片均由作者提供。
减少你的 Cloud Composer 账单(第一部分)
原文:
towardsdatascience.com/reduce-your-cloud-composer-bills-f03e112df689
使用定时 CICD 流水线关闭环境并恢复到之前的状态
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 24 日
–
图片由 Sasun Bughdaryan 提供,来源于 Unsplash
Cloud Composer 是一个托管的、可扩展的流行复杂作业调度器 Airflow 的安装版本。该服务在 Google Cloud Platform (GCP) 上提供两种版本:Cloud Composer 1 和 Cloud Composer 2,主要区别在于只有 Cloud Composer 2 才提供 Workers Autoscaling。
由于我已经使用该服务多年,我可以肯定地说它值得一试。然而,一些公司会避免使用这项服务,这个原因可能不会让你感到太意外。钱。
在这篇文章中,我将分享一个减少 Cloud Composer 账单的有效方法。尽管代码片段仅适用于 Cloud Composer 2,但提倡的策略仍适用于 Cloud Composer 1 用户。
请注意,这是一系列两部分中的第一部分。第二篇文章可以在 这里 查阅。
以下是将要涵盖的主要主题:
理解 Cloud Composer 2 定价(第一部分)
使用快照关闭 Composer 并保留其状态(第一部分)
使用快照创建 Composer 环境(第一部分)
总结(第一部分)
销毁 Composer 环境以节省费用(第二部分)
更新 Composer 环境(第二部分)
自动化 Composer 环境的创建与销毁(第二部分)
总结(第二部分)
理解 Cloud Composer 2 定价
Cloud Composer 的主要概念是环境。基本上,一个环境对应于一个 Airflow 实例,具有一个名称和版本。每个环境由一组 Google Cloud 服务组成,这些服务的使用会产生一些费用。例如,Airflow 元数据数据库在每个环境中作为Cloud SQL 实例提供,而 Airflow Scheduler 在每个环境中作为Google Kubernetes Engine Pod进行部署。
Cloud Composer 定价有 3 个主要部分:
-
计算成本:这是运行 Airflow Scheduler、Airflow Workers、Airflow Triggerers、Airflow Web Servers 和其他 Cloud Composer 组件的 Google Kubernetes Engine 节点的成本。
-
数据库存储成本:这对应于 Airflow 元数据数据库存储的 Cloud SQL 存储成本。
-
环境规模成本:环境规模与某些完全由 Google Cloud 控制的 Cloud Composer 组件相关。这些组件的规模根据为环境大小参数(小型 | 中型 | 大型)设置的值自动调整。Airflow 元数据数据库的 Cloud SQL 实例、Cloud SQL 代理和 Redis 任务队列就是这些组件的例子。
关于 Cloud Composer 2 定价的更详细文档可在这里找到。
图片来源:作者,Cloud Composer 定价
快照作为关闭 Composer 并仍然保留其状态的一种方式
在降低使用 Cloud Composer 的成本方面,尝试的方案并不多。人们通常会尝试调整环境的规模,以避免资源浪费。这意味着根据将要部署在 Composer 环境上的实际工作负载,使用最少数量的 GKE 节点和最小可能的 Cloud SQL 实例。
坦率地说,这比说起来容易。凭借其自动扩缩功能,Cloud Composer 2 通过使 Airflow Workers 数量在环境需求不大时自动减少到 1,从而简化了我们的生活。然而,不支持缩减到零,并且没有办法停止或禁用 Cloud Composer 环境。
这对许多 Cloud Composer 用户来说是一个严重的痛点,他们可能会觉得在 Cloud Composer 上花费的费用超过了他们实际获得的好处。实际上,在大多数情况下,开发和测试 Composer 环境不需要在夜间和周末保持开启。我所说的是,保持非生产环境全天候运行是不经济的,但由于缺乏原生的启动和停止功能,Cloud Composer 用户最终会这样做。
Google 于 2022 年 4 月推出了环境快照作为预览功能,并于 2022 年 12 月正式推出。顾名思义,环境快照会创建 Cloud Composer 环境的快照,这些快照可以加载以将环境恢复到创建快照时的状态。
使用此功能,可以模拟启动与停止功能,因为环境可以被销毁并重建而不会丢失其状态。
注意: 请注意,Cloud Composer 快照不会保留 Airflow 任务日志
以下是如何在非生产环境中大幅削减 Cloud Composer 费用的 3 步秘诀:
-
创建环境并加载最新的快照(首次创建环境时将没有快照可加载)
-
在环境上执行所需的任何更新
-
保存快照并销毁环境,当你不再需要它时
在专业环境中,上述步骤将通过 CICD 管道执行,这正是接下来部分将涵盖的内容。
图片由作者提供,Cloud Composer 的启动与停止功能通过快照实现
使用快照创建 Composer 环境
假设我们想要每天(周末除外)在早上 7 点创建 Composer 开发环境,并在每天晚上 9 点销毁它。我们需要按照以下步骤进行:
-
创建 Cloud Storage 备份桶和环境服务帐户
-
在 Cloud Source Repositories 中创建一个仓库来存放环境创建管道
-
配置一个 Cloud Build 触发器来运行环境创建管道
注意: 在继续之前,你需要安装 gcloud。如果没有,请参考gcloud 安装指南
步骤 1:创建 Cloud Storage 备份桶和环境服务帐户
你在问为什么吗?Cloud Storage 桶将存储环境快照和任务日志,而这些日志不会作为快照的一部分保存
gsutil mb gs://<PROJECT_ID>-europe-west1-backup
至于环境服务帐户,使用具有最小权限的用户创建的服务帐户是一个良好的实践,遵循所谓的最小权限原则。该帐户将被授予 Composer Worker 角色。
# Enable the Composer Service
gcloud services enable composer.googleapis.com
# Create the Environment service account. Name it "sac-cmp"
gcloud iam service-accounts create sac-cmp
# Add the role Composer Worker to the sac-cmp service account
gcloud projects add-iam-policy-binding <PROJECT_ID> \
--member serviceAccount:sac-cmp@<PROJECT_ID>.iam.gserviceaccount.com \
--role roles/composer.worker
# Add the role Composer ServiceAgentV2Ext to the Composer Agent
# Watch out, do not confuse the Project ID with the Project Number
gcloud iam service-accounts add-iam-policy-binding sac-cmp@<PROJECT_ID>.iam.gserviceaccount.com \
--member serviceAccount:service-<PROJECT_NUMBER>@cloudcomposer-accounts.iam.gserviceaccount.com \
--role roles/composer.ServiceAgentV2Ext
注意: 此外,Cloud Composer 服务代理在用户创建的环境服务帐户上被授予 Service Agent V2 Ext 角色
这就是所有前提条件。下一步是在 Cloud Source Repositories 中创建一个仓库来存放环境创建管道。
步骤 2:在 Cloud Source Repositories 中创建一个存放环境创建管道的仓库
让我们绕个弯,介绍一下 Cloud Source Repositories(CSR),这是 Google Cloud 提供的私有 Git 仓库托管服务。实际上,为了运行环境创建 CICD 管道,我们需要创建一个 Cloud Build Trigger,它通过克隆 Git 仓库的内容来工作。Cloud Build 支持许多流行的 Git 仓库托管服务,如 BitBucket、Github 和 Gitlab。为了简化起见,本文使用 CSR 作为 Cloud Build Git 仓库的来源。
在能够在 CSR 内创建任何 Git 仓库之前,必须处理一些先决条件。简而言之,我们需要启用 CSR API 并配置 Git 以便与 CSR 进行交互。
# Enable the CSR API
gcloud services enable sourcerepo.googleapis.com
# Configure Git. Make sure git is installed before
gcloud init && git config --global credential.https://source.developers.google.com.helper gcloud.sh
现在,我们可以继续创建 CSR Git 仓库 reduce_composer_bill,该仓库将包含环境创建 CICD 管道。为此,我们需要访问 source.cloud.google.com,点击 开始使用,然后点击 创建仓库 按钮。
作者提供的图片,创建 CSR 仓库
然后选择 创建新仓库 并点击 继续
作者提供的图片,创建 CSR 仓库
然后将仓库命名为 reduce_composer_bill 并选择你希望创建 CSR Git 仓库的 GCP 项目。
警告:请不要使用 composer-environment-mgmt,因为这对你不起作用。使用你自己的 GCP 项目。
作者提供的图片,创建 CSR 仓库
接下来的步骤是将这个 Gitlab 仓库 克隆到你的计算机上,并将其内容推送到 CSR 仓库 reduce_composer_bill。在推送到 CSR 仓库之前,编辑 3 个文件 create_environment.yaml、destroy_environment.yaml 和 update_environment.yaml,将 PROJECT_ID 和 ENV_NAME 变量分别替换为 GCP 项目名称和你希望给 Composer 环境起的名称。
注意:变量 PROJECT_ID 和 ENV_NAME 可能在 3 个文件中出现多次。
# Clone the Git repository
git clone git@gitlab.com:marcdjoh/reduce_composer_bill.git
# Push the edited files into the CSR repository reduce_composer_bill
# To do that, follow the instructions in the CSR console
步骤 3:配置云构建触发器以运行环境创建管道
Cloud Build 是 Google Cloud 提供的持续集成和持续部署(CICD)服务。环境创建管道完成 3 件事:
-
它创建一个环境
-
它加载最新的快照(如果有的话)
-
它恢复环境任务的日志
steps:
- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Create environment'
args:
- -c
- |
set -e
# This is an example project_id and env_name. Use your own
project_id=reduce-composer-bill
env_name=my-basic-environment
gcloud composer environments create ${env_name} --location europe-west1 \
--project ${project_id} --image-version=composer-2.1.10-airflow-2.4.3 \
--service-account sac-cmp@${project_id}.iam.gserviceaccount.com
- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Load Snapshot'
args:
- -c
- |
set -e
# This is an example project_id and env_name. Use your own
project_id=reduce-composer-bill
env_name=my-basic-environment
if gsutil ls gs://${project_id}-europe-west1-backup/snapshots/* ; then
snap_folder=$(gsutil ls gs://${project_id}-europe-west1-backup/snapshots)
gcloud composer environments snapshots load ${env_name} --project ${project_id} \
--location europe-west1 \
--snapshot-path ${snap_folder}
else
echo "There is no snapshot to load"
fi
- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Restore Tasks Logs'
args:
- -c
- |
set -e
# This is an example project_id and env_name. Use your own
project_id=reduce-composer-bill
env_name=my-basic-environment
if gsutil ls gs://${project_id}-europe-west1-backup/tasks-logs/* ; then
dags_folder=$(gcloud composer environments describe ${env_name} --project ${project_id} \
--location europe-west1 --format="get(config.dagGcsPrefix)")
logs_folder=$(echo $dags_folder | cut -d / -f-3)/logs
gsutil -m cp -r gs://${project_id}-europe-west1-backup/tasks-logs/* ${logs_folder}/
else
echo "There is no task logs to restore"
fi
构建由 Cloud Build 服务帐户触发。因此,我们将 Project Editor 角色添加到 Cloud Build 服务帐户,以便它可以创建、(也可以销毁和更新)Composer 环境,并将文件复制到 Cloud Storage 存储桶中。最后,我们创建了一个 Cloud Build 触发器来运行环境创建管道。
# Add the project editor role to the Cloud Build service account
gcloud projects add-iam-policy-binding <PROJECT_ID> \
--member serviceAccount:<PROJECT_NUMBER>@cloudbuild.gserviceaccount.com \
--role roles/editor
# Create a Cloud Build trigger for the Environment creation CICD pipeline
gcloud builds triggers create manual --name trg-environment-creator \
--build-config create_environment.yaml --repo reduce_composer_bill \
--branch main --repo-type CLOUD_SOURCE_REPOSITORIES
总结
本文是一个两部分系列的第一部分,旨在详细讲解一种高效的方式,以减少所有 Cloud Composer 用户的费用。该策略主要依靠保存和加载环境快照,以便在关闭非生产环境时不会丢失其状态。
CICD 管道代码可在此 Gitlab repository 中找到。请随意查看。此外,系列的第二部分可以在这里查阅。
感谢您的时间,敬请关注更多内容。