围绕 transformers 构建现代 NLP 开发环境

本文涉及的产品
模型训练 PAI-DLC,5000CU*H 3个月
模型在线服务 PAI-EAS,A10/V100等 500元 1个月
交互式建模 PAI-DSW,每月250计算时 3个月
简介: 本文将从“样本处理”,“模型开发”,“实验管理”,“工具链及可视化“ 几个角度介绍基于 tranformers 库做的重新设计,并简单聊聊个人对“软件2.0”的看法。

Intro


最近在 review 和重构团队的 NLP 炼丹基础设施,并基于 tranformers 库做了重新设计,本文将从“样本处理”,“模型开发”,“实验管理”,“工具链及可视化“ 几个角度介绍这项工作,并简单聊聊个人对“软件2.0”的看法。


样本处理

核心思路:函数式,流式,组合式,batch 做多路融合,对 datasets 兼容

虽然随机读取的数据集用起来最方便,但是在大部分实际应用场景中,随机读取往往难以实现。不过,我们能构造流式读取的接口,例如:

  • MaxCompute(ODPS) :无法通过行号快速读取数据,但是有 Tunnel 接口支持从某个下标开始顺序读取数据。
  • 文件系统:包括本地文件,HDFS,以及OSS等对象存储。本地文件虽然能用 lseek() 等函数快速跳转到某个位置(且该操作通常为 O(1)),但是如果每条样本字节数不一样,封装为随机读取还是非常复杂,但是做成流式读取就很容易。其他云上的存储介质更是如此。
  • 消息队列:例如 MetaQ,天然流式的数据,可以主动拉取,也可以以订阅的方式封装流式读取的接口。

在我们设计的数据加载框架中,可以用以下代码来实现 ”分别从两个 ODPS 表里读取正样本和负样本,用 func 函数处理后,在 batch 内以 1:1 的方式混合,正负样本分别用两个线程并行读取“。


positive = Threaded(Map(func, ODPS(access_id, access_key, project,
                            positive_sample_table_name,
                            read_once=False)))
negative = Threaded(Map(func, ODPS(access_id, access_key, project,
                            negative_sample_table_name,
                            read_once=False)))
combined = Combine([positive, negative], sample_weight=[1.0, 1.0])

返回的 combined 变量是一个普通的 python generator,可以直接从中获取数据,也可以将其传入 huggingface 的 datasets 模块。该方案优势很明显:灵活可扩展,懒加载节约资源。


# 直接读取数据
for data in combined:
  print(data)

# 使用 huggingface datasets 模块
# 之后可以直接用在 transformers.Trainer 类中参与训练
import datasets
train_dataset = datasets.IterableDataset.from_generator(combined, 
  gen_kwargs={"ranks": [0,1,2,3], "world_size": 4} # 支持分布式训练
)

技术问题:对分布式训练的支持


datasets.IterableDataset.from_generator 函数,可以额外传入一个名为 gen_kwargs 的 dict 类型参数,若某个 value 类型是 list,则会在 dataloader num_workers 数大于 1 的时候,自动进行分片,将分片后的 list 传给底层的 generator[1]。huggingface 在开发模块时,经常使用这种“隐含的调用规约”,在用户输入和输出满足某些条件时,触发特定的功能,这一点在他们开发的其他模块中也有所展现。


在我们的设计中,所有加载数据的 generator 都默认接受 ranksworld_size 两个额外参数,其中 ranks 为 list 类型,代表该 generator 处理的分片列表,world_size 时分片的总数,在实现加载逻辑时,根据这两个参数,读取对应分片的数据。


def _ODPS(access_id, access_key, project, table_name, partition_spec, read_once, retry,
          endpoint, ranks=None, world_size=None):
    # 加载 ranks + world_size 对应分片数据,实现略(计算读取 range 后,使用 PyODPS 加载数据)

def ODPS(access_id, access_key, project, table_name, partition_spec=None, read_once=True, retry=True, endpoint="http://xxxxxx"):
    return partial(_ODPS, access_id, access_key, project, table_name, partition_spec, read_once, retry, endpoint)

注意,这里使用了partial返回了一个原始函数“科里化”后的版本,这是使 generator 可组合的关键设计。


模型开发

核心思路:
  1. 继承 PreTrainedModel,PreTrainedCofig,PreTrainedTokenizer 基类,与 transformers 体系打通。
  2. 通过 mixin / monkey patching 方式,扩充现有框架功能,例如对 OSS 模型加载/存储的支持。

通过继承 PreTrainedModel,PreTrainedCofig,PreTrainedTokenizer 三件套,模型就能使用 transformers 框架的一系列基础设施,从而实现如下效果:


# 加载我们项目组开发的分类模型(多任务层次分类)
model = BertForMultiTaskHierarchicalClassification.from_pretrained("./local_dir")
# or 从 OSS 直接加载
model = BertForMultiTaskHierarchicalClassification.from_pretrained("oss://model_remote_dir")
# 保存模型
model.save_pretrained("./local_dir")
model.save_pretrained("oss://model_remote_dir")
# 加载我们使用 C++ 开发的 tokenizer
tokenizer = ShieldTokenizer.from_pretrained("oss://model_remote_dir")

# 使用 AutoClass 实现相同功能,不需要指定特定的模型类名,由框架自动推断
model = AutoModel.from_pretrained("oss://model_remote_dir")
tokenizer = AutoTokenizer.from_pretrained("oss://model_remote_dir")

# 扩展 transformers 默认的 pipeline
# 增加 multitask-hierarchical-classification 任务
pipe = pipeline("multitask-hierarchical-classification", model=model, tokenizer=tokenizer)
print(pipe("测试文本"))

可以看出,我们自研的模型使用方式和 transformers 上的开源模型别无二致,甚至还能直接从 OSS 上加载模型,极大降低了模型的使用学习成本。


如何支持集团内的存储介质


为了使 transformers 框架中的对象支持 OSS 存储,我们使用了 mixin 的方式进行了逻辑改写,让所有自研的新模型都继承 OSSRemoteModelMixin,对于 AutoModel,则直接覆盖他们的 from_pretrained 方法。


class OSSRemoteModelMixin(object):
    """
    支持用户在 from_pretrained 和 save_pretrained 时按照 oss://path 的格式指定路径 (bucket, ak, sk 需要在环境变量中指定, 见 util.oss_util 类)
    可以用于所有包含 from_pretrained 和 save_pretrained 方法的类 (config or tokenizer or model)
    """
    @classmethod
    def from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.PathLike]], *model_args, **kwargs):
        pretrained_model_name_or_path = convert_oss_to_local_path(cls, pretrained_model_name_or_path, 
            kwargs.get('cache_dir', risk_shield.CACHE_DIR))
        return super(OSSRemoteModelMixin, cls).from_pretrained(
            pretrained_model_name_or_path, *model_args, **kwargs)

    def save_pretrained(
        self,
        save_directory: Union[str, os.PathLike],
        *args,
        **kwargs
    ):
        prefix = "oss://"
        oss_path = save_directory
        if save_directory.startswith(prefix):
            # save to a temp dir
            # .......
      # 将文件拷贝到 OSS,实现略
            return res
        else:
            res = super(OSSRemoteModelMixin, self).save_pretrained(save_directory, *args, **kwargs)

# 让模型继承自 OSSRemoteModelMixin 就会自动获得 OSS 存取的能力

class BertForMultiTaskHierarchicalClassification(
  OSSRemoteModelMixin, BertPreTrainedModel):
    config_class = BertForMultiTaskHierarchicalClassificationConfig

    def __init__(self, config:BertForMultiTaskHierarchicalClassificationConfig):
       # .....

# 对于 AutoModel,则直接覆盖他们的 from_pretrained 方法。

def patch_auto_class(cls):
    """
    让 AutoClass 支持 OSS 路径
    """
    old_from_pretrained = cls.from_pretrained
    def new_from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.PathLike]], *model_args, **kwargs):
        pretrained_model_name_or_path = \
            convert_oss_to_local_path(cls, pretrained_model_name_or_path, 
            kwargs.get('cache_dir', risk_shield.CACHE_DIR))
        return old_from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
    cls.from_pretrained = classmethod(new_from_pretrained)

同样,让框架支持 HDFS,ODPS Volumn 等其他存储介质,也可采用相同的方案。


如何使用自己的 Tokenizer


并不是所有业务都适合直接使用 BertTokenizer,例如,对于我们所在的业务,采用一种融合了外部知识信息的特殊方式进行 tokenize,用来提升模型效果。而且,我们的预测服务是 C++ 代码,为了保证预测和训练时的 tokenize 逻辑完全一致,我们基于 ctypes,用同一套 C++ 代码实现 python tokenize 的底层逻辑,并在上层做好 PreTrainedTokenizer 的接口兼容。为了实现一个兼容 PreTrainedTokenizer 的 C++ tokenizer,我们重新实现了以下接口,有类似需求的同学可以参考。

类变量 vocab_files_names
__getstate__, __setstate__,解决 C++ 对象的 pickle 问题
_convert_token_to_id
_convert_id_to_token
convert_ids_to_tokens
__setattr__
vocab_size
get_vocab
tokenize
save_vocabulary
_pad 对 tokenize 后的结果进行 pad,主要用在 trainer 里的 collator
_encode_plus 处理单条文本
_batch_encode_plus 处理 batch 文本

使用我们自研的 tokenizer,在单核下比 huggingface 使用 Rust 开发的 BertTokenizerFast 在多核下更快。


训练代码


符合 transformers 标准的模型,可以直接使用框架的 Trainer 完成模型训练,并通过 data_collator,callback 机制,对训练过程进行定制。下面结合上面介绍的内容,给出训练一个完整分类模型的代码(忽略不重要的细节)。


def compute_metrics(model, eval_pred):
    logits, labels = eval_pred
  # 针对层次分类设计的评估指标
    metrics = evaluate_multitask_hierarchical_classifier(model, labels)
    return metrics

# 定义模型
tokenizer = ShieldTokenizer.from_pretrained("oss://backbone")
config = BertForMultiTaskHierarchicalClassificationConfig.from_pretrained("oss://backbone")
config.multi_task_config = {
  # 这里的分类树仅是例子
  "main": {
    "hierarchical_tree": 
    ["父类别1", "父类别2",
            ["父类别3", 
        ["父类别3-子类别1", "父类别3-子类别2", "父类别3-子类别3", "父类别3-子类别4"]
      ]
        ]
  }
}
model = BertForMultiTaskHierarchicalClassification.from_pretrained("./backbone")
# 定义训练数据加载策略
positive = Threaded(Map(func, ODPS(access_id, access_key, project,
                            positive_sample_table_name,
                            read_once=False)))
negative = Threaded(Map(func, ODPS(access_id, access_key, project,
                            negative_sample_table_name,
                            read_once=False)))
combined = Combine([positive, negative], sample_weight=[1.0, 1.0])

train_ds = datasets.IterableDataset.from_generator(combined)
training_arg = TrainingArguments(
  output_dir="./output",
  overwrite_output_dir=True,
  num_train_epochs=4,
    # ...
  # 其他训练参数
    # ...
  dataloader_num_workers=2,
)
trainer = Trainer(
  model=model,
  args=training_arg,
  train_dataset=train_ds,
  tokenizer=tokenizer,
  eval_dataset=val_ds,
  compute_metrics=partial(compute_metrics, model),
  # 针对层次分类开发的 collator
  data_collator=MultiTaskHierarchicalClassifierCollator(
    tokenizer=tokenizer, model=model, max_length=max_length, 
    task_label_smooth=task_label_smooth
  )
)
# 将实验指标写入 tensorboard 并上传到 OSS
trainer.add_callback(OSSTensorboardWriterCallback("experiment/v1/"))
trainer.train()

从代码中可以看出,唯一需要改动的,就是 data_collator(多进程组 batch 及样本后处理) 和 compute_metrics 逻辑,根据不同的任务头,需要开发特定的 collator,其他都是标准的模版代码。使用 Trainer 类后,可以使用 transformers 框架的强大功能,例如对 DDP,deepspeed 等训练技术的支持,以及对梯度爆炸等问题的 debug 能力。(设置 debug="underflow_overflow")需要注意的是,为了让 Trainer 正常工作,你的模型的返回必须符合以下格式,如果不喜欢这种 “隐含的调用规约”,可以做一个 trainer 的子类对逻辑进行更彻底的重写。


  1. 第一个元素是 loss,trainer 自动优化该值。
  2. 后续的元素只能是 python dict / list / tuple / tensor,tensor 第一维的大小必须和 batch size 一致。最理想的情况就是一个二维 logits 矩阵。


模型部署


针对项目中用到的几种部署形态,重新封装部署,预测代码。


import risk_shield
from transformers import AutoTokenizer, AutoModel
model = AutoModel.from_pretrained("oss://model.tar.gz")
tokenizer = AutoTokenizer.from_pretrained("oss://model.tar.gz")

# 导出 ONNX
model.export_onnx(output_dir)

# 或导出 TensorRT
model.export_tensorrt(output_dir)

# 或导出 Tensorflow(ODPS 部署)
model.export_tf_saved_model(output_dir)

# 导出切词器到同一目录
tokenizer.save_pretrained(output_dir)

#########################

# 部署时加载对应 pipeline
from risk_shield import ONNXHierarchicalClassifierPipeline
from risk_shield import TensorRTHierarchicalClassifierPipeline
from risk_shield import TFSavedModelHierarchicalClassifierPipeline

pipe = TensorRTHierarchicalClassifierPipeline(output_dir)
result= pipe("测试文本")

最小化依赖


在设计时,我希望整个框架对开发环境的依赖尽可能低,因此只需要 transformers 就能运行,这套“工具集” 本质上是 transformers 的外挂补丁,遵循该库的所有接口协议,因此,自然是“高内聚”,“低耦合”的,可以选择使用其中的部分,例如只用分类头,但是自己写 trainer,或者只用模型,不用数据加载模块。可以在 GPU 服务器或者 Mac 笔记本上训练模型。


实验管理


Trainer 类原生支持 wandb [2]等平台,对实验过程中的指标进行查看&对比&管理,由于这涉及到将实验数据上传到外网,我们选择将实验指标以 tensorboard 的格式保存在 OSS 中,上面代码的 OSSTensorboardWriterCallback("experiment/v1/") 部分就实现了这一功能。

此外,我们在开发了一个实时查看 OSS 上 tensorboard 的命令行工具,一般我们都会在 GPU 服务器上训练模型和上报指标,在各自的笔记本上查看和分析指标,你看,不需要依赖 wandb 这种外部平台,也能实现类似的指标管理能力。

1. 命令行工具,实时拉取 OSS 上的 tensorboard

image.png

2. 超参搜索及指标对比

image.png

工具链及可视化


结合 gradio 库,我们对所有模型任务都开发了可视化工具,在模型训练中,可以随时进行效果试用,具体可以参考 gradio 文档,由于我们实现了对 AutoModel 的支持,因此无论模型采用什么 backbone,只要任务一致,就能使用同一个工具进行查看分析。


1. 命令行工具,加载 OSS 上保存的模型 checkpoint 并打开浏览器页面

image.png

2. 基于 gradio 开发的 debug 工具(分类模型)

image.png

3. 基于 gradio 开发的 debug 工具(NER 模型)

image.png

“软件2.0”


Andrej Karpathy,前特斯拉人工智能负责人,在 2017 年发表了一篇名为《Software 2.0》 [3]的文章,文中预言了深度学习将逐步替代掉大部分传统“手工”算法,而深度学习技术将从某个传统算法系统的子模块,演化成为算法系统的主要构成部分(例如自动驾驶),或者如 Andrej 所说:“抱歉,梯度下降能比你写出更好的代码”。


image.png


他预言,为了实现软件2.0,需要一整套服务于深度模型开发的工具栈,就像传统软件需要 pip,conda 这类包管理器(package manager),GDB 这类 debug 工具,Github 这类开源社区一样,深度学习也需要模型 debug 工具,模型和数据集的管理器和开源社区。


但他没有预言到的是,类transformer 架构在之后的年月里大放异彩,不仅统一了 NLP 领域,而且正在逐步统一 CV 等其他领域。“预训练-微调” 成为业界最常见的模型开发范式。而 huggingface 公司,借着 transformers 库的东风,以及围绕它建设的模型开源社区(huggingface hub)[4],成为当前 NLP 开发事实上的标准,连目前最火的大模型,都选择在 huggingface 发布,例如 ChatGLM:


image.png

这些已经有了软件 2.0 的雏形。因此,只有做到与 transformers 框架和接口标准的充分整合,才能真正与开源社区与行业的技术进步接轨,虽然我们无法使用外部的软件 2.0 工具栈(例如上面提到的 wandb),但是我们可以在团队内部以软件 2.0 的方式工作,例如把 OSS 作为一种 huggingface hub 的替代物(😂),在本地启动 gradio 作为 spaces [5]的替代物。使用 OSS 替代 hub 还有一个额外的好处:与中心化的 hub 不同,每个用户(项目组)可以使用自己私有的 OSS 地址,用 OSS 自带的 ACL 进行权限控制。我们在开发环境中,实现了 push/pull 的能力,对于项目协作/复用来说足够了。


-- 团队 A 同学发布模型到 OSS
shield_publish ~/checkpoint_dir https://xxxx.aliyuncs.com/../xxxx.tar.gz
WARNING:root:从本地目录上传:~/checkpoint_dir
xxxx.tar.gz: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 100/100 [01:50<00:00,  1.11s/it]
WARNING:root:已发布OSS(url):https://xxxx.aliyuncs.com/../xxxx.tar.gz

-- 团队 B 同学使用 OSS 上“内部开源”的模型
AutoModel.from_pretrained("https://xxxx.aliyuncs.com/../xxxx.tar.gz")

当然,在国内也有大量与 huggingface 类似的平台,例如由达摩院开发的 modelscope 社区[6] 和百度的 paddlenlp[7]随着软件 2.0 工具栈的成熟,算法开发流程将逐步标准化,工程化,流水线化,不仅大量非科班的玩家都能用 LoRA 微调大模型,用 diffusers 生成人物了,甚至连 AI 都能开发软件[8]了,那么在未来,“算法工程师” 这个 title 会变成什么呢,会调用 import transformers 算不算懂 NLP 呢?这道题作为 课后练习,留给同学们进行思考!


参考链接:

[1]https://huggingface.co/docs/datasets/v2.13.1/en/package_reference/main_classes#datasets.Dataset.from_generator

[2]https://wandb.ai/site

[3]https://karpathy.medium.com/software-2-0-a64152b37c35

[4]https://huggingface.co/docs/hub/index

[5]https://huggingface.co/spaces

[6]https://modelscope.cn/models

[7]https://huggingface.co/docs/hub/paddlenlp

[8]https://arxiv.org/abs/2307.07924


来源|阿里云开发者公众号

作者|何磊

相关文章
|
4天前
|
机器学习/深度学习 自然语言处理 PyTorch
从零开始构建nlp情感分析模型!
本教程介绍了如何使用PyTorch和Hugging Face的Transformers库构建一个情感分析模型。主要内容包括导入所需库、读取训练数据集、加载预训练的BERT模型和分词器、定义情感数据集类、划分训练集和验证集、创建数据加载器、设置训练参数、训练模型、评估模型性能以及定义和测试预测函数。通过这些步骤,可以实现一个简单而有效的情感分析模型。
12 2
|
1月前
|
机器学习/深度学习 数据采集 自然语言处理
【NLP自然语言处理】基于PyTorch深度学习框架构建RNN经典案例:构建人名分类器
【NLP自然语言处理】基于PyTorch深度学习框架构建RNN经典案例:构建人名分类器
|
3月前
|
机器学习/深度学习 自然语言处理 PyTorch
PyTorch与Hugging Face Transformers:快速构建先进的NLP模型
【8月更文第27天】随着自然语言处理(NLP)技术的快速发展,深度学习模型已经成为了构建高质量NLP应用程序的关键。PyTorch 作为一种强大的深度学习框架,提供了灵活的 API 和高效的性能,非常适合于构建复杂的 NLP 模型。Hugging Face Transformers 库则是目前最流行的预训练模型库之一,它为 PyTorch 提供了大量的预训练模型和工具,极大地简化了模型训练和部署的过程。
160 2
|
3月前
|
Apache UED 数据安全/隐私保护
揭秘开发效率提升秘籍:如何通过Apache Wicket组件重用技巧大翻新用户体验
【8月更文挑战第31天】张先生在开发基于Apache Wicket的企业应用时,发现重复的UI组件增加了维护难度并影响加载速度。为优化体验,他提出并通过面板和组件重用策略解决了这一问题。例如,通过创建`ReusableLoginPanel`类封装登录逻辑,使得其他页面可以轻松复用此功能,从而减少代码冗余、提高开发效率及页面加载速度。这一策略还增强了应用的可维护性和扩展性,展示了良好组件设计的重要性。
52 0
|
3月前
|
人工智能 自然语言处理 机器人
掌握未来沟通的艺术:运用TensorFlow与自然语言处理(NLP)技术,从零开始构建你的专属智能对话机器人,让机器理解你的一言一行
【8月更文挑战第31天】本文详细介绍如何利用TensorFlow与自然语言处理技术开发对话机器人。从准备问答数据集开始,通过预处理、构建Seq2Seq模型、训练及预测等步骤,最终实现了一个简易的聊天机器人。示例代码涵盖数据加载、模型搭建及对话功能,适合希望在实际项目中应用AI技术的开发者参考。
42 0
|
3月前
|
机器学习/深度学习 自然语言处理 搜索推荐
构建智能搜索应用:Elasticsearch与自然语言处理的融合
【8月更文第28天】随着大数据和人工智能技术的发展,用户对搜索应用的需求已经从简单的关键词匹配转向了更加智能化、人性化的交互方式。本文将探讨如何利用Elasticsearch和自然语言处理(NLP)技术构建一个能够理解用户意图并提供精准搜索结果的智能搜索系统。
234 0
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术在自然语言处理中的应用与挑战
【10月更文挑战第3天】本文将探讨AI技术在自然语言处理(NLP)领域的应用及其面临的挑战。我们将分析NLP的基本原理,介绍AI技术如何推动NLP的发展,并讨论当前的挑战和未来的趋势。通过本文,读者将了解AI技术在NLP中的重要性,以及如何利用这些技术解决实际问题。
|
2月前
|
机器学习/深度学习 数据采集 自然语言处理
深度学习在自然语言处理中的应用与挑战
本文探讨了深度学习技术在自然语言处理(NLP)领域的应用,包括机器翻译、情感分析和文本生成等方面。同时,讨论了数据质量、模型复杂性和伦理问题等挑战,并提出了未来的研究方向和解决方案。通过综合分析,本文旨在为NLP领域的研究人员和从业者提供有价值的参考。
|
27天前
|
自然语言处理 算法 Python
自然语言处理(NLP)在文本分析中的应用:从「被动收集」到「主动分析」
【10月更文挑战第9天】自然语言处理(NLP)在文本分析中的应用:从「被动收集」到「主动分析」
43 4
|
29天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI在自然语言处理中的创新应用
【10月更文挑战第7天】本文将深入探讨人工智能在自然语言处理领域的最新进展,揭示AI技术如何改变我们与机器的互动方式,并展示通过实际代码示例实现的具体应用。
33 1

热门文章

最新文章

下一篇
无影云桌面