【LLM Course】Ch 2 - 使用 Transformer

Pipeline 的内部

使用 tokenizer 进行预处理

Transformer 无法直接处理原始文本, 需要将文本输入, 通过 tokenizer 转换为模型能够理解的数字:

  • 将输入拆分为单词、子单词或符号(如标点符号),称为 token(标记)
  • 将每个标记(token)映射到一个数字,称为 input ID(inputs ID)
  • 添加模型需要的其他输入,例如特殊标记(如 [CLS] 和 [SEP] )
    • 位置编码:指示每个标记在句子中的位置
    • 段落标记:区分不同段落的文本
    • 特殊标记:例如 [CLS] 和 [SEP] 标记,用于标识句子的开头和结尾

实战

1
2
3
4
from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
  • 下载 distilbert 作为 checkpoint, 调用它的 tokenizer
  • 然后使用 AutoTokenizer 类的 from_pretrained 这个方法, 进行实例化
1
2
3
4
5
6
raw_inputs = [
    "I've been waiting for a HuggingFace course my whole life.",
    "I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
  • 这里传入一些句子
  • 指定 tokenizer 实例的一些参数

以下是返回结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    'input_ids': tensor([
        [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
        [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
    ]), 
    'attention_mask': tensor([
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    ])
}

包含两个值:input_ids , 以及 attention_mask

探索模型

1
2
3
4
from transformers import AutoModel

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)

这里使用 Automodel 类载入一个模型

  • AutoModel 默认只加载 模型的主体(通常是 encoder 或 transformer 本体),不会附带分类头等任务相关结构

高维向量

Transformer 的矢量输出很大, 通常包含三个维度:

  • Batch size(批次大小):一次处理的序列数(在我们的示例中为 2)
  • Sequence length(序列长度):表示序列(句子)的长度(在我们的示例中为 16)
  • Hidden size(隐藏层大小):每个模型输入的向量维度
1
2
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
  • input 解包, 用 model 加载, 得到 output
  • 查看 output 的隐状态维度
1
torch.Size([2, 16, 768])

输出是这样的, 表示:

  • Batch size 为 2, 输入了两条文本数据
  • Sequence length 为 16, 是文本被分词后的序列长度
  • Hidden size 为 768, 每个 token 被编码为 768 维的向量

模型头

A Transformer network alongside its head.

  • Transformer 模型的输出会直接发给模型头进行处理
  • 模型头一般由几个线性层组成
  • 模型头围绕处理特定任务进行设计

以下是一些常见的模型头结构:

  • *Model (隐状态检索)
  • *ForCausalLM
  • *ForMaskedLM
  • *ForMultipleChoice
  • *ForQuestionAnswering
  • *ForSequenceClassification
  • *ForTokenClassification
示例
1
2
3
4
5
from transformers import AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)

这里使用序列分类头的模型, 因此可以将原先的输出分类为不同的类别

1
2
3
print(outputs.logits.shape)
---------------------------------
torch.Size([2, 2])

这是最后的输出形状, 模型头将高维向量按照两个标签的进行分类
由于是两个术语, 因此最终输出形状是 2x2

对输出进行后序处理

对于上面得到的 output.logits, 我们进一步了解和处理

1
2
3
4
print(outputs.logits)
--------------------------
tensor([[-1.5607,  1.6123],
        [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

这里我们得到的并不是最终的概率, 而是经过处理后得到的 logits
经过 softmax 处理后即可转换成概率分布

1
2
3
4
5
6
7
import torch

predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
-------------------------
tensor([[4.0195e-02, 9.5980e-01],
        [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

这里就得到了每一句对应的两个标签的预测概率分布
至于具体哪个标签对应哪个, 需要我们进一步查看模型信息

1
2
3
model.config.id2label
-----------------------
{0: 'NEGATIVE', 1: 'POSITIVE'}

模型

创建 Transformer 模型

1
2
3
4
5
6
7
from transformers import BertConfig, BertModel

# 初始化 Config 类
config = BertConfig()

# 从 Config 类初始化模型
model = BertModel(config)

这里我们初始化 Bert 模型
先加载 Config 对象, 并从 Config 类初始化模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
print(config)
---------------------
BertConfig {
  [...]
  "hidden_size": 768,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  [...]
}

这是 config 实例的详细信息

不同的加载方式

从默认配置创建

上面使用默认配置创建模型, 会使用随机值对其进行初始化
可以运行, 但是会胡言乱语, 因为未经过训练

从已有模型加载

这里使用 from_pretrained() 方法

1
2
3
from transformers import BertModel

model = BertModel.from_pretrained("bert-base-cased")

这里我们直接加载了经过预训练的 Bert 模型的参数
权重已经经过训练, 而非随机初始化
这里的 checkpoint 需要和 BertModel 相匹配

保存模型

使用 save_pretrained() 方法

1
2
3
model.save_pretrained("directory_on_my_computer")
-----------------------------------------------------
config.json pytorch_model.bin

这里是保存后的两个文件
config.json 会列出模型所需的属性, 以及一些元数据
pytorch_model.bin 被称为状态字典, 包含了模型的所有权重, 也就是模型参数

使用 Transformer 模型进行推理

model 接受 tensor 类型作为 input IDs 的输入
input IDs 是必需的参数

1
output = model(model_inputs)

这里 model_inputs 是 tensor 类型的张量

Tokenizers

  • tokenizer 将文本转换为模型可以处理的数据, 具体来说是数字
  • tokenizer 的目标是找到对于模型来说, 最有效, 为简洁的文本-数字表达方式

常见的一些 tokenization

基于单词(Word-based)的 tokenization

1
2
3
4
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
------------------------------------------------------------
['Jim', 'Henson', 'was', 'a', 'puppeteer']
  • 可以得到非常大的词汇表, 大小由语料中的 token 数确定
  • 为每个单词分配 ID, 模型通过 ID 来识别每个词
  • 这种方法会导致 ID 数量及其庞大
  • 同时, 无法捕获词语间的关系
  • 自定义 token
    • 用自定义 token 表示不再词汇表中的单词
    • 通常表示为 [UNK] 或 < unk >

基于字符(Character-based)的 tokenization

  • 把文本拆分为字符, 而非单词
  • unknown tokens 要少得多,因为每个单词都可以由字符构建
  • 有人认为字符无法表示含义
  • 模型需要处理大量的 token

基于子词(subword)的 tokenization

  • 基于一个原则 : 常用词不应被分解为更小的子词,但罕见词应被分解为有意义的子词
  • 大概就是把那种 复合词, 相对复杂的词 拆分成 词根+词缀
  • 可以在保持空间效率的同时具有语义含义

加载和保存

  • 和模型一样, 基于两种方法 : from_pretrained() 和 save_pretrained()
1
2
3
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
  • 这种是指定一种 tokenizer, 然后输出对应的 checkpoint
1
2
3
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
  • 这种是使用 AutoTokenizer , 可以根据输入的 checkpoint 自动获取正确的 tokenizer 类
1
2
3
4
5
tokenizer("Using a Transformer network is simple")
---------------------------------------------------------
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

这是分词的做法与结果

编码

  • 编码是指将文本转换成数字
  • 编码的步骤:
    • 分词
    • 转换为 inputs ID
      其实就是把 tokenizer 干的拆成了两步

分词

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)
-------------------------------------------------------------
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
  • 调用 tokenizer 的 tokenize() 方法实现
  • 输出字符串列表, 也就是 tokens

转换为 inputs ID

1
2
3
4
5
ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
----------------------------------------
[7993, 170, 11303, 1200, 2443, 1110, 3014]

转换为 tensor 后, 就可以作为模型输入了

解码

  • 与编码正好相反, 从 inputs ID 转换成字符串
1
2
3
4
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
--------------------------------------------------
'Using a Transformer network is simple'
  • 调用 tokenizer 下的 decode()方法
  • 不仅将索引转换为 token, 还把相同单词的 token 组合, 生成可读的句子

处理多个序列

将一批输入传给模型

错误示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

sequence = "I've been waiting for a HuggingFace course my whole life."

tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor(ids)
# 这一行会运行失败
model(input_ids)
----------------------
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

# input_ids 打印出来是这样的
tensor([[  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,
          2607,  2026,  2878,  2166,  1012,   102]])

这里的错误原因是: 传了一个句子, 而模型默认情况下需要一个句子列表
这里 input_ids 少了 batch 这个维度

正确方式是:

1
2
3
input_ids = torch.tensor([ids])

output = model(input_ids)

这样 id 会变成一个列表, batch size 为 1

填充输入 Padding

1
2
3
4
batched_ids = [
    [200, 200, 200],
    [200, 200]
]

这样的列表不能直接转换为张量
解决方法是, 通过填充使得张量成为标准的矩形

1
2
3
4
5
6
padding_id = 100

batched_ids = [
    [200, 200, 200],
    [200, 200, padding_id],
]

注意力掩码层 Attention Mask

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
batched_ids = [
    [200, 200, 200],
    [200, 200, tokenizer.pad_token_id],
]

attention_mask = [
    [1, 1, 1],
    [1, 1, 0],
]

outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)
  • 注意力掩码是和 inputs ID 张量形状完全相同的张量
  • 其用 0 和 1 填充
    • 1 表示应该关注的 token
    • 0 表示因该被忽略的 token

综合应用

填充

1
2
3
4
5
6
7
8
9
# 将句子序列填充到最长句子的长度
model_inputs = tokenizer(sequences, padding="longest")

# 将句子序列填充到模型的最大长度
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, padding="max_length")

# 将句子序列填充到指定的最大长度
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)

截断

1
2
3
4
5
6
7
8
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

# 将截断比模型最大长度长的句子序列
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, truncation=True)

# 将截断长于指定最大长度的句子序列
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

指定张量类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

# 返回 PyTorch tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")

# 返回 TensorFlow tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")

# 返回 NumPy arrays
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计