返回

LLM系列-3:模型与分词器

pipelines

Transformers 库最基础的对象就是 pipeline() 函数,它封装了预训练模型和对应的前处理和后处理环节(分词、编解码过程)。我们只需输入文本,就能得到预期的答案。那其背后具体做了什么呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 文本生成任务示例:
from transformers import pipeline

generator = pipeline("text-generation", model="distilgpt2")
results = generator(
    "In this course, we will teach you how to",
    max_length=30,
    num_return_sequences=2,
)
print(results)

其背后经过了三个步骤:

  • 预处理,将原始文本转换为模型可以接受的输入格式
  • 将处理好的输入送入模型,根据具体任务进行推理生成
  • 对模型的输出进行后处理,将其转换为人类方便阅读的格式

使用分词器进行预处理

因为神经网络模型无法直接处理文本,因此首先需要通过分词器 (tokenizer)将文本转换为模型可以理解的数字。

我们对输入文本的预处理需要与模型自身预训练时的操作完全一致,只有这样模型才可以正常地工作。注意,每个模型都有特定的预处理操作。因此我们使用 AutoTokenizer 类和它的 from_pretrained() 函数,它可以自动根据模型 checkpoint 名称来获取对应的分词器。

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

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

question = "魔都是哪个城市?"
inputs = tokenizer(question,padding=True,return_tensors='pt')
print(inputs)

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    '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_idsattention_mask,其中 input_ids 对应分词之后的 tokens 映射到的数字编号列表,而 attention_mask 则是用来标记哪些 tokens 是被填充的(这里“1”表示是原文,“0”表示是填充字符)。

checkpoint(检查点)是指在模型训练过程中,为了避免可能出现的意外情况,定期保存模型的状态,这个状态包含了所使用的模型名称、模型的参数、优化器的状态、训练步数等信息。

注意,此时的到的只是分词后token对应的ID,并没有得到经过LLM编码的高维向量。

将预处理好的输入送入模型

预训练模型的下载/加载方式和分词器 (tokenizer) 类似,Transformers 包提供了一个 AutoModel 类和对应的 from_pretrained() 函数。

1
2
3
4
from transformers import AutoModel

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
model = AutoModel.from_pretrained(checkpoint)

预训练模型的本体只包含基础的 Transformer 模块,对于给定的输入,它会输出一些神经元的值,称为 hidden states 或者特征 (features)。对于 NLP 模型来说,可以理解为是文本的高维语义表示。这些 hidden states 通常会被输入到其他的模型部分(称为 head),以完成特定的任务。

image-20250413150359777

相信你还没有理解,来看接下来这一段代码:

1
2
3
4
from transformers import AutoModelForCausalLM

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
model = AutoModelForCausalLM.from_pretrained(checkpoint)

那它们之间的区别就在于Head部分,如果使用AutoModel就相当于只是用预训练模型中最基础的 Transformer 模块,得到的是大模型的高维嵌入向量。而如果使用AutoModelForCausalLM,就相当于多加了一个Head来完成特定的任务。

Transformers 库封装了很多这样不同的结构,常见的有:

  • *Model (返回 hidden states)
  • *ForCausalLM (用于条件语言模型)【我用过的就是这种】
  • *ForMaskedLM (用于遮盖语言模型)
  • *ForMultipleChoice (用于多选任务)
  • *ForQuestionAnswering (用于自动问答任务)
  • *ForSequenceClassification (用于文本分类任务)
  • *ForTokenClassification (用于 token 分类任务,例如 NER)

Transformer 模块的输出是一个维度为 (Batch size, Sequence length, Hidden size) 的三维张量,其中 Batch size 表示每次输入的样本数量,即每次输入多少个句子,上例中为 2;Sequence length 表示文本序列的长度,即每个句子被分为多少个 token,上例中为 16;Hidden size 表示每一个 token 经过模型编码后的输出向量(语义表示)的维度。

AutoModel.from_pretrained(checkpoint)

  • 这个方法加载的是基础的预训练模型架构,不包含特定任务的头部(head)。基础模型通常会输出隐藏状态(hidden states),这些隐藏状态可以作为特征表示,用于下游任务的进一步处理。例如,在文本分类任务中,可以将这些隐藏状态输入到一个全连接层进行分类。
  • 适用于那些需要对模型进行自定义扩展或微调的场景,可以根据自己的需求添加特定任务的头部,以适应不同的任务。

AutoModelForCausalLM.from_pretrained(checkpoint)

  • 这个方法加载的是专门用于因果语言模型(Causal Language Model,CLM)任务的预训练模型。因果语言模型的目标是根据前面的上下文预测下一个词,常用于文本生成任务等。该模型已经包含了一个语言建模头部(language modeling head),可以直接用于生成文本,无需额外添加特定任务的头部。
  • 使用于文本生成任务类。

完整代码示例:

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

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

#------------------使用 AutoModel 加载基础模型-----------------
model = AutoModel.from_pretrained(checkpoint)
input_text = "Once upon a time"
inputs = tokenizer(input_text, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)

#------使用 AutoModelForSequenceClassification 加载模型--------
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
input_text = "Once upon a time"
inputs = tokenizer(input_text, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
print(outputs.logits)
print(outputs.logits.shape)

outputs.logits 是模型最终的未经过归一化处理的预测分数,如[-1.5607, 1.6123],它们并不是概率值。只是outputs中的一部分信息。如果直接print(outputs)输出的更全。

其输出分别为:

1
2
3
4
5
6
# AutoModel输出,(batch_size, sequence_length, vocab_size)
torch.Size([1, 4, 768])  

# AutoModelForSequenceClassification输出,句子的情感分类值
tensor([[-1.5607,  1.6123], grad_fn=<AddmmBackward0>)    
torch.Size([1, 2])    # 标签,positive 或 negative

补充:查看输出的最后隐藏状态形状

1
2
3
4
last_hidden_states = outputs.last_hidden_state
print(last_hidden_states.shape)
#-------------------等价于-------------------
print(outputs.last_hidden_state.shape)

对模型输出进行后处理

由于模型的输出只是一些数值,因此并不适合人类阅读。还要经过解码操作,比如以下代码可以得到生成的文本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer
import torch

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

model = AutoModelForCausalLM.from_pretrained(checkpoint)
input_text = "Once upon a time"
inputs = tokenizer(input_text, padding=True, truncation=True, return_tensors="pt")
outputs = model.generate(**inputs)
output_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(output_text)

可以看到, pipeline 背后的工作原理就是最底层的实现方式,它封装好了底层代码方便使用而已。下面会具体介绍组成 pipeline 的两个重要组件模型Models 类)和分词器Tokenizers 类)。

outputs = model(**inputs)

这种调用方式主要用于获取模型在输入数据上的中间结果,比如隐藏状态(hidden states)、未归一化的预测分数(logits)等。这些结果通常用于进一步的任务处理。

output = model.generate(**inputs)

此调用方式专门用于文本生成任务。它会基于输入的上下文,使用特定的生成策略(如贪心搜索、束搜索等)来生成后续的文本序列。

底层–模型

在大部分情况下,我们都应该使用 AutoModel 来加载模型。这样如果我们想要使用另一个模型(比如把 BERT 换成 RoBERTa),只需修改 checkpoint,其他代码可以保持不变。

所有存储在HuggingFace上的模型都可以通过 Model.from_pretrained() 来加载权重,参数可以像上面一样是 checkpoint 的名称,也可以是本地路径(预先下载的模型目录)。

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

model = BertModel.from_pretrained("./models/bert/")

如果本地没有下载该模型的权重文件,代码运行后会自动缓存下载的模型权重,默认保存到 ~/.cache/huggingface/transformers

底层–分词器

由于神经网络模型不能直接处理文本,因此我们需要先将文本转换为数字,这个过程被称为编码 (Encoding),包含两个步骤:

  1. 分词:使用分词器按某种策略将文本切分为 tokens;
  2. 映射:将 tokens 转化为对应的 token IDs(词表);

分词器的加载与模型相似,使用 Tokenizer.from_pretrained()函数。同样地,在大部分情况下我们都应该使用 AutoTokenizer 来加载分词器。

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

分词器编码-方式1

可以通过 encode() 函数将上述两个步骤合并,并且 encode() 会自动添加模型需要的特殊 token,例如 BERT 分词器会分别在序列的首尾添加 [CLS] 和 [SEP] 。

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

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

sequence = "Using a Transformer network is simple"
sequence_ids = tokenizer.encode(sequence)

print(sequence_ids)

其输出为如下,其中 101 和 102 分别是 [CLS] 和 [SEP] 对应的 token IDs。

1
[101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]

分词器编码-方式2

注意,上面这些只是为了演示。在实际编码文本时,最常见的是直接使用分词器进行处理,这样不仅会返回分词后的 token IDs,还自动包含了模型需要的其他输入。例如 BERT 分词器还会自动在输入中添加 token_type_idsattention_mask

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

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

sequence = "Using a Transformer network is simple"
sequence_text = tokenizer(sequence)

print(sequence_text)

其输出为如下

1
2
3
{'input_ids': [101, 7993, 170, 13809, 23763, 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]}

也就是说,两者区别在于:

  • tokenizer.encode 方法:返回的是编码后的 ID 列表
  • tokenizer 方法:更通用,返回一个包含更多信息的字典,如 attention_mask 、token_type_ids等

分词器解码

文本解码 (Decoding) 与编码相反,负责将 token IDs 转换回原来的字符串。注意,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分为多个 token 的单词。通过 decode() 函数解码前面生成的 token IDs:

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

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

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)

decoded_string = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102])
print(decoded_string)

输出为

1
2
Using a transformer network is simple
[CLS] Using a Transformer network is simple [SEP]

底层–Padding 与 Attention Mask

在实际中,一个 batch 包含多个输入,每个输入有长有短,而输入张量必须是严格的二维矩形,维度为 (batch size,sequence length),即每一段文本编码后的 token IDs 数量必须一样多。我们需要通过 Padding 操作,在短序列的结尾填充特殊的 padding token,使得 batch 中所有的序列都具有相同的长度。

在进行 Padding 操作时,我们必须明确告知模型哪些 token 是我们填充的,它们不应该参与编码。这就需要使用到 Attention Mask 了。它且仅由 0 和 1 组成的张量,0 表示对应位置的 token 是填充符,不参与计算。

正如前面所说,在实际使用时,应该直接使用分词器来完成包括分词、转换 token IDs、Padding、构建 Attention Mask、截断等操作。

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

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

sequences = [
    "I've been waiting for a HuggingFace course my whole life.", 
    "So have I!"
]

model_inputs = tokenizer(sequences, padding="longest", return_tensors="pt", max_length=8, truncation=True)
print(model_inputs)

分词器的输出包含了模型需要的所有输入项。包括 input_idsattention_mask

Padding 操作

Padding 操作通过 padding 参数来控制:

  • padding="longest": 将序列填充到当前 batch 中最长序列的长度;
  • padding="max_length":将所有序列填充到模型能够接受的最大长度,例如 BERT 模型就是 512。(如果代码指定了max_length的长度,就不是 512 了)
  • padding=True: 等同于 padding="longest"
1
model_inputs = tokenizer(sequences, padding="max_length")

截断操作

截断操作通过 truncation 参数来控制,如果 truncation=True,那么大于模型最大接受长度的序列都会被截断。此外,也可以通过 max_length 参数来控制截断长度。

1
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

返回格式

分词器还可以通过 return_tensors 参数指定返回的张量格式:设为 pt 则返回 PyTorch 张量;tf 则返回 TensorFlow 张量,np 则返回 NumPy 数组。

1
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")

完整格式

综上所述,实际使用分词器时,我们通常会同时进行 padding 操作和截断操作,并设置返回格式为 Pytorch 张量,这样就可以直接将分词结果送入模型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from transformers import AutoTokenizer, AutoModelForSequenceClassification

checkpoint = "xxx-models"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)

sequences = [
    "I've been waiting for a HuggingFace course my whole life.", 
    "So have I!"
]

tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
print(tokens)
output = model(**tokens)
print(output.logits)

其输出为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{'input_ids': tensor([
    [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,
      2607,  2026,  2878,  2166,  1012,   102],
    [  101,  2061,  2031,  1045,   999,   102,     0,     0,     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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])}

tensor([[-1.5607,  1.6123],
        [-3.6183,  3.9137]], grad_fn=<AddmmBackward0>)

padding=True, truncation=True 设置下,同一个 batch 中的序列都会 padding 到相同的长度,并且大于模型最大接受长度的序列会被自动截断。

底层–模型推理和输出

在将文本转换为模型可接受的输入格式后,就可以进行模型推理了。模型推理是指将预处理后的输入数据送入模型,让模型根据其内部的参数和结构进行计算,从而得到每个token包含上下文信息的高维嵌入。不同任务类型的模型会返回不同形式的输出。

1
2
output = model.generate(**inputs, max_length=50, num_beams=5, no_repeat_ngram_size=2)
output_text = tokenizer.decode(output[0], skip_special_tokens=True)

下面是一些参数解释:

  • **inputs:这是一个解包操作,inputs 通常是一个字典,包含了input_ids、attention_mask等信息。通过 **inputs 可以将字典中的键值对作为关键字参数传递给 generate() 方法,自动处理 input 的所有参数
  • 输出长度:指定生成文本的最大长度(以token为单位),当生成文本达到这个长度时,生成过程将停止
    • max_length 生成序列的最大总长度,包含了输入加输出的token总长度
    • max_new_tokens 仅关注新生成的token数量,而不考虑输入序列本身的长度
  • num_beams:束搜索(Beam Search)的束宽。束搜索会在每一步保留 num_beams 个最有可能的候选序列,可以提高生成文本的质量,从而找到更优的生成结果,但同时也会增加计算量和内存消耗
  • no_repeat_ngram_size:模型在生成过程中不允许出现重复的 n - gram 大小。n - gram 是指连续的 n 个标记组成的序列,例如当 n = 2 时,就是不允许连续的两个标记组成的序列重复出现。用于避免生成的文本中出现重复的短语或句子,提高生成文本的多样性和质量。例如,如果生成的文本中已经出现了 “the dog”,那么在后续的生成过程中就不会再出现 “the dog” 这个 2 - gram
  • output[0]model.generate() 方法返回的是一个包含多个生成序列的张量,output[0] 表示取第一个生成序列。在 num_return_sequences = 1 的情况下,通常只生成一个序列
  • skip_special_tokens=True:在解码过程中是否跳过特殊符号,如[CLS]、[SEP]。设置为 True 可以去除这些特殊标记,使生成的文本更加干净和易读

生成任务输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from transformers import AutoModelForCausalLM,AutoTokenizer

checkpoint = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint)

input_text = "The future of AI is"
inputs = tokenizer(input_text, return_tensors="pt", padding=True, truncation=True)

output = model.generate(**inputs, max_length=50, num_beams=5, no_repeat_ngram_size=2)
output_text = tokenizer.decode(output[0], skip_special_tokens=True)
print(output_text)

对于文本生成任务(如使用 AutoModelForCausalLM),模型通过 generate() 方法直接生成 token IDs,需通过分词器解码为文本。

隐藏状态提取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from transformers import AutoModel,AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModel.from_pretrained(checkpoint)

# 获取最后一层隐藏状态
input_text = "Once upon a time"
inputs = tokenizer(input_text, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
hidden_states = outputs.last_hidden_state
print("Hidden states shape:", hidden_states.shape)

# 输出 torch.Size([2, sequence_length, 768])

生成参数控制:max_lengthnum_beams(束搜索)、temperature(采样温度)等参数可调节生成结果的质量和多样性。

完整代码

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

model_name = "/home/caijinwei/disk1/Hugging-Face/deepseek-14B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name,device_map="auto",torch_dtype=torch.float16)

question = "魔都是哪个城市?"
inputs = tokenizer(
	question,
	max_length=512,
    padding='max_length',
    return_tensors='pt').to(model.device)
# inputs = tokenizer(question, padding=True, return_tensors='pt').to(model.device)

output = model.generate(
    **inputs, 
    num_beams=5, 
    max_new_tokens=1000,
    no_repeat_ngram_size=2,
    #  temperature=0.7,   # 增加创造性
   )
output_text = tokenizer.decode(output[0], skip_special_tokens=True)
print(output_text)

device_map="auto"

该参数用于指定模型在设备(如 CPU、GPU)上的分布方式。"auto" 表示让 transformers 库自动分配显卡,将模型拆分到多个 GPU 上,避免内存不足的问题。

注意,如果设置了device_map="auto",就不要在输入以下代码,这可能会覆盖之前的设备映射,导致模型被加载到单一设备上

1
2
3
# 检查是否有可用的 GPU,将模型移动到 GPU 设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

例如,下面代码不可取

1
2
3
4
5
6
7
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

question = "介绍一下上海大学。"
inputs = tokenizer(question,padding=True,return_tensors='pt').to(device)
光终究会洒在你的身上,你也会灿烂一场!
本博客已稳定运行