本文记录使用PyTorch、HuggingFace/Transformers 框架工作流程,仅供参考。
官网地址为Hugging Face。目前各种Pretrained的Transformer模型层出不穷,虽然这些模型都有开源代码,但是它们的实现各不相同,我们在对比不同模型时也会很麻烦。Huggingface Transformers能够帮我们跟踪流行的新模型,并且提供统一的代码风格来使用BERT、XLNet和GPT等等各种不同的模型。而且它有一个模型仓库,所有常见的预训练模型和不同任务上fine-tuning的模型都可以在这里方便的下载。
pip install transformers
如果要安装最新版本,可以
git clone https://github.com/huggingface/transformers.git
cd transformers
pip install -e
使用pipeline直接调用
使用预训练模型最简单的方法就是使用pipeline函数,它支持如下的任务:
下面举一个情感分析的例子:
from transformers import pipeline
classifier = pipeline('sentiment-analysis')
当第一次运行的时候,它会下载预训练模型和分词器(tokenizer)并且缓存下来。分词器的左右是把文本处理成整数序列。最终运行的结果为:
[{'label': 'POSITIVE', 'score': 0.9997795224189758}]
我们也可以一次预测多个结果:
results = classifier(["We are very happy to show you the Transformers library.",
"We hope you don't hate it."])
for result in results:
print(f"label: {result['label']}, with score: {round(result['score'], 4)}")
运行结果为:
label: POSITIVE, with score: 0.9998
label: NEGATIVE, with score: 0.5309
更多的pipeline使用例子详情参见官网教程,此处不再赘述。
该框架的易用性体现在以下几个方面:
因此它不是一个基础的神经网络库来一步一步构造Transformer,而是把常见的Transformer模型封装成一个building block,我们可以方便的在PyTorch或者TensorFlow里使用它。
数据读取
Datasets library 提供给了数据集快速下载的简单方式:
from datasets import load_dataset
raw_datasets = load_dataset("glue", "mrpc", cache_dir = '~/.cache/huggingface/dataset')
raw_datasets
可以得到结果:
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})
其中load_dataset
方法, 可以从不同的地方构建数据集
我们可以查看数据内容
raw_train_dataset = raw_datasets["train"]
raw_train_dataset[0]
>>>
{'idx': 0,
'label': 1,
'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberat
我们可以看到 label 已经变为了 数字,我们不需要做任何的预处理。
如果我们需要查看这个数字属于哪个类别,可以使用dataset 的 features
属性:
raw_train_dataset.features
>>>
{'sentence1': Value(dtype='string', id=None),
'sentence2': Value(dtype='string', id=None),
'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None),
'idx': Value(dtype='int32', id=None)}
其实在我们平时的使用过程中,大概率是要读取外部数据集的,我通常会用以下的方法读取,
# Load the dataset
data_files = {}
data_path = DATA_PATH
train_file = data_path + "train.json"
data_files["train"] = train_file
extension = train_file.split(".")[-1]
valid_file = data_path + "dev.json"
data_files["validation"] = valid_file
test_file = data_path + "test.json"
data_files["test"] = test_file
raw_datasets = load_dataset(extension, data_files=data_files)
model.resize_token_embeddings(len(tokenizer))
不过,如果出现报错,我们需要检查数据集的格式,是否是utf-8
编码,以及如果是json文件,还需要关注最后的逗号等标点的位置。
对于数据,我们需要对数据进行tokenizer,选择合适的tokenizer,对于输入:
tokenizer = BertTokenizerFast.from_pretrained(PATH, cache_dir=PATH)
inputs = tokenizer("This is the first sentence.", "This is the second one.")
inputs
>>>
{
'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
另外,值得注意的是from_pretrained(cache_dir)
中cache_dir
是存放自动下载的model/tokenizer文件与缓存的路径。
如果我们需要对训练集进行预处理:
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
return_tensors="pt"
)
这样子使用是OK的,但是这样子处理之后,tokenized_dataset不再是一个dataset格式。而且是一旦我们的dataset 过大,无法放在 RAM 中,那么这样子的做法会导致 Out of Memory 的异常。
然而 Datasets库使用的是 Apache Arrow 文件格式,所以你只需要加载你需要的样本到内存中就行了,不需要全量加载。
为了使我们的数据保持dataset的格式,我们需要使用 Dataset.map
方法。这能够使我们有更大的灵活度,我们直接给 map
函数传入一个函数,这个函数传入 dataset 的一个样本,然后返回映射后的结果:
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"],
truncation=True,
return_tensors="pt")
这个函数传入一个字典型的 example,然后传出一个新的字典,包含了key:input_ids, attention_mask, and token_type_ids.
注意:我们这边没有使用padding,这是因为把所有的样本padding到最大的长度效率很低,通常我们会在构造batch的时候才进行padding,因为我们只需要padding到这个batch中的最大长度,而不是训练的最大长度。这个可以节省非常多的预处理时间。
我们可以设置参数 batched=True
,这样子我们的函数可以一下子处理多条数据,而不是每次一条一条地处理。
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets
数据映射完了之后,我们会发现,我们会新增一些字典的key,即input_ids, attention_mask, and token_type_ids.
DatasetDict({
train: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 3668
})
validation: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 408
})
test: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 1725
})
})
我们可以在 Dataset.map
中使用 num_proc 参数. 我们这边没有使用是因为我们的tokenizer 使用了 use_fast=True
参数,所以采用的是多线程的方式处理样本。如果没有使用 fast tokenizer,则可以使用num_proc 参数提高速度。
最后我们需要对于一个batch 的输入进行padding,这边使用的 dynamic padding 的方式。每个batch 都padding 到这个batch 最长的长度。
另外,这里也提一下自己训练tokenizer的方法。
此处选择训练一个BertWordPieceTokenizer的分词器,由于Bert和Albert大致相似,因此分词器上选择BertWordPieceTokenizer不会有问题。
from tokenizers import BertWordPieceTokenizer
files = "./lunyu.txt" # 训练文本文件
vocab_size = 10000
min_frequency = 2
limit_alphabet = 10000
special_tokens = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] #适用于Bert和Albert
# Initialize a tokenizer
tokenizer = BertWordPieceTokenizer(
clean_text=True, handle_chinese_chars=True, strip_accents=True, lowercase=True,
)
# Customize training
tokenizer.train(
files,
vocab_size = vocab_size,
min_frequency=min_frequency,
show_progress=True,
special_tokens=special_tokens,
limit_alphabet=limit_alphabet,
wordpieces_prefix="##"
)
然后把分词器保存在硬盘中:
!mkdir tokenizer
tokenizer.save("tokenizer")
这样,我们就拥有了一个针对中文文本的分词器,这会对于论语类文本的应用更加合适,这里还可以利用tokenizers包来测试一下分词效果:
from tokenizers.implementations import BertWordPieceTokenizer
from tokenizers.processors import BertProcessing
tokenizer = BertWordPieceTokenizer(
"./tokenizer/vocab.txt",
)
tokenizer._tokenizer.post_processor = BertProcessing(
("[CLS]", tokenizer.token_to_id("[SEP]")),
("[SEP]", tokenizer.token_to_id("[CLS]")),
)
tokenizer.enable_truncation(max_length=512)
tokenizer.encode("子曰:学而时习之。").tokens
这样,可以对子曰:学而时习之。
这句话进行分词,由于我们用的是BertWordPieceTokenizer,对于中文来说,就是对每一个字进行分词。得到结果:
['[SEP]', '子', '曰', ':', '学', '而', '时', '习', '之', '。', '[CLS]']
from transformers import (
MBartConfig,
MBartForConditionalGeneration,
MBartTokenizer,
MBartTokenizerFast,
BertTokenizerFast,
Seq2SeqTrainer,
Seq2SeqTrainingArguments,
default_data_collator,
set_seed
)
configuration = MBartConfig(
vocab_size=50000,
d_model=512,
encoder_layers=6,
decoder_layers=6,
encoder_attention_heads=8,
decoder_attention_heads=8,
decoder_ffn_dim=2048,
encoder_ffn_dim=2048,
dropout=0.3,
activation_function='gelu'
)
# Set seed before initializing model.
set_seed(42)
model = MBartForConditionalGeneration.from_pretrained(model_checkpoint,
cache_dir="/data0/xp/gec/model")
model.resize_token_embeddings(len(tokenizer))
configuration = model.config
# Data collator
data_collator = DataCollatorForSeq2Seq(
tokenizer,
model=model,
)
# Definite training arguments
training_args = Seq2SeqTrainingArguments(
output_dir=OUTPUT_DIR,
learning_rate=1e-7,
per_device_train_batch_size=64,
per_device_eval_batch_size=64,
num_train_epochs=30,
weight_decay=0.01,
save_total_limit=4,
predict_with_generate=True,
)
# Initialize our Trainer
trainer = Seq2SeqTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
data_collator=data_collator,
args=training_args,
)
# Training
train_result = trainer.train()
trainer.save_model() # Saves the tokenizer too for easy upload
metrics = train_result.metrics
max_train_samples = (len(train_dataset))
metrics["train_samples"] = min(max_train_samples, len(train_dataset))
trainer.log_metrics("train", metrics)
trainer.save_metrics("train", metrics)
trainer.save_state()
# Evaluation
results = {}
logger.info("*** Evaluate ***")
metrics = trainer.evaluate(max_length=max_length, num_beams=num_beams, metric_key_prefix="eval")
max_eval_samples = len(eval_dataset)
metrics["eval_samples"] = min(max_eval_samples, len(eval_dataset))
trainer.log_metrics("eval", metrics)
trainer.save_metrics("eval", metrics)
with open(TEST_FILE_PATH, "r", encoding="utf-8") as f:
lines = f.readlines()
for row in lines:
row = row.split('\n')[0]
texts.append(row)
with torch.no_grad():
x = tokenizer(texts, padding=True, return_tensors='pt').to(device)
x = {key: value for (key, value) in x.items() if key != 'token_type_ids'}
outputs = model(**x)
def get_errors(corrected_text, origin_text):
for i, ori_char in enumerate(origin_text):
if ori_char in [' ', '“', '”', '‘', '’', '琊', '\n', '…', '—', '擤']:
# add unk word
corrected_text = corrected_text[:i] + ori_char + corrected_text[i:]
continue
if i >= len(corrected_text):
continue
if ori_char != corrected_text[i]:
if ori_char.lower() == corrected_text[i]:
# pass english upper char
corrected_text = corrected_text[:i] + ori_char + corrected_text[i + 1:]
continue
return corrected_text
result = []
for ids, text in zip(outputs.logits, texts):
_text = tokenizer.decode(torch.argmax(ids, dim=-1), skip_special_tokens=True).replace(' ', '')
corrected_text = _text[:len(text)]
corrected_text = get_errors(corrected_text, text)
result.append(corrected_text)
result.append('\n')
# print(result)
with open(OUTPUT_PATH, "w", encoding="utf-8") as f1:
f1.writelines(result)
之前有同学问如何设置GPU使用,我通常使用以下代码,
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
NLP学习1 - 使用Huggingface Transformers框架从头训练语言模型
5 使用 Transformers 预训练语言模型进行 Fine-tuning(文本相似度任务)() - AI牛丝
【pytorch-huggingface/transformer】 工作流程整理(未完成)
Huggingface Transformer教程(一)
基于transformers的自然语言处理(NLP)入门