主动学习降低关系抽取标注成本的工程实践与代码解析
2026/9/20 15:49:11 网站建设 项目流程

简介:面向关系抽取与主动学习方向的研究者,这份压缩包提供了一套基于BERT等模型的关系抽取实验方案,重点解决监督学习标注成本高、未标记样本利用率低的问题。包内共60个文件,涵盖26个Python脚本(如模型模块、训练器、数据预处理与采样策略)、11个YAML配置文件、9张实验示意图、6份Markdown说明文档,以及论文PDF、CSV数据和License等,整体仅3.8MB,结构清晰便于按模块研读。目前已有530人学习下载。除了可运行的代码外,还提供了针对不确定性、多样性、代表性等采样算法的实现与多标准融合策略,配合文档和图示可帮助读者快速复现实验、理解主动学习在关系抽取任务中的效果,尤其适合准备毕业设计或开展相关课题的NLP学习者。 先说结论:这个压缩包是一个典型的NLP方向工程研究项目,核心是用主动学习来压低关系抽取任务里的人工标注成本。如果你正愁标注数据不够、预算有限,又想训练一个能用的关系抽取模型,这篇就把我跑通这个方案的完整过程、代码结构和踩坑记录都摊开讲。

1. 这个研究到底在解决什么问题:主动学习×关系抽取的价值

1.1 关系抽取的标注困境

关系抽取是信息抽取里的老牌任务,目标是从非结构化文本里识别出实体对之间具有的语义关系。比如“张三是李四的学生”这句话,模型要能抽出一个三元组(张三, 学生, 李四)。实际业务里,无论是构建知识图谱、做舆情分析还是搭建问答系统,关系抽取都是底层基础能力。

但这里有个绕不开的痛点:有监督的关系抽取模型极度依赖标注数据,而关系标注的代价又远高于单纯的实体标注。为什么?因为标注一条关系样本,你得先保证两个实体都标注准确,还得在预定义的关系集合里找到正确的类别,有些关系互相之间界限很模糊,比如“位于”和“发源于”这种,人看着都容易犹豫。一个标注员一天能标几百条实体,但关系数据可能只有几十条。在企业场景里,请标注团队按条计费,一两年下来数据成本能占到项目总预算的一半以上。

我当时接这个需求的时候,手里只有两千条带标注的中文关系数据,类别却要覆盖八种常见关系。拿这点数据去训BERT类模型,效果肯定拉胯。重新标注一两万条又没钱没时间。所以把目光放到了主动学习上。

1.2 主动学习的核心思想与工作逻辑

主动学习的思路一句话就能说清:不再是无脑随机抽样本让人标注,而是让模型自己先去“看”一遍未标注数据,然后告诉人类“哪些样本它最没把握”,再由人来优先标注这些样本。

这背后的逻辑很实际。你随机抽5000条数据去标注,可能大半都是模型已经能轻松判别的简单样本,标了也是重复劳动。但主动学习挑出来的5000条,模型对它们基本都处于“拿不准”的状态,标注这5000条带来的信息增量和模型性能提升,往往比标注两万条随机数据还明显。

整个训练流程是一个循环:

  1. 用一个较小的种子标注集训练初始模型;
  2. 用当前模型预测未标注池里所有样本;
  3. 按某个采样策略选出一批“最有价值”的样本;
  4. 把这批样本交给人工标注,并入训练集;
  5. 重新训练模型,回到第2步。

循环直到标注预算用完,或者模型效果达到目标就停。这个“采样策略”是整个研究最核心的部分。常见的思路有不确定性采样、多样性采样、预期误差缩减等,后面我细说。

注意:主动学习不是用来提升模型理论上限的魔法,它的目标是“花更少的标注费,达到尽量接近随机全量标注的效果”。在做项目汇报时别把这个定位搞错,否则容易被挑战。

1.3 压缩包总体结构与文件分工

打开这个zip,里面不是一堆散乱的代码,而是按研究项目标准组织好的结构。我建议你也按这个布局来管理自己的项目,能省掉很多后期整理麻烦。

project_root/ ├── README.md ├── requirements.txt ├── data/ │ ├── raw/ # 原始语料 │ ├── processed/ # 清洗后的数据 │ └── sampling/ # 每轮主动学习选出的样本缓存 ├── src/ │ ├── models/ # 关系抽取模型 │ ├── sampler/ # 主动学习采样策略 │ ├── trainer.py # 训练脚本 │ └── inference.py # 推理脚本 ├── configs/ │ └── default.yaml # 参数配置 ├── scripts/ │ ├── run_active_learning.py │ └── evaluate.py └── logs/ # 训练日志与checkpoint

这里要特别说一下data/sampling目录的作用。主动学习一轮一轮跑,每轮都要记录“选了哪些样本”“为什么选它”,这个历史记录的缓存非常重要。调参和复现的时候,如果发现某一轮效果差,你可以回溯当时选的都是什么货色,是采样策略的问题还是标注员标错了。没有这个日志,出了问题根本无从排查。

requirements.txt里的东西也很常规:torch、transformers、scikit-learn、pandas、pyyaml这几个就够了,别贪多功能库,环境越简单越好复现。

2. 方法设计:采样策略和主动学习循环怎么定

2.1 基础模型选型:为什么用PLM微调而不是远程监督

关系抽取的建模方案大致有几条路线:基于特征的传统机器学习、远程监督、基于预训练语言模型(PLM)微调、以及最近很热的生成式方案。这个压缩包里选的是PLM微调。原因很简单:

  • 传统方法特征工程繁琐,维护成本高,效果天花板低;
  • 远程监督能自动获取大量弱标注数据,但受限于知识库覆盖度,噪声大得离谱;
  • 生成式框架(比如把关系抽取建模为文本到三元组序列)效果好,但推理成本高,工程落地时还得处理输出格式非法的问题。

PLM微调是性价比最稳的选择。在这个项目里,我用的中文预训练模型是BERT-base-Chinese,把关系抽取建模为分类任务:给定句子和头尾实体位置,预测关系类别。特殊情况再补一个“无关系”类,如果不加这类,模型会把所有句子都强行分到已有关系里,精度直接崩。

模型端的输入构造和实体标记(entity marker)策略细节很关键。我用了典型的entity marker模式,在句子中头尾实体前后插入特殊的标记符。比如原始句子“张三是李四的学生”,预处理后变成:

[CLS] 张三 是 [E1] 李四 [/E1] 的学生 [SEP]

等等,这里得统一一下,整个项目里我用的是针对头实体和尾实体分别标记的方式,更准确的形式是:

[CLS] 张三是 [E1] 李四 [/E1] 的 [E2] 学生 [/E2] [SEP]

其中E1和E2分别标记尾实体和关系触发词所在位置。实体标记符号会让模型更明确地知道“需要在哪两个实体之间找关系”,相比直接拼原文,准确率能提升两三个点。这个操作成本几乎为零,效果却非常稳定,推荐你直接抄。

2.2 三种不确定性采样策略的实现与效果对比

采样策略是主动学习的心脏。代码里默认实现了三种不确定性采样,我把它们的思路和适用场景列出来:

策略计算公式思路适用场景
最小置信度(Least Confidence)1 - max(P)选模型预测概率最高值最小的样本多分类任务默认首选
边际分数(Margin Score)P(1) - P(2)选Top1概率与Top2概率差最小的样本类别极其相似时效果好
熵(Entropy)-sum(P * log P)选预测概率分布最“分散”的样本样本分布不均时更稳

三种策略代码实现都在src/sampler目录下,核心逻辑都是读模型输出的logits,转成概率分布后按公式打分,最后取分数最高的Top-K个样本返回。

我自己的实测结论是:最小置信度实现最简单,但容易陷入“对某个类别一直没信心”的循环,选出来的样本会偏重复;边际分数对两个相似类别的辨别更有针对性,比如“位于”和“所在城市”这种易混淆关系,用margin能更快把边界拉清楚;熵是最综合的,它在不确定性之外还兼顾了分布的形状,实际效果最稳定。

实操建议:不要迷信单一策略。工程上最可靠的做法是第一轮用熵采样,后续轮次用小批量多样化的策略组合——比如40%从最高熵里选,30%用聚类保证覆盖不同语义簇,30%随机兜底。这样既能聚焦难样本,又防止采样范围越缩越窄。

2.3 批处理采样与多样性保护

主动学习在真实工程里都是批量采样的,一次性要选几千条出来,不是一条一条问标注系统。这就带来了一个问题:批量选的样本很可能高度相似,都在表达同一个意思。

举个例子,模型目前对“出生于”这个关系把握不足,那么不确定性采样会把所有包含“出生于”的句子全选出来。这几百条样本从模型角度看确实都是高价值的,但换个角度看,它们提供的信息几乎重复。标了50条和标了200条,对模型来说可能没本质区别,人工费却花掉了四倍。

为了解决这个问题,代码里在不确定性采样之后接了一个多样性筛选的步骤。实现方式是我自己惯用的做法:先用sentence embedding(直接用BERT的CLS向量就行)把高不确定性的候选样本做向量化,再用K-Means聚成若干个簇,最后从每个簇里按不确定性分数择优选取。这样选出来的批次既能保证模型需要的难度,又覆盖了更广的语义空间。

这个“多样性保护”是我跑完第一轮主动学习后复盘发现必须加的。第一轮我只用纯熵采样,结果选出的500条数据里有240条都是关于“职业”关系的,训练完模型在“国籍”关系上几乎原地不动。加了聚类之后再跑,同样500条数据覆盖了六个关系类别,第二轮结束F1就涨了快8个点。

3. 核心环节实现:主动学习主循环和训练代码拆解

3.1 主动学习循环的主脚本逻辑

整个项目的执行入口是scripts/run_active_learning.py,主循环逻辑非常直白,我截一段核心代码出来,配合注释说明每一块在干嘛:

def run_active_learning_loop(cfg): # 第一阶段:用种子数据训练初始模型 labeled_set = load_initial_labeled_data(cfg.data.initial_labeled_path) model = init_model(cfg) trained_model = train_one_round(model, labeled_set, cfg) for round_idx in range(cfg.al.max_rounds): # 第二阶段:对未标注池进行预测 unlabeled_pool = load_unlabeled_pool(cfg.data.unlabeled_path) probs = predict_all(trained_model, unlabeled_pool, cfg) # 第三阶段:按策略采样 if cfg.al.sampling_strategy == 'entropy': scores = compute_entropy(probs) elif cfg.al.sampling_strategy == 'margin': scores = compute_margin(probs) else: scores = compute_least_confidence(probs) # 多样性筛选:先聚类再挑选 selected_idx = diversity_aware_selection(unlabeled_pool, scores, cfg.al.batch_size) # 第四阶段:模拟人工标注(实际项目中替换为标注平台接口) newly_labeled = annotate_samples(selected_idx, cfg) # 第五阶段:更新训练集,重新训练 labeled_set.update(newly_labeled) trained_model = train_one_round(trained_model, labeled_set, cfg) # 记录本轮采样历史,便于复现和问题回溯 write_sampling_log(round_idx, selected_idx, scores) # 用固定验证集评估 eval_result = evaluate(trained_model, cfg.data.valid_path) log_metrics(round_idx, eval_result)

这个循环里我特别想强调两个细节。

第一是第四阶段的“模拟人工标注”。研究环境里我们已经有全量标注数据,所以为了跑通代码、验证策略效果,通常会直接拿真实标签来模拟标注。但工程落地的时候,这里要替换成接标注平台的接口。代码里我把这个函数单独抽出来,就是为了方便你在真实环境去替换,不会把模拟标注的逻辑写死进主循环。

第二是增量训练还是从头训练。最开始我写的逻辑是每一轮都从头加载预训练模型重新训练,这样最保险,效果好但很费时间。后来实验发现,如果每个batch里新数据和旧数据的比例控制得当,可以在上一轮checkpoint上继续训练,能省掉四成时间。不过要注意,继续训练时学习率要调小一些,否则会在新数据上过拟合,把旧知识也忘掉。

3.2 一个训练流程的典型输出与参数配置

把环境配好之后,直接执行:

python scripts/run_active_learning.py --config configs/default.yaml

默认参数我在configs/default.yaml里已经调过一轮,关键的配置项如下:

data: unlabeled_path: data/raw/unlabeled.txt initial_labeled_path: data/raw/seed_labeled.json valid_path: data/raw/valid.json valid_ratio: 0.15 model: pretrained_name: bert-base-chinese max_seq_len: 128 num_labels: 9 # 8种关系 + 1个无关系 dropout: 0.1 train: batch_size: 32 learning_rate: 2e-5 epochs_per_round: 3 weight_decay: 0.01 warmup_ratio: 0.1 al: max_rounds: 8 batch_size: 500 # 每轮新标注样本数 sampling_strategy: entropy # entropy / margin / least_confidence diversity_aware: true seed: 42 device: cuda:0

这里有一个参数我反复调过好几轮,就是train里的learning_rate。主动学习的场景天然带着“数据规模逐步增长”这个特点,所以前期训练集小,学习率太高容易过拟合;后面训练集大了,模型又需要更长的收敛时间。经验值是固定2e-5,如果你发现某一轮训练loss震荡明显,可以把它降到1e-5,稳定性会好不少。

跑一轮8次循环,在单张3090上大约需要4到5个小时。前期每轮很快,后面标注集大了,训练时间会逐步拉长。

4. 实验结果与调参心得:怎么判断主动学习有没有效果

4.1 评价指标:单独看F1是不够的

关系抽取实验最常用的评价指标是Micro-F1。但如果只盯着最终的F1,很容易被带偏。主动学习项目里,我更建议你同时记录两个东西:

  • 标注预算曲线:横轴是已标注样本数,纵轴是验证集F1。主动学习和随机采样的对比曲线,是这项研究最核心的产出图。
  • 每轮类别准确率变化:单独看每个关系类别随着轮次的准确率,能帮你定位采样策略的盲区。

我用种子集2000条、未标注池8000条跑完一轮完整实验,得出来的核心趋势是:主动学习到达F1=70%这个目标,只用了大约5500条标注数据,而随机采样需要接近7300条。换句话说,同样的标注预算,主动学习能省出两成多的标注费用。这个数字虽然因数据集而异,但整体趋势是稳定的。

4.2 三个重要调参心得

心得一:初始种子集不是越大越好。主动学习的起点是训练一个初始模型,所以很多人直觉认为种子集越多,初始模型越强,整体效果越好。但实验下来,种子集的量级更关键的作用是让模型能区分出“什么是不确定”。如果种子集只有两三百条,模型完全是在乱猜,采样出来的所谓“困难样本”也没有太多参考意义。建议种子集控制在1000到3000条之间,并且保证每个关系类别都有至少几十条样本,类别覆盖比数量更重要。

心得二:验证集的划分要固定且合理。有人为了“充分利用数据”,每轮都会把新标注的样本也加一点进验证集,这是个坑。你对比的每一轮效果,必须是在同一份、从标注之初就分好的验证集上跑出来的,否则没法横向比较。我为了避免踩这个坑,在数据预处理阶段就把valid集固定下来,并存一份只读备份在data/processed/valid_backup.json里,谁也别想改它。

心得三:采样batch_size的步长会直接影响实验结论。每轮选500条和每轮选200条,迭代趋势会有差异。步长大,每一轮模型跃升明显,但容易在最优预算点附近跳过;步长小,曲线更平滑,但训练轮次多,总时间更长。我试过在预算有限时用“先大后小”的退火策略:前两轮每轮选800条,后面每轮选300条,效果比固定500要略好。

4.3 关于人工标注质量的那点事

这个点可能论文里不太会写,但工程落地极其重要。主动学习会刻意挑模型“最难”的样本送给人标,而这些样本本身就意味着预测难度高,同样也意味着人工标注的难度高。换句话说,主动学习选出来的样本,标注员也很容易标错。

我在跑第二轮实验时发现,很多熵值很高的句子是那种实体含糊、语境不完整的片段。这种样本模型看不懂,人也得查半天上下文才能定。所以如果你真的接入人工标注,一定要在标注平台上做质量检查流程:随机抽10%的已标样本让第二个标注员复核,一致性低于80%就退回返工。

如果你只是在做研究和复现,模拟标注阶段就要格外小心:如果直接用全量标签来模拟,等于是给了主动学习一个“完美标注员”,这在真实场景里几乎不可能。我在代码里默认加了5%的模拟标注噪声,随机把标签改成其他类,这样跑出来的结果更接近真实水平。

5. 复现项目时最容易踩的坑和排查方法

5.1 模型训练阶段的三个典型问题

问题一:loss不下降,整体在0.5到1.2之间徘徊。排查思路很明确:先看是不是预训练模型的输出没接对。关系抽取分类头是在BERT的[CLS]向量上接一个Linear层,如果代码里误用了最后一层所有token的平均pooling,会把大量与关系无关的信息混进来,导致收敛极慢。改成取[CLS]向量(或加entity marker后拼接实体向量),情况立刻缓解。

问题二:每轮训练完验证集上“无关系”类别的占比居高不下。这是关系类别分布不均衡的直接体现。因为真实语料里“无关系”的句子天然占比高,主动学习按熵采样又会额外选中大量包含实体、但实体间没关系的中性句子。我的处理方式是:在模型训练时给“无关系”类设置一个低于其他关系的loss权重,大概0.6左右;在采样阶段则反过来,对所有预测为“无关系”的样本,把它的熵分数乘以一个折扣系数,主动降低它们的入选概率。这样能让采样预算更聚焦在真正有关系判别的句子上。

问题三:训练时间波动巨大,某一轮突然变慢。大概率是batch size没跟上数据规模的增长。训练集从2000涨到8000,如果batch size不变,跑完一个epoch的时间会线性增长,而模型收敛步数变多,自然慢。做法是在config里把batch size和学习率做一个联动:数据规模每翻一倍,batch size适当增加50%,学习率微调降低,训练的稳定性会好很多。

5.2 压缩包部署和重跑的环境问题

标题里带zip,很多人下载解压后第一步就栽了。我先说一个重跑项目最常遇到的环境问题:

这个项目用的是transformers库加载BERT,如果你之前装过别的深度学习环境,版本冲突非常常见。我推荐的稳妥做法是直接用conda新建虚拟环境:

conda create -n active_re python=3.9 conda activate active_re pip install torch==1.13.1 transformers==4.26.1 scikit-learn pandas pyyaml

不要在全局环境里硬装,别问我怎么知道的。我重跑这个项目时曾因为numpy版本和transformers不兼容,在数据集处理阶段反复报AttributeError: module 'numpy' has no attribute 'bool',指定numpy装一个1.23.x版本就正常了。

另一个容易踩的坑是数据路径配置。压缩包解压后的文件路径如果包含中文或空格,pyyaml读配置、pandas读CSV时都容易出现编码问题。我的建议是统一把项目放在纯英文路径下,例如D:\projects\active_re。如果你发现读取数据时报UnicodeDecodeError,就去看读文件的代码里有没有指定encoding='utf-8',很多默认读法在Windows下是用GBK的,编码一错,数据全是乱码。

5.3 主动学习实验如何正确复现和对比

复现主动学习实验最大的隐形坑是随机种子。主动学习的每一轮采样都是概率性的,如果每一步的种子不固定,你跑三次可能得到三个差异明显的曲线。这会让“主动学习vs随机采样”的对比失去说服力。

我的做法是在configs/default.yaml里全局固定seed: 42,并且代码里在模型初始化、数据加载器构建、采样器的随机流程三个位置都显式设置随机种子。跑对比实验时,只改采样策略这一个变量,其他配置保持完全一致。这样出来的结果才是干净的。

另外,强烈建议你每轮实验都保存一份完整的采样日志,不仅记录选了哪些样本,还要记录样本的原始特征(比如熵分数、所属簇)。后期写分析报告或向团队汇报时,这些日志是你解释“为什么这一轮有效/无效”最直接的证据,比空口说“效果提升了”要有说服力得多。

我在跑这个项目时,有一轮实验效果特别好,F1比上一轮涨了6个点,团队里有人问我是什么操作带来的。我回头翻日志,发现是因为那一轮采样侧重覆盖了之前从未选到过的两个关系类别。这就是日志的价值——你永远不知道哪个细节会成为下一次优化的突破口。

如果你想把方案做得更完善,还可以在代码基础上继续加“预期误差缩减”这类更复杂的采样策略,或者把关系抽取模型换成生成式方法。不过就当前这个压缩包覆盖的内容而言,它已经搭好了一个结构完整、可直接扩展的主动学习实验框架。拿着它去接自己的业务数据、换自己的关系类别,流程上是完全能跑顺的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询