免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

LTP 4.x 中文NLP实战:安装、模型下载、离线部署与报错排查

LTP 4.x 中文NLP实战:安装、模型下载、离线部署与报错排查 第一次把pip install ltp敲进终端的时候我心里想的是五分钟搞定结果当天晚上十一点我还在翻缓存在哪、模型为什么下不动、ltp.seg为什么返回值跟文档对不上。所以这篇笔记不打算写成一份转述官方文档的安装说明书而是把我在真实机器上从零装起来、跑起来、再跑稳的整个过程完整复盘一遍——包括环境怎么选、模型从哪来、五个任务的结果到底怎么读、长文本怎么切、内网机器怎么离线部署以及我踩过的那几个能让人怀疑人生的报错。需要先说一句LTP 这个词在不同圈子里指的东西完全不一样神经科学里它指长时程增强存储领域指长期存储技术而这里要说的是自然语言处理里的 Language Technology Platform也就是哈工大出品的那个中文 NLP 工具包。它把分词、词性标注、命名实体识别、依存句法分析、语义角色标注这五个任务打包成一个模型、一套 API对做中文信息抽取、文本分析、知识图谱前置处理的人来说是一个上手成本相当低的起点。这篇内容适合两类人一类是刚接触中文 NLP、想找个能用中文跑通全流程工具的新手另一类是已经在用别的分词器、想评估是否值得换成 LTP 的从业者。1. LTP 到底站在工具链的哪一层选工具之前先想清楚它在整条链路上的位置否则很容易出现装完了发现不是我要的东西。LTP 给我的感觉更像一把瑞士军刀它不是一个只会切词的轻量库也不是一个需要你自己训模型的框架而是一个已经训好、开箱即用的中文句法语义分析流水线。你给一句中文它一次性把词边界、词性、实体、句法树、语义角色全吐出来这种一模型多任务的设计在工程上省了很多拼接成本。1.1 一次调用拿五个任务的结果LTP 4.x 的核心接口是 pipeline 式的构造一个LTP实例把句子列表丢进去指定你要的任务列表它返回一个结果对象各个任务的结果挂在对象的不同属性上。这件事的价值在于五个任务共享同一个底层编码器你不需要为每个任务单独加载一个模型、单独跑一遍前向计算。对单机跑批量文本的场景来说这直接决定了你是花一份显存还是花五份。1.2 和常见中文工具放在一起比它的位置在哪很多人第一次选型时会拿 LTP 和 jieba、HanLP、spaCy 比但它们的定位并不完全重合硬比容易得出错误结论。我按实际使用感受整理了一张对照表工具核心能力依赖与部署适合的场景jieba分词、词性、关键词纯 Python极轻快速切词、关键词抽取、轻量预处理LTP 4.x分词、词性、NER、依存、SRLPyTorch transformers需下载模型需要句法结构、语义角色的中文分析HanLP分词到句法、多语言较重模型多学术研究、多任务对比实验spaCy通用 NLP 流水线英文生态强Python 模型包英文/多语言工程化流水线我想强调的是如果你的需求只是把句子切开别上 LTP杀鸡用牛刀启动一次模型的时间够 jieba 处理几十万条了。但如果你要做事件抽取、关系抽取、情感分析里那种需要谁对谁做了什么的结构依存句法和语义角色标注就是绕不开的一环这时候 LTP 的性价比就出来了。还有一个历史包袱必须提搜索LTP 安装你会看到大量关于pyltp的老文章。那是 LTP 3.x 时代的 Python 绑定需要自己编译 C 代码还要单独下载 cws.model、pos.model 之类的模型文件在 Python 3.9 以后基本是一场噩梦。新项目直接用 LTP 4.x别回头折腾pyltp。我在第 7 节会把这两个包的名字战争单独讲一遍因为这是新手最容易踩的第一个坑。2. 装在什么环境里比装什么更重要我见过太多装不上的问题追到最后不是包的问题是环境的问题。Python 生态里最经典的灾难就是你在 A 环境里 pip install在 B 环境里 python run然后对着满屏 ModuleNotFoundError 发呆。所以在动手之前先把这三个东西的版本关系理清楚。2.1 用一个干净的虚拟环境别省这一步我的建议是永远不要往系统 Python 里装 NLP 相关的包因为这类包会锁死 torch、transformers、numpy 的版本一旦冲突波及的是你机器上所有项目。用 venv 或者 conda 都行命令很简单python -m venv ltp-env # Linux / macOS source ltp-env/bin/activate # Windows ltp-env\Scripts\activate python -m pip install -U pip激活之后做一次自检确认 pip 和 python 指向同一套环境python -c import sys; print(sys.executable) python -m pip -V这两行输出的路径应该在同一级目录下。如果python的路径里有WindowsApps或者/usr/bin而 pip 指向虚拟环境那后面所有报错都能从这一条找到解释。2.2 先装 PyTorch别让 ltp 替你决定这一步是我踩出来的经验。pip install ltp会自动把 torch 作为依赖拉下来但它拉的是默认版本在 Linux 上大概率是带 CUDA 的大包在你没显卡的机器上白白占几个 G 的硬盘而在有的平台上又会装上 CPU 版等你想用 GPU 的时候又要卸了重装。正确顺序是先把 torch 按你的实际情况装好再装 ltp。# 只用 CPU python -m pip install torch --index-url https://download.pytorch.org/whl/cpu # 有 NVIDIA 显卡按驱动支持的 CUDA 版本选 python -m pip install torch --index-url https://download.pytorch.org/whl/cu121装完立刻验证import torch print(torch.__version__) print(torch.cuda.is_available())cuda.is_available()返回 False 但你有显卡通常是三件事驱动太旧、装的是 CPU 版 torch、或者 WSL 环境没打通。这三件事的排查顺序建议就是上面这个顺序从便宜的查起。2.3 装上 ltp 本体确认版本python -m pip install ltp python -c import ltp; print(ltp.__version__)这一步如果卡在依赖解析很久一般是 pip 在反复回溯 torch 的版本约束。这时候我习惯用pip install ltp --no-deps先把本体装上再手动把缺的包装齐虽然麻烦一点但能看清楚到底缺什么。装完之后建议顺手记一下版本号因为 LTP 4.x 各小版本之间的 API 有过调整后面代码跑不通时版本号是第一手线索。3. 第一次跑通模型从哪来代码怎么写包装好只是第一步真正让新手卡住的往往是模型在哪。LTP 4.x 用的是预训练模型加任务头的结构模型权重不在 pip 包里需要运行时下载或者提前准备好。这一点和 jieba 那种装完就能用、自带词典的体验完全不同所以第一次运行时看到终端长时间没有反应别急着 CtrlC。3.1 首次调用会自动下载模型from ltp import LTP ltp LTP(LTP/small)这一行执行时如果本地缓存里没有对应的模型它会去模型仓库拉取。small 是体量较小的版本适合先跑通流程base 效果更好但体积和推理开销都明显上升。我的建议是先用 small 把整条链路走通确认代码逻辑没问题再按效果需求换大模型。首次下载的体感差异很大取决于网络状况可能几十秒也可能十几分钟。判断它是不是真的在下而不是卡死了看缓存的体积变化就行Linux 下在~/.cache/huggingface/hubWindows 下在C:\Users\你的用户名\.cache\huggingface\hub。用ls -lh反复刷一下数字在涨就说明在动。3.2 一句中文五个任务一次跑完这是 LTP 4.x 我最喜欢的调用方式from ltp import LTP ltp LTP(LTP/small) output ltp.pipeline( [他叫汤姆去拿外衣。], tasks[cws, pos, ner, srl, dep] ) print(output.cws) print(output.pos) print(output.ner) print(output.dep) print(output.srl)这里有个新手极易踩的细节pipeline 的任务名用的是cws中文分词而不是你直觉里想的seg。写错任务名有的版本会直接报错有的版本会安静地跳过返回一个没有该字段的对象让人以为是模型没效果。另外注意输入必须是列表哪怕只有一句话也要包成列表直接传字符串有的版本会把它当字符序列处理结果非常诡异。3.3 分任务调用与合并调用的区别除了 pipelineLTP 还提供按任务单独调用的接口这种写法在只需要其中一两个任务时更省算力seg, hidden ltp.seg([他叫汤姆去拿外衣。]) pos ltp.pos(hidden) ner ltp.ner(hidden) dep ltp.dep(hidden) srl ltp.srl(hidden, keep_emptyFalse)注意seg返回的是两个值第二个是共享的隐藏状态后面的 pos、ner、dep 都基于它计算这样五个任务只用跑一次编码。如果你手上的文档里seg只返回一个值说明版本不同先用print(type(...))看一眼实际返回结构再写后续代码别照抄。keep_emptyFalse这个参数我强烈建议带上它会把没有谓词的句子对应的空 SRL 结果过滤掉输出干净很多否则你要在每个句子上做一次空判断。4. 结果怎么读五种输出的结构逐个拆跑通之后的第二个门槛是读结果。尤其是依存句法第一次看到[(2, SBV), (0, HED), ...]这种输出很多人会以为是坏了。其实每个元组是父节点索引 关系标签索引是 1 起算的0 表示这个节点是整句的根。4.1 分词、词性与命名实体分词输出是嵌套列表外层对应句子内层是词output ltp.pipeline([他叫汤姆去拿外衣。], tasks[cws, pos, ner]) print(output.cws) # [[他, 叫, 汤姆, 去, 拿, 外衣, 。]] print(output.pos) # [[r, v, Nh, v, v, n, wp]] print(output.ner) # [[(2, 2, Nh)]]NER 的返回值是(起始下标, 结束下标, 标签)三元组下标是闭区间这一点一定要记住切片的时候要写words[start:end1]写成words[start:end]会少一个字这种 bug 特别隐蔽。常用标签我列一下方便查标签含义标签含义Nh人名Ni机构名Ns地名n普通名词v动词r代词wp标点m数词4.2 依存句法的索引从 1 开始这一步是最容易算错的地方。以上面那句话为例假想的输出是print(output.dep) # [[(2, SBV), (0, HED), (2, DBL), (2, VOB), (4, VOB), (5, VOB), (2, WP)]]第一个元组(2, SBV)说的是第 1 个词他的主语是第 2 个词叫。因为索引从 1 开始如果你想在代码里访问父节点的词必须写words[head - 1]而不是words[head]。而要遍历某个词的孩子就得反过来做一次全表扫描把 head 等于目标索引的词收集起来。这套索引规则在解析依存树、构造关系抽取样本时如果不搞清楚会得到完全错误的结果而且错得很安静。常见的依存关系标签我也整理了一部分做实体关系抽取时最常打交道的就是这几个标签关系标签关系SBV主谓关系VOB动宾关系ATT定中关系ADV状中关系COO并列关系POB介宾关系DBL兼语HED核心根WP标点CMP动补关系上面那句他叫汤姆去拿外衣里出现DBL兼语正是叫这个兼语结构的体现——汤姆既是叫的宾语又是去的主语。能读出这一层说明你已经在用句法信息理解句子了这正是依存分析的价值所在。4.3 语义角色标注在解决什么问题SRL 输出的核心是谓词—论元结构一句话里哪个词是动作围绕它有哪些参与者各自扮演什么角色。A0 一般指施事A1 一般指受事A2 到 A4 是更外围的参与者ARGM 系列是时间、地点、方式这类修饰性成分。做事件抽取的时候SRL 可以帮你直接把谁对谁做了什么、在什么时候拆成结构化字段。不过要提醒一句SRL 的输出结构在 LTP 各版本之间变化比较大有的版本是字典列表有的是元组列表。我的习惯是先只对一个句子跑 SRL把整个对象打印出来看结构确认之后再写解析代码。不要照着博客里的例子硬写解析逻辑版本一换就崩。5. 长文本、批量和自定义词典跑通单句只是演示真正干活的时候你面对的是几十万条评论、几百万字文档。这时候有两个硬约束必须提前处理单句长度上限和批量吞吐。5.1 分句是第一道工序LTP 的底层是预训练语言模型存在最大输入长度限制通常在几百个 token 量级。你直接把一段三千字的文章丢进去结果要么被截断要么报错。所以进 LTP 之前必须先分句而且分句之后还要做一次长度兜底import re SPLIT_RE re.compile(r(?[。!?;])) def split_sents(text, max_len200): chunks [s.strip() for s in SPLIT_RE.split(text) if s.strip()] result [] for s in chunks: if len(s) max_len: result.append(s) else: # 超长句强制切分避免被模型截断 for i in range(0, len(s), max_len): result.append(s[i:i max_len]) return resultmax_len我给的是 200这是一个保守值。为什么不给到模型上限因为实际中文文本里两百字以上的句子往往本身就是标点缺失导致的粘句强行喂给模型句法树也会变得很难看。分句这一步做扎实后面所有任务的质量都会往上走一截。5.2 批处理与设备选择批量推理的写法很简单就是分批喂进去def run_batch(ltp, sentences, batch_size32, tasks(cws, pos, ner)): for i in range(0, len(sentences), batch_size): chunk sentences[i:i batch_size] yield ltp.pipeline(chunk, taskslist(tasks))batch_size不是越大越好。它和你的显存、平均句长直接相关我的经验是从 32 起步看显存占用往上调调到 OOM 之前的那一档再退回来一点。CPU 上跑的时候batch 的意义主要是摊薄 Python 侧的调用开销对速度提升远不如 GPU 明显。如果是纯 CPU 环境记得设置线程数让它别把机器吃满import torch torch.set_num_threads(4)还有一条容易忽略的整个进程里只构造一次LTP实例。我见过有人在循环里 new 一个 LTP跑一千条数据重复加载一千次模型那速度慢到你会以为是自己代码写得不好。把实例提到循环外面是所有推理类库的通用铁律。5.3 加自定义词典处理领域词通用模型遇到专业术语一定会切错。医疗、金融、法律文本里的术语在通用语料里出现频次极低模型倾向于把它们切成几个常用字。LTP 提供加词接口来缓解这个问题ltp.add_word(心脑血管疾病, freq100)需要说明的是加词的实际效果取决于版本实现有的版本影响分词解码有的只是在词典层面给一个先验。所以加完词之后一定要用几条真实样本验证一遍别加了就当它生效了。如果发现加词没用一个务实的替代方案是在分句之后、送进 LTP 之前用正则做占位替换把术语替换成一个不在文本中出现的单字跑完再把术语还原回去。这个土办法看着笨但在很多生产环境里非常稳。6. 模型缓存与内网离线部署这一节是我认为最值得写进笔记的部分因为绝大多数教程止步于跑通而真正卡人的是要在没有外网的生产机上跑。6.1 缓存目录在哪怎么改第一次下载的模型会落到默认缓存目录。Linux/macOS 是~/.cache/huggingface/hubWindows 是C:\Users\用户名\.cache\huggingface\hub。这个目录在系统盘上模型动辄几百兆机器多的时候很容易把根分区挤爆。解决办法是提前设置环境变量把缓存挪走export HF_HOME/data/hf_cache注意这个变量必须在Python 进程启动之前设置在代码里os.environ赋值往往来不及因为库在 import 阶段就已经把路径定死了。这是我踩过的一个坑在代码里改了半天没生效最后发现窗口期已经过了。Windows 上就设系统环境变量然后重开一个终端。6.2 手动下载模型并以本地路径加载离线部署的正确姿势是在一台有网的机器上把模型完整下载到本地目录再把这个目录整体拷贝到目标机器的固定路径最后用路径加载而不是用模型名# 有网的机器上把模型落到指定目录 huggingface-cli download LTP/small --local-dir /data/models/LTP-small# 目标机器上直接读本地目录 ltp LTP(/data/models/LTP-small)用本地路径加载有两个好处一是彻底摆脱运行时的网络依赖二是路径显式、可控不会出现某台机器上的缓存被清了但没人知道这种事故。拷贝之前记得检查目录完整性至少要确认配置文件和权重文件都在。如果加载时报找不到权重这一类错误八成是拷贝过程中断了或者漏了子目录用文件数量和总大小对一下最直接。6.3 打包成服务时的两个考虑如果要把它做成一个常驻的推理服务我建议进程启动时加载模型之后只处理请求不要每次请求都构造实例另外把输入长度校验放在入口处超长直接拒绝或者先分句别让异常在模型内部抛出来那样堆栈很难看也不好定位。还有内存这块要有预期。模型常驻内存之后进程的基线内存会被抬起来一个台阶做容量规划的时候要把这部分算进去别按空进程 数据估算。我见过服务上线之后被内存告警打爆最后发现就是模型本身的体积没有计入。7. 报错清单我踩过的坑和排查顺序最后这节是纯干货按我遇到问题的先后顺序排。7.1 名字战争pyltp 和 ltp 是两个东西ModuleNotFoundError: No module named ltp和ImportError: cannot import name LTP from ltp这两个报错指向的其实是同一个根因你装的东西不对。前者说明环境里根本没装后者最常见的情况是装上了pyltp或者某个同名的包而pyltp暴露的类是Segmentor、Postagger那一套没有LTP这个类。排查就三步python -c import sys; print(sys.executable) python -m pip list | grep -i ltp python -c import ltp; print(ltp.__file__)第三条输出的路径如果在虚拟环境之外的 site-packages 里那问题就是环境没对上。如果路径对、包名是pyltp那就卸载重装。顺便说pyltp需要编译 C 代码在比较新的 Python 上装起来极其痛苦新项目没有理由再选它。7.2 模型下载中断和加载失败下载中断的表现是首次运行长时间无响应然后抛出一堆网络相关的异常。处理办法不是反复重试同一条命令而是先看看缓存目录里有没有下了一半的残留文件有的话清掉再重来否则可能一直在用损坏的临时文件。更彻底的方案是走 6.2 的手动下载路线把下载和运行彻底解耦。加载失败这类错误还有个隐蔽原因磁盘空间不足导致文件写了一半。用df -h看一眼再排查比瞎猜快得多。7.3 CUDA 版本不匹配症状通常是torch.cuda.is_available()返回 False或者 import 时报找不到 CUDA 相关的动态库。排查顺序先看显卡驱动版本再看torch.version.cuda两者要能对上。对不上就重装对应 CUDA 版本的 torch而不是去升级驱动——升级驱动在生产环境里往往需要审批成本更高。另外一个细节如果你只是想验证代码逻辑先在 CPU 上跑通再切 GPU能省掉大量环境折腾时间。模型加载和结果解析的逻辑在 CPU 和 GPU 上完全一致验证效率高很多。7.4 Windows 下的编码与多进程Windows 上有两个高频问题。第一个是中文输出乱码终端编码和 Python 输出编码不一致chcp 65001切到 UTF-8或者设置PYTHONIOENCODINGutf-8基本能解决。第二个是多进程报错典型的堆栈是提示在引导阶段之前启动了新进程。原因是 Windows 用 spawn 方式创建子进程子进程会重新 import 主模块如果你的启动代码没有放进if __name__ __main__:保护块里就会无限递归。解决办法就一句话把所有启动逻辑包进那个判断里。这个坑在 Linux 上完全不会出现所以从 Linux 迁到 Windows 的时候很容易翻车。7.5 内存和显存溢出OOM 的常见诱因有三个batch 太大、单句太长、以及没有及时释放中间结果。处理顺序建议是先降 batch再砍单句长度最后才考虑上更小的模型。因为前两个是参数问题改一行就行换模型则要重新验证效果。还有一个不那么明显的原因把整批结果都堆在内存里没落盘。处理百万级文本的时候正确的做法是边跑边写用生成器逐批消费、逐批序列化别先攒成一个大 list 再统一写文件。我第一版脚本就是这么写的在十万条规模上内存直接起飞改成流式之后稳定得多。把这一圈走下来我对 LTP 的感受是它的能力上限取决于你怎么用而大部分的坑都不在模型本身而在环境、下载、输入长度和结果解析这些外围工程上。我个人在实际操作中的体会是先把 small 模型、CPU 环境、单句输入这条最简链路跑到能稳定输出再往批量、GPU、离线部署上叠加出问题时也更容易定位到是哪一层引入的。最后再分享一个小技巧把每次跑出来的结果对象用序列化方式存一份到磁盘后面调解析代码的时候就不用反复跑模型了改一行读一行调试效率能提高好几倍。
返回列表