高度なテキスト前処理と言語分析のための NLTK の 3 つの技
KDnuggets は、NLTK を活用した高度なテキスト前処理と言語分析のための具体的な 3 つのテクニックを紹介し、実務におけるデータ品質向上の可能性を提示している。
キーポイント
複雑な正規表現による構造化抽出
単なる単語分割ではなく、正規表現を活用してテキストから特定の構造的パターンやエンティティを高精度に抽出する手法が解説されている。
言語学的特徴量の詳細分析
品詞タグ付けや構文解析(パーシング)を用いて、テキストの文法的構造や意味的な特徴量を数値化・可視化するアプローチが紹介されている。
カスタムトークナイザーと前処理パイプライン
標準的なトークナライザーでは対応できない特殊なケース向けに、NLTK の機能を組み合わせた独自の前処理パイプラインを構築する実例が示されている。
影響分析・編集コメントを表示
影響分析
この記事は、LLM が主流となる現代においても、NLTK に代表される古典的な自然言語処理ライブラリが持つ「構造的・言語学的な解釈能力」の重要性を再認識させるものである。特にデータ前処理や特徴量エンジニアリングの現場において、ブラックボックス化しがちなモデルへの依存を減らし、テキストの文脈を人間が理解可能な形で制御する手法として実用的な指針を提供している。
編集コメント
生成 AI が台頭する中で、基礎的な自然言語処理の重要性が見直されるべきという視点を含んだ有益な技術記事です。
image**
# イントロダクション
近年、自然言語処理(NLP)は、大規模言語モデル(LLM)やトランスフォーマーが複雑なエンドツーエンドの理解タスクを担うようになり、明白なパラダイムシフトを経験しました。しかし、実用的な NLP ワークフローにおいては、テキストがモデルに到達する前に、依然としてトークン化、正規化、分析を行う必要があります。SpaCy や Hugging Face などの現代的な NLP ライブラリやエコシステムは、汎用ディープラーニングパイプラインの構築や LLM との統合には素晴らしいものですが、Natural Language Toolkit (NLTK) は、微細な構文言語学的分析、カスタムテキスト正規化、統計的コーパス分析において依然として有効で透明性の高い選択肢です。
残念ながら、多くの開発者は誤って、LLM の登場が従来のテキスト前処理を不要にすると考えていたり、重要な言語構造を無視する素朴な方法でテキスト前処理コードを書いたりしています。彼らは「機械学習」のような多語表現を意味のない個別の単語に分割したり、文脈を考慮しないレマティゼーション(原形化)を行って不正確な基本形を生成したり、あるいは意味のある単語間の関連性を捉えられない単純な生頻度カウントに依存したりしています。
堅牢で意味的に正確な NLP モデルを構築するには、前処理段階で構文と言語的文脈を保持する必要があります。この記事では、テキスト前処理のレベルを高めるための 3 つの重要な NLTK のテクニックをご紹介します:
- MWETokenizer を用いたフレーズの完全性の維持
- 品詞 (POS) マッピングに基づく文脈対応型語幹化
- 連想尺度を用いた統計的共起語抽出
# 1. 多単語表現トークナイザーによるドメイン用語の保持
トークン化は、あらゆる NLP パイプラインの基盤です。しかし、標準的なトークナイザーは空白文字と句読点に基づいて文を厳密に分割します。これは、「ニューラルネットワーク」や「決定木」、あるいは「サンフランシスコ」といったドメイン固有の多単語表現を取り扱う際に問題となります。これらは個々の単語が組み合わさって単一の意味概念を形成するからです。
もしトークナイザーが「ニューラルネットワーク」を「ニューラル」と「ネットワーク」に分割してしまうと、後段のベクトライザー(Bag-of-Words や TF-IDF など)はそれらを無関係な特徴として扱い、シグナルを希薄化しノイズを導入することになります。開発者はしばしば、トークン化する前に生テキストに対して検索・置換用の正規表現を記述してこれを修正しようと試みます。
文字レベルでの置換(例:text.replace("neural network", "neural_network"))は脆いものです。これは単語の境界を尊重できず、句読点の処理も不十分で、大規模なデータセット全体を実行する際に極めて遅くなります。最適化されたアプローチは、まずテキストをトークン化し、その後 NLTK のネイティブ MWETokenizer を実行してこれらのトークンをきれいに結合することです。
正規表現置換による素朴なアプローチは、文字レベルの文字列操作に依存しており、スケーラビリティが低く、無関係な単語内の部分文字列を誤って変更してしまう可能性があります:
import re
import time
サンプルコーパス
raw_texts = [
"We are studying neural networks and deep learning.",
"The decision tree is a popular model in machine learning.",
"A neural network can have many layers."
] * 5000
cleaned_texts = []
for text in raw_texts:
# ドメイン用語の手動文字列置換
text = re.sub(r"\bneural networks?\b", "neural_network", text, flags=re.IGNORECASE)
text = re.sub(r"\bdecision trees?\b", "decision_tree", text, flags=re.IGNORECASE)
text = re.sub(r"\bmachine learnings?\b", "machine_learning", text, flags=re.IGNORECASE)
# 処理された文字列のトークン化
tokens = text.lower().split()
cleaned_texts.append(tokens)
print("Sample tokens:", cleaned_texts[0])
出力:
Sample tokens: ['we', 'are', 'studying', 'neural_network', 'and', 'deep', 'learning.']
さて、NLTK のトークナイザーを使ってみましょう。まず標準的な word_tokenize メソッドを使ってトークン化し、その後に初期化された MWETokenizer を通して、トークンの境界で効率的に結合処理を行うストリームを渡します:
import nltk
from nltk.tokenize import word_tokenize, MWETokenizer
import time
NLTK リソースのダウンロードを確認する
nltk.download('punkt', quiet=True)
raw_texts = [
"We are studying neural networks and deep learning.",
"The decision tree is a popular model in machine learning.",
"A neural network can have many layers."
] * 5000
トークナイザーを初期化し、MWE(多語表現)のタプルを登録する
mwe_tokenizer = MWETokenizer([
('neural', 'network'),
('neural', 'networks'),
('decision', 'tree'),
('decision', 'trees'),
('machine', 'learning')
], separator='_')
cleaned_texts_mwe = []
for text in raw_texts:
# NLTK の標準トークナイザーを使って単語をトークン化する
tokens = word_tokenize(text.lower())
# 指定された多語表現を結合する
merged_tokens = mwe_tokenizer.tokenize(tokens)
cleaned_texts_mwe.append(merged_tokens)
print("Sample tokens:", cleaned_texts_mwe[0])
同じ出力が得られますが、よりエレガントで言語学的に正確であり、かつスケーラブルなアプローチとなります:
Sample tokens: ['we', 'are', 'studying', 'neural_network', 'and', 'deep', 'learning.']
MWETokenizer を使用することで、処理が低速な文字レベルの文字列マッチから、トークンレベルでの比較へとシフトします。
- 多語表現を独立したトークンのタプルとして定義します:('neural', 'network')。
- セパレーターを'_'に設定すると、トークナイザーは一致するシーケンスを単一の文字列トークンに結合します:"neural_network"。
- これは直接トークン配列に対して動作するため、境界マッチングのバグの影響を受けず、末尾の句読点(例:"neural networks." をまず "neural", "networks", "." に分割し、その後安全に "neural_networks", "." へ結合)も正しく処理します。実行速度が速く、数百のドメイン用語にもきれいにスケーリングできます。
# 2. POSタグマッピングを用いた文脈対応型レマティゼーション
レマティゼーション(lemmatization)とは、単語をその基本辞書形(lemma)に縮約するプロセスです。例えば "running" は "run" に、"better" は "good" になります。これは同じ単語の異なる文法屈折形をグループ化する重要な正規化ステップです。
しかし、NLTK の WordNetLemmatizer はデフォルトで全ての単語を名詞として扱います。動詞や形容詞を POS カテゴリを指定せずに渡すと、レマタイザーは単語を変更せずそのまま返してしまいます。具体的には以下のようになります:
- lemmatizer.lemmatize("running") は "running" を返します("run" ではなく)。
- lemmatizer.lemmatize("better") は "better" を返します("good" ではなく)。
これを解決するには、NLTK の POS タグラーを用いて文内の各単語の文法的役割を動的に特定し、そのタグを WordNet の簡略化されたカテゴリ(名詞、動詞、形容詞、副詞)にマッピングして、レマタイザーに渡す必要があります。
この素朴なアプローチでは、単語を直接レマティザ(語形復元器)に渡すだけなので、動詞や形容詞の変換を見落とし、結果として最適な語彙の正規化が得られません:
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
nltk.download('punkt', quiet=True)
nltk.download('wordnet', quiet=True)
sentence = "The feet of the running runners are getting better and faster."
tokens = word_tokenize(sentence.lower())
lemmatizer = WordNetLemmatizer()
素朴な語形復元:すべて名詞と仮定
naive_lemmas = [lemmatizer.lemmatize(token) for token in tokens]
print("Tokens: ", tokens)
print("Naive Lemmas:", naive_lemmas)
出力結果:
Tokens: ['the', 'feet', 'of', 'the', 'running', 'runners', 'are', 'getting', 'better', 'and', 'faster', '.']
Naive Lemmas: ['the', 'foot', 'of', 'the', 'running', 'runner', 'are', 'getting', 'better', 'and', 'faster', '.']
では、最適化されたバージョンを見てみましょう。NLTK の pos_tag が返すペンツリーバンクタグ(Penn Treebank tags)を WordNet の POS 定数にマッピングするクリーンなヘルパー辞書を作成し、すべての単語タイプを正確に語形復元できるようにします:
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
from nltk.corpus import wordnet
品詞タグ付けリソースのダウンロード
nltk.download('punkt', quiet=True)
nltk.download('wordnet', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
sentence = "The feet of the running runners are getting better and faster."
tokens = word_tokenize(sentence.lower())
トークンごとに品詞タグを生成する
pos_tags = nltk.pos_tag(tokens)
Penn Treebank タグを WordNet タグにマッピングする関数
def get_wordnet_pos(treebank_tag):
if treebank_tag.startswith('J'):
return wordnet.ADJ
elif treebank_tag.startswith('V'):
return wordnet.VERB
elif treebank_tag.startswith('N'):
return wordnet.NOUN
elif treebank_tag.startswith('R'):
return wordnet.ADV
else:
# デフォルトでは WordNet の名詞扱いに合わせる
return None
lemmatizer = WordNetLemmatizer()
マッピングされた品詞タグを利用して語形復元を行う
context_lemmas = []
for token, tag in pos_tags:
wn_tag = get_wordnet_pos(tag)
if wn_tag:
lemma = lemmatizer.lemmatize(token, pos=wn_tag)
else:
lemma = lemmatizer.lemmatize(token)
context_lemmas.append(lemma)
print("POS Tagged: ", pos_tags)
print("Context Lemmas:", context_lemmas)
出力結果:
POS Tagged: [('the', 'DT'), ('feet', 'NNS'), ('of', 'IN'), ('the', 'DT'), ('running', 'NN'), ('runners', 'NNS'), ('are', 'VBP'), ('getting', 'VBG'), ('better', 'RBR'), ('and', 'CC'), ('faster', 'RBR'), ('.', '.')]
Context Lemmas: ['the', 'foot', 'of', 'the', 'running', 'runner', 'be', 'get', 'well', 'and', 'faster', '.']
NLTK の pos_tag は、単語に Penn Treebank タグセット(例:動名詞には'VBG'、比較級形容詞には'JJR')を使用してラベルを付与します。
- ヘルパー関数 get_wordnet_pos() は、タグの最初の文字を検査します。WordNet の POS 規格に準じて、'J' で始まる場合は WordNet の形容詞タグ (wordnet.ADJ) にマッピングし、'V' で始まる場合は動詞 (wordnet.VERB) にマッピングするなどします。
- 正しい POS タグを lemmatizer.lemmatize(token, pos=wn_tag) に渡すことで、レマタイザーは「running」を「run」、「are」を「be」、「getting」を「get」、「better」を「good」、「faster」を「fast」と正しく解決します。これにより文の意味核が保持され、下流の機械学習モデルにおける語彙のスパース性が劇的に軽減されます。
# 3. コロケーションファインダーを用いた統計的フレーズ抽出
テキストから主要なフレーズや多単語概念を抽出することは、トピックモデリング、検索インデックス作成、感情分析において価値があります。これらのフレーズはコロケーションと呼ばれ、偶然に期待される頻度よりも多く共起する単語の連続です。
コロケーションを見つける素朴な方法は、すべての生ビッグラム(2 語の連続)を数え、頻度順にソートすることです。しかし、このアプローチは非常に情報量の少ないペアを生み出します。生の頻度分布のため、「of the」、「in the」、「on a」のような組み合わせが常に上位結果を支配してしまいます。ストップワードをフィルタリングした後でも、生のカウントは偶然にも数回繰り返されるランダムな組み合わせを有利に扱う可能性があります。
最適化された解決策は、NLTK の BigramCollocationFinder を統計的関連性指標と組み合わせて使用することです。単純な頻度カウントではなく、ポイントワイズ相互情報量(PMI)やカイ二乗統計などの関連性測定手法を適用します。これらの指標は、2 つの単語が純粋な偶然よりも有意に多く一緒に出現するかどうかを評価します。
まず、私たちの素朴なアプローチでは単純に生の bigram をカウントし、上位の一致結果を切り取ることで、ノイズや一般的な機能語を取り込んでしまいます:
from collections import Counter
import nltk
from nltk.tokenize import word_tokenize
from nltk.util import bigrams
サンプルコーパス
corpus = """
Natural language processing is an active field of AI. Machine learning plays a key role
in natural language processing. Deep learning architectures have revolutionized natural
language processing. We need machine learning models to solve these natural language tasks.
"""
tokens = word_tokenize(corpus.lower())
生の bigram を抽出してカウント
raw_bigrams = list(bigrams(tokens))
bigram_counts = Counter(raw_bigrams)
print("Top 5 Raw Bigrams:")
for bigram, freq in bigram_counts.most_common(5):
print(f"{bigram}: {freq}")
出力結果:
Top 5 Raw Bigrams:
('natural', 'language'): 4
('language', 'processing'): 3
('machine', 'learning'): 2
('processing', '.'): 2
('processing', 'is'): 1
ここでは、NLTK の collocation finder を初期化し、フィルタ制約を適用して、BigramAssocMeasures クラスを使用してポイントワイズ相互情報量(PMI)を用いてフレーズの関連性をスコアリングします:
import nltk
from nltk.collocations import BigramCollocationFinder
from nltk.metrics.association import BigramAssocMeasures
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
nltk.download('punkt', quiet=True)
nltk.download('stopwords', quiet=True)
corpus = """
Natural language processing is an active field of AI. Machine learning plays a key role
in natural language processing. Deep learning architectures have revolutionized natural
language processing. We need machine learning models to solve these natural language tasks.
"""
tokens = word_tokenize(corpus.lower())
Initialize the collocation finder
finder = BigramCollocationFinder.from_words(tokens)
Filter out punctuation and stop words
stop_words = set(stopwords.words('english'))
filter_stops = lambda w: w in stop_words or not w.isalnum()
finder.apply_word_filter(filter_stops)
Filter out bigrams that occur less than N times
finder.apply_freq_filter(2)
Score bigrams using pointwise mutual information
pmi_measures = BigramAssocMeasures()
top_collocations = finder.score_ngrams(pmi_measures.pmi)
print("Top Collocations by PMI:")
for bigram, pmi_score in top_collocations[:5]:
# Formulate a clean print representation
phrase = " ".join(bigram)
print(f"Phrase: {phrase:<30} | PMI Score: {pmi_score:.4f}")
Output:
PMI による上位共起語:
フレーズ:machine learning | PMI スコア:3.8074
フレーズ:language processing | PMI スコア:3.3923
フレーズ:natural language | PMI スコア:3.3923
- BigramCollocationFinder.from_words() は、構造的な位置を維持しながらすべての二語のグループを抽出します。
- finder.apply_word_filter() を使用して候補をクリーニングし、元の単語間隔の文脈を変更せずに、ストップワードや句読点を含むビッグラムを動的に除外します。
- apply_freq_filter(2) を設定することで、一度しか発生しないランダムな組み合わせを無視し、統計的なノイズを削減します。
- 最後に、ポイントワイズ相互情報量(pointwise mutual information)によるスコアリングは、二つの単語が一緒に出現する確率を、それぞれ独立して出現する確率で割ることで数学的に測定します。これにより、「machine learning」や「natural language」のような強く結合された用語が浮き彫りになり、一般的な緩い組み合わせは無視されます。
まとめ
カスタムテキスト前処理は、生データからよりクリーンなシグナルを抽出するための鍵であり、NLTK はこれらの操作をカスタマイズするために必要な構造的ツールを提供しています。
これら 3 つの NLTK 技術を組み込むことで、はるかに堅牢な NLP ワークフローを構築できます:
- MWETokenizer を用いてドメイン固有の用語を保持することで、複合語をトークンレベルで結合し、ベクトル化時に重要な概念が分解されるのを防ぎます
- 文脈を考慮した形態素解析は、品詞タグ付けと WordNet マッピングを組み合わせることで、言語学的に正確な基本形を取得し、語彙の次元数を大幅に削減します
- 統計的共起抽出は、PMI(相互情報量)などの数学的関連性指標を用いて、生データから真の意味的なフレーズを抽出し、単純な頻度カウントによるノイズを排除します
これらの構造パターンを特徴量エンジニアリングプロセスに適用することで、下流の分類、検索、クラスタリングアルゴリズムが、高品質で意味的に完全なトークンを受け取ることができます。
Matthew Mayo** (@mattmayo13) はコンピュータサイエンスの修士号とデータマイニングの大学院卒業証書を取得しています。KDnuggets と Statology の編集長、および Machine Learning Mastery の寄稿編集者として、Matthew は複雑なデータサイエンスの概念を誰もが理解できるようにすることを目的としています。彼の専門的な関心には、自然言語処理、言語モデル、機械学習アルゴリズム、そして新興 AI の探求が含まれます。彼はデータサイエンスコミュニティにおける知識の民主化という使命に駆り立てられています。Matthew は 6 歳の頃からコーディングを続けています
原文を表示

**
# Introduction
Natural language processing (NLP) has undergone an obvious paradigm shift in recent years, with large language models (LLMs) and transformers handling complex end-to-end understanding tasks. However, in any practical NLP workflow, raw text must still be tokenized, normalized, and analyzed before it ever reaches a model. While modern NLP libraries and ecosystems like SpaCy or Hugging Face are fantastic for building general-purpose deep learning pipelines or integrating with LLMs, the Natural Language Toolkit (NLTK) remains a viable, transparent option for fine-grained structural linguistics, custom text normalization, and statistical corpus analysis.
Unfortunately, many developers incorrectly believe that LLMs render traditional text preprocessing obsolete, or they write text preprocessing code using naive methods that discard critical linguistic structure. They split multi-word expressions like "machine learning" into separate, meaningless words; they perform context-blind lemmatization that yields inaccurate base forms; or they rely on simple raw frequency counts that miss meaningful word associations.
To build robust, semantically accurate NLP models, you need to preserve structural and linguistic context at the preprocessing stage. In this article, we will walk through three essential NLTK tricks to elevate your text preprocessing:
- preserving phrase integrity with the MWETokenizer
- context-aware lemmatization with Part-of-Speech (POS) mapping
- statistical collocation extraction using association measures
# 1. Preserving Domain Terminology with the Multi-Word Expression Tokenizer
Tokenization is the foundation of any NLP pipeline. However, standard tokenizers split sentences strictly by whitespace and punctuation. This becomes problematic when dealing with domain-specific multi-word expressions — such as "neural network", "decision tree", or "San Francisco" — where the individual words combine to form a single semantic concept.
If a tokenizer splits "neural network" into "neural" and "network", a downstream vectorizer (like Bag-of-Words or TF-IDF) will treat them as unrelated features, diluting the signal and introducing noise. Developers often try to fix this by writing search-and-replace regular expressions on the raw text before tokenizing.
Using character-level replacements (e.g. text.replace("neural network", "neural_network")) is brittle. It fails to respect word boundaries, handles punctuation poorly, and is incredibly slow to execute across large datasets. The optimized approach is to tokenize the text first and then run NLTK's native MWETokenizer to merge these tokens cleanly.
The naive approach of regex replacement relies on character-level string manipulation, which does not scale well and can inadvertently modify substrings inside unrelated words:
import re
import time
# Sample corpus
raw_texts = [
"We are studying neural networks and deep learning.",
"The decision tree is a popular model in machine learning.",
"A neural network can have many layers."
] * 5000
cleaned_texts = []
for text in raw_texts:
# Manual string replacements for domain terms
text = re.sub(r"\bneural networks?\b", "neural_network", text, flags=re.IGNORECASE)
text = re.sub(r"\bdecision trees?\b", "decision_tree", text, flags=re.IGNORECASE)
text = re.sub(r"\bmachine learnings?\b", "machine_learning", text, flags=re.IGNORECASE)
# Tokenize the processed string
tokens = text.lower().split()
cleaned_texts.append(tokens)
print("Sample tokens:", cleaned_texts[0])Output:
Sample tokens: ['we', 'are', 'studying', 'neural_network', 'and', 'deep', 'learning.']Now let's try using NLTK's tokenizers. We first tokenize using the standard word_tokenize method and then pass the token streams through an initialized MWETokenizer that handles merging on token boundaries efficiently:
import nltk
from nltk.tokenize import word_tokenize, MWETokenizer
import time
# Ensure NLTK resources are downloaded
nltk.download('punkt', quiet=True)
raw_texts = [
"We are studying neural networks and deep learning.",
"The decision tree is a popular model in machine learning.",
"A neural network can have many layers."
] * 5000
# Initialize tokenizer and register MWE tuples
mwe_tokenizer = MWETokenizer([
('neural', 'network'),
('neural', 'networks'),
('decision', 'tree'),
('decision', 'trees'),
('machine', 'learning')
], separator='_')
cleaned_texts_mwe = []
for text in raw_texts:
# Tokenize words using NLTK's standard tokenizer
tokens = word_tokenize(text.lower())
# Merge specified multi-word expressions
merged_tokens = mwe_tokenizer.tokenize(tokens)
cleaned_texts_mwe.append(merged_tokens)
print("Sample tokens:", cleaned_texts_mwe[0])We get the same output, but in a more elegant and linguistically-accurate — and scalable — approach:
Sample tokens: ['we', 'are', 'studying', 'neural_network', 'and', 'deep', 'learning.']Using the MWETokenizer shifts the operation from slow character-level string matches to token-level comparison.
- We define the multi-word expressions as tuples of independent tokens: ('neural', 'network').
- By setting separator='_', the tokenizer merges the matching sequence into a single string token: "neural_network".
- Because it acts directly on token arrays, it is immune to boundary matching bugs and handles trailing punctuation (like "neural networks." splitting into "neural", "networks", "." first, then safely merging to "neural_networks", ".") correctly. It executes faster and scales cleanly to hundreds of domain terms.
# 2. Context-Aware Lemmatization with POS-Tag Mapping
Lemmatization is the process of reducing a word to its base dictionary form (its lemma) — "running" -> "run", "better" -> "good". This is an essential normalization step, as it groups different grammatical inflections of the same word together.
However, NLTK's WordNetLemmatizer defaults to treating every word as a noun. If you pass verbs or adjectives without specifying their POS category, the lemmatizer will return the word unchanged. For example:
- lemmatizer.lemmatize("running") yields "running" (instead of "run")
- lemmatizer.lemmatize("better") yields "better" (instead of "good")
To solve this, we must dynamically identify the grammatical role of each word in the sentence using NLTK's POS tagger, map those tags to WordNet's simplified categories (noun, verb, adjective, adverb), and pass them to the lemmatizer.
This naive approach feeds words directly to the lemmatizer. It misses verb and adjective conversions, resulting in suboptimal vocabulary normalization:
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
nltk.download('punkt', quiet=True)
nltk.download('wordnet', quiet=True)
sentence = "The feet of the running runners are getting better and faster."
tokens = word_tokenize(sentence.lower())
lemmatizer = WordNetLemmatizer()
# Naive lemmatization: assumed to be all nouns
naive_lemmas = [lemmatizer.lemmatize(token) for token in tokens]
print("Tokens: ", tokens)
print("Naive Lemmas:", naive_lemmas)Output:
Tokens: ['the', 'feet', 'of', 'the', 'running', 'runners', 'are', 'getting', 'better', 'and', 'faster', '.']
Naive Lemmas: ['the', 'foot', 'of', 'the', 'running', 'runner', 'are', 'getting', 'better', 'and', 'faster', '.']Let's look at an optimized version: we write a clean helper dictionary mapping Penn Treebank tags (returned by NLTK's pos_tag) to WordNet POS constants, ensuring every word type is lemmatized accurately:
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
from nltk.corpus import wordnet
# Download POS tagger resources
nltk.download('punkt', quiet=True)
nltk.download('wordnet', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
sentence = "The feet of the running runners are getting better and faster."
tokens = word_tokenize(sentence.lower())
# Generate POS tags for each token
pos_tags = nltk.pos_tag(tokens)
# Map Penn Treebank tags to WordNet tags
def get_wordnet_pos(treebank_tag):
if treebank_tag.startswith('J'):
return wordnet.ADJ
elif treebank_tag.startswith('V'):
return wordnet.VERB
elif treebank_tag.startswith('N'):
return wordnet.NOUN
elif treebank_tag.startswith('R'):
return wordnet.ADV
else:
# Default to WordNet's default noun handling
return None
lemmatizer = WordNetLemmatizer()
# Lemmatize utilizing mapped POS tags
context_lemmas = []
for token, tag in pos_tags:
wn_tag = get_wordnet_pos(tag)
if wn_tag:
lemma = lemmatizer.lemmatize(token, pos=wn_tag)
else:
lemma = lemmatizer.lemmatize(token)
context_lemmas.append(lemma)
print("POS Tagged: ", pos_tags)
print("Context Lemmas:", context_lemmas)Output:
POS Tagged: [('the', 'DT'), ('feet', 'NNS'), ('of', 'IN'), ('the', 'DT'), ('running', 'NN'), ('runners', 'NNS'), ('are', 'VBP'), ('getting', 'VBG'), ('better', 'RBR'), ('and', 'CC'), ('faster', 'RBR'), ('.', '.')]
Context Lemmas: ['the', 'foot', 'of', 'the', 'running', 'runner', 'be', 'get', 'well', 'and', 'faster', '.']NLTK's pos_tag labels words using the Penn Treebank tagset (e.g. 'VBG' for a gerund verb, 'JJR' for a comparative adjective).
- Our helper function get_wordnet_pos() inspects the first character of the tag. Inline with WordNet's POS standards, if it starts with 'J', we map it to WordNet's Adjective tag (wordnet.ADJ); if it starts with 'V', to Verb (wordnet.VERB), and so on.
- By feeding the correct POS tag into lemmatizer.lemmatize(token, pos=wn_tag), the lemmatizer successfully resolves "running" to "run", "are" to "be", "getting" to "get", "better" to "good", and "faster" to "fast". This preserves the semantic core of the sentence, drastically reducing vocabulary sparsity for downstream ML models.
# 3. Statistical Phrase Extraction using Collocation Finders
Extracting key phrases or multi-word concepts from text is valuable for topic modeling, search indexing, and sentiment analysis. These phrases are known as collocations, which are sequences of words that co-occur more often than would be expected by chance.
The naive way to find collocations is to count all raw bigrams (two-word sequences) and sort them by frequency. However, this approach yields highly uninformative pairs. Due to raw frequency distributions, combinations like "of the", "in the", and "on a" will always dominate the top results. Even after filtering out stopwords, raw counts can favor random, coincidental pairings that happen to repeat a few times.
The optimized solution is to use NLTK's BigramCollocationFinder combined with statistical association metrics. Instead of counting raw frequency, we apply association measures like Pointwise Mutual Information (PMI) or Chi-Square statistics. These metrics evaluate whether two words appear together significantly more often than they would by pure chance.
First, our naive approach simply counts raw bigrams and slices the top matches, capturing noise and common function words:
from collections import Counter
import nltk
from nltk.tokenize import word_tokenize
from nltk.util import bigrams
# Sample corpus
corpus = """
Natural language processing is an active field of AI. Machine learning plays a key role
in natural language processing. Deep learning architectures have revolutionized natural
language processing. We need machine learning models to solve these natural language tasks.
"""
tokens = word_tokenize(corpus.lower())
# Extract and count raw bigrams
raw_bigrams = list(bigrams(tokens))
bigram_counts = Counter(raw_bigrams)
print("Top 5 Raw Bigrams:")
for bigram, freq in bigram_counts.most_common(5):
print(f"{bigram}: {freq}")Output:
Top 5 Raw Bigrams:
('natural', 'language'): 4
('language', 'processing'): 3
('machine', 'learning'): 2
('processing', '.'): 2
('processing', 'is'): 1Here, we initialize NLTK's collocation finder, apply filter constraints, and use the BigramAssocMeasures class to score phrase associations using Pointwise Mutual Information (PMI):
import nltk
from nltk.collocations import BigramCollocationFinder
from nltk.metrics.association import BigramAssocMeasures
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
nltk.download('punkt', quiet=True)
nltk.download('stopwords', quiet=True)
corpus = """
Natural language processing is an active field of AI. Machine learning plays a key role
in natural language processing. Deep learning architectures have revolutionized natural
language processing. We need machine learning models to solve these natural language tasks.
"""
tokens = word_tokenize(corpus.lower())
# Initialize the collocation finder
finder = BigramCollocationFinder.from_words(tokens)
# Filter out punctuation and stop words
stop_words = set(stopwords.words('english'))
filter_stops = lambda w: w in stop_words or not w.isalnum()
finder.apply_word_filter(filter_stops)
# Filter out bigrams that occur less than N times
finder.apply_freq_filter(2)
# Score bigrams using pointwise mutual information
pmi_measures = BigramAssocMeasures()
top_collocations = finder.score_ngrams(pmi_measures.pmi)
print("Top Collocations by PMI:")
for bigram, pmi_score in top_collocations[:5]:
# Formulate a clean print representation
phrase = " ".join(bigram)
print(f"Phrase: {phrase:<30} | PMI Score: {pmi_score:.4f}")Output:
Top Collocations by PMI:
Phrase: machine learning | PMI Score: 3.8074
Phrase: language processing | PMI Score: 3.3923
Phrase: natural language | PMI Score: 3.3923- BigramCollocationFinder.from_words() extracts all two-word groups while maintaining structural positions.
- We clean the candidates using finder.apply_word_filter(), which dynamically excludes bigrams containing stop words or punctuation marks without modifying the original word spacing context.
- By setting apply_freq_filter(2), we ignore random combinations that only happen once, reducing statistical noise.
- Finally, scoring with pointwise mutual information mathematically measures the probability of the two words appearing together divided by the probability of them appearing independently. This highlights highly coupled terms like "machine learning" and "natural language" while ignoring common, loose combinations.
# Wrapping Up
Custom text preprocessing is the key to extracting cleaner signals from raw text, and NLTK provides the structural tools required to customize these operations.
By incorporating these three NLTK techniques, you can build much more robust NLP workflows:
- Preserving domain terminology with MWETokenizer merges compound words at the token level, preventing key concepts from being broken apart during vectorization
- Context-aware lemmatization couples POS tag generation with WordNet mapping to retrieve linguistically accurate base forms, significantly reducing vocabulary dimensionality
- Statistical collocation extraction uses mathematical association metrics like PMI to isolate true semantic phrases from raw corpus data, bypassing the noise of simple frequency counts
Using these structural patterns in your feature engineering process ensures that downstream classification, search, and clustering algorithms receive high-quality, semantically intact tokens.
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.
関連記事
世界を埋め込む:大規模な航空画像のための多モーダル AI による検索可能化
AWS は、保険や不動産など地理空間データを必要とする業界向けに、自然言語で航空画像を検索できる基盤を提供する。従来の手動検査や個別モデル訓練の代わりに、多モーダル埋め込みとベクトル検索を活用し、一度インデックス化すれば効率的な検索を可能にする技術を発表した。
社内データ分析エージェントの構築方法について
GitHub は、大規模なデータ組織が直面する自己完結型のデータアクセスと洞察提供の課題に対し、AI を活用した信頼性の高い解決策として、社内でデータ分析エージェントを構築したことを発表した。
Photoshop と Premiere に AI アシスタントが搭載
Adobe が Creative Cloud の主要アプリに個別の AI アシスタントを公開ベータとして導入し、編集・デザイン業務を支援する機能を展開した。
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み