最近我在尝试复现DeepSeek团队发布的R1模型在信息抽取任务上的表现,特别是零样本的文本到图结构(text-to-graph)抽取能力。这个任务的核心是:给定一组预定义的实体类型和关系类型,从目标文本中抽取出所有符合要求的实体以及它们之间的关系。举个例子,对于下面这段文本:
"微软CEO萨提亚·纳德拉宣布了Azure AI的最新进展"
理想的信息抽取结果应该是这样的JSON结构:
json复制{
"entities": [
{"id": 0, "text": "微软", "type": "公司"},
{"id": 1, "text": "萨提亚·纳德拉", "type": "人物"},
{"id": 2, "text": "Azure AI", "type": "产品"}
],
"relations": [
{"head": "萨提亚·纳德拉", "tail": "微软", "type": "CEO"},
{"head": "微软", "tail": "Azure AI", "type": "开发"}
]
}
这个任务看似简单,但对于小型生成式语言模型来说却充满挑战。当不限制输出格式,让模型自由抽取所有可能的实体和关系时,语言模型表现尚可。但一旦要求模型严格按照预定义的实体和关系类型输出结构化结果,就变成了一个真正的噩梦。
在我的实验中,发现用监督学习的方式训练小型语言模型完成这种条件式文本到图的转换非常困难。主要原因包括:
输出空间爆炸:结构化输出需要考虑实体识别、关系抽取以及它们之间的对应关系,这使得输出空间呈指数级增长。
错误传播:一个实体的识别错误会导致后续所有相关关系的错误,这种级联效应使得模型训练难以收敛。
格式约束:要求模型严格遵循指定的JSON格式输出,这对小型语言模型来说是个额外的认知负担。
实际经验:在早期实验中,模型经常产生格式错误的JSON,或者在应该输出关系时却重复输出实体。这表明模型在理解任务要求方面存在困难。
与传统监督学习不同,强化学习(Reinforcement Learning)不直接告诉模型应该采取哪些具体行动(即生成哪些token),而是通过奖励机制引导模型朝着期望的目标发展。在我们的场景中:
强化学习的优势在于:
允许试错:模型可以探索不同的生成路径,而不仅限于模仿训练数据。
灵活的目标设定:可以针对不同子任务(如实体识别、关系抽取)设置不同的奖励权重。
发现新策略:模型可能自主发展出训练数据中不存在的有效推理策略。
DeepSeek团队采用了Group Relative Policy Optimization(GRPO)方法进行强化学习训练。这个算法的核心思想可以概括为:
数学表达式简化版:
code复制L(θ) = E[log πθ(a|s) * A(s,a)] - β KL(πθ||πref)
其中:
隐式负样本学习:由于模型会生成多个候选方案,那些获得低奖励的方案自然成为了"困难负样本"。
知识涌现:正如Andrej Karpathy指出的,模型可能发展出标注者无法预见的推理策略。
多目标平衡:可以灵活调整不同奖励项的权重,针对模型弱点进行专项优化。
我们设计了包含三个关键阶段的训练流程:
合成数据生成
监督训练
强化学习训练(GRPO)
实战技巧:我们发现F1奖励的权重应该设置得最高,因为模型容易陷入只生成格式正确但内容空洞的小JSON的局部最优。
我们的奖励函数由三个关键部分组成:
格式奖励(R_format):
JSON奖励(R_json):
F1奖励(R_f1):
奖励总和:
code复制R_total = 0.2*R_format + 0.3*R_json + 0.5*R_f1
我们基于Qwen2.5-0.5B模型进行微调,主要考虑:
文本预处理:
实体链接:
关系验证:
课程学习:
动态权重调整:
记忆库采样:
我们提供了开箱即用的推理代码:
python复制from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Ihor/Text2Graph-R1-Qwen2.5-0.5b"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
text = """输入文本放在这里..."""
prompt = "分析这段文本,识别实体并按照给定指令提取关系:{}"
messages = [
{
"role": "system",
"content": ("你是一个经过训练的处理文本并提取命名实体和关系的助手..."
"输出格式为JSON,结构如下:\n\n"
'{"entities": [{"type": "实体类型", "text": "实体文本", "id": 编号}], '
'"relations": [{"head": "头实体", "tail": "尾实体", "type": "关系类型"}]}')
},
{"role": "user", "content": prompt.format(text)}
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
generated_ids = model.generate(
**model_inputs,
max_new_tokens=512
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids
in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
在自建的测试集上,模型表现如下:
| 指标 | 监督学习 | GRPO训练后 |
|---|---|---|
| 实体F1 | 0.62 | 0.78 |
| 关系F1 | 0.41 | 0.65 |
| JSON格式准确率 | 85% | 98% |
| 推理速度(tokens/s) | 120 | 110 |
问题:模型有时会遗漏明显实体
问题:关系方向错误(如把A→B写成B→A)
问题:输出JSON格式错误
经过这次复现实验,我深刻体会到:
小模型也有大潜力:通过精心设计的训练流程,小型语言模型也能完成复杂的结构化抽取任务。
奖励设计是关键:如何平衡格式正确性和内容准确性是需要反复调试的艺术。
数据质量大于数量:1000个高质量样本比10000个噪声数据更有价值。
未来计划尝试:
这个项目的完整代码和数据集已经开源,欢迎社区共同改进。在实践中我发现,最有效的改进往往来自于对失败案例的深入分析,而不是盲目增加数据或参数。