Skip to content

13. テキストデータを用いた機械学習

前章では、MeCabによる形態素解析、BoW・TF-IDFによるベクトル化、コサイン類似度を使った検索を扱いました。本章では、これらを文書分類へ応用した後、Word2Vec、Doc2Vec、Transformer、BERTへ進みます。最後にSentence Transformersで文をEmbeddingへ変換し、類似文検索と文書分類に利用します。

13.1 文書分類

文書分類(Document Classification)とは、与えられたテキストがどのカテゴリに属するかを自動的に判定するタスクです。本節では、テキストのベクトル化と機械学習分類器を組み合わせた文書分類の一連のパイプラインを実践します。

13.1.1 多項ナイーブベイズによるテキスト分類

テキスト分類で最もよく使われるのが 多項ナイーブベイズ(Multinomial Naive Bayes) です。単語の出現回数を「多項分布のサンプル」と見なし、各クラスでの単語頻度から確率を計算します。ナイーブベイズの理論的な説明は7.2 節を参照してください。

scikit-learn では sklearn.naive_bayes.MultinomialNB クラスを使います。

import MeCab
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB

# サンプル文書とラベル
documents = [
    'AI は機械学習の一分野です',
    '機械学習は深層学習の基礎です',
    'AI は楽しいです',
    '深層学習は楽しいです',
]
labels = ['基礎', '基礎', '応用', '応用']

# 形態素解析(分かち書き)
tagger = MeCab.Tagger("-Owakati")
docs_wakati = [tagger.parse(d).strip() for d in documents]

# BoW でベクトル化
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs_wakati)

# 多項ナイーブベイズで学習
model = MultinomialNB()
model.fit(X, labels)

# 新しい文書を予測
test = ['AI は深層学習の応用です']
test_wakati = [tagger.parse(t).strip() for t in test]
X_test = vectorizer.transform(test_wakati)
print(f'予測: {model.predict(X_test)[0]}')
print(f'クラス確率: {dict(zip(model.classes_, model.predict_proba(X_test)[0]))}')

13.1.2 問題設定

感情分析(Sentiment Analysis)のタスクを実装することを考えます。具体的には、コメディ映画のレビュー文がポジティブであるかネガティブであるかの分類を行います。

処理の流れ:

  1. CSV ファイルからデータを読み込み
  2. MeCab で形態素解析(分かち書き)
  3. BoW によるベクトル化
  4. ロジスティック回帰による分類
  5. 予測結果の評価

13.1.3 データの準備

演習では、以下のような日本語のコメディ映画レビュー(positive, negative ともに 100 件)を含む movie_reviews.csv ファイルを使用します。本ファイルは、CHIKUWA Editor の sample フォルダ内にも置いてあります。

movie_reviews.csv
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())
Output
データの確認:
      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

13.1.4 前処理から分類まで

前処理から学習、分類、評価までの一連の流れは、以下のようなコードになります。

# 注:以下は、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']))
Output
語彙数: 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

考えてみよう

混同行列を見て、予測結果を詳しく確認してみましょう。また、誤分類されたテキストがどのようなものか調べ、なぜ誤分類されたのか考えてみましょう。

13.1.5 新しいレビューの予測

新しいレビューに対し、学習済みモデル 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}%)")
Output
レビュー: こんなに面白いコメディ映画は生まれて初めて。
予測: ポジティブ (確信度: 79.0%)

Playground:文書分類

文書分類 の Playground では、20 件の映画レビューで学習したナイーブベイズ分類器に文章を入力すると、即座に判定されます。単語が「どちらのクラスに効いたか」で色分けされる(緑=ポジティブ寄り・赤=ネガティブ寄り、濃いほど強い証拠)ので、判定の根拠が単語単位で見えます。単語を並べ替えても結果が変わらないこと、学習データにない未知語の扱いも確かめてみましょう。

課題 13: メール 3 クラス分類

課題 13

メールのテキストをまとめた emails.csv を使って、メールを 「ビジネスメール(business)」「プライベートメール(private)」「スパムメール(spam)」 の 3 クラスに分類するモデルを構築し、テスト精度・混同行列・考察をレポートにまとめてください。データは各クラス 100 件ずつ、計 300 件です。

※ 加点要素:複数手法の比較、前処理の工夫(品詞絞り込み・ストップワード除去など)、ベクトル化パラメータの調整、誤分類分析 など。

提出物

  • Word ファイルで作成し、PDF 形式で出力して提出(学生番号_氏名_13.pdf)
  • 問題文・ソースコード・結果・考察を PDF 内に含めること(参考:機械学習_レポートサンプル.docx)

提出期限:2026 年 7 月 12 日(日)23:59

提出先:manaba

13.2 感情分析

感情分析(Sentiment Analysis)とは、テキストに含まれる感情や意見の極性(ポジティブ/ネガティブ)を自動的に判定するタスクです。商品レビューの評判集計や SNS 投稿のモニタリング、顧客の声の分析などに活用されています。

感情分析は 文書分類の一種 です。実は、前節で構築した映画レビューの分類器は、レビューを肯定的(positive)・否定的(negative)に振り分けるものであり、そのまま感情分析システムとして機能しています。たとえば、次のようなテキストはポジティブ/ネガティブに分類されます。

  • 「この商品はとても使いやすくて満足しています」→ ポジティブ
  • 「品質が悪くてすぐに壊れました」→ ネガティブ

つまり、前節の「MeCab による分かち書き → BoW / TF-IDF ベクトル化 → 分類器の学習」というパイプラインは、ラベルが感情の極性でありさえすれば、映画レビューに限らず商品レビューや SNS 投稿などにもそのまま適用でき、感情分析モデルとして使えます。

ただし、BoW や TF-IDF は単語の出現パターンに基づく手法であるため、皮肉や文脈に依存する表現の正確な判定は困難です。たとえば「(ひどい出来なのに)すごいですね」のような皮肉は、肯定的な単語を含むためポジティブと誤判定されがちです。より高度な感情分析には、後述する Transformer ベースのモデルが有効です。

13.3 Word2Vec、Doc2Vec

前節までは BoW や TF-IDF といったカウントベースのベクトル化手法を用いてきました。これらの手法は単語の出現パターンに基づくため、単語間の意味的な関係を捉えることができません。たとえば、「素晴らしい」と「最高」は BoW では完全に別の特徴量として扱われますが、実際には非常に近い意味を持ちます。本節では、単語や文書の意味を反映したベクトル表現を学習する手法である Word2Vec と Doc2Vec を学びます。

13.3.1 BoW / TF-IDF の限界

単語の意味を考慮できない

第 12 章で学んだ Bag-of-Words や TF-IDF は、シンプルで実用的な手法ですが、いくつかの限界があります。その一つは、単語の意味を考慮できない点です。

例 1: 類似した意味の単語

文 A: 「犬が走っている」
文 B: 「猫が走っている」
文 C: 「車が走っている」

BoW / TF-IDF では、「犬」「猫」「車」は全く別の単語として扱われます。しかし、意味的には「犬」と「猫」は「動物」として近い関係にあります。

例 2: 同義語・類義語

文 A: 「この映画は面白い」
文 B: 「この映画は楽しい」

「面白い」と「楽しい」は似た意味ですが、BoW / TF-IDF では全く異なる単語として扱われ、類似度が低くなってしまいます。

文脈を考慮できない

第 12 章で触れた例を思い出してみましょう。

文: 「黒い目の大きな猫」

この文で「大きい」のは「目」でしょうか、「猫」でしょうか?BoW / TF-IDF では、このような文脈による意味の違いを区別できません。

スパース(疎)なベクトル

BoW / TF-IDF では、語彙数が増えるとベクトルの次元数も増加します。しかし、各文書には語彙の一部しか出現しないため、ほとんどの要素が 0 になります(スパースなベクトル)。

# 例:語彙数が 10,000 の場合
# 文章「私は猫が好きです」のベクトル
# → [0, 0, 0, ..., 1, ..., 0, 1, ..., 0]  # ほとんどが 0

これらの問題を解決するために、単語の意味を考慮したベクトル化手法 が開発されました。

13.3.2 Embedding(埋め込み)

Embedding(埋め込み) とは、単語やテキストを固定長の密なベクトルに変換する手法です。「埋め込み」という名前は、高次元の離散的なデータ(単語)を、低次元の連続的なベクトル空間に「埋め込む」ことに由来します。

BoW / TF-IDF との違い:

特徴 BoW / TF-IDF Embedding
ベクトルの種類 スパース(疎) 密(Dense)
次元数 語彙数に依存(数千〜数万) 固定(数百程度)
意味の表現 できない できる
類義語の扱い 別々のベクトル 近いベクトル

Embedding では、意味的に近い単語やテキストが、ベクトル空間上でも近くに配置されます。

たとえば、Multimodal Embedding Models - Weaviate の Figure 1 のように、「犬」と「猫」は近く、「バナナ」と「リンゴ」も近い位置に配置されます。これにより、単語の意味的な関係を数値で扱えるようになります。

13.3.3 Word2Vec とは

Word2Vec は、単語の Embedding を学習する手法です。2013 年に Google の研究者によって提案されました。

特徴

  • 似た文脈で使われる単語は、似たベクトルになる
  • 固定長の密なベクトル(例:300 次元)で表現
  • 単語の意味の演算ができる

有名な例

\[ \text{king} - \text{man} + \text{woman} \approx \text{queen} \]

「王」から「男性」の意味を引き、「女性」の意味を足すと、「女王」に近いベクトルになります。

13.3.4 Word2Vec の仕組み(概要)

Word2Vec は、大量のテキストデータから「単語の周辺にどんな単語が出現するか」を学習します。

「私 は 猫 が 好き です」
      ↓
「猫」の周辺には「私」「は」「が」「好き」が出現

同じような文脈で使われる単語(例:「犬」と「猫」)は、似たベクトルになります。これは 「分布仮説」(似た文脈に出現する単語は似た意味を持つ)に基づいています。

Word2Vec には、以下の 2 つの学習アルゴリズムがあります。

  • CBOW(continuous bag-of-words):周囲の単語(文脈)から中心の単語を予測します。学習が高速で、頻出語の表現に優れます。
  • Skip-gram:中心の単語から周囲の単語(文脈)を予測します。低頻度の単語や小規模なデータセットに強いです。

13.3.5 Gensim を用いた Word2Vec の学習

Word2Vec の実装には、Python の Gensim ライブラリの Word2Vec クラスを使用します(pip install gensim でインストールできます)。MeCab で分かち書きしたテキストデータから Word2Vec モデルを学習し、類似する単語を検索してみましょう。

import MeCab
from gensim.models import Word2Vec

# サンプルの文書コーパス
corpus = [
    "機械学習はデータからパターンを学習する技術です",
    "深層学習はニューラルネットワークを用いた機械学習の手法です",
    "自然言語処理はテキストデータを分析する技術です",
    "画像認識は写真や動画から物体を検出する技術です",
    "機械学習を使ってテキストを分類することができます",
    "自然言語処理は人工知能の重要な分野です",
    "深層学習により画像認識の精度が大幅に向上しました",
    "テキストデータの分析には自然言語処理の技術が必要です",
]

# MeCabで分かち書き(単語のリストのリストを作成)
tagger = MeCab.Tagger("-Owakati")
tokenized_corpus = [tagger.parse(doc).strip().split() for doc in corpus]

# Word2Vecモデルの学習
model_w2v = Word2Vec(
    sentences=tokenized_corpus,
    vector_size=50,    # ベクトルの次元数
    window=3,          # 文脈の窓幅
    min_count=1,       # 最低出現回数
    epochs=100,        # 学習回数
    sg=1,              # 1: Skip-gram, 0: CBOW
)

# 「学習」に類似する単語を検索
similar_words = model_w2v.wv.most_similar("学習", topn=5)
print("「学習」に類似する単語:")
for word, score in similar_words:
    print(f"  {word}: {score:.4f}")
Output
「学習」に類似する単語:
  機械: 0.9234
  深層: 0.8876
  技術: 0.7532
  処理: 0.7018
  データ: 0.6845

vector_size パラメータでベクトルの次元数、window パラメータで文脈として考慮する窓幅を指定します。sg パラメータを 1 に指定すると、Skip-gram アルゴリズムを使います。小規模なコーパスでの学習ですが、「学習」に対して「機械」「深層」「技術」など、関連性の高い単語が上位に表示されています。より大規模なコーパスを用いることで、精度はさらに向上します。

Playground:単語の分散表現

単語の分散表現 の Playground では、50 文の小さな文章から Word2Vec(Skip-gram)が学習した単語ベクトルを散布図で見られます。「同じような文脈に出る単語は似た意味を持つ」という発想だけで動物・食べ物・乗り物・天気が勝手に分かれること、単語をクリックするとコサイン類似度が近い順に並ぶことを確認できます。「車 − 乗る + 食べる」のようなベクトルの足し算・引き算や、学習エポックを動かして分散表現が育っていく途中経過も追えます。

13.3.6 Word2Vec の限界

Word2Vec にも限界があります。

  • 単語単位: テキスト全体の意味を直接表現できない
  • 文脈を考慮しない: 同じ単語は常に同じベクトル(多義語の区別ができない)
  • 未知語に対応できない: 学習データにない単語はベクトル化できない

13.3.7 Doc2Vec の概要と文書ベクトルの学習

Word2Vec は単語レベルのベクトルしか生成できないため、文や文書全体のベクトルを直接得ることができないという限界があります。Doc2Vec は、Word2Vec を拡張し、文書全体を 1 つの固定長ベクトルとして表現する手法です。

Doc2Vec では、各文書に一意の文書 IDを割り当て、単語ベクトルと同時に文書ベクトルも学習します。これにより、文書間の類似度計算や文書分類に直接利用できるベクトルが得られます。

Gensim の Doc2Vec クラスを使って、先ほどの tokenized_corpus から文書ベクトルを学習し、文書間の類似度を確認してみましょう。

from gensim.models.doc2vec import Doc2Vec, TaggedDocument

# TaggedDocumentの作成(文書IDと分かち書きしたテキスト)
tagged_docs = [
    TaggedDocument(words=tokens, tags=[f"doc_{i}"])
    for i, tokens in enumerate(tokenized_corpus)
]

# Doc2Vecモデルの学習
model_d2v = Doc2Vec(
    documents=tagged_docs,
    vector_size=50,    # ベクトルの次元数
    window=3,          # 文脈の窓幅
    min_count=1,       # 最低出現回数
    epochs=100,        # 学習回数
    dm=1,              # 1: PV-DM(分散記憶モデル), 0: PV-DBOW
)

# 文書間の類似度
similar_docs = model_d2v.dv.most_similar("doc_0", topn=3)
print(f"元の文書: {corpus[0]}\n")
print("類似する文書:")
for doc_id, score in similar_docs:
    idx = int(doc_id.split("_")[1])
    print(f"  {doc_id} (類似度: {score:.4f}): {corpus[idx]}")
Output
元の文書: 機械学習はデータからパターンを学習する技術です

類似する文書:
  doc_1 (類似度: 0.8234): 深層学習はニューラルネットワークを用いた機械学習の手法です
  doc_4 (類似度: 0.7156): 機械学習を使ってテキストを分類することができます
  doc_6 (類似度: 0.6523): 深層学習により画像認識の精度が大幅に向上しました

なお、学習済みモデルの infer_vector() メソッドを使えば、学習に含まれていない新しい文書のベクトルを推論することもできます。

Doc2Vec により、文書全体の意味を捉えたベクトルが得られるため、文書分類や類似文書検索に直接利用できます。BoW と TF-IDF に比べて、意味的な類似性をある程度反映したベクトル表現が得られる点が利点ですが、大量のコーパスが必要であり、学習にも時間がかかるという欠点があります。

13.4 Transformer の紹介

Word2Vec や Doc2Vec は、単語や文書の分散表現を学習できる強力な手法でしたが、いくつかの限界も存在します。特に、1 つの単語に対して 1 つの固定されたベクトルしか持てないため、文脈によって意味が変わる多義語を適切に表現できません。本節では、これらの限界を克服した Transformer アーキテクチャと、それを基盤とする BERT について紹介します。

13.4.1 Transformer とは

Transformer は、2017 年に Google が発表した深層学習モデルのアーキテクチャです。現在の自然言語処理の基盤技術となっています。

Transformer の特徴:

  • Attention(注意機構): 文中の重要な部分に「注意」を向けて処理
  • 並列処理: 文全体を一度に処理できるため高速
  • 長距離依存: 文の離れた位置にある単語同士の関係も捉えられる
「私 は 昨日 買った 本 を 読んでいる」
     ↓ Attention
「読んでいる」は「本」に注意を向ける
「買った」も「本」に注意を向ける

Transformer を支える深層学習は第 14 章で

Transformer は、ニューラルネットワーク(深層学習)を基盤とするアーキテクチャです。ニューラルネットワークと深層学習のしくみの詳細は、第 14 章で学びます。

13.4.2 BERT とは

BERT(Bidirectional Encoder Representations from Transformers) は、2018 年に Google が発表した言語モデルです。Transformer をベースに、大量のテキストデータで事前学習されています。

BERT の特徴

  • 双方向: 文の前後両方の文脈を考慮
  • 文脈依存: 同じ単語でも文脈によって異なるベクトルになる
  • 事前学習: 大量のデータで学習済みなので、少ないデータでも高精度

Word2Vec との違い

特徴 Word2Vec BERT
単位 単語 単語・文
文脈 考慮しない 考慮する
多義語 区別できない 区別できる
モデルサイズ 小さい 大きい

13.4.3 事前学習と転移学習

BERT などのモデルは、以下の 2 段階で利用されます。

  1. 事前学習(Pre-training): 大量のテキストデータで言語の一般的な知識を学習
  2. ファインチューニング(Fine-tuning): 特定のタスク(感情分析など)用に追加学習

私たちは、すでに事前学習されたモデルを使うことで、少ないデータでも高精度な分類が可能になります。これを 転移学習 と呼びます。

13.5 Sentence Transformers

Sentence Transformers は、BERTなどのTransformerモデルを使って、文や段落を固定長のEmbeddingへ変換するライブラリです。単語の出現回数を表すBoWやTF-IDFとは異なり、文全体の意味を反映したベクトルを得られます。

13.5.1 インストールと基本操作

この節では、SentenceTransformer クラスで、日本語を含む多言語に対応した paraphrase-multilingual-MiniLM-L12-v2 を読み込みます。文のベクトル化には encode() メソッドを使います。初回実行時にはモデルがダウンロードされます。

!pip install sentence-transformers
from sentence_transformers import SentenceTransformer

model_st = SentenceTransformer(
    "paraphrase-multilingual-MiniLM-L12-v2"
)

sentences = [
    "私は機械学習が好きです",
    "私は深層学習を勉強しています",
    "今日は天気が良いです",
]

embeddings = model_st.encode(sentences)

print(embeddings.shape)
print(embeddings[0, :5])

このモデルでは、各文が384次元の密なベクトルに変換されます。ベクトルの各要素を個別に解釈するのではなく、ベクトル間の距離や類似度を使って文同士を比較します。

13.5.2 意味の近い文を探す

sentence_transformers.util.cos_sim() 関数を使うと、Embedding間のコサイン類似度を計算できます。

from sentence_transformers import util

similarity = util.cos_sim(embeddings, embeddings)

for i, sentence in enumerate(sentences):
    print(f"\n{i}: {sentence}")
    for j, score in enumerate(similarity[i]):
        print(f"  文{j}との類似度: {score.item():.3f}")

同じ学習分野について述べた最初の2文は、天気について述べた3文目よりも高い類似度になることを確認できます。具体的な値は、ライブラリやモデルのバージョンによって変わる場合があります。

クエリに最も近い候補文を探す場合も、同じ手順を使います。

query = "新しいスマートフォンが発表された"
candidates = [
    "最新の携帯電話が公開され、注目を集めた",
    "今日の夕食はカレーライスだった",
    "台風の接近で明日は大雨になる見込みだ",
]

query_embedding = model_st.encode(query)
candidate_embeddings = model_st.encode(candidates)
scores = util.cos_sim(query_embedding, candidate_embeddings)[0]
best_index = int(scores.argmax())

print(candidates[best_index])
print(f"類似度: {scores[best_index].item():.3f}")

13.5.3 Embeddingを使った文書分類

Embeddingは、ロジスティック回帰などの分類器へ入力する特徴量としても利用できます。次の例では、映画レビューをSentence Transformersでベクトル化してから感情を分類します。

import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

df = pd.read_csv("movie_reviews.csv")

X = model_st.encode(df["text"].tolist(), show_progress_bar=True)
y = df["label"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

classifier = LogisticRegression(max_iter=1000)
classifier.fit(X_train, y_train)
y_pred = classifier.predict(X_test)

print(f"正解率: {accuracy_score(y_test, y_pred):.3f}")

Sentence Transformersは文の意味を反映した特徴量を提供しますが、常にTF-IDFより高精度になるとは限りません。データ量、文章の種類、使用モデル、評価方法を揃えて比較する必要があります。

実行環境とキャッシュ

Sentence TransformersはPyTorchと学習済みモデルを使用するため、初回のインストールとダウンロードには時間がかかります。同じ文章を繰り返し使う場合は、numpy.save() 関数などでEmbeddingを保存すると再計算を避けられます。

補足:授業用ローカルLLMサーバを使う場合

この補足は、授業中に教員用サーバが提供される場合だけを対象とします。一般の学習環境では13.5節のSentence Transformersを使用してください。

ローカルLLMサーバを使う理由

授業用PCの容量や実行時間に制約がある場合は、各自の環境へPyTorchと学習済みモデルを導入する代わりに、教員が用意したサーバでEmbeddingを計算できます。

この補足では、教員が運用するLM StudioサーバへHTTPでアクセスする構成を示します。学生側では requests を使用し、モデル本体はサーバ側で実行します。この構成は授業中にサーバが利用できる場合だけ使います。通常の学習経路では13.5節のSentence Transformersを使用してください。

授業用サーバを使う場合の違い

  • ローカルインストール不要(PyTorch などの環境構築が不要)
  • 学生側では外部クラウドAPIのキーを用意しない
  • 教員 PC の GPU を学生全員で共有
  • 後の章でも同じサーバを「チャットモデル」用に再利用する
  • API が OpenAI 互換なので、OpenAI などの外部 API(第14章)とほぼ同じ書き方で呼び出せる

サーバの接続情報

項目 内容
サーバ URL 授業でアナウンスされる URL(https://~.trycloudflare.com の形式)
Embedding モデル text-embedding-bge-m3 (多言語対応・1024 次元)
API エンドポイント POST /v1/embeddings (OpenAI 互換)

サーバ URL は毎回変わります

教員サーバは Cloudflare トンネル経由で公開しており、URL は授業のたびに変わります。毎回の授業でアナウンスされる https://~.trycloudflare.com を、コード中の LLM_URL に設定してください(末尾にポート番号 :1234 は付けません)。

サーバが使えない場合は自分の PC で LM Studio を起動

講義期間外などで上記サーバが停止している場合や、自宅環境で実験したい場合は、LM Studio を自分の PC にインストールすれば同じことができます。

  1. LM Studio を起動し、検索から bge-m3(Embedding モデル)をダウンロード
  2. 左側の「Developer」タブでサーバを起動(デフォルトで http://localhost:1234)
  3. ダウンロードした Embedding モデルをロード

その上でコード中の LLM_URL を http://localhost:1234 に変更してください。

ベクトル化の基本

Python 標準的な requests ライブラリで LM Studio の Embedding API を呼び出します。複数文をまとめてベクトル化したいので、ヘルパー関数 encode() を用意しておくと以降のコードがすっきりします。

import requests
import numpy as np

LLM_URL = "https://xxxxxxxx.trycloudflare.com"  # 授業でアナウンスされる URL に置き換える
EMBED_MODEL = "text-embedding-bge-m3"

# テキストを Embedding ベクトルに変換
def encode(texts):
    if isinstance(texts, str):
        texts = [texts]
    response = requests.post(
        f"{LLM_URL}/v1/embeddings",
        json={"model": EMBED_MODEL, "input": texts}
    )
    data = response.json()["data"]
    return np.array([d["embedding"] for d in data])

# 文のリスト
sentences = [
    "私は機械学習が好きです",
    "私は深層学習が好きです",
    "今日は天気が良いです"
]

# 文をベクトルに変換
embeddings = encode(sentences)

print(f"ベクトルの形状: {embeddings.shape}")
print(f"1文目のベクトル(最初の10要素): {embeddings[0][:10]}")
Output
ベクトルの形状: (3, 1024)
1文目のベクトル(最初の10要素): [-0.013  0.024 -0.045  0.011  0.038 -0.019  0.007 -0.022  0.041 -0.005]

各文が 1024 次元の密なベクトル に変換されました。

LM Studio の Embedding API について

LM Studio の /v1/embeddings は OpenAI 互換で、input パラメータに文字列のリストを渡せば複数文を 1 リクエストでまとめてベクトル化できます。レスポンスの data 配列に、入力と同じ順序でベクトルが入って返ってきます。

使用するモデル

本講義では Embedding モデルとして bge-m3 を使用します。

項目 内容
モデル名 bge-m3(LM Studio 上の ID は text-embedding-bge-m3)
開発元 BAAI(北京智源人工知能研究院)
対応言語 100 以上の言語(日本語を含む)
ベクトル次元 1024 次元
特徴 多言語対応・高精度・長文(最大 8192 トークン)対応

このモデルは Hugging Face で公開されており、LM Studio からダウンロードして利用します。

他の Embedding モデル

LM Studio では他にもさまざまな Embedding モデルが利用できます。サーバ側でモデルがロードされていれば、EMBED_MODEL の名前を変えるだけで切り替え可能です(/v1/models で利用可能なモデル一覧を確認できます)。

モデル 言語 次元 特徴
text-embedding-bge-m3 多言語 1024 日本語対応・本講義で使用
text-embedding-nomic-embed-text-v1.5 英語中心 768 軽量

コサイン類似度とは

コサイン類似度(Cosine Similarity) は、2 つのベクトルがなす角度のコサイン値を使って類似度を測る方法です。

\[ \text{cosine similarity}(\mathbf{A}, \mathbf{B}) = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| \times |\mathbf{B}|} \]

コサイン類似度の特徴:

  • 値の範囲: -1 から 1
  • 1 に近い: 非常に似ている
  • 0 に近い: 似ていない
  • -1 に近い: 正反対の意味

TF-IDF ベクトルによる類似文書検索は第 12 章で

コサイン類似度を TF-IDF ベクトルに適用した類似文書検索は第 12 章で扱っています。ここでは同じ考え方を Embedding ベクトルに適用し、意味ベース の類似文書検索を行います。

コサイン類似度による類似文書検索

上の「ベクトル化の基本」で作った encode() 関数を使い、文間の類似度を計算します。

コサイン類似度を計算するには、scikit-learn の cosine_similarity() 関数を使用します。ベクトルの配列を受け取り、全ペアの類似度を含む行列を返します。

import requests
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

LLM_URL = "https://xxxxxxxx.trycloudflare.com"  # 授業でアナウンスされる URL に置き換える
EMBED_MODEL = "text-embedding-bge-m3"

def encode(texts):
    if isinstance(texts, str):
        texts = [texts]
    response = requests.post(
        f"{LLM_URL}/v1/embeddings",
        json={"model": EMBED_MODEL, "input": texts}
    )
    data = response.json()["data"]
    return np.array([d["embedding"] for d in data])

# 文のリスト
sentences = [
    "私は機械学習が好きです",
    "私は深層学習が好きです",
    "今日は天気が良いです",
    "機械学習と深層学習を勉強しています"
]

# ベクトル化
embeddings = encode(sentences)

# コサイン類似度を計算
similarity_matrix = cosine_similarity(embeddings)

print("類似度行列:")
for i, sent in enumerate(sentences):
    print(f"\n文{i}: {sent}")
    for j, sim in enumerate(similarity_matrix[i]):
        print(f"  vs 文{j}: {sim:.3f}")
Output
類似度行列:

文0: 私は機械学習が好きです
  vs 文0: 1.000
  vs 文1: 0.860
  vs 文2: 0.606
  vs 文3: 0.756

文1: 私は深層学習が好きです
  vs 文0: 0.860
  vs 文1: 1.000
  vs 文2: 0.521
  vs 文3: 0.795

文2: 今日は天気が良いです
  vs 文0: 0.606
  vs 文1: 0.521
  vs 文2: 1.000
  vs 文3: 0.523

文3: 機械学習と深層学習を勉強しています
  vs 文0: 0.756
  vs 文1: 0.795
  vs 文2: 0.523
  vs 文3: 1.000

「機械学習」「深層学習」を含む 3 つの文(文 0・1・3)は互いに 0.75〜0.86 と高い類似度を示し、トピックが異なる文 2(天気)とは 0.52〜0.61 程度と低めの値になっています。bge-m3 は XLM-RoBERTa をベースにした Transformer(BERT 系)のモデルで、「意味が近い文同士はベクトル空間でも近い」という性質を持っているため、コサイン類似度で意味的な近さを測れます(具体的な数値はモデルのバージョンや量子化により多少変動します)。

Embeddingによる文書分類の流れ

Embedding を特徴量に使った文書分類は、以下の流れで行います。

  1. 文を Embedding でベクトル化(LM Studio 経由)
  2. ベクトルを特徴量として分類器(ロジスティック回帰など)に入力
  3. 分類モデルを学習・予測

BoW や TF-IDF と違って、「意味的に近い文」が特徴量空間でも近くに配置される ため、少ない訓練データでも高い精度が出やすいのが Embedding ベースの分類の強みです。

感情分析の実装

映画レビューデータ movie_reviews.csv を使って、ポジティブ/ネガティブの 2 クラス分類を実装します。

import requests
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

LLM_URL = "https://xxxxxxxx.trycloudflare.com"  # 授業でアナウンスされる URL に置き換える
EMBED_MODEL = "text-embedding-bge-m3"

def encode(texts):
    if isinstance(texts, str):
        texts = [texts]
    response = requests.post(
        f"{LLM_URL}/v1/embeddings",
        json={"model": EMBED_MODEL, "input": texts}
    )
    data = response.json()["data"]
    return np.array([d["embedding"] for d in data])

# データの読み込み
df = pd.read_csv('movie_reviews.csv')

print(f"データ数: {len(df)}")
print(df['label'].value_counts())

# 文をまとめてベクトル化(200 文を 1 リクエストで送ります)
print("\nベクトル化中...")
embeddings = encode(df['text'].tolist())

print(f"ベクトルの形状: {embeddings.shape}")

# ラベルを数値に変換
df['label_num'] = (df['label'] == 'positive').astype(int)

# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
    embeddings, df['label_num'],
    test_size=0.2,
    random_state=42
)

# ロジスティック回帰で学習
clf = LogisticRegression(random_state=42, max_iter=1000)
clf.fit(X_train, y_train)

# 予測
y_pred = clf.predict(X_test)

# 評価
accuracy = accuracy_score(y_test, y_pred)
print(f"\n精度: {accuracy:.2%}")

print("\n分類レポート:")
print(classification_report(y_test, y_pred, target_names=['Negative', 'Positive']))
Output
データ数: 200
label
positive    100
negative    100
Name: count, dtype: int64

ベクトル化中...
ベクトルの形状: (200, 1024)

精度: 100.00%

分類レポート:
              precision    recall  f1-score   support

    Negative       1.00      1.00      1.00        19
    Positive       1.00      1.00      1.00        21

    accuracy                           1.00        40
   macro avg       1.00      1.00      1.00        40
weighted avg       1.00      1.00      1.00        40

200 文のレビューを bge-m3 で 1024 次元のベクトルに変換し、その上でロジスティック回帰を回すだけで、この例ではテストデータ 40 件を全問正解(100%)できました。BoW や TF-IDF と比べて、「意味が似ているレビュー同士は似たベクトルになる」という Embedding の特性が効いています。

100% を鵜呑みにしない

ここでの 100% は「Embedding が強力」なことに加えて、「データが 200 件と小さく、テストも 40 件しかない」ことによる面が大きいです。テストが少ないと、たまたま全問正解することは珍しくありません。実務では、より多くのデータ・交差検証(cross-validation)で評価し、精度が偶然でないかを確かめます。

ベクトル化の所要時間とキャッシュ

LM Studio の /v1/embeddings は複数文をまとめて送ることができますが、200 文分の推論には GPU でも数秒かかります。実務では同じテキストを何度もベクトル化することは避けたいので、得られた embeddings を numpy.save() 関数などでキャッシュしておくと良いです。

np.save('movie_review_embeddings.npy', embeddings)
# 後で読み込む場合
embeddings = np.load('movie_review_embeddings.npy')

13.6 演習問題

演習13-4以降では、13.5節で学んだSentence Transformersを使います。

演習 13-1: TF-IDF と分類器の比較

演習 13-1

映画レビューのテキストと感情ラベル(positive / negative)が各 100 件・計 200 件入った movie_reviews.csv を使って、TF-IDF ベクトル に対する ナイーブベイズ・SVM・ロジスティック回帰 の 3 つの分類器のテスト精度を比較してください。さらに、ロジスティック回帰が学習した係数から「ポジティブ寄り」「ネガティブ寄り」の重要語を確認します。

タスク

  1. CSV を読み込み、MeCab.Tagger("-Owakati") で分かち書きする関数を定義する
  2. テキストを先に訓練 80%・テスト 20% に分割する(random_state=42)。TfidfVectorizer(tokenizer=tokenize, token_pattern=None) を訓練テキストだけで fit_transform() メソッドで学習し、テストテキストには transform() メソッドのみを使う
  3. MultinomialNB クラス・LinearSVC(max_iter=1000)・LogisticRegression(max_iter=1000) の 3 モデルで学習し、テスト正解率を比較する
  4. ロジスティック回帰の coef_ 属性を使って、係数が 最大の 10 単語(ポジティブ寄り)と 最小の 10 単語(ネガティブ寄り)を表示し、どのような単語が判定の手がかりになっているか考察する
テンプレートコード
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. データ読み込み + 分かち書き関数の定義

# 2. テキストを先に分割し、訓練側だけで TF-IDF を学習

# 3. 3 モデルで学習・正解率比較

# 4. ロジスティック回帰の係数から上位 10 / 下位 10 単語を表示・考察
解答例
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: ベクトル化の設定変更

演習 13-2

TfidfVectorizer クラスに ngram_range=(1, 2) を指定して単語 2 つの並び(バイグラム)まで特徴量に含めた場合、SVM の分類精度がどう変わるか確認してください。

タスク

  1. 演習 13-1 と同じテキストの訓練・テスト分割と分かち書き関数を使う
  2. ngram_range=(1, 2) を指定した TfidfVectorizer クラスを訓練側だけで fit_transform() メソッドで学習し、テスト側は transform() メソッドで変換する。LinearSVC(max_iter=1000) で学習する
  3. 語彙数とテスト正解率を unigram のみの場合と比較し、結果の理由を考察する
解答例
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}")
unigram: 語彙数 845, 正解率 0.7500
1-2gram: 語彙数 3256, 正解率 0.7250

考察:バイグラムを加えると語彙数は 845 から 3256 に増えましたが、正解率は 0.7500 から 0.7250 に下がりました。単語の並びを扱えても、精度が上がるとは限りません。設定を選ぶときは訓練データ内の交差検証を使い、語彙と IDF も各分割の訓練側だけで学習します。

演習 13-3: 自作レビューの判定

演習 13-3

自分で肯定的なレビューと否定的なレビューを 2 文ずつ作成し、演習 13-1 の学習済み SVM モデルで感情を判定してください。誤判定された文があれば、どのような単語が原因か考察してください。

タスク

  1. 肯定的なレビュー 2 文・否定的なレビュー 2 文を自作する
  2. 演習 13-1 の vectorizer で TF-IDF ベクトルに変換する(vectorizer.transform() メソッド)
  3. 学習済み SVM モデルで判定し、結果を表示する
  4. 誤判定された文があれば、原因となった単語を考察する
解答例
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: 文の類似度と類似文検索

演習 13-4

Sentence Transformers を使って、文の意味の近さを数値化してみましょう。前半では 3 つの文のコサイン類似度を計算し、後半では 1 つのクエリ文に対して候補文のリストの中から意味が最も近い文を探すプログラム(類似文検索)を作成します。

タスク

  1. SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") で「この映画は面白かった」「この作品はとても楽しめた」「今日は雨が降っています」の 3 文を Embedding に変換する
  2. 1 文目と 2 文目、1 文目と 3 文目のコサイン類似度(util.cos_sim() 関数)を計算し、意味の近い文のほうが高い類似度になることを確認する
  3. クエリ文と候補文をまとめて Embedding に変換し、クエリと各候補の類似度を表示して、最も類似度が高い候補文を出力する
テンプレートコード
from sentence_transformers import SentenceTransformer, util

# モデルの読み込み
model_st = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

# 1-2. 3 文を Embedding に変換し、コサイン類似度を計算する

# 3. 類似文検索
query = "スマートフォンの新製品が発表された"
candidates = [
    "新しいスマホが登場して話題になっている",
    "今日の夕食はカレーライスにした",
    "台風が接近し、明日は大雨の予報だ",
    "最新の携帯電話が公開され注目を集めた",
]
解答例

まず、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}")
1文目と2文目の類似度: 0.6442
1文目と3文目の類似度: 0.0980

続いて、類似文検索です。クエリと候補文をベクトル化し、類似度が最も高い候補文を出力します。

# 類似文検索
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 の精度比較

演習 13-5

課題 13 で使う emails.csv(ビジネス・プライベート・スパムの 3 クラス、各 100 件・計 300 件)を使って、TF-IDF と Sentence Transformers の Embedding でメール分類モデルを作り、テスト精度を比較してください。ベクトル化の方法だけを変え、分類器(ロジスティック回帰)と分割条件は揃えます。どちらが高精度か、なぜかも考えてみましょう。

タスク

  1. CSV を読み込む(ラベルは business・private・spam の 3 種類。文字列のまま使ってよい)
  2. 公平に比べるため、先に train_test_split(df, test_size=0.2, random_state=42, stratify=df['label']) で 1 回だけ訓練・テストに分割し、同じ行を両手法で使う
  3. TF-IDF 版:MeCab.Tagger("-Owakati") で分かち書き → TfidfVectorizer クラス → ロジスティック回帰で精度を求める
  4. Embedding 版:model_st.encode() メソッドでベクトル化 → ロジスティック回帰で精度を求める
  5. 2 つの精度を並べて表示し、どちらが高いか・なぜかを考察する
テンプレートコード
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_test_split で分割

# 3. TF-IDF + ロジスティック回帰で精度を求める

# 4. Sentence Transformers の Embedding + ロジスティック回帰で精度を求める

# 5. 2 つの精度を並べて表示・考察
解答例
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}")
TF-IDF    + ロジスティック回帰: 正解率 0.8500
Embedding + ロジスティック回帰: 正解率 0.8333

考察:分割を先に1回だけ行い、同じ行をそれぞれの方法でベクトル化しています。分類器も分割も同じなので、差はベクトル化の違いだけから生まれます。結果は TF-IDF が 0.8500、Embedding が 0.8333 で、テスト60件のうち1件の差です。このメールには「お世話になっております」(ビジネス)、「久しぶり」(プライベート)、「当選」「今すぐ」(スパム)のように、クラスごとに決まった言い回しがあります。そのため、単語の出現だけを見る TF-IDF でも十分に分けられます。13.5.3 項の映画レビューでは、Embedding で正解率が大きく上がりました。言い換えが多いタスクほど Embedding が有利になりやすいです。

発展課題: アンカー文による学習なし感情判定

発展課題

学習をせずに、ポジティブ/ネガティブを代表する 2 つの「アンカー文」との類似度だけで、文の感情を判定してみましょう。判定したい文を 2 つのアンカーと比べ、近い方のラベルを付けます。

タスク

  1. アンカー文(pos_anchor・neg_anchor)とテスト文をまとめて model_st.encode() メソッドでベクトル化する
  2. util.cos_sim() 関数で各テスト文とアンカー文の類似度を計算する
  3. 各テスト文について、pos アンカーとの類似度が neg アンカーより高ければ positive、そうでなければ negative と判定して表示する
テンプレートコード
from sentence_transformers import SentenceTransformer, util

model_st = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

# ポジ・ネガを代表するアンカー文
pos_anchor = "素晴らしい映画だった"
neg_anchor = "ひどい映画だった"

# 判定したい文
tests = [
    "期待以上の出来で本当に感動した",
    "時間の無駄だった。二度と見たくない",
    "話の展開が退屈で全く楽しめなかった",
    "映像が美しく、心から楽しめる作品だった",
]

# 1. アンカー文とテスト文をまとめてベクトル化する

# 2. コサイン類似度を計算する(util.cos_sim)

# 3. 各テスト文について pos/neg アンカーとの類似度を比べ、近い方のラベルを表示する
解答例
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 と、どちらとの類似度も低く差も小さめです。皮肉や中立的な文では差がさらに小さくなり、判定が不安定になります。アンカー文を書き換えて、結果がどう変わるか試してみてください。

13.7 まとめ

本章では、BoW・TF-IDF を用いた文書分類・感情分析の実践と、単語・テキストの意味を捉えるベクトル化手法について学びました。

第 13 章のまとめ

文書分類

  • 感情分析、トピック分類など
  • TF-IDF + ロジスティック回帰が基本
  • Naive Bayes、SVM、Random Forest も有効

BoW / TF-IDF の限界

  • 単語の意味を考慮できない
  • 文脈を考慮できない
  • スパースなベクトル

Embedding(埋め込み)

  • 単語やテキストを固定長の密なベクトルに変換
  • 意味的に近いものは、ベクトル空間上でも近くに配置

Word2Vec / Doc2Vec

  • 単語の意味をベクトル化
  • 似た文脈の単語 → 似たベクトル
  • 単語の演算が可能(king - man + woman ≈ queen)
  • Doc2Vec は文書全体を 1 つの固定長ベクトルとして表現

Transformer / BERT

  • Attention 機構により文脈を考慮
  • 事前学習 + ファインチューニングで高精度

Sentence Transformers

  • 文全体を固定長の密なベクトルに変換
  • 意味の近い文をベクトル空間上でも比較できる
  • 類似文検索や文書分類の特徴量として利用できる

コサイン類似度

  • ベクトル間の角度で類似度を測る
  • 1 に近いほど類似
自然言語処理(第 12 〜 13 章)の総まとめ

第 12 章:自然言語処理の基礎とテキストのベクトル化

  • テキストクリーニング(URL、記号の削除)
  • 形態素解析(MeCab)
  • 正規化(ストップワード除去、品詞フィルタリング)
  • Bag-of-Words(単語の出現回数)
  • TF-IDF(単語の重要度を考慮)

第 13 章:テキストデータを用いた機械学習

  • 文書分類・感情分析の実践
  • Embedding(埋め込み)の概念
  • Word2Vec / Doc2Vec(単語・文書の意味をベクトル化)
  • Transformer / BERT(文脈を考慮)
  • Sentence Transformersによる文のEmbedding