BERTによる文脈化埋め込み：基礎と仕組み（カレー例文付き）


## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/a3d44300-40ac-4d86-94f1-b29cc48c4088.png)

自然言語処理（NLP）の世界では、テキストデータをコンピュータが理解できる形式に変換することが不可欠です。その中でも「文脈を考慮した単語の意味表現」は近年大きく発展してきました。本記事では、Googleが開発したBERT（Bidirectional Encoder Representations from Transformers）モデルを利用した文脈化された埋め込みについて、カレーに関する例文を用いながら解説します。

この記事はGoogle Colaboratoryで実行できるコードを含み、自然言語処理の初心者から中級者を対象としています。まずはTransformerの基本的な構造からBERTの仕組み、そしてHugging Faceライブラリを使った実装方法までを解説します。

## 1. Transformerアーキテクチャの概要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/00debfc4-6c64-4137-972a-130cd49635f4.png)

### 1.1 従来の手法の限界

カレーに関する以下の二つの文を考えてみましょう：

- 「このカレーは**辛い**ので、子供には向かないかもしれません。」
- 「彼女は**辛い**思い出を抱えながらもカレーを作り続けていた。」

同じ「辛い」という単語でも、前者は「味覚的な刺激」を、後者は「心理的な苦痛」を意味しています。従来のWord2VecやGloVeなどの単語埋め込み手法では、単語に対して固定的なベクトル表現を割り当てるため、このような文脈による意味の違いを表現できませんでした。

### 1.2 Transformerモデルの登場

2017年、Googleの研究チームは「Attention is All You Need」という論文で、RNNやCNNを使わずに「Self-Attention」機構のみを用いたTransformerアーキテクチャを提案しました。これにより、文中の各単語が他のすべての単語との関係性を考慮した表現が可能になりました。

### 1.3 Transformerの基本構造

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/43e18536-6e70-4206-82f4-01e1080e0fb6.png)

Transformerは主に以下の要素で構成されています：

1. **入力埋め込み (Input Embeddings)**: 単語をベクトルに変換
2. **位置エンコーディング (Positional Encoding)**: 単語の位置情報を付加
3. **マルチヘッドセルフアテンション (Multi-Head Self-Attention)**: 文中の関連性を計算
4. **フィードフォワードネットワーク (Feed-Forward Network)**: 非線形変換を適用
5. **残差接続 (Residual Connections)**: 勾配消失問題を緩和
6. **層正規化 (Layer Normalization)**: 学習の安定化

### 1.4 Self-Attentionの仕組み

![self-attention-svg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/66f371a0-4cd5-46f3-94dc-d36b89422d48.png)

Self-Attentionの核心は、「Query」「Key」「Value」という3つのベクトルを使った計算にあります。カレーの例で説明しましょう：

```python
# Google Colabで必要なライブラリとツールをインストール
!pip install transformers torch numpy matplotlib
!apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
!apt-get install -y mecab mecab-ipadic-utf8 libmecab-dev
!pip install japanize-matplotlib

# MeCabの設定ファイルの存在確認とインストール
!ln -s /etc/mecabrc /usr/local/etc/mecabrc
```

```python
import torch
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語表示のため

# 簡単なSelf-Attentionの実装例
def self_attention(query, key, value):
    # Attentionスコアの計算
    scores = torch.matmul(query, key.transpose(-2, -1)) / torch.sqrt(torch.tensor(key.size(-1), dtype=torch.float32))
    
    # Softmaxで正規化
    attention_weights = torch.nn.functional.softmax(scores, dim=-1)
    
    # 重み付け和を計算
    output = torch.matmul(attention_weights, value)
    
    return output, attention_weights

# カレーの例文のトークン（簡略化）
tokens = ["私は", "本格的な", "カレー", "が", "食べ", "たい"]

# 擬似的な埋め込み（実際はもっと高次元）
embeddings = torch.randn(6, 4)  # 6トークン、4次元の埋め込み

# Self-Attention計算
query = key = value = embeddings
output, weights = self_attention(query, key, value)

# Attention重みの可視化
plt.figure(figsize=(8, 6))
plt.imshow(weights.detach().numpy(), cmap='viridis')
plt.xticks(range(len(tokens)), tokens, rotation=45)
plt.yticks(range(len(tokens)), tokens)
plt.colorbar()
plt.title("Self-Attention重み")
plt.tight_layout()
plt.show()
```

このコードでは、「私は本格的なカレーが食べたい」という文において、各単語がどの単語に注目しているかを可視化しています。たとえば「カレー」という単語は「本格的な」という形容詞と強い関連性を持つことが期待されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/de97025a-abb0-4467-b514-bb6fe74b6490.png)


## 2. BERTの事前学習と文脈表現

### 2.1 BERTの基本構造
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/df82544a-0c64-4447-8f7d-42a769be4089.png)

BERT（Bidirectional Encoder Representations from Transformers）は、Transformerのエンコーダー部分を積み重ねたモデルです。最大の特徴は**双方向性**にあります。従来のモデルが左から右（または右から左）の一方向だけを考慮していたのに対し、BERTは文中の単語を予測する際に左右両方の文脈を利用します。

BERTの基本アーキテクチャには2つのサイズがあります：
- **BERT-Base**: 12層のTransformerエンコーダー、768次元の隠れ層、12個のアテンションヘッド
- **BERT-Large**: 24層のTransformerエンコーダー、1024次元の隠れ層、16個のアテンションヘッド

### 2.2 事前学習タスク

BERTは2つの事前学習タスクで学習されます：

1. **マスク言語モデル (Masked Language Model, MLM)**
   - 入力文の15%の単語をランダムにマスク（[MASK]）して予測
   - 例：「私は[MASK]カレーを作った」→「私は美味しいカレーを作った」

2. **次文予測 (Next Sentence Prediction, NSP)**
   - 2つの文が連続するかどうかを予測
   - 例：「このカレーはとても辛い。」「水を飲んだ方がいいよ。」→ 連続する
   - 例：「このカレーはとても辛い。」「明日は晴れるでしょう。」→ 連続しない

### 2.3 文脈化された埋め込みの例

カレーに関する例文で、BERTの文脈表現を確認してみましょう：

```python
from transformers import BertTokenizer, BertModel
import torch
import matplotlib.pyplot as plt
import numpy as np
import japanize_matplotlib  # 日本語表示のため

# 日本語BERTモデルのロード
tokenizer = BertTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
model = BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')

# 異なる文脈での「辛い」を含む例文
texts = [
    "このカレーは辛いので、子供には向かないかもしれません。",
    "彼女は辛い思い出を抱えながらもカレーを作り続けていた。"
]

# 文全体の埋め込みを取得する関数（CLS）
def get_sentence_embedding(text):
    inputs = tokenizer(text, return_tensors="pt")
    with torch.no_grad():
        outputs = model(**inputs)
    
    # CLSトークンの埋め込みを返す
    return outputs.last_hidden_state[0, 0].detach().numpy()

# 各文の埋め込みを取得
embedding1 = get_sentence_embedding(texts[0])
embedding2 = get_sentence_embedding(texts[1])

# コサイン類似度の計算
similarity = cosine_similarity([embedding1], [embedding2])[0][0]
print(f"2つの文のコサイン類似度: {similarity:.4f}")

# 文脈の埋め込みを視覚化
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.bar(range(10), embedding1[:10])
plt.title("「辛いカレー」の文脈の埋め込み")
plt.subplot(1, 2, 2)
plt.bar(range(10), embedding2[:10])
plt.title("「辛い思い出」の文脈の埋め込み")
plt.tight_layout()
plt.show()

# トークン化の例を表示して確認
for text in texts:
    tokens = tokenizer.tokenize(text)
    print(f"文: {text}")
    print(f"トークン: {tokens}")
    print("-" * 50)
```

このコードでは、味覚的な「辛い」と心理的な「辛い」の埋め込みベクトルを比較しています。BERTのような文脈化モデルでは、同じ単語でも文脈によって異なる表現が得られます。一般的に、コサイン類似度は高くても1.0未満になるでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/76bb461f-7196-45bb-9226-9cc078c493c2.png)


## 3. トークン化とサブワード分割

### 3.1 トークン化の基本

自然言語をコンピュータで処理するには、テキストを「トークン」と呼ばれる単位に分割する必要があります。日本語の場合、形態素解析によって単語に分割することが多いです。

```python
# MeCabを使った日本語のトークン化（Google Colab用）
!pip install fugashi ipadic
!pip install unidic-lite

import fugashi

# MeCabによるトークン化
mecab = fugashi.Tagger()
text = "本格的なインドカレーを作りました"
tokens = [word.surface for word in mecab(text)]
print(tokens)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5d8a0d19-96b5-42e7-9b9b-fc9f7eb32c07.png)

### 3.2 BERTのサブワード分割

BERTでは、特に未知語に対応するため「サブワード分割」という手法を採用しています。これにより、「カレーライス」のような辞書にない複合語も「カレー」+「ライス」のように既知の部分に分解して処理できます。

日本語BERTでは、主に以下の分割方法が使われています：
- 文字単位（Character-level）
- WordPiece
- SentencePiece

```python
from transformers import BertTokenizer

# 日本語BERTのトークナイザー
tokenizer = BertTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')

# カレー関連のテキスト
texts = [
    "本格的なインドカレーを作りました",
    "カレーライスは日本の国民食です",
    "ココイチのカレーは辛さを選べます"
]

# サブワード分割を確認
for text in texts:
    tokens = tokenizer.tokenize(text)
    print(f"原文: {text}")
    print(f"トークン: {tokens}")
    print(f"トークン数: {len(tokens)}")
    print("-" * 50)

# 入力IDs、アテンションマスク、トークンタイプIDsの確認
for text in texts:
    encoding = tokenizer(text, return_tensors="pt")
    print(f"原文: {text}")
    print(f"入力IDs: {encoding['input_ids']}")
    print(f"アテンションマスク: {encoding['attention_mask']}")
    print("-" * 50)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b407ad7f-1da5-4438-8289-4319ca1fbcd6.png)

### 3.3 特殊トークン

BERTでは以下の特殊トークンが使われます：
- **[CLS]**: シーケンスの先頭に配置され、分類タスクで使用
- **[SEP]**: 文の区切りを示す
- **[MASK]**: マスク言語モデルでマスクされた単語
- **[PAD]**: バッチ処理で文長を揃えるためのパディング

```python
# 特殊トークンの確認
print(f"[CLS]のID: {tokenizer.cls_token_id}")
print(f"[SEP]のID: {tokenizer.sep_token_id}")
print(f"[MASK]のID: {tokenizer.mask_token_id}")
print(f"[PAD]のID: {tokenizer.pad_token_id}")

# 特殊トークンを含む入力の例
text_pair = ["このカレーは辛い", "水を飲んだ方がいいよ"]
encoding = tokenizer(text_pair[0], text_pair[1], return_tensors="pt")

# テンソルをPythonのリストに変換してからデコード
input_ids = encoding['input_ids'][0].tolist()
decoded = tokenizer.convert_ids_to_tokens(input_ids)

print(f"2文の入力: {text_pair}")
print(f"デコードされたトークン: {decoded}")

# 特殊トークンの位置を確認
if tokenizer.cls_token in decoded:
    cls_idx = decoded.index(tokenizer.cls_token)
    print(f"[CLS]トークンの位置: {cls_idx}")
    
if tokenizer.sep_token in decoded:
    sep_indices = [i for i, x in enumerate(decoded) if x == tokenizer.sep_token]
    print(f"[SEP]トークンの位置: {sep_indices}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/a14e6126-aa13-4a13-a5fa-c3473350b939.png)

## 4. HuggingFaceライブラリの基本的な使い方

### 4.1 Transformersライブラリの概要

Hugging Faceの`transformers`ライブラリは、BERTを含む多くの最先端NLPモデルを簡単に使えるようにしたPythonライブラリです。

```python
# 基本的な使い方
from transformers import pipeline

# センチメント分析パイプライン（英語）
sentiment_analyzer = pipeline("sentiment-analysis")
result = sentiment_analyzer("I love curry. It's delicious!")
print(result)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/df3830e5-8351-4693-9f5c-78f5284b44b5.png)

### 4.2 日本語BERTモデルのロード

```python
from transformers import BertJapaneseTokenizer, BertModel
import torch

# 日本語BERTモデルのロード
model_name = 'cl-tohoku/bert-base-japanese-whole-word-masking'
tokenizer = BertJapaneseTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)

# テキストのエンコード
text = "カレーは世界中で愛されている料理です。"
inputs = tokenizer(text, return_tensors="pt")

# モデルに入力
with torch.no_grad():
    outputs = model(**inputs)

# 最終層の隠れ状態
last_hidden_state = outputs.last_hidden_state
print(f"最終層の形状: {last_hidden_state.shape}")  # [1, シーケンス長, 隠れ層サイズ]

# [CLS]トークンの表現（文章全体の表現としてよく使われる）
cls_embedding = last_hidden_state[:, 0, :]
print(f"[CLS]トークンの埋め込み形状: {cls_embedding.shape}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/e69be61f-caa8-457c-918a-9adb3a0fe9a7.png)


### 4.3 文脈化された単語埋め込みの取得

```python
# カレーに関する複数の文での「カレー」の埋め込みを比較
texts = [
    "インドカレーは本格的な香辛料を使います。",
    "日本のカレーはルウから作ることが多いです。",
    "カレーパンは日本独自の食べ物です。"
]

# 各文の「カレー」の埋め込みを取得
curry_embeddings = []
for text in texts:
    inputs = tokenizer(text, return_tensors="pt")
    outputs = model(**inputs)
    
    # 「カレー」のトークンIDを探す
    tokens = tokenizer.convert_ids_to_tokens(inputs.input_ids[0])
    try:
        curry_idx = tokens.index("カレー")
        # 対応する埋め込みを取得
        curry_emb = outputs.last_hidden_state[0, curry_idx, :]
        curry_embeddings.append(curry_emb.detach().numpy())
    except ValueError:
        # トークンが見つからない場合
        print(f"「カレー」のトークンが見つかりません: {text}")
        print(f"トークン: {tokens}")

# コサイン類似度を計算
if len(curry_embeddings) >= 2:
    from sklearn.metrics.pairwise import cosine_similarity
    
    similarity_matrix = cosine_similarity(curry_embeddings)
    
    # 結果の表示
    print("「カレー」の文脈による類似度:")
    for i in range(len(texts)):
        for j in range(i+1, len(texts)):
            print(f"「{texts[i]}」と「{texts[j]}」の類似度: {similarity_matrix[i][j]:.4f}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/17665612-343b-4134-97c2-9d477d4c45f8.png)

### 4.4 マスク言語モデルを使った単語予測

```python
from transformers import pipeline

# 日本語BERTを使用したマスク単語予測
fill_mask = pipeline("fill-mask", model=model_name, tokenizer=model_name)

# カレーに関する例文でマスク予測
masked_texts = [
    f"日本人は{tokenizer.mask_token}カレーが好きです。",
    f"カレーには{tokenizer.mask_token}な香辛料が使われています。",
    f"カレーを食べると{tokenizer.mask_token}が出ます。"
]

for masked_text in masked_texts:
    results = fill_mask(masked_text)
    print(f"元の文: {masked_text}")
    print("予測結果:")
    for result in results[:5]:  # 上位5件を表示
        print(f"  {result['token_str']}: {result['score']:.4f}")
    print("-" * 50)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/7270d06f-fdc8-4eea-947c-932e3a14e021.png)

## 5. 実践例：カレーレビューの埋め込み分析

カレーレビューの埋め込みを可視化して、文脈による単語の意味の違いを確認してみましょう。

```python
import torch
from transformers import BertTokenizer, BertModel
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
import numpy as np

# 日本語BERTモデルのロード
tokenizer = BertTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
model = BertModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')

# カレーに関するレビュー文
curry_reviews = [
    "このカレーは甘くて子供にも人気です。",
    "スパイシーなカレーで大人の味わいです。",
    "ここのカレーはとても辛いので注意が必要です。",
    "カレーの中にゴロゴロとした野菜が入っていて美味しかった。",
    "昔ながらの日本のカレーライスという感じでした。",
    "本格的なインド風カレーを味わえるお店です。"
]

# レビュー文の埋め込みを取得
review_embeddings = []
for review in curry_reviews:
    inputs = tokenizer(review, return_tensors="pt", padding=True, truncation=True)
    
    with torch.no_grad():
        outputs = model(**inputs)
    
    # [CLS]トークンの埋め込み（文全体の表現）
    cls_embedding = outputs.last_hidden_state[0, 0, :].numpy()
    review_embeddings.append(cls_embedding)

# PCAで2次元に削減して可視化
pca = PCA(n_components=2)
reduced_embeddings = pca.fit_transform(np.array(review_embeddings))

# 可視化
plt.figure(figsize=(10, 6))
plt.scatter(reduced_embeddings[:, 0], reduced_embeddings[:, 1], c='blue', alpha=0.7)

# レビュー文のラベル
for i, txt in enumerate(curry_reviews):
    plt.annotate(txt[:10] + "...", (reduced_embeddings[i, 0], reduced_embeddings[i, 1]), fontsize=9)

plt.title("カレーレビューの文埋め込み (PCA)")
plt.xlabel("主成分1")
plt.ylabel("主成分2")
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/578118ad-a51a-4870-aa0e-769a7e138bf6.png)

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/efc8399b-ef15-4475-ac44-56a24745f165.png)

本記事では、BERTによる文脈化された埋め込みの基礎について解説しました。Transformerアーキテクチャの仕組みから始まり、BERTの事前学習方法、トークン化とサブワード分割、そしてHugging Faceライブラリを使った実装方法まで、カレーに関する例文を通じて学びました。

文脈化された埋め込みの最大の特徴は、同じ単語でも文脈によって異なる表現を獲得できる点です。「辛い」という単語が味覚的な意味と心理的な意味で異なる埋め込みを持つことを確認できました。

## おわりに

BERTはNLPの世界に革命をもたらしましたが、その後もGPT、RoBERTa、T5、XLNetなど様々なモデルが登場しています。文脈化された埋め込みの基本を理解することで、これらの発展的なモデルの理解も容易になるでしょう。

カレーに関する日本語の例文を用いて直感的に理解できるよう心がけました。