0. 前言

这部分内容还是很重要的,预计会设计常见的 pytorch 模型部署方法,理解框架中,每个模块在做什么。另外,这也是工程上必备的技能。

0.1 模型及下载地址

ModelRepoPaper
ResNet (18: 12M; 50: 26M; 152: 60M)https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py
BERT (110 / 330M)https://github.com/NVIDIA/DeepLearningExamples/tree/master/PyTorch/LanguageModeling/BERThttps://arxiv.org/abs/1810.04805
GPT-2 (1.5B)https://github.com/openai/gpt-2
OPT (125 / 350M; 1.3 / 2.7 / 6.7 / 13 / 30 / 66 / 175B)https://github.com/facebookresearch/metaseqhttps://arxiv.org/pdf/2205.01068.pdf
BLOOM (560M; 1.1 / 1.7 / 3 / 7.1 / 176B)https://huggingface.co/docs/transformers/model_doc/bloom
T5 (60 / 220 / 770M; 3 / 11B)https://github.com/google-research/text-to-text-transfer-transformer#released-model-checkpointshttps://jmlr.org/papers/volume21/20-074/20-074.pdf

0.2 模型下载方法

2023-07-08 20:10:12,重新回顾 22 年关于大模型研究的工作。从 chatgpt 爆火之后,大模型应用的框架变得火热,语言模型的社区也变得火热起来。准备在 ant_ext 工作中加入一些新的模型的评估,但是期智连接 huggingface 的网络老是抽风,导致试图从 huggingface 下载模型时出现错误。现在总结一些其他的下载方法。

Ref: https://zhuanlan.zhihu.com/p/475260268

0.2.1 git lfs

这个方法会下载所有框架的模型文件,flax_model.msgpack、tf_model.h5和pytorch_model.bin,可以看到有 3 种框架的模型文件,比较冗余。

git lfs install
git clone https://huggingface.co/bert-base-chinese

0.2.2 hungging face hub

这种方法其实仍然是通过访问 huggingface 网站来下载

python
>>> from huggingface_hub import snapshot_download
>>> snapshot_download(repo_id="facebook/opt-13b")

# 通过参数可以指定下载的文件,屏蔽不需要的文件即可
>>> snapshot_download(repo_id="facebook/opt-13b", ignore_patterns=["*.h5", "*.ot", "*.msgpack"], local_dir="/localdata_ssd/model", local_dir_use_symlinks=False)

1. Hugging Face

简介:Hugging face 是一家总部位于纽约的聊天机器人初创服务商,开发的应用在青少年中颇受欢迎,相比于其他公司,Hugging Face更加注重产品带来的情感以及环境因素。但更令它广为人知的是Hugging Face专注于NLP技术,拥有大型的开源社区。尤其是在github上开源的自然语言处理,预训练模型库 Transformers,已被下载超过一百万次,github上超过24000个star。Transformers 提供了NLP领域大量state-of-art的 预训练语言模型结构的模型和调用框架。

BERT 模型中,一个 token-wise 就是 768 维的向量。

总的来说,提供了各种易用 transformer 模型,并且有很好的社区和文档。

1.1 简介

1.1.1 用途

理解了一下,感觉是提供预训练好的模型,自己可以用来解决一些任务。

1.1.2 重要概念

1.1.2.1 Pipeline

pipeline 函数:包含 pro-processing, model, post-processing

如果只是想使用训练好的模型的功能,这是最简单的方法。pipeline 函数中应该完成了一套 workflow。下列代码下载了大概 300MB 的预训练模型,给出了对我输入的句子的情感分析。

from transformers import pipeline
classifier = pipeline("sentiment-analysis")
print(classifier("You like a pig"))

output: [{'label': 'NEGATIVE', 'score': 0.9961085915565491}]

1.1.2.2 AutoClass

AutoModelForSequenceClassification 以及 AutoTokenizer 用于支持 pipeline()。AutoClass 是一种快捷方式,可以自动从预训练模型的名称或路径中检索模型架构。 您只需要为您的任务选择适当的 AutoClass 及其关联的预处理类。

在自己使用时,选择 GPT2 模型,需要 from transformers import GPT2Model,如果要引入其他模型也是类似的做法。from transformers import AutoModel,应该可以根据模型名称自己去检索下载。

2022-10-25 21:13:01,当我有可能通过参数载入多种模型时,我意识到了 AutoClass 的作用。

1.1.2.3 Custom Model

主要途径是获取模型的 config,然后手动修改即可。

from transformers import AutoConfig
my_config = AutoConfig.from_pretrained("distilbert-base-uncased", n_heads=12)
my_model = AutoModel.from_config(my_config)

1.1.2.4 Trainer - a PyTorch optimized training loop

目前的很多框架中都有一套训练的 loop,hugging face 也提供了这套流程。设置好几个必备的参数和输入即可。

  1. A PreTrainedModel or a torch.nn.Module;自己定制一个模型应该也是可以的。
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased")
  1. TrainingArguments:超参数,比如 learning rate, batch size, epoch
from transformers import TrainingArguments
training_args = TrainingArguments(
    output_dir="path/to/save/folder/",
    learning_rate=2e-5,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
    num_train_epochs=2,
)
  1. 设置预处理过程:比如 tokenizer, feature extractor, processor
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
  1. 定义预处理的训练数据集和测试数据集;不过这段代码之前得先把 dataset load 进来吧。
train_dataset = dataset["train"]  # doctest: +SKIP
eval_dataset = dataset["eval"]  # doctest: +SKIP
  1. DataCollator() to create a batch of examples from your dataset
from transformers import DefaultDataCollator
data_collator = DefaultDataCollator()

2022-10-24 09:43:20,这个 example 比较陌生

  1. Put it all together
from transformers import Trainer

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    tokenizer=tokenizer,
    data_collator=data_collator,
)  # doctest: +SKIP

# start training
trainer.train()

1.1.2.5 预处理

对于 NLP 模型,一般就是用 tokenizer 来做。经过 tokenizer 的字典有三个重要的 items

  • input_ids, the indices corresponding to each token in the sentence.
  • attention_mask, indicates whether a token should be attended to or not.
  • token_type_ids, identifies which sequence a token belongs to when there is more than one sequence.

padding

每个 sentence 长度不一致,用 padding 来 tensor 对齐。

batch_sentences = [
    "But what about second breakfast?",
    "Don't think he knows about second breakfast, Pip.",
    "What about elevensies?",
]
encoded_input = tokenizer(batch_sentences, padding=True)
print(encoded_input)
{'input_ids': [[101, 1252, 1184, 1164, 1248, 6462, 136, 102, 0, 0, 0, 0, 0, 0, 0], 
               [101, 1790, 112, 189, 1341, 1119, 3520, 1164, 1248, 6462, 117, 21902, 1643, 119, 102], 
               [101, 1327, 1164, 5450, 23434, 136, 102, 0, 0, 0, 0, 0, 0, 0, 0]], 
 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 
 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], 
                    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 
                    [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]]}

Truncation

也是一种对齐的方式

1.1.3 模型 Model

支持的模型很多,比较热门的预训练模型基本都有。学术界的很多模型也有,比如 I-BERT,RoBERTa

1.2 API

1.2.1 from_pretrained

功能:from_pretrained 提供了模型类别判断,模型文件列表映射,模型文件下载及缓存,网络下载稳定性容错等功能。

# Download from huggingface.co and cache
model = BertModel.from_pretrained("bert-base-uncased")
model.save_pretrained('./local_model_directory/')

# Load from local
model = BertModel.from_pretrained('./local_model_directory/')

实例:

if(os.path.exists('./model/'+args.gpt2_model+'pytorch_model.bin')):
    model.GPT2ForSequenceClassification.from_pretrained('./model/'+args.gpt2_model)
else:
    model = GPT2ForSequenceClassification.from_pretrained(args.gpt2_model)
    num_labels = len(model.config.id2label)
    model = GPT2ForSequenceClassification.from_pretrained(args.gpt2_model, num_labels=num_labels)
    model.save_pretrained('./model/'+args.gpt2_model)

1.3 Fine-tune a pretrained model

可以用 1.1.2.4 中的 Trainer() 来实现微调和训练,也可以在 pytorch 框架中实现。下面是步骤。

1.3.1 Prepare a dataset

比如,从 Yelp Reviews 中 load 数据,然后进行预处理。下面的代码用到了 map() 函数,写法比较陌生,不过这个过程是通用的。

# load
from datasets import load_dataset
dataset = load_dataset("yelp_review_full")
dataset["train"][100]
{'label': 0,
 'text': 'My expectations for McDonalds are t rarely high....'}

# tokenizer
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)
tokenized_datasets = dataset.map(tokenize_function, batched=True)

1.3.2 Train

用 Trainer() 训练在 1.1.2.4 中讲过了,讲一下用 native pytorch 训练,Train in native PyTorch

1.3.2.1 DataLoader

为训练数据集和测试数据集创建一个 DataLoader,以便可以遍历数据的 batch。给出 BERT 框架中的示例代码。变量 batch 中就是一个 batch,经过 tokenizer 之后的数据。

from torch.utils.data import (DataLoader, RandomSampler, SequentialSampler, TensorDataset)
train_features = get_train_features(
    args.data_dir,
    args.gpt2_model,
    args.max_seq_length,
    args.do_lower_case,
    args.local_rank,
    args.train_batch_size,
    args.gradient_accumulation_steps,
    args.num_train_epochs,
    tokenizer,
    processor,
)
train_data = gen_tensor_dataset(train_features)
        train_dataloader = DataLoader(
            train_data,
            sampler=train_sampler,
            batch_size=args.train_batch_size,
        )
for e in range(args.num_train_epochs):
    ...
    for step, batch in enumerate(train_dataloader):
        ...

1.3.2.2 Optimizer and learning rate scheduler

优化器根据模型来定义,要传入 learning rate 吗,代码示例:

model, optimizer, scheduler = init_optimizer_and_amp(
    model,
    args.learning_rate,
    args.loss_scale,
    args.warmup_proportion,
    num_train_optimization_steps,
    args.fp16,
)
def init_optimizer_and_amp(model, learning_rate, loss_scale, warmup_proportion,
                           num_train_optimization_steps, use_fp16):
    ...
    optimizer, scheduler = None, None
    logger.info("using fp32")
    if num_train_optimization_steps is not None:
        optimizer = BertAdam(
            optimizer_grouped_parameters,
            lr=learning_rate,
            warmup=warmup_proportion,
            t_total=num_train_optimization_steps,
        )
    return model, optimizer, scheduler

1.3.2.3 Training Loop

这个无需多说。设置好 epoch,每个 epoch 会过完训练数据集中的所有数据。(当然,也有随机抽样的形式)

model.train()
for e in range(args.num_train_epochs):
    ...
    for step, batch in enumerate(train_dataloader):
        batch = tuple(t.to(device) for t in batch)
        input_ids, input_mask, segment_ids, label_ids = batch
        # outputs = model(**batch)
        outputs = model(input_ids=input_ids, token_type_ids=segment_ids, attention_mask=input_mask, labels=label_ids)
        loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

1.3.2.4 Evaluate

model.eval()
for i, (input_ids, input_mask, segment_ids, label_ids) in enumerate(eval_dataloader):
    input_ids = input_ids.to(device)
    input_mask = input_mask.to(device)
    segment_ids = segment_ids.to(device)
    label_ids = label_ids.to(device)
    with torch.no_grad():
        # label_ids = label_ids.to(torch.float)
        outputs = model(input_ids=input_ids, token_type_ids=segment_ids, attention_mask=input_mask, labels=label_ids)
        logits = outputs.logits
    ...

1.4 Accelerate

Easily rain and use PyTorch models with multi-GPU, TPU, mixed-precision

1.4.0 术语

关于分布式的一些概念和术语。

参考:

https://zhuanlan.zhihu.com/p/544273093

https://www.zhihu.com/question/453920336/answer/1828326535

  • Scatter: 把数据分发到其他线程 or 进程
  • Gather: 把数据从其他进程 or 线程中收集到一个主进程中
  • Reduce: 计算在各个进程 or 线程中进行,然后归约到一个主进程中
  • All-Reduce: 同 Reduce 一样,计算再各个进程 or 线程中进行,但是每个节点都一起归约,保持每个节点的结果一致
  • Broadcast: 把数据复制到各个进程 or 线程
  • All-Gather: 与 Gather 不同,每个进程 or 线程中完成数据的手机,保证每个节点的结果一致
  • CLI (Command Line Interface):一种通过命令行来交互的工具或者应用,比如 mkdir, cd, scp, npm 等
  • Rank: 表示进程的编号/序号。(自己观察到一个 GPU 似乎对应一个 rank,不过 rank 和 GPU 没有严格的对应关系。如果是多进程共享 GPU,那么一个 GPU 可以为多个 rank 服务)
  • Node: 物理节点,可以是一台机器或者一个容器,节点内可以有多个 GPU
  • Rank 和 Local Rank: rank是指在整个分布式任务中进程的序号;local_rank是指在一个node上进程的相对序号,local_rank在node之间相互独立。
  • nnodes、node_rank 与 nproc_per_node: nnodes是指物理节点数量,node_rank是物理节点的序号;nproc_per_node是指每个物理节点上面进程的数量。
  • word size : 全局(一个分布式任务)中,rank的数量。
  • csrc: 应该是 C source code
  • NVIDIA Megatron-LM: 针对Transformer类的模型提供半自动的分布式部署。
  • DeepSpeed: 微软提供的一个开源深度学习训练优化库。基于英伟达 Megatron-LM 进行了张量切分式的模型并行。

同步 SGD:每个 Worker 都是同步计算一个批量。

1.4.1 分布式概念 Distributed

2022-10-24 10:43:08,日后填坑。

2022-11-01 09:04:30,开始学习。

nn.parallel.scatter(data, devices),可以将一组数据 split 到多个 devices 上

1.4.1.1 DataParallel

nn.DataParallel(net, devices_ids=devices),DP 模型。单进程,多线程,适于一台机器的情况。

做法是将 model 复制到不同的 GPU 上,各个 GPU 计算不同 batch 的数据。

1.4.1.2 DistributedDataParallel

torch.nn.parallel.DistributedDataParallel,DDP 模型(PyTorch 官网更推荐)。

需要启动 init_process_group

如果想在一个有 N 个 GPU 的设备上面使用 DistributedDataParallel,则需要 spawn up N 个进程,每个进程对应0-N-1 的一个 GPU。这可以通过下面的语句实现

torch.cuda.set_device(i)

# i from 0-N-1,每个进程中都需要:
torch.distributed.init_process_group(
    backend='nccl', world_size=N, init_method='...'
)
model = DistributedDataParallel(model, device_ids=[i], output_device=i)

# 实践
if args.local_rank == -1 or args.no_cuda:
    device = torch.device(
        "cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
    n_gpu = torch.cuda.device_count()
else:
    torch.cuda.set_device(args.local_rank)
    device = torch.device("cuda", args.local_rank)
    n_gpu = 1
    # Initializes the distributed backend which will take care of
    # sychronizing nodes/GPUs.
    if not torch.distributed.is_initialized():
        torch.distributed.init_process_group(backend='nccl')

多进程,适于多台机器的情况,可以结合模型并行的方法。

使用时建议

  • 使用一个 big dataset
  • 好的 CPU-GPU 和机器-机器带宽
  • 高效的 data load 以及 preprocess
  • 模型需要有好的计算(FLOP)通讯(model size)比
    • Inception > ResNet > AlexNet
  • 使用足够大的 batch size 来获取好的系统性能
  • 使用高效的优化算法对应大批量大小

1.4.2 Hugging face 文档

1.4.2.1 步骤

  • import Accelerator 并且实例化
from accelerator import Accelerator

accelerator = Accelerator()
  • 消去 model 以及 input data 的 .to(device) 或者 .cuda() 操作。accelerator 会帮你完成这件事
  • 把相关的对象(optimizer, model, train_dataloader, lr_scheduler)传递给 prepare() 方法
model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
    model, optimizer, train_dataloader, lr_scheduler
)

每个 GPU 会加载 training dataset 中的不同部分(batch),这看起来像是数据并行。

  • Replace the line loss.backward() by accelerator.backward(loss).

1.4.2.2 Distributed evaluation

如果需要分布式评估,同理,把 validation dataloader 发送给 prepare() 方法。

validation_dataloader = accelerator.prepare(validation_dataloader)

这样的话每个 devices 只能看见 evaluation data 中的一部分,所以最后需要把结果 group 到一起,通过 gather_for_metrics() 方法。

for inputs, targets in validation_dataloader:
    predictions = model(inputs)
    # Gather all predictions and targets
    all_predictions, all_targets = accelerator.gather_for_metrics((predictions, targets))
    # Example of use with a *Datasets.Metric*
    metric.add_batch(all_predictions, all_targets)

1.4.2.3 Launch

accelerate 命令整合了在不同平台上发射脚本的命令,你无需自己记住所有命令。如果你自己熟悉 PyTorch 提供的发射脚本,也可以不使用 accelerate。

# 单 GPU 执行
CUDA_VISIBLE_DEVICES="0" accelerate launch {script_name.py} --arg1 --arg2 ...

# 默认参数,使用所有的 GPU,不启动混合精度
accelerate launch --multi_gpu {script_name.py} {--arg1} {--arg2} ...

# 当然,也可以指定参数
accelerate launch --multi_gpu --mixed_precision=fp16 --num_processes=2 {script_name.py} {--arg1} {--arg2} ...

# 查看所有参数
accelerate launch -h

accelerate config

当然,更好的方式是配置一个 config 文件。使用了 accelerate config 之后,会在 ~/.cache/huggingface/accelerate 目录下生成 default_config.yaml 文件。也可以自己定制 config 文件然后读取。

accelerate launch --config_file ~/.cache/huggingface/accelerate {script_name.py} {--arg1} {--arg2} ...

1.5 Dataset

1.5.1 load from hugging face

hugging face 下载文件存储的位置:~/.cache/huggingface

找到 dataset 之后如何下载:

# 搜索到数据集 oscar,里面有很多 subset,比如:unshuffled_deduplicated_en, unshuffled_deduplicated_br

# 第一个参数是数据集的名称,第二个参数用于索引子集,一般还有 split 参数:train, test, validation 

from datasets import load_dataset
dataset = load_dataset(path="oscar", name="unshuffled_deduplicated_en")
# 420G,赶紧中断
dataset.save_to_disk("./glue/")

# 尝试下载 wikitext
dataset = load_dataset(path="wikitext", name="wikitext-103-v1", split="train")
# 2022-10-24 14:07:31,是可以的
print(dataset[:3])
{'text': ['', ' = Valkyria Chronicles III = \n', '']}

# 保存
dataset.save_to_disk("./")

1.5.2 数据集格式

1.5.1 的例子中,默认保存为 dataset.arrow 加上 .json 文件。

现在我们想将其保存为 .csv

1.5.3 split 作用

下面是带有 feature 的 dataset 一个示例

from datasets import load_dataset
# 如果用了 split 
dataset = load_dataset("rotten_tomatoes", split="train")
print(dataset)
Dataset({
    features: ['text', 'label'],
    num_rows: 8530
})
# 如果不用 split,
dataset = load_dataset("rotten_tomatoes")
DatasetDict({
    train: Dataset({
        features: ['text', 'label'],
        num_rows: 8530
    })
    validation: Dataset({
        features: ['text', 'label'],
        num_rows: 1066
    })
    test: Dataset({
        features: ['text', 'label'],
        num_rows: 1066
    })
})

1.6 Github 文档

所有任务的 example 存放在 https://github.com/huggingface/transformers/tree/main/examples/pytorch 里面。

2. 知识补充

补充一些相关的,之前不熟悉的概念。

2.2 Data Set

2.2.1 GLUE

数据集是之前屏蔽的一个部分,但是如果想了解模型是在做什么,并且要动手做实验,必须了解数据集。

GLUE 是学术界非常常用的数据集。

数据集类型描述
MNLI句子对分类任务给定一个前提 (Premise) ,根据这个前提去推断假设 (Hypothesis) 与前提的关系。该任务的关系分为三种,蕴含关系 (Entailment)、矛盾关系 (Contradiction) 以及中立关系 (Neutral)。
QQP句子对分类任务判断 Quora 上的两个问题句是否表示的是一样的意思
SST-2分类任务情感分析
CoLA单句分类任务句子语义判断,是否是可接受的(Acceptable)
QNLI用于判断文本是否包含问题的答案
STS-B预测两个句子的相似性,包括5个级别。
MRPC句子对分类任务也是判断两个句子是否是等价的。
RTE分类类似于MNLI,但是只是对蕴含关系的二分类判断,而且数据集更小。
SWAG从四个句子中选择为可能为前句下文的那个

3. 编程实例

3.1 通用知识

3.1.1 主要的组件

  • Configuration: 编程模型中的参数或是变量。用于配置词表大小,隐藏层维数,Dropout rate 等等。给出一个 BERT_base 配置的示例。
{
  "attention_probs_dropout_prob": 0.1,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "type_vocab_size": 2,
  "vocab_size": 30522
}
  • Tokenizer: 每个模型都有对应的分词器。存储 token 到 index 的映射。
  • Model: 实现模型的计算图和编码过程,实现前向传播。对于 output 层,不同的模型有不一样的封装。比如今天(2022-10-23)debug 观察的 BaseModelOutputWithPoolingAndCrossAttentions,类型:SequenceClassifierOutput 等等。

3.1.2 任务分类

语言生成任务,对应模型:GPT,GPT-2,XLNet

语言理解任务,对应模型:BERT,RoBERTa,XLM

3.2 在 GPT-2 上运行 wikitext

README:

pip install transformers
ln -s ../BERT/glue/ ./glue

3.0.1 坑1

坑,和 BERT 区分

BERT forward 和 GPT-2 forward 不一样。之前实现的 model(inputs_id, token_type_ids, attention_mask) 按位置传参,但是 GPT-2 forward 第二个参数是 past_key_values,所以报错,按关键字传参即可解决这个问题。

他们经过前向之后,output 也不一样。

3.0.2 大坑2

2022-10-26 22:31:46,复现 GPT 的结果出了大问题,好在今晚解决了。问题来源于框架中会自动启动分布式 train/eval,但是没有很好地支持分布式(单 GPU 运行 30s,4 GPU 运行 6min),而且 loss 的处理也有问题,导致最终的 metric - PPL 出了问题。用单 GPU 可以解决这个问题。

3.0.3 坑3

2022-10-28 16:03:02,框架K 是不支持分布式的,这导致了 local_rank 索引不到,因此执行 evaluate(),进入 quant 模块时报错。

3.2.1 Prepare Dataset

读取 datasets 并保存到本地

from datasets import load_dataset
test_dataset = load_dataset("json", data_files="test.json", split="train")
test_dataset.save_to_disk("test.hf")

3.2.2 LM 预处理

针对生成类模型的预处理。如何从原始的数据集生成 tokenizer 之后的数据。

row_datasets = load_dataset(data_args.dataset_name, data_args.dataset_config_name, cache_dir=model_args.cache_dir)
# tokenizer
tokenized_datasets = raw_datasets.map(
    tokenize_function,
    batched=True,
    num_proc=data_args.preprocessing_num_workers,
    remove_columns=column_names,
    load_from_cache_file=not data_args.overwrite_cache,
    desc="Running tokenizer on dataset",
)
with training_args.main_process_first(desc="grouping texts together"):
    lm_datasets = tokenized_datasets.map(
        group_texts,
        batched=True,
        num_proc=data_args.preprocessing_num_workers,
        load_from_cache_file=not data_args.overwrite_cache,
        desc=f"Grouping texts in chunks of {block_size}",
    )
# 计算 token 长度
for split, data in lm_datasets.items():
total_eval_tokens = 0        
for chunk in data['labels']:
    total_eval_tokens += len([x for x in chunk[1:] if x != padding_index])
logger.info(f'[{split}] Total eval tokens: {total_eval_tokens}')

# 得到 eval dataset 以及 train dataset
if training_args.do_train:
    if "train" not in tokenized_datasets:
        raise ValueError("--do_train requires a train dataset")
    train_dataset = lm_datasets["train"]
    if data_args.max_train_samples is not None:
        train_dataset = train_dataset.select(range(data_args.max_train_samples))

if training_args.do_eval:
    if "validation" not in tokenized_datasets:
        raise ValueError("--do_eval requires a validation dataset")
    eval_dataset = lm_datasets[data_args.eval_subset]
    if data_args.max_eval_samples is not None:
        eval_dataset = eval_dataset.select(range(data_args.max_eval_samples))

# 这个时候 eval_dataset size (480, 3),3 个维度分别是 input_ids, attention_mask, labels

3.3 在 T5 上运行 WMT

MODEL=t5-small

CUDA_VISIBLE_DEVICES=0 python -u run_translation.py  \
  --model_name_or_path ${MODEL} \
  --dataset_name wmt16 --dataset_config_name ro-en \
  --per_device_eval_batch_size=4 \
  --output_dir checkpoints-translation/${MODEL} \
  --source_lang en --target_lang ro \
  --do_eval \
  --predict_with_generate \
  --source_prefix "translate English to Romanian: "

4. 编程 flow

利用 debug 以及 hugging face 文档,了解整个流程。在 Section 1 介绍 hugging face 指导文档,这部分结合实际编程来看。

4.1 数据读取及预处理

4.2.1 数据读取

通过参数 --data_dir,把本地的数据加载进来,应该包含 .tsv 文件。hugging face 的预训练模型可以下载到本地并保存,那么 Dataset 应该也可以。

4.2.2 DataLoader

经过处理后的 train_features,长度是 8551 examples,装的是经过 tokenizer 后的数据。

原始的 train_example,从 ./glue/CoLA/train.tsv 读入,长度是 8551 examples,每个 example 装的是:

  • guid:train-1 or train=2…
  • label:1 or 0
  • text_a:Our friends won't buy this analysis...
  • text_b:None

train_data 就是通过 gen_tensor_dataset() 把 train_features 转换为 tensor;经过 DataLoader(train_data, sampler, batch_size) 方法之后,得到 train_dataloader,sampler 就是 8551。

在一个 epoch 中,需要遍历一遍整个 train_dataloader,一个 iteration 处理其中一个 batch,train_loader 遍历完就是一个 epoch。

记得数据需要转移到 device

4.2.X Tokenizer

input_ids 存放的就是 tokenizer 后的数据本身,是一个 (64, 128) 的 tensor。mask, segment_id 也是 (64, 128),label 是 (64),这应该就是最后的分类,因为 batch size 是 64,所以有 64 个输出。

对于 hugging face BERT,经过 tokenizer 打印出来就是一整个 mapping,也就是对应 run_glue.py 中遍历 train_dataloader 的变量 batch。不过 batch 的类型是 list,而下面的 inputs 是 dict。而 hugging face BERT 的 output 也是一个 BaseModelOutputWithPoolingAndCrossAttentions 类型。

from transformers import BertTokenizer, BertModel
import torch

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased")

inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
print(inputs)
{'input_ids': tensor([[  101,  7592,  1010,  2026,  3899,  2003, 10140,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}

outputs = model(**inputs)
# output 输出包含 last_hidden_state, pooler_output,其他属性 hidden_states, past_key_values, attentions, cross_attentions 为 None

对于 hugging face BertForSequenceClassification,

  • output 类型:SequenceClassifierOutput(loss=None, logits=tensor([[-2.2095, 2.5760]]), hidden_states=None, attentions=None)

4.3 计算 loss

hugging face 有一个普通 trainer 的实例,关于如何计算 loss。看一下能不能套用到原先 BERT 的框架中。

from torch import nn
from transformers import Trainer


class CustomTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False):
        labels = inputs.get("labels")
        # forward pass
        outputs = model(**inputs)
        logits = outputs.get("logits")
        # compute custom loss (suppose one has 3 labels with different weights)
        loss_fct = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 2.0, 3.0]))
        loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1))
        return (loss, outputs) if return_outputs else loss

2022-10-23 18:32:34,直接用 forward 函数中计算的 loss

label_ids = label_ids.to(torch.float)
outputs = model(input_ids=input_ids, token_type_ids=segment_ids, attention_mask=input_mask, labels=label_ids)
logits = outputs.logits
loss = outputs.loss

注意,对于 single_label_classification 分类任务,不要将 label_ids 转化成 int。

5. 理解

5.1 深度学习研究者在做什么

转载自 https://www.zhihu.com/question/433274875/answer/2240764095

以下阶段层层递进

  • 把已有的开源模型下载下来,换成自己的数据集。这个时候应该是偏向于应用型研究,把动物图像数据集换成自己领域的,医学图像数据集,并且跑出 SOTA 的准确率。

这个过程要求看懂开源的代码架构,loss, optimizer, 基本的前向反向传播应该无需修改,但是需要了解数据的预处理,输入的数据是什么样的,如何进行数据转换,数据集从哪里下载,用哪个函数 load data。

  • 从理论上理解模型的一些算法和思想,知道一些重要的超参数背后的思想。有信心调整它,并符合自己预期的效果

最显然的,learning rate, batch size,又比如量化时的 scale factor 搜索范围

  • 深入了解模型的代码实现和细节,能够为模型添加一些主流的,提升性能的 module 或者 trick;能够分析修改前后,模型的差异;对模型的推理和训练过程了如指掌

比如分析推理时某个 epoch 的激活分布情况,分布出现了什么问题,如何解决,loss 变化有什么问题等等

  • 能够根据自己的业务需求,拓展模型的功能,让模型能做更多的事情

这个可能更多体现在业务需求层面,如何去部署公司需要的模型。不过我认为做了量化框架的拓展,也达到这个水平了。

Reference

https://zhuanlan.zhihu.com/p/120315111