11. 自然言語処理 ②:Bag-of-Words / TF-IDF による文書分類¶
第 11 回では、Bag-of-Words(BoW)と TF-IDF を使ったテキストのベクトル化と文書分類について学びます。
11.1 テキストのベクトル化¶
11.1.1 なぜベクトル化が必要か¶
機械学習モデルは数値データしか扱えません。テキストを数値ベクトルに変換することで、機械学習の各種アルゴリズムを適用できるようになります。
11.1.2 ベクトル化手法の種類¶
主要なテキストのベクトル化手法には以下があります。
- Bag-of-Words (BoW): 単語の出現回数をカウント
- TF-IDF: 単語の重要度を考慮
- Word2Vec: 単語の意味をベクトル化
- BERT/Transformer: 文脈を考慮した表現
今回は、Bag-of-Words と TF-IDF の実装方法を紹介します。
11.1.3 使用するライブラリ¶
テキストのベクトル化には、scikit-learn の feature_extraction.text モジュールを使用することができます。
CountVectorizer
テキストを BoW(単語の出現回数)ベクトルに変換するクラスです。
TfidfVectorizer
テキストを TF-IDF ベクトルに変換するクラスです。
11.2 Bag-of-Words(BoW)¶
11.2.1 Bag-of-Words とは¶
Bag-of-Words(BoW) は、文書を「単語の袋」として表現する手法です。単語の順序を無視し、各単語の出現回数のみを考慮します。
BoW の特徴:
- シンプル: 実装が簡単
- 語順を無視: "I love cats" と "cats love I" が同じベクトル
- スパース: ほとんどの要素が 0
11.2.2 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() を使うと、テストデータ独自の語彙が作られてしまい、訓練データと異なる特徴空間になるため注意が必要です。
11.2.3 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())
11.2.4 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:
['です' '好き' '好き です' '学習' '学習 が' '学習 は' '楽しい' '機械' '機械 学習']
11.3 TF-IDF¶
11.3.1 TF-IDF とは¶
TF-IDF(Term Frequency-Inverse Document Frequency) は、単語の重要度を考慮したベクトル化手法です。
TF(Term Frequency): 文書内での単語の出現頻度
IDF(Inverse Document Frequency): レアな単語ほど高い値
TF-IDF:
11.3.2 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} 倍")
11.3.3 TF-IDF の実装¶
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
11.4 文書分類の実践¶
11.4.1 問題設定¶
感情分析(Sentiment Analysis)のタスクを実装することを考えます。具体的には、コメディ映画のレビュー文がポジティブであるかネガティブであるかの分類を行います。
処理の流れ:
- CSV ファイルからデータを読み込み
- MeCab で形態素解析(分かち書き)
- BoW によるベクトル化
- ロジスティック回帰による分類
- 予測結果の評価
11.4.2 データの準備¶
演習では、以下のような日本語のコメディ映画レビュー(positive, negative ともに 100 件)を含む movie_reviews.csv ファイルを使用します。本ファイルは、CHIKUWA Editor の sample フォルダ内にも置いてあります。
label,text
positive,笑いのセンスが抜群で、映画館中が爆笑に包まれた。何度でも観たいと思える作品だった。
positive,ギャグのタイミングが絶妙で、何度も声を出して笑った。久しぶりに腹を抱えて笑える映画だった。
positive,主演俳優のコミカルな演技が光っていて、終始ニヤニヤが止まらなかった。楽しい時間を過ごせた。
...
...
...
negative,笑いを狙いすぎている感じが透けて見えて、空回りしているコメディ作品だった。冷めてしまった。
negative,ギャグのタイミングが悪く、笑うべき場面で笑うことができなかった。間の取り方に問題がある。
negative,下ネタばかりで品がなく、見ていて不快な気持ちになってしまうコメディ映画だった。趣味が悪い。
...
...
...
import pandas as pd
# データの読み込み
df = pd.read_csv('movie_reviews.csv')
print("データの確認:")
print(df.head(10))
print(f"\n総データ数: {len(df)}")
print(f"\nラベルの分布:")
print(df['label'].value_counts())
データの確認:
label text
0 positive 笑いのセンスが抜群で、映画館中が爆笑に包まれた。何度でも観たいと思える作品だった。
1 positive ギャグのタイミングが絶妙で、何度も声を出して笑った。久しぶりに腹を抱えて笑える映画だった。
2 positive 主演俳優のコミカルな演技が光っていて、終始ニヤニヤが止まらなかった。楽しい時間を過ごせた。
3 positive シュールなユーモアが随所に散りばめられていて、製作陣のセンスの良さが伝わってくる作品だった。
4 positive 脚本が練られていて、伏線を回収するギャグには思わず膝を打った。笑いの構成が本当に上手い。
5 positive テンポの良いボケとツッコミの応酬が続き、あっという間に上映時間が過ぎた。退屈する暇がなかった。
6 positive 笑いながらも心温まるストーリーで、観終わった後に幸せな気持ちになれた。大切な人と観たい映画だ。
7 positive コメディとしての完成度が高く、何度でも繰り返し観たくなる作品だと思う。友人にも勧めたい。
8 positive 脇役陣のボケが絶妙で、主役を食うほどの存在感を見せていた。キャスティングがとても良い。
9 positive 下品になりすぎず、子供から大人まで誰でも楽しめる上質なユーモアが詰まった作品だと思う。
総データ数: 200
ラベルの分布:
label
positive 100
negative 100
Name: count, dtype: int64
11.4.3 前処理から分類まで¶
前処理から学習、分類、評価までの一連の流れは、以下のようなコードになります。
# 注:以下は、df に movie_reviews.csv を読み込んでいることを前提としたコード
import MeCab
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# MeCab の初期化(分かち書きモード)
tagger = MeCab.Tagger('-Owakati')
# 形態素解析
tokenized_list = []
for text in df['text']:
tokenized = tagger.parse(text).strip()
tokenized_list.append(tokenized)
df['text_tokenized'] = tokenized_list
# ラベルを数値に変換(positive=1, negative=0)
df['label_num'] = (df['label'] == 'positive').astype(int)
# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
df['text_tokenized'], df['label_num'],
test_size=0.2,
random_state=42
)
# BoW ベクトル化
bow_vectorizer = CountVectorizer()
X_train_bow = bow_vectorizer.fit_transform(X_train)
X_test_bow = bow_vectorizer.transform(X_test)
print(f"\n語彙数: {len(bow_vectorizer.get_feature_names_out())}")
print(f"訓練データの形状: {X_train_bow.shape}")
# ロジスティック回帰で学習
model_bow = LogisticRegression(random_state=42, max_iter=1000)
model_bow.fit(X_train_bow, y_train)
# 予測
y_pred_bow = model_bow.predict(X_test_bow)
# 評価
accuracy_bow = accuracy_score(y_test, y_pred_bow)
print(f"\nBoW の結果:")
print(f"精度: {accuracy_bow:.2%}")
print("\n分類レポート:")
print(classification_report(y_test, y_pred_bow, target_names=['Negative', 'Positive']))
語彙数: 755
訓練データの形状: (160, 755)
BoW の結果:
精度: 75.00%
分類レポート:
precision recall f1-score support
Negative 0.70 0.84 0.76 19
Positive 0.82 0.67 0.74 21
accuracy 0.75 40
macro avg 0.76 0.75 0.75 40
weighted avg 0.76 0.75 0.75 40
考えてみよう
混同行列を見て、予測結果を詳しく確認してみましょう。また、誤分類されたテキストがどのようなものか調べ、なぜ誤分類されたのか考えてみましょう。
11.4.4 新しいレビューの予測¶
新しいレビューに対し、学習済みモデル model_bow を用いて予測を行いたい場合は、以下のようにします。
# 新しいレビューを予測
new_review = "こんなに面白いコメディ映画は生まれて初めて。"
# トークン化
new_review_tokenized = tagger.parse(new_review).strip()
# BoWベクトル化(リストで渡す必要がある)
new_review_bow = bow_vectorizer.transform([new_review_tokenized])
# 予測
prediction = model_bow.predict(new_review_bow)
probability = model_bow.predict_proba(new_review_bow)
sentiment = "ポジティブ" if prediction[0] == 1 else "ネガティブ"
confidence = probability[0][prediction[0]] * 100
print(f"レビュー: {new_review}")
print(f"予測: {sentiment} (確信度: {confidence:.1f}%)")
11.5 演習問題¶
演習 11-1: BoW による感情分析¶
演習 11-1
映画のレビュー(positive, negative)をまとめた movie_reviews.csv を使って、BoW による感情分析モデルを構築してください。
タスク:
- CSV ファイルを読み込む
- 形態素解析を実行
- データを訓練データとテストデータに分割
- BoW でベクトル化
- ロジスティック回帰で分類モデルを作成
- テストデータで評価(混同行列の可視化)
演習 11-2: TF-IDF による感情分析¶
演習 11-2
映画のレビュー(positive, negative)をまとめた movie_reviews.csv を使って、TF-IDF による感情分析モデルを構築してください。
タスク:
- CSV ファイルを読み込む
- 形態素解析を実行
- データを訓練データとテストデータに分割
- TF-IDF でベクトル化
- ロジスティック回帰で分類モデルを作成
- テストデータで評価(混同行列の可視化)
- 重要な特徴語を確認
11.6 課題 11: メール分類¶
課題 11
メールのテキストをまとめた emails.csv を使って、メールを「ビジネスメール (business)」「プライベートメール (private)」「スパムメール (spam)」の 3 クラスに分類するモデルを構築し、精度を検証してください。
※ 加点要素:精度を高めるための工夫(品詞抽出やストップワード除去などの前処理、ベクトル化のパラメータ調整、分類器の選択など)
提出物:
- Word ファイルで作成し、PDF 形式で出力して提出(学生番号_氏名_11.pdf)
- 問題文・ソースコード・結果・考察を PDF 内に含めること(参考:
機械学習_レポートサンプル.docx)
提出期限: 2026年1月9日(金)23:59
提出先: manaba
ヒント
3 つ以上のラベルを数値に変換したい場合、例えば以下のように for 文を if 文を使う方法が考えられます。
11.7 まとめ¶
本章では、Bag-of-Words と TF-IDF を使ったテキストのベクトル化と文書分類について学びました。
第 11 回のまとめ
テキストのベクトル化
- 機械学習モデルは数値データが必要
- テキストを数値ベクトルに変換
- 代表的手法:BoW、TF-IDF
Bag-of-Words(BoW)
- 単語の出現回数をカウント
- シンプルで実装が容易
- 語順を無視する
- スパースなベクトル
TF-IDF
- 単語の重要度を考慮
- TF(文書内頻度)× IDF(レア度)
- 頻出単語の重要度を下げる
- BoW より高精度なことが多い
N-gram
- 連続する N 個の単語を特徴とする
- Bigram、Trigram など
- 語順の情報を部分的に保持
文書分類
- 感情分析、トピック分類など
- TF-IDF + ロジスティック回帰が基本
- Naive Bayes、SVM、Random Forest も有効(本講義では割愛)