07_Hugging Face生态使用
1 概述
Hugging Face 是一个专注于人工智能的开源生态平台,类似 AI 界的 GitHub + 应用商店,提供了丰富的模型、数据集、应用、工具等资源。
Hugging Face 生态核心包括:
- Transformers:最核心的库,支持主流深度学习框架,包含大量预训练模型,比如
BERT、GPT、RoBERTa 等。
- AutoModel:预训练模型,包含多种模型,比如 BERT、GPT、RoBERTa 等。
- Tokenizers:每个模型都有自己搭配的分词器,支持分词、编解码、填充、截断、掩码等操作。
- Datasets:数据集库,提供丰富的数据集,可以高效加载和处理数据,支持多种数据格式,如 CSV、JSON、Arrow 等。
- Hugging Face Hub:模型仓库,提供模型、数据集、应用托管服务,可以方便地上传和下载。
使用 pip install transformers datasets 安装。
2 预训练模型
和 Transformers 这个库的名字一样,这个库提供了大量基于 Transformer 的预训练模型,以及训练和加载预训练模型所需的工具。
2.1 AutoModel
Transformers 包含大量的预训练模型,每个模型本质上都是一个基于
nn.Module
的类,如果加载每种模型都使用各自的加载方法,就十分麻烦。为了简化这一流程,Transformers
提供了统一的模型加载接口
AutoModel,用于自动下载和加载模型。
在 Hugging Face Hub 上找好所需的模型,只需要将模型的标识名称传给
AutoModel.from_pretrained() 方法即可,当然,HF
模型页面一般也会提供 Use this model 的加载代码。
1 | |
上述代码执行时,会去本地目录寻找模型文件,如果找不到,会去 Hugging Face Hub 下载所需的模型文件,这些文件会缓存在本地目录,之后再加载时会直接从本地目录加载。
执行下方代码可以查看模型缓存的位置:
1 | |
/Users/zhubaoduo/.cache/huggingface/hub
存储结构如下:
1 | |
如果不想缓存到默认目录,可以通过设置 cache_dir
参数临时指定缓存目录。
1 | |
或者设置全局生效的环境变量,在 .zshrc 文件中添加:
1 | |
Transformers 创建预训练模型的流程:
- 根据
config.json文件中的配置信息,自动识别模型类型,并自动实例化对应的模型类(创建的对象本质就是一个标准的神经网络模型)。 - 从
model.safetensors文件中加载权重参数。 - 模型创建成功,已经可以直接用于推理或微调。
使用 type() 可以看到 BERT 的模型类型为
BertModel。
1 | |
transformers.models.bert.modeling_bert.BertModel
使用 config 可以查看模型的配置信息。
1 | |
BertConfig {
"architectures": [
"BertForMaskedLM"
],
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"directionality": "bidi",
"dtype": "float32",
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 0,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"position_embedding_type": "absolute",
"transformers_version": "4.57.3",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 21128
}
2.2 AutoModelForXxx
AutoModel
只会加载模型的主干结构,不包含任何任务相关的输出层,也就是说没有特定任务,适合自定义模型结构,处理后续输出。
Transformers 还提供了很多有 任务头(Task Head)
的模型,也就是可以用于下游任务的模型AutoModelForXxx。在模型主干的基础上,添加了适配特定任务的输出层,使模型能够直接用于文本分类、实体识别等标准
NLP 任务。
常见任务对应的 Auto 模型:
AutoModelForSequenceClassification:序列分类任务,如情感分析。AutoModelForTokenClassification:序列标注任务,如词性标注。AutoModelForQuestionAnswering:抽取式问答任务,如阅读理解。AutoModelForSeq2SeqLM:序列到序列任务,如机器翻译。
AutoModelForXxx 的用法和 AutoModel
类似,这里做一个简单的演示。下面直接使用 bert-base-chinese
进行演示,由于该模型并不是预训练好的分类模型,会提示缺少权重,Transformers
会自动初始化了输出层的权重,用于后续训练和微调。
1 | |
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at google-bert/bert-base-chinese and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
使用type可以看出这次是一个BertForSequenceClassification类型。
1 | |
transformers.models.bert.modeling_bert.BertForSequenceClassification
直接使用 print(model)
可以输出模型的结构,分别查看上面两个模型,可以发现BertForSequenceClassification模型结构仅仅比BertModel多如下结构,核心就是一个全连接层,用于分类。
1 | |
2.3 输入输出格式
由于 Transformers 的预训练模型都继承 nn.Module
类,推理过程通过forward前向传播。每个模型都有自己的输入输出格式,具体使用方法可以查看直接每个模型的
forward 方法,或者查阅官方文档 Hugging Face
Transformers。
这里以 BertModel 为例介绍基本使用方法。
BertModel 的标准输入是一个字典,通常包含以下三个核心
Tensor。假设 batch_size 为 B,seq_len 为 L。
| 键名 | 形状 | 描述 |
|---|---|---|
| input_ids | (B,L) | 核心输入,词元对应的词表索引。 |
| attention_mask | (B,L) | 掩码,告诉模型哪些是真词(1),哪些是补全的 Padding(0),模型不会去注意 0 的部分。 |
| token_type_ids | (B,L) | 句段 ID,用于区分两句话。第一句全是 0,第二句全是 1。如果是单句任务,通常全是 0。 |
BertModel返回的是一个BaseModelOutputWithPoolingAndCrossAttentions对象,其中最重要的两个属性是:
last_hidden_state(最后一层隐藏状态)- 形状:(batch_size, seq_len, hidden_size)
- 含义:模型对每一个 Token 理解后的上下文向量表示。
- 用途:序列标注、词性识别,抽取式问答等需要每个 Token 上下文信息时。
pooler_output(池化输出)- 形状:(batch_size, hidden_size)
- 含义:取序列第一个位置(即
[CLS])的输出向量,经过了一个线性层和 Tanh 激活函数处理后的结果,包含整句话的语义信息。 - 用途:句子相似度计算等需要整句话的语义信息时。
上述用途并不绝对,由于
pooler是预训练的线性层,有可能对我们的任务适配并不那么好。比如文本分类任务,直觉上应该直接使用pooler的输出,但实际使用output第一个位置的输出,再接入自定义的线性层,效果会更好。
pooler 层结构如下:
1 | |
3 分词器
在 Transformers 库中,每一个预训练模型都有与之配套的
Tokenizer,可以将原始文本直接转换为模型所需的输入形式(如
input_ids、attention_mask和token_type_ids),集成了分词、编码、填充、截断、掩码等操作,搭配起来非常方便。
3.1 加载 Tokenizer
加载分词器和加载预训练模型流程基本一致,由于它们通常是配套使用的,所以模型的标识名称也是一样的。
1 | |
在模型的缓存目录中,在 snapshots
快照文件夹中,主要添加了如下 3 个软连接:
vocab.txt:词表文件,每行一个 token,保存了索引到 token 的映射关系。tokenizer_config.json:分词器配置文件,记录了分词器的行为参数。比如 do_lower_case=True(是否转小写),以及 model_max_length=512(最大长度限制)。tokenizer.json:非 Fast 版所需的 vocab.txt 和 special_tokens_map.json 的合并文件,Fast 版的分词器会优先使用这个文件。保存了所有词表映射关系和特殊字符。
同样的,这三个软链接指向 blobs 中真实的文件。
1 | |
transformers.models.bert.tokenization_bert_fast.BertTokenizerFast
使用
type查看tokenizer的类型,可以看到是BertTokenizerFast,这里有一个 Fast,莫非还有有 Slow 版本?tokenizer的确有两个版本,通过参数use_fast控制,默认为True,使用 RUST 实现,这个版本加载速度更快,False则使用 Python 的实现。
3.2 Tokenizer 使用
tokenize():对文本进行分词,返回一个列表。
1 | |
['自', '然', '语', '言', '处', '理', '非', '常', '有', '趣']
convert_tokens_to_ids(): 将 tokens 列表转为词表对应的索引列表。
1 | |
[5632, 4197, 6427, 6241, 1905, 4415, 7478, 2382, 3300, 6637]
convert_ids_to_tokens():将索引列表转换成对应的 tokens 列表。
1 | |
['自', '然', '语', '言', '处', '理', '非', '常', '有', '趣']
encode():对文本进行编码,先分词再映射为索引,返回编码后的列表。一般会添加特殊标记,比如[CLS]和[SEP]。
1 | |
[101, 5632, 4197, 6427, 6241, 1905, 4415, 7478, 2382, 3300, 6637, 102]
decode():对索引列表进行解码,返回对应的原始文本。
1 | |
'[CLS] 自 然 语 言 处 理 非 常 有 趣 [SEP]'
将文本转换为输入的方法还有 encode_plus() 和批处理的
batch_encode_plus,这里更加推荐使用它们的集合体———tokenizer()。
tokenizer():也就是__call__方法,允许将对象当做方法使用,这个方法进行了高度封装,自动完成了分词、转索引、加特殊符号,支持填充、截断,并且自动生成 mask 等,支持批处理,能够直接构造模型所需的所有输入,非常方便。
1 | |
返回值是一个字典,包括模型所需的所有输入,可以通过inputs['键名']访问,每个值默认都是一个列表。
1 | |
除了直接使用tokenizer(),该方法还支持非常多的参数,实现填充截断等功能,这里介绍几个常用的参数。更多参数请查阅官方文档
padding:填充True:填充到当前批最大长度False:不填充(默认)max_length:填充到max_length参数的指定长度
truncation:截断True:截断到max_length参数指定的长度False:不截断(默认)
max_length:最大长度return_tensors:字典中每个值的类型
1 | |
{'input_ids': tensor([[ 101, 5632, 4197, 6427, 6241, 1905, 4415, 102],
[ 101, 7478, 2382, 3300, 6637, 102, 0, 0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 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, 0, 0]])}
注意:这里的批次和
DataLoader的批次是两码事,DataLoader打乱数据后,每个批次内的长度可能不同,导致报错,具体解决方案在第 5 节工作流中介绍。
tokenizer
处理的字典直接解包传入模型,即可得到模型输出。
1 | |
<class 'transformers.modeling_outputs.BaseModelOutputWithPoolingAndCrossAttentions'>
模型输出是一个 ModelOutput 类型的对象
BaseModelOutputWithPoolingAndCrossAttentions,它是 Hugging
Face
专门设计的一个混合容器,不仅拥有字典的功能,还拥有对象和元组的功能。
1 | |
last_hidden_state: torch.Size([2, 8, 768])
last_hidden_state: torch.Size([2, 8, 768])
pooler_output: torch.Size([2, 768])
4 Datasets
datasets 是 Hugging Face 生态系统中的另一块基石。如果说 transformers 是用来搞定模型和算法的,那么 datasets 就是专门用来搞定数据的。
datasets 可以从 Hugging Face Hub 中一键加载数据集,或者从本地加载各种类型的文件。加载后的结构类似字典,可以划分训练集、验证集、测试集,每个数据集都是一个清晰的类表格结构。datasets 还支持强大的数据处理功能,可以批量处理数据。
相较于 Pandas 一次性把数据集读入内存中,datasets 底层使用 Apache Arrow 格式和操作系统的 Memory Mapping (mmap) 技术,将数据存储在硬盘上,操作系统将其映射到虚拟内存,即使是普通的笔记本电脑,也能流畅地浏览和处理几百 GB 的数据集,而且读取速度极快。
4.1 加载 Datasets
在 Hugging Face Hub
找好需要的数据集,可以直接使用load_dataset联网下载数据集,下载好的数据集会缓存到和模型相同的根目录下,文件结构也是类似的,这里不再赘述。
1 | |
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 9600
})
validation: Dataset({
features: ['text', 'label'],
num_rows: 1200
})
test: Dataset({
features: ['text', 'label'],
num_rows: 1200
})
})
load_dataset返回的是DatasetDict对象,该对象是字典结构,可以包括多个数据集。
加载本地数据时需要声明文件类型,如
CSV、JSON、Parquet,并通过data_files参数指定文件路径。如果只有一个文件,那么默认是键为train的数据集。
1 | |
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 16000
})
})
如果有多个数据集,需要以字典形式传入data_files,键为数据集名称,值为数据集路径。返回值为包含多个Dataset的DatasetDict对象,每个Dataset称为一个
split。
1 | |
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 16000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 4000
})
})
4.2 Datasets 使用
4.2.1 数据格式
dataset_dict['键名']:获取数据集。
1 | |
Dataset({
features: ['text', 'label'],
num_rows: 9600
})
dataset[索引/列名]:获取数据集的样本。
1 | |
第 0 个样本:
{'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', 'label': 1}
按列名访问:
Column(['选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', '15.4寸笔记本的键盘确实爽,基本跟台式机差不多了,蛮喜欢数字小键盘,输数字特方便,样子也很美观,做工也相当不错', '房间太小。其他的都一般。。。。。。。。。', '1.接电源没有几分钟,电源适配器热的不行. 2.摄像头用不起来. 3.机盖的钢琴漆,手不能摸,一摸一个印. 4.硬盘分区不好办.', '今天才知道这书还有第6卷,真有点郁闷:为什么同一套书有两种版本呢?当当网是不是该跟出版社商量商量,单独出个第6卷,让我们的孩子不会有所遗憾。'])
查看具体样本:
选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般
切片取前 3 项:
{'text': ['选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', '15.4寸笔记本的键盘确实爽,基本跟台式机差不多了,蛮喜欢数字小键盘,输数字特方便,样子也很美观,做工也相当不错', '房间太小。其他的都一般。。。。。。。。。'], 'label': [1, 1, 0]}
Dataset的使用类似 Pandas
和字典的结合。它的数据是按照列进行组织的,可以通过键名(列名)访问列数据。如果取一批样本,它们是字典形式,键对应列名,值为列数据的列表。这种组织方式有利于后续和Tokenizer联动。
1
2
3
4{
'feature1': ['text1', 'text2', 'text3'],
'feature2': [label1, label2, label3]
}
features:查看数据集的列,以及列的数据类型。
1 | |
{'text': Value('string'), 'label': Value('int64')}
ClassLabel():类标签数据类型,专门处理分类数据,维护索引到字符串标签的双向映射。
1 | |
字符串转整数: positive -> 1
整数转字符串: 0 -> negative
cast_column():转换数据集中某列的数据类型,返回新的数据集对象。
1 | |
新的特征类型: {'text': Value('string'), 'label': ClassLabel(names=['negative', 'positive'])}
1 代表标签: positive
如果获取的数据集类别列是
string类型,转换为ClassLabel类型,会自动将字符串映射为整数标签,便于后续模型训练,并且这一操作会改变底层数据。如果获取的数据集类别列是
int类型,可以直接用于训练,那么转换为ClassLabel之后,并不会修改底层数据,而是通过元数据注入的方式建立双向映射,既方便人能读懂,又可以让Trainer自动生成带标签配置的模型
4.2.2 数据预处理
train_test_split():划分训练集和测试集,不需要再导入sklearn库。
1 | |
remove_columns(): 删除数据集中的列,返回新的 dataset 对象。
1 | |
Dataset({
features: ['text'],
num_rows: 7680
})
filter():根据条件筛选数据,返回过滤后的新数据集对象。
1 | |
Filter: 100%|██████████| 7680/7680 [00:00<00:00, 305430.76 examples/s]
Dataset({
features: ['text', 'label'],
num_rows: 3839
})
map():最核心的方法,功能非常强大,可以多进程、批量化的处理数据,返回一个新的数据集对象。比如常常搭配 tokenizer 使用,批量处理数据集为可以输入模型的格式。
Datasets为了把数据放入硬盘上,存储的是 Arrow 格式的文件。Arrow 格式只认识基础类型(Integers, Floats, Lists, Strings),它不认识 PyTorch Tensor。所以即使在map中返回tensor类型,数据存储时也会变为基础类型,建议在放入DataLoader前使用set_format函数转换。
1 | |
Map (num_proc=4): 100%|██████████| 7680/7680 [00:00<00:00, 18073.44 examples/s]
Casting the dataset: 100%|██████████| 7680/7680 [00:00<00:00, 2740768.72 examples/s]
Dataset({
features: ['input_ids', 'attention_mask', 'labels'],
num_rows: 7680
})
实际没有转为张量: <class 'list'>
注意:使用
map,filter,shuffle,select等数据处理方法时,由于返回的是新对象,无论使用dataset还是dataset_dict接收这个新对象,它们都会因此独立,互不影响。也就是说,修改dataset的数据,并不会影响dataset_dict的数据,反过来也是如此。希望改变谁的数据,就使用谁来接收,比如想要改变dataset_dict['train']的数据,需要使用dataset_dict['train']的方式接收。
map 方法会对数据集中的每个样本执行指定的
function。该函数的返回值是一个字典,它将以增量更新的方式合并到原样本中:
- 新增字段:如果返回的键在原样本中不存在,则作为新列添加。
- 更新字段:如果返回的键在原样本中已存在,则覆盖原有值。
注意:它不会直接丢弃原样本中未被返回的字段,除非显式指定
remove_columns。
set_format(): 设置数据格式,喂给模型前转为tensor。原地修改,不返回新对象。
1 | |
{'input_ids': tensor([ 101, 6848, 2885, 4638, 752, 891, 1922, 4895, 1936, 749, 8024, 1930,
1920, 749, 2552, 4415, 1486, 6418, 4638, 4385, 2141, 2692, 721, 8024,
6375, 782, 1927, 1343, 749, 928, 818, 102]),
'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,
1, 1, 1, 1, 1, 1, 1, 1]),
'labels': tensor(0)}
注意:
set_format只是改变了通过__getitem__(访问dataset[i])获取数据的格式,而不会修改底层的数据存储格式。不过使用 Arrow 格式存储时,元数据 state.json 会记录数据的_format_type。
4.3 保存和加载
to_csv():将 Dataset 保存为 CSV 文件。
1 | |
Creating CSV from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 293.27ba/s]
595543
to_json():将 Dataset 保存为 JSON 文件。
1 | |
Creating json from Arrow format: 100%|██████████| 2/2 [00:00<00:00, 291.38ba/s]
1207798
上述文件的加载方式使用 4.1 介绍的 load_dataset
即可。
save_to_disk():保存 Arrow 格式,支持 DatasetDict 和 Dataset,是官方最推荐的保存方式。
1 | |
Saving the dataset (1/1 shards): 100%|██████████| 7680/7680 [00:00<00:00, 2539397.30 examples/s]
Saving the dataset (1/1 shards): 100%|██████████| 1920/1920 [00:00<00:00, 528173.65 examples/s]
每个 split 都会生成一个文件,里面有 arrow 数据文件和相应的元数据文件。
1 | |
load_from_disk():加载 Arrow 格式的数据文件。
1 | |
5 工作流
5.1 搭配 DataLoader
Hugging Face 生态可以和 PyTorch 无缝集成,使用 Datasets
获取数据并进行预处理,搭配 Tokenizer
将源数据转换为可以输入到模型的格式,Datasets 结合 PyTorch
的 DataLoader 可以实现数据的批量处理,将批量数据送入
Transformers 中的各种预训练模型当中。
1 | |
batch keys: dict_keys(['input_ids', 'attention_mask', 'labels'])
input_ids shape: torch.Size([16, 32])
attention_mask shape: torch.Size([16, 32])
labels shape: torch.Size([16])
===============Bert分类模型===============
inputs keys: dict_keys(['input_ids', 'attention_mask', 'labels'])
outputs keys: odict_keys(['loss', 'logits'])
loss: tensor(2.3796, grad_fn=<NllLossBackward0>)
logits shape: torch.Size([16, 10])
=================Bert模型================
inputs keys: dict_keys(['input_ids', 'attention_mask'])
outputs keys: odict_keys(['last_hidden_state', 'pooler_output'])
last_hidden_state shape: torch.Size([16, 32, 768])
pooler_output shape: torch.Size([16, 768])
Dataset并未继承torch.utils.data.Dataset,但由于实现了__len__()和__getitem__()这两个核心接口,因此能够被DataLoader正确识别批量迭代。
5.2 注意事项
上面也提到过,Tokenizer 填充时的批次和
DataLoader 加载时的批次是不同的,如果直接使用
DataLoader 去加载 Tokenizer
填充的数据,可能会由于批次不同导致内部数据长度不一,从而导致
DataLoader 抛出异常。
这里主要有两种解决方案:
方法一:使用动态填充(DataCollator),这是 Hugging Face
的标准做法,既省显存,又不会报错。
不要在预处理(map)阶段把所有数据死板地补齐,而是把补齐的工作推迟到数据加载(DataLoader)阶段。使用
DataCollator 在每个 Batch
形成时,动态地将这个批次补齐到当前 Batch 最长的长度。
1 | |
如果采用这种方式在数据加载阶段填充,那么在数据预处理时
tokenizer的padding参数应该设置为False,避免浪费空间。
方法二:静态填充到统一长度,在数据预处理时将 tokenizer
的填充参数设为 padding='max_length'。
由于所有数据都是统一长度,即使 DataLoader
随意打乱,每一批的数据长度也都是一致的。
1 | |