Openai
模型下游应用——知识图谱构建
本任务来自于笔者参加的阿里云天池大赛官网的比赛CCKS2023
开放环境下的知识图谱构建与补全评测任务一:指令驱动的自适应知识图谱构建。任务的实现方式很多,但是由于笔者刚刚接触
NLP 经验有限,就选择了使用 LLM
进行信息抽取和调优。赛后写出这样一篇文章总结,希望能给使用大模型实现相关任务的同学带来一些启发。
1 任务介绍
详细的任务介绍官网上有说明,在此我将其中核心的要求重复一下:你需要从一个句子中抽取信息,输出若干关系三元组,例如:
| 12
 
 | 输入: 陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。输出: (陆梦履,国籍,明代),(元礼,国籍,明代),(陆梦履,学历,进士),(元礼,学历,进士),(昆山,学历,进士)
 
 | 
            虽然抽取出来的,呃……有点离谱,但是对于机器嘛,也正常。
           
1.1 输入格式
输入格式是 json 文件,分为训练集 train.json 和测试集
test.json。数据下载地址:CCKS2023
数据集地址
训练集包含若干行 json,格式为:
| 12
 3
 4
 5
 6
 7
 8
 
 | {"id": xx,
 "cate": "...",
 "instruction": "...",
 "input": "...",
 "output": "...",
 "kg": [...]
 }
 
 | 
举例:
| 12
 3
 4
 5
 6
 7
 8
 
 | {"id": 11262,
 "cate": "人物",
 "instruction": "已知候选的关系列表:['国籍', '学历'],请你根据关系列表,从以下输入中抽取出可能存在的头实体(Subject)与尾实体(Object),并给出对应的关系三元组。请按照 (Subject,Relation,Object) 的格式回答。",
 "input": "陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。",
 "output": "(陆梦履,国籍,明代),(元礼,国籍,明代),(陆梦履,学历,进士),(元礼,学历,进士),(昆山,学历,进士)",
 "kg": [["陆梦履", "国籍", "明代"], ["元礼", "国籍", "明代"], ["陆梦履", "学历", "进士"], ["元礼", "学历", "进士"], ["昆山", "学历", "进士"]]
 }
 
 | 
测试集不含期望输出 output 和 kg,其他与训练集格式一样,举例:
| 12
 3
 4
 5
 6
 
 | {"id": 11262,
 "cate": "人物",
 "instruction": "已知候选的关系列表:['国籍', '学历'],请你根据关系列表,从以下输入中抽取出可能存在的头实体(Subject)与尾实体(Object),并给出对应的关系三元组。请按照 (Subject,Relation,Object) 的格式回答。",
 "input": "陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。"
 }
 
 | 
1.2 输出格式
假如你在进行推理预测,那么你的输出也应该是 json,包含
id、cate、input、output、kg,假如输入:
| 12
 3
 4
 5
 6
 
 | {"id": 11262,
 "cate": "人物",
 "instruction": "已知候选的关系列表:['国籍', '学历'],请你根据关系列表,从以下输入中抽取出可能存在的头实体(Subject)与尾实体(Object),并给出对应的关系三元组。请按照 (Subject,Relation,Object) 的格式回答。",
 "input": "陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。"
 }
 
 | 
你应该输出:
| 12
 3
 4
 5
 6
 7
 
 | {"id": 11262,
 "cate": "人物",
 "input": "陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。",
 "output": "(陆梦履,国籍,明代),(元礼,国籍,明代),(陆梦履,学历,进士),(元礼,学历,进士),(昆山,学历,进士)",
 "kg": [["陆梦履", "国籍", "明代"], ["元礼", "国籍", "明代"], ["陆梦履", "学历", "进士"], ["元礼", "学历", "进士"], ["昆山", "学历", "进士"]]
 }
 
 | 
2 实现思路
我们用的是 openai 的模型,所以只需要向大模型提问就行了,最简单的问答
demo:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 
 | 
 
 
 import openai
 from tenacity import retry, wait_random_exponential
 
 openai.api_key = "这里改成你自己的 api_key"
 openai.proxy = "http://localhost:7890"
 
 @retry(wait=wait_random_exponential(multiplier=1.5, max=60))
 def call_openai_engine(engine='gpt-3.5-turbo', prompt='', temperature=0.0):
 try:
 sample = openai.ChatCompletion.create(
 model=engine,
 messages=[
 {"role": "system", "content": "You are a helpful assistant."},
 {"role": "user", "content": prompt},
 ],
 temperature=temperature,
 )
 except openai.error.InvalidRequestError as e:
 traceback.print_exc()
 return None
 except:
 traceback.print_exc()
 print(f'Retrying querying OpenAI {engine}...')
 time.sleep(0.1)
 return sample
 
 
 def get_openai_generation(openai_api_response, engine='gpt-3.5-turbo'):
 """
 Extraction GPT output from OpenAI API response
 """
 if openai_api_response is None:
 return None
 generated_content = openai_api_response['choices'][0]['message']['content']
 return generated_content
 
 
 def main():
 prompt = "已知候选的关系列表:['国籍', '学历'],请你根据关系列表," + \
 "从以下输入中抽取出可能存在的头实体(Subject)与尾实体(Object),并给出对应的关系三元组。" + \
 "请按照 (Subject,Relation,Object) 的格式回答。\n" + \
 "输入:陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。\n" + \
 "请输出:"
 print(get_openai_generation(call_openai_engine(prompt=prompt)))
 
 | 
处理文件的读入和输出,就是从文件逐行读取 json
字符串然后进行处理,再从输出中提取出三元组,分别转化为字符串和列表两种形式输出到文件里。
3 优化思路
对于大模型,特别是 openai 的
gpt-3.5-turbo,我们无法得知模型的细节,自然也无法进行指令微调等操作改变模型参数来提高模型推理的准确度。但是可以通过在
prompt 中展示样例的方式让模型进行上下文学习,例如对于以下输入:
| 12
 3
 4
 5
 6
 
 | {"id": 11262,
 "cate": "人物",
 "instruction": "已知候选的关系列表:['国籍', '学历'],请你根据关系列表,从以下输入中抽取出可能存在的头实体(Subject)与尾实体(Object),并给出对应的关系三元组。请按照 (Subject,Relation,Object) 的格式回答。",
 "input": "陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。"
 }
 
 | 
可以构造如下的 prompt:
            假设你是一个人物领域的语言专家,请你根据关系列表,从输入中抽取出可能存在的头实体(Subject)、关系(Relation)和尾实体(Object),并给出所有对应的关系三元组。请按照(Subject,Relation,Object),... 的格式回答。
举例:
关系列表:['国籍', '学历']
输入:方学龙,字叔允,号望山,浙江淳安县人,明朝政治人物,同进士出身。
输出:(方学龙,国籍,明朝),(望山,国籍,明朝),(方学龙,学历,进士),(叔允,学历,进士),(望山,学历,进士)
关系列表:['别名', '国籍', '学历', '籍贯']
输入:胡心得(1537年-1616年),字元静,号襟寰,浙江仁和人,寄籍德清,明朝政治人物,进士出身。
输出:(胡心得,别名,元静),(胡心得,别名,襟寰),(胡心得,国籍,明朝),(德清,国籍,明朝),(胡心得,学历,进士),(德清,学历,进士),(胡心得,籍贯,仁和)关系列表:['别名', '国籍', '学历', '籍贯']
输入:罗汝芳(1515年-1588年),字惟德,号近溪,江西建昌府南城县人,明朝政治人物,同进士出身。
输出:(罗汝芳,别名,惟德),(罗汝芳,别名,近溪),(罗汝芳,国籍,明朝),(江西,国籍,明朝),(罗汝芳,学历,进士),(罗汝芳,籍贯,南城县)
请完成:
关系:['国籍', '学历']
输入:陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。
请输出:
           
这样一来优化思路就很多了:
- 对于一个 json 输入,在训练集的相同 cate
的数据中寻找若干句子相似度最接近的(如上例所示);
- 对于一个 json
输入,对于关系列表的每一个关系进行分别提问,每次只获取一种关系,最后将所有关系拼一起,合为一个完整的输出。对于单次提问:
- 在训练集中寻找 cate 相同且含有该关系的 json;
- 当目标不够时考虑跨关系寻找。
- ……
 
而对于第二种情况,可以构造两个 prompt 分别为:
            假设你是一个人物领域的语言专家,请你根据关系列表,从输入中抽取出可能存在的头实体(Subject)、关系(Relation)和尾实体(Object),并给出所有对应的关系三元组。请按照(Subject,Relation,Object),... 的格式回答。
给定关系:国籍
举例:
输入:方学龙,字叔允,号望山,浙江淳安县人,明朝政治人物,同进士出身。
输出:(方学龙,国籍,明朝),(望山,国籍,明朝)
输入:胡心得(1537年-1616年),字元静,号襟寰,浙江仁和人,寄籍德清,明朝政治人物,进士出身。
输出:(胡心得,国籍,明朝),(德清,国籍,明朝)
输入:罗汝芳(1515年-1588年),字惟德,号近溪,江西建昌府南城县人,明朝政治人物,同进士出身。
输出:(罗汝芳,国籍,明朝),(江西,国籍,明朝)
请完成:
关系:国籍
输入:陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。
请输出:
           
            假设你是一个人物领域的语言专家,请你根据关系列表,从输入中抽取出可能存在的头实体(Subject)、关系(Relation)和尾实体(Object),并给出所有对应的关系三元组。请按照(Subject,Relation,Object),... 的格式回答。
给定关系:学历
举例:
输入:陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。
输出:(陆梦履,学历,进士),(元礼,学历,进士),(昆山,学历,进士)
输入:方学龙,字叔允,号望山,浙江淳安县人,明朝政治人物,同进士出身。
输出:(方学龙,学历,进士),(叔允,学历,进士),(望山,学历,进士)
输入:胡心得(1537年-1616年),字元静,号襟寰,浙江仁和人,寄籍德清,明朝政治人物,进士出身。
输出:(胡心得,学历,进士),(德清,学历,进士)
输入:罗汝芳(1515年-1588年),字惟德,号近溪,江西建昌府南城县人,明朝政治人物,同进士出身。
输出:(罗汝芳,学历,进士)
输入:项维聪(1574年-?),字懋德,号听所,浙江温州府永嘉县军籍丽水县人,明朝政治人物。同进士出身。
输出:(项维聪,学历,进士),(懋德,学历,进士)
请完成:
关系:学历
输入:陆梦履,字元礼,直隶昆山(今江苏)人,明代政治人物,进士出身。
请输出:
           
那么难点就变成如何寻找文本相似度最近的样例了,目前这样的开源工具已经有了,大家可以自由选择。