Elasticsearch:如何部署 NLP:命名实体识别 (NER) 示例

431 阅读11分钟

在本文章中,我们将通过一个示例,使用命名实体识别 (NER - Name Entity Recognition) NLP 模型来定位和提取非结构化文本字段中预定义的实体类别。 使用公开可用的模型,我们将向你展示如何将该模型部署到 Elasticsearch,使用新的 _infer API在文本中查找命名实体,并在提取管道中使用 NER 模型在文档被提取到 Elasticsearch 时提取实体。

NER 模型对于使用自然语言从全文字段中提取人物(people)、**地点(places)组织(organization)**等实体很有用。

在此示例中,我们将通过 NER 模型运行《悲惨世界》一书的段落,并使用该模型从文本中提取字符和位置,并将它们之间的关系可视化。

安装

如果你还没有安装好自己的 Elasticsearch,Kibana 及 Eland,那么请阅读之前的文章 “Elasticsearch:如何部署 NLP:文本嵌入和向量搜索”。

将 NER 模型部署到 Elasticsearch

首先,我们需要选择一个可以从文本字段中提取字符名称和位置的 NER 模型。 幸运的是,我们可以在 Hugging Face 上选择一些可用的 NER 模型,并查看 Elastic 文档,我们看到一个 uncased NER model from Elastic  模型。

现在我们已经选择了要使用的 NER 模型,我们可以使用 Eland 来安装模型。 在本例中,我们将通过 docker 镜像运行 Eland 命令,但首先我们必须通过克隆 Eland GitHub 存储库来构建 docker 镜像,并在你的客户端系统上创建 Eland 的 docker 镜像。详细步骤请在文章  “Elasticsearch:如何部署 NLP:文本嵌入和向量搜索”。中进行查看,这里就不再赘述了。

我们接下来使用如下的命令来上传模型:

1.  docker run -it --rm elastic/eland \
2.      eland_import_hub_model \
3.        --url https://elastic:lOwgBZT3KowJrQWMwRWm@192.168.0.3:9200/ \
4.        --hub-model-id elastic/distilbert-base-uncased-finetuned-conll03-english \
5.        --task-type ner \
6.        --insecure \
7.        —-start 

注意:请根据自己的用户账号信息更新 --url 选项中的 Elasticsearch 信息。由于我们使用的是自签名的证书部署的,在这里,我们使用 --insecure 来规避 SSL 签名证书的检查。

由于我们在 eland import 命令末尾使用了 --start 选项,因此 Elasticsearch 会将模型部署到所有可用的机器学习节点并将模型加载到内存中。 如果我们有多个模型并且想要选择要部署的模型,我们可以使用 Kibana 的机器学习 > 模型管理用户界面来管理模型的启动和停止。

 

从上面的输出中,我们可以看出来模型上传是成功的。我们可以在 Kibana 中进行查看:

 

 

 

 

我们可以看到已经被上传的模型,并且它的状态是 started。 

测试 NER 模型

可以使用新的 _infer API 评估已部署的模型。 输入是我们要分析的字符串。 在下面的请求中, text_field 是模型期望在其中找到输入的字段名称,如模型配置中所定义。 默认情况下,如果模型是通过 Eland 上传的,则输入字段为 text_field。

在 Kibana 的开发工具控制台中尝试这个示例:

 2.  POST _ml/trained_models/elastic__distilbert-base-uncased-finetuned-conll03-english/deployment/_infer
3.  {
4.    "docs": [
5.      {
6.        "text_field": "Hi my name is Josh and I live in Berlin"
7.      }
8.    ]
9.  }

该模型发现了两个实体:人 “Josh” 和地点 “Berlin”。 predict_value 是 Annotated Text 格式的输入字符串,class_name 是预测的类,class_probability 表示预测的置信度。 start_pos 和 end_pos 是已识别实体的开始和结束字符位置。

将 NER 模型添加到推理摄取管道

_infer API 是一种有趣且简单的入门方式,但它只接受单个输入,并且检测到的实体不会存储在 Elasticsearch 中。 另一种方法是在文档通过推理处理器的摄取管道摄取时对文档执行批量推理。

你可以在 Stack Management UI中定义摄取管道或在 Kibana Console 中进行配置; 这个包含多个摄取处理器:



1.  PUT _ingest/pipeline/ner
2.  {
3.    "description": "NER pipeline",
4.    "processors": [
5.      {
6.        "inference": {
7.          "model_id": "elastic__distilbert-base-uncased-finetuned-conll03-english",
8.          "target_field": "ml.ner",
9.          "field_map": {
10.            "paragraph": "text_field"
11.          }
12.        }
13.      },
14.      {
15.        "script": {
16.          "lang": "painless",
17.          "if": "return ctx['ml']['ner'].containsKey('entities')",
18.          "source": "Map tags = new HashMap(); for (item in ctx['ml']['ner']['entities']) { if (!tags.containsKey(item.class_name)) tags[item.class_name] = new HashSet(); tags[item.class_name].add(item.entity);} ctx['tags'] = tags;"
19.        }
20.      }
21.    ],
22.    "on_failure": [
23.      {
24.        "set": {
25.          "description": "Index document to 'failed-<index>'",
26.          "field": "_index",
27.          "value": "failed-{{{ _index }}}"
28.        }
29.      },
30.      {
31.        "set": {
32.          "description": "Set error message",
33.          "field": "ingest.failure",
34.          "value": "{{_ingest.on_failure_message}}"
35.        }
36.      }
37.    ]
38.  }


从 inference 处理器开始,field_map 的目的是将 paragraph(源文档中要分析的字段)映射到 text_field(模型配置使用的字段的名称)。 target_field 是要写入推理结果的字段的名称。

脚本处理器提取实体并按类型对它们进行分组。最终结果是在输入文本中检测到的人员、位置和组织的列表。我们正在添加这个painless 脚本,以便我们可以从创建的字段构建可视化。

on_failure 子句用于捕获错误。它定义了两个动作。首先,它将 _index 元字段设置为一个新值,文档现在将存储在那里。其次,将错误消息写入一个新字段:ingest.failure。由于许多容易解决的原因,inference 可能会失败。可能模型尚未部署,或者某些源文档中缺少输入字段。通过将失败的文档重定向到另一个索引并设置错误消息,这些失败的推理不会丢失并且可以在以后查看。修复错误后,从失败的索引重新索引以恢复不成功的请求。

我们运行上面的指令。

为推理选择文本字段

NER 可以应用于许多数据集。 作为一个例子,我选择了维克多·雨果 1862 年的经典小说《悲惨世界》。 你可以使用 Kibana 的文件上传功能上传 "示例 json 文件” 的悲惨世界段落。 文本分为 14,021 个 JSON 文档,每个文档包含一个段落。 以随机段落为例:



1.  {
2.      "paragraph": "Father Gillenormand did not do it intentionally, but inattention to proper names was an aristocratic habit of his.",
3.      "line": 12700
4.  }


我们可以使用如下的方法来创建一个叫做 miserables 的索引:



1.  PUT miserables/_doc/1?pipeline=ner
2.  {
3.    "paragraph": "Father Gillenormand did not do it intentionally, but inattention to proper names was an aristocratic habit of his.",
4.    "line": 12700
5.  }


 那么当我索引的时候,我们会发现:

GET miserables/_search?filter_path=**.hit

上面的命令显示的结果为:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "miserables",
6.          "_id" : "1",
7.          "_score" : 1.0,
8.          "_source" : {
9.            "paragraph" : "Father Gillenormand did not do it intentionally, but inattention to proper names was an aristocratic habit of his.",
10.            "line" : 12700,
11.            "ml" : {
12.              "ner" : {
13.                "predicted_value" : "[Father](PER&Father) [Gillenormand](PER&Gillenormand) did not do it intentionally, but inattention to proper names was an aristocratic habit of his.",
14.                "entities" : [
15.                  {
16.                    "entity" : "father",
17.                    "class_name" : "PER",
18.                    "class_probability" : 0.603207732941365,
19.                    "start_pos" : 0,
20.                    "end_pos" : 6
21.                  },
22.                  {
23.                    "entity" : "gillenormand",
24.                    "class_name" : "PER",
25.                    "class_probability" : 0.8452480178816778,
26.                    "start_pos" : 7,
27.                    "end_pos" : 19
28.                  }
29.                ],
30.                "model_id" : "elastic__distilbert-base-uncased-finetuned-conll03-english"
31.              }
32.            },
33.            "tags" : {
34.              "PER" : [
35.                "gillenormand",
36.                "father"
37.              ]
38.            }
39.          }
40.        }
41.      ]
42.    }
43.  }


从上面的输出中,我们可以看出来,我们使用 inference processor 得到了 ml.ner 字段下面的子字段,它含有 predicted_value,entities 及 model_id 信息。在我们上面的 pipeline 中,我们通过 painless 脚本,添加了 tags 这个字段。它收集了该文档 paragraph 中所有能识别的人名,地点及组织。

我们现在使用如下的方法导入第二个文档:



1.  PUT miserables/_doc/2?pipeline=ner
2.  {
3.    "paragraph": "One day he arrived at Senez, which is an ancient episcopal city. He was mounted on an ass. His purse, which was very dry at that moment, did not permit him any other equipage. The mayor of the town came to receive him at the gate of the town, and watched him dismount from his ass, with scandalized eyes. Some of the citizens were laughing around him. “Monsieur the Mayor,” said the Bishop, “and Messieurs Citizens, I perceive that I shock you. You think it very arrogant in a poor priest to ride an animal which was used by Jesus Christ. I have done so from necessity, I assure you, and not from vanity.”",
4.    "line": 111111
5.  }


我们再次查询文档:

GET miserables/_doc/2


1.  {
2.    "_index" : "miserables",
3.    "_id" : "2",
4.    "_version" : 2,
5.    "_seq_no" : 2,
6.    "_primary_term" : 1,
7.    "_ignored" : [
8.      "paragraph.keyword",
9.      "ml.ner.predicted_value.keyword"
10.    ],
11.    "found" : true,
12.    "_source" : {
13.      "paragraph" : "One day he arrived at Senez, which is an ancient episcopal city. He was mounted on an ass. His purse, which was very dry at that moment, did not permit him any other equipage. The mayor of the town came to receive him at the gate of the town, and watched him dismount from his ass, with scandalized eyes. Some of the citizens were laughing around him. “Monsieur the Mayor,” said the Bishop, “and Messieurs Citizens, I perceive that I shock you. You think it very arrogant in a poor priest to ride an animal which was used by Jesus Christ. I have done so from necessity, I assure you, and not from vanity.”",
14.      "line" : 111111,
15.      "ml" : {
16.        "ner" : {
17.          "predicted_value" : "One day he arrived at [Senez](LOC&Senez), which is an ancient episcopal city. He was mounted on an ass. His purse, which was very dry at that moment, did not permit him any other equipage. The mayor of the town came to receive him at the gate of the town, and watched him dismount from his ass, with scandalized eyes. Some of the citizens were laughing around him. “Monsieur the Mayor,” said the Bishop, “and Messieurs Citizens, I perceive that I shock you. You think it very arrogant in a poor priest to ride an animal which was used by [Jesus Christ](PER&Jesus+Christ). I have done so from necessity, I assure you, and not from vanity.”",
18.          "entities" : [
19.            {
20.              "entity" : "senez",
21.              "class_name" : "LOC",
22.              "class_probability" : 0.9955265573589915,
23.              "start_pos" : 22,
24.              "end_pos" : 27
25.            },
26.            {
27.              "entity" : "jesus christ",
28.              "class_name" : "PER",
29.              "class_probability" : 0.9587472891276063,
30.              "start_pos" : 525,
31.              "end_pos" : 537
32.            }
33.          ],
34.          "model_id" : "elastic__distilbert-base-uncased-finetuned-conll03-english"
35.        }
36.      },
37.      "tags" : {
38.        "LOC" : [
39.          "senez"
40.        ],
41.        "PER" : [
42.          "jesus christ"
43.        ]
44.      }
45.    }
46.  }


在上面,我们可以看到除了多出来一个叫做 jesus chirst 的人名以外,我们还看到了一个叫做 senez 的地名(LOC)。

当然如果我们有《悲惨世界》的所有段落的 JSON 格式,我们可以把正本书摄入到 Elasticsearch,并形成最终的 miserables 索引。我们可以使用 Kibana 的可视化工具来进行分析。

Tag 是一种可视化,可以根据单词出现的频率对单词进行缩放,是查看《悲惨世界》中的实体的完美信息图。 打开 Kibana 并创建一个新的基于聚合的可视化,然后选择 Tag Cloud。 选择包含 NER 结果的索引并在 tags.PER.keyword 字段上添加术语聚合。

从上面的图中,我们可以看出 Marius 是出现最多的人名。

这个在我们现实的使用中,其实也是蛮有意义的。比如我们有一篇新闻文章,我们想统计出来这篇文章中出现最多的人名,地点或组织。我们不需要去阅读每一行字,并进行统计。我们可以借助 NLP 的强大功能来完成。

调整部署

返回到模型管理 UI,在 Deployment stats 下,你将找到 Avg Inference Time。这是本机进程测量的对单个请求执行推理的时间。开始部署时,有两个参数控制 CPU 资源的使用方式:inference_threads 和 model_threads。

 inference_threads 是每个请求用于运行模型的线程数。增加 inference_threads 直接减少平均推理时间。并行评估的请求数由 model_threads 控制。此设置不会减少平均推理时间,但会增加吞吐量。

通常,通过增加 inference_threads 的数量来调整延迟,并通过增加 model_threads 的数量来增加吞吐量。这两种设置都默认为一个线程,因此通过修改它们可以获得大量性能。使用 NER 模型演示了该效果。

要更改其中一项线程设置,必须停止并重新启动部署。 ?force=true 参数被传递给停止 API,因为部署由通常会阻止停止的摄取管道引用。

POST _ml/trained_models/elastic__distilbert-base-uncased-finetuned-conll03-english/deployment/_stop?force=true

并使用四个推理线程重新启动。 重新启动部署时会重置平均推理时间。

POST _ml/trained_models/elastic__distilbert-base-uncased-finetuned-conll03-english/deployment/_start?inference_threads=4

在处理 Les Misérables 段落时,每个请求的平均推理时间下降到 55.84 毫秒,而一个线程的平均推理时间为 173.86 毫秒。

学习更多并尝试一下

NER 只是现在可以使用的 NLP 任务之一。 ext classification, zero shot classification 及 text embeddings也可用。 可以在 NLP 文档中找到更多示例,以及可部署到 Elastic Stack 的模型的详尽列表

NLP 是 Elastic Stack for 8.0 中的一项重要新功能,具有令人兴奋的路线图。 通过在 Elastic Cloud 中构建集群来发现新功能并跟上最新发展。 立即注册免费试用 14 天,并尝试此博客中的示例。