2025-01-19 17:27:50
AI Agent 当前还是一个比较新兴的基于 LLM 的概念,Google 最近发布了关于它的白皮书(Agents | Kaggle),最近我看到一个说法:「AI 是风口,AI Agent则是风口的平方」,所以我打算结构性的梳理一下 AI Agent 的概念。
我个人粗浅地将 AI Agent 分为两个层次 Generative AI Agent 和 Agentic AI:
由于 AI Agent 发展还在早期,Agentic AI 目前还是大家对 AI Agent 终态的美好想象,所以本文主要就先梳理清楚 Generative AI Agent,后面提到的 AI Agent 均指 Generative AI Agent。
为了理解 AI Agent 的内部工作原理,我们首先介绍 AI Agent 运行和决策的基础组件。这些组件的组合可以描述为认知架构(cognitive architecture),并且可以通过混合和匹配这些组件来实现许多这样的架构。专注于核心功能,Agent 的认知架构中有三个基本组件,如下图所示。
了解完 AI Agent 的技术架构之后,其实会有一个疑惑:「从架构来看 AI Agent 就是在 LLM 的基础上增加了与外部交互的工作流,为什么它会被提到如此高的地位?」。
对于这个疑问我想先对比一下 workflows 和 agents:
Agent 与 Workflow 最大的区别就是 Agent 可以使用认知架构(Cognitive Architecture)来实现其最终目标,方法是迭代处理信息、做出明智的决策并根据先前的输出完善下一步行动。Agent 认知架构的核心是编排层,负责维护记忆、状态、推理和规划。它使用快速发展的即时工程领域和相关框架来指导推理和规划,使 Agent 能够更有效地与其环境交互并完成任务,例如使用 LLM 的 ReAct、CoT、ToT 等推理技术来根据任务目标选择行为和工具。
对程序员而言什么时候需要用到 AI Agent 呢?
在使用 LLMs 构建应用程序时,Claude 建议寻找最简单的解决方案,并且只在必要时增加复杂性。这可能意味着根本不需要构建 agentic system,agentic system 通常会在延迟和成本上做出妥协以换取更好的任务表现,您应该考虑这种权衡是否有意义。
当需要更多复杂性时,workflow 为明确定义的任务提供可预测性和一致性,而 agent 则是在需要大规模灵活性和模型驱动决策时的更好选择。然而,对于许多应用程序来说,通过检索和上下文示例优化单个 LLM 调用通常就足够了。
虽然语言模型 LLM 擅长处理信息,但它们缺乏直接感知和影响现实世界的能力,这限制了它们在需要与外部系统或数据交互的情况下的实用性。从某种意义上说,语言模型 LLM 的好坏取决于它从训练数据中学到的东西,但无论我们向模型投入多少数据,它们仍然缺乏与外界交互的基本能力,如何实现 tools 才能使我们的模型能够与外部系统进行实时、上下文感知的交互就是 AI Agent 需要做的第一件事情。
Google AI Agent 白皮书发布的时候,在技术架构中解决与外部系统交互和感知的方法包括:
理解 AI Agent Extensions 的最简单方法是将其视为以标准化方式弥合 API 和 Agent 之间的差距,从而允许 Agent 无缝执行 API,而不管其底层实现如何。Extensions 通过以下方式弥合 AI Agent 与 API 之间的差距:
Extension 示例:
import vertexai
import pprint
PROJECT_ID = "YOUR_PROJECT_ID"
REGION = "us-central1"
vertexai.init(project=PROJECT_ID, location=REGION)
from vertexai.preview.extensions import Extension
extension_code_interpreter = Extension.from_hub("code_interpreter")
CODE_QUERY = """Write a python method to invert a binary tree in O(n) time."""
response = extension_code_interpreter.execute(
operation_id = "generate_and_execute",
operation_params = {"query": CODE_QUERY})
print("Generated Code:")
pprint.pprint({response['generated_code']})
# The above snippet will generate the following code.
# Generated Code:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def invert_binary_tree(root):
"""
Inverts a binary tree.
Args:
root: The root of the binary tree.
Returns:
The root of the inverted binary tree.
"""
if not root:
return None
# Swap the left and right children recursively
root.left, root.right = invert_binary_tree(root.right), invert_binary_tree(root.left)
return root
# Example usage:
# Construct a sample binary tree
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
root.right.left = TreeNode(6)
root.right.right = TreeNode(9)
# Invert the binary tree
inverted_root = invert_binary_tree(root)
Functions 就是被预先定义好的独立的代码模块,可以完成特定任务,并可根据需要重复使用。LLM 可以采用一组已知的 Functions,并根据其规范决定何时使用每个函数以及函数需要哪些参数。
Functions 与 Extensions 比较类似,但也有几个不同之处:
开发人员选择使用函数而不是扩展的原因有很多,但一些常见的用例是:
Function 示例:
# function tool
def display_cities(cities: list[str], preferences: Optional[str] = None):
"""Provides a list of cities based on the user's search query and preferences.
Args:
preferences (str): The user's preferences for the search, like skiing,
beach, restaurants, bbq, etc.
cities (list[str]): The list of cities being recommended to the user.
Returns:
list[str]: The list of cities being recommended to the user.
"""
return cities
# example calling function in Agent
from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration
model = GenerativeModel("gemini-1.5-flash-001")
display_cities_function = FunctionDeclaration.from_func(display_cities)
tool = Tool(function_declarations=[display_cities_function])
message = "I’d like to take a ski trip with my family but I’m not sure where
to go."
res = model.generate_content(message, tools=[tool])
print(f"Function Name: {res.candidates[0].content.parts[0].function_call.name}")
print(f"Function Args: {res.candidates[0].content.parts[0].function_call.args}")
> Function Name: display_cities
> Function Args: {'preferences': 'skiing', 'cities': ['Aspen', 'Vail',
'Park City']}
Data Store 允许开发人员以原始格式向 Agent 提供额外数据,从而无需进行耗时的数据转换、模型再训练或微调。Data Store 将传入的文档转换为一组矢量数据库嵌入,Agent 可以使用这些嵌入来提取所需的信息,以补充其下一步操作或对用户的响应。
在 AI Agent 的上下文中,Data Store 通常实现为向量数据库,开发人员希望 Agent 在运行时能够访问该数据库。虽然我们不会在这里深入介绍向量数据库,但要理解的关键点是它们以向量嵌入的形式存储数据,这是一种高维向量或所提供数据的数学表示。近年来,数据存储在语言模型中的最常见使用示例之一是实现基于检索增强生成 (RAG) 的应用程序。这些应用程序试图通过让模型访问各种格式的数据来扩展模型知识的广度和深度,使其超越基础训练数据:
随着 AI Agent 的迭代优化,基于 Data Store 的 RAG 技术也在更新,可以简单的看一下传统 RAG 与 Agentic RAG 的对比(后面再写专题细细研究):
2025-01-11 23:18:59
偶然间从自习室 STUDY ROOM 的一期播客中知道了一本书《异类》(英文名是 Outlier),作者是马尔科姆・格拉德威尔,作者作为播客界的绝对的 OG,他是懂得怎么讲故事的,虽然源自这本书的「一万小时定律」备受争议,但是不可忽视的是这本书用大量的故事为读者展现了努力、天赋与成功之间的关系。
24年初的时候我也思考过关于努力和天赋的问题,今天对于这个问题有了一些更新。
祛魅 disenchantment,源自马克斯·韦伯借用弗里德里希·席勒的理论来描述现代化、官僚化的世俗西方社会,在这种社会中自然理解能力比信仰更有价值、过程都以合理的目标为方向,与韦伯所说的“这个世界上遗留着好的迷人的花园”的传统社会相对立。
“祛魅”和“复魅”的概念,在西方后现代哲学著作中使用的比较多,但学术界没有公认准确的定义。“祛魅”有時也稱為“除魅”,是指剥去附着在事物表面上的那层虚假的东西,即“魅”;“复魅”有时也译为“返魅”,是主张返回事物的自然状态,恢复事物的本来面貌。
当学习一物时,亦即看清一物时,我们能得到它的「真理」。此真理不是前设的,不是带有预先立场的。海德格尔认为,真理的意义为「去蔽」。真理的希腊文是 λθεια (aletheia),字是希腊文中的否定前缀(如同英文中的 un, dis, in, im),∧θη 是遮蔽,那就是「去除遮蔽」的意思。那就是说,真理的意义就是扫开遮蔽,让呈现之物被看见。
由柏拉图开始,每当哲学家思考他物时,都是由思考者主导,因而会先对真理作出定义,再按它来规定自己的思考。当哲学家定义真理时,亦同时限制了、规定了真理的意义。海德格尔称这种来自为人主导的哲学为人本主义(humanism),而这是他所反对的。他认为,真理不由人规定,只有当人不再固执,而是如其所是的去接受一物,如其所是的去描述一物,让它自然而然地呈现它自己,它就呈现出它的真理。
可以如此理解:我们理解事物时会带有很多预设,这些预设来自不同地方,例如文化、社会、科学等,这些预设影响我们对这些事物的了解,如果要看到事物本物,就必须将这些遮蔽扫除,这就是「去蔽」的意思。
天才并非一开始就表现出众,一开始他只是比别人优秀一点点!大多是马太效应作用下,逐步放大那一点点的优势,紧随着优劣分化后的资源倾斜和正反馈,优秀者只会越来越优秀。
顶级加拿大冰球运动员的生日集中在1-3月,因为加拿大的冰球选拔是从9-10岁开始,优秀的选手会组成巡回赛小组,拥有更好的教练,队友,更多的比赛数量。而体育运动,众所周知,在青少年时期的体育竞技,身体发育情况是决定性因素。同年龄的孩子,越早出生越有优势。加拿大冰球的选拔周期是1月1日,从上一年1月1日到12月31日的孩子分在同一组,那么1-3月的孩子身体发育情况好,更容易在起步阶段进入巡回赛小组,并一直优秀下去,成为顶级运动员。
人对数字的记忆周期是2秒钟,中文的数字发音非常简洁,数学系统描述数字也非常直观。比如中文对数字的发音都是单音节,非常好记忆,而且大于10的数字规律很直观,比如11就是10和1,但是英文则是 eleven,这样特殊的单词,没有规律。这导致4岁的中国孩子平均可以数到40,而同龄的美国孩子则只能数到15,大多数的美国孩子即使到了5岁也不能数到50。
在电视真人秀节目《以一敌百》中曾说道爱因斯坦的智商是150,而兰根的智商是195。高智商的兰根由于母亲的疏忽错过了奖学金申请,无法和校方沟通把上午的课换到上午,和不能融入学校生活等原因,只上了一半的大学,没有取得学位就离开学校了。他在工作的期间,深入阅读哲学、数学和物理学著作,他草草完成了自己的论文,构建大学认知的理论模型理论。但由于没有大学的支持,他的论文从来没有任何一家学生杂志社发表。
瑞文智商测试的实验告诉我们,如果一个人的智商达到了120左右,那么再增加智商并不会在现实世界给他带来明显的优势,其实你不需要最好,只需要足够好就可以了。很多在我们看来是个人原因的事情,其实背后有非常复杂的社会家庭原因,不要以为自己只是个人,你是整个文化血脉的一部分,我们所谓天才只是一切结果的初始「动量」。
Innate ability does not exist and ability is actually a function of effort expended.
努力与成功不是互为充要条件的,但是努力是一定会有回报的,但不一定是心想事成。不管发生什么,都要努力,不以成功为目标,就是要努力,因为很多时候成功和幸运看似偶然,但当幸福来敲门时,你也得努力保证自己站在门后面,不是吗!? 努力是因为我想努力,不能有悲壮感,不能去苛求努力就一定会成功!
李诞说过一句话「你是你的目的。你25岁那年有个耗尽全力去解决没有解决掉的事,你觉得五年之后还会记得吗?但是你五年之后是不是还跟25岁的自己一样呢,这个才是重要的」。努力是非常对的,这个事做好了,做对了,做错了不是很重要,关键是你怎么理解这件事情,你怎么升级自己对做事情的认知,你还会不会在同样的事情上跌倒第二次。错不可怕,你知道错在哪里就好了。至于没有结果的事,它肯定是有结果的,只是不一定是你想要的结果。从成功的角度来说,这未必是坏事,当年看起来完全是做错了的事情,后面也可能会有一些意想不到的报偿。
我深怕自己本非美玉,故而不敢加以刻苦琢磨,却又半信自己是块美玉,故又不肯庸庸碌碌,与瓦砾为伍。于是我渐渐地脱离凡尘,疏远世人,结果便是一任愤懑与羞恨日益助长内心那怯弱的自尊心。其实,任何⼈都是驯兽师,而那野兽,无非就是各人的性情而已。——《山月记》中岛敦
通过上面的讨论其实可以看出来,成功是有迹可循的,但绝对不是「基因突变」式的天赋直接带来的,成功虽然是玄学,但努力一定是它的必须项。刘嘉玲说过:努力的人不一定成功,但成功的人一定很努力。
当前下行周期的背景下很多人都选择躺平,躺平只是因为看不见努力的路,如果能看见确定性的期待的未来,没有任何人会躺平,人生充满激情与价值感,是基因就带的编码。所以,战略性后退(躺平)与努力并不冲突,我们只是在为下一次积攒能力。
埋头以一种近乎于天真跟愚蠢的方式去努力,也许才是成功的要素。
2024-12-31 23:54:39
陆陆续续一年一度的年终总结开始了,今年看到有不少人在揶揄所谓的年终总结:「总结毫无意义,没完成的事忘记吧,完成的你已经收益」,「平时养成日记或者笔记的习惯,要什么总结」。不过,我还是觉得自己在写年终总结的时候,实际是在与这一年的自己进行对话,除了简单的清点与记录,更是一种自我认知的形式,不仅帮助我反思事情本身,而且会帮我看到自己的变化与成长,说到底年终总结的本质是我自己对抗时间无常的一种手段。
今年还是老规矩依旧通过 Kepano 的40个问题进行总结回顾,另外今年的年终总结我还想做一点变化,因为「其实读者们并不关心我的2024是什么样的,或者说我的2024对读者而言无非是茶余饭后的消遣没有什么价值」,于是我想着在自我总结和反思之前,先列一个2024年度清单,把我觉得所有2024年碰到值得列出来的东西都列举上,希望这个2024年度清楚对读者们有用。最后,如果你对2024的40个问题部分感兴趣或者有共情,欢迎你也跟我讲讲你的故事和思考。
2024-12-16 22:30:42
我在 Obsidian 中管理 Newsletter 特别是文章发布之前经常需要花很长的时间在网络上搜索与文章主题匹配的图片作为文章的 featured image,同时还需要考虑版权、匹配度等等问题,如果 AI 能自动帮我完成这件事情那就太好了,顺着这个思路我发现 wordpress 已经支持类似的功能了,唯一的缺点就是得花钱🤪。于是我就想着在 personal assistant 插件中支持这个功能,这篇文章就介绍一下我是怎么在 Obsidian 插件中设计和构建这个能力的。
基于众所周知的原因,ChatGPT、Claude、Stable Diffusion、DALL-E 这些最优秀的 AI 服务,在国内基本上是没发正常访问的,所以目标对象肯定是国内可用的大模型服务,同一个大模型服务既要支持文本生成,又要支持图片生成,通义千问服务可能唯一能满足我要求的了。我想这在公有云领域,这就是所谓的「Vendor Lock-in」。
personal assistant 基于通义千问大模型的 featured image 生成的工作流程如下:
使用 AI 生成 featured image 的步骤:
效果如下所示:
2024-12-08 19:39:43
作为人类,我们可以阅读并理解文本(至少可以理解一部分),然而计算机“用数字思考”,因此它们无法自动理解单词和句子的含义。如果我们要让计算机理解自然语言,就需要将这些信息转换成计算机可以处理的格式——数字向量。这篇文章就研究一下科学家是如何一步一步让计算机理解和认识人类的语言的。
人们早在多年前就学会了如何将文本转换为机器可理解的格式(最早的版本之一是ASCII)。这种方法有助于渲染和传输文本,但并不编码单词的意义。当时,标准的搜索技术是关键词搜索,即仅查找包含特定单词或N元语法的所有文档。
然后,在几十年后,嵌入式表示(embeddings)出现了。我们可以计算单词、句子甚至图像的嵌入式表示。嵌入式表示也是数字向量,但它们能够捕捉意义。因此,你可以使用它们进行语义搜索,甚至处理不同语言的文档。
将文本转换为向量的最基本方法是词袋模型。让我们以理查德·P·费曼的一句名言为例:“我们很幸运生活在一个我们仍在不断发现的时代”。我们将使用这句话来说明词袋模型的方法。
获得词袋向量的第一步是将文本拆分为单词(标记),然后将单词还原为其基本形式。例如,“running”将转换为“run”。这个过程称为词干提取(stemming,即将文本拆分为单词并进行词干提取)。
如下代码所示,用 NLTK 实现词袋模型:
from nltk.stem import SnowballStemmer
from nltk.tokenize import word_tokenize
import nltk
import os
# Resource punkt_tab not found. Try using the NLTK Downloader
if not os.path.exists("./tokenizers/punkt_tab.zip"):
nltk.download("punkt_tab", download_dir="./")
text = "We are lucky to live in an age in which we are still making discoveries"
# tokenization - splitting text into words
words = word_tokenize(text)
print(words)
# ['We', 'are', 'lucky', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
# 'we', 'are', 'still', 'making', 'discoveries']
stemmer = SnowballStemmer(language="english")
stemmed_words = list(map(lambda x: stemmer.stem(x), words))
print(stemmed_words)
# ['we', 'are', 'lucki', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
# 'we', 'are', 'still', 'make', 'discoveri']
import collections
bag_of_words = collections.Counter(stemmed_words)
print(bag_of_words)
# {'we': 2, 'are': 2, 'in': 2, 'lucki': 1, 'to': 1, 'live': 1,
# 'an': 1, 'age': 1, 'which': 1, 'still': 1, 'make': 1, 'discoveri': 1}
实际上,如果我们想将文本转换为向量,不仅要考虑文本中的单词,还要考虑整个词汇表。假设我们的词汇表中还包括“i”、“you”和“study”,我们就可以从费曼的这句名言创建一个向量。
# bag of words vector
{'we': 2, 'are': 2, 'in': 2, 'lucki': 1, 'to': 1, 'live': 1,'an': 1, 'age': 1, 'which': 1, 'still': 1, 'make': 1, 'discoveri': 1, 'i': 0, 'you': 0, 'study': 0}
这种方法相当基础,它不考虑单词的语义意义,因此句子“the girl is studying data science”和“the young woman is learning AI and ML”在向量空间中不会接近彼此。
词袋模型的一个稍作改进的版本是TF-IDF(Term Frequency — Inverse Document Frequency,词频-逆文档频率)。它是两个度量值的乘积。
$$TF-IDF(t,d,D) = TF(t,d) \times IDF(t, D) $$
最终,我们会得到一些向量,其中常见的词(如“I”或“you”)具有较低的权重,而多次出现在文档中的罕见词则具有较高的权重。这种策略会带来稍微更好的结果,但它仍然无法捕捉语义意义。
这种方法的另一个挑战是它会产生非常稀疏的向量。向量的长度等于语料库的大小。英语中大约有47万独特的单词(来源),因此我们将得到巨大的向量。由于一个句子通常不会超过50个不同的词,因此向量中99.99%的值将是0,不携带任何信息。鉴于这一点,科学家们开始思考密集向量表示。
词语潜在信息(Latent Information)与神经网络方法(Neural Network Method)
英文语境下,当我们谈论狗(dog)时,可能会使用除了“dog”之外的其他词语。我们是否应该在我们的分类方案中考虑像“canine”(犬类)或“feline”(猫科)这样的术语?我们需要为这些词添加一个新的项,例如 (dogx, caty, caninez, felinei)。
在英语中,一个大约30,000个词的词汇表对于这种词袋模型来说效果很好。在计算机世界中,我们可以比实体图书馆更平滑地扩展这些维度,但原则上问题类似。在高维度下,事情变得难以管理。随着组合爆炸,算法运行缓慢,稀疏性(大多数文档对大多数术语的计数为0)对统计和机器学习构成了问题。
因此,要将一本书投影到潜在空间(latent space)中,我们需要一个大矩阵,定义词汇表中每个观察到的术语对每个潜在术语的贡献程度。
有几个不同的算法可以从足够大的文档集合中推断出这一点:潜在语义分析(Latent Semantic Analysis, LSA),它使用术语-文档矩阵的奇异值分解(基本上是高级线性代数),以及潜在狄利克雷分配(Latent Dirichlet Allocation, LDA),它使用一种称为狄利克雷过程的统计方法。
我们使用词频作为某种更为模糊的主题性的代理。通过将这些词频投影到嵌入空间中,我们不仅可以降低维度,还可以推断出比原始词频更好地指示主题性的潜在变量。为此,我们需要一个定义良好的算法,如LSA,它可以处理文档语料库,找到从词袋输入到嵌入空间向量的良好映射。基于神经网络的方法使我们能够推广这一过程,并突破LSA的限制。
最著名的密集表示方法之一是word2vec,这是谷歌在2013年提出的,论文标题为“Efficient Estimation of Word Representations in Vector Space”,作者是Mikolov等人。
在这篇论文中提到了两种不同的word2vec方法:
这两种方法都能生成词的密集向量表示,这些向量不仅包含了词的频率信息,还捕捉了词之间的语义关系。例如,使用word2vec生成的向量可以反映出“king”和“queen”之间的关系类似于“man”和“woman”之间的关系。
word2vec的成功在于它能够有效地将高维的词袋模型转化为低维的密集向量,同时保留了词的语义信息。这使得在自然语言处理任务中,如语义搜索、情感分析和机器翻译等,可以更高效地使用这些向量。
高维词向量的密集表示的核心思想是训练两个模型:编码器(encoder)和解码器(decoder)。以skip-gram模型为例,我们可以将单词“christmas”传递给编码器。编码器会生成一个向量,然后将这个向量传递给解码器,期望解码器能够输出“merry”、“to”和“you”等上下文单词。
具体步骤如下:
训练过程:
通过这种方式,skip-gram模型能够生成高质量的词嵌入,这些嵌入不仅保留了词的频率信息,还捕捉了词之间的语义关系,为各种自然语言处理任务提供了强大的支持。这个模型开始考虑单词的意义,因为它是在单词的上下文中进行训练的。然而,它忽略了形态学(我们从单词部分可以获得的信息,例如,“-less”意味着缺乏某物)。这一缺点后来通过查看子词skip-grams在GloVe中得到了解决。
此外,word2vec只能处理单词,但我们希望对整个句子进行编码。因此,让我们进入下一个演进步骤,即transformers。
下一次演变与 Vaswani 等人在论文“Attention is All You Need”中引入的变压器方法有关。变压器能够生成信息丰富的密集向量,并成为现代语言模型的主要技术。
变压器允许使用相同的“核心”模型,并针对不同的用例进行微调,而无需重新训练核心模型(这需要大量时间和成本)。这导致了预训练模型的兴起。其中一个早期流行的模型是Google AI的BERT(基于变压器的双向编码器表示)。
内部而言,BERT仍然像word2vec一样在词元级别上操作,但我们仍然希望获得句子嵌入。因此,一个简单的方法可能是取所有词元向量的平均值。不幸的是,这种方法的表现不佳。
这个问题在2019年随着Sentence-BERT的发布得到了解决。Sentence-BERT在语义文本相似性任务上超越了所有先前的方法,并允许计算句子嵌入。
我使用阿里云大模型服务的text-embedding-v2来生成文本嵌入向量。结果,我们得到了一个1536维的浮点数向量。现在我们可以对所有数据重复这一过程,并开始分析这些值。
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv("./env/.env"))
import dashscope
from http import HTTPStatus
from pprint import pprint
resp = dashscope.TextEmbedding.call(
model=dashscope.TextEmbedding.Models.text_embedding_v2,
input="We are lucky to live in an age in which we are still making discoveries.",
dimension=1536,
)
pprint(resp['output']) if resp.status_code == HTTPStatus.OK else print(resp)
# output
# {'embeddings': [{'embedding': [0.022378576171554372,
# -0.027432455162420308,
# -0.00355793080956962,
# -0.030121118785560987,
# ...
# ],
# 'text_index': 0}]}
嵌入实际上是向量。因此,如果我们想了解两个句子之间的相似程度,可以计算它们之间向量的距离。距离越小,表示它们的语义意义越接近。
可以使用不同的度量来测量两个向量之间的距离:
定义两点(或向量)之间距离的最标准方法是欧几里得距离或L2范数。这种度量在日常生活中最常用,例如,当我们谈论两个城镇之间的距离时。
以下是L2距离的视觉表示和公式:
$$\text{L2距离} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2 + \cdots + (z_2 - z_1)^2}$$
另一种常用的距离是L1范数或曼哈顿距离。这种距离是以纽约的曼哈顿岛命名的。该岛的街道布局呈网格状,两个点之间的最短路径将是L1距离,因为需要沿着网格行走。
$$\text{L1距离} = \sum_{i=1}^{n} |x_i - y_i|$$
另一种查看向量之间距离的方法是计算点积或标量积。以下是公式,我们可以轻松实现它:
$$\text{点积} = \vec{a} \cdot \vec{b} = |\vec{a}||\vec{b}|cos\theta= \sum_{i=1}^{n} a_i b_i$$
这种度量的解释有点棘手。一方面,它显示了向量是否指向同一方向。另一方面,结果高度依赖于向量的大小。例如,让我们计算两对向量之间的点积:
在这两种情况下,向量都是共线的,但在第二种情况下,点积大十倍:2 对 20。
余弦相似度经常被使用。余弦相似度是点积除以向量的模长(或范数)的归一化结果。
让我们谈谈这种度量的物理意义。余弦相似度等于两个向量之间的夹角的余弦值。向量越接近,度量值越高。
$$\text{余弦相似度} = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}$$
你可以使用任何距离来比较你得到的嵌入。例如,我计算了不同聚类之间的平均距离。无论是L2距离还是余弦相似度,都展示了类似的结果:
然而,对于NLP任务,最佳实践通常是使用余弦相似度。背后的原因包括:
理解数据的最佳方式是将其可视化。不幸的是,嵌入有1536个维度,因此直接查看数据非常困难。然而,有一种方法:我们可以使用降维技术将向量投影到二维空间中。
注意: 以下可视化数据来自 Stack Exchange Data Dump,由于数据量是92.3G,我就没有自己本地跑可视化结果,本文可视化视图来自 Mariya Mansurova 的Text Embeddings: Comprehensive Guide。
最基本的降维技术是主成分分析(PCA)。
PCA是一种线性算法,而在现实生活中大多数关系是非线性的。因此,由于非线性问题,我们可能无法很好地分离聚类。让我们尝试使用一种非线性算法——t-SNE,看看它是否能显示出更好的结果。
来源内容:
Is it safe to drink the water from the fountains found all over
the older parts of Rome?
When I visited Rome and walked around the older sections, I saw many
different types of fountains that were constantly running with water.
Some went into the ground, some collected in basins, etc.
Is the water coming out of these fountains potable? Safe for visitors
to drink from? Any etiquette regarding their use that a visitor
should know about?
t-SNE嵌入:
我们可以在t-SNE可视化中找到这条内容,并发现它实际上靠近咖啡聚类。
这句话的意思是,在t-SNE可视化中,这条关于罗马喷泉水的问题被映射到了一个与咖啡相关的话题附近。这可能表明,尽管这两者看似不相关,但它们在某些方面存在一定的语义相似性,或者在数据集中它们经常一起出现。
2024-11-30 23:16:35
Vector embeddings are a method to convert non-structured data, such as text, images, and videos, into numerical representations that capture their meanings and relationships. This allows computers, which only understand numbers, to process and interpret these data more effectively. Embeddings are crucial for tasks like semantic similarity and are used in various AI models, including LLMs, RAG, and multimodal processing.
vector embeddings 的出现最本质的原因是科学家为了解决一个问题 —— 让只认识0、1数字,只会做逻辑运算和浮点运算的计算机,能够理解人类语言、文字、图像、视频等数据包含的语义以及它们之间的关系。例如,「泰迪」、「狗」、「犬」、「哺乳动物」这几个单词在不同的上下文中其实是同一个意思,即一种四足、有尾巴、有尖牙的哺乳动物,这就是文字、图像、视频中包括的语义信息,科学家希望计算机可以理解这些。
既然计算机只认识数字,只会做运算,科学家的想法很直接,那就我们就将人类使用的文字、句子、文章、书本、图像、视频等等非结构化的数据转换成数字来描述,帮助计算机高效的理解和处理它们,于是就有了 vector embeddings。
vector embeddings(向量嵌入)是一种将单词、句子和其他数据转换为数字的方法,这些数字捕获了它们的含义和关系。它们将不同类型的数据表示为高维空间(多维空间)中的点,其中相似的数据点聚集在一起。这些数值表示有助于机器更有效地理解和处理这些数据。
向量嵌入在处理语义相似度时至关重要。vector(向量)仅仅是一系列数字;vector embedding(向量嵌入)是一系列代表输入数据的数字。通过使用 vector embeddings,我们可以结构化非结构化数据或通过将其转换为一系列数字来处理任何类型的数据。这种方法使我们能够对输入数据执行数学运算,而不是依赖定性比较。
在 vector embeddings (向量嵌入)的上下文中,embedding(嵌入)和 vector(向量)是同一回事。两者都指数据的数值表示,其中每个数据点都由高维空间中的向量表示。
“vector 向量”仅指具有特定维数的数字数组。在 vector embedding(向量嵌入)的情况下,这些向量表示上述任何数据点在一个连续的空间中。
“embedding 嵌入”专门指将数据表示为 vector(向量)的技术,以捕获有意义的信息、语义关系或上下文特征。embedding 嵌入旨在捕获数据的底层结构或属性,通常通过训练算法或模型来学习。
虽然嵌入和向量可以在向量嵌入的上下文中互换使用,“嵌入”强调以有意义和结构化的方式表示数据的概念,而“向量”则指数值表示本身。
vector embeddings 是深度学习模型中输入数据的内部表示,也称为嵌入模型或深度神经网络。那么,我们如何提取这些信息呢?
我们通过移除最后一层并从倒数第二层获取输出,来获得 vector 向量。神经网络的最后一层通常输出模型的预测,因此我们取倒数第二层的输出。vector embedding(向量嵌入)是馈送到神经网络预测层的数据。
vector embedding(向量嵌入)的维数等于模型中倒数第二层的尺寸,因此与向量的尺寸或长度可互换。常见的向量维数包括384(例如 Sentence Transformers Mini-LM 生成的 vector)、768(例如 Sentence Transformers MPNet 生成的 vector)、1,536(例如 OpenAI 生成的 vector)和2,048(例如 ResNet-50 生成的 vector)。
最早的 GPT-2 设置中,头的数量是 12(dimension heads),它可以整除 768。
这个数字来自于超参数优化(hyperparameter optimization)。使用 4096 大小的嵌入和 1 层的神经网络,或者使用 16 大小的嵌入和 2B 参数的神经网络是没有意义的,这些值之间需要一个良好的平衡。
那么为什么是 768 而不是 769 呢?我们通常使用 2 的幂(或接近的值)来尝试超参数,因为它们在计算上更快,并且更适合 GPU 内存分配(就像你的屏幕分辨率一样,GPU 只是一个计算矩阵的大型机器)。768 = 512 + 256 = 2**9 + 2**8
。
我曾经查找资料并试图弄懂 vector embeddings(向量嵌入)中每个维度的含义。最终的答案是,单个维度没有任何意义。vector embeddings 中的单个维度过于抽象,无法确定其含义。然而,当我们将所有维度放在一起时,它们提供了输入数据的语义含义。
向量的维度是不同属性的高级抽象表示。表示的属性取决于训练数据和模型本身。文本和图像模型生成不同的嵌入,因为它们针对根本不同的数据类型进行训练。即使是不同的文本模型也会生成不同的嵌入。有时它们在大小上不同;其他时候,它们在表示的属性上不同。例如,在法律数据上训练的模型将学习与在医疗保健数据上训练的模型不同的事物。
2012 年 AlexNet 的出现标志着图像识别技术的飞跃。自那时以来,计算机视觉领域取得了无数进展。最新的知名图像识别模型是 ResNet-50,它是一个基于前代 ResNet-34 架构的 50 层深度残差网络。
尝试使用 microsoft/resnet-50 · Hugging Face生成图像的 vector embeddings:
# Load model directly
from transformers import AutoFeatureExtractor, AutoModelForImageClassification
from PIL import Image
extractor = AutoFeatureExtractor.from_pretrained("microsoft/resnet-50")
model = AutoModelForImageClassification.from_pretrained("microsoft/resnet-50")
image = Image.open("<image path>")
# image = Resize(size=(256, 256))(image)
inputs = extractor(images=image, return_tensors="pt")
# print(inputs)
outputs = model(**inputs)
vector_embeddings = outputs[1][-1].squeeze()
人工智能对自然语言的处理已经从基于规则的嵌入发展到了一个新的高度。从最初的神经网络开始,我们通过 RNN 添加了递归关系来跟踪时间步长。从那时起,我们使用 Transformer 来解决序列转导问题。
Transformer 由编码器、注意力矩阵和解码器组成。编码器将输入编码为表示状态的矩阵,注意力矩阵和解码器对状态和注意力矩阵进行解码,以预测正确的下一个标记来完成输出序列。GPT 是迄今为止最流行的语言模型,它由严格的解码器组成。它们对输入进行编码并预测正确的下一个标记。
尝试使用 sentence-transformers生成文本的 vector embeddings:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("<model-name>")
vector_embeddings = model.encode(“<input>”)
以图生文为例,使用开源模型 CLIP VIT 来生成 embeddings:
# Load model directly
from transformers import AutoProcessor, AutoModelForZeroShotImageClassification
processor = AutoProcessor.from_pretrained("openai/clip-vit-large-patch14")
model = AutoModelForZeroShotImageClassification.from_pretrained("openai/clip-vit-large-patch14")
from PIL import Image
image = Image.open("<image path>")
# image = Resize(size=(256, 256))(image)
inputs = extractor(images=image, return_tensors="pt")
# print(inputs)
outputs = model(**inputs)
vector_embeddings = outputs[1][-1].squeeze()
以语音生成文字为例,使用开源模型 Whisper 模型来获取 embedding:
import torch
from transformers import AutoFeatureExtractor, WhisperModel
from datasets import load_dataset
model = WhisperModel.from_pretrained("openai/whisper-base")
feature_extractor = AutoFeatureExtractor.from_pretrained("openai/whisper-base")
ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation")
inputs = feature_extractor(ds[0]["audio"]["array"], return_tensors="pt")
input_features = inputs.input_features
decoder_input_ids = torch.tensor([[1, 1]]) * model.config.decoder_start_token_id
vector_embedding = model(input_features, decoder_input_ids=decoder_input_ids).last_hidden_state
视频的 embeddings 比语音、图片的 embeddings 更加复杂,它需要多模态的处理来保证语音与图片的同步,以 DeepMind 开源模型 multimodal perceiver 为例生成视频的 vector embeddings(注意代码 outputs[1][-1].squeeze()
):
def autoencode_video(images, audio):
# only create entire video once as inputs
inputs = {'image': torch.from_numpy(np.moveaxis(images, -1, 2)).float().to(device),
'audio': torch.from_numpy(audio).to(device),
'label': torch.zeros((images.shape[0], 700)).to(device)}
nchunks = 128
reconstruction = {}
for chunk_idx in tqdm(range(nchunks)):
image_chunk_size = np.prod(images.shape[1:-1]) // nchunks
audio_chunk_size = audio.shape[1] // SAMPLES_PER_PATCH // nchunks
subsampling = {
'image': torch.arange(
image_chunk_size * chunk_idx, image_chunk_size * (chunk_idx + 1)),
'audio': torch.arange(
audio_chunk_size * chunk_idx, audio_chunk_size * (chunk_idx + 1)),
'label': None,
}
# forward pass
with torch.no_grad():
outputs = model(inputs=inputs, subsampled_output_points=subsampling)
output = {k:v.cpu() for k,v in outputs.logits.items()}
reconstruction['label'] = output['label']
if 'image' not in reconstruction:
reconstruction['image'] = output['image']
reconstruction['audio'] = output['audio']
else:
reconstruction['image'] = torch.cat(
[reconstruction['image'], output['image']], dim=1)
reconstruction['audio'] = torch.cat(
[reconstruction['audio'], output['audio']], dim=1)
vector_embeddings = outputs[1][-1].squeeze()
# finally, reshape image and audio modalities back to original shape
reconstruction['image'] = torch.reshape(reconstruction['image'], images.shape)
reconstruction['audio'] = torch.reshape(reconstruction['audio'], audio.shape)
return reconstruction
return None