大模型系列:OpenAI使用技巧_在富文本上做命名实体识别(NER)


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

本笔记本演示了如何使用聊天补全函数调用进行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的模型
OPENAI_MODEL = 'gpt-3.5-turbo-0613'
openai.api_key = os.getenv("OPENAI_API_KEY")

2. 定义要识别的命名实体标签

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

# 定义了一个列表labels,包含了各种标签,用于对文本进行分类

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格式进行回复。这种能力简化了我们的提示,并使我们能够直接从模型接收结构化数据。

# 定义一个函数system_message,它需要一个参数labels
def system_message(labels):
    # 返回一个字符串,其中包含一些提示信息和参数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():
    # 使用f-string格式化字符串,返回一个示例文本和对应的实体信息
    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"],  # 时间实体,包括1440年
        "person": ["Johannes Gutenberg"],  # 人物实体,包括约翰内斯·古腾堡
        "product": ["movable-type printing press"],  # 产品实体,包括活字印刷机
        "event": ["Renaissance"],  # 事件实体,包括文艺复兴
        "quantity": ["3,600 pages"],  # 数量实体,包括每个工作日可生产3600页
        "time": ["workday"]  # 时间实体,包括工作日
    }}
--"""
3.3 用户消息

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

# 定义一个函数user_message,接收一个参数text
def user_message(text):
    # 使用f-string格式化字符串,返回一个包含text的文本
    return f"""
TASK:
    Text: {text}
"""

4. OpenAI 函数(和工具)

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

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

@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(5))
# 使用retry装饰器,设置等待时间为随机指数级增长,最小等待时间为1秒,最大等待时间为10秒,最多重试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  # 如果没有找到相关结果或者出现了异常,则返回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:
    """
    使用知识库链接丰富文本。
    """
    # 调用find_all_links函数,将实体和链接存储在字典entity_link_dict中
    entity_link_dict = find_all_links(label_entities)
    
    # 打印entity_link_dict字典的内容
    logging.info(f"entity_link_dict: {entity_link_dict}")
    
    # 遍历entity_link_dict字典中的每个实体和链接
    for entity, link in entity_link_dict.items():
        # 将文本中的实体替换为带有链接的格式
        text = text.replace(entity, f"[{entity}]({link})")

    # 返回替换后的文本
    return text

4. ChatCompletion

如前所述,gpt-3.5-turbo-0613gpt-4-0613已经被微调,以便检测何时应该调用一个function。此外,它们可以生成符合function签名的JSON响应。以下是我们遵循的步骤序列:

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

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

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

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

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

{   
    "gpe": ["德国", "欧洲"],   
    "date": ["1440"],   
    "person": ["约翰内斯·古腾堡"],   
    "product": ["活字印刷机"],   
    "event": ["文艺复兴"],   
    "quantity": ["3,600页"],   
    "time": ["工作日"]   
}   

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

# 生成函数列表
def generate_functions(labels: dict) -> list:
    # 返回一个包含函数信息的列表
    return [
        {
            "name": "enrich_entities",  # 函数名为enrich_entities
            "description": "Enrich Text with Knowledge Base Links",  # 函数描述为"使用知识库链接丰富文本"
            "parameters": {  # 函数参数
                "type": "object",  # 参数类型为对象
                "properties": {  # 参数属性
                    # 参数属性名为正则表达式,匹配labels中的任意一个标签
                    "r'^(?:' + '|'.join({labels}) + ')$'": 
                    {
                        "type": "array",  # 参数属性值为数组
                        "items": {  # 数组元素类型为字符串
                            "type": "string"
                        }
                    }
                },
                "additionalProperties": False  # 不允许额外的属性
            },
        }
    ]
4.2 聊天完成

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

# 定义一个装饰器函数retry,用于重试函数调用
@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)}
      ]

    # 使用openai.ChatCompletion.create函数发送对话消息并获取响应
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=generate_functions(labels),
        function_call={"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["function_call"]["name"]
    
    # 根据函数名获取对应的函数
    function_to_call = available_functions[function_name]
    logging.info(f"function_to_call: {function_to_call}")

    # 获取响应消息中的函数参数
    function_args = json.loads(response_message["function_call"]["arguments"])
    logging.info(f"function_args: {function_args}")

    # 调用函数并获取函数的返回值
    function_response = function_to_call(text, function_args)

    # 返回模型响应和函数响应
    return {"model_response": response, 
            "function_response": function_response}
  1. 让我们使用维基百科链接丰富一段文本。
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."""

# 调用run_openai_task函数,传入标签和语料文本
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_enriched_text("这是一段文本", {"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”字段。每个模型的详细令牌成本可以在OpenAI定价指南中找到。

# 估计使用gpt-3.5-turbo (4K context)模型的推理成本
# 获取输入文本的token数量
i_tokens  = result["model_response"]["usage"]["prompt_tokens"] 
# 获取输出文本的token数量
o_tokens = result["model_response"]["usage"]["completion_tokens"] 

# 计算输入文本的成本
i_cost = (i_tokens / 1000) * 0.0015
# 计算输出文本的成本
o_cost = (o_tokens / 1000) * 0.002

# 输出token使用情况和成本估计
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数智笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值