LLM 大语言模型实战 (四)-结合代码理解 Transformer 模型之 Tokenizer、Embbeding 向量

一、Transformers

  • Huggingface 提供的 Transformers 开源框架
  • 大模型时代,几乎所有的模型都是该框架的一些魔改
  • Huggingface 提供了海量模型、数据集的在线仓库;

二、Tokenizer

  • 文本序列怎么入模?
    • 1、每个模型都有自己训练好的字典,其中每个字(Token)都有向量表征;
    • 2、我们需要把输入的文本序列转换成token的列表 -> 张量
    • 3、我们需要保留位置信息。(Position embedding)

Tokenizer 是什么?

Tokenizer 是将输入文本序列转换为模型能够理解的数字化表示的工具。它负责将文本中的单词、子词或字符等转化为一系列标记(tokens)。每个标记可以对应一个数字,这个过程称为 tokenization(分词)

Tokenizer 是语言模型中的一个关键组件,用于将文本转换为模型输入的格式。通常,分词器会基于一个词汇表(vocabulary),这个词汇表是在模型训练时确定的,它包含模型所见过的所有词、子词或者字符,每个词都与一个唯一的 ID 相关联。

主要步骤:

  1. 文本序列转换为 token
    每个模型都有自己的词汇表(vocabulary)。当文本输入模型时,Tokenizer 将文本序列拆解成模型词汇表中已知的词、子词或字符。这些拆解的部分就称为“token”。

  2. Token 转换为张量
    这些 token 会被转换为整数 ID 列表,表示它们在词汇表中的位置。然后将这些整数 ID 变成模型可以处理的张量(tensor)。

  3. 位置嵌入(Position Embedding)
    除了 token 的 ID,Transformer 模型还需要位置信息,表示每个 token 在句子中的位置。位置嵌入向量与 token 的嵌入向量一起输入模型,使模型理解序列的顺序。

具体流程:

  1. 分词(Tokenization):将输入文本按照模型的词汇表拆解为 tokens。
  2. 将 token 转换为对应的 ID:查找词汇表,获取每个 token 对应的 ID。
  3. 生成张量:将这些 ID 生成模型可以处理的张量。
  4. 位置嵌入:为每个 token 添加对应的位置信息。

举例说明

我们使用 Hugging Face 的开源大模型 BERTGPT,这些模型都有专门的 Tokenizer。以 Hugging Face 的 transformers 库为例,首先将一段文本 tokenization,并转换为张量形式,加入位置嵌入。

Jupyter Notebook 中的代码示例

# 安装 transformers 库
!pip install transformers

from transformers import BertTokenizer

# 1. 初始化分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# 2. 输入文本
text = "The animal didn't cross the street because it was too tired."

# 3. 文本转化为 token ids
tokens = tokenizer.tokenize(text)  # 分词
token_ids = tokenizer.convert_tokens_to_ids(tokens)  # 转为 ID

# 4. 加入特殊标记 (CLS, SEP) 以适应模型输入格式
input_ids = tokenizer.encode(text, add_special_tokens=True)

# 5. 输出结果
print("Tokens:", tokens)
print("Token IDs:", token_ids)
print("Input IDs (with special tokens):", input_ids)

# 6. 将输入转化为张量
import torch
input_tensor = torch.tensor([input_ids])

print("Input Tensor:", input_tensor)

# 7. 获取位置信息
# Transformer 模型会自动处理位置嵌入,作为模型输入的一部分。
代码解析:
  1. 初始化分词器:我们加载了 BERT 的分词器 BertTokenizer,这个分词器有一个内置的词汇表。

  2. 文本转化为 tokenstokenizer.tokenize 方法将文本分解为 token,BERT 会将常见单词直接作为一个 token,但对于不常见的词会进行子词拆解(比如 didn't 会被拆分为 did, n, 't)。

  3. 将 token 转化为 IDconvert_tokens_to_ids 方法将 tokens 转化为它们在 BERT 词汇表中的唯一 ID。

  4. 加入特殊标记:BERT 在输入中使用特殊标记 [CLS](句首)和 [SEP](句尾),用于表明序列的开始和结束。

  5. 张量化输入:将 token ID 列表转换为 PyTorch 张量,方便后续输入到模型中。

  6. 位置嵌入:在 Transformer 中,位置嵌入是模型的一部分,自动根据序列中的 token 位置生成,增强模型对顺序的理解。

最终模型的输入

每个 token 会对应一个嵌入向量,位置嵌入会被加到 token 嵌入上,最终输入到模型中进行进一步处理。这一步我们没有手动实现,Transformer 模型会自动处理。

总结:

Tokenizer 是将自然语言文本转换为模型可以处理的数字化表示的工具。通过分词、查找词汇表中的 ID、加入位置嵌入等步骤,模型能够理解和处理输入的文本。

Google Colab 使用

由于本地电脑资源配置有限,在使用一个较大的模型,如 bert-base-uncased,它可能会占用大量内存。可以尝试使用较小的预训练模型,比如 distilbert,它是 BERT 的轻量级版本,使用轻量级模型还是报内核崩溃,所以,这里使用的Google Colab;

如果你的本地计算机内存不足,尝试使用云计算资源,如 Google Colab。Colab 提供免费的 GPU/TPU 和更多的内存,可以用来运行更大的模型。将你的代码粘贴到 Colab 中运行可能会避免崩溃问题。

借助 Colaboratory(简称 Colab),您可在浏览器中编写和执行 Python 代码,并且:

  • 无需任何配置
  • 免费使用 GPU
  • 轻松共享

https://colab.research.google.com/#scrollTo=5fCEDCU_qrC0

file

执行代码:

# from transformers import BertTokenizer
from transformers import DistilBertTokenizer  # 确保使用 DistilBert 的分词器

# 1、初始化分词器
# # 使用较小的 DistilBert 模型的分词器
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')

# 查看分词器词汇表大小
token_size = tokenizer.vocab_size
print("vocab_size:", token_size)

# 查看词汇表
vocab = tokenizer.get_vocab()
# print("打印所有词汇表:", vocab) 
# 每个模型都有自己的词汇表{"值":ID}
# 词汇表格式: {'[PAD]': 0, '[unused0]': 1, ..., 'alexander': 3656, 'gaze': 365, ...,'##北': 30307, '##区': 30308, '##十': 30309, '##千': 30310 }

# 按照token ID排序,并只显示前20个词汇
vocab_sorted = sorted(vocab.items(), key=lambda x: x[1])[:10]
print("查看词汇表数据,打印前10个:", vocab_sorted)

# 打印前20个词汇
for token, token_id in vocab_sorted:
    print(f"Token: {token}, ID: {token_id}")

# 2. 输入文本
text = "The animal didn't cross the street because it was too tired."

# 3. 文本转化为 token ids
tokens = tokenizer.tokenize(text) # 分词
token_ids = tokenizer.convert_tokens_to_ids(tokens) # 转为ID

# 4. 加入特殊标记 (CLS, SEP) 以适应模型输入格式
input_ids = tokenizer.encode(text, add_special_tokens=True)

# 5. 输出结果
print("Tokens:", tokens)
print("Token IDs:", token_ids)
print("Input IDs (with special tokens):", input_ids)

# 6. 将输入转换为张量
import torch
# device = torch.device('cpu')
input_tensor = torch.tensor([input_ids])

print("Input Tensor:", input_tensor)

# 7. 获取位置信息
# Transformer 模型会自动处理位置嵌入,作为模型输入的一部分。

打印输出:

vocab_size: 30522
查看词汇表数据,打印前10个: [('[PAD]', 0), ('[unused0]', 1), ('[unused1]', 2), ('[unused2]', 3), ('[unused3]', 4), ('[unused4]', 5), ('[unused5]', 6), ('[unused6]', 7), ('[unused7]', 8), ('[unused8]', 9)]
Token: [PAD], ID: 0
Token: [unused0], ID: 1
Token: [unused1], ID: 2
Token: [unused2], ID: 3
Token: [unused3], ID: 4
Token: [unused4], ID: 5
Token: [unused5], ID: 6
Token: [unused6], ID: 7
Token: [unused7], ID: 8
Token: [unused8], ID: 9
Tokens: ['the', 'animal', 'didn', "'", 't', 'cross', 'the', 'street', 'because', 'it', 'was', 'too', 'tired', '.']
Token IDs: [1996, 4111, 2134, 1005, 1056, 2892, 1996, 2395, 2138, 2009, 2001, 2205, 5458, 1012]
Input IDs (with special tokens): [101, 1996, 4111, 2134, 1005, 1056, 2892, 1996, 2395, 2138, 2009, 2001, 2205, 5458, 1012, 102]
Input Tensor: tensor([[ 101, 1996, 4111, 2134, 1005, 1056, 2892, 1996, 2395, 2138, 2009, 2001,
         2205, 5458, 1012,  102]])

Position Embedding

根据上边的示例,进一步研究token位置信息的使用:

在 Transformer 模型中,位置信息(Position Embedding)用于保留输入序列中每个 token 的位置,防止模型忽略序列的顺序。常见的做法是将位置编码(Positional Encoding)与词嵌入(Token Embedding)相加。接下来我会展示如何结合位置信息进行处理,并在代码中实现它。

Positional Encoding 介绍:

位置编码通过给定序列中每个 token 的位置来生成一个唯一的向量。通常使用以下公式:
file

在代码中实现:

  1. 生成 Positional Encoding:创建一个矩阵,其中每个位置都会生成对应的嵌入向量。
  2. 将 Token Embedding 和 Positional Encoding 相加:将词嵌入与位置编码相加,作为最终输入。

下面是代码示例,包含位置编码的实现:

import torch
import math

# Positional Encoding函数
class PositionalEncoding(torch.nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        # 创建一个(max_len, d_model)的矩阵,用于存储每个位置的编码
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))

        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)  # 扩展成 (1, max_len, d_model)
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 将位置编码与输入x相加,确保形状匹配
        x = x + self.pe[:, :x.size(1), :]
        return x

# 使用轻量的 ALBERT 模型分词器
from transformers import AlbertTokenizer
tokenizer = AlbertTokenizer.from_pretrained('albert-base-v2')

# 输入文本
text = "The animal didn't cross the street because it was too tired."
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = tokenizer.encode(text, add_special_tokens=True)

# 将输入转为张量
input_tensor = torch.tensor([input_ids])

# 假设模型的嵌入维度为 128
embedding_dim = 128
max_len = 100  # 假设句子最大长度为 100

# 生成 token embedding(假设随机初始化,实际模型会有自己的词嵌入层)
token_embedding = torch.nn.Embedding(len(tokenizer), embedding_dim)
embedded_tokens = token_embedding(input_tensor)

# 初始化位置编码模块
pos_encoder = PositionalEncoding(d_model=embedding_dim, max_len=max_len)

# 对 token embedding 添加位置编码
position_encoded_tokens = pos_encoder(embedded_tokens)

print("Position Encoded Tokens Shape:", position_encoded_tokens.shape)

打印输出:

tokenizer_config.json: 100%
 25.0/25.0 [00:00<00:00, 1.49kB/s]
spiece.model: 100%
 760k/760k [00:00<00:00, 6.10MB/s]
tokenizer.json: 100%
 1.31M/1.31M [00:00<00:00, 9.28MB/s]
config.json: 100%
 684/684 [00:00<00:00, 43.0kB/s]
Position Encoded Tokens Shape: torch.Size([1, 16, 128])

解释:

  1. PositionalEncoding:这是一个计算位置编码的类,输入 d_model 是嵌入向量的维度,max_len 是句子最大长度。
  2. forward 函数:将生成的位置信息与输入的 token 嵌入相加,以保留序列的顺序。
  3. Token Embedding 和 Positional Encoding 相加:通过 token_embedding(input_tensor) 获取 token 嵌入,并使用 pos_encoder 添加位置信息。
  4. 嵌入维度:在代码中,假设词嵌入的维度为 128,你可以根据具体模型调整这个维度。

运行输出:

  • Position Encoded Tokens Shape 将显示带有位置编码后的张量形状,通常为 [batch_size, seq_len, embedding_dim]

总结:

通过这种方法,你已经将位置编码与词嵌入结合在一起,这也是 Transformer 模型中的核心步骤之一。

五、什么是 embedding_dim 嵌入维度?

embedding_dim(嵌入维度)是指在自然语言处理中,每个词或子词在嵌入空间中的向量表示的维度。简单来说,它是模型用来表示词汇的向量的大小。

1. 为什么需要嵌入(Embedding)?

在 NLP 任务中,输入给模型的通常是文本数据。然而,神经网络无法直接处理文字数据,因此需要将文本转换为数字形式,便于模型计算。这个转换过程叫做 词嵌入(Word Embedding),它将每个词(或子词)映射为一个连续向量,这个向量捕捉了词汇的语义信息。

例如:

  • 单词 "king" 可以被表示为一个维度为 128 的向量 [0.1, 0.7, 0.2, ..., 0.3]
  • 单词 "queen" 的表示可能是 [0.2, 0.6, 0.1, ..., 0.4]

这些嵌入向量捕捉了单词之间的语义关系,例如:

  • "king" 和 "queen" 这两个单词的嵌入向量会非常相似,因为它们在语义上接近。

2. 什么是嵌入维度(embedding_dim)?

embedding_dim 是表示每个单词的向量的长度,即每个单词在嵌入空间中的维度。通常可以是 64、128、256 等数字,具体值取决于模型的复杂性和应用场景。维度越高,向量可以捕捉到的信息越丰富,但也会导致计算成本增加。

例如:

  • 如果 embedding_dim = 128,那么每个词的嵌入向量将由 128 个数字(或浮点数)组成。
  • 如果 embedding_dim = 256,那么每个词的嵌入向量将由 256 个数字组成,意味着模型对词的表征更加详细。

3. 如何选择嵌入维度?

选择合适的嵌入维度取决于任务的复杂性和计算资源:

  • 较小的嵌入维度(50-100):适合小型任务,处理简单文本或低资源环境。
  • 中等嵌入维度(128-300):适合大多数常见的 NLP 任务,如情感分析、机器翻译等。
  • 较大的嵌入维度(>300):适合处理复杂任务或丰富语义信息的任务,如大型机器翻译、复杂语言建模。

4. 示例

假设我们有以下三个词:"king", "queen", "apple"。在 3 维嵌入空间中(embedding_dim=3),它们的嵌入向量可能如下所示:

king   -> [0.5, 0.2, 0.3]
queen  -> [0.4, 0.3, 0.5]
apple  -> [0.1, 0.7, 0.9]

这里我们使用 3 维的嵌入空间来表示每个词。这些向量数字代表了在嵌入空间中不同维度上单词的特征。

在实际模型中,embedding_dim 通常会比 3 大得多,可能是 128、256 甚至 768,这样能够捕捉到更丰富的语义信息。

5. 与模型架构的关系

嵌入维度直接影响 Transformer、BERT、GPT 等深度学习模型的性能。在这些模型中,输入的文本会首先被映射为嵌入向量,之后这些向量再经过多层自注意力机制和前馈神经网络进行处理。因此,embedding_dim 是模型中的重要参数之一。

6. 代码示例

import torch
from transformers import BertTokenizer, BertModel

# 使用BERT的预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

# 输入文本
text = "The animal didn't cross the street because it was too tired."
inputs = tokenizer(text, return_tensors="pt")

# 获取嵌入表示
with torch.no_grad():
    outputs = model(**inputs)

# outputs 是包含多个内容的字典
# outputs.last_hidden_state 是每个 token 的嵌入表示,形状为 (batch_size, seq_len, embedding_dim)
embeddings = outputs.last_hidden_state
print("Embedding Shape:", embeddings.shape)  # 形状为 (1, 14, 768) - 表示 batch_size=1,序列长度=14,嵌入维度=768

在这个示例中:

  • embedding_dim 为 768,因为 bert-base-uncased 模型使用了 768 维的嵌入。
  • outputs.last_hidden_state 返回每个 token 的嵌入表示,形状为 (batch_size, seq_len, embedding_dim),其中 seq_len 是输入序列的长度。

总结:

  • embedding_dim 是嵌入空间中向量的维度,用来表示每个 token 的语义特征。
  • 维度越高,表示越丰富,但计算成本也越高。
  • 在实际模型中,嵌入维度是模型的一部分,用来决定词汇在模型中的表示复杂度。

六、如何将token序列与embedding 向量关联起来

问题:上边输入的文本通过token 分词后,然后计算出来的 token 转为张量,之后怎么和维度embedding 关联起来呢?假设维度为 5,请再详细解释?

当我们将输入的文本通过tokenizer进行分词后,生成了一个 token 序列(或 token 的 ID 列表),下一步是将这些 token 转换为嵌入向量(embedding)。每个 token 对应的 ID 会映射到一个指定维度的向量(如 5 维向量)。这个过程是通过嵌入层(embedding layer)来完成的。

下面我们将逐步解释这个过程,并结合假设的 5 维度的嵌入进行详细说明。

1. 文本到 token 的过程

首先,通过 tokenizer,文本被分割为 token,并将每个 token 转换为相应的 ID。比如:

from transformers import BertTokenizer

# 1. 初始化分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# 2. 输入文本
text = "The animal didn't cross the street because it was too tired."

# 3. 文本转化为 token ids
tokens = tokenizer.tokenize(text) 
token_ids = tokenizer.convert_tokens_to_ids(tokens) 

print("Tokens:", tokens)
print("Token IDs:", token_ids)

输出可能如下(注意每个词已被拆分为子词并映射到一个 ID):

Tokens: ['the', 'animal', 'didn', "'", 't', 'cross', 'the', 'street', 'because', 'it', 'was', 'too', 'tired', '.']
Token IDs: [1996, 4111, 2134, 1005, 1056, 2892, 1996, 2395, 2138, 2009, 2001, 2205,  tired, 1012]

2. 将 token 序列转换为张量

为了能够在神经网络中处理,这些 token IDs 需要转换为张量:

import torch

# 4. 将 token id 列表转换为张量
input_tensor = torch.tensor([token_ids])

print("Input Tensor:", input_tensor)

输出为:

Input Tensor: tensor([[1996, 4111, 2134, 1005, 1056, 2892, 1996, 2395, 2138, 2009, 2001, 2205,  tired, 1012]])

3. 与维度为 5 的嵌入关联起来

每个 token ID 会通过一个嵌入层(Embedding Layer)映射到一个指定维度的向量(假设这里的维度是 5)。

Embedding 层的工作原理

嵌入层是一个查找表,里面存储了所有词汇的向量表示。比如在一个 vocab_size 为 30,000 的字典中,嵌入层的大小为 [30000, embedding_dim],其中 embedding_dim 是每个词的向量维度。嵌入层会将每个 token ID 对应到一个向量。

对于这个例子,我们假设 embedding_dim = 5,每个词 ID 将映射到一个 5 维的向量。例如:

  • 'the' 的 token ID 是 1996,它可能映射到 5 维向量 [0.1, 0.2, -0.1, 0.05, 0.3]
  • 'animal' 的 token ID 是 4111,它可能映射到 5 维向量 [0.3, -0.2, 0.1, -0.05, 0.4]

下面是如何在 PyTorch 中通过嵌入层将 token IDs 映射到向量:

# 5. 假设 vocab_size 是 30,000,embedding_dim 是 5
vocab_size = 30000
embedding_dim = 5

# 定义嵌入层
embedding_layer = torch.nn.Embedding(vocab_size, embedding_dim)

# 通过嵌入层,将 token_ids 张量映射为嵌入向量
embedded_tensor = embedding_layer(input_tensor)

print("嵌入后的张量形状:", embedded_tensor.shape)  # 输出: [1, 序列长度, 5]
print("嵌入后的张量:", embedded_tensor)

在这个例子中,embedding_layer 会将 input_tensor 中的 token IDs 转换为一个大小为 [1, 序列长度, embedding_dim] 的张量。输出的形状是 (1, 14, 5),表示有 14 个 token,每个 token 被映射为一个 5 维向量。

4. 嵌入示例

假设经过嵌入层,前两个 token 对应的嵌入向量如下:

嵌入后的张量:
tensor([[[ 0.12,  0.23, -0.15,  0.32, -0.17],  # 'the' 对应的嵌入
         [ 0.34, -0.25,  0.18,  0.40, -0.12],  # 'animal' 对应的嵌入
         ...  # 其他 token 的嵌入向量
]])

对于每个 token,都会有一个 5 维向量。这个嵌入向量是通过嵌入层查找得到的,最初是随机初始化的,经过训练后会调整,以捕捉词与词之间的语义关系。

5. 位置嵌入(Positional Embeddings)

除了每个 token 的语义嵌入,我们还需要将每个 token 在序列中的位置信息也嵌入到模型中。这是因为 Transformer 模型本身并没有顺序意识,位置信息帮助模型理解每个词在句子中的顺序。

位置嵌入是一个固定的或可学习的嵌入层,维度与 token 嵌入相同,表示每个 token 在序列中的位置。例如,句子中的第一个 token 可能有一个嵌入向量表示它是第1个词,第二个词有另一个嵌入向量表示它是第2个词。

6. 结合嵌入的完整示例

import torch

# 1. 假设 vocab_size = 30,000,embedding_dim = 5
vocab_size = 30000
embedding_dim = 5
max_len = 20  # 最大序列长度(假设为20)

# 2. 定义 token 嵌入层和位置嵌入层
token_embedding = torch.nn.Embedding(vocab_size, embedding_dim)
position_embedding = torch.nn.Embedding(max_len, embedding_dim)

# 3. 输入 token 序列
input_tensor = torch.tensor([token_ids])  # [1, 序列长度]

# 4. 获取 token 嵌入
token_embedded = token_embedding(input_tensor)

# 5. 获取位置嵌入
positions = torch.arange(0, input_tensor.size(1)).unsqueeze(0)  # [1, 序列长度]
position_embedded = position_embedding(positions)

# 6. 最终的嵌入是 token 嵌入和位置嵌入的相加
final_embedding = token_embedded + position_embedded

print("最终嵌入形状:", final_embedding.shape)
print("最终嵌入:", final_embedding)

在这个例子中:

  • token_embedded 是每个词的嵌入表示。
  • position_embedded 是每个词在序列中的位置信息表示。
  • 最终的嵌入是两者相加的结果,形状为 [1, 序列长度, embedding_dim]

7. 总结

  • 嵌入维度表示每个 token 被映射到的向量的大小,比如 5 维表示每个 token 被映射为 5 个数值的向量。
  • 连续向量通过嵌入层从词典 ID 映射到一个多维向量。
  • 位置嵌入通过保留每个 token 在句子中的位置信息,帮助模型理解句子结构。

为者常成,行者常至