简介:这是一套面向人工智能初学者与NLP项目实践者的文本标注工具实战资源,聚焦文本分类任务中的关键环节——人工打标签与语义要素提取。资源提供开箱即用的Python桌面标注工具,支持为单条文本批量添加多个类别标签,并自动识别并抽取其中的地名、人名及中心词,适用于语料构建、模型预处理与教学演示等场景。压缩包共10个文件(4个Python脚本实现核心功能与数据转换,2个文本文件记录标注结果与说明,1个JSON示例数据,1个JPG与1个PNG展示界面效果,1个Markdown文档含使用指引),整体仅209KB,轻量易部署。已有664人学习下载,包含完整目录结构、可运行主程序(mainwin.py/basewin.py)、数据处理模块(file2file.py/generate_json.py)及真实标注案例(example.json与record_01.txt),兼顾实操性与教学示范价值。
1. 不是“打标签”,而是构建可训练的语义锚点:为什么文本标注工具必须从任务闭环出发
很多刚接触 NLP 项目的人,看到“给文本打标签”第一反应是打开 Excel 表格,手动在每行后面加一列“正面/负面”或“人名/地名/机构名”。但真实工业级场景里,这种操作连数据清洗环节都过不去——标注一致性低、多人协作冲突频发、标签体系无法版本化、标注结果无法直接喂给模型训练。所谓“文本标注工具”,本质是语义定义 → 标注执行 → 质量校验 → 模型对接这一闭环中的关键基础设施。它不解决“要不要标”,而解决“怎么标才让下游模型真正学得会”。适合两类人:一是正在做课程设计、毕业设计或企业 PoC 的 NLP 初学者,需要快速产出结构化训练集;二是已有标注团队但面临标签漂移、跨项目复用难、质检覆盖率不足的技术负责人。本文聚焦“最小可行标注系统”的搭建逻辑:不依赖 SaaS 平台、不绑定特定模型框架、所有配置可 Git 管理、输出格式直通 Hugging Face Datasets 或 spaCy 的 train.jsonl。
2. 用 Doccano 在本地跑通文本分类标注的最小命令
Doccano 是当前 GitHub Star 数最高的开源文本标注工具(截至 2024 年中已超 2.1 万),其核心优势在于:纯前端交互 + Python 后端 API + 内置角色权限 + 原生支持序列标注/文本分类/问答抽取三类任务。它不强制要求 Docker,但推荐用 Docker Compose 启动以规避 Python 环境冲突——这是新手最容易卡住的第一步。
2.1 三行命令完成本地部署与初始化
# 下载官方 docker-compose.yml(注意:必须用 v1.7.0+ 版本,旧版不支持中文标签名) curl -O https://raw.githubusercontent.com/doccano/doccano/v1.7.0/docker-compose.prod.yml mv docker-compose.prod.yml docker-compose.yml # 启动服务(后台运行,自动拉取镜像并初始化数据库) docker-compose up -d # 创建管理员账户(替换 admin@example.com 和 password123 为实际值) docker-compose exec server python manage.py createsuperuser --username admin --email admin@example.com提示:若
docker-compose exec server报错 “No such service: server”,说明docker-compose.yml中 service 名称已变更。此时应先docker-compose ps查看实际 service 名(常见为web或app),再替换命令中的server。这是 Doccano v1.8.0 后的常见配置差异。
2.2 配置文本分类项目:标签体系与导入格式的硬约束
登录http://localhost:8000后,创建新项目时选择Text Classification。关键配置项有三项必须显式设置:
| 字段 | 可选值 | 必填说明 | 实际影响 |
|---|---|---|---|
| Project Name | 自定义 | 必填 | 生成独立数据库表前缀,建议含业务缩写(如news_sentiment_v1) |
| Description | 自定义 | 推荐填 | 影响团队协作时的上下文理解,非技术字段但降低沟通成本 |
| Label Type | Single Label/Multi Label | 必选 | 单标签(如情感极性)选前者;多标签(如“科技+金融+政策”)必须选后者,否则前端不显示多选框 |
导入数据时,Doccano只接受 JSONL 格式(每行一个 JSON 对象),且字段名严格固定:
{"text": "苹果公司发布新款 iPhone,市场反应热烈。", "labels": ["正面"]} {"text": "这家餐厅卫生状况堪忧,服务员态度恶劣。", "labels": ["负面"]}注意:
labels字段必须是字符串数组,即使单标签也需写成["正面"]而非"正面";text字段不能含换行符(\n),否则前端解析失败。批量清洗可用 Python 脚本:import json with open("raw.txt", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] with open("doccano_input.jsonl", "w", encoding="utf-8") as out: for line in lines: # 移除换行、转义引号、补全 labels 占位 clean_text = line.replace("\n", " ").replace('"', '\\"') json.dump({"text": clean_text, "labels": []}, out, ensure_ascii=False) out.write("\n")
2.3 标签管理界面的隐藏逻辑:为什么“删除标签”会清空所有标注记录
在项目设置页点击Labels进入标签管理,表面看只是增删改标签名。但底层机制是:每个标签名对应数据库中唯一 label_id,所有标注记录通过外键关联该 id。因此:
- 新建标签时,系统分配新 id(如
label_abc123); - 修改标签名(如将“正面”改为“积极”),仅更新
label_name字段,历史标注仍指向同一 id; - 删除标签名,则所有关联标注记录的
label_id被置为 NULL,前端显示为空白,且无法恢复。
实操建议:首次建标前,用
python manage.py shell预置标签集(避免 UI 操作失误):from app.models import Label project = Project.objects.get(name="news_sentiment_v1") Label.objects.bulk_create([ Label(project=project, text="正面", prefix_key="p", suffix_key=""), Label(project=project, text="负面", prefix_key="n", suffix_key=""), Label(project=project, text="中性", prefix_key="u", suffix_key=""), ])其中
prefix_key是键盘快捷键(按 p 键即标为正面),大幅提升标注效率。
3. 用 spaCy 的ner.manual模块实现细粒度命名实体标注
当任务从“整句情感分类”升级到“识别句子中所有产品名、型号、价格区间”,就需要序列标注(Sequence Labeling)。Doccano 支持此模式,但对中文长文本的 token 边界处理较弱;此时更推荐用 spaCy 的ner.manual命令行工具——它基于 spaCy 的 tokenizer,天然兼容中文分词规则,且标注结果可直接用于训练spacy.blank("zh")模型。
3.1 准备符合 spaCy 输入规范的 JSONL 数据
spaCy 的ner.manual要求输入为JSONL,每行含text和spans字段,其中spans是字节位置数组(非字符索引!)。中文文本需先用jieba或pkuseg分词获取字节偏移:
import jieba import json def text_to_spacy_jsonl(input_file, output_file): with open(input_file, encoding="utf-8") as f: texts = [line.strip() for line in f if line.strip()] with open(output_file, "w", encoding="utf-8") as out: for text in texts: # 获取所有可能的实体候选(此处简化:假设人工预标了部分位置) # 实际项目中应由领域专家提供初始 span 列表 spans = [] # 示例:标记“iPhone 15 Pro Max”为 PRODUCT,起始字节=6,长度=12 # 注意:Python 字符串 encode('utf-8') 后,中文字符占 3 字节 encoded = text.encode('utf-8') for word in jieba.cut(text): if word in ["iPhone", "华为", "Mate", "骁龙"]: start = text.find(word) end = start + len(word) # 转换为字节位置(关键!) byte_start = len(text[:start].encode('utf-8')) byte_end = len(text[:end].encode('utf-8')) spans.append({"start": byte_start, "end": byte_end, "label": "PRODUCT"}) record = {"text": text, "spans": spans} json.dump(record, out, ensure_ascii=False) out.write("\n") text_to_spacy_jsonl("raw_news.txt", "spacy_input.jsonl")3.2 启动交互式标注界面并导出训练集
# 安装 spaCy 中文模型(v3.7+ 推荐 zh_core_web_sm) pip install spacy && python -m spacy download zh_core_web_sm # 启动标注服务(--patterns 指定预定义标签,--port 自定义端口) python -m spacy annotation \ --loader jsonl \ --patterns '["PRODUCT","PRICE","DATE"]' \ --port 5001 \ spacy_input.jsonl访问http://localhost:5001后,界面左侧显示原文,右侧为标签面板。关键操作逻辑:
- 拖选文本 → 点击标签名:生成 span 记录;
- Shift+拖选:扩展当前 span 范围;
- Delete 键:删除当前选中 span;
- Ctrl/Cmd+S:保存当前页进度(自动写入
./annotations/目录)。
参数说明:
--patterns中的标签名必须全大写且无空格,否则 spaCy 解析失败;--loader jsonl不可省略,否则默认读取.txt文件;导出的annotations/下文件为doccano_format.jsonl,需用以下脚本转为 spaCy 训练格式:import srsly from spacy.tokens import Doc from spacy.vocab import Vocab # 读取标注结果 examples = srsly.read_jsonl("./annotations/doccano_format.jsonl") train_data = [] for eg in examples: doc = Doc(Vocab(), words=[c for c in eg["text"]]) # 按字符切分 entities = [] for span in eg.get("spans", []): entities.append((span["start"], span["end"], span["label"])) train_data.append((eg["text"], {"entities": entities})) srsly.write_jsonl("train.spacy", train_data)
3.3 验证标注质量:用 spaCy 的debug-data检查标签冲突
标注完成后,必须验证数据是否满足模型训练前提。spaCy 提供内置诊断工具:
# 检查训练数据格式合法性(是否重叠、越界、标签未注册等) python -m spacy debug-data zh ./train.spacy --verbose # 关键输出解读: # - "Entity spans overlap":同一位置被两个标签覆盖,需人工修正; # - "Unregistered label":标注中出现未在 config.cfg 中声明的标签名; # - "Low type frequency":某标签出现少于 5 次,模型易过拟合。注意:
debug-data要求config.cfg中已定义components.ner.labels。若未配置,先生成基础 config:python -m spacy init config --lang zh --pipeline ner --output config.cfg # 编辑 config.cfg,在 [components.ner] 下添加: # labels = ["PRODUCT","PRICE","DATE"]
4. 构建可复用的标注工作流:Git 版本化 + CI 自动质检
单次标注产出 JSONL 文件后,若直接扔进训练脚本,会面临三个致命问题:多人修改冲突、历史版本不可追溯、脏数据流入训练 pipeline。解决方案是将标注过程纳入软件工程流程——用 Git 管理原始文本与标注文件,用 GitHub Actions(或 GitLab CI)自动执行质检。
4.1 标注仓库的标准目录结构
text-labeling-repo/ ├── raw/ # 原始未标注文本(.txt 或 .csv) │ ├── news_2024_q1.txt │ └── reviews_q2.csv ├── annotations/ # 标注结果(按项目分目录) │ ├── sentiment_v1/ # 情感分析 v1 版本 │ │ ├── doccano_export.jsonl │ │ └── verified.jsonl # 经 QA 人工复核后的终版 │ └── ner_product_v2/ # 产品实体识别 v2 ├── scripts/ # 数据清洗与转换脚本 │ ├── clean_chinese.py │ └── doccano2spacy.py ├── tests/ # 质检规则(pytest) │ └── test_label_consistency.py └── README.md # 标签定义文档(含业务规则示例)4.2 用 pytest 编写可执行的标注质检规则
在tests/test_label_consistency.py中定义断言:
import json import pytest def test_no_duplicate_labels_in_single_doc(): """同一文档内不得出现相同标签的重复标注""" with open("annotations/sentiment_v1/verified.jsonl", encoding="utf-8") as f: for i, line in enumerate(f): try: record = json.loads(line) labels = record.get("labels", []) assert len(labels) == len(set(labels)), \ f"第{i+1}行存在重复标签:{labels}" except json.JSONDecodeError: pytest.fail(f"第{i+1}行 JSON 格式错误") def test_entity_span_not_overlap(): """NER 标注中,同一文档内实体 span 不得重叠""" with open("annotations/ner_product_v2/verified.jsonl", encoding="utf-8") as f: for i, line in enumerate(f): record = json.loads(line) spans = record.get("spans", []) # 按 start 排序后检查相邻 span sorted_spans = sorted(spans, key=lambda x: x["start"]) for j in range(1, len(sorted_spans)): prev = sorted_spans[j-1] curr = sorted_spans[j] assert prev["end"] <= curr["start"], \ f"第{i+1}行第{j}个 span 与前一个重叠:{prev} vs {curr}"4.3 GitHub Actions 自动触发质检流水线
在.github/workflows/label-qa.yml中定义:
name: Label Quality Assurance on: push: paths: - 'annotations/**' - 'tests/**' jobs: run-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: | pip install pytest srsly spacy python -m spacy download zh_core_web_sm - name: Run label consistency tests run: pytest tests/ -v效果:每次向
annotations/目录推送新文件,GitHub 自动运行pytest,失败则阻断合并,并在 PR 页面显示具体哪一行、哪个规则失败。这比人工抽检效率高 10 倍以上,且杜绝“我以为标对了”的主观误差。
5. 从标注工具到训练闭环:用 Hugging Face Datasets 直接加载标注数据
标注完成 ≠ 模型可用。最后一步是打通“标注文件 → 训练数据集 → Trainer API”的链路。Hugging Face Datasets 库提供零配置加载 JSONL 的能力,但需注意字段映射与类型转换。
5.1 将 Doccano 导出的 JSONL 转为 Dataset 对象
from datasets import Dataset, Features, Value, ClassLabel import json # 定义 schema(关键:ClassLabel 必须显式传入 label names) features = Features({ "text": Value("string"), "label": ClassLabel(names=["正面", "负面", "中性"]) # 顺序必须与 Doccano 标签一致 }) # 读取并转换 data = [] with open("annotations/sentiment_v1/verified.jsonl", encoding="utf-8") as f: for line in f: record = json.loads(line) # Doccano 输出的 labels 是 list,需取第一个(单标签模式) label_name = record["labels"][0] if record["labels"] else "中性" data.append({"text": record["text"], "label": label_name}) dataset = Dataset.from_list(data, features=features) print(dataset) # Dataset({ # features: ['text', 'label'], # num_rows: 1247 # })5.2 分割数据集并应用预处理函数
# 划分训练/验证集(固定随机种子保证可复现) train_test = dataset.train_test_split(test_size=0.2, seed=42) train_ds = train_test["train"] val_ds = train_test["test"] # 加载 tokenizer(以 bert-base-chinese 为例) from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") def tokenize_function(examples): return tokenizer( examples["text"], truncation=True, padding=True, max_length=128 ) # 批量处理(num_proc 启用多进程加速) tokenized_train = train_ds.map( tokenize_function, batched=True, num_proc=4, remove_columns=["text"] # 删除原始文本列,只保留 tokenized 字段 )5.3 验证 tokenized 数据集是否符合 Trainer 输入要求
# 检查首条样本结构 sample = tokenized_train[0] print("Input IDs shape:", len(sample["input_ids"])) # 应为 128 print("Label value:", sample["label"]) # 应为 0/1/2(ClassLabel 映射后) print("First 5 tokens:", tokenizer.convert_ids_to_tokens(sample["input_ids"][:5])) # 关键验证:label 字段是否为 int 类型(Trainer 要求) assert isinstance(sample["label"], int), "label must be integer after ClassLabel mapping"参数说明:
max_length=128是 BERT 类模型的典型输入长度,过长会导致显存溢出;remove_columns=["text"]是必须操作,否则 Trainer 会因找不到input_ids字段报错;num_proc=4在 8 核 CPU 上可提速 3 倍以上。至此,tokenized_train可直接传入Trainer(train_dataset=...),完成从“给文本打标签”到“启动模型训练”的最后一跳。
本文还有配套的精品资源,点击获取