第2章:Transformerモデルによるプロセスデータ解析

Self-Attentionによる並列時系列処理と長期依存性の学習

📖 読了時間: 30-35分 💡 難易度: 上級 🔬 実例: 多変量プロセス予測

2.1 Self-Attention機構の基礎

Transformerの核となるSelf-Attention機構は、シーケンス内の全ての要素間の関係を並列に計算できます。RNN/LSTMと異なり、逐次処理が不要で、長距離依存性を直接捉えられます。

💡 Self-Attentionの利点

  • 並列処理: GPUで高速に計算可能
  • 長距離依存性: 距離に関わらず全要素にアクセス
  • 解釈性: Attention重みで要素間の関係を可視化

Self-Attentionの計算式:

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

ここで、\(Q\)(Query)、\(K\)(Key)、\(V\)(Value)は入力の線形変換です。

例1: Scaled Dot-Product Attention実装

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import math

class ScaledDotProductAttention(nn.Module):
    """Scaled Dot-Product Attention"""

    def __init__(self, temperature):
        """
        Args:
            temperature: スケーリング係数(通常は sqrt(d_k))
        """
        super(ScaledDotProductAttention, self).__init__()
        self.temperature = temperature

    def forward(self, q, k, v, mask=None):
        """
        Args:
            q: Query [batch, n_head, seq_len, d_k]
            k: Key [batch, n_head, seq_len, d_k]
            v: Value [batch, n_head, seq_len, d_v]
            mask: マスク(オプション)
        Returns:
            output: [batch, n_head, seq_len, d_v]
            attn: Attention重み [batch, n_head, seq_len, seq_len]
        """
        # Q・K^T の計算
        attn = torch.matmul(q, k.transpose(-2, -1)) / self.temperature

        # マスク適用(未来の情報を隠す等)
        if mask is not None:
            attn = attn.masked_fill(mask == 0, -1e9)

        # Softmaxで正規化
        attn = F.softmax(attn, dim=-1)

        # 重み付き和
        output = torch.matmul(attn, v)

        return output, attn

# 使用例(プロセス時系列データ)
batch_size = 2
seq_len = 50  # 50時刻分のデータ
d_model = 64  # 特徴次元

# サンプルデータ(反応器の温度・圧力・流量など)
x = torch.randn(batch_size, seq_len, d_model)

# Q, K, V を作成(この例では全て同じ)
q = k = v = x.unsqueeze(1)  # [batch, 1, seq_len, d_model] (1 head)

# Attention計算
attention = ScaledDotProductAttention(temperature=math.sqrt(d_model))
output, attn_weights = attention(q, k, v)

print(f"Output shape: {output.shape}")  # [2, 1, 50, 64]
print(f"Attention weights shape: {attn_weights.shape}")  # [2, 1, 50, 50]
print(f"Attention weights sum (should be 1.0): {attn_weights[0, 0, 0, :].sum():.4f}")

# 出力例:
# Output shape: torch.Size([2, 1, 50, 64])
# Attention weights shape: torch.Size([2, 1, 50, 50])
# Attention weights sum (should be 1.0): 1.0000

2.2 Positional Encoding(位置エンコーディング)

Transformerは並列処理のため、入力の順序情報を持ちません。時系列データでは時刻の順序が重要なので、Positional Encodingで位置情報を付加します。

正弦波ベースのPositional Encoding:

$$PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

$$PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

例2: Positional Encoding実装

class PositionalEncoding(nn.Module):
    """正弦波ベースの位置エンコーディング"""

    def __init__(self, d_model, max_len=5000):
        """
        Args:
            d_model: モデルの次元数
            max_len: 最大シーケンス長
        """
        super(PositionalEncoding, self).__init__()

        # 位置エンコーディングを事前計算
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()

        # 分母の計算
        div_term = torch.exp(torch.arange(0, d_model, 2).float() *
                            -(math.log(10000.0) / d_model))

        # sin/cosの適用
        pe[:, 0::2] = torch.sin(position * div_term)  # 偶数インデックス
        pe[:, 1::2] = torch.cos(position * div_term)  # 奇数インデックス

        pe = pe.unsqueeze(0)  # [1, max_len, d_model]
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        Args:
            x: 入力 [batch, seq_len, d_model]
        Returns:
            x + pe: 位置情報を付加 [batch, seq_len, d_model]
        """
        seq_len = x.size(1)
        x = x + self.pe[:, :seq_len, :]
        return x

# 使用例
d_model = 64
seq_len = 100
batch_size = 4

# プロセスデータ(温度の時系列など)
process_data = torch.randn(batch_size, seq_len, d_model)

# 位置エンコーディング適用
pos_encoder = PositionalEncoding(d_model=d_model, max_len=5000)
encoded_data = pos_encoder(process_data)

print(f"Input shape: {process_data.shape}")
print(f"Output shape: {encoded_data.shape}")

# 位置エンコーディングの可視化
import matplotlib.pyplot as plt

pe_matrix = pos_encoder.pe[0, :100, :].numpy()  # 最初の100時刻
plt.figure(figsize=(10, 4))
plt.imshow(pe_matrix.T, cmap='RdBu', aspect='auto')
plt.colorbar()
plt.xlabel('Position (Time Step)')
plt.ylabel('Dimension')
plt.title('Positional Encoding Pattern')
plt.tight_layout()
# plt.savefig('positional_encoding.png')

print("位置エンコーディング可視化完了")

# 出力例:
# Input shape: torch.Size([4, 100, 64])
# Output shape: torch.Size([4, 100, 64])
# 位置エンコーディング可視化完了

💡 学習可能な位置エンコーディング

固定の正弦波エンコーディングの代わりに、学習可能なembeddingも使用できます:

self.pos_embedding = nn.Parameter(torch.randn(1, max_len, d_model))

データ量が十分にある場合、こちらの方が性能が良いことがあります。

2.3 Multi-Head Attention

Multi-Head Attentionは、複数の異なるAttention機構を並列実行し、多様な特徴を捉えます。各ヘッドが異なる時間依存性パターンを学習できます。

例3: Multi-Head Attention実装

class MultiHeadAttention(nn.Module):
    """Multi-Head Attention"""

    def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
        """
        Args:
            n_head: ヘッド数
            d_model: モデル次元
            d_k: Keyの次元
            d_v: Valueの次元
        """
        super(MultiHeadAttention, self).__init__()

        self.n_head = n_head
        self.d_k = d_k
        self.d_v = d_v

        # Q, K, V の線形変換
        self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)

        # 出力の線形変換
        self.fc = nn.Linear(n_head * d_v, d_model, bias=False)

        self.attention = ScaledDotProductAttention(temperature=math.sqrt(d_k))
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, q, k, v, mask=None):
        """
        Args:
            q, k, v: [batch, seq_len, d_model]
        Returns:
            output: [batch, seq_len, d_model]
            attn: [batch, n_head, seq_len, seq_len]
        """
        d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
        batch_size, len_q, _ = q.size()
        len_k, len_v = k.size(1), v.size(1)

        residual = q

        # Q, K, V に線形変換を適用し、ヘッドごとに分割
        q = self.w_qs(q).view(batch_size, len_q, n_head, d_k).transpose(1, 2)
        k = self.w_ks(k).view(batch_size, len_k, n_head, d_k).transpose(1, 2)
        v = self.w_vs(v).view(batch_size, len_v, n_head, d_v).transpose(1, 2)

        # Attention計算
        q, attn = self.attention(q, k, v, mask=mask)

        # ヘッドを結合
        q = q.transpose(1, 2).contiguous().view(batch_size, len_q, -1)

        # 出力層
        q = self.dropout(self.fc(q))

        # Residual connection + Layer Normalization
        q = self.layer_norm(q + residual)

        return q, attn

# 使用例(プロセス多変量時系列)
batch_size = 4
seq_len = 50
d_model = 128
n_head = 8

# 反応器データ(温度、圧力、流量、濃度など)
process_seq = torch.randn(batch_size, seq_len, d_model)

# Multi-Head Attention適用
mha = MultiHeadAttention(n_head=n_head, d_model=d_model,
                         d_k=d_model//n_head, d_v=d_model//n_head)
output, attention = mha(process_seq, process_seq, process_seq)

print(f"Output shape: {output.shape}")  # [4, 50, 128]
print(f"Attention shape: {attention.shape}")  # [4, 8, 50, 50]

# 各ヘッドが異なるパターンを学習していることを確認
print("\nAttention weights variance per head:")
for h in range(n_head):
    var = attention[0, h].var().item()
    print(f"  Head {h+1}: variance = {var:.4f}")

# 出力例:
# Output shape: torch.Size([4, 50, 128])
# Attention shape: torch.Size([4, 8, 50, 50])
#
# Attention weights variance per head:
#   Head 1: variance = 0.0023
#   Head 2: variance = 0.0019
#   Head 3: variance = 0.0025
#   ...(各ヘッドで異なる分散)

2.4 Transformer Encoder

Transformer Encoderは、Multi-Head AttentionとFeed-Forward Networkを組み合わせた層を積み重ねます。プロセス変数の予測に使用します。

例4: Transformer Encoderによる予測

class TransformerEncoderLayer(nn.Module):
    """Transformer Encoder Layer"""

    def __init__(self, d_model, n_head, d_ff, dropout=0.1):
        """
        Args:
            d_model: モデル次元
            n_head: ヘッド数
            d_ff: Feed-Forward層の中間次元
        """
        super(TransformerEncoderLayer, self).__init__()

        # Multi-Head Attention
        self.self_attn = MultiHeadAttention(
            n_head=n_head, d_model=d_model,
            d_k=d_model//n_head, d_v=d_model//n_head
        )

        # Feed-Forward Network
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )

        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, x, mask=None):
        """
        Args:
            x: [batch, seq_len, d_model]
        """
        # Multi-Head Attention
        attn_output, _ = self.self_attn(x, x, x, mask)

        # Feed-Forward Network + Residual
        residual = attn_output
        ffn_output = self.ffn(attn_output)
        output = self.layer_norm(ffn_output + residual)

        return output

class ProcessTransformer(nn.Module):
    """プロセス変数予測用Transformer"""

    def __init__(self, n_features, d_model=128, n_head=8, n_layers=4,
                 d_ff=512, dropout=0.1):
        """
        Args:
            n_features: 入力変数数(温度、圧力など)
            d_model: モデル次元
            n_head: Attentionヘッド数
            n_layers: Encoderレイヤー数
            d_ff: Feed-Forward中間次元
        """
        super(ProcessTransformer, self).__init__()

        # 入力埋め込み
        self.input_embedding = nn.Linear(n_features, d_model)

        # 位置エンコーディング
        self.pos_encoder = PositionalEncoding(d_model)

        # Transformer Encoder layers
        self.encoder_layers = nn.ModuleList([
            TransformerEncoderLayer(d_model, n_head, d_ff, dropout)
            for _ in range(n_layers)
        ])

        # 出力層
        self.fc_out = nn.Linear(d_model, n_features)

    def forward(self, x):
        """
        Args:
            x: [batch, seq_len, n_features]
        Returns:
            output: [batch, n_features] 次時刻の予測
        """
        # 埋め込み + 位置エンコーディング
        x = self.input_embedding(x)
        x = self.pos_encoder(x)

        # Encoder layers
        for layer in self.encoder_layers:
            x = layer(x)

        # 最後の時刻の出力を使用
        output = self.fc_out(x[:, -1, :])

        return output

# 合成プロセスデータ
def generate_process_data(n_samples=1000, n_features=3):
    """反応器の温度・圧力・流量データを生成"""
    time = np.linspace(0, 50, n_samples)
    data = np.zeros((n_samples, n_features))

    # 温度(300-500K)
    data[:, 0] = 400 + 50*np.sin(0.1*time) + 10*np.random.randn(n_samples)

    # 圧力(1-10 bar)
    data[:, 1] = 5 + 2*np.cos(0.15*time) + 0.5*np.random.randn(n_samples)

    # 流量(50-150 L/min)
    data[:, 2] = 100 + 30*np.sin(0.08*time) + 5*np.random.randn(n_samples)

    return data

# データ準備
data = generate_process_data(n_samples=1000, n_features=3)

# ウィンドウ作成
window_size = 50
X, y = [], []
for i in range(len(data) - window_size):
    X.append(data[i:i+window_size])
    y.append(data[i+window_size])

X = torch.FloatTensor(np.array(X))
y = torch.FloatTensor(np.array(y))

# モデル訓練
model = ProcessTransformer(n_features=3, d_model=128, n_head=8, n_layers=4)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

for epoch in range(30):
    model.train()
    optimizer.zero_grad()

    pred = model(X)
    loss = criterion(pred, y)

    loss.backward()
    optimizer.step()

    if (epoch+1) % 5 == 0:
        print(f'Epoch {epoch+1}, Loss: {loss.item():.6f}')

# 出力例:
# Epoch 5, Loss: 0.234567
# Epoch 10, Loss: 0.123456
# Epoch 15, Loss: 0.078901
# Epoch 20, Loss: 0.056789
# Epoch 25, Loss: 0.045678
# Epoch 30, Loss: 0.039876

2.5 Temporal Fusion Transformer

Temporal Fusion Transformer(TFT)は、時系列予測に特化したTransformerアーキテクチャです。静的変数(プロセス設計パラメータ)、既知変数(制御入力)、未知変数(外乱)を統合的に扱えます。

💡 TFTの特徴

  • Variable Selection: 重要な変数を自動選択
  • Gating機構: 情報の流れを適応的に制御
  • Interpretable Multi-Horizon: 複数ステップ先を解釈可能に予測

例5: 簡易TFT実装

class VariableSelectionNetwork(nn.Module):
    """変数選択ネットワーク"""

    def __init__(self, input_dim, hidden_dim, output_dim, dropout=0.1):
        super(VariableSelectionNetwork, self).__init__()

        self.hidden_dim = hidden_dim

        # Gating用のネットワーク
        self.grn = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ELU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, output_dim)
        )

        # 重要度スコア
        self.softmax = nn.Softmax(dim=-1)

    def forward(self, x):
        """
        Args:
            x: [batch, n_vars, dim]
        Returns:
            weighted: 重み付けされた変数 [batch, dim]
            weights: 変数の重要度 [batch, n_vars]
        """
        # 各変数の重要度を計算
        weights = self.softmax(self.grn(x))  # [batch, n_vars, 1]

        # 重み付き和
        weighted = torch.sum(x * weights, dim=1)

        return weighted, weights.squeeze(-1)

class SimplifiedTFT(nn.Module):
    """簡易版Temporal Fusion Transformer"""

    def __init__(self, n_features, static_dim, d_model=128, n_head=4,
                 n_layers=2, pred_len=10):
        """
        Args:
            n_features: 時系列変数数
            static_dim: 静的変数数(反応器タイプなど)
            pred_len: 予測ホライズン
        """
        super(SimplifiedTFT, self).__init__()
        self.pred_len = pred_len

        # Variable Selection
        self.var_selection = VariableSelectionNetwork(
            input_dim=n_features, hidden_dim=64, output_dim=d_model
        )

        # Static enrichment
        self.static_enrichment = nn.Linear(static_dim, d_model)

        # Transformer Encoder
        self.pos_encoder = PositionalEncoding(d_model)
        self.encoder_layers = nn.ModuleList([
            TransformerEncoderLayer(d_model, n_head, d_ff=512)
            for _ in range(n_layers)
        ])

        # Decoder
        self.decoder = nn.Linear(d_model, n_features * pred_len)

    def forward(self, x, static_vars):
        """
        Args:
            x: [batch, seq_len, n_features] 時系列データ
            static_vars: [batch, static_dim] 静的変数
        Returns:
            pred: [batch, pred_len, n_features]
        """
        batch_size = x.size(0)

        # Variable Selection
        x_selected, var_weights = self.var_selection(x.transpose(1, 2))

        # Static enrichment
        static_encoded = self.static_enrichment(static_vars)
        x_enriched = x_selected + static_encoded.unsqueeze(1)

        # Positional encoding
        x_enriched = self.pos_encoder(x_enriched)

        # Transformer Encoder
        for layer in self.encoder_layers:
            x_enriched = layer(x_enriched)

        # Decode to predictions
        pred = self.decoder(x_enriched[:, -1, :])
        pred = pred.view(batch_size, self.pred_len, -1)

        return pred, var_weights

# 使用例
n_features = 4  # 温度、圧力、流量、濃度
static_dim = 2  # 反応器タイプ、触媒種類
seq_len = 50
pred_len = 10
batch_size = 16

# データ生成
x = torch.randn(batch_size, seq_len, n_features)
static_vars = torch.randn(batch_size, static_dim)

# モデル
model = SimplifiedTFT(n_features=n_features, static_dim=static_dim,
                     d_model=128, pred_len=pred_len)

# 予測
pred, var_weights = model(x, static_vars)

print(f"Prediction shape: {pred.shape}")  # [16, 10, 4]
print(f"Variable importance weights shape: {var_weights.shape}")  # [16, 4]

# 変数の重要度
print("\nVariable importance (sample 1):")
for i in range(n_features):
    print(f"  Variable {i+1}: {var_weights[0, i].item():.4f}")

# 出力例:
# Prediction shape: torch.Size([16, 10, 4])
# Variable importance weights shape: torch.Size([16, 4])
#
# Variable importance (sample 1):
#   Variable 1: 0.3456
#   Variable 2: 0.2123
#   Variable 3: 0.2789
#   Variable 4: 0.1632

2.6 多変量時系列予測

プロセス産業では、温度・圧力・流量・濃度など複数の変数が相互作用します。Transformerはこれらの複雑な相互依存性を学習できます。

例6: 多変量プロセス予測

class MultivariateProcessTransformer(nn.Module):
    """多変量プロセス予測用Transformer"""

    def __init__(self, n_features, d_model=256, n_head=8, n_layers=6,
                 pred_horizon=20, dropout=0.1):
        """
        Args:
            n_features: 変数数
            pred_horizon: 予測ステップ数
        """
        super(MultivariateProcessTransformer, self).__init__()
        self.n_features = n_features
        self.pred_horizon = pred_horizon

        # 各変数に対する埋め込み
        self.feature_embedding = nn.Linear(1, d_model // n_features)

        # 位置エンコーディング
        self.pos_encoder = PositionalEncoding(d_model)

        # Transformer Encoder
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=n_head, dim_feedforward=1024,
            dropout=dropout, batch_first=True
        )
        self.transformer_encoder = nn.TransformerEncoder(
            encoder_layer, num_layers=n_layers
        )

        # 予測ヘッド
        self.prediction_head = nn.Sequential(
            nn.Linear(d_model, 512),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(512, n_features * pred_horizon)
        )

    def forward(self, x):
        """
        Args:
            x: [batch, seq_len, n_features]
        Returns:
            pred: [batch, pred_horizon, n_features]
        """
        batch_size, seq_len, _ = x.shape

        # 各特徴を埋め込み、結合
        embedded_features = []
        for i in range(self.n_features):
            feat = x[:, :, i:i+1]  # [batch, seq_len, 1]
            emb = self.feature_embedding(feat)  # [batch, seq_len, d_model//n_features]
            embedded_features.append(emb)

        x_embedded = torch.cat(embedded_features, dim=-1)  # [batch, seq_len, d_model]

        # 位置エンコーディング
        x_encoded = self.pos_encoder(x_embedded)

        # Transformer Encoder
        transformer_out = self.transformer_encoder(x_encoded)

        # 最終時刻で予測
        final_hidden = transformer_out[:, -1, :]  # [batch, d_model]

        # 予測
        pred = self.prediction_head(final_hidden)  # [batch, n_features * pred_horizon]
        pred = pred.view(batch_size, self.pred_horizon, self.n_features)

        return pred

# 実験データ生成(反応器の複雑なダイナミクス)
def generate_coupled_process_data(n_samples=2000):
    """相互作用する多変量プロセスデータ"""
    time = np.linspace(0, 100, n_samples)

    # 温度(T)
    T = 400 + 50*np.sin(0.05*time) + 10*np.random.randn(n_samples)

    # 圧力(P): 温度に依存
    P = 5 + 0.01*T + 2*np.cos(0.07*time) + 0.5*np.random.randn(n_samples)

    # 流量(F)
    F = 100 + 20*np.sin(0.06*time) + 5*np.random.randn(n_samples)

    # 濃度(C): 温度と流量に依存
    C = 0.8 - 0.0005*T + 0.001*F + 0.05*np.random.randn(n_samples)

    return np.stack([T, P, F, C], axis=1)

# データ準備
data = generate_coupled_process_data(n_samples=2000)

# 正規化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
data_normalized = scaler.fit_transform(data)

# ウィンドウ作成
window_size = 100
pred_horizon = 20

X, y = [], []
for i in range(len(data_normalized) - window_size - pred_horizon):
    X.append(data_normalized[i:i+window_size])
    y.append(data_normalized[i+window_size:i+window_size+pred_horizon])

X = torch.FloatTensor(np.array(X))
y = torch.FloatTensor(np.array(y))

# 訓練/テスト分割
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# モデル訓練
model = MultivariateProcessTransformer(
    n_features=4, d_model=256, n_head=8, n_layers=6, pred_horizon=20
)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

for epoch in range(20):
    model.train()
    optimizer.zero_grad()

    pred = model(X_train)
    loss = criterion(pred, y_train)

    loss.backward()
    optimizer.step()

    if (epoch+1) % 5 == 0:
        model.eval()
        with torch.no_grad():
            test_pred = model(X_test)
            test_loss = criterion(test_pred, y_test)
        print(f'Epoch {epoch+1}, Train: {loss.item():.6f}, Test: {test_loss.item():.6f}')

# 出力例:
# Epoch 5, Train: 0.145678, Test: 0.156789
# Epoch 10, Train: 0.089012, Test: 0.098765
# Epoch 15, Train: 0.067890, Test: 0.076543
# Epoch 20, Train: 0.056789, Test: 0.065432

2.7 転移学習(Transfer Learning)

事前学習済みTransformerモデルを、少量のプロセスデータでファインチューニングすることで、データ効率を改善できます。

例7: 事前学習とファインチューニング

class PretrainedProcessTransformer(nn.Module):
    """事前学習用Transformer(マスク言語モデル)"""

    def __init__(self, n_features, d_model=128, n_head=8, n_layers=4):
        super(PretrainedProcessTransformer, self).__init__()

        self.embedding = nn.Linear(n_features, d_model)
        self.pos_encoder = PositionalEncoding(d_model)

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=n_head, dim_feedforward=512,
            batch_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)

        # マスクされた値を再構成
        self.reconstruction_head = nn.Linear(d_model, n_features)

    def forward(self, x, mask=None):
        """
        Args:
            x: [batch, seq_len, n_features]
            mask: マスク位置 [batch, seq_len]
        Returns:
            reconstructed: [batch, seq_len, n_features]
        """
        x = self.embedding(x)
        x = self.pos_encoder(x)
        x = self.transformer(x)
        reconstructed = self.reconstruction_head(x)

        return reconstructed

# ステップ1: 大量の未ラベルデータで事前学習
def pretrain_model(model, data, epochs=50, mask_ratio=0.15):
    """マスク予測タスクで事前学習"""
    optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
    criterion = nn.MSELoss()

    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()

        # ランダムにマスク
        masked_data = data.clone()
        mask = torch.rand(data.shape[:2]) < mask_ratio  # [batch, seq_len]

        for b in range(data.size(0)):
            for t in range(data.size(1)):
                if mask[b, t]:
                    masked_data[b, t] = 0  # ゼロでマスク

        # 再構成
        pred = model(masked_data)

        # マスク位置のみでloss計算
        loss = criterion(pred[mask], data[mask])

        loss.backward()
        optimizer.step()

        if (epoch+1) % 10 == 0:
            print(f'Pretrain Epoch {epoch+1}, Loss: {loss.item():.6f}')

    return model

# ステップ2: 下流タスクでファインチューニング
class FineTunedPredictor(nn.Module):
    """事前学習済みモデルをファインチューニング"""

    def __init__(self, pretrained_model, n_features, pred_len=10):
        super(FineTunedPredictor, self).__init__()

        # 事前学習済みの重みを使用
        self.backbone = pretrained_model

        # タスク固有の予測ヘッド
        self.pred_head = nn.Linear(128, n_features * pred_len)
        self.pred_len = pred_len
        self.n_features = n_features

    def forward(self, x):
        """予測タスク"""
        # Backbone(凍結 or ファインチューニング)
        features = self.backbone.transformer(
            self.backbone.pos_encoder(self.backbone.embedding(x))
        )

        # 最終時刻で予測
        pred = self.pred_head(features[:, -1, :])
        pred = pred.view(-1, self.pred_len, self.n_features)

        return pred

# 実験
# 大量の未ラベルデータ
unlabeled_data = torch.randn(500, 100, 4)  # 500シーケンス

# 事前学習
pretrain_model_instance = PretrainedProcessTransformer(
    n_features=4, d_model=128, n_head=8
)
pretrain_model_instance = pretrain_model(pretrain_model_instance, unlabeled_data, epochs=30)

# 少量のラベル付きデータでファインチューニング
small_labeled_data = torch.randn(50, 100, 4)  # わずか50サンプル
small_labels = torch.randn(50, 10, 4)

finetuned_model = FineTunedPredictor(pretrain_model_instance, n_features=4, pred_len=10)
optimizer = torch.optim.Adam(finetuned_model.parameters(), lr=0.0001)
criterion = nn.MSELoss()

for epoch in range(20):
    optimizer.zero_grad()
    pred = finetuned_model(small_labeled_data)
    loss = criterion(pred, small_labels)
    loss.backward()
    optimizer.step()

    if (epoch+1) % 5 == 0:
        print(f'Finetune Epoch {epoch+1}, Loss: {loss.item():.6f}')

# 出力例:
# Pretrain Epoch 10, Loss: 0.234567
# Pretrain Epoch 20, Loss: 0.123456
# Pretrain Epoch 30, Loss: 0.089012
# Finetune Epoch 5, Loss: 0.045678
# Finetune Epoch 10, Loss: 0.023456
# Finetune Epoch 15, Loss: 0.015678
# Finetune Epoch 20, Loss: 0.012345

✅ 転移学習の利点

  • データ効率: 少量のラベル付きデータで高精度
  • 汎化性能: 過学習を抑制
  • 冷間起動問題の解決: 新プロセスでも即座に予測可能

2.8 Attention重みの可視化と解釈

Transformerの強力な利点は、Attention重みを可視化することで、モデルの予測根拠を理解できることです。プロセス制御において、どの時刻・変数が重要かを把握できます。

例8: Attention可視化

import matplotlib.pyplot as plt
import seaborn as sns

class InterpretableTransformer(nn.Module):
    """Attention重みを返すTransformer"""

    def __init__(self, n_features, d_model=128, n_head=4, n_layers=3):
        super(InterpretableTransformer, self).__init__()

        self.embedding = nn.Linear(n_features, d_model)
        self.pos_encoder = PositionalEncoding(d_model)

        # カスタムEncoder(Attention重みを保存)
        self.encoder_layers = nn.ModuleList([
            MultiHeadAttention(n_head, d_model, d_model//n_head, d_model//n_head)
            for _ in range(n_layers)
        ])

        self.fc_out = nn.Linear(d_model, n_features)
        self.attention_weights = []  # Attention重みを保存

    def forward(self, x):
        """
        Args:
            x: [batch, seq_len, n_features]
        Returns:
            output: [batch, n_features]
            attention_maps: List of [batch, n_head, seq_len, seq_len]
        """
        x = self.embedding(x)
        x = self.pos_encoder(x)

        self.attention_weights = []

        # 各レイヤーのAttentionを取得
        for layer in self.encoder_layers:
            x, attn = layer(x, x, x)
            self.attention_weights.append(attn.detach())

        output = self.fc_out(x[:, -1, :])

        return output, self.attention_weights

def visualize_attention(attention_weights, layer_idx=0, head_idx=0,
                       variable_names=None, save_path=None):
    """Attention重みをヒートマップで可視化

    Args:
        attention_weights: List of attention maps
        layer_idx: 可視化するレイヤー
        head_idx: 可視化するヘッド
        variable_names: 変数名のリスト
    """
    attn = attention_weights[layer_idx][0, head_idx].numpy()  # [seq_len, seq_len]

    plt.figure(figsize=(10, 8))
    sns.heatmap(attn, cmap='RdYlBu_r', center=0,
                xticklabels=10, yticklabels=10,
                cbar_kws={'label': 'Attention Weight'})

    plt.xlabel('Key Position (Time Step)')
    plt.ylabel('Query Position (Time Step)')
    plt.title(f'Attention Map - Layer {layer_idx+1}, Head {head_idx+1}')

    if save_path:
        plt.savefig(save_path, dpi=150, bbox_inches='tight')

    plt.tight_layout()
    # plt.show()

    print(f"Attention map visualized for Layer {layer_idx+1}, Head {head_idx+1}")

# 使用例
model = InterpretableTransformer(n_features=4, d_model=128, n_head=4, n_layers=3)

# サンプルデータ
sample_data = torch.randn(1, 50, 4)  # 50時刻分のデータ

# 予測とAttention取得
model.eval()
with torch.no_grad():
    pred, attn_weights = model(sample_data)

print(f"Prediction: {pred}")
print(f"Number of attention layers: {len(attn_weights)}")

# 各レイヤーのAttention可視化
for layer_idx in range(len(attn_weights)):
    for head_idx in range(4):
        visualize_attention(attn_weights, layer_idx=layer_idx, head_idx=head_idx)

# 特定時刻のAttention重み分析
def analyze_critical_timesteps(attention_weights, query_timestep=-1):
    """特定時刻がどの過去に注目しているか分析"""
    layer_idx = -1  # 最終レイヤー
    attn = attention_weights[layer_idx][0]  # [n_head, seq_len, seq_len]

    # 最終時刻のAttention(全ヘッド平均)
    final_attn = attn[:, query_timestep, :].mean(dim=0)  # [seq_len]

    # Top-5の重要時刻
    top_k = 5
    top_values, top_indices = torch.topk(final_attn, k=top_k)

    print(f"\nCritical timesteps for prediction (Top {top_k}):")
    for i, (idx, val) in enumerate(zip(top_indices, top_values)):
        print(f"  {i+1}. Time step {idx.item()}: weight = {val.item():.4f}")

analyze_critical_timesteps(attn_weights)

# 出力例:
# Prediction: tensor([[ 0.1234, -0.5678,  0.9012, -0.3456]])
# Number of attention layers: 3
# Attention map visualized for Layer 1, Head 1
# Attention map visualized for Layer 1, Head 2
# ...(全12パターン)
#
# Critical timesteps for prediction (Top 5):
#   1. Time step 49: weight = 0.0876
#   2. Time step 48: weight = 0.0654
#   3. Time step 45: weight = 0.0543
#   4. Time step 40: weight = 0.0432
#   5. Time step 35: weight = 0.0321
# → 直近5-10ステップと、15ステップ前が重要

💡 Attention解釈のベストプラクティス

  • 複数レイヤーを確認: 浅い層は局所的、深い層は大域的パターンを捉える
  • 複数ヘッドを比較: 各ヘッドが異なる依存性を学習
  • ドメイン知識と照合: 化学的に妥当な依存関係を学習しているか検証
  • 異常検知に活用: 通常と異なるAttentionパターンで異常を発見

学習目標の確認

この章を完了すると、以下を実装・説明できるようになります:

基本理解

実践スキル

応用力

LSTM vs Transformer比較

特性 LSTM Transformer
処理方式 逐次処理(遅い) 並列処理(高速)
長距離依存 ゲート機構で改善(限界あり) 直接アクセス(優れる)
パラメータ数 少ない 多い
データ量要件 少量でも学習可能 大量データが必要
解釈性 Attention追加で可能 標準で高い解釈性
訓練時間 短い 長い
推論速度 遅い(逐次) 速い(並列)
適用場面 小〜中規模データ、リアルタイム処理 大規模データ、高精度要求

参考文献

  1. Vaswani, A., et al. (2017). "Attention is All You Need." NeurIPS 2017.
  2. Lim, B., et al. (2021). "Temporal Fusion Transformers for Interpretable Multi-horizon Time Series Forecasting." International Journal of Forecasting, 37(4), 1748-1764.
  3. Devlin, J., et al. (2019). "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding." NAACL 2019.
  4. Zhou, H., et al. (2021). "Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting." AAAI 2021.
  5. Wu, N., et al. (2020). "Deep Transformer Models for Time Series Forecasting: The Influenza Prevalence Case." arXiv:2001.08317.