openbot之自然语言解析器

行程才是目的,顿悟在每日的实践中 --《UNIX设编程艺术》

openbot

openbot是我的业余项目,对NLP和AI的兴趣由来已久,想通过造轮子的方式来学习

我是api.ai的忠实用户,也使用wit.ai(给这个项目的client提交过源码)。国内的yige.ai也很棒。openbot的web/sdk接口模仿着它们设计

openbot最初作为deepThought的衍生项目,相关思路可以参考:deepThought#衍生计划

后来觉得我的兴趣更多在架构,我日常接触的更多的也是架构,想把它变为一个通用的框架:这个框架提供通用的webapp,对外提供restful api,也提供各个语言的sdk以便开发者集成,目前完成了python sdk,nodejs的正在编写

近期在看《Unix编程艺术》,13.4部分在谈论emacs的设计哲学,作者把emacs视为一个框架,作者说到:

当编制一个框架时,牢记分离原则:框架是机制,尽可能少地包含策略...尽可能多地将行为分解到使用框架的模块中去

我之前的架构简图如下

openbot致力于将webapp和sdk做得开箱可用,而nlu可插拔,不限制实现策略,只要满足相关约定,即可插入到框架中对最终用户提供nlp服务,在结构图中,我以chatterbot作为bot server为例

openbot项目近期没有开源,朋友的创业公司打算用它,如果之后合适,我会将其开源。

语法解析

轮子哥@vczh有句话在技术圈很有名

程序员的三大浪漫是编译原理,图形学和操作系统

编译原理确实一个有趣的话题,仅其中涉及的词法分析和语法分析就已十分迷人

相较于解析编程语言(python程序员的入门资料可以参考字符串令牌解析),我对解析自然语言的兴趣更大些,一方面来自对人工智能的兴趣,另一方面来自一直关注的分析哲学流派,他们把大多哲学问题处理为语言问题,分析哲学阵营的路德维希.维特根斯坦有句名言

语言的极限便是世界的极限 --《逻辑哲学论》 5.6

今天的nlp领域同大多涉及智能的领域一样,基于统计/大数据。

parsetron

缘起

我在嘿 Siri 关灯里提到我需要一个自然语言解析器,我准备使用yige.ai来替代Siri,以实现跨平台使用。我又想把它运行在本地

《嘿 Siri 关灯》是典型的nlp+硬件控制的案例(你可能想到扎尔伯格的Jarvis),在我的场景中(使用自然语言驱动硬件),自然语言处理模块并不需要很强大,对话的模式也很简单,多是嘿siri,把灯打开/把空调打开/把风扇打开

Kitt.ai开放的https://github.com/Kitt-AI/parsetron是个不错的选择,小巧而实用,可以一言不合就修改源码。

关于

parsetron是个非常有趣的项目,来自Kitt.ai

parsetron将自己定位在一个足够小的领域(诸如我手头的硬件控制),这样需要处理的语言模式比较单一,绕开了nlp领域会遇到的大多复杂问题(许多问题在学术圈也依旧困难,遑论工程)

parsetron是个典型的通过降低选择复杂度,让项目变得健壮而实用的案例,按照《UNIX编程艺术》的说法:通过选择合适的目标能有效降低复杂度

人工智能在一个细分领域可能做得很好,可如果我们一开始提高人们的期望,把它定位在通用型AI,往往容易给人"人工智障"的感觉,目前好用的bot多是找了一个具体的小场景,这也符合UNIX哲学里的:

Do one thing and do well

本文关注如何使用parsetron,之后有时间再分析原理源码

入门

安装很简单pip install parsetron,目前只支持python2,parsetron没有任何外部依赖

文档里的demo是非常好的入门案例

from parsetron import Set, Regex, Optional, OneOrMore, Grammar, RobustParser

class LightGrammar(Grammar):

    action = Set(['change', 'flash', 'set', 'blink'])
    light = Set(['top', 'middle', 'bottom'])
    color = Regex(r'(red|yellow|blue|orange|purple|...)')
    times = Set(['once', 'twice', 'three times']) | Regex(r'\d+ times')
    one_parse = action + light + Optional(times) + color
    GOAL = OneOrMore(one_parse)

    @staticmethod
    def test():
        parser = RobustParser((LightGrammar()))
        sents = [
            "set my top light to red",
            "set my top light to red and change middle light to yellow",
            "set my top light to red and change middle light to yellow and flash bottom light twice in blue"
        ]
        for sent in sents:
            tree, result = parser.parse(sent)
            assert result.one_parse[0].color == 'red'

            print '"%s"' % sent
            print "parse tree:"
            print tree
            print "parse result:"
            print result
            print

运行LightGrammar.test()得到:

"set my top light to red"
parse tree:
(GOAL
  (one_parse
    (action "set")
    (light "top")
    (color "red")
  )
)

parse result:
{
  "one_parse": [
    {
      "one_parse": [
        "set",
        "top",
        "red"
      ],
      "light_span_": [
        2,
        3
      ],
      "action_span_": [
        0,
        1
      ],
      "light": "top",
      "color_span_": [
        5,
        6
      ],
      "color": "red",
      "action": "set",
      "one_parse_span_": [
        0,
        6
      ]
    }
  ],
  "GOAL": [
    [
      "set",
      "top",
      "red"
    ]
  ]
}

"set my top light to red and change middle light to yellow"
parse tree:
(GOAL
  (one_parse
    (action "set")
    (light "top")
    (color "red")
  )
  (one_parse
    (action "change")
    (light "middle")
    (color "yellow")
  )
)

parse result:
{
  "one_parse": [
    {
      "one_parse": [
        "set",
        "top",
        "red"
      ],
      "light_span_": [
        2,
        3
      ],
      "action_span_": [
        0,
        1
      ],
      "light": "top",
      "color_span_": [
        5,
        6
      ],
      "color": "red",
      "action": "set",
      "one_parse_span_": [
        0,
        6
      ]
    },
    {
      "one_parse": [
        "change",
        "middle",
        "yellow"
      ],
      "light_span_": [
        8,
        9
      ],
      "action_span_": [
        7,
        8
      ],
      "light": "middle",
      "color_span_": [
        11,
        12
      ],
      "color": "yellow",
      "action": "change",
      "one_parse_span_": [
        7,
        12
      ]
    }
  ],
  "GOAL": [
    [
      "set",
      "top",
      "red"
    ],
    [
      "change",
      "middle",
      "yellow"
    ]
  ]
}

"set my top light to red and change middle light to yellow and flash bottom light twice in blue"
parse tree:
(GOAL
  (one_parse
    (action "set")
    (light "top")
    (color "red")
  )
  (one_parse
    (action "change")
    (light "middle")
    (color "yellow")
  )
  (one_parse
    (action "flash")
    (light "bottom")
    (Optional(times)
      (times
        (Set(three times|twice|once) "twice")
      )
    )
    (color "blue")
  )
)

parse result:
{
  "one_parse": [
    {
      "one_parse": [
        "set",
        "top",
        "red"
      ],
      "light_span_": [
        2,
        3
      ],
      "action_span_": [
        0,
        1
      ],
      "light": "top",
      "color_span_": [
        5,
        6
      ],
      "color": "red",
      "action": "set",
      "one_parse_span_": [
        0,
        6
      ]
    },
    {
      "one_parse": [
        "change",
        "middle",
        "yellow"
      ],
      "light_span_": [
        8,
        9
      ],
      "action_span_": [
        7,
        8
      ],
      "light": "middle",
      "color_span_": [
        11,
        12
      ],
      "color": "yellow",
      "action": "change",
      "one_parse_span_": [
        7,
        12
      ]
    },
    {
      "one_parse": [
        "flash",
        "bottom",
        "twice",
        "blue"
      ],
      "color_span_": [
        18,
        19
      ],
      "light_span_": [
        14,
        15
      ],
      "action_span_": [
        13,
        14
      ],
      "light": "bottom",
      "Optional(times)": "twice",
      "times": "twice",
      "Optional(times)_span_": [
        16,
        17
      ],
      "times_span_": [
        16,
        17
      ],
      "Set(three times|twice|once)": "twice",
      "action": "flash",
      "Set(three times|twice|once)_span_": [
        16,
        17
      ],
      "one_parse_span_": [
        13,
        19
      ],
      "color": "blue"
    }
  ],
  "GOAL": [
    [
      "set",
      "top",
      "red"
    ],
    [
      "change",
      "middle",
      "yellow"
    ],
    [
      "flash",
      "bottom",
      "twice",
      "blue"
    ]
  ]
}

非常漂亮!

更完整的例子可以参考 Complex Example

使用场景

It is mainly used to convert natural language command to executable API calls (e.g., “set my bedroom light to red” –> set_light('bedroom', [255, 0, 0]))

parsetron的典型使用场景是将自然语言转变为api调用,实际是将非结构话的自然语言,解析为结构化的数据,进而调用api。

作者给出了一些典型使用场景:

诸如使用自然语言控制你的智能灯泡:

  • give me something romantic
  • my living room light is too dark
  • change bedroom light to sky blue
  • blink living room light twice in red color

以及控制你的微波炉:

  • defrost this chicken please, the weight is 4 pounds
  • heat up the pizza for 2 minutes 20 seconds
  • warm up the milk for 1 minute

作者给出了这类需求的通用解决方案:从自然语言命令中提取关键信息,以帮助开发人员调用API来控制智能设备。由此完成了自然语言指令到传统程序的映射

作者进一步分析说传统的方法是编写一系列规则,如正则表达式,这些规则难以维护和扩展。而采用编译原理中的词法/语法解析。不但学习曲线很高,且解析器输出通常是树结构,对我们的任务没有直接的帮助

我在deepThought的目标里写过

构造通用的解析工具,将自然语言解析为结构化信息

这点与parsetron不谋而合,parsetron对我而言是作为bot server的理想实现,用于解析硬件指令

其实目前Siri也具有这个功能(需要hack,而且比较繁琐),我在《嘿 Siri 关灯》主要就用到这个功能。

问题

parsetron暂不支持中文,项目的todo中有支持unicode的计划,不过项目一年来似乎没更新了,我fork了这个项目,近期试着为它增加中文支持

Pyparsing

Parsetron的文档里写道:

Parsetron is inspired by pyparsing.

我们顺路了解一下pyparsing,顾名思义,Pyparsing是python实现的解析器。

入门

简单做个入门了解。以解析"Hello, World!"为例

from pyparsing import Word, alphas
greet = Word( alphas ) + "," + Word( alphas ) + "!" # <-- grammar defined here
hello = "Hello, World!"
print (hello, "->", greet.parseString( hello ))

输出为:Hello, World! -> ['Hello', ',', 'World', '!']

上边的greet可以解析任何具有以下模式的句子: <salutation>, <addressee>!

更多案例和使用方法参考:Examples

支持中文

值得一提的是pyparsing支持python3和unicode,这对我们处理中文会有大帮助。我们来做个试验:

# python3
from pyparsing import Word, alphas,CharsNotIn
greet = CharsNotIn( ',!' ) + "," + CharsNotIn( ',!' ) + "!" # <-- grammar defined here
hello = "你好, 世界!"
print (hello, "->", greet.parseString( hello ))

输出为 : 好, 世界! -> ['你好', ',', ' 世界', '!']

这个解决方案可行,但比较丑陋,如何在pyparsing使用unicode,可以自行google




Fork me on GitHub