简介:这份资源是百度开源的中文词法分析工具 LAC 的 Python 版本,面向自然语言处理初学者与需要中文分词能力的开发者。中文分词是文本分类、情感分析、信息检索等任务的基础,该工具可帮助读者快速搭建从连续文本中切分出语义完整词汇序列的处理流程。压缩包共 101 个文件,约 4.81MB,包含 15 个 Python 脚本、14 个 C 头文件、10 个 C++ 源文件,以及 Java、Gradle 相关文件,另有词典、模型库、说明文档与示例图片,兼顾 Python 调用与底层 C++ 实现。已有 510 人学习下载。读者可从中获得可直接运行的分词示例、多语言接口参考、词典与配置资源,以及从源码理解词法分析实现思路的完整素材,适合作为中文分词入门与工程落地的实践参考。
1. 中文分词总翻车,LAC 到底能不能救场
做中文 NLP 的,几乎都经历过分词这一步的玄学时刻。同一个句子,换一个工具,切出来的结果能差出十万八千里。尤其是遇到「的」「了」「着」这类虚词,或者「南京市长江大桥」这种经典歧义句,很多轻量级分词器直接摆烂。百度开源的 LAC(Lexical Analysis of Chinese)就是冲着这个痛点来的,它把分词、词性标注、命名实体识别三件事打包成一个模型,用 Python 就能直接调。我第一次在某个舆情分析项目里用它,是因为原来的 jieba 加规则方案在机构名和地名上错得离谱,而 LAC 的 NER 标签体系直接覆盖了 PER、LOC、ORG、TIME 这几类高频实体。这篇文章不讲空泛的「中文分词发展史」,只讲一件事:如果你手头有一个中文文本处理任务,LAC 能不能用、怎么装、参数怎么调、哪些坑必须提前知道。适合已经写过 Python、调过至少一个 NLP 库、但还没把 LAC 跑进生产环境的开发者。
2. 把 LAC 装进本地环境:从 pip 到模型加载的完整链路
2.1 为什么选 LAC 而不是继续用 jieba 或 HanLP
先说选型理由,不然后面调参没有方向。jieba 的优势是轻、快、零依赖,但它的词性标注和 NER 能力基本靠规则和词典堆出来的,遇到新词、网络用语、专业术语就露馅。HanLP 功能全,但早期版本依赖重,模型文件大,部署到一些资源受限的环境里比较费劲。LAC 的定位在两者之间:它基于百度自研的深度学习模型,分词和词性标注是一体化的,NER 也直接输出,不需要你再单独训一个实体识别模型。我一般会在以下场景优先考虑 LAC:需要同时拿到分词和词性;文本里机构名、地名、人名密度高;不想自己维护一套词典和规则。如果只是做简单的关键词提取,jieba 确实够用,但一旦涉及实体抽取,LAC 的召回率明显更稳。
2.2 安装 LAC 的两种方式与依赖检查
LAC 的 Python 包叫lac,直接 pip 安装是最省事的路径。但这里有一个血泪经验:不要在一个已经装了 PyTorch 或 TensorFlow 多个版本的虚拟环境里直接装,容易触发依赖冲突。我习惯先建一个干净的 conda 环境,再装 LAC。
# 创建独立环境,避免和已有深度学习框架冲突 conda create -n lac_env python=3.8 -y conda activate lac_env # 安装 LAC,官方包名就是 lac pip install lac # 验证安装是否成功,同时查看版本 python -c "import lac; print(lac.__version__)"逻辑说明:LAC 的 pip 包会自动拉取它需要的推理后端依赖,通常是 PaddlePaddle 的 CPU 版本。如果你机器上有 GPU 并且想用 GPU 加速,需要先手动安装对应 CUDA 版本的 PaddlePaddle,再装 LAC,否则 pip 会默认装 CPU 版。参数上,Python 版本建议 3.7 到 3.9,太高或太低都可能遇到 wheel 包不兼容。安装完成后,第一次运行会自动下载模型文件,默认缓存在用户目录下的.lac文件夹里。如果公司内网限制外网访问,需要提前把模型文件离线拷贝到对应目录,否则会卡在下载阶段。
2.3 最小可运行示例:分词、词性、NER 一次拿到
装好之后,先跑一个最小示例,确认三个任务都能正常输出。不要一上来就灌几万条数据,先用两句话把链路跑通。
from lac import LAC # 初始化 LAC 对象,默认加载分词、词性、NER 三个任务 lac = LAC(mode='lac') # 输入文本,注意这里用的是 list,不是单个字符串 texts = ["某高校实验室发布了新版本图像处理Demo,地点在某个科技园区。"] # 调用 run 方法,返回结果是一个列表,每个元素对应一条输入 results = lac.run(texts) # results[0] 是一个元组,依次是:分词列表、词性列表、NER 列表 words, tags, ners = results[0] print("分词结果:", words) print("词性标注:", tags) print("命名实体:", ners)逻辑说明:LAC(mode='lac')里的mode参数决定加载哪些任务。默认的lac模式同时加载分词、词性和 NER。如果你只需要分词,可以改成mode='seg',这样模型加载更快、内存占用更低。run方法接收一个字符串列表,返回一个列表,每个元素是一个三元组。注意 NER 列表的长度和分词列表一致,每个词对应一个实体标签,非实体位置是O。参数上,run还有一个batch_size参数,默认是 1,处理大批量文本时建议调到 16 或 32,能明显提升吞吐。
3. 把 LAC 用进真实管道:批量处理、自定义词典与性能调优
3.1 批量推理的写法与 batch_size 的取舍
单条跑通之后,下一步就是批量处理。很多人直接写一个 for 循环,一条一条调lac.run([text]),结果发现速度慢得离谱。LAC 的run方法本身支持批量输入,你只需要把文本攒成一个列表,一次性传进去。
from lac import LAC lac = LAC(mode='lac') # 模拟一批文本,实际场景可以从文件或数据库读取 corpus = [ "某公司发布了新款智能硬件产品。", "A同学在某实验室完成了图像分割实验。", "这个方案在某个科技园区进行了落地测试。" ] # 批量推理,batch_size 设为 16 results = lac.run(corpus, batch_size=16) for i, (words, tags, ners) in enumerate(results): print(f"文本{i+1} 分词: {words}") print(f"文本{i+1} 词性: {tags}") print(f"文本{i+1} 实体: {ners}")逻辑说明:batch_size不是越大越好。我实测下来,CPU 环境下 16 到 32 是比较稳的区间,再往上内存占用会明显上升,而吞吐提升有限。GPU 环境下可以试到 64,但要注意显存。如果你处理的文本长度差异很大,比如有的句子十几个字,有的段落几百字,建议先按长度分桶,再分别批量推理,否则 padding 会浪费大量计算。参数上,run方法还有一个lac参数可以覆盖默认的模型路径,一般不用动。
3.2 自定义词典的加载方式与生效边界
LAC 内置的词典覆盖了通用领域,但如果你做的是垂直领域,比如医疗、金融、法律,很多专业术语会被切碎。这时候需要加载自定义词典。LAC 支持通过custom_dict参数传入一个词典文件路径。
from lac import LAC # 自定义词典文件,每行一个词,格式为:词 词性 频率(频率可省略) # 示例内容: # 图像处理Demo n 100 # 某科技园区 n 100 lac = LAC(mode='lac', custom_dict='./custom_dict.txt') texts = ["某高校实验室发布了新版本图像处理Demo。"] results = lac.run(texts) print(results[0][0])逻辑说明:自定义词典的格式是每行一个词条,至少包含词本身,后面可以跟词性和词频。词性用标准的 ICTCLAS 标签体系,比如n表示名词,nz表示其他专名。词频越高,模型越倾向于把多个字合并成一个词。但要注意,自定义词典不是万能的。如果某个词在训练数据里从来没出现过,强行加进去可能会导致分词边界错乱。我一般会先跑一批样本,看哪些词被切错了,再有针对性地加,而不是一次性把几百个词全塞进去。另外,词典文件必须是 UTF-8 编码,Windows 下用记事本保存容易变成 GBK,导致加载失败。
3.3 词性标签与 NER 标签的对照速查
LAC 输出的词性和 NER 标签是两套体系,很多人第一次用会搞混。词性标签描述的是这个词的语法类别,NER 标签描述的是这个词是否属于某个实体。下面这张表是我从实际输出里整理出来的高频标签,方便你快速对照。
| 标签类型 | 标签值 | 含义 | 示例 |
|---|---|---|---|
| 词性 | n | 普通名词 | 实验室、方案 |
| 词性 | nz | 其他专名 | 图像处理Demo |
| 词性 | v | 动词 | 发布、完成 |
| 词性 | a | 形容词 | 新、稳定 |
| 词性 | d | 副词 | 很、已经 |
| NER | PER | 人名 | A同学 |
| NER | LOC | 地名 | 某个科技园区 |
| NER | ORG | 机构名 | 某高校 |
| NER | TIME | 时间 | 2024年 |
| NER | O | 非实体 | 其他所有词 |
逻辑说明:词性标签和 NER 标签是独立输出的,同一个词可能既有词性标签又有 NER 标签。比如「某高校」的词性是n,NER 是ORG。在实际管道里,我一般会先用 NER 过滤出实体,再用词性做辅助规则。注意 LAC 的 NER 标签是 BIESO 体系的简化版,它直接输出每个词的实体类别,不区分 B 和 I,所以后处理时不需要做标签拼接,直接按词取即可。
4. 避坑指南:LAC 在实际项目里最容易翻车的五个点
4.1 模型下载卡住或超时
现象:第一次运行LAC()时,程序卡在下载模型文件,终端没有任何输出,等了几分钟直接报连接超时。原因:LAC 默认从远端拉取模型,内网环境或网络不稳定时就会失败。解决:提前在有外网的机器上跑一次,把用户目录下的.lac文件夹完整拷贝到目标机器的相同路径。或者设置环境变量LAC_MODEL_PATH指向离线模型目录。我一般会在 Dockerfile 里加一步RUN python -c "from lac import LAC; LAC()",把模型层固化到镜像里,避免每次启动都下载。
4.2 多线程调用时结果错乱
现象:在 Flask 或 FastAPI 里用多线程并发调lac.run,偶尔出现返回结果和输入文本对不上,或者直接崩溃。原因:LAC 的底层推理引擎不是线程安全的,多个线程同时调用同一个 LAC 实例会竞争内部状态。解决:每个线程单独创建一个 LAC 实例,或者用线程锁把run方法包起来。更推荐的做法是改用进程池,每个进程一个 LAC 实例,避免 GIL 和线程安全问题。参数上,如果并发量不大,加一个threading.Lock()是最省事的。
4.3 长文本截断导致实体丢失
现象:输入一段超过 500 字的文本,输出里后半部分的实体全部变成O。原因:LAC 模型有最大输入长度限制,默认是 512 个字符左右,超长部分会被截断。解决:在调用run之前先做分句,把长文本切成短句再批量推理。我一般用正则按句号、问号、感叹号切分,然后对每个短句单独跑 LAC,最后把结果按顺序拼回去。注意拼接时要保留原始标点,否则后续做关系抽取时会对不上位置。
4.4 自定义词典不生效
现象:明明在custom_dict里加了词,但分词结果还是把词切碎了。原因:词典文件的编码不是 UTF-8,或者词性标签写错了,或者词频设得太低。解决:先用file命令确认编码,再用iconv转成 UTF-8。词性标签必须用 LAC 支持的体系,不确定就只写词本身,不写词性和词频。词频建议设成 100 以上,太低会被模型忽略。另外,自定义词典是在模型加载时读取的,运行中修改文件不会生效,必须重启进程。
4.5 内存占用随批量增大而暴涨
现象:处理几万条文本时,内存从几百兆涨到几个 G,最后被 OOM 杀掉。原因:run方法会把所有结果一次性返回,如果输入列表太大,中间张量和输出列表都会占内存。解决:把大列表切成小批次,比如每 1000 条调一次run,处理完立刻把结果写文件或数据库,再释放引用。参数上,batch_size控制的是单次前向传播的样本数,而输入列表的长度控制的是总处理量,两者要配合调整。我一般会写一个生成器,每次 yield 一批文本,避免一次性加载全部数据。
5. 进阶技巧:用 LAC 做实体关系抽取的预处理与效果验证
5.1 从 NER 输出到关系抽取的中间层设计
LAC 本身只做词法分析,不做关系抽取。但它的 NER 输出可以直接作为关系抽取模型的输入特征。我一般的做法是:先用 LAC 拿到每个词的词性和实体标签,然后按实体标签把连续的实体词合并成一个实体片段,再记录每个实体在句子中的起止位置。这样后续无论是用规则匹配还是用深度学习模型,都能直接拿到结构化的实体列表。
from lac import LAC lac = LAC(mode='lac') def extract_entities(text): words, tags, ners = lac.run([text])[0] entities = [] current_entity = None for i, (word, ner) in enumerate(zip(words, ners)): if ner != 'O': if current_entity is None: current_entity = {'type': ner, 'words': [word], 'start': i} else: current_entity['words'].append(word) else: if current_entity is not None: current_entity['end'] = i - 1 current_entity['text'] = ''.join(current_entity['words']) entities.append(current_entity) current_entity = None if current_entity is not None: current_entity['end'] = len(words) - 1 current_entity['text'] = ''.join(current_entity['words']) entities.append(current_entity) return entities text = "某高校实验室在某个科技园区发布了图像处理Demo。" print(extract_entities(text))逻辑说明:这段代码把连续的相同 NER 标签合并成一个实体,并记录起止位置。注意 LAC 的 NER 输出不区分 B 和 I,所以合并逻辑就是简单的连续同标签合并。参数上,start和end是词级别的索引,不是字符级别,如果需要字符级位置,还要根据分词结果累加每个词的长度。这个中间层设计的好处是解耦:LAC 负责词法,关系抽取模型负责语义,两边可以独立迭代。
5.2 用人工标注小样本验证 LAC 的实体召回
LAC 在通用领域表现不错,但换到垂直领域,一定要先做小样本验证。我一般会从业务数据里随机抽 200 条,人工标注出所有实体,然后跑 LAC,算准确率和召回率。如果某个实体类型的召回率低于 80%,就要考虑加自定义词典或者换模型。验证脚本可以写得很简单:
# 假设 gold_entities 是人工标注的实体列表,pred_entities 是 LAC 输出的实体列表 def evaluate(gold_entities, pred_entities): gold_set = set([e['text'] for e in gold_entities]) pred_set = set([e['text'] for e in pred_entities]) tp = len(gold_set & pred_set) precision = tp / len(pred_set) if pred_set else 0 recall = tp / len(gold_set) if gold_set else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0 return precision, recall, f1逻辑说明:这个评估只比较实体文本,不比较实体类型和位置,适合快速摸底。如果要更严格,可以按实体类型分别算 P/R/F1。参数上,gold_set和pred_set用集合去重,避免同一个实体出现多次影响计算。我一般会把这个脚本跑三遍,取平均值,减少随机波动。
5.3 一个我踩过的坑:不要用 LAC 做情感分析
最后说一个我自己的教训。早期做舆情分析时,我图省事,直接拿 LAC 的词性标注结果去推情感倾向,比如看到形容词就认为是正面,看到否定词就反转。结果在反讽句和双重否定句上翻车翻得很惨。LAC 是词法分析工具,它的词性标签只描述语法类别,不携带情感极性。要做情感分析,必须单独训一个分类模型,或者用专门的情感词典。LAC 的正确用法是给下游模型提供分词和实体特征,而不是越俎代庖。这个习惯我后来一直保持:先明确每个工具的能力边界,再决定它在管道里的位置。希望帮到你。
本文还有配套的精品资源,点击获取