12. 自然言語処理の基礎¶
本章では、自然言語処理(NLP: Natural Language Processing)におけるテキストの前処理からベクトル化、類似文書検索までを学びます。テキストデータを機械学習で扱うためには、画像と同じく適切な前処理が必要になります。
12.1 テキストデータの基礎¶
12.1.1 自然言語処理とは¶
自然言語処理は、人間が日常的に使用する言葉(自然言語)をコンピュータで処理・理解・生成する技術です。
自然言語処理の応用例
- 機械翻訳: Google 翻訳、DeepL など
- 音声アシスタント: Siri、Alexa、Google Assistant
- 感情分析: SNS の投稿から感情を分析
- チャットボット: 自動応答システム
- 文章要約: ニュース記事の自動要約
- スパム判定: メールのスパム検出
12.1.2 自然言語処理の難しさ¶
自然言語は、画像データと比べて以下のような難しさがあります。
- 曖昧さ: 「黒い目の大きな猫」で「黒い」が「目」と「猫」のどちらにかかるかで意味が変わる
- 文脈依存性: 「彼は走った」は過去のいつのことか
- 言語の多様性: 方言、スラング、新語、造語
- 背景知識が必要: 常識、文化的背景、皮肉、比喩
12.1.3 自然言語処理のワークフロー¶
自然言語処理の典型的なワークフローは以下のとおりです。
- テキストデータの取得
- テキストクリーニング
- 形態素解析
- 不要語の除去・正規化
- ベクトル化
- 機械学習モデルの作成
- 予測
本章では、主に「テキストクリーニング」から「ベクトル化」までを学びます。
12.1.4 テキストの読み込みと基本操作¶
Python には、文字列を操作するための便利な関数やメソッドがさまざま用意されています。以下ではその代表例を紹介します。
文字列の結合¶
文字列の結合を行うには、join() メソッドを用います。[区切り文字].join([文字列のリスト]) のようにすることで、文字列のリスト(または他のイテラブル)を区切り文字で連結し、一つの文字列にすることができます。
文字列の分割¶
文字列を分割したい場合は、split() メソッドを用います。[文字列].split([区切り文字]) のようにすることで、文字列を指定した区切り文字で分割し、リストにすることができます。区切り文字を省略した場合は、スペースで分割されます。
['Python is simple', ' powerful', ' and widely used.']
['Python', 'is', 'simple,', 'powerful,', 'and', 'widely', 'used.']
文字列の置換¶
特定の文字列を別の文字列に置換したり、除去したりしたい場合は、replace() メソッドを用います。[文字列].replace([置換前の文字列], [置換後の文字列]) のようにすることで、文字列の置換を行うことができます。
replace による文字列の除去
置換後の文字列を空文字列 ("") にすることで、指定した文字列を取り除くことができます。
ファイルからのテキスト読み込み¶
テキストファイル (.txt) を開いてデータを読み込むには、with 文と open() 関数、read() メソッドを用います。
# テキストファイルの読み込み
with open("sample.txt", "r", encoding="utf-8") as f:
text = f.read()
print(text[:100]) # 最初の100文字を表示
ここで "r" は、ファイルを「読み取り専用」モード (read mode) で開こうとしていることを意味します。
日本語テキストの特性¶
日本語テキストは、英語とは異なる特性があります。
# 日本語テキストの例
japanese_text = "これは日本語のサンプルテキストです。機械学習を学びます。"
# 文字数
print(f"文字数: {len(japanese_text)}")
# 日本語の分割(単純なsplitでは不十分)
words = japanese_text.split()
print(f"split結果: {words}") # スペースで区切られていないので1つの文字列
# 文字単位で処理
chars = list(japanese_text)
print(f"文字リスト(最初の10文字): {chars[:10]}")
文字数: 30
split結果: ['これは日本語のサンプルテキストです。機械学習を学びます。']
文字リスト(最初の10文字): ['こ', 'れ', 'は', '日', '本', '語', 'の', 'サ', 'ン', 'プ']
日本語の特性
- 単語の区切りがない: 英語のようにスペースで単語が区切られていない
- 本書では形態素解析を利用: MeCab で語を単位として扱う
- 文字種が複数: ひらがな、カタカナ、漢字、英数字が混在
12.2 形態素解析¶
12.2.1 形態素解析とは¶
形態素解析 とは、文を意味を持つ最小単位の単語(形態素)に分割する操作のことです。本書では、MeCab による形態素解析で文を単語に区切ります。
例えば、「吾輩は猫である」という文は、「吾輩」「は」「猫」「で」「ある」に分割されます。
12.2.2 MeCab のインストール¶
形態素解析を実際に実行するには、形態素解析器と辞書データをインストールする必要があります。Google Colaboratory などでは、以下のコマンドで高性能な形態素解析器 MeCab と、軽量な日本語辞書 unidic-lite をまとめてインストールできます。
インストール後、以下のようにインポートすることで、MeCab を使えるようになります。
CHIKUWA Editor における形態素解析器と辞書データ
CHIKUWA Editor にも MeCab が標準で含まれており、本章の形態素解析の練習をそのまま実行できます(デフォルトの辞書は unidic 系です)。
12.2.3 MeCab による形態素解析の基礎¶
MeCab を用いた形態素解析の基本的な流れを見ていきましょう。
まずは、MeCab の解析器(Tagger)のインスタンスを作成します。
そして、Tagger クラスのインスタンスから parse() メソッドを呼び出すことで、形態素解析が実行されます。
import MeCab
# MeCab のインスタンス生成
tagger = MeCab.Tagger()
# 解析するテキスト
text = "すもももももももものうち"
# 形態素解析を実行し、結果を表示
result = tagger.parse(text)
print(result)
すもも スモモ スモモ 李 名詞-普通名詞-一般 0
も モ モ も 助詞-係助詞
もも モモ モモ 桃 名詞-普通名詞-一般 0
も モ モ も 助詞-係助詞
もも モモ モモ 桃 名詞-普通名詞-一般 0
の ノ ノ の 助詞-格助詞
うち ウチ ウチ 内 名詞-普通名詞-副詞可能 0
EOS
12.2.4 形態素解析のオプション¶
MeCab では、MeCab.Tagger クラスにオプションの文字列を渡すことで、形態素解析の出力結果を細かく制御することができます。
分かち書き (-Owakati)¶
-Owakati を指定することで、単語ごとにスペースで区切った単純な分かち書きの結果を出力することができます。
単語の読み (-Oyomi)¶
-Oyomi を指定することで、単語の読み(カタカナ)だけを出力することができます。
辞書による出力形式の違い
使用する辞書によって、feature 属性の形式や使用可能なオプションが異なる場合があります。本章では unidic を使用しています。ipadic を使用した場合、feature 属性の項目は少なく、品詞は 名詞,一般,*,*,*,*,... のような形式になります。
12.2.5 特定の品詞の抽出¶
形態素解析ができたら、特定の品詞を抽出してみましょう。文書の解析でよく利用される重要な品詞は、「名詞」「形容詞」「動詞」です。
まずは、parseToNode() メソッドを用いて、形態素解析の結果をノードに変換します。ノードとは、数珠つなぎの要素のようなイメージで、next 属性によって隣り合うノードを順番に見て回ることができる仕組みを指します。
ノードの surface 属性には単語が、feature 属性には単語の特徴に関する情報が入っています。形態素解析をした後、単語のリストと品詞のリストを作成するプログラムは以下のようになります。
import MeCab
# MeCab のインスタンス生成
tagger = MeCab.Tagger()
# 解析するテキスト
text = "すもももももももものうち"
# 形態素解析を実行し、ノードに変換
node = tagger.parseToNode(text)
# 単語と品詞を格納するためのリスト
words = []
types = []
# ノードがなくなるまで解析結果を表示
while node:
if node.surface: # surface 属性(単語)が存在する場合
words.append(node.surface)
types.append(node.feature.split(",")[0])
node = node.next # 次のノードに移動
print("単語:", words)
print("品詞:", types)
node.feature 属性の中には、unidic の場合 "名詞,普通名詞,一般,*,*,*,スモモ,李,すもも,..." のように、複数の情報がカンマ区切りでまとめて入っています。ここから "名詞" という情報だけ抽出したい場合には、上のコードで示しているように、split() メソッドでテキストをカンマで区切り、生成されたリストの先頭を参照すれば良いというわけです。
品詞が「名詞」の単語のみを抽出してリスト化するには、以下のようにループの中で if 文を使用します。
import MeCab
# MeCab のインスタンス生成
tagger = MeCab.Tagger()
# 解析するテキスト
text = "すもももももももものうち"
# 形態素解析を実行し、ノードに変換
node = tagger.parseToNode(text)
# 単語を格納するためのリスト
nouns = []
# ノードがなくなるまで解析結果を表示
while node:
pos = node.feature.split(",")[0] # 品詞を取得
if pos == "名詞": # 品詞が「名詞」であった場合
nouns.append(node.surface)
node = node.next # 次のノードに移動
print("名詞:", nouns)
Playground:形態素解析
形態素解析 の Playground では、入力欄に文を打ち込むと、すぐに単語(形態素)に分割され、品詞ごとに色分けして表示されます。「ストップワードを除去」を切り替えると、「は」「を」「が」のような意味の薄い語が落ちて機械学習で使う語だけが残る様子が分かります。単語数と異なり語数(種類)の違いも確認してみましょう。
12.3 テキストの前処理¶
12.3.1 クリーニングの重要性¶
生のテキストデータには、不要な情報(記号、空白、URL など)が含まれていることがあります。これらを除去することで、機械学習モデルの性能を向上させます。このような前処理はクリーニング(またはクレンジング)と呼ばれます。
12.3.2 replace によるクリーニング¶
最も簡単なクリーニング方法は、replace() メソッドを使った文字列の置換です。
text = "これは【重要】なお知らせです。詳細は★こちら★をご覧ください。"
# 不要な記号を削除
text = text.replace("【", "")
text = text.replace("】", "")
text = text.replace("★", "")
print(text)
replace() メソッドでは、置換後の文字列を空文字 "" にすることで、特定の文字列を削除できます。
12.3.3 正規表現によるクリーニング¶
複数のパターンを一度に処理したい場合は、Python の re モジュールを使うと便利です。re.sub() 関数を使うと、正規表現にマッチする部分を置換できます。
正規表現の基本パターン¶
正規表現(Regular Expression)は、文字列のパターンを表現するための記法です。代表的なパターンを以下に示します。
| パターン | 意味 | 例 |
|---|---|---|
. |
改行以外の任意の 1 文字 | a.c → "abc", "a1c" など |
* |
直前の文字の 0 回以上の繰り返し | ab*c → "ac", "abc", "abbc" など |
+ |
直前の文字の 1 回以上の繰り返し | ab+c → "abc", "abbc" など("ac" は不可) |
? |
直前の文字の 0 回または 1 回 | ab?c → "ac", "abc" |
\d |
数字(半角 0-9 に限らない) | \d+ → "123", "42" など |
\s |
空白文字(スペース、タブ、改行) | \s+ → 連続する空白 |
\S |
空白以外の文字 | \S+ → 空白以外の連続する文字 |
\w |
単語を構成する文字(英数字とアンダースコア) | \w+ → "hello", "test_1" など |
[...] |
角括弧内のいずれか 1 文字 | [abc] → "a", "b", "c" のいずれか |
[^...] |
角括弧内以外の 1 文字 | [^abc] → "a", "b", "c" 以外 |
{n} |
直前の文字の n 回の繰り返し | \d{4} → 4 桁の数字 |
{n,m} |
直前の文字の n 回以上 m 回以下の繰り返し | \d{2,4} → 2〜4 桁の数字 |
^ |
文字列の先頭 | ^Hello → 先頭が "Hello" |
$ |
文字列の末尾 | end$ → 末尾が "end" |
正規表現を使ったクリーニングの例¶
import re
# 日本語テキストのサンプル
text = """
これは日本語のサンプルです。
メールアドレス: test@example.com
URL: https://example.jp
電話番号: 03-1234-5678
"""
print("元のテキスト:")
print(text)
# URL の削除
text = re.sub(r'https?://\S+', '', text)
# メールアドレスの削除
text = re.sub(r'\S+@\S+', '', text)
# 電話番号の削除(日本の形式)
text = re.sub(r'\d{2,4}-\d{2,4}-\d{4}', '', text)
# 余分な空白・改行の削除
text = re.sub(r'\s+', ' ', text).strip()
print("クリーニング後:")
print(text)
元のテキスト:
これは日本語のサンプルです。
メールアドレス: test@example.com
URL: https://example.jp
電話番号: 03-1234-5678
クリーニング後:
これは日本語のサンプルです。 メールアドレス: URL: 電話番号:
上記のコードで使用した正規表現パターンを解説します。
| パターン | 解説 |
|---|---|
https?://\S+ |
http または https(s? で s が 0 回か 1 回)で始まり、:// の後に空白以外の文字(\S+)が続く |
\S+@\S+ |
@ の前後に空白以外の文字が 1 文字以上ある(メールアドレスの簡易パターン) |
\d{2,4}-\d{2,4}-\d{4} |
2〜4 桁の数字、ハイフン、2〜4 桁の数字、ハイフン、4 桁の数字(電話番号のパターン) |
\s+ |
1 つ以上の連続する空白文字(スペース、タブ、改行など) |
raw 文字列(r'')について
正規表現パターンの前に r を付けると、バックスラッシュをエスケープせずにそのまま使えます。例えば、r'\d+' は '\\d+' と同じ意味になります。正規表現ではバックスラッシュを多用するため、r'' を使うのが一般的です。
12.3.4 不要語の除去と正規化¶
形態素解析で得られた単語列から、分析に不要な単語を取り除きます。また、表記を統一された形式にそろえる 正規化(Normalization) も、必要に応じてこの段階で行います。
- ストップワード除去: 「は」「を」「が」などを削除(不要語の除去)
- 品詞によるフィルタリング: 名詞・動詞・形容詞のみ抽出(不要語の除去)
- 表記の統一: 全角・半角の統一(正規化)
12.3.5 ストップワード除去¶
除去する語は目的に応じて選びます。たとえば感情分類では、「ない」などの否定表現を残す必要があります。
ストップワード(Stop Words) は、頻繁に出現するが意味的には重要でない単語です。日本語では「は」「を」「が」「に」「で」などの助詞がこれに該当します。
import MeCab
# 日本語のストップワード(例)
japanese_stop_words = ['は', 'を', 'が', 'に', 'で', 'と', 'の', 'も', 'や', 'など', 'て', 'た', 'だ', 'する', 'ある', 'いる']
# MeCab のインスタンス生成(分かち書き)
tagger = MeCab.Tagger("-Owakati")
text = "私は機械学習を勉強しています。"
# 形態素解析を実行
result = tagger.parse(text)
tokens = result.strip().split()
print(f"元の単語: {tokens}")
# ストップワード除去
filtered_tokens = [word for word in tokens if word not in japanese_stop_words]
print(f"フィルタ後: {filtered_tokens}")
元の単語: ['私', 'は', '機械', '学習', 'を', '勉強', 'し', 'て', 'い', 'ます', '。']
フィルタ後: ['私', '機械', '学習', '勉強', 'し', 'い', 'ます', '。']
12.3.6 品詞によるフィルタリング¶
より効果的な方法として、品詞情報を使って必要な単語のみを抽出する方法があります。
import MeCab
tagger = MeCab.Tagger()
text = "私は機械学習を勉強しています。自然言語処理は面白いです。"
node = tagger.parseToNode(text)
# 名詞、動詞、形容詞のみを抽出
content_words = []
while node:
pos = node.feature.split(",")[0]
if pos in ["名詞", "動詞", "形容詞"]:
content_words.append(node.surface)
node = node.next
print("内容語:", content_words)
12.3.7 前処理パイプライン¶
これまで学んだ処理を組み合わせて、前処理パイプラインを作成しましょう。
import re
import MeCab
# 日本語テキストの前処理パイプライン
def preprocess_japanese_text(text):
# 1. URLの削除
text = re.sub(r'https?://\S+', '', text)
# 2. メールアドレスの削除
text = re.sub(r'\S+@\S+', '', text)
# 3. 余分な空白・改行の削除
text = re.sub(r'\s+', '', text)
# 4. 形態素解析
tagger = MeCab.Tagger()
node = tagger.parseToNode(text)
tokens = []
while node:
# 品詞情報を取得
features = node.feature.split(",")
pos = features[0] # 品詞
surface = node.surface # 表層形
# 名詞、動詞、形容詞のみを抽出
if pos in ['名詞', '動詞', '形容詞']:
tokens.append(surface)
node = node.next
return tokens
# テスト
sample_text = """
私は機械学習を勉強しています。
自然言語処理は非常に面白いです。
詳細は https://example.jp をご覧ください。
"""
print("元のテキスト:")
print(sample_text)
processed_tokens = preprocess_japanese_text(sample_text)
print("前処理後の単語:")
print(processed_tokens)
print(f"\n単語数: {len(processed_tokens)}")
元のテキスト:
私は機械学習を勉強しています。
自然言語処理は非常に面白いです。
詳細は https://example.jp をご覧ください。
前処理後の単語:
['私', '機械', '学習', '勉強', 'し', 'い', '自然', '言語', '処理', '非常', '面白い', '詳細', 'ご覧', 'ください']
単語数: 14
12.3.8 複数文書の前処理¶
複数の文書を一度に処理する場合の例です。
import MeCab
# テキストから名詞を抽出
def extract_nouns(text, tagger):
node = tagger.parseToNode(text)
nouns = []
while node:
pos = node.feature.split(",")[0]
if pos == "名詞":
nouns.append(node.surface)
node = node.next
return nouns
# 複数の文書
documents = [
"吾輩は猫である。名前はまだ無い。",
"どこで生れたかとんと見当がつかぬ。",
"何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。"
]
# MeCab のインスタンス生成
tagger = MeCab.Tagger()
# 各文書から名詞を抽出
for i, doc in enumerate(documents, 1):
nouns = extract_nouns(doc, tagger)
print(f"文書{i}: {nouns}")
Playground:テキストの前処理
テキストの前処理 の Playground では、URL・メールアドレス・電話番号・装飾記号・余分な空白といったノイズを含む生テキストに対し、クリーニング項目をチェックで付け外しできます。削除される箇所が赤くハイライトされ、対応する re.sub() 関数の Python コードも一緒に表示されるので、正規表現がどこに効いているのかを対応づけながら確認できます。
12.4 テキストのベクトル化(BoW、TF-IDF)¶
12.4.1 なぜベクトル化が必要か¶
機械学習モデルは数値データしか扱えません。テキストを数値ベクトルに変換することで、機械学習の各種アルゴリズムを適用できるようになります。
12.4.2 ベクトル化手法の種類¶
主要なテキストのベクトル化手法には以下があります。
- Bag-of-Words (BoW): 単語の出現回数をカウント
- TF-IDF: 単語の重要度を考慮
- Word2Vec: 単語の意味をベクトル化
- BERT/Transformer: 文脈を考慮した表現
本節では、Bag-of-Words と TF-IDF の実装方法を紹介します。
12.4.3 使用するライブラリ¶
テキストのベクトル化には、scikit-learn の feature_extraction.text モジュールを使用することができます。
CountVectorizer クラス
テキストを BoW(単語の出現回数)ベクトルに変換するクラスです。
- 公式ドキュメント:
sklearn.feature_extraction.text.CountVectorizerクラス
TfidfVectorizer クラス
テキストを TF-IDF ベクトルに変換するクラスです。
- 公式ドキュメント:
sklearn.feature_extraction.text.TfidfVectorizerクラス
12.4.4 Bag-of-Words とは¶
Bag-of-Words(BoW) は、文書を「単語の袋」として表現する手法です。単語の順序を無視し、各単語の出現回数のみを考慮します。
BoW の特徴
- シンプル: 実装が簡単
- 語順を無視: "I love cats" と "cats love I" が同じベクトル
- スパース: ほとんどの要素が 0
12.4.5 BoW の実装例¶
BoW ベクトルを作成するには、scikit-learn の CountVectorizer クラスの fit_transform() メソッドを使用します。
import MeCab
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
# MeCab の初期化(分かち書きモード)
mecab = MeCab.Tagger("-Owakati")
# 日本語のサンプル文書
documents = [
"私は機械学習が好きです",
"私は機械学習も深層学習も好きです",
"機械学習は楽しいです"
]
# 形態素解析を実行
tokenized_docs = []
for doc in documents:
tokenized = mecab.parse(doc).strip()
tokenized_docs.append(tokenized)
print("形態素解析後:")
for doc in tokenized_docs:
print(f" {doc}")
# CountVectorizerの初期化
vectorizer = CountVectorizer()
# BoWベクトルの作成
bow_matrix = vectorizer.fit_transform(tokenized_docs)
# 語彙の確認
vocab = vectorizer.get_feature_names_out()
print("\n語彙:")
print(vocab)
# ベクトルの確認(密行列に変換)
bow_array = bow_matrix.toarray()
print("\nBoW ベクトル:")
print(bow_array)
# DataFrame 表示
df = pd.DataFrame(bow_array, columns=vocab)
print("\nDataFrame 表示:")
print(df)
形態素解析後:
私 は 機械 学習 が 好き です
私 は 機械 学習 も 深層 学習 も 好き です
機械 学習 は 楽しい です
語彙:
['です' '好き' '学習' '楽しい' '機械' '深層']
BoW ベクトル:
[[1 1 1 0 1 0]
[1 1 2 0 1 1]
[1 0 1 1 1 0]]
DataFrame 表示:
です 好き 学習 楽しい 機械 深層
0 1 1 1 0 1 0
1 1 1 2 0 1 1
2 1 0 1 1 1 0
1 文字の単語について
CountVectorizer クラスはデフォルトで 1文字の単語を除外 します。これは token_pattern パラメータのデフォルト値が r"(?u)\b\w\w+\b"(2文字以上の単語のみマッチ)になっているためです。
そのため「は」「が」「も」「私」などの1文字の単語は語彙に含まれません。
1文字の単語も含めたい場合
(?u): Unicode モード\b: 単語の境界\w+: 1文字以上の単語文字(\w\w+だと2文字以上)
fit, transform, fit_transform の違い
CountVectorizer クラスや TfidfVectorizer クラスには 3 つの主要なメソッドがあります。
| メソッド | 説明 |
|---|---|
fit(X) |
データから語彙を学習する(語彙の構築のみ) |
transform(X) |
学習済みの語彙を使ってデータをベクトル化する |
fit_transform(X) |
fit() と transform() を同時に実行する |
使い分け
- 訓練データ:
fit_transform()メソッドを使用(語彙の学習とベクトル化を同時に行う) - テストデータ:
transform()メソッドのみを使用(訓練データで学習した語彙を使う)
# 訓練データ: 語彙を学習しながらベクトル化
X_train_vec = vectorizer.fit_transform(X_train)
# テストデータ: 学習済みの語彙でベクトル化(fit しない)
X_test_vec = vectorizer.transform(X_test)
テストデータに fit_transform() メソッドを使うと、テストデータ独自の語彙が作られてしまい、訓練データと異なる特徴空間になるため注意が必要です。
Playground:Bag-of-Words
Bag-of-Words の Playground では、1 行 1 文書としてテキストを書き換えると、語彙とカウント行列がその場で作り直されます。「語順が違う2文を試す」ボタンで「私は猫が好き」と「猫は私が好き」を並べると、まったく同じ行になり、BoW が語順を一切見ないことを実感できます。語彙数やスパース率(0 の割合)の変化も確認してみましょう。
12.4.6 BoW のパラメータ¶
CountVectorizer クラスの主要なパラメータ
import MeCab
from sklearn.feature_extraction.text import CountVectorizer
# MeCab の初期化(分かち書きモード)
mecab = MeCab.Tagger("-Owakati")
# パラメータの例
vectorizer = CountVectorizer(
max_features=100, # 最大語彙数
min_df=2, # 最低文書頻度(2文書以上に出現)
max_df=0.8, # 最大文書頻度(80%以下の文書に出現)
ngram_range=(1, 2) # n-gram(単語と2単語の組み合わせ)
)
documents = [
"私は機械学習が好きです",
"私は機械学習も深層学習も好きです",
"機械学習は楽しいです",
"深層学習は面白いです"
]
# 形態素解析を実行
tokenized_docs = []
for doc in documents:
tokenized = mecab.parse(doc).strip()
tokenized_docs.append(tokenized)
bow_matrix = vectorizer.fit_transform(tokenized_docs)
print("語彙(n-gram含む):")
print(vectorizer.get_feature_names_out())
print("\nBoW ベクトル:")
print(bow_matrix.toarray())
12.4.7 N-gram¶
N-gram は、連続する N 個の単語を一つの特徴として扱う手法です。
- Unigram(1-gram): 単語単位(例: "機械", "学習")
- Bigram(2-gram): 2 単語の組み合わせ(例: "機械 学習")
- Trigram(3-gram): 3 単語の組み合わせ(例: "機械 学習 は")
import MeCab
from sklearn.feature_extraction.text import CountVectorizer
# MeCab の初期化(分かち書きモード)
mecab = MeCab.Tagger("-Owakati")
documents = [
"私は機械学習が好きです",
"機械学習は楽しいです"
]
# 形態素解析を実行
tokenized_docs = []
for doc in documents:
tokenized = mecab.parse(doc).strip()
tokenized_docs.append(tokenized)
# Unigram(デフォルト)
vectorizer_1gram = CountVectorizer(ngram_range=(1, 1))
bow_1gram = vectorizer_1gram.fit_transform(tokenized_docs)
# Bigram
vectorizer_2gram = CountVectorizer(ngram_range=(2, 2))
bow_2gram = vectorizer_2gram.fit_transform(tokenized_docs)
# Unigram + Bigram
vectorizer_both = CountVectorizer(ngram_range=(1, 2))
bow_both = vectorizer_both.fit_transform(tokenized_docs)
print("Unigram:")
print(vectorizer_1gram.get_feature_names_out())
print("\nBigram:")
print(vectorizer_2gram.get_feature_names_out())
print("\nUnigram + Bigram:")
print(vectorizer_both.get_feature_names_out())
Unigram:
['です' '好き' '学習' '楽しい' '機械']
Bigram:
['好き です' '学習 が' '学習 は' '機械 学習']
Unigram + Bigram:
['です' '好き' '好き です' '学習' '学習 が' '学習 は' '楽しい' '機械' '機械 学習']
12.4.8 TF-IDF とは¶
TF-IDF(Term Frequency-Inverse Document Frequency) は、単語の重要度を考慮したベクトル化手法です。
TF(Term Frequency): 文書内での単語の出現頻度
IDF(Inverse Document Frequency): レアな単語ほど高い値
TF-IDF
12.4.9 TF-IDF の直感的理解¶
例: "私" と "機械学習" の比較
- "私": ほとんどの文書に出現 → IDF が低い → 重要度が低い
- "機械学習": 特定の文書のみに出現 → IDF が高い → 重要度が高い
import numpy as np
# サンプル文書
documents = [
"私は猫が好きです",
"私は犬が好きです",
"猫と犬は動物です"
]
# 単語 "私" の出現状況
# 文書1: 1回, 文書2: 1回, 文書3: 0回 → 3文書中2文書に出現
# 単語 "動物" の出現状況
# 文書1: 0回, 文書2: 0回, 文書3: 1回 → 3文書中1文書に出現
# IDF の計算
total_docs = 3
docs_with_watashi = 2
docs_with_animal = 1
idf_wa = np.log(total_docs / docs_with_watashi)
idf_animal = np.log(total_docs / docs_with_animal)
print(f"IDF('私'): {idf_wa:.4f}")
print(f"IDF('動物'): {idf_animal:.4f}")
print(f"\n'動物' の IDF は '私' の {idf_animal / idf_wa:.2f} 倍")
12.4.10 TF-IDF の実装¶
TfidfVectorizer クラスの既定の IDF は log((1+n)/(1+df(t))) + 1 です。上の手計算の式とは異なり、分子・分母と対数の外に1を加えます。出現回数に IDF を掛けた後、文書ベクトルを L2 正規化します。
TF-IDF ベクトルを作成するには、scikit-learn の TfidfVectorizer クラスの fit_transform() メソッドを使用します。
import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
# MeCab の初期化(分かち書きモード)
mecab = MeCab.Tagger("-Owakati")
# 日本語のサンプル文書
documents = [
"私は機械学習が好きです",
"私は機械学習も深層学習も好きです",
"機械学習は楽しいです",
"猫が窓辺に座っています"
]
# 形態素解析を実行
tokenized_docs = []
for doc in documents:
tokenized = mecab.parse(doc).strip()
tokenized_docs.append(tokenized)
# TF-IDF ベクトライザ
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(tokenized_docs)
# 語彙
vocab = tfidf_vectorizer.get_feature_names_out()
print("語彙:")
print(vocab)
# TF-IDF 行列
tfidf_array = tfidf_matrix.toarray()
print("\nTF-IDF ベクトル:")
print(tfidf_array)
# DataFrame 表示
df = pd.DataFrame(tfidf_array, columns=vocab)
print("\nDataFrame 表示:")
print(df.round(3))
# IDF 値の確認
print("\nIDF 値:")
idf_df = pd.DataFrame({
'単語': vocab,
'IDF': tfidf_vectorizer.idf_
}).sort_values('IDF', ascending=False)
print(idf_df)
語彙:
['です' 'ます' '好き' '学習' '座っ' '楽しい' '機械' '深層' '窓辺']
TF-IDF ベクトル:
[[0.47006328 0. 0.58062167 0.47006328 0. 0.
0.47006328 0. 0. ]
[0.3165406 0. 0.39099061 0.63308119 0. 0.
0.3165406 0.49592201 0. ]
[0.42817512 0. 0. 0.42817512 0. 0.67081906
0.42817512 0. 0. ]
[0. 0.57735027 0. 0. 0.57735027 0.
0. 0. 0.57735027]]
DataFrame 表示:
です ます 好き 学習 座っ 楽しい 機械 深層 窓辺
0 0.470 0.000 0.581 0.470 0.000 0.000 0.470 0.000 0.000
1 0.317 0.000 0.391 0.633 0.000 0.000 0.317 0.496 0.000
2 0.428 0.000 0.000 0.428 0.000 0.671 0.428 0.000 0.000
3 0.000 0.577 0.000 0.000 0.577 0.000 0.000 0.000 0.577
IDF 値:
単語 IDF
1 ます 1.916291
8 窓辺 1.916291
4 座っ 1.916291
7 深層 1.916291
5 楽しい 1.916291
2 好き 1.510826
0 です 1.223144
3 学習 1.223144
6 機械 1.223144
Playground:TF-IDF
TF-IDF の Playground では、1 行 1 文書のテキストを書き換えると TF-IDF 行列が作り直され、値が大きいセルほど濃く塗られます。TF が高くても IDF(レア度)が低ければ重みは伸びないこと、「です」「は」のように全文書に出る語は IDF が 0 になって TF-IDF も 0 になることを確かめられます。文書を足し引きして IDF が動く様子も試してみましょう。
12.5 類似文書検索¶
テキストをベクトル化できるようになると、文書間の類似度を計算し、ある文書に類似した文書を検索することが可能になります。本節では、TF-IDF ベクトルと コサイン類似度 を用いた類似文書検索の方法を学びます。
12.5.1 コサイン類似度¶
コサイン類似度(cosine similarity)は、2 つのベクトル間の角度に基づく類似度の指標です。ベクトルの大きさ(ノルム)ではなく方向の近さを測るため、文書の長さの違いに影響されにくいという特徴があります。コサイン類似度は以下の式で定義されます。
TF-IDF ベクトルの場合は要素がすべて非負のため、値は \(0\) から \(1\) の範囲になります。\(1\) に近いほど類似度が高く、\(0\) はこのベクトル表現で共通する語の成分がないことを意味します。文書の意味が無関係だとは限りません。
12.5.2 TF-IDF ベクトルを用いた類似文書検索の実装¶
文書コーパス(自然言語処理で用いる、大量のテキストを集めたデータ)を TF-IDF でベクトル化し、クエリ(検索したい文書)とのコサイン類似度を scikit-learn の cosine_similarity() 関数で計算すれば、コーパス中の文書を類似度の高い順に並べ替えることができます。
import numpy as np
import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# サンプル文書コーパス
documents = [
"機械学習はデータからパターンを学習する技術です",
"深層学習はニューラルネットワークを用いた機械学習の手法です",
"自然言語処理はテキストデータを分析する技術です",
"画像認識は写真や動画から物体を検出する技術です",
"機械学習を使ってテキストを分類することができます",
]
# MeCabで分かち書き
tagger = MeCab.Tagger("-Owakati")
wakati_docs = [tagger.parse(doc).strip() for doc in documents]
# TF-IDFベクトルの生成
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(wakati_docs)
# クエリに類似した文書を検索して順位付けする
def search_similar(query, documents, vectorizer, tfidf_matrix, top_n=3):
# クエリをMeCabで分かち書き
tagger = MeCab.Tagger("-Owakati")
query_wakati = tagger.parse(query).strip()
# クエリをTF-IDFベクトルに変換(学習済みのvectorizerを使用)
query_vec = vectorizer.transform([query_wakati])
# クエリとコーパス内の各文書とのコサイン類似度を計算
similarities = cosine_similarity(query_vec, tfidf_matrix)[0]
# 類似度の高い順にソート
ranking = np.argsort(similarities)[::-1]
print(f"クエリ: 「{query}」\n")
print("類似文書ランキング:")
for rank, idx in enumerate(ranking[:top_n], 1):
print(f" {rank}位 (類似度: {similarities[idx]:.4f}): {documents[idx]}")
# 検索の実行
query = "テキストデータの分析と分類の方法"
search_similar(query, documents, vectorizer, tfidf_matrix)
クエリ: 「テキストデータの分析と分類の方法」
類似文書ランキング:
1位 (類似度: 0.5013): 自然言語処理はテキストデータを分析する技術です
2位 (類似度: 0.3467): 機械学習を使ってテキストを分類することができます
3位 (類似度: 0.1510): 機械学習はデータからパターンを学習する技術です
クエリに「テキスト」「分析」が含まれているため、テキストデータの分析について述べた文書3が最も高い類似度で検索されています。次いで、「テキスト」「分類」を含む文書5が2位となっています。
このように、TF-IDF ベクトルとコサイン類似度を組み合わせることで、キーワードの出現パターンに基づいた類似文書検索を実現できます。次章では、この手法を文書分類や感情分析といったより高度なテキスト分析タスクに応用していきます。
Playground:類似文書検索
類似文書検索 の Playground では、クエリを 1 文字打つごとに文書がコサイン類似度の高い順に並び替わります。注目したいのは限界のほうで、「人工知能を学びたい」で検索すると、意味は近いのに共通する単語が 1 つもないため類似度が 0 になります。TF-IDF が単語の表面一致しか見られないという 語彙の壁 を体験できます。これが次章の分散表現につながります。
12.6 演習問題¶
演習 12-1: テキストクリーニング¶
演習 12-1
以下のテキストには URL・メールアドレス・電話番号・装飾記号が含まれています。正規表現を使ってこれらをすべて除去し、最後に余分な空白も整えてください。
text = """
明日のセミナー【締切間近!】の案内です★
詳しくは https://example.com まで!
質問は contact@example.jp までお願いします。
電話番号: 03-1234-5678
"""
タスク
解答例
import re
text = """
明日のセミナー【締切間近!】の案内です★
詳しくは https://example.com まで!
質問は contact@example.jp までお願いします。
電話番号: 03-1234-5678
"""
# 1. URL の削除
text = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", text)
# 2. メールアドレスの削除
text = re.sub(r"\S+@\S+", "", text)
# 3. 電話番号の削除
text = re.sub(r"\d{2,4}-\d{2,4}-\d{4}", "", text)
# 4. 装飾記号の削除
text = re.sub(r"[【】★]", "", text)
# 5. 余分な空白の整理 + strip
text = re.sub(r"\s+", " ", text).strip()
print(text)
考察:正規表現で消えるのは、指定したパターンに一致する部分だけです。URL、メールアドレス、電話番号、装飾記号は消えましたが、「!」や「:」、句点は残っています。これらも正規表現で消せますし(書籍本文の preprocess() 関数は記号類をまとめて除去しています)、演習 12-3 のように形態素解析の後で品詞を絞って落とすこともできます。なお、メールアドレスに使った \S+@\S+ は「空白以外の文字列+@+空白以外の文字列」という緩いパターンで、メールアドレス以外の文字列に一致することもあります。もっと厳密にするなら、演習 12-3 で使った [\w\.-]+@[\w\.-]+\.\w+ のようなパターンにします。
演習 12-2: 名詞(内容語)の抽出¶
演習 12-2
本章の品詞フィルタリングのコードを変更し、指定した品詞のみを抽出する関数を作成して、以下の文から名詞のみを抽出してください。
タスク
MeCab.Taggerクラスで解析器を作成する- 指定した品詞のみを抽出する関数
extract_by_pos(text, target_pos)を定義する(parseToNode()メソッドで各ノードの品詞を調べ、target_posに含まれるsurface属性だけをリストに追加する) - 上の文から名詞のみを抽出して表示する
- 名詞・動詞・形容詞を残した場合と結果を比較する
- 自分で考えた文にも適用して、抽出結果を確認する
解答例
import MeCab
tagger = MeCab.Tagger()
def extract_by_pos(text, target_pos):
"""指定した品詞の単語のみを抽出する"""
words = []
node = tagger.parseToNode(text)
while node:
if node.surface != "":
pos = node.feature.split(",")[0]
if pos in target_pos:
words.append(node.surface)
node = node.next
return words
text = "機械学習の技術は急速に進歩している"
print("名詞のみ:", extract_by_pos(text, ["名詞"]))
print("名詞・動詞・形容詞:",
extract_by_pos(text, ["名詞", "動詞", "形容詞"]))
# 自分で考えた文でも確認
my_text = "昨日は友達と新宿で美味しいラーメンを食べました"
print("自作文の名詞:", extract_by_pos(my_text, ["名詞"]))
名詞のみ: ['機械', '学習', '技術', '進歩']
名詞・動詞・形容詞: ['機械', '学習', '技術', '進歩', 'し', 'いる']
自作文の名詞: ['昨日', '友達', '新宿', 'ラーメン']
考察:名詞だけに絞ると、「し」「いる」(動詞)が除かれ、内容を表す単語が残ります。「進歩している」の「進歩」はサ変接続の名詞なので、名詞だけに絞っても残ります。文書分類やキーワード抽出では、「する」「いる」のような意味の薄い動詞がじゃまになりやすいため、名詞だけを使うことがよくあります。一方、感情分析では「良い」「悪い」のような形容詞が大事なので、目的に合わせて品詞を選びます。自作文でも、「昨日・友達・新宿・ラーメン」の名詞だけで、文の話題がおおよそわかります。
演習 12-3: 前処理パイプラインの構築¶
演習 12-3
複数の日本語テキストに対して、「クリーニング → 形態素解析 → 品詞フィルタ → ストップワード除去」までを一つの関数 preprocess(text) にまとめ、4 つのテキストを処理してください。
texts = [
"AI技術の発展は素晴らしいです!詳細は https://example.jp へ。",
"機械学習を勉強することで、新しいことができるようになります。",
"本日のテーマ:ディープラーニング入門。質問がある方は contact@ml.example.com まで★",
"価格は1980円、送料は300円です。",
]
stopwords = {"する", "ある", "いる", "なる", "こと", "もの", "ため", "とき"}
タスク
- 関数
preprocess(text)を定義する。中で以下を順に行う- URL(
https?://...)とメールアドレスをre.sub()関数で削除 - 数字(
\d+)を削除 - 装飾記号(
【・】・★・:)を削除 - 連続する空白を整理
MeCabで形態素解析し、品詞が 名詞・動詞・形容詞 のものだけを残す- 上のストップワードに含まれる単語を除外する(
node.surface属性での照合で OK)
- URL(
- 4 つのテキストそれぞれに
preprocess()関数 を適用し、結果を表示する
※ 本来ストップワードは活用形まで考慮した除去をすることが望ましいですが、本演習では surface(表層形)での照合で十分です。
import re
import MeCab
texts = [
"AI技術の発展は素晴らしいです!詳細は https://example.jp へ。",
"機械学習を勉強することで、新しいことができるようになります。",
"本日のテーマ:ディープラーニング入門。質問がある方は contact@ml.example.com まで★",
"価格は1980円、送料は300円です。",
]
stopwords = {"する", "ある", "いる", "なる", "こと", "もの", "ため", "とき"}
# 1. preprocess(text) 関数の定義
# 2. 各テキストに適用して結果を表示
解答例
import re
import MeCab
texts = [
"AI技術の発展は素晴らしいです!詳細は https://example.jp へ。",
"機械学習を勉強することで、新しいことができるようになります。",
"本日のテーマ:ディープラーニング入門。質問がある方は contact@ml.example.com まで★",
"価格は1980円、送料は300円です。",
]
stopwords = {"する", "ある", "いる", "なる", "こと", "もの", "ため", "とき"}
tagger = MeCab.Tagger()
def preprocess(text):
"""クリーニング→形態素解析→品詞フィルタ→ストップワード除去"""
# 1. テキストクリーニング
text = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", text) # URL除去
text = re.sub(r"[\w\.-]+@[\w\.-]+\.\w+", "", text) # メールアドレス除去
text = re.sub(r"\d+", "", text) # 数字の除去
text = re.sub(r"[【】★:]", "", text) # 装飾記号の除去
text = re.sub(r"\s+", " ", text).strip() # 空白の正規化
# 2. 形態素解析と品詞フィルタリング + ストップワード除去
words = []
node = tagger.parseToNode(text)
while node:
if node.surface != "":
pos = node.feature.split(",")[0]
if pos in ["名詞", "動詞", "形容詞"] and node.surface not in stopwords:
words.append(node.surface)
node = node.next
return words
for i, text in enumerate(texts, 1):
print(f"テキスト{i}: {preprocess(text)}")
テキスト1: ['AI', '技術', '発展', '素晴らしい', '詳細']
テキスト2: ['機械', '学習', '勉強', '新しい', 'できる', 'なり']
テキスト3: ['本日', 'テーマ', 'ディープ', 'ラーニング', '入門', '質問', '方']
テキスト4: ['価格', '円', '送料', '円']
考察:数字を除去したので、テキスト4では「1980」「300」が消え、「価格」「円」「送料」が残りました。数字の除去は、URL とメールアドレスの除去より後に置きます。先に数字を消すと、数字を含む URL やメールアドレスがパターンに一致しなくなり、一部の文字列が残ることがあります。また、テキスト2では「なり」が残っています。ストップワードには基本形の「なる」しか登録しておらず、表層形(node.surface)で照合しているため、活用した形は除けません。node.feature から基本形を取り出して照合すれば除けます。
演習 12-4: BoW と TF-IDF の比較¶
演習 12-4
本章のサンプル文書コーパス(5 文)に対して、CountVectorizer クラス(BoW)と TfidfVectorizer クラス(TF-IDF)のそれぞれでベクトル化を行い、同じ単語の値がどう異なるかを比較してください。
documents = [
"機械学習はデータからパターンを学習する技術です",
"深層学習はニューラルネットワークを用いた機械学習の手法です",
"自然言語処理はテキストデータを分析する技術です",
"画像認識は写真や動画から物体を検出する技術です",
"機械学習を使ってテキストを分類することができます",
]
タスク
MeCab.Tagger("-Owakati")で全文書を分かち書きするCountVectorizerクラスとTfidfVectorizerクラスのそれぞれでベクトル化する- 「技術」「学習」「画像」「分類」などいくつかの単語について、文書ごとの BoW の値と TF-IDF の値を並べて表示する
- 多くの文書に登場する単語と、特定の文書にしか登場しない単語とで、TF-IDF の値にどのような違いが現れるか考察する
import MeCab
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
documents = [
"機械学習はデータからパターンを学習する技術です",
"深層学習はニューラルネットワークを用いた機械学習の手法です",
"自然言語処理はテキストデータを分析する技術です",
"画像認識は写真や動画から物体を検出する技術です",
"機械学習を使ってテキストを分類することができます",
]
# 1. MeCab で分かち書き
# 2. CountVectorizer と TfidfVectorizer でベクトル化
# 3. 単語ごとに BoW と TF-IDF の値を並べて表示
解答例
import MeCab
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
# 本章のサンプル文書コーパス(5文)
documents = [
"機械学習はデータからパターンを学習する技術です",
"深層学習はニューラルネットワークを用いた機械学習の手法です",
"自然言語処理はテキストデータを分析する技術です",
"画像認識は写真や動画から物体を検出する技術です",
"機械学習を使ってテキストを分類することができます",
]
# MeCabで分かち書き
tagger = MeCab.Tagger("-Owakati")
wakati_docs = [tagger.parse(doc).strip() for doc in documents]
# BoWとTF-IDFの両方でベクトル化
count_vec = CountVectorizer()
bow = count_vec.fit_transform(wakati_docs)
tfidf_vec = TfidfVectorizer()
tfidf = tfidf_vec.fit_transform(wakati_docs)
# 特徴的な単語について、文書ごとの値を比較
vocab = list(count_vec.get_feature_names_out())
print("単語 / BoW(各文書の出現回数) / TF-IDF(各文書の値)")
for w in ["技術", "学習", "画像", "分類"]:
i = vocab.index(w)
bow_col = bow.toarray()[:, i]
tfidf_col = tfidf.toarray()[:, i].round(2)
print(f"{w}: {bow_col} {tfidf_col}")
単語 / BoW(各文書の出現回数) / TF-IDF(各文書の値)
技術: [1 0 1 1 0] [0.28 0. 0.27 0.24 0. ]
学習: [2 2 0 0 1] [0.56 0.49 0. 0. 0.26]
画像: [0 0 0 1 0] [0. 0. 0. 0.36 0. ]
分類: [0 0 0 0 1] [0. 0. 0. 0. 0.38]
考察:BoW は出現回数なので、「技術」も「画像」も 1 回出れば同じ 1 です。TF-IDF では、5 文書中 3 文書に出る「技術」は 0.24〜0.28 に下がり、文書4だけの「画像」(0.36)や文書5だけの「分類」(0.38)のほうが高くなります。多くの文書に出る単語は文書を見分ける手がかりにならないので、TF-IDF はその重みを小さくします。そのため文書分類や検索では、TF-IDF のほうが BoW より役に立つことが多くなります。
なお、感情分析などのテキスト分類を題材とした演習は、第 13 章で扱います。
まとめ¶
本章では、自然言語処理におけるテキストの前処理からベクトル化、類似文書検索までを学びました。
第 12 章のまとめ
自然言語処理の基礎
- 人間の言葉をコンピュータで処理する技術
- 機械翻訳、感情分析、チャットボットなど幅広い応用
- 言語の曖昧性、文脈依存性が課題
テキストクリーニング
- URL、メールアドレス、記号の削除
- 余分な空白の削除
- 正規表現を使った柔軟な処理
形態素解析(MeCab)
- 文を形態素(単語)に分割
parse(): 形態素解析の実行parseToNode(): ノード単位での詳細情報取得-Owakati: 分かち書き出力
不要語の除去
- ストップワード除去: 頻出だが重要でない単語を削除
- 品詞フィルタリング: 名詞・動詞・形容詞のみ抽出
前処理パイプライン
- 複数の処理を順番に適用
- データの品質向上に不可欠
- タスクに応じて適切な処理を選択
テキストのベクトル化
- 機械学習モデルは数値データが必要
- BoW: 単語の出現回数をカウント。シンプルだが語順を無視
- TF-IDF: TF(文書内頻度)× IDF(レア度)で単語の重要度を考慮
- N-gram: 連続する N 個の単語を特徴とし、語順の情報を部分的に保持
類似文書検索
- コサイン類似度: ベクトル間の角度に基づく類似度の指標
- TF-IDF ベクトル + コサイン類似度で類似文書を検索・順位付け