简介:en_core_web_sm-2.2.5.tar.gz 是 spaCy 面向英文文本处理的小型预训练模型离线包,版本 2.2.5,适合 Python 自然语言处理初学者、数据科学学习者及内存资源有限的项目环境,可用于分词、词性标注、依存句法分析与命名实体识别等基础任务。包内共 30 个文件,以 json 配置与元数据、txt 说明、cfg 与 py 安装脚本、model/bin 模型权重及 tokenizer、vectors 等组件为主,压缩包约 11.46MB,结构紧凑,便于在本地快速部署和查阅模型组成。已有 1236 人学习下载,说明其在英文 NLP 入门与轻量级项目中具有一定参考价值。读者可据此了解 spaCy 小模型的目录组织、核心组件与基础能力边界,并把它用于实体识别、句法分析等练习或原型验证,在性能与资源占用之间取得平衡。
1. 拿到 en_core_web_sm-2.2.5.tar.gz 先别急着解压:它根本不是用来“打开”的
十个人里有八个拿到 en_core_web_sm-2.2.5.tar.gz,第一反应是双击解压,然后翻遍目录找 exe、找 README、找任何能“运行”的入口——结果只看到 setup.py、PKG-INFO 和几个名字像二进制文件的目录,直接懵住。它是一个标准的 Python 源码分发包(sdist),只不过它分发的东西不是代码,而是训练好的模型权重、词表和流水线配置。正确的“打开方式”是把它交给 pip,而不是交给解压软件。
它能提供的是一套固定的英文基础分析能力:分词、词性标注、依存句法、命名实体识别,11MB 量级,纯 CPU 就能跑批量文本。适合三类人:内网或离线机器上必须把英文 NLP 流水线拉起来的人;老项目锁在 spaCy 2.2.x 上、需要严格对齐版本的人;以及想用最低成本拿到一份实体抽取与句法基线的人。不适合拿它做语义召回,这点后面单独说。
2. 拆包:en_core_web_sm-2.2.5 的目录结构、meta.json 与版本锁
2.1 解开之后,哪个目录才是能加载的
这个包解开以后不是“一个模型文件夹”,而是“一个 Python 包 + 一个模型文件夹”。结构大致是这样:
en_core_web_sm-2.2.5/ ├── PKG-INFO ├── setup.py # 注册 entry point,pip install 靠它 └── en_core_web_sm/ ├── __init__.py # 里面就一行 load(),转调模型目录 ├── meta.json # 包级元信息,给 spacy validate 读 └── en_core_web_sm-2.2.5/ # ← 真正的模型目录 ├── meta.json # 加载器认的身份证 ├── tokenizer/ ├── tagger/ ├── parser/ ├── ner/ └── vocab/__init__.py里做的事非常少:拿到自己所在目录,再拼出内层模型目录的路径,调用加载函数。所以spacy.load("en_core_web_sm")之所以能工作,靠的不是这个目录名,而是 pip 安装时写进环境里的 entry point——你在任何工作目录下执行都能加载,是因为入口已经被注册进了解释器的包索引。
判断“哪个目录能加载”有个很土但从不失灵的办法:看哪个目录里同时存在meta.json和至少一个组件目录(tagger/、parser/、ner/)。只满足其中一半的目录,指过去就会报错。
2.2 meta.json 是这套东西的身份证,先读它再谈别的
很多人装完直接写业务代码,跑了两周才发现模型版本、组件顺序、评测值全对不上。花三十秒把 meta 打出来,能省掉后面一堆扯皮:
import json, spacy nlp = spacy.load("en_core_web_sm") meta = nlp.meta print("model version :", meta["version"]) # 2.2.5 print("spacy require :", meta["spacy_version"]) # 形如 >=2.2.2 print("pipeline :", meta["pipeline"]) # ['tagger', 'parser', 'ner'] print("vectors :", meta["vectors"]) # sm 是零向量 print("accuracy :", json.dumps(meta.get("accuracy", {}), indent=2))几个字段值得逐条抠:
version是模型自身的版本号,和 spaCy 的版本号是两条独立的线。装错模型版本时,业务代码可能照跑不误,只是精度悄悄掉一截,这种坑最难查。spacy_version是这个模型声明的运行时约束,2.2.5 这一版要求的是 2.2.x 区间。它不是一个建议值,加载时会被校验,越界基本就是启动即失败。pipeline是组件的执行顺序。顺序本身就是语义:分词器先出 token,tagger 给 token 打词性,parser 基于词性建依存树,ner 再基于前两者的输出判实体。少一环,后面那环的输入就变味了。vectors里width和keys都是 0,这是 sm 的定义性特征——它没有静态词向量。这个字段后面会被反复提起,因为它是绝大多数“相似度算出来是玄学”的根因。
accuracy里记录的是这一版发布时在通用英文语料上的评测值,量级大致是词性 97 上下、依存 UAS 约 91.7 / LAS 约 89.7、NER 的 F 值 86 左右。别把这些数字当承诺,它是“同分布测试集上的历史成绩”,换个领域(合同、病历、日志)掉十几个点是常态。但它的价值在于横向对比:当你纠结要不要换 md 或换 3.x 的新模型时,这是唯一能拿来做同口径比较的锚点。
2.3 三种落地方式,选错一种就白折腾半天
第一种,本地 pip 安装。最稳,适合单机开发机和能联网拉包的 CI:
# 注意:必须给路径,不能只写包名 pip install ./en_core_web_sm-2.2.5.tar.gz # 装完自检,会列出环境里所有已注册模型及其版本 python -m spacy validate参数说明:路径可以是相对路径也可以是绝对路径;pip install en_core_web_sm这种写法会去索引里找,离线环境必然失败,这是新手第一个翻车点。spacy validate会输出一张表,把模型名、版本、要求的三方版本列出来,装完必跑。
第二种,不安装,直接指向模型目录。适合容器镜像里不想污染 site-packages,或者要同时挂载多个版本做 AB 对比的场景:
import spacy from pathlib import Path MODEL_DIR = Path("/opt/models/en_core_web_sm/en_core_web_sm-2.2.5") nlp = spacy.load(MODEL_DIR) # 传目录,不传 tar.gz,也不传解压后的顶层目录这个写法要求路径精确到含meta.json的那一层。多一层少一层都会得到一个“找不到模型”的错误,而这个错误信息不会告诉你它到底找过哪些路径,所以第一次排查往往靠猜——不如一开始就把路径resolve()出来打印一遍。
第三种,打进镜像做固化。生产上更常见的做法是构建期pip install,运行期直接用名字加载,同时把 tar.gz 的 sha256 写进构建记录。这样半年后回滚时,你能确定当时跑的到底是哪一份权重,而不是“大概是那个 2.2 的包”。
3. 跑通最小闭环:加载、批量管道与结构化输出
3.1 先看默认流水线,再决定关掉谁
加载完第一件事不是写抽取逻辑,是确认组件和开销:
import spacy nlp = spacy.load("en_core_web_sm") print(nlp.pipe_names) # ['tagger', 'parser', 'ner'] print(len(nlp.vocab.vectors)) # 0 —— 再次确认没有静态词向量 # 只要词性和分词,句法和实体都不要 nlp_fast = spacy.load("en_core_web_sm", disable=["parser", "ner"]) print(nlp_fast.pipe_names) # ['tagger']disable传的是组件名列表,不是“功能名”。这几个组件里,parser 是最贵的一个:它要做完全依赖转移,长句上开销随长度近似线性上升,而 ner 又依赖 parser 的输出特征。实测里把 parser 关掉,单纯分词词性的吞吐通常能翻倍以上;NER 也关掉,还能再快一截。
提示:关掉 parser 的副作用是
doc.sents和doc.noun_chunks会直接不可用(抛异常,不是返回空)。如果业务只要分句不要句法树,可以自己插一个sentencizer:
import spacy nlp = spacy.load("en_core_web_sm", disable=["parser", "ner"]) sentencizer = nlp.create_pipe("sentencizer") nlp.add_pipe(sentencizer, first=True)add_pipe的first=True保证它在最前面执行,后面所有组件都能用上分句边界。这里要注意:create_pipe是 2.x 的写法,换成 3.x 得改成nlp.add_pipe("sentencizer", first=True),两套 API 不通用,抄代码时必须先看版本。
3.2 批量处理:nlp.pipe才是正路,for text in texts是慢性自杀
单个nlp(text)每次都会重新分配缓冲区、重新走一遍前向。批量场景下必须用nlp.pipe,它会把文本流按批打包、复用内存:
import spacy nlp = spacy.load("en_core_web_sm") nlp.max_length = 2_000_000 # 默认 1000000,超长文档先抬上限 def iter_docs(texts): # batch_size 按平均文本长度调,短文本 128~256,长文档 8~32 # n_threads=4 表示开 4 个线程;-1 表示用满核数 for doc in nlp.pipe(texts, batch_size=128, n_threads=4): yield doc参数该怎么定,说点实操的:
batch_size不是越大越好。它控制的是“一次打包多少篇做前向”,太大会把内存顶起来,尤其是文档平均几千词的时候。经验值是让单批的 token 总量落在 3 万到 10 万之间,按你的平均长度反推篇数。n_threads在 2.2.5 里是有效的,底层走的是按批并行。换成 3.x 这个参数名变成了n_process,写n_threads会被当成未知参数直接抛 TypeError——这个差异在老项目升级时坑过不少人。max_length改的是单篇文本的上限,不是批量的上限。它对应的是那个著名的 E088 报错,后面避坑章节细说。
3.3 把分析结果落成能入库的结构
模型输出的Doc对象是内存结构,跨进程传不了、也没法直接进数据库。落库前先拍平成字典,这一步的字段设计决定了后面能不能复用:
import json def to_record(doc): return { "text": doc.text, "tokens": [ { "i": t.i, "text": t.text, "lemma": t.lemma_, "pos": t.pos_, # 粗粒度词性,如 NOUN / VERB "tag": t.tag_, # 细粒度标签,如 NNS / VBD "dep": t.dep_, # 依存关系,如 nsubj / dobj "head": t.head.i, # 支配词的下标,-1 表示自指(ROOT) } for t in doc ], "ents": [ { "start": e.start_char, "end": e.end_char, "label": e.label_, "text": e.text, } for e in doc.ents ], "chunks": [c.text for c in doc.noun_chunks], } with open("out.jsonl", "w", encoding="utf-8") as f: for doc in nlp.pipe(texts, batch_size=64): f.write(json.dumps(to_record(doc), ensure_ascii=False) + "\n")几个参数说明:head存的是下标而不是文本,是因为文本会重复,下标在同一条记录内唯一,做树重建时更省事;start/end用字符偏移而不是 token 下标,是为了和原文对齐——后面要做高亮、要做规则回填,都依赖字符位置。chunks依赖 parser,如果前面 disable 了 parser,这一行要删掉,否则整个函数会在运行时炸掉。
词性和依存标签本身是有限集合,实际取值大约四十来个词性、四十来个依存关系。花十分钟把nsubj(主语)、dobj(直接宾语)、amod(形容词修饰)、prep/pobj(介词及其宾语)这几个记住,写规则时就不用每次都去查表:
| 标签 | 含义 | 典型用途 |
|---|---|---|
| nsubj | 名词性主语 | 抽取“谁做了什么”的主体 |
| dobj | 直接宾语 | 抽取动作承受者 |
| amod | 形容词修饰语 | 抽取属性词,做商品/评论挖掘 |
| compound | 名词复合 | 拼多词术语,如 “data pipeline” |
| pobj | 介词宾语 | 抽取 “in 2023” 这类时间/地点 |
4. 避坑清单:五个真实翻车点,从 E050 到长文本截断
4.1 加载类:E050 与版本锁
现象一:ValueError: [E050] Can't find model 'en_core_web_sm'.
原因只有两种,且九成是第一种:tar.gz 只是解压了、没有pip install,所以 entry point 根本没注册;或者装到了另一个解释器环境里(比如系统 python 装完,实际跑的是虚拟环境里的)。少数情况是spacy.load传的字符串拼错了,模型名是全小写下划线,不是目录名。
解决:先跑python -m spacy validate确认当前解释器到底装了什么;再python -c "import sys; print(sys.executable)"确认这个解释器路径和你执行脚本用的是同一个。两步都对了,E050 不会出现。离线机器上没条件装包,就直接走“指向目录”那条路,绕开 entry point。
现象二:装完能 import,但加加载就报组件工厂缺失(E002 系列),或者直接抛出版本不满足的 ValueError。
原因是模型和运行时的版本线错位。2.2.5 这个模型包声明的运行时区间锁在 2.2.x,如果你所在的环境已经升到 3.x,加载时会先撞上版本校验;偶尔校验没拦住的场景,会退化成“组件名在工厂表里找不到”的报错——因为 3.x 重构了组件注册机制,2.x 的组件名映射关系已经变了。
解决:要么把 spaCy 降回 2.2.x 区间,要么换成 3.x 对应的模型包,两者不要混搭。生产上更稳的做法是把这个依赖写成spacy>=2.2.2,<2.3.0这样的区间约束,而不是spacy裸装——裸装今天能跑,明天构建就可能挂。
现象三:pip install en_core_web_sm==2.2.5报找不到可用发行版。
原因是这个包根本没发布到公共索引上,它是以 tar.gz 形式分发的本地文件。写包名去装,索引里当然没有。
解决:pip install ./en_core_web_sm-2.2.5.tar.gz,路径必须带上。如果是内网机器,还可以先把包传到内网的文件目录再装;对于容器,可以在 Dockerfile 里先COPY再RUN pip install ./xxx.tar.gz,注意COPY要放在 pip 安装之前,否则缓存层会失效导致每次都重装。
4.2 运行类:长文本、线程参数与相似度
现象四:处理长文档时报[E088] Text of length X exceeds maximum of 1000000。
原因是 2.x 的句法和实体组件要求整篇文本作为一个整体进入模型,默认上限一百万字符。一份几万字的技术手册或一整本转录稿很容易撞线。
解决分两种:无损方案是先按段落切块,逐块nlp(chunk),再把结果按偏移量拼回去——注意拼的时候每个块的start_char要加上该块在原文档中的起点;有损但省事的方案是直接nlp.max_length = 5000000,代价是内存占用会跟着涨,一份两百万字符的文本在做依存分析时可能吃掉好几个 G。我的做法是:超过十万字符的文档一律走切块路线,max_length只当兜底。
现象五:算两个词的similarity(),结果时高时低,同义词经常不如反义词像。
原因不在代码,在模型。sm 版本的vocab.vectors是空的(前面len(nlp.vocab.vectors) == 0验证过),相似度只能退化成上下文张量的均值比较,而张量是随句子变化的,同一对词换个句子分数就变。这不是玄学,是缺数据。
解决:做语义召回就换带静态词向量的模型(md 或 lg 那两档),或者干脆上专门的句向量模型。sm 的正确用途是词性、句法、实体这些“结构信息”,别让它干语义的活。这个坑我见过太多次,本质是把“小模型”误解成“轻量版大模型”,其实它们是两种不同的东西——sm 砍掉的就是向量。
注意:
n_threads与 3.x 的n_process不通用,create_pipe与add_pipe("name")不通用,GoldParse与Example不通用。这三处是 2.2.5 与 3.x 之间最容易抄错的 API 分界线,抄任何示例代码前先看一眼它属于哪条版本线。
5. 在 sm 模型上做定制:EntityRuler 注入与 ner 增量微调
5.1 规则先行:EntityRuler 解决 80% 的领域词问题
通用模型在你自己的领域里必然漏实体——产品型号、内部系统名、行业缩写,这些词在训练语料里就没出现过,模型没有任何理由认识它们。最快的补法不是训练,是规则:
import spacy from spacy.pipeline import EntityRuler nlp = spacy.load("en_core_web_sm") # overwrite_ents=True:规则命中时覆盖模型给出的实体边界 # phrase_matcher_attr="LOWER":匹配前统一小写,避免大小写不一致漏匹配 ruler = EntityRuler(nlp, overwrite_ents=True, phrase_matcher_attr="LOWER") ruler.add_patterns([ {"label": "PRODUCT", "pattern": [{"LOWER": "widget"}, {"LOWER": "pro"}]}, {"label": "PRODUCT", "pattern": [{"LOWER": "widget"}, {"LOWER": "pro"}, {"LOWER": "max"}]}, {"label": "ORG", "pattern": [{"LOWER": "acme"}, {"LOWER": "lab"}]}, ]) # before="ner":规则先跑,模型后跑,这样 overwrite_ents 才有意义 nlp.add_pipe(ruler, before="ner") doc = nlp("We deployed Widget Pro Max on the Acme Lab cluster.") print([(e.text, e.label_) for e in doc.ents])参数解释:before="ner"是关键,顺序反了规则就只能补模型的漏,没法纠正模型的错边界;overwrite_ents=True打开后,规则命中区间内模型原本给出的实体会被替换掉,适合“模型把产品名误判成人名”这类场景;phrase_matcher_attr="LOWER"让小写模式能匹配任意大小写写法,代价是无法再区分大小写敏感的模式——如果两者都要,就写两条规则,一条带LOWER,一条带TEXT。
规则模式本身是 token 级的字典列表,每个字典是一组属性约束,相邻字典之间是“紧邻”关系。想放宽成“中间允许隔几个词”,得换 PhraseMatcher 或者用{"OP": "*"}这类操作符。别一上来就写复杂模式,先用最朴素的紧邻匹配跑一遍,看召回够不够,不够再加。
5.2 规则不够了:用几十条标注做 ner 增量微调
规则覆盖率上不去的时候(长尾写法太多),才轮到微调。2.2.5 这条版本线的训练接口和 3.x 完全不同,是GoldParse+nlp.update的组合:
import spacy from spacy.gold import GoldParse from spacy.util import minibatch nlp = spacy.load("en_core_web_sm") ner = nlp.get_pipe("ner") # 新标签必须先注册,否则训练时会被静默忽略 ner.add_label("PRODUCT") # 只训练 ner,冻结其他组件,避免几十条数据把词性和句法带偏 frozen = [name for name in nlp.pipe_names if name != "ner"] with nlp.disable_pipes(*frozen): optimizer = nlp.resume_training() for itn in range(20): losses = {} for batch in minibatch(TRAIN, size=8): docs, golds = [], [] for text, annots in batch: doc = nlp.make_doc(text) # 只跑分词,不跑流水线 gold = GoldParse(doc, entities=annots["entities"]) docs.append(doc) golds.append(gold) nlp.update(docs, golds, sgd=optimizer, losses=losses) print(itn, losses)TRAIN的格式是[("文本", {"entities": [(start, end, "LABEL"), ...]}), ...],偏移量是字符级、左闭右开,标错一位整个样本就废了——这是标注阶段最常见的低级错误,建议写完先做一次边界断言。
几个参数的分量:minibatch(size=8)的小批量大小在几十条样本的规模下别超过 16,否则一轮只有两三次更新,收敛全靠运气;nlp.resume_training()拿到的是接着现有权重的优化器,不是从头初始化,这正是“增量”的含义;disable_pipes(*frozen)这一步很多人省掉,结果是精度没涨、原有的词性标注反而退化了——血泪经验是:小样本微调时,能冻结的一律冻结。
样本量上,新实体类型给到 50~200 条基本能看出效果,低于 30 条建议回到规则路线。训练轮次不用多,20 轮之后 losses 不降就说明数据量到顶了,继续训只会过拟合。
5.3 把定制结果固化下来,别让它活在 notebook 里
调完不保存,下次重启全没了。同时把版本信息一起写进去,否则半年后没人说得清这份权重是怎么来的:
import json, hashlib, pathlib nlp.meta["version"] = "2.2.5+custom-rules-2024" # 自定义后缀,便于区分 nlp.meta["description"] = "en_core_web_sm 2.2.5 + entity_ruler + ner finetune" nlp.to_disk("./model_custom") # 顺手记录一份可追溯的指纹 digest = hashlib.sha256( pathlib.Path("./model_custom/ner/model").read_bytes() ).hexdigest()[:16] print("ner weights sha256[:16] =", digest) # 从磁盘加载:这里走的是目录,不需要 entry point nlp2 = spacy.load("./model_custom") print(nlp2.pipe_names) # ['tagger', 'parser', 'entity_ruler', 'ner']to_disk会把整个流水线(包括规则里挂的 patterns)序列化出去,entity_ruler的规则也一并保存,所以加载回来的pipe_names里能看到它。这个自定义的version字段不会影响加载,纯粹给人看——但正是这种“给人看”的字段,在出问题时能救你一命。
6. 验收这份模型:吞吐、边界与版本三元组的三个硬指标
装完、跑通、定制完,还不算结束。真正决定这份模型能不能进生产的,是三件事:跑得够不够快、边界情况会不会崩、半年后能不能原样复现。这三项都不需要复杂工具,一段脚本加一张表就够。
import time import spacy TEXTS = ["..."] * 500 # 换成你业务里的真实样本,别用玩具句子 CONFIGS = { "full": [], "no_parser": ["parser"], "tagger_only": ["parser", "ner"], } for name, disable in CONFIGS.items(): nlp = spacy.load("en_core_web_sm", disable=disable) # 预热:第一次调用会触发内存分配和缓存建立,计时必须跳过 list(nlp.pipe(TEXTS[:20], batch_size=20)) t0 = time.perf_counter() n_tok = sum(len(doc) for doc in nlp.pipe(TEXTS, batch_size=64, n_threads=4)) cost = time.perf_counter() - t0 print(f"{name:14s} {cost:6.2f}s {n_tok / cost:9.0f} tok/s")这段脚本有两个细节决定了结果可不可信:预热不能省,第一次pipe里混着模型文件映射和线程池创建,能占到总耗时的三成以上;统计口径用 token 数而不是文档数,否则长短文本混在一起时,两次跑出来的数字没有可比性。
三项验收指标,我一般按这个表逐条过:
| 验收项 | 关注点 | 通过标准 | 怎么测 |
|---|---|---|---|
| 吞吐 | tagger / parser / ner 各自的边际成本 | 关掉不需要的组件后,吞吐满足业务峰值 | 上面的计时脚本,逐档对比 |
| 边界 | 超长文档、全大写、纯符号、空串 | 不抛 E088,不返回半截结果 | 构造极值样本跑一遍,看异常类型 |
| 实体边界 | 多词实体是否被截断 | 抽样 50 条人工核对 start/end | 打印e.start_char, e.end_char与原文切片对比 |
| 可复现 | 模型版本 + 运行时版本 + 自定义后缀 | 三者齐全,能从零重建 | 记录nlp.meta["version"]、spacy.__version__、权重哈希 |
边界测试里最容易漏的是空串和纯符号串。空串在 2.2.5 上多数情况能安全返回空Doc,但如果你的后处理里有doc[0]这种写法,立刻就是 IndexError;纯符号串则会暴露分词器的 special case 规则,某些连字符、引号组合会被切成意料之外的 token 序列,进而影响下游的偏移量计算。
可复现这一项,我习惯叫它“版本三元组”:模型版本、运行时版本、自定义后缀,三个都要写进配置,缺一个就等于没有。只写en_core_web_sm是不够的,因为同名的包在不同时间点可能指向不同版本;只写2.2.5也不够,因为你不知道当时跑的是纯模型还是加过规则的版本。
顺带一个提速技巧,不改变任何精度:如果业务里同一段文本要反复分析多次(比如先抽实体、再抽关系),务必只调一次nlp(text),把Doc对象存下来复用。Doc是惰性计算的,doc.ents第一次访问才真正触发 ner,重复访问不会再跑一遍;而每次重新nlp(text)都是完整前向。这一个习惯在批量任务里省下的时间,通常比调batch_size明显得多。
最后说个踩过的坑:早期我把这份模型直接塞进一个长时间运行的服务里,加载一次复用,结果某次热更新后新加的规则完全不生效——原因是服务进程里nlp对象是启动时构造的,配置热加载只更新了文件、没重建对象。从那以后我每次改完流水线配置,都强制走一遍「重载 →pipe_names断言 → 三条抽样文本比对实体」的流程,宁可多停两秒,也不相信“应该已经生效了”。希望帮到你。
本文还有配套的精品资源,点击获取