HuggingFace BERT模型部署和微调训练#

HuggingFace 是一个开源开放的AI社区平台,允许用户共享自己的AI项目、数据集和模型,同时也为用户提供了各种机器学习工具,包括transformersdiffusersaccelerate等。通过HuggingFace社区,用户可以轻松地构建和训练自己的模型,并将其应用于各种实际场景中。

当前文档中,我们以HuggingFace提供的BERT预训练模型-英文-base预训练模型为示例,展示如何在PAI微调训练和部署BERT模型,主要内容包括以下:

  1. SDK安装和配置:

安装所需的SDK,并完成PAI Python SDK配置。

  1. 直接部署BERT模型创建推理服务

将HuggingFace上的BERT模型直接模型部署到PAI-EAS,创建一个在线推理服务。

  1. 使用BERT模型微调训练

基于BERT模型,我们使用公共数据集进行微调训练,以获得一个可以用于情感分类的模型,然后将输出的模型部署到PAI-EAS,创建一个在线推理服务。

Step1: SDK的安装配置#

我们将使用PAI提供的Python SDK,提交训练作业,部署模型。请通过以下命令安装PAI Python SDK,以及需要使用到的Huggingface datasets等依赖库。

!python -m pip install --upgrade alipai
!python -m pip install datasets huggingface_hub

SDK需要配置访问阿里云服务需要的AccessKey,以及当前使用的工作空间和OSS Bucket。在PAI Python SDK安装之后,通过在命令行终端中执行以下命令,按照引导配置密钥,工作空间等信息。

# 以下命令,请在命令行终端中执行.

python -m pai.toolkit.config

我们可以通过执行以下代码验证当前的配置是否成功。

import pai
from pai.session import get_default_session

print(pai.__version__)
sess = get_default_session()

assert sess.workspace_name is not None

Step2: 部署BERT模型创建推理服务#

PAI-EAS (Elastic Algorithm Service) 是PAI平台上的模型在线预测服务,支持使用镜像模式部署模型,并且提供了常见的机器学习框架的推理镜像。 在以下示例中,我们将使用PAI-EAS提供的镜像,将HuggingFace上的BERT模型直接部署到PAI,创建一个在线推理服务。

BERT是Google提出的一种预训练语言模型,使用自监督学习方法在大型英文语料库上进行训练。他可以直接用于"完形填空"的任务,也可以作为下游任务的预训练模型,通过微调训练,用于分类,问答等不同的任务。我们通过以下代码下载HuggingFace提供的BERT模型,用于创建一个支持“完形填空”的推理服务。

对于如何在离线模式下保存和使用HuggingFace模型,用户可以参考HuggingFace的官方文档: HuggingFace Offline Mode

from huggingface_hub import snapshot_download


# 下载BERT模型(PyTorch版本)
model_dir = snapshot_download(
    repo_id="bert-base-uncased",
    local_dir="./bert",
    allow_patterns=[
        "config.json",
        "pytorch_model.bin",
        "vocab.txt",
        "tokenizer_config.json",
        "tokenizer.json",
    ],
)

用户也可以通过以下的方式保存模型(需要用户在本地installtransformers, pytorch等依赖库):


from transformers import BertTokenizer, BertModel

# 下载模型
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained("bert-base-uncased")

# 保存模型到本地路径
model_dir = "./bert/"
model.save_pretrained(model_dir)
tokenizer.save_pretrained(model_dir)

保存的模型,可以直接通过transformers库加载使用:


from transformers import BertTokenizer, BertModel

model = BertModel.from_pretrained("./bert/")
tokenizer = BertTokenizer.from_pretrained("./bert/")

将保存在本地的BERT模型和tokenizer上传到OSS Bucket,拿到模型的OSS路径。

from pai.common.oss_utils import upload

# 上传模型
bert_model_uri = upload(
    source_path=model_dir, oss_path="huggingface/model/bert/", bucket=sess.oss_bucket
)
print(bert_model_uri)

在部署模型之前,我们需要准备模型推理服务的代码,用于加载模型,提供HTTP服务。在以下示例中,我们使用FastAPI编写了一个简单的HTTP服务,用于加载模型,提供预测服务。

# 创建推理服务使用的代码
!mkdir -p serving_src

完整的推理服务程序代码如下:

%%writefile serving_src/run.py

import os
import logging

import uvicorn, json, datetime
from fastapi import FastAPI, Request
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification

# 用户指定模型,默认会被加载到当前路径下
MODEL_PATH = "/eas/workspace/model/"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("model_server")

app = FastAPI()

@app.post("/")
async def predict(request: Request):
    global bert_pipeline
    json_data = await request.json()
    logger.info("Input data: %s", json_data)
    result = bert_pipeline(json_data["text"])
    logger.info("Prediction result: %s", result)
    return result


if __name__ == '__main__':
    task = os.environ.get("HF_TASK", "fill-mask")
    bert_pipeline = pipeline(task=task, model=MODEL_PATH, tokenizer=MODEL_PATH)

    uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get("LISTENING_PORT", 8000)))

SDK 提供的 pai.model.InferenceSpec 用于描述如何加载模型,以及如何提供预测服务。在以下代码中,我们使用 pai.model.container_serving_spec 方法,使用 PAI 提供的推理镜像和本地代码 serving_src,创建一个 InferenceSpec 对象。对应的本地代码会被上传保存到用户OSS,然后通过挂载的方式将相应的代码准备到运行容器中。

from pai.model import Model, container_serving_spec
from pai.image import retrieve, ImageScope


# 使用 PAI 提供的 PyTorch CPU 推理镜像
image_uri = retrieve(
    "PyTorch",
    framework_version="latest",
    accelerator_type="CPU",
    image_scope=ImageScope.INFERENCE,
).image_uri
print(image_uri)


# 构建一个使用镜像部署的InferenceSpec,可以用于BERT模型部署为推理服务.
bert_inference_spec = container_serving_spec(
    # 模型服务的启动命令
    command="python run.py",
    # 模型服务依赖的代码
    source_dir="./serving_src",
    image_uri=image_uri,
    requirements=[
        "transformers",
        "fastapi",
        "uvicorn",
        # 推理 pipeline 使用 device_map="auto" 时需要安装
        "accelerate",
    ],
)

print(bert_inference_spec.to_dict())

模型部署#

通过构建Model,调用Model.deploy方法,可以将模型部署到PAI-EAS,生成在线服务。

关于如何使用SDK部署模型的详细介绍,用户可以参考文档:PAI Python SDK部署推理服务

from pai.model import Model
from pai.common.utils import random_str

m = Model(
    inference_spec=bert_inference_spec,
    model_data=bert_model_uri,
)

p = m.deploy(
    service_name="hf_bert_serving_{}".format(random_str(6)),  # 推理服务名称.
    instance_type="ecs.c6.xlarge",  # 服务使用的机器实例规格: 4 vCPU, 8 GB
)

deploy方法返回的Predictor对象,指向了新创建的推理服务,他提供了.predict方法,支持用户向推理服务发送预测请求。

res = p.predict(data={"text": "Hello, I'm a [MASK] model."})

print(res)

在测试完成之后,我们可以通过predictor.delete_service删除推理服务,释放资源。

# 执行完成之后,删除对应的服务

p.delete_service()

Step3: Finetune BERT预训练模型#

BERT使用自监督学习方法在大型英文语料库上进行训练,他学习到了英语语言的内在表示,可以通过微调的方式,应用于不同的下游任务,从而获得更好的性能。在当前示例中,我们将使用Huggingface上 Yelp英文评论数据集yelp_review_full 对BERT模型进行微调,以获得一个可以用于情感分类的模型。

准备模型和数据集#

在当前步骤中,我们将准备微调训练使用的数据集,然后上传到OSS上供训练作业使用。

通过HuggingFace提供的transformers和datasets库可以使用读取本地文件的方式(离线模式),或是从HuggingFace Hub下载模型和数据的方式。为了提高训练作业的执行速度,我们在当前示例中,将模型和数据集准备到OSS,挂载到训练作业执行环境中,供训练作业直接加载使用。

from datasets import load_dataset
from pai.common.oss_utils import upload

data_path = "./train_data"

# 从HuggingFace Hub加载数据集
dataset = load_dataset("yelp_review_full")

# 保存到数据集,保存的数据集可以通过`datasets.load_from_disk`加载使用
dataset.save_to_disk(data_path)

train_data_uri = upload(
    source_path=data_path,
    oss_path="huggingface/dataset/yelp_review_full/",
    bucket=sess.oss_bucket,
)

print(train_data_uri)

准备训练代码#

参考HuggingFace提供的对于Masked Language Model 的微调文档,我们编写了以下训练脚本,它将使用我们上传的数据集完成模型的微调。

# 创建代码保存目录
!mkdir -p train_src

在我们编写的训练作业脚本中,通过环境变量的方式获取训练作业的超参,输出数据,输出模型保存地址。对于PAI训练服务提供的环境变量的详细介绍,可以见文档:训练作业预置环境变量

完整的训练代码如下:

%%writefile train_src/finetune.py

import os

from datasets import load_dataset, load_from_disk
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, DataCollatorWithPadding, HfArgumentParser
import numpy as np
import evaluate


def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)


def train():
    # 通过环境变量获取预训练模型地址, 训练数据,以及模型保存地址
    model_name_or_path = os.environ.get("PAI_INPUT_MODEL", "bert-base-cased")
    input_train_data = os.environ.get("PAI_INPUT_TRAIN_DATA")
    output_dir=os.environ.get("PAI_OUTPUT_MODEL", "./output")

    # 使用环境变量获取训练作业超参
    num_train_epochs=int(os.environ.get("PAI_HPS_EPOCHS", 2))
    save_strategy=os.environ.get("PAI_HPS_SAVE_STRATEGY", "epoch")

    print("Loading Model...")
    model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, num_labels=5)
    tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)

    print("Loading dataset from disk...")
    dataset = load_from_disk(input_train_data)
    tokenized_datasets = dataset.map(lambda examples: tokenizer(examples["text"], padding="max_length", truncation=True, max_length=512),
                                     batched=True)

    data_collator = DataCollatorWithPadding(tokenizer)
    small_train_dataset = tokenized_datasets['train'].shuffle(seed=42).select(range(1000))
    small_eval_dataset = tokenized_datasets['test'].shuffle(seed=42).select(range(1000))

    training_args = TrainingArguments(
        output_dir=output_dir,
        # 使用环境变量获取训练作业超参
        num_train_epochs=num_train_epochs,
        # 使用环境变量获取训练作业保存策略
        save_strategy=save_strategy,
    )
    print("TrainingArguments: {}".format(training_args.to_json_string()))
    metric = evaluate.load('accuracy')

    print("Training...")
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=small_train_dataset,
        eval_dataset=small_eval_dataset,
        data_collator=data_collator,
        tokenizer=tokenizer,
        compute_metrics=compute_metrics,
    )

    trainer.train()
    print("Saving Model...")
    trainer.save_model()


if __name__ == "__main__":
    train()

我们的训练作业将使用PAI提供的PyTorch镜像执行,需要在镜像中安装 transformersevaluate 库才能够执行相应的训练脚本。通过在训练作业目录下提供 requirements.txt 文件,PAI的训练服务会自动安装指定的第三方依赖。

%%writefile train_src/requirements.txt

transformers
datasets
evaluate

提交训练作业#

通过PAI Python SDK提供的训练作业APIpai.estimator.Estimator,我们可以将训练脚本提交到PAI执行。在以下代码中,我们将指定使用的训练代码 train_src ,使用PAI提供的PyTorch GPU镜像训练,提交运行微调训练作业。对于使用SDK提交训练作业的详细介绍,用户可以参考文档:PAI Python SDK提交训练作业

from pai.huggingface.estimator import HuggingFaceEstimator
from pai.image import retrieve


# 使用 PAI 提供的 PyTorch GPU 训练镜像
image_uri = retrieve(
    "PyTorch", framework_version="latest", accelerator_type="GPU"
).image_uri


# 配置训练作业
est = HuggingFaceEstimator(
    command="python finetune.py",  # 训练作业启动命令
    source_dir="./train_src/",  # 训练作业代码
    instance_type="ecs.gn6i-c4g1.xlarge",  # 训练使用的作业机器类型, 4 vCPU, 15 GB, 1* T4 GPU
    transformers_version="latest",
    hyperparameters={  # 训练作业超参,用户可以通过环境变量,或是
        "save_strategy": "epoch",
        "epochs": "1",
    },
    base_job_name="hf-bert-training",
)


# est = Estimator(
#     image_uri=image_uri,  # 训练作业使用的镜像
#     command="python finetune.py",  # 训练作业启动命令
#     source_dir="./train_src/",  # 训练作业代码
#     instance_type="ecs.gn6i-c4g1.xlarge",  # 训练使用的作业机器类型, 4 vCPU, 15 GB, 1* T4 GPU
#     hyperparameters={  # 训练作业超参,用户可以通过环境变量,或是
#         "save_strategy": "epoch",
#         "epochs": "1",
#     },
#     base_job_name="hf-bert-training",
# )

print(est)
print(est.hyperparameters)

# 提交训练作业到PAI执行
# 提交之后SDK会打印作业URL,我们可以作业详情页查看训练日志,输出模型,资源使用情况等
est.fit(
    # 作业使用的预训练模型和数据集使用inputs方式传递
    # 相应的OSS URI会被挂载到作业环境中,用户可以通过 `PAI_INPUT_{ChannelNameUpperCase}` 的环境变量获取挂载后的路径
    inputs={
        "model": bert_model_uri,
        "train_data": train_data_uri,
    }
)
# 训练任务产出的模型地址
print(est.model_data())

部署Finetune获得的模型#

我们将复用以上推理服务的代码,将微调训练获得的模型部署到PAI-EAS,创建一个在线推理服务。

Note: 微调模型用于情感分析任务,我们显式得修改HuggingFace pipeline的Task参数。这里我们通过环境变量的方式传入Task参数。

from pai.model import Model, container_serving_spec
from pai.image import retrieve, ImageScope


# 使用 PAI 提供的 PyTorch CPU 推理镜像
image_uri = retrieve(
    "PyTorch",
    framework_version="latest",
    accelerator_type="CPU",
    image_scope=ImageScope.INFERENCE,
).image_uri


# 构建一个使用镜像部署的InferenceSpec,可以用于将以上产出的BERT模型部署为推理服务.
inference_spec = container_serving_spec(
    # 模型服务的启动命令
    command="python run.py",
    # 模型服务依赖的代码
    source_dir="./serving_src",
    image_uri=image_uri,
    requirements=[
        "transformers",
        "fastapi",
        "uvicorn",
    ],
    # 使用情感分析任务pipeline,通过环境变量的方式传递给到推理服务脚本。
    environment_variables={"HF_TASK": "sentiment-analysis"},
)

print(inference_spec.to_dict())
from pai.model import Model
from pai.common.utils import random_str

# 使用训练作业产出的模型
model_data = est.model_data()

m = Model(
    inference_spec=inference_spec,
    model_data=model_data,
)

p = m.deploy(
    service_name="hf_bert_ft_serving_{}".format(random_str(6)),  # 推理服务名称
    instance_type="ecs.c6.xlarge",  # 服务使用的机器实例规格: 4 vCPU, 8 GB
)

通过Predictor向新创建的推理服务发送预测请求,获取模型预测结果。

res = p.predict({"text": "i am so happy today"})
print(res)

res = p.predict({"text": "i am so sad today"})
print(res)

在测试完成之后,我们通过predictor.delete_service删除推理服务,释放资源。

# 执行完成之后,删除对应的服务

p.delete_service()