LLM大模型架构之词嵌入(Part3)

以下是已更新文章:
1. LLM大模型架构专栏|| 从NLP基础谈起
2.LLM大模型架构专栏|| 自然语言处理(NLP)之建模
3. LLM大模型架构之词嵌入(Part1)
4. LLM大模型架构之词嵌入(Part2)
欢迎关注公众号【柏企阅文

此前,我们一同探寻了词嵌入的基础概念与相关技术的冰山一角,今天,让我们继续深入这片知识的海洋,从 3.2 Static Embeddings(静态嵌入)部分开始,进一步挖掘词嵌入技术的奥秘。

3.2 静态嵌入

密集向量,也就是词嵌入,成功地突破了独热编码的局限,为单词提供了更具信息量且更为紧凑的表示方式。

降维处理

与独热编码的向量长度等同于词汇表大小不同,词嵌入通常采用维度小得多的向量(例如 50、100 或 300 维)。这就好比在一个庞大的图书馆中,我们不再为每个单词单独建立一个巨大的书架(独热编码),而是将相关的单词整理到几个紧凑的书架上(词嵌入),既节省了空间,又能快速找到所需的信息。

语义邻近性

在向量空间中,密集向量能够让语义相似的单词彼此靠近。例如,“cat”(猫)和“dog”(狗)的向量余弦相似度会高于“cat”和“fish”(鱼)。这就如同在一个城市中,同类的商店(语义相似的单词)会聚集在同一条街道或同一个街区,方便人们找到它们。

例如,Word2Vec 通过预测单词在上下文窗口中的周围单词来学习词嵌入;GloVe 则运用矩阵分解来获取词嵌入;而像 BERT、GPT 这样的 Transformer 模型,能够基于周围的上下文生成捕捉单词含义的上下文词嵌入。

那么,词嵌入解决了独热向量的什么关键问题呢?其核心在于泛化能力。假设我们认为“cat”和“tiger”(老虎)确实相似,我们就需要一种方式将这种信息传递给模型。当其中一个单词比较罕见(如“liger”,狮虎兽)时,这一点尤为重要。因为在训练过程中,如果模型已经学习了“cat”的处理方式,那么当遇到“liger”时,如果它的嵌入与“cat”相似,模型就可以借鉴处理“cat”的路径,而不是从头开始学习。这就像我们在学习新知识时,如果能与已有的知识建立联系,就会更容易理解和掌握。

下面通过一个简单的示例来进一步说明。假设我们从词汇表中选取 5 个单词(如“aardvark”(土豚)、“black”(黑色)、“cat”、“duvet”(羽绒被)和“zombie”(僵尸)),并查看由独热编码方法创建的嵌入向量。其结果可能是每个单词都由一个大部分为零的向量表示,只有在对应单词索引位置为 1。但实际上,作为使用语言的人类,我们知道单词具有丰富的内涵和意义。

我们可以为这 5 个单词手工制作一些语义特征。具体来说,我们为每个单词在四个语义属性(“animal”(动物)、“fluffiness”(毛绒绒)、“dangerous”(危险)和“spooky”(诡异))上赋予 0 到 1 之间的值。例如,对于“aardvark”,我们给“animal”属性赋予较高的值,因为它显然是一种动物,而给“fluffiness”、“dangerous”和“spooky”属性赋予相对较低的值;对于“cat”,我们给“animal”和“fluffiness”属性赋予较高的值,给“dangerous”和“spooky”属性赋予中等的值。

这样,每个语义特征都可以看作是更广泛、更高维语义空间中的一个维度。在这个示例中,有四个语义特征,我们可以每次选取两个作为坐标轴绘制二维散点图。每个单词在这个空间中的坐标由其在相关特征上的特定值确定。例如,“aardvark”在“fluffiness”与“animal”的二维图中的坐标可能是(x = 0.97, y = 0.03)。同样,我们也可以考虑三个特征(如“animal”、“fluffiness”和“dangerous”),并在三维语义空间中绘制单词的位置。

虽然这只是一个手工制作的示例,但实际的嵌入算法会自动为输入语料库中的所有单词生成嵌入向量。我们可以将 Word2Vec 等词嵌入算法视为单词的无监督特征提取器,它们就像勤劳的小矿工,从文本数据中挖掘出单词的语义和语法特征,将其转化为机器能够理解的向量形式。

词嵌入的维度

一般而言,词嵌入的维度指的是定义单词向量表示的维度数量,这通常是在创建词嵌入时确定的一个固定值。词嵌入的维度代表了向量表示中编码的特征总数。

不同的词嵌入生成方法可能会导致不同的维度。最常见的是,词嵌入的维度范围在 50 到 300 之间,但更高或更低的维度也是有可能的。例如,下面的图展示了在三维空间中“king”(国王)、“queen”(女王)、“man”(男人)和“woman”(女人)的词嵌入情况。

3.2.1 Word2Vec

由 Mikolov 等人提出的 Word2Vec 是一种广受欢迎的基于预测的方法,它通过预测上下文窗口内的周围单词来学习词嵌入。这种方法能够生成捕捉单词间语义关系的密集向量表示。

Bengio 等人提出的方法为 NLP 研究人员开辟了新的途径,他们将单词输入到一个包含嵌入层、隐藏层和 softmax 函数的前馈神经网络中。这些嵌入具有可学习的向量,通过反向传播进行优化。本质上,该架构的第一层就产生了词嵌入,因为它是一个浅层网络。

然而,这种架构在隐藏层和投影层之间的计算成本较高。原因在于投影层产生的值是密集的,而隐藏层需要计算词汇表中所有单词的概率分布。

为了解决这个问题,Mikolov 等人在 2013 年提出了 Word2Vec 模型。Word2Vec 模型有效地解决了 Bengio 的 NLM 存在的问题。它去掉了隐藏层,但投影层与 Bengio 的模型一样被所有单词共享。不足之处在于,在数据较少的情况下,这个没有神经网络的简单模型可能无法像神经网络那样精确地表示数据。但在较大的数据集上,它能够在嵌入空间中精确地表示数据,同时降低了复杂度,并且可以在更大的数据集上进行训练。

Word2Vec 有两种嵌入方法:连续词袋模型(CBOW)和跳字模型(Skip - gram)。

3.2.1.1 连续词袋模型(CBOW)

CBOW 模型的任务是根据周围的单词预测一个单词出现的概率。我们可以考虑单个单词或一组单词作为上下文,但为了简单起见,这里我们以单个上下文单词为例,尝试预测一个目标单词。

由于英语词汇量庞大,例如包含近 120 万个单词,在示例中不可能全部涵盖。所以我们考虑一个小例子,假设有四个单词“live”(生活)、“home”(家)、“they”(他们)和“at”(在),并且假设语料库中只有一个句子“They live at home”。

首先,我们将每个单词转换为独热编码形式。然后,我们不是考虑句子中的所有单词,而是只选取在一个窗口内的某些单词。例如,对于窗口大小为 3 的情况,我们只考虑句子中的三个单词,中间的单词是要预测的目标单词,周围的两个单词作为上下文输入到神经网络中。然后滑动窗口,重复这个过程。

通常,我们会选择窗口大小在 8 - 10 个单词左右,并设置向量大小为 300 维。经过多次训练后,我们得到的权重就可以用来生成词嵌入。

以下是一段 Python 示例代码,展示如何使用 gensim 库实现 CBOW 模型:

import gensim
from gensim.models import Word2Vec

# 示例句子
sentences = [["they", "live", "at", "home"]]

# 训练 CBOW 模型
model = Word2Vec(sentences, vector_size=300, window=3, min_count=1, sg=0)  # sg=0 表示使用 CBOW 模型

# 查看单词的词嵌入
word_embedding = model.wv["home"]
print(word_embedding)

3.2.1.2 跳字模型(Skip - gram)

跳字模型的架构通常与 CBOW 模型相反,它试图根据一个目标单词(中心单词)预测源上下文单词(周围单词)。


跳字模型的工作原理与 CBOW 模型有相似之处,但在神经网络的架构和权重矩阵的生成方式上存在差异。

在获得权重矩阵后,获取词嵌入的步骤与 CBOW 模型相同。

那么在实际应用中,我们应该选择哪种算法来实现 Word2Vec 呢?对于大规模、高维度的语料库,跳字模型通常能取得更好的效果,但训练速度较慢;而 CBOW 模型则更适合小规模语料库,并且训练速度更快。

以下是一段 Python 示例代码,展示如何使用 gensim 库实现 Skip - gram 模型:

import gensim
from gensim.models import Word2Vec

# 示例句子
sentences = [["they", "live", "at", "home"]]

# 训练 Skip-gram 模型
model = Word2Vec(sentences, vector_size=300, window=3, min_count=1, sg=1)  # sg=1 表示使用 Skip-gram 模型

# 查看单词的词嵌入
word_embedding = model.wv["home"]
print(word_embedding)

3.2.2 GloVe(全局词向量表示)

斯坦福大学的 GloVe 结合了基于计数和基于预测的方法的优点,通过利用共现统计信息来训练词嵌入。通过优化全局单词 - 单词共现矩阵,GloVe 生成的嵌入能够捕捉局部和全局的语义关系。

GloVe 是一种无监督学习算法,它通过分析文本语料库中单词的共现统计信息来获得单词的向量表示。这些单词向量能够捕捉单词之间的语义含义和关系。

GloVe 背后的关键思想是通过研究整个语料库中单词共现的概率来学习词嵌入。它首先构建一个全局的单词 - 单词共现矩阵,然后对其进行分解,以得到表示单词在连续向量空间中的向量。

这些单词向量在自然语言处理任务中广受欢迎,因为它们能够捕捉单词之间的语义关系。它们被应用于各种领域,如机器翻译、情感分析、文本分类等,在这些任务中,理解单词的含义和上下文至关重要。

以下是 GloVe 词嵌入的创建过程:
首先,GloVe 模型创建一个巨大的单词 - 上下文共现矩阵,其中每个元素表示一个单词与一个上下文(可以是一个单词序列)共同出现的频率。然后,如图所示,应用矩阵分解来近似这个矩阵。

考虑到 WC = WF × FC,我们的目标是通过将 WF 和 FC 相乘来重构 WC。为此,我们通常用一些随机权重初始化 WF 和 FC,然后尝试将它们相乘以得到 WC’(WC 的近似值),并测量它与 WC 的接近程度。我们使用随机梯度下降(SGD)多次重复这个过程,以最小化误差。最后,单词 - 特征矩阵(WF)就为每个单词提供了词嵌入,其中 F 可以预先设置为特定的维度。

需要注意的是,Word2Vec 和 GloVe 模型在工作方式上有相似之处。它们都旨在构建一个向量空间,其中每个单词的位置受到其相邻单词的上下文和语义的影响。Word2Vec 从单词共现对的局部个体示例开始,而 GloVe 则从语料库中所有单词的全局聚合共现统计信息开始。

以下是一段 Python 示例代码,展示如何使用 GloVe 模型:

import numpy as np
import pandas as pd

# 假设已经有了预训练的 GloVe 词向量文件,这里以一个简单的示例格式为例
glove_vectors = {}
with open("glove.6B.50d.txt", encoding="utf-8") as f:
    for line in f:
        values = line.split()
        word = values[0]
        vector = np.array(values[1:], dtype="float32")
        glove_vectors[word] = vector

# 示例单词
words = ["cat", "dog", "apple"]

# 计算单词之间的余弦相似度
def cosine_similarity(vec1, vec2):
    dot_product = np.dot(vec1, vec2)
    norm_vec1 = np.linalg.norm(vec1)
    norm_vec2 = np.linalg.norm(vec2)
    similarity = dot_product / (norm_vec1 * norm_vec2)
    return similarity

for i in range(len(words)):
    for j in range(i + 1, len(words)):
        word1 = words[i]
        word2 = words[j]
        vec1 = glove_vectors[word1]
        vec2 = glove_vectors[word2]
        similarity = cosine_similarity(vec1, vec2)
        print(f"Cosine similarity between {word1} and {word2}: {similarity}")

3.2.3 FastText

虽然 Word2Vec 在自然语言处理领域是一个重大突破,但它仍存在一些改进的空间。

未登录词(Out of Vocabulary,OOV)问题

在 Word2Vec 中,每个单词都有一个对应的嵌入,但它无法处理在训练过程中未遇到的单词。例如,“tensor”(张量)和“flow”(流)可能在 Word2Vec 的词汇表中,但如果尝试获取复合词“tensorflow”的嵌入,就会出现未登录词错误。

形态学问题

对于具有相同词根的单词,如“eat”(吃)和“eaten”(吃的过去分词),Word2Vec 不会进行参数共享。每个单词都是根据其出现的上下文单独学习的,因此有机会利用单词的内部结构来提高学习效率。

为了解决上述挑战,Bojanowski 等人提出了一种新的嵌入方法 FastText。他们的关键见解是利用单词的内部结构来改进从跳字方法获得的向量表示。

对跳字方法的修改如下:

子词生成

对于一个单词,我们生成其中长度为 3 到 6 的字符 n - 元组。我们在单词的开头和结尾添加尖括号来表示单词的边界。然后,通过滑动窗口生成指定长度的字符 n - 元组。例如,对于单词“eating”,可以生成长度为 3 的字符 n - 元组,如“<ea”、“eat”、“ati”等。

由于可能会产生大量独特的 n - 元组,我们应用哈希来限制内存需求。我们不是为每个独特的 n - 元组学习一个嵌入,而是学习总共 B 个嵌入,其中 B 表示桶的大小。例如,论文中使用了大小为 200 万的桶。每个字符 n - 元组被哈希到 1 到 B 之间的一个整数。虽然这可能会导致冲突,但有助于控制词汇表的大小。论文使用了 Fowler - Noll - Vo 哈希函数的 FNV - 1a 变体将字符序列哈希为整数值。

带负采样的跳字模型

为了理解预训练过程,我们来看一个简单的示例。假设我们有一个句子,中心单词是“eating”,需要预测上下文单词“am”和“food”。

首先,中心单词的嵌入通过字符 n - 元组的向量和单词本身的向量相加来计算。对于实际的上下文单词,我们直接从嵌入表中获取它们的单词向量,而不添加字符 n - 元组。

然后,我们按照与一元频率的平方根成比例的概率随机收集负样本。对于一个实际的上下文单词,我们采样 5 个随机的负单词。

接下来,我们计算中心单词和实际上下文单词之间的点积,并应用 sigmoid 函数得到 0 到 1 之间的匹配分数。

最后,根据损失,我们使用随机梯度下降(SGD)优化器更新嵌入向量,使实际上下文单词更接近中心单词,同时增加与负样本的距离。

以下是一段 Python 示例代码,展示如何使用 FastText 模型:

import fasttext

# 训练 FastText 模型
model = fasttext.train_unsupervised("your_text_corpus.txt", model="skipgram", dim=300, ws=5, min_count=5)

# 查看单词的词嵌入
word_embedding = model.get_word_vector("cat")
print(word_embedding)

在自然语言处理的征程中,词嵌入技术不断发展演进,从早期的简单方法到如今的先进模型,每一次的突破都为机器更好地理解和处理人类语言带来了新的希望。后续我们还将继续探索其他相关的前沿技术和应用场景,敬请期待!

希望这篇文章能帮助大家更深入地理解词嵌入技术,欢迎关注公众号 柏企科技圈 如果您有任何问题或建议,欢迎在评论区留言交流!