【PyTorch】セルフアテンション

トランスフォーマーやBERTなどの現代的な深層学習モデルにおいて、セルフアテンションは非常に重要なメカニズムです。この記事では、セルフアテンションの基本概念と、PyTorchを使った具体的な実装方法について解説します。

## セルフアテンションとは

セルフアテンションは、シーケンス内の各要素が同じシーケンス内の他の要素との関連性を考慮して重み付けを行うメカニズムです。これにより、特定の位置の要素が、シーケンス内の他のどの要素と関連が強いかを学習することができます。

従来のRNN（Recurrent Neural Network）やLSTM（Long Short-Term Memory）が時系列に沿って情報を処理するのに対し、セルフアテンションは全ての位置を一度に参照できるため、長距離依存関係の捕捉に優れています。

セルフアテンションの計算プロセスは以下の図のように表現できます：

```mermaid
flowchart TD
    subgraph "セルフアテンション計算"
        Input["入力テンソル X"] --> Q["クエリ (Q)"]
        Input --> K["キー (K)"]
        Input --> V["値 (V)"]
        
        Q & K --> MatMul["行列積 QK^T"]
        MatMul --> Scale["スケーリング ÷ sqrt(d_k)"]
        Scale --> SM["Softmax"]
        SM & V --> WV["重み付き和 (出力)"]
    end
    
    WV --> ResConn["残差接続<br />出力 + 入力"]
    ResConn --> LN["レイヤー正規化"]
    LN --> Output["最終出力"]
    
    classDef process fill:#bbf,stroke:#333,stroke-width:1px
    classDef dataflow fill:#dfd,stroke:#333,stroke-width:1px
    classDef io fill:#f9f,stroke:#333,stroke-width:2px
    
    class Q,K,V,MatMul,Scale,SM,WV process
    class ResConn,LN dataflow
    class Input,Output io
```

## PyTorchによるセルフアテンションの実装

以下はPyTorchを使用したセルフアテンションモジュールの基本的な実装です。

```python
class SelfAttention(nn.Module):
    """セルフアテンションモジュール"""
    
    def __init__(self, hidden_dim: int, num_heads: int = 4):
        """
        初期化
        
        Args:
            hidden_dim: 隠れ層の次元
            num_heads: アテンションヘッド数
        """
        super().__init__()
        self.attention = nn.MultiheadAttention(hidden_dim, num_heads)
        self.norm = nn.LayerNorm(hidden_dim)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        順伝播
        
        Args:
            x: 入力テンソル (batch_size, seq_len, hidden_dim)
            
        Returns:
            torch.Tensor: 出力テンソル (batch_size, seq_len, hidden_dim)
        """
        # 入力の順番を変更 (batch_size, seq_len, hidden_dim) -> (seq_len, batch_size, hidden_dim)
        x_t = x.transpose(0, 1)
        
        # セルフアテンションの適用
        attn_out, _ = self.attention(x_t, x_t, x_t)
        
        # 残差接続と正規化
        attn_out = attn_out + x_t
        attn_out = self.norm(attn_out)
        
        # 出力の順番を戻す (seq_len, batch_size, hidden_dim) -> (batch_size, seq_len, hidden_dim)
        return attn_out.transpose(0, 1)
```

この実装について、詳しく見ていきましょう。

## コード解説

### クラスの初期化

```python
def __init__(self, hidden_dim: int, num_heads: int = 4):
    super().__init__()
    self.attention = nn.MultiheadAttention(hidden_dim, num_heads)
    self.norm = nn.LayerNorm(hidden_dim)
```

ここでは2つの重要なコンポーネントを初期化しています：

1. **MultiheadAttention**: PyTorchの`nn.MultiheadAttention`クラスを使用しています。このクラスは複数のアテンションヘッドを並列に計算します。各ヘッドは異なる表現サブ空間に注目することができるため、モデルの表現能力が向上します。

2. **LayerNorm**: 正規化層として`nn.LayerNorm`を使用しています。これにより、ネットワークの学習が安定し、勾配消失や勾配爆発の問題を緩和できます。

### 順伝播（forward）の実装

```python
def forward(self, x: torch.Tensor) -> torch.Tensor:
    # 入力の順番を変更
    x_t = x.transpose(0, 1)
    
    # セルフアテンションの適用
    attn_out, _ = self.attention(x_t, x_t, x_t)
    
    # 残差接続と正規化
    attn_out = attn_out + x_t
    attn_out = self.norm(attn_out)
    
    # 出力の順番を戻す
    return attn_out.transpose(0, 1)
```

このメソッドでは以下の処理を行っています：

1. **テンソルの転置**: PyTorchの`MultiheadAttention`は入力形状として`(seq_len, batch_size, hidden_dim)`を期待するため、入力テンソル`x`の形状を`(batch_size, seq_len, hidden_dim)`から変換します。

2. **セルフアテンションの計算**: `self.attention`メソッドを同じテンソル`x_t`に対して3回（クエリ、キー、バリュー）適用します。これがセルフアテンションの本質的な部分です。このメソッドは注意重みも返しますが（2番目の戻り値）、ここでは使用していないので`_`として無視しています。

3. **残差接続**: 注意出力を元の入力に足し合わせます。これにより、勾配が直接伝播できるパスが提供され、深いネットワークの学習が容易になります。

4. **レイヤー正規化**: 注意出力と入力の和に対して正規化を適用します。これにより、モデルの学習が安定します。

5. **テンソルの再転置**: 出力テンソルを元の形状`(batch_size, seq_len, hidden_dim)`に戻します。

## マルチヘッドアテンションとは

このコードでは`num_heads`引数を使用してマルチヘッドアテンションを設定しています。マルチヘッドアテンションとは、同じ入力に対して複数のアテンション計算を並列に行い、異なる表現空間で関連性を学習する仕組みです。

例えば、4つのヘッドを持つマルチヘッドアテンションでは、隠れ層の次元を4分割し、各部分に対して別々のアテンション計算を行います。これにより、モデルは異なる観点から入力データの関係性を捉えることができます。

以下の図は、マルチヘッドアテンションの概念を示しています：

```mermaid
graph TD
    Input["入力テンソル\n(batch_size, seq_len, hidden_dim)"] --> Transpose["次元の転置\n(seq_len, batch_size, hidden_dim)"]
    
    Transpose --> HeadSplit["隠れ層の分割"]
    HeadSplit --> Head1["ヘッド 1"]
    HeadSplit --> Head2["ヘッド 2"]
    HeadSplit --> Head3["ヘッド 3"]
    HeadSplit --> Head4["ヘッド 4"]
    
    subgraph "マルチヘッドアテンション"
        Head1 --> Q1["クエリ 1"]
        Head1 --> K1["キー 1"]
        Head1 --> V1["値 1"]
        
        Q1 & K1 --> Score1["注意スコア計算\nQ·K^T / sqrt(d_k)"]
        Score1 --> Softmax1["Softmax"]
        Softmax1 & V1 --> WeightedSum1["重み付き和"]
        
        Head2 --> Q2["クエリ 2"]
        Head2 --> K2["キー 2"]
        Head2 --> V2["値 2"]
        
        Q2 & K2 --> Score2["注意スコア計算\nQ·K^T / sqrt(d_k)"]
        Score2 --> Softmax2["Softmax"]
        Softmax2 & V2 --> WeightedSum2["重み付き和"]
        
        Head3 --> Q3[" ... "]
        Head4 --> Q4[" ... "]
    end
    
    WeightedSum1 & WeightedSum2 & Q3 & Q4 --> Concat["出力の連結"]
    Concat --> ProjectionLayer["射影層"]
    Transpose -.-> |"残差接続"| AddNorm
    ProjectionLayer --> AddNorm["加算 & 正規化"]
    
    AddNorm --> OutputTranspose["次元の転置<br />(batch_size, seq_len, hidden_dim)"]
    OutputTranspose --> Output["出力テンソル<br />(batch_size, seq_len, hidden_dim)"]
    
    classDef inputOutput fill:#f9f,stroke:#333,stroke-width:2px
    classDef operation fill:#bbf,stroke:#333,stroke-width:1px
    classDef attention fill:#dfd,stroke:#333,stroke-width:1px
    
    class Input,Output inputOutput
    class Transpose,HeadSplit,Concat,ProjectionLayer,AddNorm,OutputTranspose operation
    class Head1,Head2,Head3,Head4,Q1,K1,V1,Q2,K2,V2,Q3,Q4,Score1,Score2,Softmax1,Softmax2,WeightedSum1,WeightedSum2 attention
```

## 残差接続と正規化の重要性

トランスフォーマーアーキテクチャの重要な特徴の一つに、残差接続（Residual Connection）と層正規化（Layer Normalization）があります。

```python
# 残差接続と正規化
attn_out = attn_out + x_t
attn_out = self.norm(attn_out)
```

残差接続は、入力信号が変換を経由せずに直接出力に影響を与えるショートカットパスを提供します。これにより、深いネットワークでも勾配が消失することなく効率的に学習できます。

https://qiita.com/yonaka15/items/7a057ebbc8447ecaf689

層正規化は、入力の平均と分散を調整して、学習プロセスを安定させる役割を果たします。これらの技術により、より深いネットワークを効果的に訓練することが可能になりました。

## 実際の使用例

このセルフアテンションモジュールは以下のように使用できます：

```python
# モデルのインスタンス化
hidden_dim = 256
num_heads = 4
self_attention = SelfAttention(hidden_dim, num_heads)

# 入力データの作成（バッチサイズ=32、シーケンス長=10、隠れ層次元=256）
batch_size = 32
seq_len = 10
x = torch.randn(batch_size, seq_len, hidden_dim)

# 順伝播の実行
output = self_attention(x)
print(output.shape)  # torch.Size([32, 10, 256])
```

## 応用と発展

このシンプルなセルフアテンション実装を基に、以下のような発展的な機能を追加することができます：

1. **位置エンコーディング**: シーケンスの位置情報を組み込むため
2. **ドロップアウト**: オーバーフィッティングを防ぐため
3. **フィードフォワードネットワーク**: トランスフォーマーの完全なブロックを構築するため
4. **マスキング**: 自己回帰モデルや可変長シーケンスの処理のため

## まとめ

セルフアテンションは現代の深層学習、特に自然言語処理において不可欠なメカニズムです。本記事では、PyTorchを使ってセルフアテンションモジュールを実装し、その内部動作について解説しました。

このシンプルな実装を理解することで、BERT、GPT、その他のトランスフォーマーベースのアーキテクチャがどのように動作するかの基礎を把握することができたのではないでしょうか