Doccano是开源的文本标注工具,支持序列标注、文本分类、关系抽取和序列到序列标注等多种任务类型。它提供多人协作标注、进度追踪和质量评估功能,通过Web界面降低标注操作门槛,是中小团队构建标注数据集的实用选择。在数据驱动的AI系统中,该方案对保障数据质量和模型可靠性具有重要作用。

岗位关联

需要学习Doccano文本标注,Doccano是开源的文本标注工具,支持序列标注、文本分类、关系抽取和序列到序列标注等多种任务类型。Doccano文本标注属于数据标注与管理 > 标注策略的关键内容,掌握后可更好地衔接后续章节。

目录
  1. 可视化演示
  2. Doccano工具定位与核心任务类型支持
  3. 序列标注与关系抽取的标注模式设计
  4. 多人协作标注与角色权限管理
  5. 标注进度追踪与质量评估机制
  6. 数据导入导出格式与外部集成
  7. 部署配置与生产环境最佳实践
  8. AI 应用场景
  9. Python 演示
  10. 常见问答
  11. 常见误区
  12. 参考来源

可视化演示

展示Doccano标注工具的四种标注类型、多人协作标注流程与质量评估机制,通过动画理解文本标注数据集构建的核心环节。

本节配有交互动效(进入视口后自动加载,约 3KB)。

标注类型:四种任务类型可视化 序列标注NER:对文本中实体进行BIO标签标注 文本分类/关系抽取/序列到序列多种任务支持 标注精度 = 正确标注数 / 总标注数 Doccano支持多种标注任务类型,覆盖NLP核心需求。

Doccano工具定位与核心任务类型支持

Doccano 是一款开源的文本标注工具,旨在为中小团队提供无需自研标注系统即可快速构建 NLP 数据集的方案。它原生支持序列标注(NER)、文本分类、关系抽取和序列到序列标注四大任务类型,通过 Web 界面降低标注操作门槛,是连接标注与训练的关键工具。

在序列标注(NER)模式中,标注员通过鼠标选中文本片段并指定实体类型来创建标注,支持 BIO 和 BIOES 标签体系。文本分类模式支持多标签分类,标注员为每条文本选择一个或多个类别标签。关系抽取模式在实体标注基础上进一步标注实体间的关系类型和方向。序列到序列模式覆盖翻译、摘要等任务。

Doccano 的核心定位是纯标注工具——负责数据标注与质量管理,标注完成后导出为 spaCy、HuggingFace 等格式交给外部训练框架训练模型。工具本身不含训练功能,这种职责分离使团队可以灵活选择训练框架,同时保持标注流程的标准化和一致性。

序列标注与关系抽取的标注模式设计

序列标注(NER)是 Doccano 最常用的模式。标注员在 Web 界面中用鼠标选中文本片段(如'张三'),然后从预定义的标签列表中选择实体类型(如'当事人'),系统自动在选中文本上添加标注框。Doccano 支持 BIO 和 BIOES 两种标签体系,BIO 用 B-X 表示实体起始、I-X 表示实体内部、O 表示非实体;BIOES 额外用 E-X 表示实体结束、S-X 表示单字实体。

关系抽取模式在实体标注基础上进行。标注员先完成实体标注,然后在两个已标注实体之间拖拽连接线并选择关系类型(如'签约'、'违约')。标注规范需要在项目初始化时预定义标签集与关系类型集,后续修改标签集会影响已标注数据的一致性,因此标签集设计应在项目启动前充分论证。

实体边界的精确标注是 NER 数据质量的关键。'逐字选择'而非'词级选择'可以更精确地控制实体边界,避免分词错误导致的边界偏移。标注规范中应明确边界判定规则——例如人名是否包含称谓('张律师'中的'律师'是否属于人名实体),日期格式如何统一标注等。

多人协作标注与角色权限管理

Doccano 支持多人协作标注并区分三类角色:项目管理员(Admin)负责项目配置、标签集管理和成员分配;标注员(Annotator)领取样本进行标注;审核员(Approver)对标注结果进行复审并可以退回不合格标注。权限分级避免了越权操作与数据污染,是多人协作质量管控的基础。

协作流程通常为:管理员创建项目并导入数据,分配样本给标注员,标注员独立完成标注后提交,审核员抽样复审。审核员可以标记样本为'通过'或'退回',退回的样本重新进入标注队列。这种流水线式协作确保每个样本都经过标注和审核两道工序。

多人标注的关键是独立标注——同一样本应由不同标注员独立标注后比对一致性。如果标注员之间互相参考,会共享同一规范误解而产生系统性偏差,一致性度量也无法发现真正的问题。因此 Doccano 的权限设计确保标注员只能看到分配给自己的样本,无法查看他人的标注结果。

标注进度追踪与质量评估机制

Doccano 的项目仪表盘实时展示标注进度:每个标注员的完成数、剩余数和待审核数。进度可视化让项目管理者及时发现瓶颈并调整任务分配。仪表盘还展示标签分布统计,帮助发现标签不平衡问题——某些标签样本过少可能需要在数据采集阶段补充。

质量评估通过多人标注同一样本后计算一致性指标来实现。Doccano 支持对同一样本分配给多个标注员,系统自动统计标注员间的一致性。常用指标包括 Cohen Kappa(两人一致性)和 Fleiss Kappa(多人一致性)。对低一致性样本触发复审流程,分析分歧原因并修订规范。

进度与质量的闭环管理形成闭环:进度仪表盘发现瓶颈,质量度量发现分歧,分歧分析驱动规范修订,规范修订提升后续标注质量。这种闭环管理是大规模标注项目保证数据质量的核心方法论,而非仅依赖标注员的个人能力。

数据导入导出格式与外部集成

Doccano 的数据导入支持 JSONL 和 CSV 两种基础格式。JSONL 格式每行一个 JSON 对象,包含 text 字段和可选的 meta 元数据字段。CSV 格式以表格形式存储,适合从 Excel 等工具导入。导入时系统自动解析格式并分配样本到标注队列。

导出格式按任务类型差异化提供:序列标注可导出为 JSONL(含实体偏移量和标签)和 spaCy 训练格式(JSON 含 tokens 和 entities);文本分类导出为 JSONL 和 CSV;HuggingFace datasets 格式支持直接对接 Transformers 训练管道。这种格式转换是连接标注与训练的关键纽带。

内部存储与导出格式的区别需注意:Doccano 内部以 PostgreSQL 表结构存储标注数据(包含样本、标注、用户、项目等关系表),导出时按任务类型转换为不同格式。直接读取数据库而非使用导出接口会导致字段缺失或偏移错误,应始终使用官方导出功能获取标准化数据。

部署配置与生产环境最佳实践

Doccano 通过 Docker 镜像部署,包含前端服务(Django Web 应用)和 PostgreSQL 数据库两个核心组件。官方提供 docker-compose 配置文件一键启动。生产环境需要配置 HTTPS 反向代理(如 Nginx),确保标注数据传输加密,并定期备份 PostgreSQL 数据库防止标注数据丢失。

生产环境的并发标注性能需关注:Doccano 默认部署在单实例上,百人规模以下可正常运行。更大规模需要考虑水平扩展——通过负载均衡分发前端请求,PostgreSQL 配置连接池或读写分离。同时需监控数据库查询性能,避免大量标注数据导致查询变慢。

数据安全最佳实践包括:定期自动备份 PostgreSQL 数据库到独立存储,配置访问控制列表限制非授权访问,标注完成后及时导出并归档数据。对于敏感数据标注(如医疗、法律),还需考虑数据脱敏和审计日志,确保标注操作可追溯。

AI 应用场景

法律NER数据集协作构建

阶段:数据工程

解决的问题:解决多名法律人员协作标注1万份合同时标签混乱与质量不可控的问题

具体案例:在合同审查模型数据集构建中,使用Doccano创建NER项目并预定义当事人、金额、违约条款等12类标签,10名法律人员分领样本标注,审核员对抽样的高分歧标注单独培训,导出为spaCy JSONL格式直接对接训练管道,最终1万份合同标注一致性Cohen Kappa达0.82,模型F1较标注前版本提升6个百分点。

客服问答意图分类数据集构建

阶段:数据工程

解决的问题:解决客服对话日志意图分类标签体系缺失与人工归纳效率低的问题

具体案例:在客服问答意图分类项目中,使用Doccano文本分类模式对2万条历史对话标注意图标签,由产品经理初始设定8类意图标签集,3名标注员独立标注相同样本抽样计算一致性,对分歧样本讨论并修订标签定义,最终导出为HuggingFace datasets格式直接加载训练BERT分类器,使意图识别准确率从71%提升至88%。

Python 演示

python181 行
import jsonfrom dataclasses import dataclass, fieldfrom typing import List, Dict, Optional, Tuplefrom collections import defaultdict  @dataclassclass Entity:    start: int    end: int    label: str    text: str = ""  @dataclassclass AnnotationSample:    sample_id: str    text: str    entities: List[Entity] = field(default_factory=list)    annotator: str = ""    approved: bool = False  @dataclassclass DoccanoProject:    """模拟 Doccano 标注项目的核心数据结构与导出功能"""    name: str    project_type: str    labels: List[str] = field(default_factory=list)    relation_types: List[str] = field(default_factory=list)    samples: List[AnnotationSample] = field(default_factory=list)     def add_sample(self, text: str, sample_id: str = "") -> AnnotationSample:        if not sample_id:            sample_id = f"doc-{len(self.samples)+1:04d}"        sample = AnnotationSample(sample_id=sample_id, text=text)        self.samples.append(sample)        return sample     def add_entity(self, sample_id: str, start: int, end: int, label: str):        for s in self.samples:            if s.sample_id == sample_id:                entity_text = s.text[start:end]                s.entities.append(Entity(start=start, end=end, label=label, text=entity_text))                return        raise ValueError(f"Sample {sample_id} not found")     def to_jsonl(self) -> List[dict]:        """导出为 JSONL 格式(Doccano 原生格式)"""        result = []        for s in self.samples:            result.append({                "id": s.sample_id,                "text": s.text,                "entities": [                    {"start": e.start, "end": e.end, "label": e.label, "text": e.text}                    for e in s.entities                ],                "annotator": s.annotator,                "approved": s.approved,            })        return result     def to_spacy(self) -> List[dict]:        """导出为 spaCy 训练格式"""        result = []        for s in self.samples:            entities = [(e.start, e.end, e.label) for e in s.entities]            result.append({                "text": s.text,                "entities": entities,            })        return result     def to_huggingface(self) -> dict:        """导出为 HuggingFace datasets 格式"""        return {            "data": [                {"text": s.text,                 "labels": [e.label for e in s.entities],                 "entities": [                     {"start": e.start, "end": e.end, "label": e.label, "text": e.text}                     for e in s.entities                 ]}                for s in self.samples            ]        }     def statistics(self) -> dict:        """标注进度与标签分布统计(模拟仪表盘)"""        total = len(self.samples)        annotated = sum(1 for s in self.samples if s.entities)        approved = sum(1 for s in self.samples if s.approved)        label_dist = defaultdict(int)        for s in self.samples:            for e in s.entities:                label_dist[e.label] += 1        return {            "total_samples": total,            "annotated": annotated,            "approved": approved,            "progress": round(annotated / max(1, total) * 100, 1),            "label_distribution": dict(label_dist),        }  def calculate_iou(ent1: Entity, ent2: Entity) -> float:    """计算两个实体的交并比(IoU)用于一致性评估"""    intersection_start = max(ent1.start, ent2.start)    intersection_end = min(ent1.end, ent2.end)    intersection = max(0, intersection_end - intersection_start)    union_start = min(ent1.start, ent2.start)    union_end = max(ent1.end, ent2.end)    union = max(1, union_end - union_start)    return round(intersection / union, 4)  def inter_annotator_agreement(samples_a: List[AnnotationSample],                               samples_b: List[AnnotationSample]) -> dict:    """计算两位标注员的标注一致性(标签级+边界级)"""    dict_b = {s.sample_id: s for s in samples_b}    label_match = 0    boundary_iou_sum = 0.0    total_entities = 0     for sa in samples_a:        if sa.sample_id not in dict_b:            continue        sb = dict_b[sa.sample_id]        for ea in sa.entities:            total_entities += 1            best_iou = 0.0            best_label_match = False            for eb in sb.entities:                iou = calculate_iou(ea, eb)                if iou > best_iou:                    best_iou = iou                    best_label_match = (ea.label == eb.label)            if best_label_match and best_iou >= 0.5:                label_match += 1            boundary_iou_sum += best_iou     return {        "total_entities": total_entities,        "matched": label_match,        "label_agreement": round(label_match / max(1, total_entities), 4),        "avg_boundary_iou": round(boundary_iou_sum / max(1, total_entities), 4),    }  # === 演示 ===if __name__ == "__main__":    project = DoccanoProject(        name="合同NER标注项目",        project_type="sequence_labeling",        labels=["当事人", "金额", "日期", "违约条款"],    )     s1 = project.add_sample("甲方张三与乙方公司于2024年1月签订合同,违约金50万元。")    project.add_entity(s1.sample_id, 2, 4, "当事人")    project.add_entity(s1.sample_id, 6, 8, "当事人")    project.add_entity(s1.sample_id, 10, 20, "日期")    project.add_entity(s1.sample_id, 24, 29, "违约条款")    project.add_entity(s1.sample_id, 29, 33, "金额")    s1.annotator = "annotator_A"    s1.approved = True     s2 = project.add_sample("丙方李四应于2024年3月前支付100万元违约金。")    project.add_entity(s2.sample_id, 2, 4, "当事人")    project.add_entity(s2.sample_id, 6, 16, "日期")    project.add_entity(s2.sample_id, 18, 23, "金额")    s2.annotator = "annotator_A"     print("=== 项目统计 ===")    print(json.dumps(project.statistics(), indent=2, ensure_ascii=False))     print("\n=== JSONL导出(前1条) ===")    print(json.dumps(project.to_jsonl()[0], indent=2, ensure_ascii=False))     print("\n=== spaCy格式导出(前1条) ===")    print(json.dumps(project.to_spacy()[0], indent=2, ensure_ascii=False))
目的:模拟Doccano标注项目的核心数据结构、多格式导出(JSONL/spaCy/HuggingFace)和标注一致性评估,展示从标注到训练数据导出的完整流程。
  1. class DoccanoProject:模拟Doccano标注项目的核心数据结构,含项目名、类型、标签集和样本列表
  2. def add_sample(self, text: str, sample_id: str = '') -> AnnotationSample:向项目中添加待标注文本样本,自动生成样本ID
  3. def add_entity(self, sample_id: str, start: int, end: int, label: str):为指定样本添加实体标注,记录起始位置、结束位置和标签
  4. entity_text = s.text[start:end]根据偏移量从原文中提取实体文本,验证边界正确性
  5. def to_jsonl(self) -> List[dict]:导出为JSONL格式(Doccano原生格式),含样本ID、文本、实体列表和标注员信息
  6. def to_spacy(self) -> List[dict]:导出为spaCy训练格式,实体以(start, end, label)元组列表表示
  7. "entities": [(e.start, e.end, e.label) for e in s.entities],spaCy格式中entities字段为偏移量元组列表,直接用于训练NER模型
  8. def to_huggingface(self) -> dict:导出为HuggingFace datasets格式,可直接加载到Transformers训练管道
  9. def statistics(self) -> dict:生成标注进度与标签分布统计,模拟Doccano项目仪表盘功能
  10. "progress": round(annotated / max(1, total) * 100, 1),计算标注完成进度百分比,展示已标注样本占比
  11. def calculate_iou(ent1: Entity, ent2: Entity) -> float:计算两个实体的交并比IoU,用于评估边界标注一致性
  12. intersection = max(0, intersection_end - intersection_start)计算两个实体边界的交集长度,IoU的核心分子
  13. def inter_annotator_agreement(samples_a, samples_b):计算两位标注员的标注一致性,含标签一致率和边界IoU
  14. if best_label_match and best_iou >= 0.5:当标签匹配且边界IoU超过0.5阈值时计为一致,衡量标注质量

常见问答

Doccano能直接训练NLP模型吗?
不能。Doccano是纯标注工具,只负责数据标注与质量管理。标注完成后需导出为spaCy、HuggingFace等格式,交给外部训练框架训练模型。工具本身不含训练功能,这种职责分离让团队可以灵活选择训练框架。
Doccano支持哪些任务类型?
Doccano原生支持四大任务类型:序列标注(NER)、文本分类、关系抽取和序列到序列标注。序列标注支持BIO和BIOES标签体系,文本分类支持多标签分类,关系抽取在实体标注基础上标注关系类型。
Doccano的多人协作如何保证标注独立性?
Doccano的权限设计确保标注员只能看到分配给自己的样本,无法查看他人的标注结果。同一样本由不同标注员独立标注后比对一致性,避免互相参考导致共享同一规范误解的系统性偏差。
Doccano支持哪些导出格式?
导出格式按任务类型差异化提供:JSONL(原生格式含实体偏移量和标签)、spaCy训练格式(JSON含entities元组)、HuggingFace datasets格式(直接对接Transformers训练管道)、CSV格式。应始终使用官方导出功能获取标准化数据。
Doccano如何部署?
Doccano通过Docker镜像部署,包含前端服务(Django Web应用)和PostgreSQL数据库。官方提供docker-compose配置文件一键启动。生产环境需配置HTTPS反向代理(如Nginx),定期备份PostgreSQL数据库。没有官方托管SaaS版本,需自行部署。
实体边界标注为什么应该用逐字选择?
逐字选择而非词级选择可以更精确地控制实体边界,避免分词错误导致的边界偏移。例如'张律师'中的'律师'是否属于人名实体,逐字选择可以精确标注'张三'而不包含'律师',提高NER数据的边界质量。

常见误区

误区认为Doccano能直接训练NLP模型

正解Doccano是纯标注工具,只负责数据标注与质量管理。标注完成后导出为spaCy或HuggingFace格式交给外部训练框架训练模型,工具本身不含训练功能。

为何常见:Doccano常与spaCy、HuggingFace一起出现在教程中,学习者容易误以为它集成训练能力,忽略了标注与训练的工具边界。

误区认为Web界面无需部署即可在线使用

正解Doccano官方只提供本地Docker镜像和源码部署方式,需自行运行前端服务与数据库,没有官方托管SaaS版本,生产环境还需配置HTTPS和备份策略。

为何常见:现代SaaS工具普遍开箱即用,学习者容易假设所有Web工具都有托管版本,忽略了自部署开源工具的运维成本。

误区认为多人协作就等于标注质量有保障

正解多人协作只提供了质量度量的前提,必须配置抽样复审与一致性度量指标,并对低一致性样本重新复审。否则多人可能共享同一规范误解而产生系统性偏差。

为何常见:团队协作常被等同于质量管控,学习者容易忽视多人标注的前提是独立标注,忽略了一致性度量这个必要机制。

误区认为Doccano内部存储格式即可导出格式

正解Doccano内部以PostgreSQL表结构存储标注数据,导出时按任务类型转换为JSONL、spaCy、HuggingFace等不同格式。直接读取数据库而非使用导出接口会导致字段缺失或偏移错误。

为何常见:学习者习惯假设工具有即时存取功能,容易直接读取数据库而忽略官方导出接口的格式转换逻辑。

参考来源