简介:基于Python与Jupyter构建的医疗实体识别模型项目,面向需要完成NLP期末大作业、课程设计或项目开发的在校生与开发者。项目以疾病、症状、身体部位三大词典为基础,分别通过互联网爬取与ICD10等来源清洗去重,构成39615条、7457条与1929条实体数据;语料标注采用最大匹配策略定位实体并标记类型,同时提供训练数据与评估结果。压缩包共147个文件,整体581.14MB,核心内容包括ipynb交互式分析脚本、py可执行源码、dic词典、xlsx标注数据、checkpoint与pkl模型文件,并附有Markdown说明文档。源码经过严格测试,支持直接运行和二次扩展,可用于电子病历实体抽取、健康问答等典型场景,帮助读者理解医疗命名实体识别的完整流程。目前已有90人学习下载,是兼顾教学与工程实践的高性价比参考项目。
1. 医疗实体识别课程设计:这个项目到底在做什么、值不值得投入
“基于 python+Jupyter 构建医疗实体识别模型,包含词典和语料标注+源码+文档”,这行标题浓缩了一个典型 NLP 课程设计的全部交付物:用 Python 生态在 Jupyter Notebook 里做医疗文本命名实体识别(NER),从医疗文本中抽出疾病、症状、药物、检查等实体,最后以“可运行的源码 + 说明文档 + 标注数据”的形式交作业。这类项目很实在:不依赖高端 GPU,不需要巨额语料,用词典 + CRF 序列标注就能做出一套可演示、可解释、可答辩的系统,特别适合期末大作业、课程设计和中小型项目开发练手。下面按“选型→数据→建模→避坑→提分”的顺序,把完整做法和血泪经验一次讲透。
2. 先定技术路线:词典匹配、CRF 还是深度学习,怎么选才不翻车
医疗实体识别看着高大上,实际落地时技术选型非常现实。你的时间、机器、语料规模三样东西决定路线。不要一上来就奔着 BERT 去,先把词典匹配、CRF、BiLSTM 这几条路的性价比算清楚,再决定框架。
2.1 词典匹配:为什么它永远是医疗 NER 的地基
医疗实体和新闻实体最大的区别是封闭性。新闻里的“苹果”可能是水果也可能是公司,医疗文本里的“糖尿病”“阿司匹林”基本没有歧义,即便有,也是一眼能看出来的罕见情况。这意味着一个整理干净的实体词典就能覆盖 80% 以上的高频实体,这一层做的匹配,直接决定后续模型的底线。
我在课程设计里一定会先做词典匹配,理由有三个。第一,它是零成本基线:不需要任何训练数据,写一个“正向最大匹配”函数就能出第一版结果,可以拿去交差、演示,也可以作为后续模型的对比对象。第二,它是 CRF 的特征来源:把“当前位置是否命中词典、命中哪个类型”作为特征喂给 CRF,模型在训练数据不足时依然能保持不错的召回。第三,它让答辩逻辑变得好讲:评委问“系统怎么识别出高血压”,你可以直接指向词典命中路径,不一定非要绕到神经网络的黑匣子里。
词典匹配的常见做法是“正向最大匹配加类型回溯”:把句子按字或词逐个位置尝试匹配词典中最长的实体名,命中后打上对应类型标签。实现上要注意,中文几乎没有天然词边界,所以一般先把句子切分成单字,再在每个位置上做最大窗口尝试;匹配失败就跳过,不强行合并。窗口大小我一般设成 6,医疗实体很少超过 6 个字,太大会拖慢速度。
词典匹配的局限也很清楚:对未登录实体束手无策,对“无高血压”“未见异常”这类否定语境完全无感。所以它只配当地基,不配当顶层方案。顶层还是得交给序列标注模型去学上下文。
2.2 序列标注模型:CRF 是课程设计的舒适区,BiLSTM 是加分项
如果只有几百句标注语料,BiLSTM-CRF 并不比 CRF 强,反而更容易翻车:训练不稳定、收敛慢、对随机种子敏感,经常出现换一个 seed 结果差 10 个点的情况。而 CRF 结构简单,直接对“标签序列”建模,能学习到 B 后面必须跟 I、实体类型不能随意跳转这类强约束,训练只要几秒钟,CPU 就能跑,这在课程设计环境下是极大的优势。
BERT 系列虽然效果好,但医疗领域的预训练权重动辄几个 GB,加载慢、推理慢,在普通笔记本上训练一个 epoch 可能要十几分钟。如果你的任务不是“冲击 SOTA”而是“把项目完整做出来”,我不会推荐把宝押在预训练模型上。更常见的稳妥组合是:词典匹配做基线和特征,CRF 做主力模型,时间富余再加一层 BiLSTM 作为对比实验写进文档。
下面这张表你可以直接抄进课程设计报告,用来解释为什么选 CRF:
| 方案 | 可解释性 | 所需标注数据 | 训练耗时 | 硬件要求 | 实现难度 |
|---|---|---|---|---|---|
| 词典匹配 | 很高 | 0 | 0 | 无 | 低 |
| CRF | 高,特征可回溯 | 100~200 句起步 | 秒级 | CPU 即可 | 低 |
| BiLSTM-CRF | 低,黑匣子 | 1000 句以上更稳 | 分钟级 | CPU 可跑但慢 | 中 |
| BERT/BioBERT | 低 | 越多越好 | 小时级 | 推荐 GPU | 高 |
表格说明:课程设计的数据量通常只有几百句,CRF 正好处在“效果够用、成本低、解释得清”的位置。选 BiLSTM 更多是为了向评委展示你了解神经网络结构,属于加分项而不是必需品。如果你决定上 BiLSTM,我建议把 CRF 结果写进对比报告,用数据说明“为什么在有限语料下线性模型更可靠”,这比单纯堆模型更能体现工程判断力。
3. 词典构建与语料标注:从零攒出可训练数据,附转换脚本
数据是医疗 NER 最大的门槛。公开的中文医疗数据集不是没有,但它和你任务里的文本风格、实体定义往往对不上,直接拿来训练会导致模型在你自己测试集上表现很差。所以课程设计的通用做法是自建一个小规模词典和一份标注语料,量不用大,300 句左右即可,关键是格式统一、标注规范可复现。
3.1 医疗词典从哪来:公开术语表加手工清洗的兜底方案
医疗实体词典的搭建不需要原创,常见做法是三个来源合并:疾病名来自 ICD-10 中文编码表,药物名来自药品说明书或国家药典的通用名附表,症状名来自症状学教材和百科词条。这些都是公开资料,整理成下面这种三列结构就行:
| 实体名 | 别名 | 类型 |
|---|---|---|
| 高血压 | 高血压病 | Disease |
| 糖尿病 | 2型糖尿病 | Disease |
| 阿司匹林 | 乙酰水杨酸 | Drug |
| 头痛 | 头疼 | Symptom |
整理时有一个很容易忽略的细节:词条不要只收标准名,一定要收别名。原因是医疗文本里患者描述非常口语化,“头疼”和“头痛”是同一个实体,如果你只在词典里收“头痛”,匹配阶段就会漏掉一半。另外,实体名里不要带“病”“症”以外的后缀词,比如“高血压病”和“高血压”存在包含关系,正向最大匹配会优先命中更长的“高血压病”,这没问题;但如果你同时收“糖尿”这种半截词,就会把“糖尿病患者”错误地切成一个实体。
我一般会把整理好的词典存成 CSV,控制字段为 entity、alias、type 三列,编码用 UTF-8。Jupyter Notebook 读 CSV 时用 pandas 的read_csv,指定encoding="utf-8"和dtype=str,避免类型字段被读成 float。这一步的细节决定了后面所有代码的稳定性,值得多花半小时清洗格式、去重、排查空行。
3.2 Excel 标注到 BIO 格式:一段可直接运行的转换脚本
语料标注是课程设计里最耗时也最容易被低估的环节。常见做法是:把待标注句子放在 Excel 里,一句一行,旁边一列用“实体名@类型;实体名@类型”的方式写出该句中出现的实体。这样标注人不需要关心字符偏移量,只负责把自己的判断写清楚,偏移量交给脚本计算。等到标注完成,再用下面这段 Python 脚本一次性转成 BIO 格式:
import pandas as pd def excel_to_bio(excel_path, output_path): df = pd.read_excel(excel_path, dtype=str).fillna("") bio_lines = [] for _, row in df.iterrows(): sent = str(row["句子"]).strip() annos = str(row["实体"]).strip() spans = [] if annos: for item in annos.split(";"): if "@" not in item: continue word, etype = item.rsplit("@", 1) start = sent.find(word) if start >= 0: spans.append((start, start + len(word), word, etype)) else: # 实体在句子中找不到,打印出来人工复核 print(f"警告:句子中找不到实体 {word} -> {sent}") spans.sort(key=lambda x: x[0]) char_tags = ["O"] * len(sent) for start, end, word, etype in spans: char_tags[start] = "B-" + etype for pos in range(start + 1, end): char_tags[pos] = "I-" + etype for ch, tag in zip(sent, char_tags): bio_lines.append(f"{ch}\t{tag}") bio_lines.append("") # 句子之间用空行分隔 with open(output_path, "w", encoding="utf-8") as f: f.write("\n".join(bio_lines)) return len(df) # 参数说明: # excel_path: 标注 Excel 的路径,必须包含“句子”“实体”两列 # output_path: 输出的 BIO 文件路径,每行是“字<TAB>标签” print(excel_to_bio("annotations.xlsx", "data/train.bio"))这段代码的逻辑是:先解析 Excel 里的“实体”列,在句子中用find()定位实体字符串,算出起止偏移;再初始化一整行O标签,把实体的第一个字标成B-类型,后续字标成I-类型;最后把“字 + 制表符 + 标签”按行写出,句子间用空行隔开。
参数说明里最需要关注的是start = sent.find(word)这一行:它只查找实体第一次出现的位置。如果一个句子中同一个词出现多次,比如“发热伴咳嗽,发热已退”,标注时只写“发热@Symptom”,脚本只会标注第一个“发热”,第二个会被漏掉。稳妥做法是在标注 Excel 里直接写清楚是第几个出现的位置,或者干脆在标注阶段按“第 n 个出现”来写。对课程设计来说,更省事的方案是标注时把同一实体重复出现的情况拆成两条记录,例如“发热@Symptom;发热@Symptom”分别对应两处,再把脚本里的find改成循环查找。下面是改进片段:
# 用循环查找替代单次 find,支持同一实体在同一句中多次出现 start = 0 while True: idx = sent.find(word, start) if idx < 0: break spans.append((idx, idx + len(word), word, etype)) start = idx + len(word) if start >= len(sent): break这段代码替换原脚本中的start = sent.find(word)之后,同一个词出现 N 次就会产生 N 个实体区间,再配合标注时写两条记录,就不会丢实体。循环查找的终止条件是start越过句子末尾,避免死循环。
转换完成后,BIO 文件的样子如下,每一行是一个字符加一个标签:
发 O 热 B-Symptom 伴 O 咳 B-Symptom 嗽 I-Symptom , O 头 B-Symptom 痛 I-Symptom 糖 B-Disease 尿 I-Disease 病 I-DiseaseBIO 的含义很直观:B 表示实体首字,I 表示实体延续,O 表示非实体。CRF 训练时会把每行当作一个样本,所以字符和标签之间的分隔符必须统一用制表符,不能混用空格;句子之间的空行也不能省略,它告诉模型“这是一个新的序列开始”。
3.3 标注规范:先定好标签体系,避免标注到一半后悔
标注规范是整个项目最容易踩坑的地方,没有之一。如果你一开始不定义清楚“疾病”和“症状”的边界,标注到第 100 句时一定会遇到边界模糊的句子,然后你会发现前面标的数据和后面的不一致,模型效果直接受影响。这事的残酷之处在于:标注返工没有后悔药,只能重标。
我建议课程设计只定义四类实体,够用且不容易打架:
| 标签 | 含义 | 示例 |
|---|---|---|
| Disease | 疾病名 | 高血压、2型糖尿病、冠心病 |
| Symptom | 症状表现 | 头痛、发热、心悸 |
| Drug | 药物 | 阿司匹林、二甲双胍 |
| Check | 检查项 | 血常规、CT、心电图 |
规范上强制三条约定:第一,实体内不允许包含标点,比如“头痛、发热”要么拆成两个实体,要么只标“头痛”,“发热”再单独标。第二,实体内不允许包含修饰成分,“间断性头痛”只标“头痛”,“间断性”留给 CRF 去学。第三,否定语境如“无发热”“未见异常”,实体本身照常标注,否定词不进入实体。这样标注的好处是,模型学的是“头痛”这个词本身,而“无”这个词会被 CRF 当成上下文特征,后续如果要加否定规则,也可以在模型输出之后做后处理。
标签体系定好后,把标注示例直接写在 README 里,标注的人(哪怕是和你合作的室友)也必须按照同一套约定来标。我在实际做的时候,会让两个标注者各标 30 句,然后比对一致率,发现一致性低就回到规范去讨论边界案例,而不是直接开标全部数据,这能减少大量返工。
4. 模型训练与评估:Jupyter 里跑通 CRF 的最小完整代码
数据备好后,模型部分反而轻松。我习惯把整个流程拆成三个 Notebook:数据预览与转换、CRF 训练、错误分析。这样答辩时可以直接按顺序运行,每一步都能看到输出,不需要评委来回翻代码。训练之前先把 BIO 文件读进来,转成 Python 列表结构:一个句子是一个字符列表,对应一个标签列表。
4.1 读入数据与构造特征:字符级特征加词典命中的组合
CRF 不能直接吃中文字符,要把每个位置转成特征字典。医疗 NER 的常见特征组合是:当前字、前后字、当前字的类型(中文/英文/数字/标点)、当前位置是否命中词典、命中词典的实体类型。下面这段代码可以直接放进 Notebook:
def load_bio(filepath): sentences, tag_sequences = [], [] sent, tags = [], [] with open(filepath, encoding="utf-8") as f: for line in f: line = line.rstrip("\n") if not line: if sent: sentences.append(sent) tag_sequences.append(tags) sent, tags = [], [] continue parts = line.split("\t") if len(parts) != 2: continue sent.append(parts[0]) tags.append(parts[1]) if sent: sentences.append(sent) tag_sequences.append(tags) return sentences, tag_sequences def get_char_type(ch): if "\u4e00" <= ch <= "\u9fff": return "ZH" if ch.isdigit(): return "NUM" if ch.isalpha(): return "EN" return "PUNCT" def build_features(sent, idx, dict_hit): ch = sent[idx] prev_ch = sent[idx - 1] if idx > 0 else "#" next_ch = sent[idx + 1] if idx + 1 < len(sent) else "#" return { "ch": ch, "prev": prev_ch, "next": next_ch, "char_type": get_char_type(ch), "dict_type": dict_hit[idx], # 如 "Disease" / "Drug",未命中为 "NONE" }特征字典的 key 是 CRF 内部的特征名,value 是特征值。sent[idx]根据 Python 基础语法的索引规则取当前字;prev_ch和next_ch在句子首尾取#作为填充,目的是让序列首尾的特征维度保持一致。char_type是我个人比较依赖的特征,因为医疗文本经常混着英文缩写(CT、MRI)和数字(血压 120/80),把字符类型显式交给模型,能帮助它区分“心电图”和“CT”的实体边界。
build_features里我额外传了dict_hit参数,这是一个和句子等长的列表,每个位置记录当前字是否命中了词典、命中哪种类型。这个特征的意义很大:CRF 本身是线性模型,无法直接查词典,但通过特征注入词典信息后,它相当于学会了“看见这个位置有词典命中,就倾向于以它为实体起点”。这比单独跑一遍词典匹配再合并结果要干净得多,因为 CRF 会统一处理实体边界和类型转移。
4.2 训练、预测与评估:用 sklearn-crfsuite 跑通完整流程
训练部分我直接用 sklearn-crfsuite,它封装了 python-crfsuite 的 sklearn 接口,API 贴近普通机器学习库,写起来不绕。先把所有句子转成特征列表,再切分训练集和测试集,然后训练:
import sklearn_crfsuite from sklearn_crfsuite import metrics sentences, tags = load_bio("data/train.bio") # 按句子切分,不要把同一个句子的字符拆到不同集合 split = int(len(sentences) * 0.8) train_sents, test_sents = sentences[:split], sentences[split:] train_tags, test_tags = tags[:split], tags[split:] dict_hits = match_entity_dict(train_sents + test_sents, "entities_dict.csv") def prepare(sents, tag_list): X, y = [], [] for sent, tags_one in zip(sents, tag_list): hit = dict_hits[tuple(sent)] X.append([build_features(sent, i, hit) for i in range(len(sent))]) y.append(list(tags_one)) return X, y X_train, y_train = prepare(train_sents, train_tags) X_test, y_test = prepare(test_sents, test_tags) crf = sklearn_crfsuite.CRF( algorithm="lbfgs", c1=0.1, c2=0.01, max_iterations=100, all_possible_transitions=True, ) crf.fit(X_train, y_train) y_pred = crf.predict(X_test) print(metrics.flat_f1_score(y_test, y_pred, average="weighted"))参数解释:algorithm="lbfgs"是 CRF 最常用的优化算法,小数据量下收敛快;c1和c2分别是 L1 和 L2 正则系数,常见起步值是 0.1 和 0.01,先固定这两个值跑通,再微调;max_iterations=100对几百句语料足够,我试过 500 次迭代,F1 基本没变化,反而训练时间从几秒涨到几十秒;all_possible_transitions=True必须开启,它允许模型学习从任意标签转移到任意标签的代价,比如学习“B-Symptom 后面当然可以是 I-Symptom,但 O 后面不应该直接接 I-Disease”。
match_entity_dict这个函数我没有展开,你可以自己实现一个词典匹配器:遍历句子中的每个位置,尝试匹配 CSV 词典中对应窗口内的实体,返回一个{句子的元组: 命中列表}的字典。dict_hits[tuple(sent)]的写法是为了避免句子作为列表无法哈希的问题。这里要注意:测试集句子也要参与词典特征构造,因为词典匹配不依赖训练过程,属于外部知识注入。
评估指标优先看实体级的 F1,而不是字符级准确率。flat_f1_score把序列展平后按标签计算 F1,其中average="weighted"表示按每个标签出现数量加权。实际做的时候我还会额外打印每一类实体的 precision、recall、F1,因为课程设计报告里最好有一张分类型的结果表,说明系统在哪类实体上强、哪类实体上弱。
5. 常见问题与避坑:5 个让课程设计翻车的典型坑记录
这一章全部来自实际跑项目的血泪经验。每个坑都按“现象 → 原因 → 解决”写清楚,你提前扫一遍,能省下大半天排查时间。
5.1 标注错位导致实体静默丢失
现象:转换脚本运行完没有报错,BIO 文件里却有大量句子只标出部分实体,甚至完全没有实体。
原因:excel_to_bio里的sent.find(word)找不到实体字符串时只会打印警告,不会中断程序。一个常见的隐蔽场景是 Excel 里实体写成全角字符,而句子里的字是半角,find返回 -1,实体被跳过,但你盯着屏幕很难看出来。
解决:在转换函数里加一个失败计数器,找不到实体的数量超过阈值就直接抛出异常,而不是打印警告后继续。同时建议标注前统一做一次全半角转换,用 Python 的str.replace把全角数字、字母、标点替换成半角版本再写入转换流程。注意,中文全角标点如逗号、句号是要保留的,替换范围只针对数字、字母、英文符号。
5.2 python-crfsuite 装不上,或装好后 Jupyter 内核崩溃
现象:pip install sklearn-crfsuite报错提示缺少 Microsoft Visual C++ 14.0;或者安装成功,但在 Jupyter 里import sklearn_crfsuite时内核直接重启。
原因:python-crfsuite 是 C 扩展,Windows 下需要 MSVC 运行库,且 Jupyter 内核如果指向的不是你安装包的那个 Python 环境,就会出现“能 pip 但 import 崩”的诡异情况。
解决:最常见做法是用 Anaconda 创建一个干净的 Python 3.9 环境,在环境里执行conda install -c conda-forge python-crfsuite,然后启动 Jupyter 时确认 kernel 选的是这个环境。如果你用浏览器打开 Jupyter 网页版登录入口后发现 kernel 列表里没有新环境,就在终端里跑python -m ipykernel install --user --name crf_env把环境注册进去。不要在系统 Python 和 conda 环境里混着装包,那是给自己埋雷。
5.3 Jupyter 单元格只显示最后一个输出,或重复运行导致数据叠加
现象:一个 cell 里写了df.head()和print(...)两行,运行后只看到表格,print 的输出不见了;另外,同一个 cell 反复运行,发现训练集句子数越来越多。
原因:Jupyter 的 cell 默认只回显最后一个表达式,而print是标准输出,两者混在一起时表格会盖住 print 的视觉存在感。重复运行时,如果你的代码里有类似all_sentences += new_sentences这样的累加操作,每次运行都会在原有基础上追加,造成数据量翻倍、标签错位。
解决:需要同时看多个输出时,显式写print()或from IPython.display import display; display(df.head())。数据拼接类的代码统一放在独立 cell,且开头加一行sentences, tags = load_bio(...)用覆盖式赋值,不用+=。这是一个很小的习惯,但能避免很多“这个 cell 怎么越跑越慢”的困惑。
5.4 词典匹配结果和模型输出重复计数
现象:词典匹配找到了 40 个实体,CRF 预测了 35 个实体,两个集合合并去重后 F1 只有 0.6,不如单独用 CRF 的结果。
原因:两侧识别的实体在字符位置上不完全一致。词典匹配认为“高血压病”是一个实体,CRF 认为“高血压”才是实体,两个字符串长度不同,用字符串去重会当成两个实体,实际上它们覆盖的是同一段原文。
解决:最终评估以 CRF 输出为准,词典只作为特征,不参与最终实体合并。如果确实要做词典后处理,合并前先按实体坐标(start,end)做元组去重,再比较类型是否一致:
def dedup_by_span(entities): seen = set() result = [] for start, end, ent_type in entities: key = (start, end, ent_type) if key not in seen: seen.add(key) result.append((start, end, ent_type)) return result这个函数接受(起止位置, 类型)的三元组列表,按坐标加类型去重,避免“高血压病”和“高血压”被当成两个结果重复计数。代码里seen.add(key)前要先判断key是否已存在,这里用not in加add是标准写法,目的是保证每个坐标加类型组合只保留一次。
5.5 训练集太小,F1 卡在 0.4 上不去
现象:标注了 200 句,CRF 训练完,总体 F1 只有 0.4 左右,其中 Check 类实体的召回率接近 0。
原因:数据规模小且分布不均。200 句里症状类实体占了 60%,检查类只有 20 多个样本,CRF 学到“检查类实体出现频率低、转移证据少”,自然倾向于把所有位置都预测成 O 或 Symptom,以最小化整体损失。
解决:先把低频类的同义词扩充进词典,让dict_type特征能覆盖更多“血常规”“尿常规”“心电图”等变体写法;再用 5 折交叉验证替换单次切分,观察每折分数波动。如果某一折 F1 骤降,说明测试集里恰好有一批罕见实体没有被训练集覆盖,这不是超参问题,而是标注覆盖不足。最后的手段是给低频类补充标注,挑 20 句包含检查实体的句子专门补标,这比增广所有类别更高效。不要急着把模型换成 BiLSTM,数据量不够时换模型只会换来更玄学的波动。
6. 把成绩往上再拉一档:学会看 bad case,比堆模型更值
课程设计答辩时,评委问得最多的不是“你的 F1 是多少”,而是“你分析过失败案例吗”。这一问就能区分出谁是调包侠、谁真正理解项目。我一般会在最后一个 Notebook 里专门做错误分析:把 CRF 预测错的句子、真实标签、预测标签打印出来,按错误类型归类,写进文档。
具体做法是:先跑一遍测试集预测,然后遍历每个句子的真实标签和预测标签,找出存在差异的位置,打印整句并高亮差异处。你可以用一个表格记录错误类型:实体边界错误(多字少字)、类型混淆(把 Disease 标成 Symptom)、遗漏(真实有实体但预测全 O)。这些数据就是课程设计报告里最值钱的“结果分析”章节素材。
三个实际有效的小技巧值得一试。第一,加否定规则后处理:模型输出实体后,检查实体前 1~2 个字是否包含“无、未见、否认、排除”,一旦命中就把该实体从结果中滤除,这能把医疗文本里的假阳实体压掉不少。第二,同义词归一:把“头疼”和“头痛”在词典层统一映射到同一个标准名,训练时告诉模型它们是同一个类型,预测时输出标准名,报告里的统计数据会好看很多。第三,用 5 折交叉验证替代单次切分:代码改动很小,但报告里能写“模型在 5 折验证下 F1 方差不超过 0.02”,这句话的可靠性远超单次切分的“F1 0.68”。交叉验证代码其实就是把训练循环包进for fold in range(5),每折换不同切分点训练一次,最后取平均。
以前我也觉得 NER 嘛,堆个模型跑出准确率就完事了。后来发现标注规范和 bad case 分析才是这门课真正的分水岭:规范决定了数据质量,数据质量决定模型上限,而 bad case 决定了你还能往上走多远。这套方案做完,你手里会有一套干净的标注流程、一份可扩展的 CRF 训练代码、一份说清楚边界的文档,不管以后转做 BERT 还是其他 NLP 方向,这套基础都不会白搭。希望帮到你。
本文还有配套的精品资源,点击获取