首发:AI公园公众号
作者:Pratik Bhavsar
编译:ronghuaiyang
导读
模型训练只是产品化中的很小的一个环节。
问题描述💰
======
最近,我一直在巩固在不同ML项目中工作的经验。我将从我最近的NLP项目的角度来讲述这个故事,对短语进行分类 — 一个多类单标签问题。
团队结构 👪
======
搭建人工智能团队是相当棘手的。如果公司内部人员不具备这些技能,你就必须招聘。由于每个项目都有一个开始和结束时间,很难从头开始让整个团队都参与进来。幸运的是,项目需要的大多数的人我们都有,我们的小团队包括以下成员。
- 产品负责人(1) — 制定项目需求
- 项目经理(1) — 负责项目计划和技术问题
- 敏捷主管(1) — 确保敏捷执行并解决障碍
- 数据分析师(2) — 对领域知识进行迁移,并协助从各种数据存储收集数据
- 数据科学家(2) — 制定数据pipelines,ML POC,软件工程和部署计划。设计和构建部署pipelines、Python软件工程、服务器规模、无服务器pipelines和再训练模型
数据 📊
这是一个NLP项目,数据在RDBMS数据库中。坦率地说,我们很幸运,不需要做太多就能得到训练数据。查询所有权属于与我们一起工作的数据团队,而数据pipeline是由数据科学家创建的。
如果你没有训练数据,你可能必须遵循以下路线之一。
创建训练数据需要时间,最好有一个标注工具。如果你没有,你可以使用Spacy team的prodi.gy获取文本数据。
还有另一个开源工具Doccano。
历史优化
当我们训练模型时,我们意识到我们可能不需要所有的数据,在我们的案例中,这些数据已经有5年了。我们尝试用不同数量的历史数据建模,发现3年足够了。
在不牺牲度量标准的前提下,尽可能少地使用历史可以让我们更快地训练模型,更好地学习最近的模式。
POC 👻
经过对经典和深度学习的多次迭代,我们决定采用(feature extractor + head)基于单词嵌入的方法来完成分类任务。
我们还必须处理数据中的不平衡,并尝试了许多技术。
- 偏采样和降采样
- 用scikit-imblearn进行过采样
- 用scikit-imblearn进行降采样
- model.fit中的类别权重
度量 🙈
由于我们处理的是一个多类(~190)不平衡数据集,我们选择加权f1作为度量,因为它对少数类健壮且容易理解。
软件工程 👀
工程结构
text_classifier│├───notebooks│ ├───classifier-a│ ├───classifier-b│ └───classifier-c│ ├───data(common sample training data)│ ├───preparation│ ├───modelling│ ├───evaluation│ └───final│ ├───tc(acronym of text_classifier. contains core modules)│ │ queries.py│ │ base.py│ │ preparation.py│ │ models.py│ │ train.py│ │ postprocessing.py│ │ predict.py│ │ document_processing.py│ ││ ├───config│ │ input_data_config.py│ │ splunklog_config.py│ │ training_config.py│ ││ ├───nlp│ │ embeddings.py│ │ preprocessors.py│ ││ └───utilities│ helpers.py│ loggers.py│ metrics.py│ plotting.py│ preprocessors.py│ aws.py│ db_connectors.py│ html.py│├───data│ ├───classifier-a│ ├───classifier-b│ └───classifier-c│├───api│ router.py│ flask_app.py│ request_handlers.py│ inference.py│├───env│ base.yaml│ cpu.yaml│ gpu.yaml│ build.yaml││├───deployment│ └───terraform│├───persistence│├───scripts│└───tests
为了让代码以结构化的方式发展,在一开始就设置好项目结构是非常重要的。我们花了很多时间,讨论了很多次才达成一致。看看这个,从基本的框架开始。
这就是我们训练AWS EC2/local模型和AWS s3上的备份代码、数据、模型和报告的方式。目录结构是由prepare和train类自动创建的。
data └───region(we have models trained on many regions) ├───model-a(model for predicting a) ├───model-b(model for predicting b) └───model-c(model for predicting c) └───2019-08-01(model version as per date) ├───code.zip(codebase backup) ├───raw(fetched-data) ├───processed(training-data) └───models └───1(NN-architecture-type) ├───model.h5 ├───encoders.pkl └───reports ├───train_report.csv ├───test_report.csv ├───keras_train_history.csv └───keras_test_history.csv
在进行POCs时,我们不知道哪些模块将成为最终解决方案的一部分,因此不太重视模块化或可重用性。但是一旦我们完成了POC,我们应该将最终的代码合并到notebooks中,并将它们保存在**/notebook /final**中。我们有一个notebook 用于数据准备步骤,另一个用于建模。
notebooks └───classifier-a ├───data ├───preparation ├───modelling ├───evaluation └───final ├───preparation.ipynb └───modelling.ipynb
这些notebooks也成为了我们的展示材料。
继承/引用⏬
我们在写训练类的时候,考虑到了预测类会重用训练类。所以每次我们在预处理或编码步骤上做任何改变时,我们只需要在训练类上做。
推理类
我们的推理模块使用predict类,并对数据进行某些检查,以防止出现故障,比如空字符串。我们还将结果保存到一个中央PostgreSQL推理数据库中。
我们的路由器是一个简单的flask路由器对不同的模型有不同的方法。所有重要的异常都被捕获并与适当的消息一起返回。
推理数据库
我们保存所有的推理结果,以分析生产中的模型,如输入值,预测值,模型版本,模型类型,概率等。
我们的下一步是创建用于创建ML性能报告的api。
设计模式 🐗
Singleton模式来初始化嵌入,并为不同的模型使用相同的对象。这节省了ec2的内存使用。
工厂模式用不同的配置初始化模型训练类。
装饰模式
- 时间函数的装饰器,以了解哪些函数花费更多的时间。
- 一个装饰器,用于在DB查询失败时重试。这确保了数据的获取,并且不会使训练pipeline失败。
- 用于函数执行开始和结束的Splunk日志记录的装饰器。我们在Splunk和AWS Cloudwatch上保存日志。
扩展性 🌀
从一开始,我们就想开发代码库来使用它处理不同的数据。所以我们通过configs来参数化输入数据和模型超参数。
重构🐵
在我们完成了这个项目之后,我们有了许多可以用于任何项目的公用工具。
内部开源
占项目总时间的百分比数字。这对于不同项目来说是不同的。
内部开源允许贡献者组成一个生态系统,为每个人开发和使用可重用组件。我们发现好的软件工程要比POC花费更多的时间。通过创建库,开发人员和数据科学家现在可以集中精力更快地开发和部署模型。
去掉common utilities还使得项目代码库更轻,更容易理解。
部署 🐙
AWS基础设施
- S3
- EC2
- ECR
- ECS
- Cloudwatch
我们使用Conda, Docker, Terraform, Jenkins, ECR, ALB和ECS作为我们的部署pipeline。
![](Productionizing NLP Models.assets/1\_F1TxMnnwoZACG5uPXNngWw.png)
环境 🛠
经过大量的实验和讨论,我们选择通过4个yml配置来处理windows/linux上的所有pip/conda对cpu/gpu的python依赖。
- base.yml → 所有非深度学习包通过pip和conda安装
- cpu.yml → Tensorflow cpu通过pip安装 (pip不会安装cuda toolkit和cuDNN,这样让我们的环境更轻)
- gpu.yml → Tensorflow gpu通过conda安装 (conda可以搞定cuda toolkit和cudnn)
- build.yml → 其他服务需要的包通过pip和conda安装。我们使用gunicorn进行模型服务。gunicorn在windows中不可用,我们将它安装在Linux docker环境中用于生产。
本地测试的环境
conda env create -f env/base.ymlconda env update -f env/cpu.yml
训练模型的Docker/EC2环境
conda env create -f env/base.ymlconda env update -f env/gpu.yml
模型服务的Docker/EC2环境 在cpu实例上 (通过Docker容器)
conda env create -f env/base.ymlconda env update -f env/cpu.ymlconda env update -f env/build.yml
每次我们开始使用一个新包时,我们都会手动将其添加到yml中。我们尝试了pipreqs和conda export — no-builds来自动导出包,但是发现很多依赖项和package-build-info也被导出,使得我们的环境看起来很脏。通过手动添加包,我们可以确定包的使用情况,也可以在POC之后删除一些未使用的包。
最初,我们使用AllenNLP来生成嵌入,并在我们的环境中添加了许多包。由于我们使用Keras进行建模,我们决定完全切换tensorflow生态系统,从tensorflow-hub获取模型。
负载测试 💥
最初,负载测试非常简单。通过使用JMeter测试负载情况,我们优化了这些参数的服务。
- ECS中的任务的数量
- 任务中gunicorn workers的数量
- 每个worker的线程数
我们对自动扩容给出了一个很好的想法,它可以通过平均/最大RAM使用量、平均/最大CPU使用量和API调用的数量来触发。任何方法对我们都不起作用,因为我们不想浪费EC2的资源来为自动扩容保留空间。没有保留空间导致了需要创建EC2,这需要时间。因为知道创建实例需要1-5分钟,所以所有请求都将转到现有的服务,而不会向部署在新EC2上的新任务发送任何内容。
我们也考虑过AWS Fargate,但它比EC2贵两倍。
最后唯一有意义的是分配全部CPU和一半RAM给任务。自动扩容需要RAM,所以我们保留空间用于部署更多的任务,但确保不会浪费CPU,因为它是瓶颈。
我们选择AWS t3实例而不是t2作为其默认的burstable行为,这有助于我们使用累积的积分。
成本优化 🔥
缓存
您可能知道,与word2vec和glove这些固定的非上下文嵌入的词汇不同,像ELMo和BERT这样的语言模型是有上下文的,没有任何固定的词汇表。这样做的缺点是每次都需要通过模型计算这个词的嵌入。这给我们带来了相当大的麻烦,因为我们看到了模型处理导致的大量CPU峰值。
由于我们的文本短语的平均长度为5,并且是有很多重复的,所以我们缓存了短语的嵌入以避免重新计算。我们的代码可以通过添加这个小方法我们得到了一个20 x加速🏄。
#Earlierlanguage_model.get_sentence_embedding(sentence)#Laterfrom cachetools import LRUCache, cached@cached(cache=LRUCache(maxsize=10000))def get_sentence_embedding(sentence): return language_model.get_sentence_embedding(sentence)
缓存大小优化
因为LRU(最近最少使用)缓存的时间复杂度是O(log(n)),缓存越小越好。但我们也知道,我们想要尽可能多地缓存。所以越大越好。这意味着我们必须根据经验优化缓存的最大大小。我们发现50000是我们的甜蜜点。
修正负载测试方法
通过使用缓存,我们不能只使用几个测试样本,因为缓存会使它们不需要计算。因此,我们必须定义可变的测试用例,以模拟真实的文本样本。我们使用python脚本创建请求示例,并使用JMeter进行测试。
Central embedder结构 💢
最后,当我们从3个模型扩展到21个模型时,我们必须考虑如何使其健壮且经济。语言模型是一个沉重的组成部分,而文本清洗和前馈头模型的计算比较轻。
由于语言模型对所有模型都是通用的,所以我们决定创建一个单独的服务供所有模型使用。这让我们减少了一个沉重的负担。
目前,我们也在考虑使用AWS lambda为模型提供服务,摆脱基础设施。
学习 😅
项目完成后,你真的希望有些事情做得更好,而有些事情根本就没做。我脑海中浮现出的几个建议是:
- Apache Airflow + Sagemaker — 对于完成的scheduled pipelines Airflow非常有用,Sagemaker对于超参数调试有一个非常好用的层。
![](Productionizing NLP Models.assets/0\_o6PvGxgnuOMH7ARk.gif)
- 模型重训练 — 避免为项目生成标记数据,否则每次再训练模型时都必须创建它。你可以在工作流程中为训练模型创建标记数据。你可以了解如何保存原始/收集的数据,并编写脚本来创建训练数据,以便使用新数据对模型进行再训练。如果没有上述选项,你还可以利用半监督学习。
- 模型压缩 — 如果你的神经网络模型的延迟超过你的需求,你可以使用剪枝和量化使它们更快。
- 偏差检查 使用MIT的这框架
历史偏差 — 因为数据分布会随着时间而改变表示偏差 — 当数据的某些部分没有得到充分表达时
度量偏差 — 当标签被用作真实标签的代理时
聚合偏差 — 当同一个模型用于不同的数据集时
评估偏差 — 当测试数据与真实世界的数据不匹配时
- 可解释性 — 使用像[eli5]这样的库来理解模型预测和偏差
奖励 🍹
现在,我创建了一个清单,让自己保持在正确的轨道上。有时候在拥挤的人群中很容易迷失方向。
建模清单 📘
- 我们的模型度量和业务度量是什么?它们是相同的还是不同的?
- 更多的数据会改善度量标准吗?我们能得到更多的数据吗?
- 我们是否使用了fp16和多gpu来减少训练时间?我们是否优化了批大小并尝试了one\_cycle\_fit来减少训练时间?我们是用Adam,Radam,ranger还是新的优化器?
- 如果这个问题是通过深度学习解决的,我们是否已经尝试了足够多的经典方法?经典方法和DL在度量和推理时间上有什么不同?
- 模型是手动调优还是算法调优?我们需要超参调优层来进行再训练吗?数据是否随时间快速变化,目前的模型参数是否足够?
- 训练、验证和测试量度之间的区别是什么?
- 数据科学家和领域专家做过错误分析吗?
- 我们可以尝试一下解释性吗?我们试过错误的可解释性吗?
- 模型的错误中有模式吗?它可以用后处理层或一个新特征来解决吗?
- 在预测和客户端使用之后是否需要人工干预?如何还原呢?
部署清单 📗
1、我们是否用模型和编码器支持代码、数据和度量?
2、我们检查过缓存的机会吗?
3、我们是否根据流量定义了一个实际的负载测试吗?如果峰值负载似乎很少,我们是否可以设计为平均流量,并让失败的请求重试吗?
4、我们是用flask,WSGI,uWSGI还是gunicorn提供服务?
5、我们是否已经完成了缓存,workers和线程的大小调整?(不要盲目听从gunicorn对workers的2 n + 1的建议,测试所有的经验值)
6、我们是否处理了请求中输入文本或数字字段的所有边界情况?
7、推理数据库中的字段是否与输入的数据保持一致吗?DB推断错误会导致响应错误吗?
8、我们是否发送适当的消息/标志来响应来自客户端的调试错误?
9、我们应该部署在CPU还是GPU上吗?哪些组件需要GPU吗?
10、预测pipeline中的共性或瓶颈是什么?我们能把它们分离出来吗?
11、我们可以在一个docker镜像中保存所有的模型吗?(我们有22个人在8个不同的ECS上工作,我们想知道我们是否应该把他们都放到一个ECS上。这是灵活性,简单性和降低成本之间的权衡。)
12、我们可以使用无服务器部署吗?(我们每天,每周和每年的负载都是可变的,但流量适中,我们想知道如何利用serverless。)
13、我们有模型回滚计划吗?
14、数据会随时间变化还是增加?是否需要对模型进行再训练?我们在整个项目计划中计划了吗?
15、代码和部署pipeline是否足够灵活,可以用最小的更改进行再训练吗?
16、你将如何分析模型在生产中的性能?报告生成的频率如何?
—END—
英文原文:https://medium.com/modern-nlp...
推荐阅读
关注图像处理,自然语言处理,机器学习等人工智能领域,请点击关注AI公园专栏。
欢迎关注微信公众号