第 13 章 演習解答¶
第 13 章「テキストデータを用いた機械学習」の演習問題の解答例です。問題は 第 13 章 演習問題 を参照してください。
演習 13-1: TF-IDF と分類器の比較¶
import numpy as np
import pandas as pd
import MeCab
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# 1. データ読み込み + 分かち書き関数の定義
df = pd.read_csv("movie_reviews.csv")
tagger = MeCab.Tagger("-Owakati")
def tokenize(text):
return tagger.parse(text).strip().split()
texts = df["text"].values
labels = df["label"].values
# 2. テキストを先に分割し、訓練側だけで語彙とIDFを学習
train_texts, test_texts, y_train, y_test = train_test_split(
texts, labels, test_size=0.2, random_state=42
)
vectorizer = TfidfVectorizer(tokenizer=tokenize, token_pattern=None)
X_train = vectorizer.fit_transform(train_texts)
X_test = vectorizer.transform(test_texts)
# 3. 3 つの分類器を比較
models = {
"ナイーブベイズ": MultinomialNB(),
"SVM": LinearSVC(max_iter=1000),
"ロジスティック回帰": LogisticRegression(max_iter=1000),
}
for name, model in models.items():
model.fit(X_train, y_train)
acc = accuracy_score(y_test, model.predict(X_test))
print(f"{name}: 正解率 {acc:.4f}")
# 4. ロジスティック回帰の係数から重要語を抽出
# classes_ は ['negative' 'positive'] なので、係数が大きいほど positive 寄り
model_lr = models["ロジスティック回帰"]
feature_names = vectorizer.get_feature_names_out()
coef = model_lr.coef_[0]
top_pos = np.argsort(coef)[::-1][:10]
top_neg = np.argsort(coef)[:10]
print("\nポジティブ寄りの単語 トップ10:")
print(", ".join(feature_names[i] for i in top_pos))
print("\nネガティブ寄りの単語 トップ10:")
print(", ".join(feature_names[i] for i in top_neg))
ナイーブベイズ: 正解率 0.7500
SVM: 正解率 0.7500
ロジスティック回帰: 正解率 0.7750
ポジティブ寄りの単語 トップ10:
に, ある, あり, それ, 幸せ, 満足, 気, する, できる, せ
ネガティブ寄りの単語 トップ10:
ない, た, 悪い, しか, 印象, が, しまう, 。, 感じ, レベル
考察:語彙と IDF は訓練テキストだけから求め、テストテキストには同じ変換を使います。この分割ではロジスティック回帰(0.7750)が最も高くなりました。重要語には「満足」「悪い」などの感情語のほか、「に」「た」のような助詞や助動詞も入ります。ストップワードを除くときは、否定の「ない」まで消さないよう注意します。分類器を選ぶときは訓練データ内の交差検証を使い、このテスト結果で選び直さないようにします。
演習 13-2: ベクトル化の設定変更¶
import pandas as pd
import MeCab
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.metrics import accuracy_score
# 演習13-1と同じテキストの分割
df = pd.read_csv("movie_reviews.csv")
train_texts, test_texts, y_train, y_test = train_test_split(
df["text"].values, df["label"].values, test_size=0.2, random_state=42
)
tagger = MeCab.Tagger("-Owakati")
def tokenize(text):
return tagger.parse(text).strip().split()
# 両方とも訓練テキストだけから語彙とIDFを学習する
for label, ngram in [("unigram", (1, 1)), ("1-2gram", (1, 2))]:
vectorizer = TfidfVectorizer(tokenizer=tokenize, token_pattern=None,
ngram_range=ngram)
X_train = vectorizer.fit_transform(train_texts)
X_test = vectorizer.transform(test_texts)
model = LinearSVC(max_iter=1000)
model.fit(X_train, y_train)
accuracy = accuracy_score(y_test, model.predict(X_test))
print(f"{label}: 語彙数 {X_train.shape[1]}, 正解率 {accuracy:.4f}")
考察:バイグラムを加えると語彙数は 845 から 3256 に増えましたが、正解率は 0.7500 から 0.7250 に下がりました。単語の並びを扱えても、精度が上がるとは限りません。設定を選ぶときは訓練データ内の交差検証を使い、語彙と IDF も各分割の訓練側だけで学習します。
演習 13-3: 自作レビューの判定¶
import pandas as pd
import MeCab
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
# データの準備と SVM の学習(演習13-1と同じ)
df = pd.read_csv("movie_reviews.csv")
tagger = MeCab.Tagger("-Owakati")
def tokenize(text):
return tagger.parse(text).strip().split()
train_texts, test_texts, y_train, y_test = train_test_split(
df["text"].values, df["label"].values, test_size=0.2, random_state=42
)
vectorizer = TfidfVectorizer(tokenizer=tokenize, token_pattern=None)
X_train = vectorizer.fit_transform(train_texts)
X_test = vectorizer.transform(test_texts)
model_svm = LinearSVC(max_iter=1000)
model_svm.fit(X_train, y_train)
# 自作レビューの判定
my_reviews = [
"映像が美しく、最後まで夢中で観た。文句なしの傑作だと思う。",
"テンポが良くて登場人物も魅力的。友人にも勧めたい映画だった。",
"話の展開が遅くて退屈だった。時間を返してほしい。",
"期待して観たが、脚本が薄くてがっかりした。",
]
X_my = vectorizer.transform(my_reviews)
for text, pred in zip(my_reviews, model_svm.predict(X_my)):
print(f"{pred}: {text}")
positive: 映像が美しく、最後まで夢中で観た。文句なしの傑作だと思う。
positive: テンポが良くて登場人物も魅力的。友人にも勧めたい映画だった。
negative: 話の展開が遅くて退屈だった。時間を返してほしい。
negative: 期待して観たが、脚本が薄くてがっかりした。
考察:4文とも意図どおりのラベルになりました。ただし、モデルは positive か negative を必ず返すので、中立的な文にもどちらかが付きます。unigram の TF-IDF は語順を表さないため、否定や文脈に左右される文は誤判定しやすくなります。4文だけで、未知のレビューへの性能は判断できません。
演習 13-4: 文の類似度と類似文検索¶
まず、3つの文の類似度を計算します。
from sentence_transformers import SentenceTransformer, util
# モデルの読み込み
model_st = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
sentences = [
"この映画は面白かった",
"この作品はとても楽しめた",
"今日は雨が降っています",
]
embeddings = model_st.encode(sentences)
sim_12 = util.cos_sim(embeddings[0], embeddings[1]).item()
sim_13 = util.cos_sim(embeddings[0], embeddings[2]).item()
print(f"1文目と2文目の類似度: {sim_12:.4f}")
print(f"1文目と3文目の類似度: {sim_13:.4f}")
続いて、類似文検索です。クエリと候補文をベクトル化し、類似度が最も高い候補文を出力します。
# 類似文検索
query = "スマートフォンの新製品が発表された"
candidates = [
"新しいスマホが登場して話題になっている",
"今日の夕食はカレーライスにした",
"台風が接近し、明日は大雨の予報だ",
"最新の携帯電話が公開され注目を集めた",
]
query_emb = model_st.encode([query])
cand_emb = model_st.encode(candidates)
# クエリと各候補の類似度(1×4 の行列の先頭行)
sims = util.cos_sim(query_emb, cand_emb)[0]
for text, sim in zip(candidates, sims):
print(f"{sim.item():.4f}: {text}")
best = int(sims.argmax())
print(f"\n最も近い文: {candidates[best]}")
0.8258: 新しいスマホが登場して話題になっている
0.0226: 今日の夕食はカレーライスにした
0.0774: 台風が接近し、明日は大雨の予報だ
0.7206: 最新の携帯電話が公開され注目を集めた
最も近い文: 新しいスマホが登場して話題になっている
考察:「この映画は面白かった」と「この作品はとても楽しめた」は、共通の単語が「この」「は」「た」くらいなのに、類似度は 0.6442 と高くなりました。「映画」と「作品」、「面白かった」と「楽しめた」の意味の近さを Embedding が捉えているためです。無関係な雨の文との類似度は 0.0980 でした。単語の一致を数える TF-IDF では、1文目と2文目の類似度は低くなります。類似文検索でも、「スマホ」「携帯電話」と書き方の違う2文が上位(0.8258 と 0.7206)に来ています。
演習 13-5: TF-IDF と Embedding の精度比較¶
import pandas as pd
import MeCab
from sentence_transformers import SentenceTransformer
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# 1. データ読み込み(ラベルは文字列のまま使う)
df = pd.read_csv("emails.csv")
# 2. 先に 1 回だけ訓練・テストに分割(同じ行を両手法で使う)
train_df, test_df = train_test_split(
df, test_size=0.2, random_state=42, stratify=df["label"]
)
# 3. TF-IDF + ロジスティック回帰
tagger = MeCab.Tagger("-Owakati")
def tokenize(text):
return tagger.parse(text).strip().split()
vectorizer = TfidfVectorizer(tokenizer=tokenize, token_pattern=None)
X_train_tfidf = vectorizer.fit_transform(train_df["text"])
X_test_tfidf = vectorizer.transform(test_df["text"])
model_tfidf = LogisticRegression(max_iter=1000)
model_tfidf.fit(X_train_tfidf, train_df["label"])
acc_tfidf = accuracy_score(test_df["label"], model_tfidf.predict(X_test_tfidf))
# 4. Sentence Transformers の Embedding + ロジスティック回帰
model_st = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
X_train_emb = model_st.encode(train_df["text"].tolist())
X_test_emb = model_st.encode(test_df["text"].tolist())
model_emb = LogisticRegression(max_iter=1000)
model_emb.fit(X_train_emb, train_df["label"])
acc_emb = accuracy_score(test_df["label"], model_emb.predict(X_test_emb))
# 5. 2 つの精度を並べて表示
print(f"TF-IDF + ロジスティック回帰: 正解率 {acc_tfidf:.4f}")
print(f"Embedding + ロジスティック回帰: 正解率 {acc_emb:.4f}")
考察:分割を先に1回だけ行い、同じ行をそれぞれの方法でベクトル化しています。分類器も分割も同じなので、差はベクトル化の違いだけから生まれます。結果は TF-IDF が 0.8500、Embedding が 0.8333 で、テスト60件のうち1件の差です。このメールには「お世話になっております」(ビジネス)、「久しぶり」(プライベート)、「当選」「今すぐ」(スパム)のように、クラスごとに決まった言い回しがあります。そのため、単語の出現だけを見る TF-IDF でも十分に分けられます。13.5.3 項の映画レビューでは、Embedding で正解率が大きく上がりました。言い換えが多いタスクほど Embedding が有利になりやすいです。
発展課題: アンカー文による学習なし感情判定¶
from sentence_transformers import SentenceTransformer, util
model_st = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
# ポジ・ネガを代表するアンカー文
pos_anchor = "素晴らしい映画だった"
neg_anchor = "ひどい映画だった"
# 判定したい文
tests = [
"期待以上の出来で本当に感動した",
"時間の無駄だった。二度と見たくない",
"話の展開が退屈で全く楽しめなかった",
"映像が美しく、心から楽しめる作品だった",
]
# 1. アンカー文とテスト文をまとめてベクトル化する
embeddings = model_st.encode([pos_anchor, neg_anchor] + tests)
anchor_emb = embeddings[:2]
test_emb = embeddings[2:]
# 2. 各テスト文とアンカー文の類似度(4×2 の行列)
sims = util.cos_sim(test_emb, anchor_emb)
# 3. 近い方のラベルを表示する
for text, sim in zip(tests, sims):
pos_sim, neg_sim = sim[0].item(), sim[1].item()
label = "positive" if pos_sim > neg_sim else "negative"
print(f"{label} (pos={pos_sim:.3f}, neg={neg_sim:.3f}): {text}")
positive (pos=0.261, neg=0.175): 期待以上の出来で本当に感動した
negative (pos=0.183, neg=0.348): 時間の無駄だった。二度と見たくない
negative (pos=0.303, neg=0.479): 話の展開が退屈で全く楽しめなかった
positive (pos=0.683, neg=0.232): 映像が美しく、心から楽しめる作品だった
考察:ラベル付きデータで学習していないのに、4文とも妥当な判定になりました。Embedding では意味の近い文どうしが近くに置かれるので、どちらのアンカー文に近いかを測るだけで分類できます。これはゼロショット分類と呼ばれる方法の簡単な形です。ただし、類似度はアンカー文の言い回しで変わります。1文目は pos=0.261、neg=0.175 と、どちらとの類似度も低く差も小さめです。皮肉や中立的な文では差がさらに小さくなり、判定が不安定になります。アンカー文を書き換えて、結果がどう変わるか試してみてください。