Every trained pipeline is defined by its config, and custom functions or architectures plug in through registries.
A spaCy pipeline is fully described by its config (Thinc's config system, an extended INI format). nlp.config shows it for any loaded pipeline, and it is saved as config.cfg with the model. Sections nest with dots, such as [components.ner.model], and values are JSON-like.
Two mechanisms make it powerful:
- Interpolation:
${section.key}refers to another value, so a setting like a width or a path is defined once. - Registered functions: a block containing
@architectures = "spacy.Tok2Vec.v2",@misc = ...or@readers = ...is replaced, when the config is resolved, by the result of calling that registered function with the block's other keys as arguments. Registries includearchitectures,misc,readers,loggers,optimizers,schedulesand more.
Register your own with a decorator such as @spacy.registry.misc("name.v1") or @spacy.registry.architectures(...) for a custom Thinc model. Version names (.v1, .v2) so old configs keep resolving after you change code. For spacy train, pass --code my_functions.py so the registrations are imported first.
Reading the config of en_core_web_sm shows the listener pattern: the shared tok2vec component computes token embeddings once, and the tagger and parser use spacy.Tok2VecListener.v1 to reuse them, while the NER has its own internal Tok2Vec. That's why disabling tok2vec breaks the tagger but not the NER, and why fine-tuning a shared tok2vec for one task can affect the others. Training configs can replace listeners with independent copies when components must not interfere.
Example
import spacy
from thinc.api import Config
@spacy.registry.misc("keyword_set.v1")
def keyword_set(words: list, lower: bool = True) -> set:
return {w.lower() if lower else w for w in words}
cfg = Config().from_str('''
[settings]
batch = 128
size = ${settings.batch}
[keywords]
@misc = "keyword_set.v1"
words = ["Refund", "Invoice"]
''')
print(cfg["settings"]["size"]) # 128 (interpolated)
print(sorted(spacy.registry.resolve(cfg)["keywords"])) # ['invoice', 'refund']
nlp = spacy.load("en_core_web_sm")
comps = nlp.config["components"]
print(comps["tagger"]["model"]["tok2vec"]["@architectures"]) # spacy.Tok2VecListener.v1
print(comps["ner"]["model"]["tok2vec"]["@architectures"]) # spacy.Tok2Vec.v2 (own copy)
print(comps["ner"]["model"]["@architectures"]) # spacy.TransitionBasedParser.v2Remember
- The config fully defines a pipeline; nlp.config shows it
- ${section.key} interpolates values; @registry blocks call registered functions
- Register custom functions and architectures with versioned names; use --code in training
- Listeners let components share one tok2vec; NER in core pipelines has its own
Watch out for
- Renaming or changing a registered function without bumping its version, breaking older configs that reference it.