BERT(Bidirectional Encoder Representations from Transformers)は、2018年に Google の Devlin らが提案した事前学習済み言語モデルです。Transformer の Encoder を積層し、双方向の文脈を同時に考慮して言語表現を学習する点が最大の特徴です。BERT の登場により、多くの NLP ベンチマークで当時の最高性能が大幅に更新され、NLP 分野に「事前学習→ファインチューニング」というパラダイムを確立しました。
従来の言語モデル(ELMo や GPT-1)は、左から右への一方向(あるいは左右を独立に学習する双方向)でしたが、BERT は Masked Language Model(MLM)という巧妙な事前学習タスクにより、真の意味での双方向文脈を捉えることに成功しました。
本記事の内容
- BERT の位置づけ(双方向エンコーダ)と従来手法との比較
- Transformer Encoder の積層アーキテクチャ
- [CLS] / [SEP] トークンの役割
- 入力表現(トークン埋め込み + セグメント埋め込み + 位置埋め込み)
- 事前学習タスク 1: MLM の目的関数と 80/10/10 ルール
- 事前学習タスク 2: NSP の定式化
- 各レイヤーが学習する言語的特徴(構文→意味の階層)
- ファインチューニング戦略の概要
- Python で huggingface transformers を使った BERT の推論・マスク予測・文埋め込み抽出
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
BERT の位置づけ
従来手法の限界
BERT 以前の代表的な言語表現学習手法とその限界を整理します。
Word2Vec / GloVe(文脈非依存の静的埋め込み):
- 各単語に1つの固定ベクトルを割り当てるため、多義語(”bank” = 銀行 / 川岸)を区別できない
ELMo(2018, Peters et al.):
- 順方向 LSTM と逆方向 LSTM を独立に学習し、それらの隠れ状態を連結して文脈依存の埋め込みを得る
- ただし、左→右と右→左のモデルが独立であり、両方向の文脈を同時に考慮できない
GPT-1(2018, Radford et al.):
- Transformer Decoder を用いた左→右の自己回帰言語モデル
- 左側の文脈しか見ないため、双方向の文脈を活用できない
BERT は、Transformer Encoder を用いて入力系列全体を一度に見ることで、各トークンが左右両方の文脈を直接参照できる真の双方向モデルを実現しました。
なぜ単純に双方向言語モデルではダメなのか
通常の言語モデルは $P(w_t \mid w_1, \dots, w_{t-1})$ を学習しますが、これを双方向に拡張して $P(w_t \mid w_1, \dots, w_{t-1}, w_{t+1}, \dots, w_T)$ を直接学習しようとすると、ターゲット単語自身が入力に含まれてしまい、モデルが「答えを見てしまう」問題が生じます。BERT は MLM(一部の単語をマスクして予測する)という手法でこの問題を回避しました。
BERT のアーキテクチャ
全体構造
BERT は Transformer Encoder を $L$ 層積層した構造です。
| モデル | レイヤー数 $L$ | 隠れ次元 $d_{\text{model}}$ | アテンションヘッド数 $A$ | パラメータ数 |
|---|---|---|---|---|
| BERT-Base | 12 | 768 | 12 | 110M |
| BERT-Large | 24 | 1024 | 16 | 340M |
各レイヤーは以下の2つのサブレイヤーで構成されます。
- Multi-Head Self-Attention
- Position-wise Feed-Forward Network
それぞれのサブレイヤーに残差接続とレイヤー正規化が適用されます。
Multi-Head Self-Attention
入力系列 $\bm{H}^{(l-1)} = [\bm{h}_1, \bm{h}_2, \dots, \bm{h}_T] \in \mathbb{R}^{T \times d_{\text{model}}}$ に対して、各ヘッド $a = 1, \dots, A$ のアテンションを計算します。
$$ \bm{Q}_a = \bm{H}^{(l-1)} \bm{W}_a^Q, \quad \bm{K}_a = \bm{H}^{(l-1)} \bm{W}_a^K, \quad \bm{V}_a = \bm{H}^{(l-1)} \bm{W}_a^V $$
ここで $\bm{W}_a^Q, \bm{W}_a^K \in \mathbb{R}^{d_{\text{model}} \times d_k}$, $\bm{W}_a^V \in \mathbb{R}^{d_{\text{model}} \times d_v}$ であり、$d_k = d_v = d_{\text{model}} / A$ です。
Scaled Dot-Product Attention は
$$ \text{Attention}(\bm{Q}_a, \bm{K}_a, \bm{V}_a) = \text{softmax}\left(\frac{\bm{Q}_a \bm{K}_a^\top}{\sqrt{d_k}}\right) \bm{V}_a $$
重要な点: BERT は Encoder なので、因果マスク(causal mask)を使いません。各トークンは系列内の全てのトークンにアテンションを向けることができます。これが「双方向」の本質です。
全ヘッドの出力を連結して射影します。
$$ \text{MultiHead}(\bm{H}^{(l-1)}) = \text{Concat}(\text{head}_1, \dots, \text{head}_A) \bm{W}^O $$
ここで $\bm{W}^O \in \mathbb{R}^{d_{\text{model}} \times d_{\text{model}}}$ です。
Position-wise Feed-Forward Network
各位置に独立に適用される2層の全結合ネットワークです。
$$ \text{FFN}(\bm{x}) = \text{GELU}(\bm{x} \bm{W}_1 + \bm{b}_1) \bm{W}_2 + \bm{b}_2 $$
ここで $\bm{W}_1 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}}$, $\bm{W}_2 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}$ であり、BERT では $d_{\text{ff}} = 4 d_{\text{model}}$ です。
BERT では活性化関数として GELU(Gaussian Error Linear Unit)を使用します。
$$ \text{GELU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}\left[1 + \text{erf}\left(\frac{x}{\sqrt{2}}\right)\right] $$
ここで $\Phi(x)$ は標準正規分布の累積分布関数です。
残差接続とレイヤー正規化
各サブレイヤーの出力は
$$ \bm{H}^{(l)} = \text{LayerNorm}(\bm{H}^{(l-1)} + \text{Sublayer}(\bm{H}^{(l-1)})) $$
と計算されます。レイヤー正規化は、各トークン位置の隠れベクトルに対して
$$ \text{LayerNorm}(\bm{x}) = \frac{\bm{x} – \mu}{\sigma + \epsilon} \odot \bm{\gamma} + \bm{\beta} $$
ここで $\mu, \sigma$ はベクトル $\bm{x}$ の要素の平均と標準偏差、$\bm{\gamma}, \bm{\beta}$ は学習可能なパラメータです。
入力表現
BERT の入力は、以下の3つの埋め込みの要素ごとの和です。
$$ \bm{E}_{\text{input}} = \bm{E}_{\text{token}} + \bm{E}_{\text{segment}} + \bm{E}_{\text{position}} $$
1. トークン埋め込み(Token Embedding)
WordPiece トークナイザで分割された各サブワードトークンに対する埋め込みベクトルです。語彙サイズは 30,522(BERT-Base)で、埋め込み行列は $\bm{W}_{\text{token}} \in \mathbb{R}^{|\mathcal{V}| \times d_{\text{model}}}$ です。
2. セグメント埋め込み(Segment Embedding)
2つの文を入力する場合(NSP タスクなど)、各トークンがどちらの文に属するかを示します。
$$ \bm{E}_{\text{segment}}(t) = \begin{cases} \bm{s}_A & \text{if token } t \text{ belongs to sentence A} \\ \bm{s}_B & \text{if token } t \text{ belongs to sentence B} \end{cases} $$
$\bm{s}_A, \bm{s}_B \in \mathbb{R}^{d_{\text{model}}}$ は学習可能なパラメータです。
3. 位置埋め込み(Position Embedding)
Transformer の原論文ではサイン・コサインの固定的な位置エンコーディングが使われましたが、BERT では学習可能な位置埋め込みを使用します。
$$ \bm{E}_{\text{position}}(t) = \bm{p}_t, \quad t = 0, 1, \dots, T_{\max}-1 $$
$\bm{p}_t \in \mathbb{R}^{d_{\text{model}}}$ は各位置に対する学習可能なパラメータであり、$T_{\max} = 512$ です。
特殊トークン
入力系列には以下の特殊トークンが追加されます。
- [CLS]: 系列の先頭に置かれるトークン。このトークンの最終層の隠れ状態が、文全体の集約表現として使われる
- [SEP]: 文の境界を示すトークン。2文入力の場合、文 A と文 B の間に挿入される
- [MASK]: MLM タスクでマスクされた位置に挿入されるトークン
入力例:
[CLS] The cat sat on the mat [SEP] It was sleeping [SEP]
事前学習タスク 1: Masked Language Model(MLM)
目的関数の定式化
MLM は、入力系列の一部のトークンをマスクし、そのマスクされたトークンを周囲の文脈から予測するタスクです。
入力トークン系列を $\bm{x} = (x_1, x_2, \dots, x_T)$ とし、マスクされたトークンのインデックス集合を $\mathcal{M} \subset \{1, 2, \dots, T\}$ とします。マスク後の入力を $\tilde{\bm{x}}$ と書きます。
MLM の目的関数は、マスクされた位置の元のトークンを予測する交差エントロピー損失です。
$$ \begin{equation} \mathcal{L}_{\text{MLM}} = -\sum_{t \in \mathcal{M}} \log P(x_t \mid \tilde{\bm{x}}; \bm{\theta}) \end{equation} $$
具体的には、BERT の最終層の $t$ 番目の位置の隠れ状態 $\bm{h}_t^{(L)} \in \mathbb{R}^{d_{\text{model}}}$ に対して
$$ P(x_t = w \mid \tilde{\bm{x}}) = \text{softmax}(\bm{W}_{\text{vocab}} \cdot \text{GELU}(\bm{W}_{\text{proj}} \bm{h}_t^{(L)} + \bm{b}_{\text{proj}}) + \bm{b}_{\text{vocab}})_w $$
ここで $\bm{W}_{\text{proj}} \in \mathbb{R}^{d_{\text{model}} \times d_{\text{model}}}$, $\bm{W}_{\text{vocab}} \in \mathbb{R}^{|\mathcal{V}| \times d_{\text{model}}}$ です。$\bm{W}_{\text{vocab}}$ はトークン埋め込み行列と重みを共有する場合があります。
15% マスク戦略: 80/10/10 ルール
入力トークンの 15% をランダムに選択してマスク対象 $\mathcal{M}$ とします。選択された各トークンに対して:
- 80% の確率: [MASK] トークンに置換
- 10% の確率: ランダムな単語に置換
- 10% の確率: 元のトークンのまま
この戦略の根拠は以下の通りです。
なぜ 100% [MASK] ではないか: ファインチューニング時には [MASK] トークンが入力に現れないため、事前学習とファインチューニングの間にミスマッチが生じます。10% をランダム置換、10% をそのままにすることで、モデルが [MASK] だけでなく任意のトークンに対してもコンテキストに基づく予測を行えるようになります。
なぜランダム置換が 10% か: モデルが「全てのトークンが正しいとは限らない」と学習し、文脈からの推論能力を強化します。ランダム置換は全トークンの 1.5%(= 15% × 10%)に過ぎないため、学習を大きく乱すことはありません。
MLM と自己回帰言語モデルの比較
自己回帰言語モデル(GPT)の目的関数は
$$ \mathcal{L}_{\text{LM}} = -\sum_{t=1}^{T} \log P(x_t \mid x_1, \dots, x_{t-1}) $$
MLM は全トークンの 15% のみを予測するため、1エポックあたりの「学習シグナル」は自己回帰モデルより少ないです。しかし、双方向の文脈を利用できるため、同じ量のシグナルからより豊かな表現を学習できます。
事前学習タスク 2: Next Sentence Prediction(NSP)
定式化
NSP は、2つの文 A と B が連続した文であるか否かを判定する二値分類タスクです。
入力は [CLS] sentence_A [SEP] sentence_B [SEP] の形式で、ラベルは
$$ y = \begin{cases} \text{IsNext} & \text{文 B が文 A の次の文である場合} \\ \text{NotNext} & \text{文 B がランダムに選ばれた文である場合} \end{cases} $$
[CLS] トークンの最終層の隠れ状態 $\bm{h}_{\text{[CLS]}}^{(L)}$ を用いて
$$ P(y = \text{IsNext} \mid \bm{h}_{\text{[CLS]}}^{(L)}) = \sigma(\bm{w}_{\text{NSP}}^\top \bm{h}_{\text{[CLS]}}^{(L)} + b_{\text{NSP}}) $$
NSP の損失は二値交差エントロピーです。
$$ \mathcal{L}_{\text{NSP}} = -[y \log P(\text{IsNext}) + (1 – y) \log (1 – P(\text{IsNext}))] $$
全体の事前学習損失
$$ \mathcal{L}_{\text{pretrain}} = \mathcal{L}_{\text{MLM}} + \mathcal{L}_{\text{NSP}} $$
NSP に関する後続研究の知見: RoBERTa(Liu et al., 2019)は NSP タスクを除去しても性能が向上することを示しました。代わりに、より長い系列での学習やダイナミックマスキングが有効であることが報告されています。
各レイヤーが学習する言語的特徴
BERT の各レイヤーが何を学習しているかについて、多くの分析研究(probing study)が行われています。
階層的な言語特徴
| レイヤー位置 | 学習される特徴 | 具体例 |
|---|---|---|
| 下位層(1–4) | 表層的・局所的特徴 | 品詞タグ、形態素情報 |
| 中位層(5–8) | 構文的特徴 | 係り受け関係、構文木 |
| 上位層(9–12) | 意味的特徴 | 意味的役割、含意関係 |
アテンションパターンの分析
- 下位層: 隣接トークンや句読点に強くアテンション
- 中位層: 構文的に関連するトークン(主語-動詞、修飾語-被修飾語)にアテンション
- 上位層: 意味的に関連する離れたトークンにアテンション
- [CLS] トークン: 上位層で全てのトークンから情報を集約
数学的な見方
$l$ 番目のレイヤーのアテンション重み行列を $\bm{A}^{(l)} \in \mathbb{R}^{T \times T}$ とすると、$A_{ij}^{(l)}$ はトークン $i$ がトークン $j$ にどれだけ注意を向けるかを表します。Clark et al. (2019) は、特定のヘッドがほぼ決定的に特定の構文関係に対応するアテンションパターンを示すことを報告しています。
ファインチューニング戦略の概要
BERT の事前学習済みモデルを下流タスクに適用する際の基本的な枠組みを説明します。
タスク別の出力ヘッド
| 下流タスク | 入力形式 | 出力 |
|---|---|---|
| テキスト分類 | [CLS] text [SEP] | [CLS] の隠れ状態 → 分類ヘッド |
| 文ペア分類 | [CLS] text_A [SEP] text_B [SEP] | [CLS] の隠れ状態 → 分類ヘッド |
| 固有表現認識 | [CLS] text [SEP] | 各トークンの隠れ状態 → トークン分類ヘッド |
| 質問応答 | [CLS] question [SEP] context [SEP] | 各トークンの隠れ状態 → span 予測ヘッド |
テキスト分類の場合、[CLS] トークンの最終隠れ状態に対して
$$ P(y = c \mid \bm{x}) = \text{softmax}(\bm{W}_{\text{cls}} \bm{h}_{\text{[CLS]}}^{(L)} + \bm{b}_{\text{cls}})_c $$
という分類ヘッドを追加し、タスク固有の損失で全パラメータ(BERT + 分類ヘッド)を更新します。
Python で huggingface transformers を使った BERT の推論
マスク予測(MLM)
import torch
from transformers import BertTokenizer, BertForMaskedLM
# モデルとトークナイザの読み込み
model_name = "bert-base-uncased"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForMaskedLM.from_pretrained(model_name)
model.eval()
# マスクを含む入力文
text = "The cat sat on the [MASK]."
inputs = tokenizer(text, return_tensors="pt")
print(f"入力テキスト: {text}")
print(f"トークン: {tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])}")
# マスク位置のインデックスを取得
mask_token_id = tokenizer.mask_token_id
mask_positions = (inputs['input_ids'] == mask_token_id).nonzero(as_tuple=True)[1]
# 推論
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits # (batch_size, seq_len, vocab_size)
# マスク位置の予測トップ5
for pos in mask_positions:
pos = pos.item()
probs = torch.softmax(logits[0, pos], dim=-1)
top5 = torch.topk(probs, 5)
print(f"\nマスク位置 {pos} の予測トップ5:")
for i in range(5):
token = tokenizer.convert_ids_to_tokens([top5.indices[i].item()])[0]
prob = top5.values[i].item()
print(f" {token}: {prob:.4f}")
文埋め込みの抽出
import torch
import numpy as np
import matplotlib.pyplot as plt
from transformers import BertTokenizer, BertModel
# モデルの読み込み
model_name = "bert-base-uncased"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name, output_hidden_states=True)
model.eval()
def get_sentence_embedding(text, pooling="cls"):
"""文埋め込みを抽出する"""
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
with torch.no_grad():
outputs = model(**inputs)
if pooling == "cls":
# [CLS] トークンの最終層の隠れ状態
return outputs.last_hidden_state[:, 0, :].numpy()
elif pooling == "mean":
# 全トークンの平均([PAD] を除外)
attention_mask = inputs['attention_mask'].unsqueeze(-1)
token_embeddings = outputs.last_hidden_state
sum_embeddings = (token_embeddings * attention_mask).sum(dim=1)
count = attention_mask.sum(dim=1)
return (sum_embeddings / count).numpy()
# テスト文
sentences = [
"The cat is sleeping on the sofa.",
"A kitten is resting on the couch.",
"The stock market crashed yesterday.",
"Financial markets experienced a downturn.",
"I love playing basketball.",
"Basketball is my favorite sport.",
]
# 文埋め込みの計算
embeddings = np.vstack([get_sentence_embedding(s, pooling="mean") for s in sentences])
# コサイン類似度行列の計算
def cosine_sim_matrix(emb):
norms = np.linalg.norm(emb, axis=1, keepdims=True)
normalized = emb / (norms + 1e-10)
return normalized @ normalized.T
sim_matrix = cosine_sim_matrix(embeddings)
# 類似度行列の可視化
plt.figure(figsize=(8, 6))
plt.imshow(sim_matrix, cmap='Blues', vmin=0.5, vmax=1.0)
plt.colorbar(label='Cosine Similarity')
# ラベルを短縮
short_labels = [s[:30] + "..." if len(s) > 30 else s for s in sentences]
plt.xticks(range(len(sentences)), short_labels, rotation=45, ha='right', fontsize=8)
plt.yticks(range(len(sentences)), short_labels, fontsize=8)
plt.title("BERT Sentence Embedding Similarity")
plt.tight_layout()
plt.show()
# 類似度の数値確認
print("=== 文間のコサイン類似度 ===")
for i in range(len(sentences)):
for j in range(i + 1, len(sentences)):
print(f"sim({i}, {j}) = {sim_matrix[i, j]:.4f} | '{sentences[i][:40]}' vs '{sentences[j][:40]}'")
各レイヤーの隠れ状態の分析
import torch
import numpy as np
import matplotlib.pyplot as plt
from transformers import BertTokenizer, BertModel
# モデルの読み込み(全レイヤーの隠れ状態を出力)
model_name = "bert-base-uncased"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name, output_hidden_states=True)
model.eval()
# 入力テキスト
text = "The bank by the river is beautiful."
inputs = tokenizer(text, return_tensors="pt")
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
# 推論(全レイヤーの隠れ状態を取得)
with torch.no_grad():
outputs = model(**inputs)
# hidden_states: (num_layers + 1, batch, seq_len, hidden_dim)
hidden_states = outputs.hidden_states
num_layers = len(hidden_states) - 1 # 埋め込み層を除く
print(f"レイヤー数: {num_layers}")
print(f"トークン: {tokens}")
# 各レイヤー間のコサイン類似度(レイヤーごとの表現変化)
layer_sims = []
for l in range(1, num_layers + 1):
h_prev = hidden_states[l - 1][0].numpy() # (seq_len, hidden_dim)
h_curr = hidden_states[l][0].numpy()
# 各トークンのコサイン類似度の平均
sims = []
for t in range(len(tokens)):
cos_sim = np.dot(h_prev[t], h_curr[t]) / (
np.linalg.norm(h_prev[t]) * np.linalg.norm(h_curr[t]) + 1e-10)
sims.append(cos_sim)
layer_sims.append(np.mean(sims))
plt.figure(figsize=(8, 5))
plt.plot(range(1, num_layers + 1), layer_sims, 'o-', color='steelblue')
plt.xlabel("Layer")
plt.ylabel("Average Cosine Similarity with Previous Layer")
plt.title("Representation Change Across BERT Layers")
plt.grid(True, alpha=0.3)
plt.xticks(range(1, num_layers + 1))
plt.tight_layout()
plt.show()
BERT のパラメータ数の計算
BERT-Base のパラメータ数を具体的に計算してみましょう。
トークン埋め込み: $30522 \times 768 = 23{,}440{,}896$
位置埋め込み: $512 \times 768 = 393{,}216$
セグメント埋め込み: $2 \times 768 = 1{,}536$
各 Transformer レイヤー(12層分): – Multi-Head Attention: $4 \times (768 \times 768) = 2{,}359{,}296$(Q, K, V, O の重み行列) – バイアス: $4 \times 768 = 3{,}072$ – FFN: $768 \times 3072 + 3072 + 3072 \times 768 + 768 = 4{,}722{,}432$ – LayerNorm: $2 \times (768 + 768) = 3{,}072$ – 合計(1層): $\approx 7{,}087{,}872$ – 12層合計: $\approx 85{,}054{,}464$
Pooler: $768 \times 768 + 768 = 590{,}592$
合計: $\approx 109{,}480{,}704 \approx 110\text{M}$
まとめ
本記事では、BERT のアーキテクチャと事前学習について解説しました。
- BERT の位置づけ: Transformer Encoder を積層した双方向モデルであり、MLM タスクにより真の双方向文脈を学習します。ELMo(独立な双方向 LSTM)や GPT(左→右の自己回帰)の限界を克服しました
- アーキテクチャ: Multi-Head Self-Attention + FFN + 残差接続 + LayerNorm の構造を $L$ 層積層。BERT-Base は 110M パラメータ
- 入力表現: トークン埋め込み + セグメント埋め込み + 位置埋め込みの和。[CLS], [SEP], [MASK] の特殊トークンを使用
- MLM: 入力の 15% をマスクして予測。80/10/10 ルールにより事前学習とファインチューニングのミスマッチを軽減
- NSP: 2文の連続性を判定する二値分類。後続研究(RoBERTa)では不要とされた
- 各レイヤーの役割: 下位層で構文的特徴、上位層で意味的特徴を学習する階層構造
- 実装: huggingface transformers を用いたマスク予測、文埋め込み抽出、レイヤー分析を実装
次のステップとして、以下の記事も参考にしてください。