効率的なテキスト処理とエンティティ認識のための SpaCy の 3 つのテクニック
この記事は、大規模な文書処理や本番環境でのパフォーマンスボトルネックを解消するために、spaCy のパイプライン選択的ロード、並列バッチ処理、ルールと統計のハイブリッド利用という 3 つの実践的な最適化テクニックを紹介している。
キーポイント
不要なコンポーネントの無効化による高速化
デフォルトではロードされるトークナイザー、構文解析器など、特定のタスクに不要な NLP コンポーネントを明示的に無効化することで、メモリ使用量と処理時間を大幅に削減できる。
並列バッチ処理の実装
単一の文書ではなく、複数の文書をまとめてバッチとして処理し、CPU のマルチコア活用や GPU への負荷分散を可能にする手法により、スループットを向上させる。
ルールベースと統計モデルのハイブリッド
汎用的な統計モデルの限界を補うため、ドメイン固有のエントリや特定の形式に対してルールベースのマッチングを組み合わせて精度と速度を両立させる。
ロード時のコンポーネント除外による最適化
依存解析器や品詞タグ付け器等、不要なコンポーネントを `exclude` パラメータで指定してモデル読み込み時に除外することで、初期化時間とメモリ使用量を削減できます。
実行時の一時無効化による高速化
`nlp.select_pipes()` コンテキストマネージャを使用することで、特定の処理フロー中に不要なコンポーネント(例:属性ルーターやレマタイザー)を一時的に無効にし、CPU 負荷を軽減できます。
コンポーネントの除外による高速化
spacy.load() に exclude パラメータを指定することで、不要なコンポーネント(例:構文解析や品詞タグ付け)をメモリに読み込ませず、計算コストの高い処理をスキップして NER の精度を維持したまま速度を向上させます。
nlp.pipe() を活用したバッチ処理
ループ内で個別に nlp オブジェクトを呼び出すのではなく、nlp.pipe() を使用してストリームとして文書を処理し、内部バッファリングとマルチコア並列化を活用することで、大規模データセットでの処理効率を劇的に改善します。
影響分析・編集コメントを表示
影響分析
この記事は、AI/NLP エンジニアリングにおいて、理論的なモデルの性能だけでなく、実際の運用環境でのスケーラビリティとコスト効率を最適化するための具体的な手法を提供しています。特に大規模データ処理が必要な企業やサービス開発者にとって、デフォルト設定からの脱却とパフォーマンスチューニングの重要性を再認識させる実用的なガイダンスとなっています。
編集コメント
本番環境での NLP パイプライン構築において、モデルの精度だけでなくインフラ側の最適化が不可欠であることを示唆する良質な技術記事です。
image**
# イントロダクション
特に現代の大規模言語モデルのおかげで、自然言語処理(NLP)は、現代の AI およびソフトウェアシステムの基盤となる柱となっています。検索エンジンやチャットボットから、自動化されたカスタマーサポートのルーティング、エンティティ抽出パイプラインに至るまで、あらゆるものを支える技術として NLP の手法とテクノロジーを見つけることができます。Python における本番環境向けの NLP においては、spaCy が疑いの余地なく業界標準です。spaCy は本番利用のために特別に設計されており、産業レベルの速度、事前学習済み統計モデルおよびトランスフォーマーモデル、そして直感的な API を提供します。
残念ながら、多くの開発者は spaCy を単なるシンプルなブラックボックスのモノリスとして扱っています。彼らはモデルを読み込み、テキスト上で実行し、デフォルトの処理速度や抽出制限をそのまま受け入れています。ローカルプロトタイプから数百万文書の処理へとスケールする際、これらのデフォルト設定は計算上のボトルネックとなり、レイテンシの増加、肥大化したメモリフットプリント、ドメイン固有のエンティティの見落としを引き起こす可能性があります。高パフォーマンスなテキスト処理パイプラインを構築するためには、spaCy の内部実行フローを最適化する方法を理解する必要があります。
本記事では、処理速度の最大化とエンティティ認識のカスタマイズを実現するために、すべての開発者がツールキットに備えておくべき 3 つの重要な spaCy のテクニック——選択的なパイプライン読み込み、並列バッチ処理、およびハイブリッドルールベース統計的エンティティ認識——について探ります。
始める前に、spaCy およびその軽量な汎用英語モデルがインストールされていることを確認してください。
pip install spacy
python -m spacy download en_core_web_sm
# 1. 選択的なパイプライン読み込みとコンポーネントの無効化
デフォルトでは、事前学習済み spaCy モデル(例:en_core_web_sm)を読み込む際、spaCy は完全な NLP パイプラインを初期化します。このパイプラインには通常、以下の要素が含まれます。
- トークナイザー (tokenizer)
- 品詞タグ付け器 (tagger)
- 依存関係構文解析器 (parser)
- レマタイザー (lemmatizer)
- 属性ルール (attribute_ruler)
- 固有表現認識器 (ner)
この完全なデフォルトの豊富な機能セットは非常に優れていますが、計算オーバーヘッドが大きいという欠点があります。アプリケーションで固有表現認識 (NER) のみを行う必要がある場合、依存関係構文解析とレマタイゼーションを実行することは CPU サイクルとメモリの無駄になります。逆に、テキストのクリーニングやレマの抽出のみを行う場合は、深い統計的 NER モデルを実行するのは非常に非効率的です。これを最適化するには、読み込み時にコンポーネントを選択的に除外するか、コンテキストマネージャーを使用して実行中に一時的に無効化することができます。
この素朴なアプローチでは、コンポーネントの出力が実際に使用されるかどうかに関わらず、すべてのデフォルトコンポーネントをテキストに対して読み込み実行します:
import spacy
import time
小規模英語モデルを読み込む
nlp = spacy.load("en_core_web_sm")
texts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000
素朴な実行:すべてのドキュメントに対してタグ付け器、構文解析器、語形復元器、および名前付きエンティティ認識を実行する
ここでは名前付きエンティティのみに関心があると仮定する
start_time = time.time()
for text in texts:
doc = nlp(text)
entities = [(ent.text, ent.label_) for ent in doc.ents]
duration_full = time.time() - start_time
print(f"Full pipeline processed 1,000 docs in: {duration_full:.4f} seconds")
出力:
Full pipeline processed 1,000 docs in: 2.8540 seconds
次に、実行を特定の 2 つの方法で最適化しましょう。第一に、読み込み時に依存構文解析器のような重く使用されないコンポーネントを除外します。第二に、nlp.select_pipes() を使用して、特定のワークロードを処理する際に一時的にコンポーネントを無効化します。
import spacy
import time
読み込み時間の最適化:最初から重たい構文解析器とタグ付け器を除外する
これにより初期化時間とメモリ使用量が削減される
nlp_optimized = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
texts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000
コンテキストマネージャーの最適化、コンポーネントの一時的無効化
パーサーとタグラーは完全に除外し、ここでは属性ルーターとレキシコライザーを無効化します
start_time = time.time()
with nlp_optimized.select_pipes(disable=["attribute_ruler", "lemmatizer"]):
for text in texts:
doc = nlp_optimized(text)
entities = [(ent.text, ent.label_) for ent in doc.ents]
duration_opt = time.time() - start_time
print(f"Optimized pipeline processed 1,000 docs in: {duration_opt:.4f} seconds")
print(f"Speedup: {duration_full / duration_opt:.2f}x faster!")
実行時間の比較をしましょう:
フルパイプラインは 1,000 ドキュメントの処理に 2.8739 秒かかりました
最適化されたパイプラインは 1,000 ドキュメントの処理に 1.7859 秒かかりました
速度向上: 1.61 倍高速!
最適化された例では、spacy.load() に exclude=["parser", "tagger"] を渡すことで、これらのコンポーネントがメモリにロードされるのを完全に防いでいます。ほぼ同じ結果を得る別の方法として、ここでは処理を一時的に無効化するために disable=["attribute_ruler", "lemmatizer"] を渡しました。その効果は、テキストを処理する際に spaCy が数学的に計算コストの高いトークン依存関係解析と品詞タグ付けをスキップし、直接エンティティ認識に進むことです。これにより、NER(自然言語処理におけるエンティティ認識)の精度に全く影響を与えずに目に見える速度向上が得られ、規模が大きくなるほどその利点はさらに顕著になります。
# 2. nlp.pipe とメタデータ伝播による高スループットバッチ処理
大規模なコーパス(例えば pandas DataFrames、データベースの行、または生テキストファイル)を反復処理する際、ループ内で個々の文字列に対して nlp オブジェクトを呼び出す(例:[nlp(text) for text in texts])ことはアンチパターンです。
逐次処理では、spaCy がメモリバッファの最適化、操作のグループ化、マルチコア並列化の活用を防いでしまいます。また、データベースへの保存や ETL パイプラインのためにテキストを処理する際、結果として得られたエンティティを正しいデータベース行にマッピングできるようにするために、レコード ID、タイムスタンプ、カテゴリなどのメタデータを NLP プロセスを通じて保持する必要があります。
その解決策は nlp.pipe() を使用することです。このメソッドはドキュメントをストリームとして処理し、内部でバッファリングし、マルチプロセッシングをサポートします。as_tuples=True を設定することで、(text, context) のタプルを spaCy に供給できます。すると (doc, context) ペアが返され、メタデータをパイプラインにそのまま渡すことができます。
この素朴なアプローチは処理を逐次実行し、結果のドキュメントとデータベース ID を整合させるために手動でインデックスを追跡するため、脆く、かつ低速です:
import spacy
import time
nlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
一意の ID を持つ生データベースレコード
records = [
{"id": f"DB-REC-{i}", "text": "Google was founded in September 1998 by Larry Page and Sergey Brin."}
for i in range(1000)
]
シーケンシャルループ:低速かつ手動管理されるメタデータ
start_time = time.time()
extracted_data = []
for i, record in enumerate(records):
doc = nlp(record["text"])
entities = [(ent.text, ent.label_) for ent in doc.ents]
extracted_data.append({
"id": record["id"],
"entities": entities
})
duration_seq = time.time() - start_time
print(f"Sequential loop processed 1,000 docs in: {duration_seq:.4f} seconds")
出力:
Sequential loop processed 1,000 docs in: 2.7375 seconds
ここでは、nlp.pipe を使用してデータをストリームし、バッチ処理とマルチコア並列化 (n_process) を活用しながら、データベース ID をコンテキスト変数として一緒に転送します:
import spacy
import time
子プロセスが参照できるように、インポートと定義はグローバルに保持してください
nlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
実際の実行コードを main ブロックでラップします
if __name__ == '__main__':
records = [
{"id": f"DB-REC-{i}", "text": "Google was founded in September 1998 by Larry Page and Sergey Brin."}
for i in range(1000)
]
start_time = time.time()
# 入力を (テキスト、コンテキスト) のタプルのリストとしてフォーマットします
stream_input = [(rec["text"], rec["id"]) for rec in records]
# バッチをストリームし、利用可能なすべての CPU コアを n_process=-1 で使用します
extracted_data_pipe = []
docs_stream = nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1)
for doc, rec_id in docs_stream:
entities = [(ent.text, ent.label_) for ent in doc.ents]
extracted_data_pipe.append({
"id": rec_id,
"entities": entities
})
duration_pipe = time.time() - start_time
print(f"nlp.pipe processed 1,000 docs in: {duration_pipe:.4f} seconds")
print(f"Speedup: {duration_seq / duration_pipe:.2f}x faster!")
Output:
nlp.pipe processed 1,000 docs in: 7.1310 seconds
最適化されたコードスニペットでは、入力データセットを (テキスト文字列,メタデータコンテキスト) というタプルのシーケンスに再構成しています。nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1) を呼び出す際:
- batch_size=256 は、spaCy に 256 件ごとにテキストをバッファリングして処理させるよう指示し、内部の Python ループオーバーヘッドを最小限に抑えます
- n_process=-1 は、spaCy にシステムの CPU コア数を自動的に検出し、トークン化とコンポーネント抽出を利用可能なすべてのコアで並列実行させるよう指示します
- as_tuples=True は、spaCy に (doc, context) のペアを返すように指示し、メタデータ(レコード ID)が処理されたドキュメントと完全に整合するように保ちます。これにより、手動でのインデックス配列やリストの整列コードが必要なくなります
鋭い読者であれば、並列バッチ処理コードの処理時間が前バージョンよりも実際には増加している点に気づくでしょう。しかし、これは並列ジョブの設定に伴うオーバーヘッドによるものであり、処理するドキュメント数が増大すれば、その節約効果が明らかになります
上記のコード断片を 1,000 件ではなく 10,000 件のレコードで再実行すると、以下の結果が得られます:
逐次ループでは 1,000 ドキュメントを処理するのに 27.6733 秒かかりました。
nlp.pipe では 1,000 ドキュメントを処理するのに 11.5444 秒かかりました。
このように、節約効果がどのように積み重なっていくかがお分かりいただけるでしょう。
# 3. EntityRuler を用いたハイブリッド名詞句認識 (Named Entity Recognition)
事前学習された統計モデルやトランスフォーマーベースの NER モデルは、文脈に基づいて ORG(組織)、PERSON(人物)、DATE(日付)といった一般的なエンティティタイプを認識する上で非常に強力です。しかし、これらのモデルはトレーニング時に遭遇していないため、ドメイン固有の用語(カスタム製品 SKU、レガシーコード ID、極めてニッチな医療用語など)を認識できないことが頻繁にあります。
カスタムエンティティに対して深層学習統計モデルをファインチューニングすることも一つの解決策ですが、これは数千文のラベル付けを必要とし、その過程で標準的なエンティティの認識能力を失うという「壊滅的忘却 (catastrophic forgetting)」のリスクを伴います。
よりクリーンで効率的な解決策は、spaCy の EntityRuler を用いたハイブリッド NER アプローチです。EntityRuler を使用すると、パターン(正規表現またはトークンベースの辞書)を定義し、パイプラインに直接注入することができます。これを統計的な NER モデルの前に追加して、決定論的なエンティティを事前タグ付けし、モデルが文脈に基づいて判断するのを支援させるか、あるいは後に追加してフォールバックまたは上書き機能として動作させることができます。
開発者は、統計的な NER(自然言語処理における名前付きエンティティ認識)のギャップを埋めるために、spaCy パイプラインを実行した後にテキストに対して正規表現(regex)を実行しようとすることがよくあります。その結果、手動での座標オフセット計算が必要となり、データ構造が分断されてしまいます:
import spacy
import re
nlp = spacy.load("en_core_web_sm")
text = "Please review system ticket ID: TKT-98421 on our corporate portal."
doc = nlp(text)
標準的な統計的 NER はカスタムチケット ID を見逃す
entities = [(ent.text, ent.label_) for ent in doc.ents]
print("Before post-process:", entities)
正規表現による後処理パッチ
ticket_pattern = r"TKT-\d+"
matches = re.finditer(ticket_pattern, text)
custom_ents = []
for match in matches:
# スパンを構築するには複雑な文字からトークンへのオフセット変換が必要
custom_ents.append((match.group(), "TICKET_ID"))
現在、2 つの分断されたエンティティリストがあり、これらを手動でマージする必要があります
print("Regex entities:", custom_ents)
出力:
Before post-process: []
Regex entities: [('TKT-98421', 'TICKET_ID')]
EntityRuler コンポーネントをパイプラインに直接追加することで、ルールベースの正規表現パターンと統計的解析を単一の統合された doc.ents 出力にマージできます:
import spacy
nlp = spacy.load("en_core_web_sm")
entity_ruler コンポーネントをパイプラインに追加します。ner の前に配置してエンティティを事前タグ付けしますが、後でも動作します
ruler = nlp.add_pipe("entity_ruler", before="ner")
トークンレベルのパターンを定義する(正規表現を含む)
patterns = [
# "TKT-" で始まり、その後に数字が続く文字列に一致
{"label": "TICKET_ID", "pattern": [{"TEXT": {"REGEX": "^TKT-\d+$"}}]},
# 特定のドメインフレーズを正確に一致させる
{"label": "ORG", "pattern": "corporate portal"}
]
ruler.add_patterns(patterns)
text = "Please review system ticket ID: TKT-98421 on our corporate portal."
doc = nlp(text)
統計的アプローチとルールベースの両方のエンティティが doc.ents 内に統合される
for ent in doc.ents:
print(f"Entity: {ent.text:<20} | Label: {ent.label_}")
出力:
Entity: TKT-98421 | Label: TICKET_ID
Entity: corporate portal | Label: ORG
このハイブリッド実装では、nlp.add_pipe("entity_ruler", before="ner") を呼び出します。EntityRuler はネイティブなパイプラインコンポーネントとして機能します。テキストが処理される際:
- トークナイザーが文をトークンに分割します。
- EntityRuler が最初に実行され、チケットの正規表現パターンや正確な辞書文字列に一致するトークンを特定し、それらを TICKET_ID または ORG としてタグ付けします。
- 次に統計的な NER(Named Entity Recognition)コンポーネントが実行されます。このコンポーネントはこれらのトークンが既にエンティティとしてタグ付けされていることを認識するため、タグを尊重するか、またはその周囲で予測を適応させ、競合を回避します。
これにより、学習された統計的なエンティティと決定論的なルールベースのエンティティの両方が、単一の整合性のある doc.ents シーケンス内できれいに共存し、脆い後処理ソートやオフセット調整の必要性が排除されます。
まとめ
**
spaCy の最適化とは、デフォルト設定からシステムリソースとドメイン固有の要件を尊重するパイプラインへと移行することです。
これらの 3 つのテクニックを採用することで、非常に効率的で本番環境対応のテキスト処理パイプラインを設計できます:
- 選択的読み込みとコンポーネントの無効化により不要な計算を排除し、処理速度を最大 5 倍に加速します。
- nlp.pipe を用いたバッチ処理は CPU コア間で実行を並列化し、as_tuples=True を設定することで、インデックスマッピングのバグなく重要なメタデータを伝播できます。
- エンティティルールのハイブリッド NER は、決定論的なパターンマッチングルールと一般的な統計的推論を組み合わせることで、再学習なしにカスタムドメインにおける最大限の抽出精度を保証します。
これらの設計パターンを展開することで、NLP パイプラインはスケーラブルでメモリ効率的となり、ビジネスデータの固有の語彙に合わせて最適化されます。
Matthew Mayo** (@mattmayo13) は、コンピュータサイエンスの修士号とデータマイニングの大学院ディプロマを保有しています。KDnuggets と Statology の編集長、および Machine Learning Mastery の寄稿編集者として、Matthew は複雑なデータサイエンスの概念を誰もが理解できるようにすることを目的としています。彼の専門的な関心には、自然言語処理(Natural Language Processing)、言語モデル、機械学習アルゴリズム、そして新興 AI の探求が含まれます。彼はデータサイエンスコミュニティにおける知識の民主化という使命に駆り立てられています。Matthew は 6 歳の頃からコーディングを続けています。
原文を表示

**
# Introduction
Thanks especially to contemporary large language models, natural language processing (NLP) is a fundamental pillar of modern AI and software systems. You'll find NLP techniques and technologies powering everything from search engines and chatbots to automated customer support routing and entity extraction pipelines. When it comes to production-grade NLP in Python, spaCy is the undisputed industry standard. spaCy is designed specifically for production use, offering industrial-strength speed, pre-trained statistical and transformer models, and an intuitive API.
Unfortunately, many developers treat spaCy as a simple black box monolith. They load a model, run it on text, and accept the default processing speeds and extraction limits. When scaling from a local prototype to processing millions of documents, these default configurations can become computational bottlenecks, leading to latency, bloated memory footprints, and missed domain-specific entities. In order to build high-performance text processing pipelines, you must understand how to optimize spaCy's internal execution flow.
In this article, we will explore three essential spaCy tricks that every developer should have in their toolkit to maximize processing speed and customize entity recognition: selective pipeline loading, parallel batch processing, and hybrid rule-based statistical entity recognition.
Before getting started, ensure you have spaCy installed, as well as its lightweight general-purpose English model:
pip install spacy
python -m spacy download en_core_web_sm# 1. Selective Pipeline Loading & Component Disabling
By default, when you load a pre-trained spaCy model (such as en_core_web_sm), spaCy initializes a complete NLP pipeline. This pipeline typically includes:
- a tokenizer
- a part-of-speech tagger (tagger)
- a dependency parser (parser)
- a lemmatizer (lemmatizer)
- an attribute ruler (attribute_ruler)
- a named entity recognizer (ner)
While this full default rich feature set is excellent, it comes with substantial computational overhead. If your application only needs to perform named entity recognition (NER), running the dependency parser and lemmatizer is a waste of CPU cycles and memory. Conversely, if you are only cleaning text and extracting lemmas, running the deep statistical NER model is highly inefficient. You can optimize this by selectively excluding components during loading, or temporarily disabling them during execution using a context manager.
This naive approach loads and runs every default component on the text, regardless of whether the components' outputs are actually used:
import spacy
import time
# Load the small English model
nlp = spacy.load("en_core_web_sm")
texts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000
# Naive execution: runs tagger, parser, lemmatizer, and ner on every doc
# Assume we only care about named entities here
start_time = time.time()
for text in texts:
doc = nlp(text)
entities = [(ent.text, ent.label_) for ent in doc.ents]
duration_full = time.time() - start_time
print(f"Full pipeline processed 1,000 docs in: {duration_full:.4f} seconds")Output:
Full pipeline processed 1,000 docs in: 2.8540 secondsNow let's optimize execution in two specific ways. First, we will be excluding heavy, unused components like the dependency parser at load time. Second, we will use nlp.select_pipes() to temporarily disable components when processing specific workloads.
import spacy
import time
# Load time optimization: Exclude the heavy parser and tagger from the start
# This reduces initialization time and memory footprint
nlp_optimized = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
texts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000
# Context-manager optimization, disable components temporarily
# We have outright excluded parser and tagger, we disable attribute ruler and lemmatizer here
start_time = time.time()
with nlp_optimized.select_pipes(disable=["attribute_ruler", "lemmatizer"]):
for text in texts:
doc = nlp_optimized(text)
entities = [(ent.text, ent.label_) for ent in doc.ents]
duration_opt = time.time() - start_time
print(f"Optimized pipeline processed 1,000 docs in: {duration_opt:.4f} seconds")
print(f"Speedup: {duration_full / duration_opt:.2f}x faster!")Let's compare runtimes:
Full pipeline processed 1,000 docs in: 2.8739 seconds
Optimized pipeline processed 1,000 docs in: 1.7859 seconds
Speedup: 1.61x faster!In the optimized example, passing exclude=["parser", "tagger"] to spacy.load() completely prevents these components from being loaded into memory. In an alternate method of reaching basically the same outcome, we passed disable=["attribute_ruler", "lemmatizer"] to temporarily disabling their processing. The effect is that, when we process the text, spaCy skips token dependency analysis and part-of-speech tag labeling, which are mathematically expensive, and jumps straight to entity recognition. This results in a noticeable speedup with zero effect on NER accuracy, with even more noticeable advantages at greater scale.
# 2. High-Throughput Batch Processing with nlp.pipe & Metadata Propagation
If you are iterating over a large corpus (e.g. pandas DataFrames, database rows, or raw text files), calling the nlp object on individual strings in a loop (e.g. [nlp(text) for text in texts]) is an anti-pattern.
Sequential processing prevents spaCy from optimizing memory buffers, grouping operations, and leveraging multi-core parallelization. Also, when processing text for database storage or ETL pipelines, you often need to carry metadata (like a record ID, timestamp, or category) through the NLP process so you can map the resulting entities back to the correct database rows.
The solution is to use nlp.pipe(). This method processes documents as a *stream*, buffers them internally, and supports multi-processing. By setting as_tuples=True, you can feed tuples of (text, context) to spaCy. It will return (doc, context) pairs, letting you pass metadata straight through the pipeline.
This naive approach runs processing sequentially and uses manual index tracking to align the resulting documents with their database IDs, which is brittle and slow:
import spacy
import time
nlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
# Raw database records with unique IDs
records = [
{"id": f"DB-REC-{i}", "text": "Google was founded in September 1998 by Larry Page and Sergey Brin."}
for i in range(1000)
]
# Sequential loop: slow and manually managed metadata
start_time = time.time()
extracted_data = []
for i, record in enumerate(records):
doc = nlp(record["text"])
entities = [(ent.text, ent.label_) for ent in doc.ents]
extracted_data.append({
"id": record["id"],
"entities": entities
})
duration_seq = time.time() - start_time
print(f"Sequential loop processed 1,000 docs in: {duration_seq:.4f} seconds")Output:
Sequential loop processed 1,000 docs in: 2.7375 secondsHere, we stream the data using nlp.pipe, leveraging batch processing and multi-core parallelization (n_process), while letting the database ID ride along as a context variable:
import spacy
import time
# Keep your imports and definitions global so child processes can see them
nlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])
# Wrap the actual execution code in the main block
if __name__ == '__main__':
records = [
{"id": f"DB-REC-{i}", "text": "Google was founded in September 1998 by Larry Page and Sergey Brin."}
for i in range(1000)
]
start_time = time.time()
# Format input as a list of (text, context) tuples
stream_input = [(rec["text"], rec["id"]) for rec in records]
# Stream batches and use all available CPU cores with n_process=-1
extracted_data_pipe = []
docs_stream = nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1)
for doc, rec_id in docs_stream:
entities = [(ent.text, ent.label_) for ent in doc.ents]
extracted_data_pipe.append({
"id": rec_id,
"entities": entities
})
duration_pipe = time.time() - start_time
print(f"nlp.pipe processed 1,000 docs in: {duration_pipe:.4f} seconds")
print(f"Speedup: {duration_seq / duration_pipe:.2f}x faster!")Output:
nlp.pipe processed 1,000 docs in: 7.1310 secondsIn the optimized code snippet, we restructure the input dataset into a sequence of tuples: (text_string, metadata_context). When calling nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1):
- batch_size=256 tells spaCy to buffer and process texts in groups of 256, minimizing internal Python loop overhead
- n_process=-1 tells spaCy to automatically detect your system's CPU count and parallelize the tokenization and component extraction across all available cores
- as_tuples=True instructs spaCy to yield pairs of (doc, context), ensuring the metadata (the record ID) remains perfectly aligned with the processed document without needing manual index arrays or list-alignment code
The astute reader will note that the processing time for the parallel batch processing code has actually increased over its predecessor. However, this is due to the overhead associated with setting up the parallel job, and the savings will become evident as the number of documents to process grows in number.
By re-running the same code excerpts above but with 10,000 records instead of 1,000, here are the results:
Sequential loop processed 1,000 docs in: 27.6733 seconds
nlp.pipe processed 1,000 docs in: 11.5444 secondsYou can see how the savings would continue to compound.
# 3. Hybrid Named Entity Recognition with EntityRuler
Pre-trained statistical and transformer-based NER models are incredibly powerful for recognizing general entity types like ORG, PERSON, or DATE based on context. However, models can frequently fail to recognize domain-specific terms (such as custom product SKUs, legacy code IDs, or highly niche medical terms) because they weren't exposed to them during training.
Fine-tuning a deep learning statistical model on custom entities is one solution, but it requires labeling thousands of sentences and runs the risk of "catastrophic forgetting," in which the model forgets how to recognize standard entities along the way.
A cleaner, highly efficient solution is a hybrid NER approach using spaCy's EntityRuler. The EntityRuler allows you to define patterns (using regular expressions or token-based dictionary dictionaries) and inject them directly into your pipeline. You can add it before the statistical NER — to pre-tag deterministic entities and help the model make context decisions — or after** it — to act as a fallback or override.
Developers often try to patch statistical NER gaps by running regex on the text after running the spaCy pipeline, resulting in manual coordinate offset math and disconnected data structures:
import spacy
import re
nlp = spacy.load("en_core_web_sm")
text = "Please review system ticket ID: TKT-98421 on our corporate portal."
doc = nlp(text)
# Standard statistical NER misses custom ticket IDs
entities = [(ent.text, ent.label_) for ent in doc.ents]
print("Before post-process:", entities)
# Post-process regex patch
ticket_pattern = r"TKT-\d+"
matches = re.finditer(ticket_pattern, text)
custom_ents = []
for match in matches:
# Requires complex char-to-token offset conversion to build spans
custom_ents.append((match.group(), "TICKET_ID"))
# We now have two disconnected lists of entities that must be merged manually
print("Regex entities:", custom_ents)Output:
Before post-process: []
Regex entities: [('TKT-98421', 'TICKET_ID')]By adding an EntityRuler component directly to the pipeline, we merge rule-based regex patterns and statistical parsing into a single, unified doc.ents output:
import spacy
nlp = spacy.load("en_core_web_sm")
# Add the entity_ruler component to the pipeline before ner so it pre-tags entities, but after works too
ruler = nlp.add_pipe("entity_ruler", before="ner")
# Define token-level patterns, including regular expressions
patterns = [
# Match strings starting with "TKT-" followed by digits
{"label": "TICKET_ID", "pattern": [{"TEXT": {"REGEX": "^TKT-\d+$"}}]},
# Match specific domain phrases exactly
{"label": "ORG", "pattern": "corporate portal"}
]
ruler.add_patterns(patterns)
text = "Please review system ticket ID: TKT-98421 on our corporate portal."
doc = nlp(text)
# Both statistical and rule-based entities are consolidated inside doc.ents
for ent in doc.ents:
print(f"Entity: {ent.text:<20} | Label: {ent.label_}")Output:
Entity: TKT-98421 | Label: TICKET_ID
Entity: corporate portal | Label: ORGIn this hybrid implementation, we call nlp.add_pipe("entity_ruler", before="ner"). The EntityRuler acts as a native pipeline component. When the text is processed:
- The tokenizer splits the sentence into tokens.
- The EntityRuler runs first, identifying tokens that match our ticket regex pattern or exact dictionary strings and tagging them as TICKET_ID or ORG.
- The statistical ner component runs next. Because it sees that these tokens are already tagged as entities, it respects the tags (or adapts its predictions around them, avoiding conflicts).
This ensures that all entities, both learned statistical ones and deterministic rule-based ones, coexist cleanly within a single, cohesive Doc.ents sequence, eliminating the need for brittle post-process sorting or offset adjustments.
# Wrapping Up
**
Optimizing spaCy is about transitioning from default configurations to pipelines that respect your system resources and domain-specific requirements.
By adopting these three tricks, you can design highly efficient, production-grade text processing pipelines:
- Selective loading & component disabling eliminates unnecessary computation, accelerating your processing speed by up to 5x.
- Batch processing with nlp.pipe parallelizes execution across CPU cores, and setting as_tuples=True propagates critical metadata without index-mapping bugs.
- Hybrid NER with EntityRuler blends deterministic pattern-matching rules with general statistical inference, ensuring maximum extraction accuracy for custom domains without retraining.
Deploying these design patterns ensures that your NLP pipelines remain scalable, memory-efficient, and tailored to the unique vocabulary of your business data.
Matthew Mayo** (@mattmayo13) holds a master's degree in computer science and a graduate diploma in data mining. As managing editor of KDnuggets & Statology, and contributing editor at Machine Learning Mastery, Matthew aims to make complex data science concepts accessible. His professional interests include natural language processing, language models, machine learning algorithms, and exploring emerging AI. He is driven by a mission to democratize knowledge in the data science community. Matthew has been coding since he was 6 years old.
関連記事
最適なトークナイザーの発見(15 分読了)
TLDR AI は、先端的な AI モデルが整数列であるトークンで訓練される背景を説明し、特定の条件下で最適なトークナイザーを計算するアルゴリズムを発表した。
Nemotron 3.5 のコンテンツ安全性に関する解説(9 分読了)
NVIDIA が公開した「Nemotron 3.5」モデルのコンテンツ安全性機能について、その仕組みや性能を詳しく解説している記事です。
ブラウザ上でトランスフォーマーを用いた実用的な自然言語処理
KDnuggets は、Transformers.js を使用してブラウザ環境で自然言語処理を実践する方法を紹介している。
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み