HuggingFace Transformers框架使用教程

本文记录使用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函数,它支持如下的任务:

  • 情感分析(Sentiment analysis):一段文本是正面还是负面的情感倾向
  • 文本生成(Text generation):给定一段文本,让模型补充后面的内容
  • 命名实体识别(Name entity recognition):识别文字中出现的人名地名的命名实体
  • 问答(Question answering):给定一段文本以及针对它的一个问题,从文本中抽取答案
  • 填词(Filling masked text):把一段文字的某些部分mask住,然后让模型填空
  • 摘要(Summarization):根据一段长文本中生成简短的摘要
  • 翻译(Translation):把一种语言的文字翻译成另一种语言
  • 特征提取(Feature extraction):把一段文字用一个向量来表示

下面举一个情感分析的例子:

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使用例子详情参见官网教程,此处不再赘述。

使用细节

该框架的易用性体现在以下几个方面:

  • 只有configuration,models和tokenizer三个主要类。
  • 所有的模型都可以通过统一的from_pretrained()函数来实现加载,transformers会处理下载、缓存和其它所有加载模型相关的细节。而所有这些模型都统一在Hugging Face Models管理。
  • 基于上面的三个类,提供更上层的pipeline和Trainer/TFTrainer,从而用更少的代码实现模型的预测和微调。

因此它不是一个基础的神经网络库来一步一步构造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方法, 可以从不同的地方构建数据集

  • from the HuggingFace Hub,
  • from local files, e.g. CSV/JSON/text/pandas files,
  • from in-memory data like python dict or a pandas dataframe.

我们可以查看数据内容

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)

Tips

之前有同学问如何设置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)入门

你可能感兴趣的:(nlp,huggingface,深度学习,人工智能,自然语言处理)