NLP 实战 (1) | AI 编程也遵循软件工程的基本原理

结构化 NLP 服务之路,本文长期更新。


团队成员 NLP 专栏目录


管道(pipeline)

  • 从不同数据源(source)获取数据
  • 清洗数据
  • 构建数据集(dataset)
    • 数据集管理
  • 拆分训练集/验证集/测试集
  • 选择机器学习框架/算法(framework/algorithm)
  • 模型训练(train)/预训练/微调训练
  • 构建分类器(classifier)
  • 基于分类器提供 Rest 服务(server)
  • 生成测试结果,统计两个重要指标:
    • 召回率
    • 精度
  • 输出结构化数据
    • 结构化数据提供给目标(dest)应用服务
  • 服务于应用层(application)

Python 环境

使用Python,一开始都会浪费很多时间在环境上,以至于我们需要一开始就单独一章把它彻底梳理清楚,这些问题包括:

  • Python 的不同版本,目前比较推荐的是
    • Python3.6/Python3.7/Python3.8
    • 一般来说,建议固化到某个主版本,例如我们默认固化到 Python3.8,对有些低版本需求,固化到 Python3.6
  • pip 的不同版本,每个 Python 版本都对应一个pip,装 Python 版本还需要装对应的 pip
  • Python 某个库在某个版本下可以装,但是它依赖的库在这个版本下的版本不能跑
    • 有依赖关系的包,例如 a 依赖 b,如果先装了 b,有可能先装的 b 的版本就不适配 a 需要的 b的版本,这个时候安装的先后顺序也就有关系。
  • Python 某个库在某个系统下可以装,但是在另外一种系统上它的依赖库版本不对,无法安装或正确执行,例如 libc库的 依赖。
  • Python 的某个包依赖底层运行时版本不匹配:
    • glibc / gcc 版本升级
    • wget/./configure/make/make install 四件套
手工方式(manual)
  • 安装目标 python 版本,查看 python 的所有版本,确定一个大版本的最高小版本,例如 3.8的最高版本: python3.8.10
    • wget https://www.python.org/ftp/python/3.8.10/Python-3.8.10.tgz
    • tar xzf Python-3.8.10.tgz
    • cd Python-3.8.10
    • ./configure --enable-optimizations
    • make altinstall
  • 安装对应 pip版本
    • 下载 get-pip 脚本
    • 使用对应版本的 python 安装对应版本的 pip
      • python3.8 get-pip.py
      • 更新到可用的最新版本:python3.8 -m pip install --upgrade pip
  • 手工更改 /usr/local/bin 下 python 和 pip 的软连接映射
    • pip 软链接
      • ln -s -f /usr/local/bin/pip3.8 /usr/bin/pip
      • ln -s -f /usr/local/bin/pip3.8 /usr/bin/pip3
      • ln -s -f /usr/local/bin/pip3.8 /usr/bin/pip3.8
    • python 软链接
      • ln -s -f /usr/local/bin/python3.8 /usr/bin/python
      • ln -s -f /usr/local/bin/python3.8 /usr/bin/python3
      • ln -s -f /usr/local/bin/python3.8 /usr/bin/python3.8
使用 pyenv 管理
  • 使用 pyenv 管理python环境
  • 查看管理的 python 版本:pyenv versions ,带*号带是当前使用的版本
  • 验证当前python版本:python --version
  • 查看有哪些可用版本的python:pyenv install --list
  • 安装指定版本python:pyenv install 3.8.10
  • 切换版本:pyenv global 3.8.10
  • 如果是 Mac 系统,zsh 和 fish 两个shell 环境还需要为两个 shell 添加一些配置,参考 pyenv git 里的说明,请搜关键词 Zshfish
使用 conda 管理
  • 使用 conda 管理python环境
  • 安装 minicoda
  • 创建并安装指定版本的 python 环境:conda create -n py3.8 python=3.8.10
  • 切换环境:source activate py3.8
  • 查看当前生效的python和pip版本:python --version, pip --version
依赖库配置

项目内的依赖库配置,应该做版本固化配置,所有现代的包管理软件都支持结构化的包依赖配置,例如:

  • nodejs 的 package.json 使用 json 配置包依赖,区分开发依赖库和运行依赖库;
  • rust 使用 toml 配置包依赖,并且可以使用 feature 支持根据需求递归的切换依赖分组。

而pip 的包依赖文件比较简单,直接是一个文本文件分行配置包名,可以指定版本。我们的策略是:

  • 为不同 Python 版本提供不同的包配置文件:
    • pip3.6.txt: 配置 3.6 环境的 python 库和版本
    • pip3.8.txt: 配置 3.8 环境的 python 库和版本
  • 强制要求库必须指定版本号
  • 可选的可以为开发环境依赖添加开发环境库依赖,例如
    • pip3.8-dev.txt

进一步,在这个基础上构建 Docker 环境。

机器学习库

万变不离其宗,程序=数据结构+算法,每一种特定的库处理的是特定数据结构相关的算法,理解这点,保持目标问题导向的库选择和使用。

整体设计:统一命令行接口

将整个管道的不同阶段操作统一到一致的命令行接口,不要让NLP任务变成一堆无序的项目和脚本。设计上,类似 git,将管道中的多任务统一到一致的接口里。

基本操作心智模型:python main.py -p {profile} -a action_name [sub options]

环境(profile)
  • -p 指定配置环境:
    • dev 开发环境配置
    • fat 测试环境配置
    • pre 预发布环境配置
    • pro 线上环境配置

程序的每个模块运行时,都会受两个配置的影响:

  • 环境配置(config),命令行通过 -p {profile} 指定了环境后,程序启动时就会加载对应的环境配置文件,配置文件可以是本地配置的,也可以是远程集中式配置中心拉取的。
  • 命令行参数(options),命令行与定义了一些通用的选项,每个模块运行时,都可以根据约定根据选项执行不同的行为。

环境配置(config)和命令行参数(options)构成了一个上下文(context) ,每个模块、服务,都可以灵活地根据 context 对行为做动态切换。通过 context,达成程序整体的参数化深度可调试能力。

行为(action)
  • -a 指定action,例如:
    • 构建数据集
      • -a dataset.build.tag.top5: 构建 top5 标签数据集
      • -a dataset.build.questions.top5: 构建 top5 问题数据集
    • 数据集应该支持上传和下载能力(TODO)
      • -a upload.questions.top5: 上传 top5 问题数据集
      • -a download.questions.top5: 下载 top5 问题数据集
    • 启动服务
      • -a server.ask: 启动问答服务
      • -a server.bert: 启动bert词向量计算服务
    • 测试服务
      • -a test 执行所有测试
      • -a test.code: 测试代码服务
        • -a test.code.clasifier 测试代码类型分类服务
        • -a test.code.extract 测试代码提取服务
      • -a test.title: 测试标题合成服务
      • -a test.tag: 测试标签分类服务

我们都知道一个 HTTP 服务可以通过 Rest API 使用 path+query 提供参数,以及GET/POST 接口来支持不同的 API 调用。HTTP 服务内部实现通过对 path 的路由来完成调用请求的分发。 命令行参数一般会提供一堆的选项来控制程序的行为。这带来了灵活性也带来了记忆的痛点。在经过多个命令行接口的设计与实现后,我觉的目前的这种方式是比较方便又能保持简洁的。

-a 或者 --action 选项,被设计成通过.分割符来支持类似 HTTP 请求的 path 的能力,query部分则继续由独立的命令行选项来支持。于是我们的程序不同模块的 __init__.py 就负责对 --action 的分发(dispatch) 。

上面的例子,也示例了我们做到了:

  • 对NLP不同管道行为做精细的操作
    • 数据构建
    • 启动服务
    • 执行测试
  • 测试上可以做精细力度的测试控制

后续我们会把 训练标注数据 的管道任务也统一进来,达成对管道任务的持续集成,我们相信良构是为输出优秀的结果服务的。

整体设计:目录结构

如果理解了上面的统一命令行设计思路。目录结构会是一个自然导出的设计:

  • 提供唯一的入口:main.py
    • 以后可进一步做成全局命令
  • 根据管道任务切分大目录
    • 大目录下的__init__.py 负责进一步分发行为。
  • 每个管道下切分子模块
    • 子模块的库依赖应该尽可能通过局部化来隔离
    • 子模块的加载尽量使用延迟加载
  • 数据目录下也根据类别保持多层结构
    • 根据环境分大目录
    • 根据类别递归切分
  • 保持正交设计
    • 每个管道下,对应二级子模块

参考例子:

.
├── README.md
├── doc
├── log
├── pip3.8.txt
├── src
│ ├── main.py
│ ├── options.py
│ ├── common
│ ├── config
│ ├── data
│ │ ├── dev
│ │ ├── fat
│ │ ├── pre
│ │ └── pro
│ │     ├── datasets
│ │     │ ├── codes
│ │     │ ├── keywords
│ │     │ ├── questions
│ │     │ ├── stopwords
│ │     │ └── tags
│ │     ├── models
│ │     │ ├── answer
│ │     │ ├── code
│ │     │ ├── ocr
│ │     │ ├── tag
│ │     │ └── title
│ │     └── test
│ │         ├── answer
│ │         ├── code
│ │         ├── ocr
│ │         ├── tag
│ │         └── title
│ ├── dataset
│ │ ├── __init__.py
│ │ ├── build
│ │ ├── download
│ │ ├── query
│ │ ├── store
│ │ └── upload
│ ├── server
│ │ ├── __init__.py
│ │ ├── answer
│ │ ├── book
│ │ ├── code
│ │ ├── interface.py
│ │ ├── ocr
│ │ ├── service.py
│ │ ├── tag
│ │ └── title
│ ├── test
│ │ ├── __init__.py
│ │ ├── test_answer.py
│ │ ├── test_bert_client.py
│ │ ├── test_code_classifier.py
│ │ ├── test_code_extract.py
│ │ ├── test_html_parse.py
│ │ ├── test_img_2_pl.py
│ │ ├── test_skill_tree.py
│ │ ├── test_tag.py
│ │ ├── test_title.py
│ └── train
│     ├── answer
│     ├── code
│     ├── ocr
│     ├── tag
│     └── title
└── version.json

通过目录的正交设计,保持良构。

如何迭代

开发/内部部署发布/测试/迭代/发布,其中内部部署发布 是首要重要的事情,遵循一些必要的原则有助于达成这点:

  • 内部发布优于第1版正确性,先把流程打通并发布一个版本,快速进入测试-开发迭代优先于1版正确性,最好能达成每周发布。
  • 第一性原理,NLP处理的数据是非结构化的,NLP的能力是通过对数据向量化,对数据进行分类和标注,提供数据背后的结构化信息。有了结构化信息,构建这些结构化信息的关系,进而可以对这些结构化的关系信息进行查询或推理。围绕这点带着要解决的目标问题去寻找工具,而不是先找工具,再找问题。
  • NLP任务也和其他所有软件开发一样,任务必须遵循SMART原则才能保持持续发版的能力。一个NLP任务可以是很模糊的,也可以是定向和明确的,例如标注和多分类相对明确的目标,聊天机器人是相对模糊的目标。一个模糊的目标不是一锤子买卖,它应该是基于一系列的有明确目标的子任务的基础之上来做。因此,要盯着那一个一个明确的子任务去做,再去合成一个大的任务。
  • 一个明确的具体的任务,一开始就可以做脚手架:部署后的接口先定下来,先提供一个fake版本。再通过一周一次的发布目标去反推改进。
  • 一个NLP任务,内部部署后要让标注对测试数据做仔细标注和统计,再分析用例的情况,这样去迭代。没有衡量,就没有改进的基准。

如何有效交付

NLP任务不纯粹只是算法问题,它也是一个产品,含有这些步骤:

  • 需求是什么?(Need)
  • 解决方式是什么?(Approach)
  • 好处是什么?(Benefits)
  • 跟什么竞争?(Competition)
  • 如何推广(Delivery)

一个不好的开发策略是:

  • 试图一下子开发一个大的功能:例如直接产出一个聊天机器人
  • 开发一个功能,但是一直在改进精确度,迟迟不上线获得真实的反馈

一个好的开发策略是使用NABCD模型来迭代:

分析痛点:例如用户在 blink.csdn.net 的 #你问我答 活动里提问题,我们希望引导用户到 问答(ask.csdn.net) 上提问。但是用户的很多问题是直接截图提问。我们需要识别:

  • 用户提的是什么编程语言的问题?这样我们可以给这个问题打上合适的 CSDN统一标签
  • 用户截图的代码文本能否提取出来,这样回答者可以直接从文本里拷贝有用的关键字。

找到AI的解决方式:通过 OCR 识别文本,再对文本进行编程语言类型识别。这就是一个具体的算法解决方式。

好处 是从 blink 上生成的问题获得了更好的结构化信息:编程语言标签和代码文本。这样回答者可以更好地回答该问题,从而解决提问者的需求。

跟什么竞争? 问答网站很多,其他网站没有这个功能,那么 问答(ask.csdn.net) 上的 “截图提问” 会更有信息含量。一个功能的改进可能不起眼,10个功能的信息含量改进,会获得整体信息含量的差距。另一个方面,跟自己以前的功能比,有了初步的 “智能” 。

如何推广? 前面说了,在 NLP 开发中,算法一直改进会很花时间,并且也会导致迟迟不交付的现象。因此,Delivery实际上就是为上线花时间:

  • 花时间把功能服务化,提供 Rest API
  • 花时间与标注做测试,统计 API 的召回率和精确度,定义好 “足够好” 的指标,达到足够好就应该尽快与应用集成。
  • 花时间与产品讨论,设计好反馈路径。
  • 花时间与应用开发集成,把功能集成到应用处理的流程里。

从这个分析过程中,我们在定NLP任务的时候,应该一开始就考虑好如何Delivery,避免一致忙于做算法模块,但是与团队其他人的迭代“延迟”很大。我们后面会基于这个原则来迭代所有其他NLP任务。

持续集成

NLP任务需要经过一系列的管道过程。在过程中,SoftwareTeacher 定好每周4做一次集成,周5达成一次发布的节奏。共识是无论多小的结果,都要保持发布的节奏,发布可以是对外的也可以是对内的。每周一次的发布,就要在开发中以结果为导向平衡好紧急的事情和重要的事情。总的来说每周一次发布,团队就会有一个很强的开发驱动力和迭代节奏。

持续集成有这些具体的结果:

  • 所有子服务,无论结果好坏,都通过集成保持接口的可运行,从而倒推程序的良构。
  • 所有子服务,在集成中都要通过模块测试,可尽早发现测试中出现的各种奇怪问题。
  • 所有子服务,在集成时逐渐要求提交小规模测试和标注。跑下结果通常可以发现问题,为进一步解决带来具体的思路。“没有衡量,就没有改进的基础”,我们逐渐会明确以召回率和精度来实施这个迭代。
  • 开发环境和部署环境通常会有一些依赖不一致的问题,通过集成,就迫使程序一直和环境保持适配。
  • 单一服务运行正常,多服务集成时,则会尽早暴露模型加载、内存占用等隔离和性能问题。
  • 持续的内部集成,不断检测指标和 “足够好” 之间的差距,调整方向。内部集成是应用集成的 “孵化器”。

解决资源需求

我们是一个小的分布式团队。需要解决一些资源的需求。做NLP开发,需要为团队解决资源的问题。包括:

  • 团队如何提供 GPU 资源,需要综合评估 GPU云服务的资源和线下GPU服务器/工作站的资源优劣点,从团队快速迭代的角度思考、评估并解决。
  • AI训练的数据集、模型都有一定的大小。构建数据集、训练模型,这些数据都需要解决同步、部署的问题。

解决这些问题,让 NLP 工程师专注在目标问题上保持有效迭代。

目标:点线面

我们有很多数据,这些数据的特征是:

  • 文本/代码/图片混排
  • 半结构化
  • 用户有一些弱标注信息
  • 信息不全或缺失
  • 重复问题
  • 过时

从单点上,我们的目标是对这些数据进行深度的结构化:

  • 从图片中提取有用的代码并识别代码类型
  • 识别图文中的代码段及其类型
  • 生成推荐标题,提升整体标题质量
  • 去重

从线上,我们在做:

  • 合成的的技能树(SkillTree)
  • 将结构化的数据 Fill 到 SkillTree
    • 初/中/难 不同阶的结构化知识
  • 在多个维度上让开发者在技术成长中找到自己在领域成长树中的位置,在一条线上学习,而不是碎片化
  • 一个问题搜索了很多链接还是不能解决问题?

从面上来说:

  • 做到这里再说!

部署上线

把一个服务部署上线,有时候离不开全流程的打通:

  • 如果代码分层有问题,就需要彻底改造成可灵活配置
  • 必须有足够的测试数据集,自动计算召回率和精度
  • 必须解决性能和内存占用问题
  • 能反复做测试-迭代,达成目标

从 demo 到 mvp 到部署上线总是有个非线性的过程,很多人不能理解为什么有时候我们需要大段的时间连续不断地解决问题,其实根本原因就是解决问题大部分情况下是非线性的。

–end–

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值