跳到主要内容

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

nbviewer

命名实体识别(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

Note: you may need to restart the kernel to use updated packages.
Note: you may need to restart the kernel to use updated packages.
Note: you may need to restart the kernel to use updated packages.

1.2 加载包和OPENAI_API_KEY

您可以在OpenAI的网页界面中生成一个API密钥。请参阅https://platform.openai.com/account/api-keys获取详细信息。

这个笔记本适用于最新的OpenAI模型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", "<your OpenAI API key if not set as env var>"))

2. 定义要识别的NER标签

我们定义了一组标准的NER标签,以展示各种用例。然而,对于我们特定的任务——用知识库链接丰富文本,实际上只需要其中的一个子集。

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

3. 准备消息

聊天完成API接受消息列表作为输入,并生成模型生成的消息作为输出。虽然聊天格式主要设计用于促进多轮对话,但同样适用于没有任何先前对话的单轮任务。对于我们的目的,我们将为系统、助手和用户角色指定消息。

3.1 系统消息

系统消息(提示)通过定义所需的角色和任务来设置助手的行为。我们还明确了我们的目标是识别的特定实体标签集合。

尽管可以指示模型格式化其响应,但需要注意的是,gpt-3.5-turbo-0613gpt-4-0613 都经过微调,可以区分何时应调用函数,并根据函数签名格式化为 JSON 格式的响应。这种能力简化了我们的提示,并使我们能够直接从模型中接收结构化数据。

def system_message(labels):
return f"""
You are an expert in Natural Language Processing. Your task is to identify common Named Entities (NER) in a given text.
The possible common Named Entities (NER) types are exclusively: ({", ".join(labels)})."""

3.2 助理消息

“助手消息”通常存储先前的助手响应。然而,在我们的场景中,它们也可以被设计为提供所需行为的示例。虽然OpenAI能够执行“零-shot”命名实体识别,但我们发现“一-shot”方法产生更精确的结果。

def assisstant_message():
return f"""
EXAMPLE:
Text: 'In Germany, in 1440, goldsmith Johannes Gutenberg invented the movable-type printing press. His work led to an information revolution and the unprecedented mass-spread /
of literature throughout Europe. Modelled on the design of the existing screw presses, a single Renaissance movable-type printing press could produce up to 3,600 pages per workday.'
{{
"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调用中,我们可以描述functionsgpt-3.5-turbo-0613gpt-4-0613,并让模型智能地选择输出一个包含调用这些functions参数的JSON对象。重要的是要注意,chat completions API实际上并不执行这个function。相反,它提供JSON输出,然后可以在我们的代码中用来调用这个function。更多详情,请参考OpenAI Function Calling Guide

我们的函数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:
# 天真地认为第一个结果就是最好的
page = wikipedia.page(titles[0])
return page.url
except (wikipedia.exceptions.WikipediaException) as ex:
logging.error(f'Error occurred while searching for Wikipedia link for entity {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 已经被微调以检测何时应该调用一个 function。此外,它们可以生成符合 function 签名的 JSON 响应。我们遵循以下顺序:

  1. 定义我们的 function 及其相关的 JSON Schema。
  2. 使用 messagestoolstool_choice 参数调用模型。
  3. 将输出转换为一个 JSON 对象,然后使用模型提供的 arguments 调用 function

在实践中,一个可能希望通过将 function 响应作为新消息附加到模型中,让模型将结果总结回用户。然而,对于我们的目的,这一步是不需要的。

请注意,在真实情况下,强烈建议在执行操作之前构建用户确认流程。

4.1 定义我们的函数和JSON模式

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

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

我们需要定义相应的JSON模式,以便传递给tools参数:

def generate_functions(labels: dict) -> list:
return [
{
"type": "function",
"function": {
"name": "enrich_entities",
"description": "Enrich Text with Knowledge Base Links",
"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)}
]

# 待办事项:functions 和 function_call 已弃用,需要更新。
# 查看: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. 使用维基百科链接丰富文本

在这个任务中,我们将学习如何使用Python库来丰富文本内容,将文本中的关键词替换为维基百科页面的链接。这样可以帮助读者更深入地了解文本中提到的概念或主题。

5.1 运行OpenAI任务

text = """披头士乐队是一支成立于1960年的英国摇滚乐队,来自利物浦,成员包括约翰·列侬、保罗·麦卡特尼、乔治·哈里森和林戈·斯塔尔。"""
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 令牌使用

为了估计推理成本,我们可以解析响应中的“usage”字段。每个模型的详细令牌成本可在OpenAI Pricing Guide中找到:

# 假设使用GPT-3.5-Turbo(4K上下文)进行推理成本估算。
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