从0开始LLM-词嵌入(embedding)

701 阅读7分钟

什么是词嵌入(embedding)

神经网络是没有办法直接处理视频、音频和文本等原始数据格式的,因此我们需要一种将原始数据表示为密集向量,以满足训练神经网络的数学运算的要求,这样神经网络就可以轻松理解和处理原始数据了。

嵌入:将数据转换为向量格式

image.png 图中展示了将视频、音频、文本三种原始数据转换为三维数值向量的过程。

注意:1. 不同的数据格式需要不同的嵌入模型;2. 可以转换为任意维度的向量维度。

从本质上讲,嵌入是一种映射,能够将离散对象:单词、图像、文档映射到连续向量空间中的点,其目的是将非数字数据转换为神经网络可以处理的格式。

词嵌入是文本嵌入的最常见的形式(NLP),但是也可以用于句子、段落和整个文档的嵌入(RAG)。

LLM中需要的是词嵌入,将一个个单词转换为向量。

在使用词嵌入技术时,相似概念的词在嵌入空间(向量空间)中通常彼此接近,也就是向量的相似度较高。 image.png 图中展示了Word2Vec二维词嵌入可视化结果:在嵌入空间中,不同类型的鸟类相对于国家和城市更为靠近。

Word2Vec训练神经网络架构是通过预测给定目标词的上下文或反之来生成词嵌入。 Word2Vec 架构的主要思想是,出现在相似上下文中的词往往具有相似的含义。

词嵌入可以有不同的维度,从一维到数千维不等。更高的维度可能会捕捉到词之间更多细微的关系,但作为代价,计算效率将会下降。

LLM通常会生成自己的嵌入,这些嵌入是输入层的一部分,会参与训练期间的参数更新。将嵌入作为 LLM 训练的一部分进行优化,而不是使用 Word2Vec 的优势在于,嵌入被优化以适应手头的特定任务和数据。

并且,在LLM中使用的维度远远大于上图中的维度。

对于GPT-2和GPT-3,嵌入大小(通常被称为模型隐藏状态的维度)根据具体的模型变种和大小而变化。 这是性能与效率之间的权衡。 最小的GPT-2(1.17亿参数)和GPT-3(1.25亿参数)模型使用768维的嵌入大小来提供具体示例。最大的GPT-3模型(1750亿参数)使用的嵌入大小为12,288维。

1. 文本分词(序列化)

对于文本中的单词,如何转换为向量呢? image.png 图中展示LLM中涉及的文本处理步骤。首先将输入文本分割为单独的token,然后转换为token id并创建token嵌入。

首先要做的第一步就是将输入文本切分为单独的token(标记),这是为LLM创建嵌入的必须预处理步骤。这些标记可能是单个单词、特殊字符和标点符号等。

分割方法(英文文本为例子):

  1. 使用正则表达式
import re
text = "Hello, world. This, is a test."
result = re.split(r'(\s)', text)
print(result)
#结果是一个包含单个单词、空格和标点符号的列表
['Hello,', ' ', 'world.', ' ', 'This,', ' ', 'is', ' ', 'a', ' ', 'test.']

#单独列出标点符号
result = re.split(r'([,.]|\s)', text)
print(result)
#结果中仍包含空白字符
['Hello', ',', '', ' ', 'world', '.', '', ' ', 'This', ',', '', ' ', 'is', ' ', 'a', ' ', 'test', '.', '']

#移除空白字符
result = [item.strip() for item in result if item.strip()]
print(result)

['Hello', ',', 'world', '.', 'This', ',', 'is', 'a', 'test', '.']

Q:是否移除空格?

A:在开发一个简单的分词器时,是否应该将空格编码为单独的字符或者直接移除它们,这取决于我们的应用及其需求。 移除空格可以减少内存和计算需求。然而,保留空格在我们训练对文本的精确结构敏感的模型时可能是有用的 (例如,Python代码对缩进和间距非常敏感)

2. 转换为token id

为了将生成的单独token转换为token id,首先需要构建一个词汇表。单独的token按照字母顺序进行排序,并移除重复的token。然后,将这些独特的token聚集成一个词汇表,该词汇表定义了每个token到一个独特整数值的映射。

image.png 同样地,在将LLM的输出转换为文本时,需要一种方法将token id转换为文本。

因此,设计一个分词器类,其中包括一个编码(encode)方法:将文本分割成token,并通过词汇表执行映射以生成token id。此外,还需要一个解码器(decode)方法:执行整数向文本的反向映射,将token id转换为文本。

image.png

class SimpleTokenizerV1:
    def __init__(self, vocab):
        self.str_to_int = vocab #A
        self.int_to_str = {i:s for s,i in vocab.items()} #B
 
    def encode(self, text): #C
        preprocessed = re.split(r'([,.?_!"()']|--|\s)', text)
        preprocessed = [item.strip() for item in preprocessed
if item.strip()]
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids
 
    def decode(self, ids): #D
        text = " ".join([self.int_to_str[i] for i in ids])  
        text = re.sub(r'\s+([,.?!"()'])', r'\1', text) #E
        return text

3. 添加特殊上下文tokens

在词汇表中添加特殊的标记来处理特定的上下文。例如,我们添加 <|UNK|> token表示新的和未知的单词,这些单词不是训练数据的一部分,因此也不是现有词汇表的一部分。

当处理多个独立的文本源时,我们在这些文本间添加叫做 <|endoftext|> 的tokens。这些<|endoftext|>tokens作为标记,标志着一个特定段落的开始和结束,这使得LLM能更有效地处理和理解文本。

# 一个处理未知单词的简单文本标记器
class SimpleTokenizerV2:
    def __init__(self, vocab):
        self.str_to_int = vocab
        self.int_to_str = { i:s for s,i in vocab.items()}
    def encode(self, text):
        preprocessed = re.split(r'([,.?_!"()']|--|\s)', text)
        preprocessed = [item.strip() for item in preprocessed if item.strip()]
        preprocessed = [item if item in self.str_to_int else '<|unk|>' for item in preprocessed] #A
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids
    def decode(self, ids):
        text = " ".join([self.int_to_str[i] for i in ids])
        text = re.sub(r'\s+([,.?!"()'])', r'\1', text) #B
        return text

与SimpleTokenizerV1相比,SimpleTokenizerV2将未知单词替换为<|UNK|> token。

text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " <|endoftext|> ".join((text1, text2))
print(text)

#输出
'Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace.'

两段文本中间使用了<|endoftext|>token隔开处理。

4. 其他的特殊token

  • [BOS](beginning of sequence):此标记标记文本的开始。LLM表示一段内容开始的位置。
  • [EOS](end of sequence):这个标记位于文本的末尾,在连接多个不相关的文本时特别有用,类似于<|内文|>.例如,当合并两个不同的维基百科文章或书籍时,[EOS]令牌指示一篇文章结束的位置和下一篇文章开始的位置。
  • [PAD](padding):当训练批量大小大于1的LLMs时,该批可能包含不同长度的文本。为了确保所有文本具有相同的长度,使用[PAD]标记扩展或“填充”较短的文本,直到批次中最长文本的长度。

Att:

用于GPT模型的标记器不需要上面提到的任何标记,而只使用<|内文|> token for simplicity.的<|内文|”这是一个类似于上面提到的[EOS]令牌。此外,<|内文|“也是用来填充的。

然而,当在批量输入上训练时,我们通常使用掩码,这意味着我们不关注填充的令牌。因此,选择用于填充的特定令牌变得无关紧要。

此外,用于GPT模型的tokenizer也不使用<|UNK|>用于词表外的单词的标记。相反,GPT模型使用字节对编码标记器,它将单词分解为子单词单元,这部分我们将在下一节中讨论。