命名实体识别 (NER) 以丰富文本

命名实体识别 (NER) 是一项自然语言处理任务,用于识别命名实体 (NE) 并将其分类到预定义的语义类别中(例如人物、组织、地点、事件、时间表达式和数量)。通过将原始文本转换为结构化信息,NER 使数据更具可操作性,从而促进信息提取、数据聚合、分析和社交媒体监控等任务。

本笔记本演示了如何使用 聊天补全函数调用 来丰富文本,并链接到知识库,例如维基百科:

文本:

1440年,在德国,金匠约翰内斯·古腾堡发明了活字印刷机。他的工作引发了一场信息革命,并前所未有地大规模传播了欧洲的文学。以现有螺旋压机的设计为模型,一台文艺复兴时期的活字印刷机每工作日可生产多达3600页。

用维基百科链接丰富后的文本:

德国 ,1440年,金匠 约翰内斯·古腾堡 发明了 活字印刷机 。他的工作引发了一场 信息革命 ,并前所未有地大规模传播了 欧洲 的文学。以现有螺旋压机的设计为模型,一台 文艺复兴 活字印刷机 每工作日可生产多达3600页。

推理成本: 本笔记本还说明了如何估算 OpenAI API 的成本。

1. 设置

1.1 安装/升级 Python 包

%pip install --upgrade openai --quiet
%pip install --upgrade nlpia2-wikipedia --quiet
%pip install --upgrade tenacity --quiet
注意:您可能需要重启内核才能使用更新的包。
注意:您可能需要重启内核才能使用更新的包。
注意:您可能需要重启内核才能使用更新的包。

1.2 加载包和 OPENAI_API_KEY

您可以在 OpenAI 网页界面生成 API 密钥。有关详细信息,请参阅 https://platform.openai.com/account/api-keys 。

本笔记本适用于最新的 OpeanAI 模型 gpt-3.5-turbo-0613gpt-4-0613

import json
import logging
import os

import openai
import wikipedia

from typing import Optional
from IPython.display import display, Markdown
from tenacity import retry, wait_random_exponential, stop_after_attempt

logging.basicConfig(level=logging.INFO, format=' %(asctime)s - %(levelname)s - %(message)s')

OPENAI_MODEL = 'gpt-3.5-turbo-0613'

client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<您的 OpenAI API 密钥,如果未设置为环境变量>"))

2. 定义要识别的 NER 标签

我们定义了一组标准的 NER 标签,以展示广泛的用例。但是,对于我们用知识库链接丰富文本的特定任务,实际上只需要其中的一部分。

labels = [
    "person",      # 人,包括虚构人物
    "fac",         # 建筑物、机场、高速公路、桥梁
    "org",         # 组织、公司、机构、部门
    "gpe",         # 地缘政治实体,如国家、城市、州
    "loc",         # 非地缘政治地点
    "product",     # 车辆、食品、服装、电器、软件、玩具
    "event",       # 命名的体育赛事、科学里程碑、历史事件
    "work_of_art", # 书籍、歌曲、电影的标题
    "law",         # 命名的法律、法案或立法
    "language",    # 任何命名的语言
    "date",        # 绝对或相对日期或时期
    "time",        # 小于一天的时间单位
    "percent",     # 百分比(例如,“百分之二十”,“18%”)
    "money",       # 货币价值,包括单位
    "quantity",    # 测量值,例如重量或距离
]

3. 准备消息

聊天补全 API 以消息列表作为输入,并以模型生成的消息作为输出。虽然聊天格式主要用于促进多轮对话,但对于没有先前对话的单轮任务也同样有效。就我们而言,我们将为系统、助手和用户角色指定一条消息。

3.1 系统消息

系统消息(提示)通过定义所需的个性和任务来设置助手的行为。我们还规定了我们旨在识别的特定实体标签集。

虽然可以指示模型格式化其响应,但必须注意的是,gpt-3.5-turbo-0613gpt-4-0613 都经过微调,能够识别何时应调用函数,并以符合函数签名的 JSON 格式进行回复。此功能简化了我们的提示,并使我们能够直接从模型接收结构化数据。

def system_message(labels):
    return f"""
您是自然语言处理专家。您的任务是识别给定文本中的常见命名实体 (NER)。
可能的常见命名实体 (NER) 类型仅限于:({", ".join(labels)})。"""

3.2 助手消息

助手消息通常存储先前的助手响应。但是,在我们的场景中,它们也可以用来提供所需行为的示例。虽然 OpenAI 能够执行零样本命名实体识别,但我们发现单样本方法可以产生更精确的结果。

def assisstant_message():
    return f"""
示例:
    文本:“1440年,在德国,金匠约翰内斯·古腾堡发明了活字印刷机。他的工作引发了一场信息革命,并前所未有地大规模传播了欧洲的文学。以现有螺旋压机的设计为模型,一台文艺复兴时期的活字印刷机每工作日可生产多达3600页。”
    {{
        "gpe": ["Germany", "Europe"],
        "date": ["1440"],
        "person": ["Johannes Gutenberg"],
        "product": ["movable-type printing press"],
        "event": ["Renaissance"],
        "quantity": ["3,600 pages"],
        "time": ["workday"]
    }}
--"""

3.3 用户消息

用户消息提供了助手的具体文本任务:

def user_message(text):
    return f"""
任务:
    文本:{text}
"""

4. OpenAI 函数(和实用程序)

在 OpenAI API 调用中,我们可以将 函数 描述给 gpt-3.5-turbo-0613gpt-4-0613,并让模型智能地选择输出一个包含调用这些 函数 的参数的 JSON 对象。需要注意的是,聊天补全 API 实际上并不执行 函数。相反,它提供 JSON 输出,然后可以使用该输出在我们的代码中调用 函数。有关更多详细信息,请参阅 OpenAI 函数调用指南

我们的函数 enrich_entities(text, label_entities) 获取一个文本块和一个包含已识别标签和实体的字典作为参数。然后,它将识别出的实体与相应的维基百科文章链接关联起来。

@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(5))
def find_link(entity: str) -> Optional[str]:
    """
    查找给定实体的维基百科链接。
    """
    try:
        titles = wikipedia.search(entity)
        if titles:
            #  naively consider the first result as the best
            page = wikipedia.page(titles[0])
            return page.url
    except (wikipedia.exceptions.WikipediaException) as ex:
        logging.error(f'在搜索实体 {entity} 的维基百科链接时出错:{str(ex)}')

    return None
def find_all_links(label_entities:dict) -> dict:
    """
    查找白名单标签列表中实体字典的所有维基百科链接。
    """
    whitelist = ['event', 'gpe', 'org', 'person', 'product', 'work_of_art']

    return {e: find_link(e) for label, entities in label_entities.items()
                            for e in entities
                            if label in whitelist}
def enrich_entities(text: str, label_entities: dict) -> str:
    """
    用知识库链接丰富文本。
    """
    entity_link_dict = find_all_links(label_entities)
    logging.info(f"entity_link_dict: {entity_link_dict}")

    for entity, link in entity_link_dict.items():
        text = text.replace(entity, f"[{entity}]({link})")

    return text

4. 聊天补全

如前所述,gpt-3.5-turbo-0613gpt-4-0613 经过微调,可以检测何时应调用 函数。此外,它们还可以生成符合 函数 签名的 JSON 响应。以下是我们遵循的顺序:

  1. 定义我们的 函数 和相关的 JSON 架构。
  2. 使用 messagestoolstool_choice 参数调用模型。
  3. 将输出转换为 JSON 对象,然后使用模型提供的 arguments 调用 函数

实际上,人们可能希望通过将 函数 响应作为新消息附加来重新调用模型,然后让模型向用户总结结果。尽管如此,对我们而言,此步骤不是必需的。

请注意,在实际场景中,强烈建议在采取行动之前建立用户确认流程。

4.1 定义我们的函数和 JSON 架构

由于我们希望模型输出标签和已识别实体的字典:

{
    "gpe": ["Germany", "Europe"],
    "date": ["1440"],
    "person": ["Johannes Gutenberg"],
    "product": ["movable-type printing press"],
    "event": ["Renaissance"],
    "quantity": ["3,600 pages"],
    "time": ["workday"]
}

我们需要定义要传递给 tools 参数的相应 JSON 架构:

def generate_functions(labels: dict) -> list:
    return [
        {
            "type": "function",
            "function": {
                "name": "enrich_entities",
                "description": "用知识库链接丰富文本",
                "parameters": {
                    "type": "object",
                        "properties": {
                            "r'^(?:' + '|'.join({labels}) + ')$'":
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                }
                            }
                        },
                        "additionalProperties": False
                },
            }
        }
    ]

4.2 聊天补全

现在,我们调用模型。需要注意的是,我们通过将 tool_choice 参数设置为 {"type": "function", "function" : {"name": "enrich_entities"}} 来指示 API 使用特定函数。

@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(5))
def run_openai_task(labels, text):
    messages = [
          {"role": "system", "content": system_message(labels=labels)},
          {"role": "assistant", "content": assisstant_message()},
          {"role": "user", "content": user_message(text=text)}
      ]

    # TODO: functions and function_call are deprecated, need to be updated
    # See: https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools
    response = openai.chat.completions.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        tools=generate_functions(labels),
        tool_choice={"type": "function", "function" : {"name": "enrich_entities"}},
        temperature=0,
        frequency_penalty=0,
        presence_penalty=0,
    )

    response_message = response.choices[0].message

    available_functions = {"enrich_entities": enrich_entities}
    function_name = response_message.tool_calls[0].function.name

    function_to_call = available_functions[function_name]
    logging.info(f"function_to_call: {function_to_call}")

    function_args = json.loads(response_message.tool_calls[0].function.arguments)
    logging.info(f"function_args: {function_args}")

    function_response = function_to_call(text, function_args)

    return {"model_response": response,
            "function_response": function_response}

5. 让我们用维基百科链接丰富文本

5.1 运行 OpenAI 任务

text = """The Beatles were an English rock band formed in Liverpool in 1960, comprising John Lennon, Paul McCartney, George Harrison, and Ringo Starr."""
result = run_openai_task(labels, text)
 2023-10-20 18:05:51,729 - INFO - function_to_call: <function enrich_entities at 0x0000021D30C462A0>
 2023-10-20 18:05:51,730 - INFO - function_args: {'person': ['John Lennon', 'Paul McCartney', 'George Harrison', 'Ringo Starr'], 'org': ['The Beatles'], 'gpe': ['Liverpool'], 'date': ['1960']}
 2023-10-20 18:06:09,858 - INFO - entity_link_dict: {'John Lennon': 'https://en.wikipedia.org/wiki/John_Lennon', 'Paul McCartney': 'https://en.wikipedia.org/wiki/Paul_McCartney', 'George Harrison': 'https://en.wikipedia.org/wiki/George_Harrison', 'Ringo Starr': 'https://en.wikipedia.org/wiki/Ringo_Starr', 'The Beatles': 'https://en.wikipedia.org/wiki/The_Beatles', 'Liverpool': 'https://en.wikipedia.org/wiki/Liverpool'}

5.2 函数响应

display(Markdown(f"""**Text:** {text}   
                     **Enriched_Text:** {result['function_response']}"""))

Text: The Beatles were an English rock band formed in Liverpool in 1960, comprising John Lennon, Paul McCartney, George Harrison, and Ringo Starr.
Enriched_Text: The Beatles were an English rock band formed in Liverpool in 1960, comprising John Lennon, Paul McCartney, George Harrison, and Ringo Starr.

5.3 Token 使用量

为了估算推理成本,我们可以解析响应的“usage”字段。模型详细的 token 成本可在 OpenAI 定价指南 中找到:

# estimate inference cost assuming gpt-3.5-turbo (4K context)
i_tokens  = result["model_response"].usage.prompt_tokens
o_tokens = result["model_response"].usage.completion_tokens

i_cost = (i_tokens / 1000) * 0.0015
o_cost = (o_tokens / 1000) * 0.002

print(f"""Token Usage
    Prompt: {i_tokens} tokens
    Completion: {o_tokens} tokens
    Cost estimation: ${round(i_cost + o_cost, 5)}""")
Token Usage
    Prompt: 331 tokens
    Completion: 47 tokens
    Cost estimation: $0.00059