AI開発の最前線において、モデルの内部構造を深く理解することの重要性は日に日に高まっています。ビジネスの現場でも開発の現場でも、よく交わされる言葉があります。
"Don't trust magic."(魔法を信じるな)
Hugging FaceのTransformersは、2025年1月にv5.0.0へのメジャーアップデートを果たしました。複数の公式情報によると、この約5年ぶりの大規模刷新では内部設計のモジュール化が推進され、8ビットや4ビットの量子化が第一級の概念としてサポートされるなど、実用的なアーキテクチャの再設計が行われています。ここで特筆すべきは、PyTorchが主要フレームワークとして明確に位置づけられ、TensorFlowとFlaxのサポートが正式に終了した点です。
from transformers import BertModel と書けば、誰でも最先端のAIモデルを呼び出せます。プロトタイプを素早く構築し、仮説を即座に形にして検証する上では、これは非常に強力な武器です。しかし、中身がブラックボックスのまま「魔法」として使っていると、いざビジネス要件に合わせて自社データでファインチューニングを行いたいときや、予期せぬエラーに直面したときに、手も足も出なくなってしまいます。また、最新環境への移行ガイドを確認し、変更されたAPIや廃止された機能の代替手段を検討する際にも、基礎的な仕組みの把握が欠かせません。技術の本質を見抜くことこそが、実はプロジェクト成功への最短距離なのです。
「なぜモデルが収束しないのか?」
「このハイパーパラメータは何を意味しているのか?」
「内部のAttentionモジュールをどう最適化すべきか?」
こうした疑問に答えるには、一度ボンネットを開けて、エンジンを分解してみる必要があります。数式を眺めるだけでは不十分です。エンジニアにとっての共通言語、すなわち「コード」を通じて理解を深めることが最も確実なアプローチです。
この記事では、生成AIの心臓部であるTransformer、特にその核心となるSelf-Attentionメカニズムを、Transformers v5の主要フレームワークとなったPyTorchの基本機能だけでゼロから実装します。理論だけでなく「実際にどう動くか」を重視し、複雑な数式は最小限に留め、データの流れ(テンソルの形状)を追いながら、この技術の革新性を紐解いていきましょう。
なぜ「Attention」だけでいいのか?技術的革新の核心
Googleの研究チームが2017年に発表した論文『Attention Is All You Need』は、AIの歴史における決定的な転換点でした。それまで自然言語処理(NLP)のデファクトスタンダードだったRNN(リカレントニューラルネットワーク)やLSTM(長短短期記憶)に対し、複雑な再帰構造を排して「Attention(注意機構)のみで十分である」と提唱したのです。
RNN/LSTMが抱えていた「並列化」と「長期記憶」の課題
かつての主力であったRNNやLSTMは、データを時系列順に一つずつ処理するアーキテクチャでした。これは人間が文章を読むプロセスに近い直感的な手法でしたが、大規模化するAI開発においては2つの致命的なボトルネックとなっていました。
- 逐次処理による計算の非効率性: 前のタイムステップの計算結果を待つ必要があるため、GPUの強力な並列演算能力を活かせず、学習時間が膨大になるという構造的な弱点がありました。
- 長期依存性の喪失: シーケンス(文章)が長くなると、初期の情報が伝播する過程で徐々に薄れてしまい、離れた単語間の文脈を維持することが困難でした。
現在、RNNやLSTM自体は時系列データの解析など特定のニッチな領域で活用され続けていますが、言語モデルの基盤としては、これらの課題を克服したTransformerアーキテクチャへの移行が完了しています。現代のNLP開発において、RNNベースのアプローチが新規に採用されるケースは極めて稀と言えるでしょう。
Transformerがもたらした「全トークン同時参照」というパラダイムシフト
Transformerの革新性は、シーケンス制御(順番に読むこと)を廃止し、すべてのトークンを同時に処理する点にあります。入力された文章全体を一括でGPUメモリに展開し、並列計算を行うのです。
ここで鍵となるのがSelf-Attention(自己注意機構)です。
これは、入力された文章中のある単語が、他のすべての単語と「どの程度関連しているか」を計算する仕組みです。例えば、「彼」という代名詞が、文脈上の「ジョン」を指すのか、それとも「その犬」を指すのか。Self-Attentionは全単語間の相互関係(Attention Weight)を並列計算することで、単語間の距離に関係なく正確な文脈を捉えます。
このアーキテクチャにより計算効率は劇的に向上し、大規模言語モデル(LLM)の高度化を牽引しています。複数の公式情報によれば、OpenAIのモデル展開においても、GPT-4oやGPT-4.1といったレガシーモデルは利用率の低下(0.1%未満)に伴い2026年2月13日に廃止され、長い文脈理解やツール実行、画像理解などの汎用知能が大幅に向上したGPT-5.2(InstantおよびThinking)が新たな標準モデルへと移行しています。旧モデルから新モデルへの進化において、文脈適応能力や長文処理が飛躍的に向上しているのも、根底にあるSelf-Attentionの並列処理能力と情報伝播の効率性が極限まで引き出されている結果です。
ブラックボックス化しがちなこの仕組みですが、数式とコードに落とし込めば、実は非常にシンプルな行列演算の集合体であることがわかります。
概念の整理はここまでにして、実際にコードを書いて実装の詳細に迫りましょう。まずは動くものを作ることが、理解への第一歩です。
実装環境のセットアップとデータ準備
まずは、開発環境を整えます。今回の実装では、PyTorchの基本機能(テンソル操作とニューラルネットワークモジュール)のみを使用します。複雑なトークナイザー(文章を数値に変換するツール)や外部のデータセットは使用せず、構造理解に集中するためにランダムな数値データで入力をシミュレーションします。
ライブラリの準備と環境設定
PyTorchは頻繁にアップデートされており、最新バージョンではCUDAやROCmへの対応強化、推論の最適化(FP8サポートなど)が進んでいますが、Transformerの基本構造を理解する上では標準的なインストールで十分です。
ただし、GPUを使用する場合は、お手持ちの環境(CUDAバージョンなど)とPyTorchのビルドが一致していることが重要です。最新のインストール手順や対応バージョンについては、必ずPyTorch公式サイトで確認してください。
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
# 再現性確保のためのシード固定
torch.manual_seed(42)
入力データのベクトル化シミュレーション
Transformerに入力されるデータは、通常「埋め込み(Embedding)」と呼ばれる処理を経て、固定長のベクトルに変換されています。
ここでは、以下の設定でシミュレーションデータを作成します。
- batch_size (B): 2 (一度に処理する文章の数)
- seq_len (T): 4 (1つの文章に含まれる単語数)
- d_model (D): 8 (各単語を表すベクトルの次元数。実際は512や768などが使われますが、見やすくするために小さくします)
# ハイパーパラメータの設定
B, T, D = 2, 4, 8
# ランダムな入力テンソルを作成(Embedding後の状態を想定)
# 形状: [バッチサイズ, シーケンス長, 埋め込み次元]
x = torch.randn(B, T, D)
print("Input shape:", x.shape)
print(x)
この x が、これからTransformerの層を流れていくデータの実体です。常にこの [Batch, Time, Dimension] の形状を意識してください。
【核心実装】Self-Attentionメカニズムを1行ずつ書く
ここからが本番です。Self-Attentionの仕組みは、よくデータベースの検索に例えられます。
- Query (Q): 検索クエリ(何を探しているか?)
- Key (K): 索引(検索対象の特徴は?)
- Value (V): 値(実際に取り出したい情報は?)
入力された単語ベクトルそれぞれから、この3つのベクトルを作り出すところから始まります。
Query, Key, Value行列の生成ロジック
まずは、入力 x を線形変換して、Q, K, V を生成します。
# Q, K, V 用の線形層を定義
# バイアスは簡単のため省略する場合もありますが、ここでは含めます
w_q = nn.Linear(D, D)
w_k = nn.Linear(D, D)
w_v = nn.Linear(D, D)
# 線形変換を実行
Q = w_q(x) # Shape: [B, T, D]
K = w_k(x) # Shape: [B, T, D]
V = w_v(x) # Shape: [B, T, D]
print("Query shape:", Q.shape)
ドット積による類似度スコア(Attention Score)の算出
次に、「どの単語がどの単語に注目すべきか」を表すスコアを計算します。これは Query と Key の内積(ドット積)で求められます。
内積が大きいほど、ベクトル同士の向きが似ている、つまり「関連性が高い」ことを意味します。
# Q と K の転置行列との行列積を計算
# K.transpose(-2, -1) で最後の2次元を入れ替えます
# [B, T, D] @ [B, D, T] -> [B, T, T]
attn_scores = torch.matmul(Q, K.transpose(-2, -1))
print("Attention Scores shape:", attn_scores.shape)
# 出力は [2, 4, 4] になります。
# これは「バッチ内の各文章について、4つの単語×4つの単語の関係性スコア」を表します。
Scale & Softmax:確率分布への変換と勾配消失の防止
計算されたスコアは、次元数 d_model が大きくなると値が大きくなりすぎ、学習時の勾配が消えてしまう問題があります。これを防ぐため、次元数の平方根で割ってスケーリングします。
その後、softmax 関数を通して、合計が1になる「確率」に変換します。
# スケーリング(ルートDで割る)
scale = math.sqrt(D)
attn_scores = attn_scores / scale
# Softmaxで正規化(最後の次元=行ごとに適用)
# これで「各単語が、他のどの単語にどれくらい注目するか」の割合になります
attn_weights = F.softmax(attn_scores, dim=-1)
print("Attention Weights (first batch):\n", attn_weights[0])
# 各行の合計が 1.0 になっているはずです
重み付き和によるコンテキストベクトルの取得
最後に、計算した重み attn_weights を使って、Value (V) の加重平均をとります。これが、Self-Attention層の出力となります。
# 重みとValueの行列積
# [B, T, T] @ [B, T, D] -> [B, T, D]
output = torch.matmul(attn_weights, V)
print("Output shape:", output.shape)
これで、文脈を考慮した新しい単語ベクトルが得られました。入力と同じ [B, T, D] の形状に戻っている点に注目してください。
Multi-Head Attentionへの拡張と統合
先ほどの実装では、1つのAttentionメカニズムですべての関係性を捉えようとしました。しかし、言葉の関係性は複雑です。「誰が」という主語の関係もあれば、「いつ」という時間の関係もあります。
これらを同時に捉えるために、Multi-Head Attentionを使います。文字通り、Attentionを行う「ヘッド(視点)」を複数用意する手法です。
「視点」を増やす意味:複数のヘッド分割の実装
実装上のトリックは、巨大な行列を実際に複数作るのではなく、大きなベクトルを分割して扱うことです。
例えば d_model = 8 で n_heads = 2 の場合、各ヘッドは head_dim = 4 の次元を担当します。
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
self.d_model = d_model
self.n_heads = n_heads
self.head_dim = d_model // n_heads
# Q, K, V用の全結合層(まとめて定義)
self.w_q = nn.Linear(d_model, d_model)
self.w_k = nn.Linear(d_model, d_model)
self.w_v = nn.Linear(d_model, d_model)
# 最後に統合するための出力層
self.w_o = nn.Linear(d_model, d_model)
def forward(self, x):
B, T, D = x.shape
# 1. 線形変換
Q = self.w_q(x)
K = self.w_k(x)
V = self.w_v(x)
# 2. ヘッド分割 (Split & Transpose)
# [B, T, D] -> [B, T, n_heads, head_dim] -> [B, n_heads, T, head_dim]
# transposeにより、ヘッドごとに独立してAttention計算ができるように次元を入れ替えます
Q = Q.view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
K = K.view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
V = V.view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
# 3. Scaled Dot-Product Attention
# [B, n_heads, T, head_dim] @ [B, n_heads, head_dim, T] -> [B, n_heads, T, T]
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
attn_weights = F.softmax(scores, dim=-1)
# [B, n_heads, T, T] @ [B, n_heads, T, head_dim] -> [B, n_heads, T, head_dim]
out = torch.matmul(attn_weights, V)
# 4. 統合 (Concat)
# transposeで元に戻し、contiguous()でメモリ配置を整えてからreshape
# [B, n_heads, T, head_dim] -> [B, T, n_heads, head_dim] -> [B, T, D]
out = out.transpose(1, 2).contiguous().view(B, T, D)
# 5. 最後の線形変換
return self.w_o(out)
# 動作確認
mha = MultiHeadAttention(d_model=8, n_heads=2)
output = mha(x)
print("MHA Output shape:", output.shape)
この view と transpose の操作が、PyTorch実装における最大の難所であり、かつ最も美しい部分です。ループ処理を使わずにテンソル操作だけで並列計算を実現している点に注目してください。いかに効率的にハードウェアの性能を引き出すかという、エンジニアリングの醍醐味がここにあります。
仕上げ:Positional Encodingとフィードフォワード
Attentionメカニズムには一つだけ欠点があります。それは「入力順序を無視する」ことです。「私が犬を噛む」と「犬が私を噛む」を、単語の組み合わせだけで見ると区別できません。
順序情報を埋め込むためのサイン・コサイン関数
そこで、入力データに「位置情報」を足し合わせます。Transformerでは、サイン波とコサイン波を用いた独特なエンコーディングを使います。
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
# 位置情報の定数テーブルを作成
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# バッチ次元を追加して登録(学習パラメータではないのでbufferとして登録)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
# 入力xに位置情報を加算
return x + self.pe[:, :x.size(1)]
Transformerブロックとしてのクラス化
最後に、これらをまとめて1つのTransformerブロック(Encoder Layer)を構築します。ここでは、学習を安定させるための残差結合(Residual Connection)とレイヤー正規化(Layer Normalization)、そしてフィードフォワードネットワーク(FFN)を追加します。
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads, hidden_dim, dropout=0.1):
super().__init__()
self.attention = MultiHeadAttention(d_model, n_heads)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
# Feed Forward Network
self.ffn = nn.Sequential(
nn.Linear(d_model, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, d_model)
)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# 1. Attention + Residual + Norm
attn_out = self.attention(x)
x = self.norm1(x + self.dropout(attn_out))
# 2. FFN + Residual + Norm
ffn_out = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_out))
return x
# 最終確認
block = TransformerBlock(d_model=8, n_heads=2, hidden_dim=32)
final_output = block(x)
print("Final Transformer Block Output:", final_output.shape)
これで、Transformerの基本単位が完成しました!
次のステップ:ブラックボックスから信頼できるツールへ
お疲れ様でした。これであなたは、Transformerの中身が「魔法」ではなく、行列演算と確率計算の巧みな組み合わせであることを知っています。いかがでしたか?意外とシンプルだと感じたのではないでしょうか。
この基本構造を理解していれば、Hugging Faceなどのライブラリを使う際も視点が変わります。
num_attention_headsパラメータを変えるとき、「視点の細かさ」を調整していることがイメージできます。- 推論速度が遅いとき、
seq_lenの二乗で計算量が増えるAttentionの仕組みがボトルネックかもしれないと推測できます。 - ファインチューニングでLossが下がらないとき、LayerNormや学習率のスケジューリングを疑うことができます。
さらなる学習のために
今回実装したのはEncoder部分の基礎です。ここからGPT(Decoderのみ)やBERT(Encoderのみ)へと派生していきます。次は、実際のテキストデータを使って、小規模な翻訳モデルや文章生成モデルの学習にチャレンジしてみてください。
自らの手でコードを書き、データを流し、エラーと向き合うこと。それこそが、AIエンジニアとしての「直感」を磨き、ビジネスに直結する価値を生み出す最短ルートです。
まとめ
- Attentionの革新性: 逐次処理を廃し、全トークンの関係性を並列計算することで、学習効率と文脈理解力を飛躍的に向上させました。
- テンソル操作の重要性:
view,transpose,matmulを駆使した次元操作が実装の鍵です。形状(Shape)を常に意識しましょう。 - ブラックボックスの解消: 基本構造をスクラッチ実装することで、既存ライブラリのパラメータの意味やエラーの原因を深く理解できるようになります。
コメント