几年前,当我第一次开始使用文本数据时,嵌入的整个概念似乎太复杂了,我对我的单词包方法和简单的 TF-IDF 向量感到舒适,我认为它们是简单的,易于理解,并有效地完成了任务。
直到我在情绪分析项目中遇到一个挑战,我才认真考虑嵌入式产品,我正在处理产品评论,我的传统模型一直在用语或色调的语言错误分类评论,问题变得清晰:我的模型没有理解“这个产品生病了”实际上可能是积极的,或者“正如预期的那样工作”可能取决于背景是中性的或消极的。
那时我发现嵌入式实际上解决了哪些问题,它们不仅仅是一个精彩的新技术 – 它们解决了机器如何理解语言的根本局限性。
The Old Days: Life Before Embeddings
旧时光:嵌入前的生活让我们理解在嵌入之前使用文本表示的历史,而不进入这些方法和方法的细节。
One-Hot Encoding
它代表了每个单词作为一个稀缺的矢量,所有的零,除了一个单一的“1”在词汇中相应的位置。它代表了单词作为巨大的,稀缺的矢量,每一个单词都有自己的尺寸。 如果你的词汇有10万个单词(这是温和的),每个单词矢量有99999个零和一个单一的1。这些表示告诉我们绝对没有任何意义。 单词“优秀”和“奇妙”在数学上与“优秀”和“可怕”一样不同 – 完全缺乏明显的语义关系。
“猫” → [1, 0, 0, 0, ..., 0] (词汇第 5432 号) “狗” → [0, 1, 0, 0, ..., 0] (词汇第 8921 号)
限制
- 是的
- 尺寸爆炸:矢量尺寸与词汇尺寸相同(通常超过10万) 是的
- 没有语义关系:“猫”和“猫”与“猫”和“飞机”一样不同(所有均等距离) 是的
- 计算效率低下:将这些稀缺的矩阵倍增是极其资源密集的 是的
- 没有一般化:系统无法理解原始词汇之外的单词 是的
Bag-of-Words Approach
它计算文档中的单词发生,有时以其重要性为重量,它将文档中的单词视为未分类的单词集,完全抛弃了单词的顺序。
文件:“猫坐在地毯上” BoW: {”the”: 2, “cat”: 1, “sat”: 1, “on”: 1, “mat”: 1}
限制:
- 是的
- “狗咬人”和“人咬狗”有相同的代表性 是的
- Sparse高维矢量:仍然需要词汇大小的矢量 是的
- 没有语义理解:同义词被表示为完全不同的特征 是的
- 没有背景意义:每个单词都有固定的代表,不论其背景如何。 是的
N-grams
为了捕捉某些单词顺序,我们开始使用n-grams(n连续单词的序列)。
使用unigrams(单个单词),你可能拥有10万个词汇库. 使用bigrams(单词对),突然你正在寻找潜在的数百万个特征. 使用trigrams? 理论上,数十亿。
Limitations:
限制:- 是的
- 组合爆炸:可能的n克的数量呈指数增长 是的
- 数据稀缺性:大多数可能的n-grams从未出现在训练数据中 是的
- 有限的背景窗口:只有在小窗口中捕捉到的关系(通常2-5个单词) 是的
TF-IDF (Term Frequency-Inverse Document Frequency)
TF-IDF通过根据它们对特定文档的重要性来衡量字体来改善事物,但它仍然将“惊人的”和“出色的”视为完全无关的术语。
Limitations:
限制:没有语义意义:它是单词的数量和频率决定其使用的重要性。
The Embedding Revolution: What Changed?
嵌入式革命:什么改变了?向嵌入式的过渡不仅仅是一个渐进的改进;它是我们如何代表语言的范式转变。
Meaning Through Context
嵌入背后的基本见解很简单:在类似的背景下出现的单词可能具有相似的含义. 如果你看到“狗”和“猫”围绕相同类型的单词(“宠物”、“食物”、“毛衣”)出现,他们可能在语义上相关。
Word2Vec等早期嵌入式模型通过训练神经网络来预测:
- 是的
- 一个基于其周围的语境的单词(连续的单词袋) 是的
- 基于一个单词的周围背景(Skip-gram) 是的
这些模型中的隐藏层重量成为我们的单词矢量,在矢量空间的几何特性中编码语义关系。
当我第一次编写单词矢量并看到“国王” - “男人” + “女人” ≈ “女王”时,我知道我们正在做一些革命性的事情,这些不仅仅是任意的数字;他们捕捉了有意义的语义关系。
接下来的大跳跃是背景嵌入。 Word2Vec 和 GloVe 等早期模型给了每个单词一个矢量,而不论背景如何,但同一个单词在不同的背景下可能意味着不同的东西:
"I need to bank the money" vs. "I'll meet you by the river bank"
BERT 和 GPT 等模型通过根据其周围的背景来生成相同单词的不同嵌入来解决这一问题,这是用于名称实体识别和情绪分析等任务的游戏变更器,而背景决定意义。
因此,让我们先了解嵌入式是什么,以及它们如何改变NLP,并解决了以前的方法的局限性。
What Are Embeddings?
什么是嵌入式?嵌入式是数据(文本,图像,音频等)在连续的矢量空间中的数值表示,对于文本来说,嵌入式捕捉了单词或文档之间的语义关系,使机器能够以数学上可处理的方式理解含义。
Key Concepts:
- 是的
- 矢量:在多维空间中代表一个点的序列列表 是的
- 维度:每个矢量中的值数(例如,768-dim,1024-dim) 是的
- 矢量空间:嵌入存在的数学空间 是的
- 语义相似性:以矢量之间的距离或角度衡量(更近 =更相似) 是的
What Do Dimensions Represent?
嵌入式矢量中的每个维度代表了数据的学习特征或方面,而不是经典的特征工程,在现代嵌入式模型中,人类定义了每个维度的含义:
- 是的
- 维度在训练过程中出现,以代表抽象的“概念” 是的
- 个体维度往往缺乏具体的人类解释的含义 是的
- 然而,完整的矢量在整体上捕捉了语义信息。 是的
- 某些维度可能与情绪、形式、主题或语法相匹配,但大多数都代表了复杂的特征组合。 是的
Why We Need Embeddings
计算机基本上是用数字工作,而不是单词。在处理语言时,我们需要将文本转换为数字表示,这些表示:
- 捕捉语义关系 - 类似的概念应该有类似的表示 是的
- 保持背景意义 - 同一个词在不同的背景下可能意味着不同的东西 是的
- 允许数学操作 - 如寻找相似之处或执行相似之处 是的
- Work efficiently at scale – Process large volumes of text without computational explosion 是的
嵌入式解决了这些问题,通过将单词,短语或文档作为密集的矢量在一个连续的空间中,其中语义关系被保存为几何关系。
嵌入式基础
密集的矢量代表性
与数千或数百万维度的稀有矢量相反,嵌入式使用几百个密度,每个维度都有意义。
"cat" → [0.2, -0.4, 0.1, -0.8, ..., 0.3] (300 dimensions)
"kitten" → [0.19, -0.38, 0.15, -0.75, ..., 0.29] (similar to "cat")
这使得大小计算顺序更有效率,同时允许更丰富的语义表示。
分布式语义
嵌入式是建立在“你会知道一个单词由它所拥有的公司”的原则上(J.R. Firth)。
例如,“国王”和“女王”将有相似的背景,所以他们将有相似的嵌入,即使他们很少出现在相同的位置。
数学属性
嵌入空间具有惊人的数学特性:
vector("king") - vector("man") + vector("woman") ≈ vector("queen")
这允许模拟推理和语义操作直接在矢量空间。
转移学习
预先训练的嵌入式捕捉了可以针对特定任务精心调整的通用语言知识,大幅减少了新应用所需的数据。
背景理解
现代的语境嵌入(如BERT、GPT等)根据语境不同地代表同一个单词:
"I'll deposit money in the bank" → "bank" relates to finance
"I'll sit by the river bank" → "bank" relates to geography
有了关于嵌入式的历史和理解的所有知识,是时候开始使用它们了。
使用LLM/SLM模型生成嵌入式
各种研究小组已经开发了嵌入式模型,在跨多种语言和领域的各种数据集上进行培训。这种多样性导致具有非常不同的词汇和语义理解能力的模型。例如,主要在英语科学文献上训练的模型将不同于在多语言社交媒体内容上训练的技术概念进行编码。
嵌入的实际实施已被Hugging Face的SentenceTransformer包等库大大简化,该库为使用各种嵌入模型提供全面的SDK。类似地,OpenAI的SDK提供了直接访问他们的嵌入模型,这些工具在许多基准中表现出令人印象深刻的性能。
为了这个文章,模型应该被视为一个黑匣子,它将句子作为输入,并返回相应的矢量表示。
使用 SentenceTransformers 图书馆用于嵌入
使用SentenceTransformer生成嵌入式的最简单方法:
from sentence_transformers import SentenceTransformer
# Load a pre-trained model
model = SentenceTransformer('all-MiniLM-L6-v2') # 384 dimensions
# Generate embeddings
texts = ["This is an example sentence", "Each sentence becomes a vector"]
embeddings = model.encode(texts)
print(f"Shape: {embeddings.shape}") # (2, 384)
max_seq_length = model.tokenizer.model_max_length
print(max_seq_length) # 256
HuggingFace 提供的 “all-MiniLM-L6-v2” 模型有 384 个维度,这意味着它可以捕捉给定单词或句子的 384 个特征或色调。 该模型的序列长度为 256 个代币。 代码器在嵌入过程中将句子分解为单词和单词。 一句子所产生的代币数量通常比句子中的单词数量高出 25% 到 40%。
序列长度表示模型可以作为给定的输入处理的代币数量. 任何较少的东西都被折叠,使其长度为256,而任何其他东西都被丢弃。
SentenceTransformer 类的编码方法是使用模型的 PyTorch 推理模式的包装,上面列的代码类似于 PyTorch 代码:
from sentence_transformers import SentenceTransformer
import torch
# Load the model directly with SentenceTransformer
model = SentenceTransformer("sentence-transformers/msmarco-distilbert-base-tas-b")
# Input text
texts = ["This is an example sentence", "Each sentence becomes a vector"]
# Get embedding directly
with torch.no_grad():
embedding = model.encode(texts, convert_to_tensor=True)
print(embedding)
在这里,torch.no_grad 函数确保在背部扩散时没有计算梯度。
另一种更通用的方法是使用 PyTorch 生成嵌入式:
# Load the model
model = AutoModel.from_pretrained("sentence-transformers/msmarco-distilbert-base-tas-b")
# Get the tokenizer
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/msmarco-distilbert-base-tas-b")
# Tokenize input
text = ["This is an example sentence"]
encoded_input = tokenizer(text, padding=True, truncation=True, return_tensors='pt')
# Get embedding of the [CLS] token
with torch.no_grad():
outputs = model(**encoded_input, return_dict=True)
cls_embedding = outputs.last_hidden_state[:, 0]
print(cls_embedding)
这个和以前的代码片段之间的区别在于,代码函数已被用代码器和模型来明确取代。
另一个区别是,我们正在使用 outputs.last_hidden_state[:, 0]来检索与CLS代币相关的矢量. 这个CLS特殊代币在每个句子开始时被添加到每个句子中,并且包含有关整个句子的累积信息。
值得注意的是,这种添加CLS代币的方法仅适用于某些基于变压器的架构,包括BERT及其变体和仅基于编码器的变压器。
Best for:分类和序列级预测任务
Why they work:BERT 风格模型中的 [CLS] 代币是专门训练的,用于在预训练期间从整个序列中汇总信息,它作为一个“总结”代币,捕捉了整体含义。
When to choose:
- 是的
- 使用BERT、ROBERTa或类似模型进行分类时 是的
- 当您需要一个代表整个序列的单个矢量时 是的
- 当下游任务涉及预测整个文本的一种属性时 是的
所使用的CLS方法只是捕捉句子的嵌入方法之一,还有其他方法。
Mean Pooling
采取所有代币嵌入的平均值对于许多任务来说令人惊讶地有效,当我使用嵌入用于相似性或检索任务时,这是我的步骤方法。
Best for:语义相似性、检索和一般用途表示。
Why it works:通过在所有代币表示中平均化,平均聚合捕捉了集体语义内容,同时减少了噪音。
When to choose:
- 是的
- 用于文档相似性或语义搜索应用 是的
- 当您需要强大的代表性,而不是由任何单个代币主导 是的
- 当实证测试显示它优于其他方法(通常用于类似性任务) 是的
import torch
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# Mean pooling
with torch.no_grad():
outputs = model(**inputs)
# Get attention mask to ignore padding tokens
attention_mask = inputs['attention_mask']
# Sum token embeddings and divide by the number of tokens
sum_embeddings = torch.sum(outputs.last_hidden_state * attention_mask.unsqueeze(-1), dim=1)
count_tokens = torch.sum(attention_mask, dim=1, keepdim=True)
mean_embeddings = sum_embeddings / count_tokens
print(f"Shape: {mean_embeddings.shape}") # (2, 768)
Max Pooling
Max 聚合对每个维度的最大值在所有代币中占有最大值,无论它们在文本中出现在何处,都很擅长捕捉重要特征。
Best for:功能检测和信息提取任务
Why it works:Max 聚合在所有代币中选择每个维度最强的激活,有效捕捉最突出的特征,无论它们在文本中出现在何处。
When to choose:
- 是的
- 当特定的特征比其频率或位置更重要时 是的
- 当寻找特定概念或实体的存在时 是的
- 当处理长文时,重要信号可能会在平均值中稀释 是的
import torch
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# Max pooling
with torch.no_grad():
outputs = model(**inputs)
# Create a mask to ignore padding tokens for max pooling
attention_mask = inputs['attention_mask'].unsqueeze(-1)
# Replace padding token representations with -inf so they're never selected as max
token_embeddings = outputs.last_hidden_state.masked_fill(attention_mask == 0, -1e9)
# Take max over token dimension
max_embeddings = torch.max(token_embeddings, dim=1)[0]
print(f"Shape: {max_embeddings.shape}") # (2, 768)
Weighted Mean Pooling
重量化聚合方法试图根据位置给更重要的代币更多的重量(例如,给后来的代币更多的重量)。
Best for: Tasks where different parts of the input have different importance
Why it works:重量组合允许您根据其位置,关注分数或其他相关度指标强调某些代币。
When to choose:
- 是的
- 当序列顺序重要时(例如,给后来的代币更多的重量) 是的
- 当某些代币本质上更具信息性时(例如,词汇和词汇与文章) 是的
- 當你有一個特定的重要性,這對你的任務有意義 是的
import torch
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# Weighted mean pooling - more weight to later tokens
with torch.no_grad():
outputs = model(**inputs)
# Get token embeddings and attention mask
token_embeddings = outputs.last_hidden_state
attention_mask = inputs['attention_mask']
# Create position-based weights (later positions get higher weights)
input_lengths = torch.sum(attention_mask, dim=1).unsqueeze(-1)
position_indices = torch.arange(token_embeddings.size(1)).unsqueeze(0).expand_as(attention_mask)
position_weights = position_indices.float() / input_lengths.float()
position_weights = position_weights * attention_mask
# Normalize weights to sum to 1
position_weights = position_weights / torch.sum(position_weights, dim=1, keepdim=True)
# Apply weights and sum
weighted_embeddings = torch.sum(token_embeddings * position_weights.unsqueeze(-1), dim=1)
print(f"Shape: {weighted_embeddings.shape}") # (2, 768)
最后一张包裹
最后的代币聚合是通过仅选择最终代币的代表来从代币嵌入的序列中创建单个嵌入量子的一种技术。
Best for: Autoregressive models and sequential processing
Why it works:在GPT等左向右模型中,最终的代币包含整个序列的积累背景,使其为特定任务提供丰富的信息。
When to choose:
- 是的
- 使用 GPT 或其他仅用于解码器的模型时 是的
- 工作时,主要取决于前面的全部背景的任务 是的
- 用于文本生成或完成任务 是的
import torch
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# Last token pooling
with torch.no_grad():
outputs = model(**inputs)
# Get the last non-padding token for each sequence
attention_mask = inputs['attention_mask']
last_token_indices = torch.sum(attention_mask, dim=1) - 1
batch_indices = torch.arange(attention_mask.size(0))
# Extract the last token embedding for each sequence
last_token_embeddings = outputs.last_hidden_state[batch_indices, last_token_indices]
print(f"Shape: {last_token_embeddings.shape}") # (2, 768)
还有很多方法,这些方法可以结合在一起,并创建自定义方法,这只是理解嵌入式作为一个概念和基本实现的开始,以使用不同的技术获得嵌入式。
Looking Forward: Where Embeddings Are Headed
向前看:嵌入式指向的地方嵌入空间(pun intended)继续演变:
- 是的
- 多式嵌入式正在打破文本,图像,音频和视频之间的壁垒. CLIP 和 DALL-E 等模型使用嵌入式来创建不同的模式之间共享的语义空间。 是的
- 更高效的架构,如MobileBERT和DistilBERT,使得在资源有限的边缘设备上可以使用强大的嵌入。 是的
- 在专门的公司上预先培训的域特定的嵌入式正在推动医学,法律和金融等领域的最先进。 是的
我特别兴奋的构成意识的嵌入式更好地捕捉如何从更小的单位构建意义,这最终可以解决长期存在的否定和构成短语的挑战。
Final Thoughts
最后的想法嵌入不仅仅是另一种NLP技术 - 它们是机器理解和处理语言的方式的一个根本性转变,它们使我们从将文本作为任意符号处理到捕捉人类直观理解的含义和关系的丰富而复杂的网络。
無論您正在處理的 NLP 任務,有可能仔細地應用嵌入式可以讓它變得更好,關鍵在於了解不僅是如何生成它們,而且是什麼時候以及為什麼要使用不同的方法。
如果你仍然在使用单词袋或单热编码来分析文本...
好吧,有全世界的可能性在等着你。