HuggingFace本地调用GPT2:分词器、模型结构与采样参数

做大模型应用开发,绕不开的第一个问题就是模型从哪里来。答案是HuggingFace,它相当于模型界的GitHub,上面有大量开源模型、数据集和配套工具,后面的很多工作都会围绕它展开。

本文以一个中文GPT2模型为例,用HuggingFace的transformers库把它下载到本地,看清它的文件和内部结构,再脱离网络把它调用起来。

安装只需要transformers这一个库。分词器所在的tokenizers库会在安装transformers时自动装上,不用单独安装。

bash 复制代码
pip install transformers

01 下载模型与分词器

下载模型用AutoModelForCausalLM,下载分词器用AutoTokenizer,两者都通过cache_dir参数指定保存路径。本文使用的模型是uer/gpt2-chinese-cluecorpussmall,一个在中文语料上训练的GPT2。

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer

# 模型名称和本地保存路径
model_name = "uer/gpt2-chinese-cluecorpussmall"
cache_dir = "model/uer/gpt2-chinese-cluecorpussmall"

# 下载模型
AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
# 下载分词器
AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir)

print(f"模型分词器已下载到:{cache_dir}")

下载完成后,模型文件会保存在cache_dir下的snapshots目录里,其中包含config.json的那一层就是真正的模型目录。

02 分词器:文本如何变成模型能读的数字

模型只认数字,不认文字,所以文本要先经过分词器切成一个个token,再到词表里查出每个token的编号,转换成数字之后才能输入模型。这里的词表就是模型目录里的vocab.txt,共21128行,编号就是token在vocab.txt中的编号。

这个模型的分词器类型是BertTokenizer,按字切分。以"今天天气不错,我们去旅游吧!"为例,会被切成今 / 天......。"今"在vocab.txt中的编号是791,"天"的编号是1921,所以模型实际接收到的输入就是791 / 1921......。

除了vocab.txt,分词器还靠两个文件决定自己的行为。special_tokens_map.json的作用很简单,就是告诉分词器哪个字符串扮演哪个角色。

unk_token是Unknown的缩写,词表里没有的字(生僻字、部分emoji)统一替换成UNK。

sep_token是Separator,也就是分隔符,在BERT里用来分隔两个句子,也标记句子结尾。

pad_token是Padding,也就是填充符,一个batch里的句子长短不一,短的要补到同样长度,补的就是PAD。

cls_token是Classification,放在序列开头。

mask_token用于BERT预训练,训练时会随机遮住一些字让模型去猜,遮住的位置用MASK表示。

json 复制代码
{
  "unk_token": "[UNK]",
  "sep_token": "[SEP]",
  "pad_token": "[PAD]",
  "cls_token": "[CLS]",
  "mask_token": "[MASK]"
}

tokenizer_config.json 则是分词器的行为配置,前五项特殊符号与上面相同,后面四项决定了切分时的具体处理。do_lower_case决定是否把英文统一转成小写,false表示不转。

tokenize_chinese_chars决定是否在每个汉字两侧加空格,让每个汉字单独成为一个片段。BERT分词器的第一步是按空格切词,中文没有空格,整句话会被当成一个"词"。这个开关会在每个汉字前后插入空格,于是每个汉字都成为独立片段,再去词表里查编号。

strip_accents决定是否去掉重音符号(如 é → e),null表示跟随do_lower_case,也就是如果把英文统一转成小写,则去掉重音符号,反之亦然。

model_max_length决定分词器认为的最大长度,和模型的n_positions对应。

json 复制代码
{
  "do_lower_case": false,
  "unk_token": "[UNK]",
  "sep_token": "[SEP]",
  "pad_token": "[PAD]",
  "cls_token": "[CLS]",
  "mask_token": "[MASK]",
  "tokenize_chinese_chars": true,
  "strip_accents": null,
  "model_max_length": 1024
}

03 模型结构:编号如何变成向量,再穿过12层

模型的结构由config.json决定,它记录了向量维度、层数、上下文长度、词表大小等关键参数。

n_embd是隐藏层维度,表示每个token在模型内部被表示成768维向量。n_head是注意力头数,表示模型有12个注意力头,每个头768/12 = 64 维,各自独立做一次注意力计算,最后再把12个结果拼回768维。

n_layer是Transformer解码器的层数,对应下面模型结构中的12个GPT2Block。n_positions是最大上下文长度,提示词和生成内容加起来不能超过1024个 token,超出会报错或效果崩坏。

do_sample和max_length是文本生成时的配置,do_sample表示是否开启采样生成,max_length表示生成的总长度(含提示词),提示词写得越长,实际生成的内容就越少。

vocab_size是词表大小,对应vocab.txt中的21128行。

json 复制代码
{
  "activation_function": "gelu_new",
  "architectures": [
    "GPT2LMHeadModel"
  ],
  "attn_pdrop": 0.1,
  "embd_pdrop": 0.1,
  "gradient_checkpointing": false,
  "initializer_range": 0.02,
  "layer_norm_epsilon": 1e-05,
  "model_type": "gpt2",
  "n_ctx": 1024,
  "n_embd": 768,
  "n_head": 12,
  "n_inner": null,
  "n_layer": 12,
  "n_positions": 1024,
  "output_past": true,
  "resid_pdrop": 0.1,
  "task_specific_params": {
    "text-generation": {
      "do_sample": true,
      "max_length": 320
    }
  },
  "tokenizer_class": "BertTokenizer",
  "vocab_size": 21128
}

加载模型之后,用print(model) 就能打印出完整的结构。下面是代码和打印结果,其中的注释是后加的,用来对应config.json里的参数。

python 复制代码
from transformers import AutoModelForCausalLM

# 包含 config.json 的目录(snapshots 下的哈希目录),只支持绝对路径
model_dir = r"D:\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3"

model = AutoModelForCausalLM.from_pretrained(model_dir)

# 打印模型结构
print(model)
text 复制代码
GPT2LMHeadModel(
  (transformer): GPT2Model(
    // 词嵌入,对应vocab_size和n_embd,把token编号查表变成768维向量。每个token的向量已经提前训练好,存在wte的权重矩阵里,所以这一步只是查表,不需要计算
    (wte): Embedding(21128, 768)   
    // 位置嵌入,对应 n_positions和n_embd,把位置编号(0~1023)查表变成768维向量
    (wpe): Embedding(1024, 768)    
    (drop): Dropout(p=0.1, inplace=False)
    (h): ModuleList(
      // 12个块,对应n_layer
      (0-11): 12 x GPT2Block(
        (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True, bias=True)
        (attn): GPT2Attention(
          (c_attn): Conv1D(nf=2304, nx=768)
          (c_proj): Conv1D(nf=768, nx=768)
          (attn_dropout): Dropout(p=0.1, inplace=False)
          (resid_dropout): Dropout(p=0.1, inplace=False)
        )
        (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True, bias=True)
        (mlp): GPT2MLP(
          (c_fc): Conv1D(nf=3072, nx=768)
          (c_proj): Conv1D(nf=768, nx=3072)
          (act): NewGELUActivation()
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )
    )
    (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True, bias=True)
  )
  // 输出层,输出维度21128,对应vocab_size
  (lm_head): Linear(in_features=768, out_features=21128, bias=False)   
)

这个结构里最先发生的是wte的查表,也就是向量化。分词器给出的编号只是一个序号,本身没有含义,模型要理解文本,必须把编号映射到词向量空间里,让它在其中成为一个点。

可以用男女分类来理解特征空间,以身高为横轴、头发长度为纵轴,身高高、头发短的点更可能是男性,身高矮、头发长的点更可能是女性,这两个点只有放在这个坐标系里才有了男和女的含义,换成一个没有意义的空间,同样的点就什么也表达不了。

词也是这样,可以把"今"对应的编号791想象成特征空间里的一个点,被映射到词向量空间之后,它才能代表"今"这个字,这一步在模型内部完成,作用是提取并传递语言的含义。向量化之后,向量再加上位置信息,依次穿过12个GPT2Block,最后由lm_head输出结果。

04 输出层:生成下一个字本质是分类

模型的最后一层是lm_head,它把768维的向量映射成21128个输出,数量正好等于词表大小,每个输出对应词表里一个字被选中的概率。所以文本生成模型的本质依然是分类任务,生成下一个字,就是在21128个类别里选一个。

举个例子,输入"白( )",候选字"色、天、大、下"的概率分别是 0.4、0.4、0.1、0.1,模型会按照这个概率分布去选下一个字。选出的字接在原来的输入后面,再送进模型生成下一个字,如此循环,就得到了一整段文字。

05 采样参数与离线调用

既然模型是按概率选字,就有了怎么选的问题,这正是采样参数要控制的事情。top_k=50 表示只保留概率最大的前50个候选token,top_p=0.9 表示从累积概率达到0.9的候选集合里采样,temperature=0.7控制随机性,值越低越保守,越高越多样。三者配合使用时,是在筛出来的候选里带着随机性选出一个字。

如果temperature=0,则直接取概率最大的token,也就是说提示词不变时,模型每次的输出都是相同的。不过HuggingFace目前不允许直接设置 temperature=0,需要固定输出时,应改用do_sample=False。

有了这些,就可以把模型离线调用起来了。

python 复制代码
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# 包含 config.json 的目录(snapshots 下的哈希目录),只支持绝对路径
model_dir = r"D:\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3"

# 加载模型和分词器
model = AutoModelForCausalLM.from_pretrained(model_dir)
tokenizer = AutoTokenizer.from_pretrained(model_dir)

# 创建文本生成 pipeline,device="cpu" 表示用 CPU 运行
generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device="cpu")

output = generator(
    "你好,我是一款语言模型,",  # 提示词,模型根据它生成后续文本
    max_length=50,               # 生成文本的最大长度(含输入的token),受n_positions=1024限制;如果只想限制新生成的长度,可以改用max_new_tokens
    num_return_sequences=1,      # 返回几段独立生成的文本
    truncation=True,             # 输入超过模型最大长度时是否截断
    temperature=0.7,             # 随机性,越低越保守
    top_k=50,                    # 只从概率最高的前50个词里选
    top_p=0.9,                   # 核采样,从累积概率达到0.9的词里选
    clean_up_tokenization_spaces=True,  # 是否清理分词带来的多余空格
)
print(output)

小结

回头看,GPT2生成一句话的过程是一条连贯的链路。分词器按字把文本切开,到vocab.txt里查出每个字的编号,编号进入模型后通过wte查表变成768维向量,再加上wpe提供的位置信息,依次穿过12个GPT2Block,最后由lm_head输出 21128 个概率,每个概率对应词表里的一个字。

生成下一个字就是在这21128个类别里选一个,选的方式由 temperature、top_k、top_p控制,选出的字接在输入后面,再进入下一轮,直到达到设定的长度。

相关推荐
All The Way North-4 天前
Transformers 5.19.0 首发源码研读:Tokenizer.call() 核心机制与实战验证
huggingface·transformers·qwen·tokenizer·源码解读·autotokenizer·v 5.19
北风催呀催21 天前
HuggingFace下载镜像加速技巧
linux·huggingface
zh路西法1 个月前
【上手一只MicroDuck】:(一)从机器人结构到强化学习训练框架
python·强化学习·huggingface·ppo·mujoco·microduck
微软技术分享2 个月前
基于 HuggingFace Tokenizers 训练自定义分词器
python·ubuntu·大模型·huggingface
slacker-kian2 个月前
HuggingFace API加载模型超时:用 ModelScopeAPI 替代
人工智能·python·transformer·huggingface·blip·modelscope·blipprocessor
uncle_ll3 个月前
深入探索 Hugging Face核心组件及实践
llm·nlp·bert·huggingface·hf-mirror·hf
All The Way North-4 个月前
【Transformers 实战——特征提取】从 Tokenizer 编码到 BERT 四类返回值详解与 NER 选型避坑
bert·huggingface·transformers·tokenizer·特征提取·pooler_output·last_hidden
chen_zn954 个月前
Hugging Face 无法访问的解决方案
huggingface
baidu_392113505 个月前
window如何利用hfd下载huggingface模型
huggingface