实验室项目记录——为RAG构造数据集
"在框架如此完备的情况下,使用模型是俗手;设计模型是本手;而构造训练数据集,my friend,这才是妙手。"
实验室项目记录——为RAG构造数据集
前置准备
背景和需求
最后要做成一个知识图谱类的RAG模型,做知识库训练的数据主要是论文。
在构造数据集时,前期用人工手动标注论文中的实体类型和关系,由于标注人员的专业水平和对文本的理解均达不到要求,故效果不佳。对于英文版本的数据集更是如此,甚至不能直接标注英文数据集,需要手动翻译并进行实体替换,这样造成数据极不准确,召回率和准确率都非常低。本次实验主要目的是利用大模型来标注中英文论文并构造成json数据,主要难点在于关系提取,详见规则部分。
构造规则
数据标注完全按这个文档行事,对于数据集的构造,一句话要弄成一个json格式,具体包括:
source_entity
target_entity
source_type
target_type
description
例:
输入给大模型:
目标检测的准确率是评估侦察目标性能的重要指标之一。论文提出了基于faster r-cnn的无人机侦察目标检测方法,对模型的rpn模块、多任务损失函数、卷积特征共享等算法进行了分析和研究,选取油库、舰艇、立交桥、飞机等四种典型目标,以faster r-cnn为基准模型进行训练和测试,模型平均准确率为89.47%,目标检测准确率高。
应输出:
{
"spo_list": [
{"source_entity": "无人机侦察目标检测方法","target_entity": "基于Faster R-CNN的无人机侦察目标检测方法","source_type":"算法","target_type":"算法","type": "子类","description": "基于Faster R-CNN的无人机侦察目标检测方法是无人机侦察目标检测方法的具体实现形式,专门针对无人机平台优化,展示了该领域内算法的特化和发展。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "Faster R-CNN","type": "包含","description": "Faster R-CNN算法是构成基于Faster R-CNN的无人机侦察目标检测方法的核心部分,提供了高效的目标检测和识别能力。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "RPN模块","type": "应用","description": "RPN模块在基于Faster R-CNN的无人机侦察目标检测方法中被应用以生成区域提议,这是连接区域选择与目标分类的关键步骤。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "多任务损失函数","type": "包含","description": "多任务损失函数在该方法中被包含用以同时优化目标分类与边框回归,增强了模型在处理多目标检测时的准确性和效率。"
},...
}
主要过程
模型选择
我打算先用OneKE,这是一个专门做知识抽取的模型。我先尝试这个可能会失去一些节目效果,毕竟我觉得如果这个都达不到要求我也懒得去试别的模型了。祈祷它一步到位吧。
模型部署
下载
git clone https://github.com/zjunlp/deepke
git clone https://www.modelscope.cn/ZJUNLP/OneKE.git
本来用的是huggingface,但是学校的服务器不出所料地连不上,故换成modelscope。
环境安装
conda create -n OneKE python=3.9
conda activate OneKE
pip install -r requirements.txt
按照我以往的经验环境不可能一次装成功,果然,seqeval报错:
/share/home/202321044387/anaconda3/envs/OneKE/lib/python3.9/site-packages/setuptools/__init__.py:94: _DeprecatedInstaller: setuptools.installer and fetch_build_eggs are deprecated.
!!
```shell
********************************************************************************
Requirements should be satisfied by a PEP 517 installer.
If you are using pip, you can try `pip install --use-pep517`.
********************************************************************************
!!
dist.fetch_build_eggs(dist.setup_requires)
```
这倒不是什么大问题,把这个包单拎出来先安上:
pip install --use-pep517 seqeval==1.2.2
可以装上,现在再去装txt。
好,omegaconf又报错说找不到版本:
ERROR: No matching distribution found for omegaconf<2.1,>=2.0.5
我也不知道能不能这么写,我把它规定成2.0.6算了。
结果还是不行,原来根源在我pip版本太高了:
Please use pip<24.1 if you need to use this version.
把它降级到24.0再试试。
这次果然可以装上了。
总体来说我觉得这次环境配置简直顺利得蹊跷,往常那些torch、tqdm什么的报错大户今天都乖得很,就报了两个版本的小错瞬间就能解决了。
模型测试和debug
环境安好之后切到计算节点,我打算先跑一遍他给的快速运行代码,结果一跑直接报错:
Traceback (most recent call last):
File "/share/home/202321044387/code/OneKE/fastrun.py", line 2, in <module>
from transformers import (
ImportError: cannot import name 'BitsAndBytesConfig' from 'transformers' (/share/home/202321044387/anaconda3/envs/OneKE/lib/python3.9/site-packages/transformers/__init__.py)
说transformers里缺包,我首先想到可能是版本问题,查了requirements的版本发现是对的,于是上网找偏方,发现github有人说要额外装一个bitsandbytes,我装上以后还是不行。
后来我发现,这个github项目里有一堆的requirements,我看的这篇文档在example/llm目录下,它有个自己的requirements,里面的transformers是4.33版本……
更新后果然不报了。
后续的一些小错误都是requirements安装错误引起的,直到它报了一个:
RuntimeError: CUDA error: no kernel image is available for execution on the device
CUDA kernel errors might be asynchronously reported at some other API call,so the stacktrace below might be incorrect.
For debugging consider passing CUDA_LAUNCH_BLOCKING=1.
这八成是CUDA、torch和显卡匹配不上导致的,我干脆先把正确的requirements装上再看。我觉得如果真是CUDA版本不对,那就麻烦大了,毕竟我不太想在学校服务器上搞换CUDA版本这种大活。
幸运的是装上所有包后不再报这个错了,运行之后时间明显长了很多,CUDA也能正常setup。但是又报:
undefined symbol: cquantize_blockwise_fp16_nf4
这是因为bitsandbytes用了cpu的so但不是gpu的(这里我其实很奇怪为什么,他为什么不能安装的时候自动按CUDA版本选gpu的so?难道说所有用gpu的人都要手动去改so文件吗?)
按照网上的方法cp了一下:
cd /home/xxx/.conda/envs/xxx/lib/python3.x/site-packages/bitsandbytes
cp libbitsandbytes_cuda1xx.so libbitsandbytes_cpu.so
可行,到这里模型基础debug完毕,可以跑通输出结果。
{"person": ["Robert Allenby", "Allenby", "Miguel Angel Martin"], "organization": [], "else": [], "location": ["Australia", "Spain"]}
模型使用
现在我手里的raw data都是txt格式,这无疑给我减小了不少难度,至少我不用拿着文档去做切分什么的。现在我要做的就是把这些txt一个个喂给模型让它输出符合条件的数据关系格式。
我初步打算把示例文档整个当成prompt喂给模型,看看效果怎么样。
初步尝试
我的prompt和input分别放在两个txt里,在代码里其实只需要加一步读取并且改一下输出格式:
# 加载自定义提示
with open('custom_prompt.txt', 'r', encoding='utf-8') as f:
system_prompt = f.read()
# 加载自定义输入
with open('input_text.txt', 'r', encoding='utf-8') as f:
user_input = f.read()
# 生成输出
generation_output = model.generate(
input_ids=input_ids,
generation_config=GenerationConfig(
max_length=1024,
max_new_tokens=512,
return_dict_in_generate=True
),
pad_token_id=tokenizer.eos_token_id
)
剩下的照抄即可。
这次如果效果不满意,我就拿人工标注好的数据集当outputs去训练它。当然我还是希望它能尽量无师自通,因为如果要训练可能又是一堆的bug,况且人工标的本来也不咋地,用那玩意训练出的东西更是指望不上。
无法解决的技术困难
我用示例文档当成prompt,并额外编写了instruction,整个instruction_dict是这样的:
instruction_dict = {
"instruction": (
"You are an expert in relationship extraction. "
"Please extract all unique relationship triples from the input text, usually it contains lots of relationship triples, you need to find out all of them, do not output empty list. "
"Each triple should include 'source_entity', 'type', 'target_entity', and 'description'. "
"Ensure there are no duplicate triples and the output is free from any garbled text. "
"Please respond in the following JSON format:\n"
"{\n"
' "spo_list": [\n'
' {\n'
' "source_entity": "source_entity",\n'
' "type": "type",\n'
' "target_entity": "target_entity",\n'
' "description": "description"\n'
' },\n'
' {\n'
' "source_entity": "source_entity",\n'
' "type": "type",\n'
' "target_entity": "target_entity",\n'
' "description": "description"\n'
' },\n'
' ...\n'
' ]\n'
'}'
),
"input": user_input
}
结果模型居然给我输出了一个空列表……我可是直接在instruction里说了不允许输出空列表啊……
之后我把格式示例改成:
"Please respond in the following JSON format:\n"
"{\n"
' "spo_list": [\n'
' {\n'
' "source_entity": "source_entity",\n'
' "type": "type",\n'
' "target_entity": "target_entity",\n'
' "description": "description"\n'
' }\n'
' ]\n'
'}'
这样确实可以有正确的关系输出,但是这又有另一个问题,就是模型只输出一组关系。
Model Output:
{"spo_list": [{"source_entity": "基于大涡模拟方法", "type": "应用", "target_entity": "亚格子模型", "description": "对冲击射流的噪声特性进行了数值模拟"}]}
这个问题我至今没想到是为什么,我有怀疑过是输出给的token不够长,但是就算我改到4096也无济于事,我觉得问题还是出在prompt上。
2024年9月20日17:52:50
2024年9月25日08:39:31
第二次尝试
要解决这个问题,还是应该仔细研究一下那个instruction_dict是干嘛用的、该怎么写。
'instruction'
的格式采用了类JSON字符串的结构,实质上是一种字典类型的字符串。它由以下三个字段构成: (1) 'instruction'
,即任务描述,以自然语言指定模型扮演的角色以及需要完成的任务; (2) 'schema'
,这是一份需提取的标签列表,明确指出了待抽取信息的关键字段,反应用户的需求,是动态可变的; (3) 'input'
,指的是用于信息抽取的源文本。
按文档的要求,instruction要写成:
{
"instruction": "你是专门进行实体抽取的专家。请从input中抽取出符合schema定义的实体,不存在的实体类型返回空列表。请按照JSON字符串的格式回答。",
"schema": {
"职位": "实体类型描述个人或群体的职业或职务,包括特定角色名称如'制片方','报分员','苦行僧','油画家'。",
"景点": "景点实体类型包括建筑物、博物馆、纪念馆、美术馆、河流、山峰等。代表性实体有五角大楼、泰特当代美术馆、郑成功纪念馆、都喜天阙、巴里卡萨、罗博河、gunungbatur、愚公移山LIVE、徐悲鸿纪念馆、杜莎夫人蜡像馆等。",
"公司": "公司是一个实体类型,代表任何法人实体或商业组织。这个类型的实体可以是餐饮集团,制造商,零售商,酒店,银行,设计院等各种类型的公司。例如:'香格里拉酒店集团', 'JVC', '上海酷蕾专业电竞外设装备店', 'k2•海棠湾', '武钢', 'louisvuitton', '苏格兰银行', '北京市建筑设计研究院', '7天', '万科集团'。",
"地址": "地址实体是指具有地理位置信息的实体,它可以代表一个国家、城市、区域、街道等具体的地方或者一个抽象的地理区域。例如:'曼哈顿下城区东南尖上的河边码头', '图阿普谢', '意大利威尼斯水乡', '湖州温泉高尔夫球场', '北卡罗来纳州', '京津区域', '开心网吧', '颐年护理院', '上塘镇浦东', '内蒙古自治区赤峰市'等。",
"组织机构": "组织机构实体是指集体性质的组织,比如公司、商铺、俱乐部、学校等。它们在社会和经济活动中扮演一定角色,并拥有一定的人格权。",
"电影": "电影实体包括中文或英文电影名称,有时也包括电影人物角色名。"
},
"input": "我很难想象在另外一个项目再做一个海渔广场,当时我们拿到这个项目的时候,我正好在三亚,"
}
这里边包含了我写在system prompt里的东西。我其实怀疑过这些内容可不可以分开在prompt和instruction里,一个是它没用langchain,我不知道prompt和instruction是怎么组合的;另一个是我当时不知道这俩有什么区别。
把instruction按要求整理结合一下:
instruction_dict = {
"instruction": "你是专门进行实体及其关系抽取的专家。请从input从文本中识别出所有这些符合schema定义的实体以及它们之间所有的关系,同一个实体可能出现在多个关系中。",
"schema": {
"source_entity": "源实体名称",
"source_type":"以下类型之一:[\"算法\",\"模块\",\"指标\"]。算法: 为了达到特定的目标或者实现特定的需求,形成的一种可复现的、对于知识的应用的非物质实体。模块: 系统是一组相互关联或相互作用的部分,构成复杂整体的实体(或成熟的软件、程序或系统)。指标: 衡量性能的名词,如速度、准确率等。",
"target_entity": "目标实体名称",
"target_type":"以下类型之一:[\"算法\",\"模块\",\"指标\"]",
"type": "关系类型,以下类型之一:[\"包含\", \"应用\" ,\"子类\", \"度量\"]。包含关系: 一个技术系统或过程中包括另一个技术作为其不可分割的一部分的组成性质的关系。应用关系: 一个技术为了实现特定的功能或解决特定的问题而使用或依赖另一个技术的功能性使用关系。子类关系: 一个技术是从另一个更广泛或更基础的技术演化或衍生出来的,保留了原始技术的某些核心特征同时又有所创新的关系。度量关系: 使用指标来衡量和评估一个技术的性能或效能的关系。"
},
"input": user_input
}
如果只用这个instruction,就会输出一条很乱的东西:
{"源_实体": "大涡模拟方法", "源_type": "算法", "目标_entity": "冲击射流的噪声特性", "type": "数值模拟"}
我怀疑是它把schema里的东西当成一个单独的实体了。
更改思路
这次我只用prompt,并且把一句话里的多个关系作为例子,这样能帮助模型更好地认识到并不是只有一条数据。
\- 示例(以下示例仅告知你输出格式,你在输出时请把user_input作为输入文档)
\- 文档内容
目标检测的准确率是评估侦察目标性能的重要指标之一。论文提出了基于faster r-cnn的无人机侦察目标检测方法,对模型的rpn模块、多任务损失函数、卷积特征共享等算法进行了分析和研究,选取油库、舰艇、立交桥、飞机等四种典型目标,以faster r-cnn为基准模型进行训练和测试,模型平均准确率为89.47%,目标检测准确率高。
\- 输出
{
"spo_list": [
{"source_entity": "无人机侦察目标检测方法","target_entity": "基于Faster R-CNN的无人机侦察目标检测方法","source_type":"算法","target_type":"算法","type": "子类","description": "基于Faster R-CNN的无人机侦察目标检测方法是无人机侦察目标检测方法的具体实现形式,专门针对无人机平台优化,展示了该领域内算法的特化和发展。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "Faster R-CNN","type": "包含","description": "Faster R-CNN算法是构成基于Faster R-CNN的无人机侦察目标检测方法的核心部分,提供了高效的目标检测和识别能力。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "RPN模块","type": "应用","description": "RPN模块在基于Faster R-CNN的无人机侦察目标检测方法中被应用以生成区域提议,这是连接区域选择与目标分类的关键步骤。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "多任务损失函数","type": "包含","description": "多任务损失函数在该方法中被包含用以同时优化目标分类与边框回归,增强了模型在处理多目标检测时的准确性和效率。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "卷积特征共享","type": "包含","description": "卷积特征共享技术在此方法中被包含,通过共享卷积层的特征计算减少了模型的运算量和提升处理速度,使得实时处理成为可能。"
},
{"source_entity": "基于Faster R-CNN的无人机侦察目标检测方法","target_entity": "模型平均准确率","type": "度量","description": "模型平均准确率作为评价基于Faster R-CNN的无人机侦察目标检测方法性能的指标,直接反映了该方法在实际应用中的效果和可靠性。"
}
]
}
在instruction dict中,我只保留了拼接input的部分:
\# 构建指令
instruction_dict = {
\# "instruction": "你是专门进行实体及其关系抽取的专家。请从input文本中识别出所有符合定义的关系。",
"instruction": system_prompt,
"input": user_input
}
这样运行之后可以看到模型已经能输出多条结果,但是很多字段它识别不出来:
{"spo_list":
[
{
"source_entity": "基于大涡模拟方法",
"target_entity": "Smagorinsky-Lilly(SL)模型",
"source_type": "无",
"target_type": "无",
"type": "度量",
"description": "模拟准确性较差"
},
{
"source_entity": "基于大涡模拟方法",
"target_entity": "Wall-Adapting Local Eddy-Viscosity(WALE)模型",
"source_type": "无",
"target_type": "无",
"type": "度量",
"description": "模拟结果与实验数据对比分析"
},
{
"source_entity": "基于大涡模拟方法",
"target_entity": "Kinetic-Energy Transport(KET)模型",
"source_type": "无",
"target_type": "无",
"type": "度量",
"description": "能够更加准确地模拟噪声频谱"
},
{
"source_entity": "基于大涡模拟方法",
"target_entity": "WALE模型",
"source_type": "无",
"target_type": "无",
"type": "度量",
"description": "对高频段噪声频谱的模拟误差偏大"
}
]
}
第三次尝试
我发现官方有给提取关系时的数据该怎么写RE,但是我看了半天发现它们是用来做训练的:
这四种数据都是训练流程需要的,似乎并不能直接拿来用。
仿照示例写了一个instruction(实体和关系的识别规则写在prompt里了):
"{\"instruction\": \"You are an expert in named entity recognition. Please extract entities that match the schema definition from the input. Don't return an empty list. Please respond in the format of a JSON string.\", \"schema\": [\"source_entity\", \"target_entity\", \"source_type\", \"target_type\", \"type\", \"description\"], \"input\": \"基于大涡模拟方法,采用三种亚格子模型对冲击射流的噪声特性进行了数值模拟,将模拟结果与实验数据对比分析,结果表明:Smagorinsky-Lilly(SL)模型数值耗散较大,模拟准确性较差;Wall-Adapting Local Eddy-Viscosity(WALE)模型和Kinetic-Energy Transport(KET)模型对总声压级的模拟结果比较准确;KET模型能够更加准确地模拟噪声频谱,而WALE模型对高频段噪声频谱的模拟误差偏大.最后,使用KET模型计算了噪声监测点在不同位置时冲击射流的总声压级,得到了不同冲击距离下的噪声指向分布情况.为了分析饱和土中浮承桩纵向振动特性,基于Biot理论提出了一种桩底饱和虚土桩模型.采用Novak薄层法计算得出饱和土体位移解,利用饱和土-桩-饱和虚土桩完全耦合条件,推导出桩顶纵向振动动力阻抗解析解,并对饱和土中浮承桩纵向振动特性进行参数化分析.计算结果表明,当桩底饱和土层厚度为4和6 m时,桩顶动力阻抗函数曲线呈现出大、小峰值交替现象,这与单相虚土桩模型的计算结果差异较大,且桩周和桩底饱和土体孔隙率对桩顶动力阻抗曲线的影响不可忽视.当桩底土饱和性显著且排水性较差时,桩底土单相虚土桩模型会引起较大误差,宜采用饱和虚土桩模型和所得相关解析解答分析浮承桩纵向振动特性.\"}"
但是效果仍然不好:
{"算法": [], "模块": [], "指标": [], "关系类型": [], "源类型": [], "目标类型": [], "type": ["模型"], "描述": ["Smagorinsky-Lilly(SL)模型", "Wall-Adapting Local Eddy-Viscosity(WALE)模型", "Kinetic-Energy Transport(KET)模型", "KET模型", "WALE模型", "桩底饱和虚土桩模型", "Novak薄层法", "饱和土-桩-饱和虚土桩完全耦合条件"]}
坏结果分析
我仔细看了他给的训练和测试数据,我发现这个模型好像只能找出他给的那几种实体和关系,等于把输入的一句话映射到目标列表上。但是怎么让它识别自定义的目标列表呢?我一开始想的是在prompt或instruction里告诉它怎么识别,但是这样不行,它会直接输出空列表。之后我觉得可能这种比较复杂的事是需要训练的,等于说我需要先把一些提取好关系的数据按它的规则做成训练数据,训练好后才能喂生数据。
如果要做训练就麻烦了,人工标的数据去训练也不一定效果有多好。我先问问组里还打不打算用它,要我说换Qwen或者什么别的算了。
2024年9月25日18:50:51
2024年9月28日20:14:32
实验结果
最终经讨论发现这个模型在未经训练微调的情况下几乎无法胜任这项工作。我们打算彻底放弃这个模型,转而用Qwen去做关系抽取。