LLM 大语言模型实战 (四)-结合代码理解 Transformer 模型之 Tokenizer、Embbeding 向量
一、Transformers
- Huggingface 提供的 Transformers 开源框架
- 大模型时代,几乎所有的模型都是该框架的一些魔改
- Huggingface 提供了海量模型、数据集的在线仓库;
二、Tokenizer
- 文本序列怎么入模?
- 1、每个模型都有自己训练好的字典,其中每个字(Token)都有向量表征;
- 2、我们需要把输入的文本序列转换成token的列表 -> 张量
- 3、我们需要保留位置信息。(Position embedding)
Tokenizer 是什么?
Tokenizer 是将输入文本序列转换为模型能够理解的数字化表示的工具。它负责将文本中的单词、子词或字符等转化为一系列标记(tokens)。每个标记可以对应一个数字,这个过程称为 tokenization(分词)。
Tokenizer 是语言模型中的一个关键组件,用于将文本转换为模型输入的格式。通常,分词器会基于一个词汇表(vocabulary),这个词汇表是在模型训练时确定的,它包含模型所见过的所有词、子词或者字符,每个词都与一个唯一的 ID 相关联。
主要步骤:
-
文本序列转换为 token:
每个模型都有自己的词汇表(vocabulary)。当文本输入模型时,Tokenizer 将文本序列拆解成模型词汇表中已知的词、子词或字符。这些拆解的部分就称为“token”。 -
Token 转换为张量:
这些 token 会被转换为整数 ID 列表,表示它们在词汇表中的位置。然后将这些整数 ID 变成模型可以处理的张量(tensor)。 -
位置嵌入(Position Embedding):
除了 token 的 ID,Transformer 模型还需要位置信息,表示每个 token 在句子中的位置。位置嵌入向量与 token 的嵌入向量一起输入模型,使模型理解序列的顺序。
具体流程:
- 分词(Tokenization):将输入文本按照模型的词汇表拆解为 tokens。
- 将 token 转换为对应的 ID:查找词汇表,获取每个 token 对应的 ID。
- 生成张量:将这些 ID 生成模型可以处理的张量。
- 位置嵌入:为每个 token 添加对应的位置信息。
举例说明
我们使用 Hugging Face 的开源大模型 BERT
或 GPT
,这些模型都有专门的 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 模型会自动处理位置嵌入,作为模型输入的一部分。
代码解析:
-
初始化分词器:我们加载了
BERT
的分词器BertTokenizer
,这个分词器有一个内置的词汇表。 -
文本转化为 tokens:
tokenizer.tokenize
方法将文本分解为 token,BERT 会将常见单词直接作为一个 token,但对于不常见的词会进行子词拆解(比如didn't
会被拆分为did
,n
,'t
)。 -
将 token 转化为 ID:
convert_tokens_to_ids
方法将 tokens 转化为它们在 BERT 词汇表中的唯一 ID。 -
加入特殊标记:BERT 在输入中使用特殊标记
[CLS]
(句首)和[SEP]
(句尾),用于表明序列的开始和结束。 -
张量化输入:将 token ID 列表转换为 PyTorch 张量,方便后续输入到模型中。
-
位置嵌入:在 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
执行代码:
# 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 的位置来生成一个唯一的向量。通常使用以下公式:
在代码中实现:
- 生成 Positional Encoding:创建一个矩阵,其中每个位置都会生成对应的嵌入向量。
- 将 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])
解释:
PositionalEncoding
类:这是一个计算位置编码的类,输入d_model
是嵌入向量的维度,max_len
是句子最大长度。forward
函数:将生成的位置信息与输入的 token 嵌入相加,以保留序列的顺序。- Token Embedding 和 Positional Encoding 相加:通过
token_embedding(input_tensor)
获取 token 嵌入,并使用pos_encoder
添加位置信息。 - 嵌入维度:在代码中,假设词嵌入的维度为 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 在句子中的位置信息,帮助模型理解句子结构。
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)