命名实体识别 (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-0613
和 gpt-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-0613
和 gpt-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-0613
和 gpt-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-0613
和 gpt-4-0613
经过微调,可以检测何时应调用 函数
。此外,它们还可以生成符合 函数
签名的 JSON
响应。以下是我们遵循的顺序:
- 定义我们的
函数
和相关的JSON
架构。 - 使用
messages
、tools
和tool_choice
参数调用模型。 - 将输出转换为
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