🤖

GPT-2 で理解する Transformer の入出力 - データ構造から読み解く仕組み

に公開

はじめに

この記事では、OpenAI の GPT-2 モデルを使って、Transformer の入出力を データ構造 の観点から理解していきます。実際にコードを動かしながら、テキストがどのように処理されて「次の単語」を予測するのかを追いかけてみましょう。

Transformer とは?

Transformer の誕生

Transformer は、2017 年に Google が発表した論文「Attention Is All You Need」で提案されたモデルアーキテクチャです。それまでの RNN(再帰型ニューラルネットワーク)に代わり、Attention(注意機構) を中心に据えた設計が特徴です。

GPT-2 とは?

GPT-2(Generative Pre-trained Transformer 2)は、OpenAI が 2019 年に公開した言語モデルです。大量のテキストデータで事前学習されており、次の単語を予測する タスクに特化しています。

環境セットアップ

必要なライブラリ

import torch
from transformers import GPT2Tokenizer, GPT2Model, GPT2LMHeadModel
import matplotlib.pyplot as plt
import numpy as np

主なライブラリ:

  • transformers: Hugging Face が提供する Transformer モデルのライブラリ
  • torch: PyTorch(深層学習フレームワーク)
  • matplotlib: グラフ描画ライブラリ

GPT-2 モデルのロード

# トークナイザーとモデルをロード
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2Model.from_pretrained('gpt2', attn_implementation="eager")
model_lm = GPT2LMHeadModel.from_pretrained('gpt2', attn_implementation="eager")

# 推論モードに設定(学習はしない)
model.eval()
model_lm.eval()

ここでは 2 種類のモデルをロードしています:

  • GPT2Model: Transformer の基本モデル(隠れ状態を出力)
  • GPT2LMHeadModel: 言語モデル用(次の単語の確率を出力)

GPT-2 の基本スペック

config = model.config
print(f"語彙サイズ: {config.vocab_size}")
print(f"隠れ層の次元: {config.n_embd}")
print(f"Transformer レイヤー数: {config.n_layer}")
print(f"Attention ヘッド数: {config.n_head}")
print(f"最大シーケンス長: {config.n_positions}")
項目 説明
vocab_size 50257 語彙数(モデルが知っている「単語」の数)
hidden_size 768 内部表現の次元数(各トークンを 768 次元のベクトルで表現)
n_layer 12 Transformer ブロックの層数
n_head 12 Attention ヘッドの数(異なる観点で文脈を捉える)
max_position 1024 一度に処理できる最大トークン数

Tokenizer: テキストを数字に変換する

なぜ数字に変換するのか?

コンピュータは文字列をそのまま理解できません。数学的な演算を行うためには、テキストを 数字の配列 に変換する必要があります。この変換を行うのが Tokenizer(トークナイザー) です。

トークン化の流れ

テキストからモデルへの入力までの流れは以下の通りです:

テキスト → トークン(部分文字列) → Token ID(数字)

実際に見てみましょう:

text = "Hello, how are you?"

# テキストをトークンに分割
tokens = tokenizer.tokenize(text)
print(f"元のテキスト: {text}")
print(f"トークン列: {tokens}")
print(f"トークン数: {len(tokens)}")

出力:

元のテキスト: Hello, how are you?
トークン列: ['Hello', ',', ' how', ' are', ' you', '?']
トークン数: 6

GPT-2 は BPE(Byte Pair Encoding) というアルゴリズムでトークン化を行います。単語単位ではなく、よく出現する文字列のパターンを学習して分割するため、「how」が「 how」(先頭にスペース付き)のようになることがあります。

Token ID への変換

# トークンを Token ID に変換
token_ids = tokenizer.encode(text)
print(f"Token IDs: {token_ids}")

# 各トークンと ID の対応
for token, token_id in zip(tokens, token_ids):
    print(f"  '{token}' -> {token_id}")

出力:

Token IDs: [15496, 11, 703, 389, 345, 30]
  'Hello' -> 15496
  ',' -> 11
  ' how' -> 703
  ' are' -> 389
  ' you' -> 345
  '?' -> 30

各トークンには一意の ID(0 〜 50256 の整数)が割り当てられています。

モデル入力用の形式

実際にモデルに入力する際は、tokenizer() を直接呼び出すのが便利です:

inputs = tokenizer(text, return_tensors='pt')

print(f"input_ids: {inputs['input_ids']}")
print(f"input_ids.shape: {inputs['input_ids'].shape}")  # (batch_size, sequence_length)
print(f"attention_mask: {inputs['attention_mask']}")

出力:

input_ids: tensor([[15496,    11,   703,   389,   345,    30]])
input_ids.shape: torch.Size([1, 6])  # (バッチサイズ=1, シーケンス長=6)
attention_mask: tensor([[1, 1, 1, 1, 1, 1]])
  • input_ids: Token ID のテンソル(2 次元配列)
  • attention_mask: 各トークンを Attention 計算に含めるかどうか(1 = 含める)

Embedding: 数字をベクトルに変換する

なぜベクトルにするのか?

Token ID は単なる整数で、数字自体には意味がありません。例えば、「cat」が 100 で「dog」が 200 だとしても、200 が 100 より「大きい」わけではありません。

そこで、各トークンを 高次元のベクトル(768 次元) に変換します。このベクトルは「単語の意味」を数学的に表現しており、似た意味の単語は近いベクトルになるよう学習されています。

トークン埋め込みと位置埋め込み

GPT-2 では 2 種類の埋め込み(Embedding)を使います:

# トークン埋め込み層
print(f"トークン埋め込み (wte): {model.wte.weight.shape}")  # (50257, 768)

# 位置埋め込み層
print(f"位置埋め込み (wpe): {model.wpe.weight.shape}")  # (1024, 768)
埋め込み 形状 役割
wte (Word Token Embedding) (50257, 768) 各トークンの「意味」を表すベクトル
wpe (Word Position Embedding) (1024, 768) 各位置の「文中での場所」を表すベクトル

埋め込みの計算

最終的な入力は、トークン埋め込みと位置埋め込みを 足し合わせて 作ります:

text = "Hello world"
inputs = tokenizer(text, return_tensors='pt')
input_ids = inputs['input_ids']

# トークン埋め込みを取得
token_embeddings = model.wte(input_ids)
print(f"トークン埋め込み: {token_embeddings.shape}")  # (1, 2, 768)

# 位置 ID を作成
seq_length = input_ids.shape[1]
position_ids = torch.arange(seq_length).unsqueeze(0)  # [[0, 1]]

# 位置埋め込みを取得
position_embeddings = model.wpe(position_ids)
print(f"位置埋め込み: {position_embeddings.shape}")  # (1, 2, 768)

# 最終的な入力 = トークン埋め込み + 位置埋め込み
final_embeddings = token_embeddings + position_embeddings
print(f"最終入力: {final_embeddings.shape}")  # (1, 2, 768)

この final_embeddings が Transformer ブロックへの入力となります。

モデルの出力: 次の単語を予測する

基本モデルの出力

まずは基本モデル(GPT2Model)の出力を確認しましょう:

text = "The quick brown fox"
inputs = tokenizer(text, return_tensors='pt')

with torch.no_grad():  # 勾配計算をオフ(推論時)
    outputs = model(**inputs)

print(f"入力形状: {inputs['input_ids'].shape}")  # (1, 4)
print(f"出力形状: {outputs.last_hidden_state.shape}")  # (1, 4, 768)

出力:

入力形状: torch.Size([1, 4])  # (バッチサイズ=1, トークン数=4)
出力形状: torch.Size([1, 4, 768])  # (バッチ, シーケンス, 隠れ次元)

last_hidden_state は、各トークンの 文脈を考慮した表現 です。入力時の埋め込みは単語単独の意味でしたが、Transformer を通過することで周囲の単語の情報が反映されます。

言語モデルの出力(Logits)

次に、言語モデル(GPT2LMHeadModel)の出力を見てみましょう:

with torch.no_grad():
    outputs_lm = model_lm(**inputs)

print(f"logits 形状: {outputs_lm.logits.shape}")  # (1, 4, 50257)

出力:

logits 形状: torch.Size([1, 4, 50257])  # (バッチ, シーケンス, 語彙サイズ)

logits は、各位置での「次に来る単語の確率(未正規化)」を表します。最後の次元が 50257 なのは、語彙にある全単語それぞれのスコアを持っているためです。

次の単語を予測してみる

「The quick brown fox」の続きを予測してみましょう:

# 最後の位置の logits を取得
last_logits = outputs_lm.logits[0, -1, :]  # (50257,)

# softmax で確率に変換
probs = torch.softmax(last_logits, dim=-1)

# 上位 5 件を表示
top_k = 5
top_probs, top_ids = torch.topk(probs, top_k)

print(f"'{text}' の次に来る可能性が高い単語:")
for prob, token_id in zip(top_probs, top_ids):
    token = tokenizer.decode([token_id])
    print(f"  '{token}': {prob.item():.4f}")

出力例:

'The quick brown fox' の次に来る可能性が高い単語:
  ' jumps': 0.1842
  ' jumped': 0.0821
  ',': 0.0534
  ' is': 0.0312
  ' runs': 0.0287

「The quick brown fox jumps over the lazy dog」という有名なフレーズを学習しているため、「jumps」が最も確率が高くなっています。

Attention: どこに注目しているか

Attention とは?

Attention(注意機構)は、Transformer の核となる仕組みです。文中の各単語が、他のどの単語に注目しているか を計算します。

例えば、「The cat sat on the mat」という文で「sat」という単語を処理するとき、Attention によって「cat」に強く注目することで、「誰が座ったのか」という情報を取り込むことができます。

Attention Weights の取得

Attention の重みを可視化してみましょう:

text = "The cat sat on the mat"
inputs = tokenizer(text, return_tensors='pt')

with torch.no_grad():
    outputs = model(**inputs, output_attentions=True)

# Attention weights を取得
attentions = outputs.attentions
print(f"レイヤー数: {len(attentions)}")
print(f"各レイヤーの形状: {attentions[0].shape}")

出力:

レイヤー数: 12
各レイヤーの形状: torch.Size([1, 12, 7, 7])  # (バッチ, ヘッド数, シーケンス, シーケンス)

Attention の形状 (1, 12, 7, 7) の意味:

  • 1: バッチサイズ
  • 12: Attention ヘッドの数(異なる観点で注目パターンを学習)
  • 7 × 7: 7 トークン同士の注目度合い(行 = Query、列 = Key)

ヒートマップで可視化

ヒートマップの見方:

  • 横軸(Key): 注目される側のトークン
  • 縦軸(Query): 注目する側のトークン
  • 色の濃さ: 注目度合い(濃いほど強く注目)

Causal Attention(因果的注意)

GPT-2 は 自己回帰モデル なので、各トークンは 自分より前のトークンにしか注目できません。これを Causal Attention(または Masked Self-Attention)と呼びます。

ヒートマップを見ると、対角線より上(未来のトークン)の値がすべて 0 になっていることが分かります。

attn = attentions[0][0, 0].numpy()  # Layer 0, Head 0
print("Attention 行列(下三角のみ有効):")
print(np.round(attn, 2))

これにより、モデルは「カンニング」せずに、過去の情報のみから次の単語を予測することができます。

複数ヘッドの Attention パターン

GPT-2 には 12 個の Attention ヘッドがあり、それぞれ異なるパターンで注目します:

各ヘッドの役割(例):

  • 直前のトークンに注目: 局所的な文法関係を捉える
  • 文頭のトークンに注目: 文全体の主題を捉える
  • 特定の品詞に注目: 動詞と主語の関係を捉える

複数のヘッドを持つことで、多様な観点から文脈を理解できます。

入力長とバッチ処理

入力長による出力形状の変化

Transformer の出力は、入力の長さに応じて変わります:

short_text = "Hi"
long_text = "The quick brown fox jumps over the lazy dog"

inputs_short = tokenizer(short_text, return_tensors='pt')
inputs_long = tokenizer(long_text, return_tensors='pt')

print(f"短いテキスト: トークン数 = {inputs_short['input_ids'].shape[1]}")
print(f"長いテキスト: トークン数 = {inputs_long['input_ids'].shape[1]}")

with torch.no_grad():
    outputs_short = model_lm(**inputs_short)
    outputs_long = model_lm(**inputs_long)

print(f"短い出力: {outputs_short.logits.shape}")  # (1, 1, 50257)
print(f"長い出力: {outputs_long.logits.shape}")   # (1, 10, 50257)

最大シーケンス長

GPT-2 は最大 1024 トークンまでしか処理できません。それを超える場合は切り詰め(truncation)が必要です:

very_long_text = "Hello " * 600  # 非常に長いテキスト

inputs = tokenizer(
    very_long_text,
    return_tensors='pt',
    truncation=True,  # 切り詰めを有効化
    max_length=1024   # 最大長を指定
)
print(f"切り詰め後: {inputs['input_ids'].shape[1]} トークン")

バッチ処理とパディング

複数のテキストを同時に処理する場合、長さを揃える必要があります:

# GPT-2 にはデフォルトで pad_token がないので設定
tokenizer.pad_token = tokenizer.eos_token

texts = [
    "Hello",
    "Hello, how are you?",
    "The quick brown fox jumps over the lazy dog"
]

# バッチ処理(パディングあり)
batch_inputs = tokenizer(texts, return_tensors='pt', padding=True)

print(f"バッチ形状: {batch_inputs['input_ids'].shape}")  # (3, 10)
print(f"Attention Mask:\n{batch_inputs['attention_mask']}")

出力:

バッチ形状: torch.Size([3, 10])  # 3 つのテキスト、最大 10 トークン
Attention Mask:
tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # 実際は 1 トークン、残りパディング
        [1, 1, 1, 1, 1, 1, 0, 0, 0, 0],  # 6 トークン
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]) # 10 トークン(最長)

attention_mask の役割:

  • 1: 実際のトークン(Attention 計算に含める)
  • 0: パディングトークン(無視する)

まとめ: データの流れ全体像

テキストから次の単語予測までのデータの流れを整理しましょう:

テキスト
   ↓ Tokenizer
Token ID: (Batch, Sequence)
   ↓ Embedding
埋め込み: (Batch, Sequence, Hidden=768)
   ↓ Transformer × 12 層
Hidden State: (Batch, Sequence, Hidden=768)
   ↓ LM Head
Logits: (Batch, Sequence, Vocab=50257)
   ↓ Softmax
確率分布 → 次の単語を予測

主要なデータ構造まとめ

データ 形状 説明
input_ids (Batch, Sequence) トークンの ID 列
attention_mask (Batch, Sequence) パディングのマスク
token_embeddings (Batch, Sequence, 768) トークンの意味ベクトル
position_embeddings (Batch, Sequence, 768) 位置のベクトル
last_hidden_state (Batch, Sequence, 768) 文脈を考慮した表現
logits (Batch, Sequence, 50257) 次の単語の確率(未正規化)
attentions (Batch, 12, Sequence, Sequence) Attention の重み

参考リンク

GitHubで編集を提案

Discussion