PatchTST — パッチ化とチャネル独立設計で時系列Transformerを革新する【ICLR 2023】

Transformerは自然言語処理や画像認識で圧倒的な成功を収めてきましたが、時系列予測への適用は長い間苦戦していました。2022年の衝撃的な論文(Zeng et al., “Are Transformers Effective for Time Series Forecasting?”)は、単純な線形モデルがTransformerベースの時系列モデルを上回ることを示し、時系列コミュニティに大きな疑問を投げかけました。

なぜTransformerは時系列で苦戦するのでしょうか? 根本的な原因は、トークン化の方法にありました。NLPでは「単語」という意味のある単位をトークンとしますが、初期の時系列Transformerは各時刻の1つのスカラー値をトークンとしていました(point-wise tokenization)。長さ $L = 512$ の系列なら512個のトークンが生成され、Self-Attentionの計算量は $O(512^2) = O(262,144)$ に達します。さらに、1つのスカラー値にはほとんど意味的情報が含まれていません。

PatchTST(Nie et al., ICLR 2023)は、この根本問題を2つのシンプルなアイデアで解決しました。

  1. パッチ化(Patching): 時系列を固定長のパッチ(部分系列)に分割してトークンとする
  2. チャネル独立(Channel Independence): 多変量時系列の各チャネルを独立に処理する

PatchTSTを理解することは、以下のような場面で直接役立ちます。

  • 後続モデルの理解: MOMENT、Sundial、TRACE、TimeSiam等、2024-2025年の主要な時系列基盤モデルの多くがPatchTSTのパッチ化設計を採用しています。PatchTSTは現代の時系列モデルの「共通語」です
  • 衛星テレメトリとの親和性: テレメトリの軌道周期構造(約90分)がパッチサイズと自然に対応し、1パッチ ≈ 1つの物理的な振る舞い区間として機能します

本記事の内容

  • Point-wise tokenization の問題点
  • パッチ化の動機と数理(系列長の削減、局所パターンの保持)
  • チャネル独立設計の意義
  • PatchTSTのアーキテクチャ詳細
  • 自己教師あり学習(マスクパッチ再構成)
  • 後続モデルへの影響
  • Pythonによるパッチ化Transformerの実装

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

Point-wise Tokenization の問題点

各時刻をトークンにする非効率性

初期の時系列Transformer(Informer, Autoformer, FEDformer等)は、各時刻のスカラー値 $x_t$ をトークンとして扱いました。多変量の場合は、各時刻のベクトル $\bm{x}_t = (x_t^{(1)}, \ldots, x_t^{(C)})$($C$ チャネル)が1トークンです。

この方法には3つの深刻な問題があります。

計算量の爆発: 系列長 $L$ のトークン列に対するSelf-Attentionの計算量は $O(L^2 d)$ です。$L = 512$ では $O(262,144 \cdot d)$ となり、NLPの典型的なトークン数(数百〜数千)と同程度ですが、時系列ではさらに長い系列を扱いたい場面が多く($L = 10,000$ 以上)、計算量が問題になります。

意味の希薄さ: NLPの1トークン(単語やサブワード)は豊富な意味情報を持ちますが、時系列の1時刻のスカラー値にはほとんど意味がありません。「温度が25.3°C」という1つの値からは、トレンドも周期性も読み取れません。意味が生まれるのは、値の時間的な並びからです。

局所パターンの分断: Self-Attentionは全時刻間のペアワイズ関係を計算しますが、時系列の重要な情報は多くの場合局所的な構造(短期トレンド、振動パターン等)に含まれます。point-wise tokenization では局所構造が個別のトークンに分解され、Transformerが再構成する必要があります。

線形モデルに負けた理由

DLinear(Zeng et al., 2022)が示したのは、時系列予測では局所パターンの抽出が最も重要だということです。単純な移動平均分解 + 線形層で十分に高い予測精度が得られる場合、Transformerの大域的注意は過剰適合のリスクを高めるだけで利点がありません。

この洞察から、PatchTSTの設計思想が生まれました — まず局所パターンを保持するトークン化を行い、そのうえでTransformerの大域的注意を活用する

point-wise tokenization の限界が明確になったところで、PatchTSTがこれをどう解決するか見ていきましょう。

パッチ化の数理

パッチの定義

長さ $L$ の時系列 $\bm{x} = (x_1, x_2, \ldots, x_L)$ を、パッチサイズ $P$、ストライド $S$ で分割します。$i$ 番目のパッチは、

$$ \bm{p}_i = (x_{(i-1)S+1}, x_{(i-1)S+2}, \ldots, x_{(i-1)S+P}), \quad i = 1, \ldots, N $$

パッチ数は、

$$ N = \left\lfloor \frac{L – P}{S} \right\rfloor + 1 $$

NLPとの対比で考えると、パッチは「単語」に相当します。1つの文字(時刻)ではなく、複数の文字をまとめた単語(パッチ)をトークンとすることで、各トークンに意味的な情報が凝縮されます。

計算量の削減

パッチ化による最大の恩恵は、トークン数の劇的な削減です。

パラメータ point-wise パッチ化 ($P=16, S=8$)
トークン数 $L = 512$ $N = 63$
Attention計算量 $O(512^2) = O(262,144)$ $O(63^2) = O(3,969)$
削減率 約66倍

$P = 16, S = 8$ の場合、$N = \lfloor (512 – 16) / 8 \rfloor + 1 = 63$ です。Attentionの計算量は $L^2 / N^2 = (512/63)^2 \approx 66$ 倍削減されます。

これにより、より長い系列($L = 2048$ や $L = 4096$)もTransformerで効率的に処理できるようになります。

局所パターンの保持

パッチ化のもう一つの重要な効果は、局所パターンが1つのトークン内に保持されることです。

パッチサイズ $P = 16$ の場合、各パッチは16時刻の部分系列を含みます。サンプリングレートが1Hzなら16秒間のパターン、1分なら16分間のパターンです。トレンド、振動の1周期、ステップ変化といった局所構造がパッチ内に自然に収まります。

衛星テレメトリの場合、軌道周期が約5400秒(90分)で、サンプリングレートが1Hzなら $P = 300$(5分間のパッチ)で軌道の1/18をカバーします。$P = 900$(15分間)なら1/6の軌道区間です。パッチサイズを軌道の特性的な時間スケールに合わせることで、物理的に意味のあるトークン化が実現できます。

パッチ化の効果を理解したところで、次にPatchTSTのもう一つの革新 — チャネル独立設計 — について見ていきます。

チャネル独立設計

多変量時系列の扱い

多変量時系列($C$ チャネル)を処理する方法には、大きく2つのアプローチがあります。

チャネル混合(Channel Mixing): 全チャネルを1つのトークンに結合する。時刻 $t$ のトークンは $\bm{x}_t = (x_t^{(1)}, \ldots, x_t^{(C)}) \in \mathbb{R}^C$ です。チャネル間の相関を直接モデル化できますが、パラメータ数がチャネル数に依存し、チャネル数が変わるとモデルを再学習する必要があります。

チャネル独立(Channel Independence): 各チャネルを独立な単変量時系列として処理する。$C$ チャネルの多変量時系列は $C$ 個の独立な単変量時系列に分解され、同じモデルが各チャネルに適用されます。

PatchTSTはチャネル独立を採用します。直感に反するかもしれません — チャネル間の相関を使った方が良いのでは?と思うでしょう。しかし、PatchTSTの実験では、多くのベンチマークでチャネル独立がチャネル混合を上回りました。

なぜチャネル独立が有効なのか

過剰適合の防止: チャネル混合モデルは、訓練データのチャネル間の偽の相関を学習してしまうリスクがあります。例えば、気温と湿度の間に訓練データでは強い負の相関が見られても、予測時には異なる気象条件でこの相関が崩れる可能性があります。チャネル独立は各チャネル内の時間パターンのみを学習するため、汎化性能が高くなります。

正則化効果: $C$ チャネルの多変量系列をチャネル独立で処理すると、実質的に学習サンプル数が $C$ 倍になります。これはバッチサイズの増加と同等の効果を持ち、勾配推定の分散を下げます。

柔軟性: チャネル数が異なるデータセットに同じモデルをそのまま適用できます。衛星テレメトリでは、衛星ごとにセンサ構成が異なることが一般的ですが、チャネル独立モデルなら再学習なしで適用可能です。

計算効率: チャネル混合ではトークンの次元が $C \times P$ になりますが、チャネル独立では $P$ のままです。$C = 10$ の場合、パラメータ数は10分の1です。

ただし、チャネル間の物理的な因果関係が明確に存在する場合(例: 電圧→電流→温度の因果連鎖)には、チャネル混合の方が有利な場合もあります。PatchTSTはデフォルトでチャネル独立を推奨しつつ、混合モードも選択肢として残しています。

PatchTSTのアーキテクチャ

全体構造

PatchTSTの全体構造は以下のコンポーネントで構成されます。

1. パッチ埋め込み(Patch Embedding): パッチ $\bm{p}_i \in \mathbb{R}^P$ を線形層で $d$ 次元のトークン埋め込みに変換します。

$$ \bm{h}_i^{(0)} = \bm{p}_i \bm{W}_{\text{patch}} + \bm{b}_{\text{patch}} + \bm{e}_i^{\text{pos}} $$

ここで $\bm{W}_{\text{patch}} \in \mathbb{R}^{P \times d}$、$\bm{e}_i^{\text{pos}} \in \mathbb{R}^d$ は学習可能な位置埋め込みです。

2. Transformer Encoder: $L_{\text{enc}}$ 層のTransformer Encoderブロック。各ブロックはMulti-Head Self-AttentionとFFNで構成されます。

$$ \bm{h}^{(l’)} = \bm{h}^{(l)} + \text{MHSA}(\text{LN}(\bm{h}^{(l)})) $$

$$ \bm{h}^{(l+1)} = \bm{h}^{(l’)} + \text{FFN}(\text{LN}(\bm{h}^{(l’)})) $$

Encoder-onlyであり、Decoderは使いません。注意マスクは不要(全パッチ間のAttentionが計算される)で、これはBERTと同じ双方向構造です。

3. 予測ヘッド(Prediction Head): Encoder の出力トークン列を平坦化し、線形層で予測系列を出力します。

$$ \hat{\bm{x}}_{\text{future}} = \text{Flatten}(\bm{h}_1^{(L)}, \ldots, \bm{h}_N^{(L)}) \bm{W}_{\text{pred}} + \bm{b}_{\text{pred}} $$

予測長 $H$ のフラットベクトル $\hat{\bm{x}}_{\text{future}} \in \mathbb{R}^H$ が出力されます。

自己教師あり学習 — マスクパッチ再構成

PatchTSTは教師あり学習(次ステップ予測)に加えて、マスクパッチ再構成による自己教師あり事前学習もサポートします。

BERTの[MASK]トークンと同じアイデアで、入力パッチの一定割合 $r$(通常 $r = 0.4$)をマスクし、残りのパッチからマスクされたパッチの値を再構成します。

マスキング: $N$ 個のパッチのうち $\lfloor rN \rfloor$ 個をランダムに選択し、学習可能なマスクトークン $\bm{m} \in \mathbb{R}^d$ で置き換えます。

$$ \tilde{\bm{h}}_i^{(0)} = \begin{cases} \bm{h}_i^{(0)} & i \notin \mathcal{M} \\ \bm{m} + \bm{e}_i^{\text{pos}} & i \in \mathcal{M} \end{cases} $$

再構成: マスクされた位置の出力から元のパッチ値を再構成し、MSE損失で学習します。

$$ \mathcal{L}_{\text{pretrain}} = \frac{1}{|\mathcal{M}|} \sum_{i \in \mathcal{M}} \|\bm{p}_i – \hat{\bm{p}}_i\|_2^2 $$

この事前学習により、モデルは「局所パターンの文脈的補完」を学習します。つまり、周囲のパッチから欠損パッチの波形を推定する能力を獲得します。事前学習後にファインチューニングすることで、少量のラベルデータでも高い予測精度が得られます。

後続モデルへの影響

PatchTSTが確立した「パッチ化 + Transformer Encoder」の設計は、後続の時系列基盤モデルに広く採用されています。

MOMENT(CMU, ICML 2024): PatchTSTと同じパッチ化 + マスク再構成の事前学習を大規模データに適用。Encoder-only構造を維持。

Sundial(Tsinghua, ICML 2025): パッチ化をFlow Matchingと組み合わせ、確率的予測を実現。パッチ埋め込みの構造はPatchTSTを踏襲。

TRACE(NeurIPS 2025): パッチ化 + チャネル独立の設計を維持しつつ、テキストとのマルチモーダルアライメントを追加。

TimeSiam(ICML 2024): パッチ化された時系列に対してSiameseネットワークで自己教師あり学習。

PatchTSTはいわば「時系列Transformerの基盤設計のスタンダード」を確立した論文です。

後続モデルの広がりを確認したところで、Pythonでパッチ化Transformerを実装し、パッチ化の効果を実験で確認しましょう。

Pythonによる実装

パッチ化の効果の可視化

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# --- 合成テレメトリデータ ---
def generate_orbital_telemetry(length=1024, period=90, n_channels=3):
    """軌道周期を持つ多変量テレメトリ"""
    t = np.arange(length, dtype=float)
    channels = []
    names = ['Temperature (°C)', 'Voltage (V)', 'Angular Rate (°/s)']
    for c in range(n_channels):
        freq = 2 * np.pi / period
        phase = c * 2 * np.pi / n_channels
        amplitude = [20, 2, 0.5][c]
        offset = [25, 28, 0][c]
        noise_level = [0.5, 0.1, 0.05][c]
        signal = amplitude * np.sin(freq * t + phase) + offset
        noise = np.random.randn(length) * noise_level
        channels.append(signal + noise)
    return np.array(channels), names

data, ch_names = generate_orbital_telemetry(length=1024, period=90, n_channels=3)

# --- パッチ化の可視化 ---
patch_size = 32
stride = 16

fig, axes = plt.subplots(3, 1, figsize=(16, 9), sharex=True)
colors_patch = plt.cm.Set3(np.linspace(0, 1, 20))

for c in range(3):
    axes[c].plot(data[c], color='gray', alpha=0.3, linewidth=0.5)
    # パッチを色分けして表示
    for i in range(0, len(data[c]) - patch_size, stride):
        patch_idx = i // stride
        if patch_idx < 20:  # 最初の20パッチ
            axes[c].fill_between(
                range(i, i + patch_size),
                data[c, i:i+patch_size] - 0.3,
                data[c, i:i+patch_size] + 0.3,
                alpha=0.3, color=colors_patch[patch_idx % 20]
            )
    axes[c].set_ylabel(ch_names[c])
    axes[c].grid(True, alpha=0.2)

n_patches = (len(data[0]) - patch_size) // stride + 1
axes[0].set_title(f'Orbital Telemetry: Patching (P={patch_size}, S={stride}, '
                   f'L={len(data[0])} → N={n_patches} tokens)')
axes[2].set_xlabel('Time step')
plt.tight_layout()
plt.savefig('patchtst_patching.png', dpi=150, bbox_inches='tight')
plt.show()

print(f"系列長: {len(data[0])}")
print(f"パッチ数: {n_patches}")
print(f"Attention計算量の削減: {len(data[0])**2 / n_patches**2:.1f}x")

パッチ化の可視化から、各チャネルの時系列がパッチに分割される様子がわかります。色分けされた各パッチは局所的な波形パターンを含んでおり、point-wise tokenization(各時刻が1トークン)では失われる局所構造がパッチ内に保持されています。系列長1024のデータがわずか62個のトークンに変換され、Attention計算量は約273倍削減されます。軌道周期90ステップに対してパッチサイズ32は約1/3周期に相当し、物理的に意味のある区間がパッチとして切り出されていることが確認できます。

簡易パッチ化Transformerの実装

# --- 簡易パッチTST ---
class SimplePatchTST:
    def __init__(self, patch_size=32, stride=16, d_model=32,
                 n_heads=4, pred_len=96, lr=0.005):
        self.patch_size = patch_size
        self.stride = stride
        self.d_model = d_model
        self.n_heads = n_heads
        self.pred_len = pred_len
        self.lr = lr
        self.head_dim = d_model // n_heads

        # パッチ埋め込み
        self.W_patch = np.random.randn(patch_size, d_model) * 0.1
        self.b_patch = np.zeros(d_model)

        # Self-Attention (1層簡易版)
        self.W_q = np.random.randn(d_model, d_model) * 0.05
        self.W_k = np.random.randn(d_model, d_model) * 0.05
        self.W_v = np.random.randn(d_model, d_model) * 0.05
        self.W_o = np.random.randn(d_model, d_model) * 0.05

        # FFN
        self.W_ff1 = np.random.randn(d_model, d_model * 2) * 0.05
        self.b_ff1 = np.zeros(d_model * 2)
        self.W_ff2 = np.random.randn(d_model * 2, d_model) * 0.05
        self.b_ff2 = np.zeros(d_model)

    def create_patches(self, x):
        """単変量系列をパッチに分割"""
        patches = []
        for i in range(0, len(x) - self.patch_size + 1, self.stride):
            patches.append(x[i:i+self.patch_size])
        return np.array(patches)

    def patch_embed(self, patches):
        """パッチ → トークン埋め込み"""
        N = len(patches)
        h = patches @ self.W_patch + self.b_patch
        # 位置埋め込み(正弦波)
        pos = np.arange(N)[:, None]
        dim = np.arange(self.d_model)[None, :]
        pe = np.sin(pos / 10000 ** (dim / self.d_model))
        h = h + 0.1 * pe
        return h

    def attention(self, h):
        """Multi-Head Self-Attention(簡易版)"""
        Q = h @ self.W_q
        K = h @ self.W_k
        V = h @ self.W_v

        scale = np.sqrt(self.d_model)
        scores = Q @ K.T / scale
        attn = np.exp(scores - scores.max(axis=-1, keepdims=True))
        attn /= attn.sum(axis=-1, keepdims=True)

        out = attn @ V @ self.W_o
        return out, attn

    def ffn(self, h):
        """Feed-Forward Network"""
        hidden = np.maximum(0, h @ self.W_ff1 + self.b_ff1)  # ReLU
        return hidden @ self.W_ff2 + self.b_ff2

    def forward(self, x):
        """順伝播"""
        patches = self.create_patches(x)
        h = self.patch_embed(patches)
        h_attn, attn_weights = self.attention(h)
        h = h + h_attn  # 残差接続
        h = h + self.ffn(h)  # 残差接続 + FFN
        return h, attn_weights, patches

# --- Attention パターンの可視化 ---
model = SimplePatchTST(patch_size=32, stride=16, d_model=32, n_heads=4)
# 温度チャネルで実行
h_out, attn_weights, patches = model.forward(data[0])

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# (a) Attention重み行列
im = axes[0].imshow(attn_weights[:30, :30], cmap='viridis', aspect='auto')
axes[0].set_title('Self-Attention Weights (first 30 patches)')
axes[0].set_xlabel('Key patch index')
axes[0].set_ylabel('Query patch index')
plt.colorbar(im, ax=axes[0])

# (b) 特定パッチのAttention分布
query_patches = [5, 15, 25]
for qp in query_patches:
    axes[1].plot(attn_weights[qp], label=f'Query patch {qp}', alpha=0.7)
axes[1].set_xlabel('Key patch index')
axes[1].set_ylabel('Attention weight')
axes[1].set_title('Attention Distribution for Selected Query Patches')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('patchtst_attention.png', dpi=150, bbox_inches='tight')
plt.show()

Attentionパターンの可視化から、パッチ化されたTransformerが時系列の構造を捉えていることがわかります。左のAttention重み行列では、対角線上に強い重みが見られます(各パッチは近傍パッチに強く注目する)。加えて、一定間隔で強い重みのパターンが現れており、これは軌道周期に対応しています。パッチ5の時点から約5〜6パッチ離れた位置(軌道の半周期に相当)に強い注意があります。右のグラフは特定のクエリパッチのAttention分布を示しており、局所的な集中と周期的な構造の両方が確認できます。これはパッチ化により、各トークンが意味のある局所パターンを持つため、Attentionが有意義な時間的関係を学習できていることを示しています。

チャネル独立 vs チャネル混合の比較

# --- チャネル独立 vs チャネル混合の比較 ---
# チャネル独立: 各チャネルを独立に処理
embeddings_ci = []  # Channel Independent
for c in range(3):
    h_c, _, _ = model.forward(data[c])
    embeddings_ci.append(h_c.mean(axis=0))  # パッチの平均埋め込み
embeddings_ci = np.array(embeddings_ci)

# チャネル混合: 全チャネルを結合(簡易版)
data_mixed = data.T  # (L, C)
# 各時刻の3チャネルを結合してパッチ化(パッチサイズ分だけ)
patches_mixed = []
for i in range(0, len(data_mixed) - 32, 16):
    # 各チャネルのパッチを結合
    p = data_mixed[i:i+32].flatten()  # (32*3,) = (96,)
    patches_mixed.append(p)
patches_mixed = np.array(patches_mixed)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# (a) チャネル独立の埋め込み
for c in range(3):
    h_c, _, patches_c = model.forward(data[c])
    # パッチごとの埋め込みノルムを時系列として表示
    norms = np.linalg.norm(h_c, axis=1)
    t_patches = np.arange(len(norms)) * model.stride
    axes[0].plot(t_patches, norms, label=ch_names[c], alpha=0.7)
axes[0].set_xlabel('Time step')
axes[0].set_ylabel('Embedding L2 norm')
axes[0].set_title('Channel Independent: Patch Embedding Norms')
axes[0].legend(fontsize=8)
axes[0].grid(True, alpha=0.3)

# (b) パッチ数とパラメータ数の比較
categories = ['Channel\nIndependent', 'Channel\nMixing']
n_ch = 3
n_patches_ci = ((1024 - 32) // 16 + 1)
params_ci = 32 * 32 + 32 * 32  # W_patch + W_q (per channel, shared)
params_cm = (32 * n_ch) * 32 + 32 * 32  # larger patch embedding

bars1 = axes[1].bar([0, 1], [params_ci, params_cm], color=['#00d4ff', '#ff6b6b'])
axes[1].set_xticks([0, 1])
axes[1].set_xticklabels(categories)
axes[1].set_ylabel('Parameters (patch embedding)')
axes[1].set_title(f'Parameter Count Comparison (C={n_ch} channels)')
axes[1].grid(True, alpha=0.3, axis='y')
for bar, val in zip(bars1, [params_ci, params_cm]):
    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height(),
                 f'{val}', ha='center', va='bottom')

plt.tight_layout()
plt.savefig('patchtst_channel.png', dpi=150, bbox_inches='tight')
plt.show()

チャネル比較の可視化から、2つの重要な洞察が得られます。左のグラフでは、チャネル独立処理による各チャネルの埋め込みノルムが表示されています。各チャネル(温度、電圧、角速度)がそれぞれ異なる時間パターンを持っており、独立に処理することで各チャネルの特性が忠実に捉えられています。右の棒グラフはパラメータ数の比較で、チャネル独立の場合はパッチ埋め込みのパラメータがチャネル数に依存しないため、チャネル混合に比べて少ないパラメータ数で済みます。チャネル数が増えるほどこの差は大きくなり、100チャネルのテレメトリでは100倍の差になります。

まとめ

本記事では、PatchTST(ICLR 2023)が提案するパッチ化とチャネル独立設計について解説しました。

  • パッチ化: 時系列をパッチ(部分系列)単位でトークン化し、局所パターンを保持しつつトークン数を大幅に削減(Attention計算量を数十〜数百倍削減)
  • チャネル独立: 多変量時系列の各チャネルを独立に処理し、過剰適合を防止しつつパラメータ効率を向上
  • 自己教師あり学習: BERTスタイルのマスクパッチ再構成で事前学習し、少量データでのファインチューニングを可能に
  • 後続モデルへの影響: MOMENT、Sundial、TRACE、TimeSiam等が「パッチ化 + Transformer Encoder」の設計を採用

次のステップとして、以下の記事も参考にしてください。