首页 > 热点 > > >正文

2023-02-22 13:13:41 来源:   腾讯云

环球实时:[ES三周年]使用ES Suggester对ASR语音识别的地址进行纠错

项目需求/痛点

作者所在的团队是世界某500强公司AI中心的语音团队,ASR业务面向整个集团。

在ASR识别中,公司单名,公司地址和居住地址的识别率一直不理想,业务BU多次反馈要求提高,以便于客户语音陈述完地址后,能尽量少的修改所述的地址,提高用户体验。


(相关资料图)

纠错方案

我们具有几亿的地址数据,除了用于模型的finetune,我们计划用此数据通过搜索的方式对ASR的识别结果进行纠错。

ASR语音识别场景的特征是,模型容易识别出同音字和发音相似的字,因此,搜索纠错的主要策略基于拼音相似的原理实现。

对于纠错而言,误纠是无法避免的,无法保证搜索的TOP1就一定是正确结果。因此,没有采用在ASR模型输出之后,对其进行搜索TOP1结果的替换,因为,不仅会额外增加识别的时延(N亿级的复杂模糊查询会带来一定的时延),而且会导致模型的原输出的丢失。

由于APP在用户陈述完公司单名或地址后,会返回TOP5结果。因此,方案最后为,业务BU在收到ASR的识别结果后,单独调用搜索API,得到TOP5的公司单名或地址,并返回给用户选择。

搜索服务将会综合使用elasticsearcg的建议(suggester)和搜索(query)2大功能提供最相似的TOP5。

考虑篇幅,这里重点陈述phrase suggester纠错策略,不仅因为是目前效果最好的策略,而且网上phrase suggester的深度文章很少,这里要补齐这个技术短板。

基于phrase suggester的地址纠错设计

地址数据的特征是,一般具有省市区街道路门牌号等级别,这里不采用传统的将每个级别下的内容单独识别,而是采用一种更通用的不区分级别,而是基于ngram的思想来实现。

这种实现不依赖地址领域知识,纠错服务会具有更广的使用场景和更强的泛化性。

外置分词器

地址数据比较特别,传统的分词器(非深度学习)效果并不理想。

横向对比主流的中文分词器效果,发现基于深度学习的hanlp V2的electra模型具有较好的效果。

但是ES的插件无法支持python及深度模型,因此,只能通过外置服务的提供更好效果的分词。

原输入文本经过外置分词器后,通过空格进行拼接,ES索引的analyzer采用

地址类数据通过electra模型进行细粒度分词,将分词结果传入基于msra数据集的electra ner模型,只保留location和organization的ner,即得到地址的基本分词。

什么是phrase suggester?

elasticsearch的搜索query,大家比较熟悉,但是建议suggester就相对陌生,建议大家可以先了解suggester的知识。

ES官方Suggester介绍

建议和搜索的区别

phrase suggester是基于term Suggester,加入了ngram思想的Suggester设计。

要理解phrase Suggester,必须先理解term Suggester和ngram。

官方文档内容不在此累述,本文重点通过形象的数据让大家理解phrase suggester。

为什么选择phrase suggester而不是更简单的term Suggester?

下面采用真实例子进行陈述,空格关系代表分词关系。

Asr文本:深圳市 福田区 香蜜湖北路 西园

期望纠错:深圳市 福田区 香蜜湖北路 熙园

Term Suggester效果

ES输入

GET address-company-广东省-深圳市/_search{  "suggest": {    "term_suggestion": {      "text": "深圳市 福田区 香蜜湖北路 西园",      "term": {        "field": "ner",        "suggest_mode": "popular",        "size": 20,        "min_word_length": 2,        "prefix_length": 0      }    }  }  }

ES返回

"suggest" : {    "term_suggestion" : [      {        "text" : "深圳市",        "offset" : 0,        "length" : 3,        "options" : [ ]      },      {        "text" : "福田区",        "offset" : 4,        "length" : 3,        "options" : [ ]      },      {        "text" : "香蜜湖北路",        "offset" : 8,        "length" : 5,        "options" : [          {            "text" : "香蜜湖路",            "score" : 0.75,            "freq" : 1228          },          {            "text" : "香蜜湖街道",            "score" : 0.6,            "freq" : 37053          },          {            "text" : "香蜜湖大厦",            "score" : 0.6,            "freq" : 84          },          {            "text" : "香蜜湖1号",            "score" : 0.6,            "freq" : 39          },          {            "text" : "香蜜湖水榭",            "score" : 0.6,            "freq" : 33          },          {            "text" : "香蜜湖酒店",            "score" : 0.6,            "freq" : 14          },          {            "text" : "香蜜湖体育",            "score" : 0.6,            "freq" : 12          },          {            "text" : "香蜜湖公寓",            "score" : 0.6,            "freq" : 9          },          {            "text" : "香蜜湖新村",            "score" : 0.6,            "freq" : 8          },          {            "text" : "香蜜湖一号",            "score" : 0.6,            "freq" : 6          },          {            "text" : "香梅北路",            "score" : 0.5,            "freq" : 292          },          {            "text" : "香密湖路",            "score" : 0.5,            "freq" : 11          }        ]      },      {        "text" : "西园",        "offset" : 14,        "length" : 2,        "options" : [          {            "text" : "家园",            "score" : 0.5,            "freq" : 1339          },          {            "text" : "福园",            "score" : 0.5,            "freq" : 1033          },          {            "text" : "园西",            "score" : 0.5,            "freq" : 890          },          {            "text" : "佳园",            "score" : 0.5,            "freq" : 739          },          {            "text" : "名园",            "score" : 0.5,            "freq" : 620          },          {            "text" : "南园",            "score" : 0.5,            "freq" : 552          },          {            "text" : "松园",            "score" : 0.5,            "freq" : 513          },          {            "text" : "围园",            "score" : 0.5,            "freq" : 445          },          {            "text" : "可园",            "score" : 0.5,            "freq" : 407          },          {            "text" : "桃园",            "score" : 0.5,            "freq" : 404          },          {            "text" : "竹园",            "score" : 0.5,            "freq" : 310          },          {            "text" : "兰园",            "score" : 0.5,            "freq" : 277          },          {            "text" : "桂园",            "score" : 0.5,            "freq" : 275          },          {            "text" : "公园",            "score" : 0.5,            "freq" : 260          },          {            "text" : "北园",            "score" : 0.5,            "freq" : 259          },          {            "text" : "乐园",            "score" : 0.5,            "freq" : 201          },          {            "text" : "丽园",            "score" : 0.5,            "freq" : 182          },          {            "text" : "智园",            "score" : 0.5,            "freq" : 172          },          {            "text" : "嘉园",            "score" : 0.5,            "freq" : 166          },          {            "text" : "庄园",            "score" : 0.5,            "freq" : 140          }        ]      }    ]  }

可以看到

可园的候选项,返回的size设置为20,都没有包含正确答案。(实际 熙园 的排序在70多位,因为词频低)。

phrase Suggester的效果

GET address-company-广东省-深圳市/_search{  "suggest": {    "ner_pinyin_suggest": {      "text": "深圳市 福田区 香蜜湖北路 西园",      "phrase":{        "field": "ner.trigram",        "gram_size": 3,        "direct_generator": [ {          "field": "ner.trigram",          "suggest_mode": "always",          "min_word_length": 2,          "prefix_length": 0,          "size": 100        } ],        "highlight": {          "pre_tag": "",          "post_tag": ""        },        "shard_size": 2000,        "max_errors": 2,        "size": 10      }    }  }  }

ES返回

"suggest" : {    "ner_pinyin_suggest" : [      {        "text" : "深圳市 福田区 香蜜湖北路 西园",        "offset" : 0,        "length" : 16,        "options" : [          {            "text" : "深圳市 福田区 香蜜湖街道 熙园",            "highlighted" : "深圳市 福田区 香蜜湖街道 熙园",            "score" : 0.07858969          },          {            "text" : "深圳市 福田区 香蜜湖街道 嘉园",            "highlighted" : "深圳市 福田区 香蜜湖街道 嘉园",            "score" : 0.07858969          },          {            "text" : "深圳市 福田区 香蜜湖街道 竹园",            "highlighted" : "深圳市 福田区 香蜜湖街道 竹园",            "score" : 0.07858969          },          {            "text" : "深圳市 福田区 香蜜湖街道 西路",            "highlighted" : "深圳市 福田区 香蜜湖街道 西路",            "score" : 0.07858969          },          {            "text" : "深圳市 福田区 香蜜湖路 熙园",            "highlighted" : "深圳市 福田区 香蜜湖路 熙园",            "score" : 0.04161656          },          {            "text" : "深圳市 福田区 香密湖路 西南",            "highlighted" : "深圳市 福田区 香密湖路 西南",            "score" : 0.023261044          },          {            "text" : "深圳市 福田区 香蜜湖路 西南",            "highlighted" : "深圳市 福田区 香蜜湖路 西南",            "score" : 0.019111233          },          {            "text" : "深圳市 福田区 香蜜湖路 西北",            "highlighted" : "深圳市 福田区 香蜜湖路 西北",            "score" : 0.017365677          },          {            "text" : "深圳市 福田区 香密湖路 西北",            "highlighted" : "深圳市 福田区 香密湖路 西北",            "score" : 0.017214466          },          {            "text" : "深圳市 福田区 香蜜湖北路 西乡",            "highlighted" : "深圳市 福田区 香蜜湖北路 西乡",            "score" : 0.002201289          }        ]      }    ]  }

可以看到

phrase suggester的第一条就是:深圳市 福田区 香蜜湖街道 熙园

虽然 熙园 是低频词,但是考虑了ngram之后,得分却是最高的。

详细解密Phrase Suggester

GET address-company-广东省-深圳市

可以查看索引的设计如下:

"mappings" : {      "properties" : {        "address" : {          "type" : "text"        },        "area" : {          "type" : "keyword"        },        "city" : {          "type" : "keyword"        },        "id" : {          "type" : "keyword"        },        "ner" : {          "type" : "text",          "fields" : {            "trigram" : {              "type" : "text",              "analyzer" : "trigram",              "search_analyzer" : "whitespace"            }          },          "analyzer" : "whitespace"        },        "ner_pinyin" : {          "type" : "text",          "fields" : {            "trigram" : {              "type" : "text",              "analyzer" : "trigram",              "search_analyzer" : "whitespace"            }          },          "analyzer" : "whitespace"        },        "province" : {          "type" : "keyword"        }      }    },

可以看到ner 和 ner_pinyin除了主field(text,whitespace)外,还有 trigram的field,这个trigram是自定义的类型,专门服务phrase suggester。

trigram这个field里有一个自定义的trigram analyzer。

索引信息里可以查看到这个analyzer的定义:

"analyzer" : {            "trigram" : {              "filter" : [                "lowercase",                "shingle"              ],              "type" : "custom",              "tokenizer" : "whitespace"            },

这个analyzer除了使用空格分词,关键使用了filter。

filter 并不是过滤器,更像流式编程中的map函数,输入的token流经常变换得到新的token流 .

tokenfilters

lowercase非常好理解,就是全部变换为小写;

shingle是一个自定义的filter

什么是shingle filter?

shingle就是token ngram(词级别的ngram)的意思,这个词来自ES的底层lucene。

自定义的shingle filter如下:

shingle filter的定义

"analysis" : {          "filter" : {            "shingle" : {              "max_shingle_size" : "3",              "min_shingle_size" : "2",              "output_unigrams": false,              "type" : "shingle"            }          },

我们可以通过如下方法,形象的查看和测试shingle filter的行为

如何查看shingle的行为?

GET /_analyze{  "tokenizer": "whitespace",  "filter": [    {      "type": "shingle",      "min_shingle_size": 2,      "max_shingle_size": 3,      "output_unigrams": false    }  ],  "text": "深圳市 福田区 香蜜湖北路 西园"}

输出如下:

{  "tokens" : [    {      "token" : "深圳市 福田区",      "start_offset" : 0,      "end_offset" : 7,      "type" : "shingle",      "position" : 0    },    {      "token" : "深圳市 福田区 香蜜湖北路",      "start_offset" : 0,      "end_offset" : 13,      "type" : "shingle",      "position" : 0,      "positionLength" : 2    },    {      "token" : "福田区 香蜜湖北路",      "start_offset" : 4,      "end_offset" : 13,      "type" : "shingle",      "position" : 1    },    {      "token" : "福田区 香蜜湖北路 西园",      "start_offset" : 4,      "end_offset" : 16,      "type" : "shingle",      "position" : 1,      "positionLength" : 2    },    {      "token" : "香蜜湖北路 西园",      "start_offset" : 8,      "end_offset" : 16,      "type" : "shingle",      "position" : 2    }  ]}

如何理解shingle的参数定义

"min_shingle_size": 2,
"max_shingle_size": 3,
"output_unigrams": false

根据前面shingle的实例输出,可以发现,这是一个3gram的输出(但不输出单词条,因为output_unigrams为false)

如何提升shingle性能?

shingle是动态生成的,如果需要更高性能,则需要提前预计算,这时可以采用index-phrases。

简单的说,就是将ngram的输出在建索引时,就写在另一个field上,用空间换时间。

https://www.elastic.co/guide/en/elasticsearch/reference/current/index-phrases.html

shingle和ngram tokenizer的区别?

shingle:token ngram ,是一个基于词级别的ngram https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-shingle-tokenfilter.html

ngram tokenizer: char ngram,是一个基于字符级别的ngram https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html

举例:

GET /_analyze{  "tokenizer": {    "type": "ngram",    "min_gram": 3,    "max_gram": 3  },  "text": "深圳市 福田区 香蜜湖北路 西园"}

返回

{  "tokens" : [    {      "token" : "深圳市",      "start_offset" : 0,      "end_offset" : 3,      "type" : "word",      "position" : 0    },    {      "token" : "圳市 ",      "start_offset" : 1,      "end_offset" : 4,      "type" : "word",      "position" : 1    },    {      "token" : "市 福",      "start_offset" : 2,      "end_offset" : 5,      "type" : "word",      "position" : 2    },    {      "token" : " 福田",      "start_offset" : 3,      "end_offset" : 6,      "type" : "word",      "position" : 3    },    {      "token" : "福田区",      "start_offset" : 4,      "end_offset" : 7,      "type" : "word",      "position" : 4    },    {      "token" : "田区 ",      "start_offset" : 5,      "end_offset" : 8,      "type" : "word",      "position" : 5    },    {      "token" : "区 香",      "start_offset" : 6,      "end_offset" : 9,      "type" : "word",      "position" : 6    },    {      "token" : " 香蜜",      "start_offset" : 7,      "end_offset" : 10,      "type" : "word",      "position" : 7    },    {      "token" : "香蜜湖",      "start_offset" : 8,      "end_offset" : 11,      "type" : "word",      "position" : 8    },    {      "token" : "蜜湖北",      "start_offset" : 9,      "end_offset" : 12,      "type" : "word",      "position" : 9    },    {      "token" : "湖北路",      "start_offset" : 10,      "end_offset" : 13,      "type" : "word",      "position" : 10    },    {      "token" : "北路 ",      "start_offset" : 11,      "end_offset" : 14,      "type" : "word",      "position" : 11    },    {      "token" : "路 西",      "start_offset" : 12,      "end_offset" : 15,      "type" : "word",      "position" : 12    },    {      "token" : " 西园",      "start_offset" : 13,      "end_offset" : 16,      "type" : "word",      "position" : 13    }  ]}

很明显,ngram的返回并不是我们预期需要的。

什么是direct generator?

phrase suggester是基于term suggester的ngram,那么direct generator就类似term suggester,生成候选集,然后ngram基于这些基础数据,进行计算。

所以direct generator的配置,要参考term suggester.

但有几个配置不一样。

"direct_generator": [ {          "field": "ner",          "suggest_mode": "always",          "min_word_length": 2,          "prefix_length": 0,          "size": 100        } ]

suggest_mode 是选popular 还是 always ?

term suggester建议为popular,通过更高词频来判断纠错

但是phrase suggester是基于ngram,有上下文关系,不需要通过不严谨的词频来设计,因此,应该为always。

field 是否要加.trigram ?

网上的教程会有加.trigram的用法,那到底是用 ner 还是 ner.trigram ?

我们将ner.trigram 应用在term suggester中,看看其行为

GET address-company-广东省-深圳市/_search{  "suggest": {    "term_suggestion": {      "text": "深圳市 福田区 香蜜湖北路 西园",      "term": {        "field": "ner.trigram",        "suggest_mode": "always",        "size": 80,        "min_word_length": 2,        "prefix_length": 0      }    }  }  }

输出,和ner的差不多,但是,增加了一些:

香蜜湖 1,香蜜湖 店,香蜜湖 北环路 等等的输出。

很明显。ner.trigram的行为是,不仅仅用单个词条作为纠错,而是可以将后续的2,3个词,一起作为整体进行纠错。

如果建索引和搜索时,采用的是相同粒度的分词,则采用ner即可。

如果建索引采用细粒度分词,搜索的时候,采用粗粒度分词,则采用ner.trigram。

Phrase Suggester的参数如何设置?

gram_size:3

深圳市 福田区 香蜜湖北路 西园

如果不设置,第一条纠错建议为:深圳市 福田区 香蜜湖街道 西乡, 也就是unigram的纠错能力。(西乡是西园的最高频单词条纠错建议)—— 很奇怪,官方说会从filed的filter中推导这个值,实际不会推导,因此手动设置。

max_errors:2

表示最多纠错的词条数量(注意,不是一个词条内的最大纠错字数)

举例:

深圳市 福田区 香蜜湖北路 西园

因为最大错误数量是2,所以可以纠正为:深圳市 福田区 香蜜湖街道 熙园

如果设置为1,则只能纠正为:深圳市 福田区 香蜜湖北路 西乡

shard_size:100

每个shard返回的最大数量的建议词条,默认是5

如果采用默认值,会发现, 无法将 西园 纠错为 熙园。 因为,熙园的词频低,shard只返回了Top 5的词频词条,熙园不在phrase suggester的候选数据里,因此无法纠正对。

使用collate过滤掉不合理的suggestion

在phrase suggestion的建议中,存在一些不合理的,如:深圳市 福田区 香蜜湖北路 西乡。(因为 福田区 根本没有西乡,西乡在 宝安区)

这是一个unigram的纠错(即使shingle设置不输出unigram,phrase suggester还是会有unigram的纠错,不知道为什么)

可以采用collate参数,如下是示例:(具体使用参见:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html#phrase-suggester)

match_phrase是要求全部精确匹配,且词的顺序也要符合的严格match模式。

prune默认为false,表示不符合query条件的,不输出。

这里设置为true,表示都会输出,但是输出增加了collate_match的标记,query匹配的为true,不匹配的为false,方便调试和做后续的优先级设计等。

(之所以保留不匹配的原因如下:

用户输入:AAA BXB CCC DDD

语料有:AAA BBB CCC 和 AAA BBB DDD

根据BBB CCC,ES将BXB CCC 修正为 BBB CCC,最终输出为:AAA BBB CCC DDD

根据match_phrase的全部匹配要求,语料里没有一条可以和它匹配。)

"collate": {          "query": {             "source" : {              "match_phrase": {                "{{field_name}}" : "{{suggestion}}"               }            }          },          "params": {"field_name" : "ner"},           "prune": true         }

Phrase Suggester的打分机制

smooth 模型,默认采用 stupid backoff 。

stupid backoff 比较简单,匹配上3gram是1,匹配不上,如果匹配上2gram,权重乘以0.4,如果还匹配不上,匹配unigram,权重在2gram的基础上,再乘以0.4

详细可以了解google的论文 https://aclanthology.org/D07-1090.pdf

Phrase Suggester输出结果的重排序

基于拼音的编辑距离排序

根据phrase suggester的建议,存在高频词排序靠前的问题。

输入:深圳市 龙岗区 龙岗街道 宝平路 五号

期望:深圳市 龙岗区 龙岗街道 宝坪路 五号

phrase suggester 纠错为:

深圳市 龙岗区 龙岗街道 宝荷路 五号

而ASR地址纠错的特点是音近,因此,需要加入一个根据拼音编辑距离排序的功能。

重排序后,可以得到期望的答案。

纠错效果

在不同测试数据集和不同ASR模型下,正确地址出现在TOP5中,仅phrase suggester单纠错策略,就能达到十几个点(部分甚至超过20%)的提升。在模型要提升1个点就比较难的情况,通过Elasticsearch的phrase suggester纠错引入,做到了更准的ASR识别效果,提升了用户体验。

最后

Phrase Suggester是Elasticsearch里相对比较难的部分,参数较多,但相关参考实践却很少,希望本案例实践的分享,可以补齐ES这个领域的知识短板。

标签: JavaScript

热门话题
精彩推荐
今日推荐
花木