51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 222|回复: 0
打印 上一主题 下一主题

[原创] 从0到1搭建机器人:深度学习与测试驱动开发的实战经验

[复制链接]
  • TA的每日心情
    无聊
    3 天前
  • 签到天数: 1016 天

    连续签到: 3 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2024-7-12 09:55:52 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    前言
    随着一众国内外大模型免费开放API,零成本构建一些性能不是那么强的大模型应用成为了可能。作为Digital IDE的作者,锦恢在完成学业的同时也需要负责维护一些自己的开源项目。
    在维护一个产品的时候,为了获得更好的用户反馈,我们往往会建立一个 QQ 群来和用户直接互动。
    不过随着时间的推移,群内会有很多的问题变得重复,或者很多问题都可以从我们提供的官方文档中找到答案。当然,我们知道,大部分的人都是懒得去翻官方文档的。

    项目目前已经开源,欢迎各位点个 star:Lagrange.RagBot
    这篇文章,我就分享一下我在开发 QA 机器人中的心得,以及其中最核心的模块“意图识别”模块是如何基于深度学习和单元测试的进行迭代的。

    项目架构 & 流程讲解
    因为这个项目是一个将机器人接入 QQ 的项目,涉及到比较多的环节,项目整体有一定的复杂性。本着先感性再理性的认知原则,所以我打算先讲讲系统的整体的比较粗略的架构,先让我的观众大概知道 QA 机器人的整理架构和大致的一个原理。

    项目的整体架构如下图所示:
    其中最上层的 Lagrange.Core 负责整个 QQ 的鉴权和底层通信,包括登录注册,和 QQ 的服务器直接发起安全连接和信息发送接受。此处感谢 Lagrange.Core 项目组 的无私奉献,也希望大家可以去点个 star。
    中间层的 Lagrange.onebot 则是整个 QQ 的后端框架,负责编写对于不同的群聊和私聊的逻辑函数,这个项目完全由我开发完成,官方文档在这里 Lagrange.onebot 文档
    在逻辑层面,当一个用户在在群内发言后,onebot 层在将消息转换为更加规整的文本信息后,会交给意图识别模块,也就是 Intent Recogition 进行意图分类,该模块会将用户输入的一段话分类成如下几个类别之一:

    • bug 询问
    • usage 询问
    • 表达情感
    • others

    对于99%的群内的聊天内容,都属于 others,当意图识别模块将 others 识别返回给 onebot 后,onebot 就会自动拒绝回答当前的消息。这么做会让我们的 QA 机器人显得很乖,不会胡乱插嘴。这在客服机器人和 QA 机器人的开发中是非常重要的。实际上,因为深度学习上数据不足造成的 out of distribution 问题,我在系统上还加上了我今年提出的一个深度学习可信计算的算法,阁下暂时不需要了解它的原理,理解它需要阁下去复习高等数学和多元统计分析的知识。阁下只需要知道,这个模型能返回当前意图分类的不确定性。下图是我对 Tiphereth 进行的一个意图识别测试。

    可以看到,它很好地完成了意图的识别,并返回了对应的不确定度,不确定度如果太高了(>0.3),那么,系统也会拒绝回答用户的问题。这样就能很好地解决 out of distribution 的问题。

    如果意图识别模块返回的是内容是 bug 询问,那么 onebot 层就会去询问大模型,从而获得对应的回答结果。不过这么做太简单了,对于我们自己的项目或者公司内部的技术文档,大模型肯定不知道其中的技术。因此,我们需要引入知识库来让大模型“更了解我们的内部资料”。具体做法为,将我们的内部资料切成一个个小块扔进向量数据库中,等到用户询问相关问题时,使用用户的询问语句去向量数据库的语义空间中找出 topk 接近的小块,将小块作为 prompt 再去询问 大模型,从而得到具备内部知识库的回答。而且最棒的是,我们的程序在询问大模型时是可以获取到从向量数据库中返回的切块的来源的(比如来源于某个网址),这样我们的 QA 机器人在回答时就能像 new bing 一样返回答案的参考链接了,这对于需要专业信息咨询的用户而言,是非常重要的:


    我们比较关心的是 usage, bug, expression 和 others 这几个量,那么我根据上图右上角的标签,去找这四个类别对应的颜色的散点。我们需要通过这四个类别是不是足够好分,来判断我们需要构造多么复杂的模型,越是复杂的模型,能做出的决策边界可以越复杂,最简单的单层线性模型只能用切蛋糕的方式,通过给上面的图“划几刀”的方式来直接划定楚汉两界。比如,在线左边的就是 usage,在右边的就是 bug。
    我们的例子中,上面的图中四个类别对应的散点并不算非常难以区分,结合业界对于意图模型的通用做法,我最终采取了单层 MLP + ELU 激活函数的方式进行网络构造。

    假设我们通过预训练的得到了上述的模型(详细请参考 Lagrange.RagBot 的 ./notebook/experiment.ipynb 文件中的实验结果),loss 损失函数下降得非常舒服:
    说明训练收敛了。
    我们可以写个程序和对应的 test suite 来测试一下准确率,这其实就相当于深度学习模型在测试集上的准确率:
    1. <font face="微软雅黑" size="3">test_suite = [
    2.     # input 代表输入值,expect 代表模型输出的目标值
    3.     { 'input': '如何使用 digital ide 这个插件?', 'expect': 'usage' },
    4.     # 此处的 expect 使用逗号进行分隔,代表只要符合其中的某一个就行。这是因为很多时候,问题的意图并不明显,很可能某几个值都满足要求
    5.     { 'input': '我今天打开 vscode,发现 自动补全失效了,我是哪里没有配置好吗?', 'expect': 'usage,bug' },
    6.     { 'input': 'path top.v is not a hdlFile 请问报这个错误大概是啥原因啊', 'expect': 'usage,bug' },
    7.     { 'input': '我同学在学习强国看到小麦收割了,然后就买相应的股就赚了', 'expect': 'others' },
    8.     { 'input': '我平时写代码就喜欢喝茶', 'expect': 'others' },
    9.     { 'input': '请问报这个错误大概是啥原因啊', 'expect': 'usage,bug' },
    10.     { 'input': '感觉现在啥都在往AI靠', 'expect': 'others' },
    11.     { 'input': '别人设置的肯定有点不合适自己的', 'expect': 'others' },
    12.     { 'input': '在企业里面最大的问题是碰见傻逼怎么办?', 'expect': 'others' },
    13.     { 'input': '几乎完全不喝牛奶2333', 'expect': 'others' },
    14.     { 'input': 'command not found: python', 'expect': 'usage,bug,others' },
    15.     { 'input': '兄弟们有没有C语言绘图库推荐', 'expect': 'usage' },
    16.     { 'input': '我早上开着机去打论文 回来发现我电脑切换到Linux了', 'expect': 'usage,bug,others' },
    17.     { 'input': '我在Windows下遇到的只要问题就是对于C程序,包管理和编译管理器偶尔会不认识彼此但除此之外,都很安稳(win11除外)', 'expect': 'usage,others' },
    18.     { 'input': '我的反撤回还能用', 'expect': 'others' },
    19.     { 'input': '因为这是养蛊的虚拟机,放了些国产垃圾软件,得用国产流氓之王才能镇得住他们', 'expect': 'others' },
    20.     { 'input': '你咋装了个360', 'expect': 'others' },
    21.     { 'input': '???', 'expect': 'expression' },
    22. ]
    23. for test in test_suite:
    24.     embd = model.embed_documents([test['input']])
    25.     embd = torch.FloatTensor(embd)
    26.     with torch.no_grad():
    27.         evidence, prob = enn_model(embd)

    28.     e = evidence
    29.     alpha = e + 1
    30.     S = alpha.sum(1)
    31.     b = e / S
    32.     u = out_dim / S
    33.     pre_label = prob.argmax(1)
    34.     name = engine.id2intent[pre_label[0].item()]
    35.     ok = '√' if name in test['expect'] else '×'
    36.     print(name, test['expect'], ok, u)
    37. 输出:
    38. usage usage √ tensor([0.0501])
    39. bug usage,bug √ tensor([0.0773])
    40. bug usage,bug √ tensor([0.0758])
    41. others others √ tensor([0.1678])
    42. others others √ tensor([0.0887])
    43. bug usage,bug √ tensor([0.0902])
    44. others others √ tensor([0.0453])
    45. others others √ tensor([0.0424])
    46. others others √ tensor([0.1416])
    47. others others √ tensor([0.1441])
    48. bug usage,bug,others √ tensor([0.1615])
    49. usage usage √ tensor([0.0562])
    50. others usage,bug,others √ tensor([0.0820])
    51. others usage,others √ tensor([0.0798])
    52. others others √ tensor([0.1282])
    53. others others √ tensor([0.1034])
    54. others others √ tensor([0.0967])
    55. expression expression √ tensor([0.0802])
    56. </font>
    复制代码
    可以看到,全都答对了。

    但是此时,并不能直接参与部署的,请听我娓娓道来。
    QA 机器人是需要不断迭代的
    这里需要阁下了解一个关于 QA 机器人开发的基本流程, QA 机器人的开发(其他很多深度学习应用也一样)一定是一个不断迭代的过程,因为你的无法在一开始就制作出一个能够 cover 所有问题的数据集,在 QA 机器人上线测试的过程中,用户一定会不断地提出问题,或是闲聊,这些数据是非常非常珍贵的!所以! QA 机器人能跑,能说人话了,只是第一步,最最重要的是需要设计出一套机制,能够收集用户数据,并周期性地自动使用这些数据去更新模型,从而最终趋于完美。

    如果阁下运行了我的项目,会发现在项目的根目录下,出现了一个 logs 文件夹,它里面就存储了我的系统自动保存的用户的对话和模型将该对话识别成的意图。


    在 ./notebook/clear-logs.ipynb 中,给出了一个例程,该例程会自动将 logs 下识别意图不是 others 的对话内存展示出来,你可以自己进行干预,重新审核这些非 others 的意图是否正确,如果不正确,它的意图应该是什么。然后基于这些信息,可以快速更新我们的训练集。新增的数据会自动录入到 ./config/qq.story.yml 中(这个文件我并没有开源,因为里面涉及到了用户的个人隐私),然后我们重新训练模型即可。因为模型只有一层,所以就算使用 CPU,也能在超短时间内瞬间训练完成。
    迭代后的测试
    在重新训练完成后,我们需要知道更新参数后的模型是不是还能把之前的问题答对,我将这一部分逻辑写成了单元测试。启动项目后,在项目中输入 npm run test 即可开启单元测试:
    开发者只需要根据单元测试的反馈,就能大致了解到重新训练后的模型的性能,从而做出判断,到底是继续上线,还是继续添加数据,让不满足条件的测试样例通过。
    很多做深度学习的工程师都没有做单元测试的习惯,这是非常不好的。因为实际场景下,是需要保证模型在特定例子上一定能通过,测试集的正确率并不能反应这一点。

    如此,反复迭代,就能够在最终得到一个很乖的AI啦!在我稳定运行的1个月以来,大概迭代到第二代时,意图识别模块就足够稳定了。对了,每次训练完后,你还可以用 curl 工具先将模型重新加载到服务中:

    1. <font face="微软雅黑" size="3">curl -X POST http://127.0.0.1:8081/intent/reload-embedding-mapping</font>
    复制代码
    返回:
    1. <font face="微软雅黑" size="3">{"code":200,"data":"load model from ./model/intent.enn.pth","msg":200}</font>
    复制代码
    这么做最大的好处就是不需要重启服务,然后我们可以使用单个例子来测试上线的模型:
    1. <font face="微软雅黑" size="3">curl -X POST -H "Content-Type: application/json" -d '{"query": "今天吃不吃疯狂星期四?"}' http://127.0.0.1:8081/intent/get-intent-reco
    2. gition
    3. </font>
    复制代码
    输出:
    1. <font face="微软雅黑" size="3">{"code":200,"data":{"id":6,"name":"others","uncertainty":0.2645169198513031},"msg":200}</font>
    复制代码
    可以看到,输出结果为 others,不确定度也非常高,代表当前的语句是闲聊。当然,你也可以拿更多的例子来测试。
    这样,一个稳定的意图识别模块就搞定了,至于后面如何结合向量数据库和大模型给出带有参考链接的回答,这就不属于本篇文章的内容了。感兴趣的朋友可以参考 ./bot/services/intent.ts 这个文件的内容。
    各位,玩得开心点!
    总结
    本文简单讲解了一下我的 QA 机器人的设计与开发经验,特别是关于 TDD 模式下的意图识别模块的迭代。除了意图识别,也就是文本分类,对于其他的需要基于深度学习进行的精准任务而言,这套开发流程也是具有一定的参考价值的,特别是对于深度学习同行而言,一定不要想着一个很鲁棒的算法+数据集就能一招解千愁,一个优秀的深度学习应用一定是要在测试中反复迭代升级的,毕竟,对于深度学习而言,算法 > 数据 > 算法。




    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-9-8 09:10 , Processed in 0.074006 second(s), 24 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表