LLM 上层设施 Huggingface Transformers

DataWhale 的文档里写了推荐换成 Local LLM (计费确实有点贵),然后精讲了 baseline 源码。这里这一章是关于 Huggingface Transformers。

Hugging Face Transformers 已经事实上统一了 NLP 领域的模型调用接口,基本所有的公开 LLM 都可以通过 Transformers 调用,或提供了 Transformers 的代码。

由于本项目是 NLP 任务,所以我们只介绍 Transformers 的 NLP 部分。

Pipeline

Transformers 提供了一个简单的 Pipeline 类,可以直接调用模型,比直接使用模型更方便。

Hugging face 的 Pipeline 有三部分组成:Tokenizer、Model、Post-Processor。Tokenizer 用于将输入转换为模型的输入,Model 用于处理输入,Post-Processor 用于将模型的输出转换为人类可读的输出。

不过实际使用时,我们不需要太关注这些细节,只需要调用 pipeline 函数即可。这里我们只介绍text-generation任务,其他任务需要参考官方文档。

from transformers import pipeline

text_generation = pipeline("text-generation", model="gpt2")

pipeline 的第一个参数是任务的类型,第二个参数是模型的名字,可以是模型的名字,也可以是模型的路径。一般来说,它会自动找到模型的 Tokenizer 和 Post-Processor。而且大部分时候,你也不需要制定任务的名字,因为 Transformers 会自动识别。

此外还有一些重要的参数,比如 device,用于制定模型运行的设备;device_map,用于指定模型的输入输出在哪个设备上,不过一般用auto就好;torch_dtype,可以使用torch.bfloat16来减少显存占用。

对于 text-generation 任务,返回的是一个TextGenerationPipeline对象。要使用它,可以直接。

generator(
    "你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为\"答案是:A\"。",
    max_new_tokens=24
)

如果你需要更详细地参数说明,请参考官方文档

不过很可惜 Hugging Face 接口使用的是kwargs,所以 IDE 的提示可能不太友好。

langchain-huggingface

langchain-huggingface 库提供了从 Huggingface Pipeline 到 Langchain 的接口,可以直接调用 Huggingface 的模型。

from langchain_huggingface import HuggingFacePipeline

model = HuggingFacePipeline(pipeline=generator)

这样就能集成到 Langchain 里了。

Transformers 模型标准

虽然 Pipeline 提供了一个很 nice 的抽象,但如果不是做上层应用,例如做微调、模型结构修改等,还是需要直接调用 Transformers 的模型。Transformers 的模型本质上就是一个 PyTorch 模型,只是有一些固定的方法。如果对模型做一些底层的操作,本质上就是回到 PyTorch 的世界。

Transformers 的文档对于应用讲的很详细,不过一些底层的细节有点难找,这里我们简单介绍一下。同样的,这里我们只讲纯 NLP 的模型。

在介绍 Transformers 各个任务的模型类前,我们现复习一下 Transformer Decoder 的架构,因为后面我们会用到。

Transformer 接受一个待补全的序列,生成对应位置的下一个位置 token id。待补全的序列首先经过 Embedding 和 Positional Encoding,然后经过多层的 Decoder Block,最后经过一个线性层,输出每个位置的 token 的概率。

对于每一个 Decoder Block,如果是 Decoder-Only 的架构,那么每一层的输入都是(batch_size, sequence_length, hidden_size)形的张量,逐次经过 Self-Attention、LayerNorm、FeedForward、LayerNorm。如果是 Encoder-Decoder 的架构,那么每一层的输入是(batch_size, sequence_length, hidden_size)(batch_size, sequence_length, hidden_size)形的张量,分别表示 Query 和 Key-Value,然后经过 Multi-Head Attention、LayerNorm、Cross-Attention、LayerNorm、FeedForward、LayerNorm。每一层传递的这个张量就是hidden_states

Transformers 的模型有 3 部分,Tokenizer、Model、Model Config,分别负责分词与编码、模型、模型的配置。Tokenizer 用于将输入转换为模型的输入,Model 用于处理输入,Model Config 用于配置模型。

先介绍最复杂最核心的部分,Model。

Transformers 模型的加载

加载模型使用from_pretrained方法,这个方法会自动下载模型的权重。这个方法的参数是模型的名字或路径,还有一些额外的参数,例如config,用于指定模型的配置;cache_dir,用于指定缓存的位置;revision,用于指定模型的版本。

from transformers import BertModel

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

也可以用路径加载模型。

model = BertModel.from_pretrained("path/to/model")

可以用AutoModel加载模型,这个方法会自动识别模型的类型。

from transformers import AutoModel

model = AutoModel.from_pretrained("bert-base-chinese")

加载的模型即是对应框架的模型,如 PyTorch 的(nn.Module),只是有一些额外的继承和实现关系。

其它的 Tokenizer 和 Config 也可以用同样的方法加载。

from transformers import BertTokenizer, BertConfig

tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
config = BertConfig.from_pretrained("bert-base-chinese")

from_pretrained 方法可以指定devicedevice_maptorch_dtype等参数,用于指定模型运行的设备,数据类型等。

Transformers 模型的保存

Transformers 的模型可以用save_pretrained方法保存,这种方法会自动保存模型的权重和配置。

model.save_pretrained("path/to/model")

基于不同的模型,会保存成不同的格式,例如pytorch_model.binconfig.json等。

现在一般推荐使用 Hugging Face 的 Safe Tensor 格式,这种格式会保存模型的权重和配置,而且可以跨框架使用。

model.save_pretrained("path/to/model", save_format="safe")

Transformers 模型的输入

不同模型的forward函数参数略有不同,常见的有以下几个,这里面,除了input_idsattention_mask(也可以不使用 attention mask),其它都调好了默认值,一般不需要修改。

  • input_ids,输入的 token id,形状为(batch_size, sequence_length)
  • attention_mask,注意力掩码,形状为(batch_size, sequence_length),用于指定哪些位置是 padding 的。如果是 1,表示不是 padding;如果是 0,表示是 padding。
  • position_ids,位置 id,形状为(batch_size, sequence_length),用于指定每个 token 的位置。可参考文档。简单而言,就是每个 token 的位置。这样可以使得模型不从第零个 token 开始,而是从中间开始,对于 pad 也可以跳过。
  • past_key_values,用于加速解码的缓存。这个参数是一个元组,每个元素是一个元组,每个元组包含两个张量,形状为(batch_size, num_heads, sequence_length, embed_size_per_head)。这个参数是用于加速解码的。
  • inputs_embeds,输入的嵌入,形状为(batch_size, sequence_length, hidden_size)。如果你不想使用模型内部的嵌入,可以自己传入嵌入。
  • use_cache,是否使用缓存。如果设置为True,则会返回past_key_values,用于加速解码。
  • output_attentions,是否返回注意力权重。如果设置为True,则会返回每一层的注意力权重。
  • output_hidden_states,是否返回隐藏状态。如果设置为True,则会返回每一层的隐藏状态。
  • return_dict,是否返回ModelOutput。如果设置为True,则会返回ModelOutput,否则会返回一个元组。
  • cache_position,被缓存的位置。这个参数是一个张量,形状为(sequence_length),用于更新缓存的位置,这个缓存的位置不受 padding 影响。

有些模型的forward会支持labels,用于计算 loss。不同模型对labels的要求不同。一般来说,对于生成任务,labels是下一个 token 的 id,即input_ids向右移动一位;而对于序列分类任务,labels是分类的 id。基础模型一般没有labels参数。

Transformer 模型的输出

Transformers 根据模型的 task 和具体的模型提供不同的类,不同的类有不同的输出。

模型名通常是框架在前面,不过因为大部分都是使用 PyTorch,因此没有前缀就是 PyTorch;模型的名称在中间;任务名在后/例如BertForSequenceClassification,即用于序列分类的 BERT 模型,因为它是基于 PyTorch 的,所以没有前缀。模型名称可以用AutoModel,任务名可以用AutoModelForSequenceClassification,这样就可以自动识别模型。

如果需要查阅其它类型,首先查看 Task Guide 了解该任务的模型应当如何使用,然后选择一个具体的模型查看其文档(该文档为 llama2)

常见的有任务有以下几种:

基础模型 无后缀

例如BertModel,用于生成模型的基类。这类模型就是最基本的 PyTorch 模型,就只有 forward方法。

这类模型的forward返回BaseModelOutput或包含有额外字段(取决于模型)的对象。它包含了如下的字段:

  • last_hidden_state,最后一层输入的隐藏状态,(batch_size, sequence_length, hidden_size)形的张量。
  • hidden_states,最后一层输出的隐藏状态,(batch_size, sequence_length, hidden_size)形的张量。
  • pooler_output,即hidden_states经过在序列长度维度上池化后的结果,(batch_size, hidden_size)形的张量,用于分类任务。

文字生成模型 ForCausalLM

例如GPT2ForCausalLM,这类模型的任务是生成模型,即给定前面的文本,预测下一个词。这类模型会有一个generate方法,用于生成一段回复。具体generate的参数参考文档generate来自GenerationMixin,本质上是对forward进行了封装。

相对于基础模型,这类模型在基础模型后添加了一层线性层,一层 Un-Embedding 层,用于生成下一个词。

这类模型的forward方法返回一个CausalLMOutput或包含有额外字段(取决于模型)的对象。它包含了如下的字段:

  • loss,如果提供了labels,则会返回 loss。label是下一个词的标签,形状为(batch_size, sequence_length),是一个整数张量,每个元素是当前位置下一个 token id。
  • logits,预测的概率,(batch_size, sequence_length, vocab_size)形的张量。
  • hidden_states,每一层的隐藏状态,(batch_size, sequence_length, hidden_size)形的张量。
  • attentions,每一层的注意力权重,(batch_size, num_heads, sequence_length, sequence_length)形的张量。

后面两个是在模型内部使用的,一般不需要关心。

文段分类模型 ForSequenceClassification

例如BertForSequenceClassification,这类模型的任务是序列分类,即给定一个序列,预测它的类别。

相比于基础模型,这类模型在基础模型后先做了一个池化操作,然后再接一个线性层,用于分类。

这类模型的forward方法返回SequenceClassifierOutput或包含有额外字段(取决于模型)的对象。它包含了如下的字段:

  • loss,如果提供了labels,则会返回 loss。label是分类的标签,形状为(batch_size,),是一个整数张量,每个元素是当前位置的分类 id。
  • logits,预测的概率,(batch_size, num_labels)形的张量。
  • hidden_states,每一层的隐藏状态,(batch_size, sequence_length, hidden_size)形的张量。
  • attentions,每一层的注意力权重,(batch_size, num_heads, sequence_length, sequence_length)形的张量。

同样的,有用的是logitsloss

Transformers 模型的配置

Transformers 的模型配置是一个类,用于配置模型的参数。这个类的名字通常是模型名加Config,例如BertConfig。这个类的参数是模型的参数,例如hidden_sizenum_layers等。

这个类的实例可以通过to_dict方法转换为字典,用于保存模型的配置。如果你本地下载了文件,一般有一个config.json文件,就是这个字典的内容。

这个配置文件里除了含有模型的参数,还有一些额外的参数,例如vocab_size,此外对于分类模型,还有num_labelsid2labellabel2id等。

Tokenizer

Tokenizer 是将人类可读的文本转换为模型可读的 token id 的类。这个类的名字通常是模型名加Tokenizer,例如BertTokenizer。这个类的实例可以通过from_pretrained方法加载预训练模型的配置。

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")

Tokenizer 有两个主要的方法,encodedecodeencode方法将文本转换为 token id,decode方法将 token id 转换为文本。后者只会返回一个数字列表。

不过,做 encoding 时,一般是直接调用对象。

tokenizer("你好")

这个方法返回一个Encoding对象,包含了如下的两个重要字段:

  • input_ids,输入的 token id,形状为(sequence_length,)
  • attention_mask,注意力掩码,形状为(sequence_length,),用于指定哪些位置是 padding 的。如果是 1,表示不是 padding;如果是 0,表示是 padding。

可以直接喂给模型。

decode方法可以将 token id 转换为文本。

tokenizer.decode([101, 872, 1962, 102])

这个方法返回一个字符串。

小结

这一章是对 Huggingface Transformers 库的简单介绍和使用,以及如何将其集成到 Langchain 中。