NLPで変わる私の人生

NLPにおける単語埋め込みの基礎:Word2VecとGloVeによる実践

Tags: NLP, 単語埋め込み, Word2Vec, GloVe, Python, Gensim, 自然言語処理

自然言語処理(NLP)において、テキストデータを機械学習モデルが扱える形式に変換することは、非常に重要なステップです。その中でも「単語埋め込み(Word Embedding)」は、単語の意味的な情報を効率的に数値表現へと変換する技術として広く活用されています。

この記事では、情報科学を専攻し、Pythonプログラミングや機械学習の基礎知識をお持ちの大学院生の皆様に向けて、NLPにおける単語埋め込みの基本的な概念から、代表的な手法であるWord2VecとGloVeの理論、そしてPythonを使った実践的な実装方法までを、豊富なコード例とともに詳細に解説いたします。

単語埋め込みとは何か:単語の数値表現の課題と分散表現

まず、なぜ単語埋め込みが必要とされるのか、その背景から見ていきましょう。

離散表現の限界

テキスト中の単語をコンピュータで処理するためには、単語を数値データに変換する必要があります。最も単純な方法は「One-Hot Encoding(ワンホットエンコーディング)」と呼ばれる手法です。これは、語彙中の各単語に一意のインデックスを割り当て、そのインデックスに対応する要素のみが1で、他が0であるようなベクトルとして表現します。

例えば、「猫」「犬」「鳥」という3つの単語からなる語彙の場合、以下のように表現されます。

しかし、One-Hot Encodingにはいくつかの課題があります。

  1. 次元の呪い: 語彙サイズが大きくなると、ベクトルの次元数も膨大になり、計算コストが増大します。
  2. 意味的関係の欠如: どの単語ベクトルも互いに直交しており、単語間の意味的な類似性や関連性を表現することができません。「犬」と「猫」が「鳥」よりも意味的に近い関係にあるとしても、One-Hot表現からはそのような情報は一切読み取れません。

分散表現としての単語埋め込み

これらの課題を解決するために登場したのが「単語埋め込み」、すなわち「分散表現(Distributed Representation)」です。分散表現では、各単語を低次元かつ密な実数ベクトルとして表現します。このベクトルは、単語が文脈の中でどのように使われるか、あるいは他の単語とどの程度共起するかといった情報に基づいて学習されます。

分散表現の最も重要な特徴は、意味的に似た単語は、ベクトル空間内で互いに近い位置に配置されるという点です。これにより、単語間の類似度をベクトル間の距離(例:コサイン類似度)で測ることが可能になり、より高度なNLPタスクの実現に貢献します。

Word2Vec:単語の意味を捉える革新的な手法

Word2Vecは、Googleの研究者によって2013年に発表された単語埋め込みの手法です。この手法は、ニューラルネットワークを用いて単語の分散表現を学習します。Word2Vecには主に2つのモデルがあります。

一般的に、Skip-gramの方が少量のデータでより精度の高い埋め込みを生成する傾向があるとされています。

Word2Vecの学習原理(Skip-gramの場合)

Skip-gramモデルでは、ある単語(入力単語)が与えられたときに、その単語の周囲に出現する単語(出力単語)を予測するようにニューラルネットワークを訓練します。具体的には、学習データから(入力単語, 出力単語)のペアを抽出し、このペアが多く出現するほど、それらの単語の埋め込みベクトルが互いに近い位置になるように学習が進められます。

この学習プロセスには、「負例サンプリング(Negative Sampling)」という効率的な手法が用いられることが多く、これにより計算コストを大幅に削減しながら、高品質な埋め込みベクトルを獲得できます。

PythonでのWord2Vec実践:Gensimライブラリ

PythonでWord2Vecを扱う際には、gensimライブラリが非常に便利です。ここでは、簡単なテキストデータを用いてWord2Vecモデルを学習し、その結果を確認する手順を示します。

まず、gensimライブラリをインストールします。

pip install gensim

次に、簡単な文章データを用意し、前処理として単語に分割(トークン化)します。

import gensim
from gensim.models import Word2Vec
import re

# サンプル文章データ
text_data = [
    "猫は可愛い動物です。",
    "犬は忠実なペットです。",
    "鳥は空を飛びます。",
    "動物園にはたくさんの動物がいます。",
    "可愛い猫と忠実な犬が一緒に遊びます。",
    "ペットは家族の一員です。"
]

# 前処理:トークン化と正規化
# 簡単な例として、日本語の分かち書きは行わず、スペースで分割
# 実際にはMeCabやJanomeなどの形態素解析器を使用します
processed_sentences = []
for sentence in text_data:
    # 記号の除去、小文字化など
    sentence = re.sub(r'[^\w\s]', '', sentence) # 記号除去
    words = sentence.split() # スペースで分割 (簡易的なトークン化)
    if words: # 空のリストでないことを確認
        processed_sentences.append(words)

print("トークン化された文章:")
for sentence in processed_sentences:
    print(sentence)

上記のコードを実行すると、以下のようなトークン化された文章が表示されます。

トークン化された文章:
['猫は可愛い動物です']
['犬は忠実なペットです']
['鳥は空を飛びます']
['動物園にはたくさんの動物がいます']
['可愛い猫と忠実な犬が一緒に遊びます']
['ペットは家族の一員です']

注意点: 上記の日本語トークン化は非常に簡易的なものです。実用的な日本語処理では、MeCabやJanomeなどの形態素解析器を用いて、文を形態素に分割する「分かち書き」を行う必要があります。

続いて、このトークン化されたデータを使ってWord2Vecモデルを学習させます。

# Word2Vecモデルの学習
# vector_size: 埋め込みベクトルの次元数
# window: 文脈窓のサイズ (対象単語から前後何単語を見るか)
# min_count: 考慮する単語の最小出現回数 (これ以下の単語は無視)
# workers: 学習に使用するスレッド数
# sg: 0=CBOW, 1=Skip-gram
model = Word2Vec(
    sentences=processed_sentences,
    vector_size=100,
    window=5,
    min_count=1,
    workers=4,
    sg=1
)

# モデルの学習 (通常は上記で自動的に学習されますが、明示的に呼び出すことも可能です)
# model.train(processed_sentences, total_examples=len(processed_sentences), epochs=model.epochs)

print("\nWord2Vecモデル学習完了。")

学習が完了したら、モデルを使って単語ベクトルを取得したり、類似単語を検索したりできます。

# 特定の単語のベクトルを取得
# モデルの語彙に存在しない単語はKeyErrorとなるため注意
if '猫は可愛い動物です' in model.wv: # 簡易トークン化のため、フレーズが単語として扱われる可能性
    cat_vector = model.wv['猫は可愛い動物です']
    print(f"\n'猫は可愛い動物です'のベクトル(最初の5要素):\n{cat_vector[:5]}")
else:
    print("\n'猫は可愛い動物です'はモデルの語彙に含まれていません。")


# 類似単語の検索
# ここでも、簡易トークン化の影響で意図した結果が得られない可能性があります。
# 実際には「猫」「犬」「動物」といった単語で検索します。
try:
    print("\n'可愛い猫と忠実な犬が一緒に遊びます'に類似する単語:")
    # topnで表示する数を指定
    similar_words = model.wv.most_similar('可愛い猫と忠実な犬が一緒に遊びます', topn=3)
    for word, similarity in similar_words:
        print(f"  {word}: {similarity:.4f}")
except KeyError:
    print("\n指定された単語はモデルの語彙に含まれていません。")

# モデルの語彙リストを表示(簡易トークン化のため、リストは小さい)
print("\nモデルの語彙に含まれる単語:")
for word in model.wv.index_to_key[:5]: # 最初の5つだけ表示
    print(f"- {word}")

上記のコードでは、非常に少ないデータと簡易的なトークン化のため、意味的な類似度が明確に出にくい場合があります。実際の研究や開発では、数百万から数十億規模の大量のテキストデータを用いて学習させることが一般的です。

GloVe:共起統計情報に基づく単語埋め込み

Word2Vecが局所的な文脈(単語の周辺の単語)から単語埋め込みを学習する「予測ベース」のアプローチであるのに対し、GloVe (Global Vectors for Word Representation) は、コーパス全体の「共起統計情報」を直接利用する「計数ベース」のアプローチです。スタンフォード大学の研究者によって2014年に発表されました。

GloVeの学習原理

GloVeは、単語の共起確率の対数比に注目します。ある2つの単語 ij がどれだけ共起するかという情報(共起行列)を全体的に分析し、それらを低次元のベクトルとして表現することを目指します。

具体的には、単語ベクトル w_iw_j の内積が、単語 ij の共起確率の対数に近づくように学習されます。これにより、コーパス全体の統計情報を効率的に取り込み、単語間の意味的な関係性をよりグローバルな視点から捉えることが可能になります。

PythonでのGloVe実践:事前学習済みモデルの利用

GloVeのモデルを一から学習させるには大規模な計算資源が必要となるため、通常はすでに学習済みのモデル(事前学習済みモデル)をダウンロードして利用することが一般的です。

事前学習済みGloVeモデルは、Common CrawlやWikipediaなどの大規模なコーパスで学習されており、さまざまな次元数(例: 50, 100, 200, 300次元)のものが公開されています。

ここでは、gensimライブラリの KeyedVectors クラスを使ってGloVeの事前学習済みモデルをロードし、利用する例を示します。まず、Gensimの提供するAPIを用いてGloVeモデルをダウンロードします。

import gensim.downloader as api
import numpy as np

# GloVeの事前学習済みモデルをダウンロード
# 'glove-wiki-gigaword-100' は、WikipediaとGigawordで学習された100次元のGloVeモデルを指します。
# 初回実行時はダウンロードに時間がかかります。
try:
    glove_vectors = api.load("glove-wiki-gigaword-100")
    print("\nGloVe事前学習済みモデルのロードが完了しました。")
except ValueError as e:
    print(f"\nGloVeモデルのダウンロードまたはロードに失敗しました: {e}")
    print("インターネット接続を確認するか、モデル名が正しいか確認してください。")
    print("利用可能なモデルリスト: api.info()['models'].keys()")
    # エラーが発生した場合、以降の処理はスキップ
    glove_vectors = None

if glove_vectors:
    # 単語ベクトルを取得
    try:
        word_vector_cat = glove_vectors['cat']
        word_vector_dog = glove_vectors['dog']
        word_vector_computer = glove_vectors['computer']

        print(f"\n'cat'のベクトル(最初の5要素):\n{word_vector_cat[:5]}")
        print(f"'dog'のベクトル(最初の5要素):\n{word_vector_dog[:5]}")
        print(f"'computer'のベクトル(最初の5要素):\n{word_vector_computer[:5]}")

        # 類似単語の検索
        print("\n'king'に類似する単語:")
        similar_to_king = glove_vectors.most_similar('king', topn=5)
        for word, similarity in similar_to_king:
            print(f"  {word}: {similarity:.4f}")

        # 単語間の類似度を計算
        similarity_cat_dog = glove_vectors.similarity('cat', 'dog')
        similarity_cat_computer = glove_vectors.similarity('cat', 'computer')

        print(f"\n'cat'と'dog'の類似度: {similarity_cat_dog:.4f}")
        print(f"'cat'と'computer'の類似度: {similarity_cat_computer:.4f}")

        # アナロジー(類推)の計算
        # 「king - man + woman = queen」のような関係を求める
        print("\nアナログ推論: 'man'が'king'に対して'woman'が'?'")
        result = glove_vectors.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
        print(f"  {result[0][0]}: {result[0][1]:.4f}")

    except KeyError as e:
        print(f"\n指定された単語 '{e}' はモデルの語彙に含まれていません。")
    except Exception as e:
        print(f"\nGloVeモデル利用中にエラーが発生しました: {e}")

このコードでは、glove-wiki-gigaword-100 モデルを利用して、単語のベクトル取得、類似単語の検索、単語間の類似度計算、そして単語のアナロジー(類推)の計算を行っています。特にアナログ推論は、単語埋め込みが単語間の意味的・文法的な関係性を捉えていることを示す良い例です。

Word2VecとGloVeの比較

Word2VecとGloVeはどちらも単語埋め込みを生成する強力な手法ですが、そのアプローチには違いがあります。

| 特徴 | Word2Vec (予測ベース) | GloVe (計数ベース) | | :-------------- | :------------------------------------------------------ | :----------------------------------------------------------- | | 学習アプローチ | 局所的な文脈から単語を予測するニューラルネットワークを使用 | コーパス全体の共起統計情報(共起行列)を直接利用し、最適化問題として解く | | 強み | 大規模なコーパスで効率的に学習でき、柔軟性が高い | 全体的な統計情報を考慮するため、より安定した埋め込みを生成する傾向がある | | 弱み | 共起行列を直接扱うわけではないため、グローバルな情報を捉えにくい場合がある | 共起行列の構築に一定のメモリと計算コストがかかる。大規模なコーパスでの一からの学習は資源を要する | | 使い分け | データ量が非常に多い場合や、文脈による意味の違いを強調したい場合 | 安定した埋め込みが必要な場合や、事前学習済みモデルを利用する場合 |

どちらの手法も優れた単語埋め込みを提供しますが、プロジェクトの要件や利用可能な計算資源に応じて適切なものを選択することが重要です。

まとめと次のステップ

この記事では、NLPにおける単語埋め込みの基本概念、One-Hot Encodingの限界、そして分散表現の重要性について解説しました。さらに、代表的な単語埋め込み手法であるWord2VecとGloVeの理論的背景を学び、Pythonのgensimライブラリを用いた具体的な実装方法を体験しました。

単語埋め込みは、セマンティック検索、文書分類、機械翻訳など、さまざまなNLPタスクの性能向上に不可欠な技術です。今回学んだWord2VecやGloVeは、現代のTransformerベースのモデル(BERT, GPTなど)の基礎となる概念でもあります。

次のステップとして、より発展的な単語埋め込み手法や、Transformerモデルにおける「サブワード埋め込み」や「文脈埋め込み」について学習を進めることをお勧めします。例えば、Hugging Face Transformersライブラリを使って、BERTのようなモデルがどのように単語を扱っているかを深掘りしてみるのも良いでしょう。

この知識が、皆様のNLP研究や開発の一助となれば幸いです。