共计 2209 个字符,预计需要花费 6 分钟才能阅读完成。
低效标注的血泪教训
去年参与某三甲医院电子病历项目时,我们曾因标注不规范导致模型性能下降 40%。标注人员将『Ⅱ型糖尿病』时而标注为疾病实体,时而拆分成『Ⅱ型』和『糖尿病』,最终 F1 值仅 0.52。更糟糕的是某基因突变检测项目,因未统一标注 BRCA1 基因的别名(如 BRCA1_HUMAN),召回率直接归零。

工具选型:为什么选择 brat
- 生物医学特化:原生支持实体嵌套(如『HER2 阳性乳腺癌』中的基因与疾病)
- 关系可视化:通过箭头直观显示『药物 - 不良反应』等复杂关系
- 轻量化部署:单机运行无需 SaaS 订阅费(对比 Prodigy 每年 $490/ 人)
pie
title 生物医学标注工具使用占比
"brat" : 68
"Label Studio" : 22
"Prodigy" : 10
核心原理:standoff 标注详解
- 文本与标注分离:原始文本存为.txt,标注数据存为.ann
- 坐标定位:T1 Disease 25 32 肺癌 表示从第 25 到 32 字符的文本
- 关系定义:R1 Cause Arg1:T1 Arg2:T2 建立实体间因果关系
规范编写黄金法则
# annotation.conf 关键配置示例
[entities]
# 必须定义允许嵌套的实体
Gene
Disease
[relations]
# 生物医学典型关系
Cause|Arg1:Gene, Arg2:Disease
Treatment|Arg1:Drug, Arg2:Disease
[events]
# 临床事件定义
Diagnosis|Theme:Disease
Python 交互实战
import requests
from typing import List, Dict
class BratClient:
def __init__(self, base_url: str = "http://localhost:8001"):
self.session = requests.Session()
self.base_url = base_url
def get_annotations(self, doc_id: str) -> Dict:
try:
res = self.session.get(f"{self.base_url}/data/{doc_id}.ann",
timeout=10
)
res.raise_for_status()
return self._parse_ann(res.text)
except requests.exceptions.RequestException as e:
print(f"API 请求失败: {str(e)}")
return {}
@staticmethod
def _parse_ann(ann_text: str) -> Dict:
"""解析标准 brat .ann 格式"""
entities = []
for line in ann_text.split('\n'):
if not line.startswith('T'):
continue
parts = line.split('\t')
ent_id = parts[0]
type_span = parts[1].split()
entities.append({
"id": ent_id,
"type": type_span[0],
"start": int(type_span[1]),
"end": int(type_span[2]),
"text": parts[2]
})
return {"entities": entities}
性能优化双刃剑
内存管理方案
- 分块加载:每处理 5000 字符自动保存并清空内存
- 懒加载:仅当标注者查看具体文档时才加载对应.ann 文件
多标注者协作
- Git 分支策略:每个标注者在独立分支工作
- 冲突检测 :定期执行
git diff --check查找重叠标注
避坑指南
实体嵌套三大陷阱
- 跨层级嵌套:『EGFR 突变型肺癌』应标记为:
T1 Gene 0 4 EGFR T2 Disease 0 9 EGFR 突变型肺癌 - 部分重叠:『BRAF V600E 突变』中『V600E』需单独标注为蛋白质变异体
- 术语变异:『赫赛汀(曲妥珠单抗)』需建立同义词链接
BERT 对齐秘诀
# 处理 BERT 分词导致的 offset 错位
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
def align_offsets(text: str, start: int, end: int) -> tuple:
pre_text = text[:start]
tokenized = tokenizer.tokenize(pre_text + text[start:end])
new_start = len(tokenizer.tokenize(pre_text))
return (new_start, new_start + len(tokenizer.tokenize(text[start:end])))
开放问题
- 如何量化标注者间一致性?Krippendorff’s alpha 是否适合医学文本?
- 当标注指南需要频繁更新时,如何保证历史标注仍然可用?
- 对于『可能』『疑似』等不确定表述,应该建立独立标签还是通过置信度表示?
经过三个月实战,我们的标注效率从每天 200 条提升到 650 条,关键是严格执行了本文的嵌套实体规范和定期交叉校验。特别提醒:brat 的标注界面在 Chrome 114+ 版本存在渲染 bug,建议使用 Firefox 进行标注作业。
正文完
