同じ深層学習モデルでも、RNN(再帰型ニューラルネットワーク)は「学習が遅い」と言われ、CNN(畳み込みニューラルネットワーク)は「GPUで一気に回る」と言われます。一方で、長い文章の文脈理解や時系列の遠く離れた因果関係では、CNNよりもTransformer(self-attention)が選ばれます。なぜCNNは速いのに、長距離の関係を捉えるのは苦手なのでしょうか?
この2つの疑問は、実は同じ1つの構造から生まれています。CNNの「局所的な窓を全位置に同時に当てる」という設計が、並列計算のしやすさを生むと同時に、離れた位置どうしがすぐには影響し合えないという弱点も生むのです。この記事では、この表と裏の関係を、RNN・Transformerと比べながら解剖します。
理解できると、次のような実務判断が根拠を持ってできるようになります。
- 時系列異常検知や音声処理で、どのくらい離れた依存を捉えたいかに応じて「畳み込みを何層積むか」「拡張畳み込みを使うか」「attentionに切り替えるか」を設計できる
- モデルの推論速度を見積もれる。なぜRNNは系列長に比例して遅く、CNN/Transformerは並列で速いのか、その根っこがわかる
本記事の内容
- CNNが並列計算できる理由 —「逐次依存が無い」を依存グラフと実測で理解する
- CNNが長距離依存を苦手とする理由 — 受容野と最大パス長という2つのものさし
- RNN・CNN・Self-Attention の計算特性を1枚の表で比較する
- 拡張畳み込みという対策と、それでも残る限界
- Pythonで「受容野を超えた依存は解けない」という相転移を実測する
前提知識
この記事は、CNNとRNNの基本構造を知っていると格段に読みやすくなります。
まず全体像 — 並列性と長距離依存は「依存グラフ」で決まる
細かい数式に入る前に、CNNとRNNが入力をどう処理するかを「計算の依存グラフ」で並べてみましょう。ここでいう依存グラフとは、「ある出力を計算するために、どの計算が先に終わっている必要があるか」を矢印で表したものです。

左のRNNでは、時刻 $t$ の隠れ状態 $h_t$ が前の時刻の $h_{t-1}$ に依存しています。つまり $h_1 \to h_2 \to h_3 \to \dots$ と、鎖のように前から順に計算するしかありません。右のCNNでは、各出力 $y_t$ は入力の局所的な窓(前後数個)だけから決まり、出力どうしには依存が無い。だから全位置の $y_t$ を同時に計算できます。
この「出力どうしに依存があるか否か」こそが、並列計算のしやすさと長距離依存の届きやすさを同時に決める分かれ目です。まずは並列性の側から見ていきましょう。
なぜCNNは並列計算できるのか
畳み込みは「同じ窓を全位置に一斉に当てる」演算
1次元の畳み込み層は、幅 $k$ のカーネル(フィルタ)$\bm{w}$ を入力系列 $\bm{x}$ の上でスライドさせ、各位置で局所的な重み付き和を計算します。位置 $t$ の出力は次式です。
$$ \begin{equation} y_t = \sum_{j=0}^{k-1} w_j \, x_{t + j – \lfloor k/2 \rfloor} + b \end{equation} $$
ここで大事なのは、$y_t$ の計算に必要なのは入力 $\bm{x}$ の一部だけで、他の出力 $y_{t’}$ の値は一切要らないという点です。$y_1$ を計算するのに $y_2$ の結果を待つ必要がありません。したがって、$y_1, y_2, \dots, y_N$ を好きな順番で、あるいは全部いっぺんに計算してかまいません。
GPUは数千個の演算コアを持ち、独立な計算を同時にこなすのが得意です。畳み込みは「独立な計算の塊」なので、この並列性をフルに使えます。
パラメータ共有が並列性をさらに後押しする
もう1つ、CNNの並列性を支えているのがパラメータ共有(weight sharing)です。式(1)のカーネル $\bm{w}$ は、位置 $t$ によらず全位置で同じものを使い回します。位置ごとに別々の重みを持つ全結合層と違い、「位置1用の重み」「位置2用の重み」……を個別に用意する必要がありません。
これは実装上、$N$ 個の出力すべてがまったく同じ小さな重み集合を参照することを意味します。GPUは同じパラメータを共有メモリに一度だけ載せ、全位置の計算を一斉に流せます。実際、畳み込みは行列積(im2col + GEMM)や専用カーネルにまとめられ、$N$ 個の局所演算が1回の大きな並列演算に集約されます。「同じ演算を大量のデータに一斉に適用する」というのは、まさにGPUが最も得意とする形です。
パラメータ共有には、並列性とは別の御利益もあります。同じフィルタをどこでも使うので、入力が横にずれても同じ特徴を検出できる(平行移動不変性)。しかも重みの数が入力長 $N$ に依存せず $k \times d$ 程度で済むため、パラメータ効率も良い。局所性・共有・並列性は、CNNという設計に一体で組み込まれた性質なのです。
RNNは前の計算を待たなければならない
対してRNNの更新式は、前の隠れ状態を入力に含みます。
$$ \begin{equation} h_t = \tanh(\bm{W}_x \bm{x}_t + \bm{W}_h h_{t-1} + \bm{b}) \end{equation} $$
$h_t$ を計算するには $h_{t-1}$ が確定していなければならず、その $h_{t-1}$ は $h_{t-2}$ を待つ……と、系列の長さ $N$ だけ逐次に計算するしかありません。GPUに何千コアあっても、$h_5$ を計算するコアは $h_4$ が終わるまで手持ち無沙汰です。これが「RNNは並列化しにくい」の正体です。

このタイムライン図が示すように、RNNは系列長 $N$ に比例した「待ち時間」が壁時計に乗ります。CNNは全位置を1ステップで処理できるので、系列が長くなっても計算の「段数」は増えません。
実測してみる — 同じ演算でも「逐次」か「一括」かで速さが変わる
この差を、まったく同じ演算量で確かめてみましょう。各時刻に線形変換 $\bm{y}_t = \bm{x}_t \bm{W}$ を施す処理を、(a) 全位置まとめて1回の行列積で行う場合と、(b) 時刻ごとに1つずつforループで行う場合で計測します。
import torch, time
d = 128
W = torch.randn(d, d)
def bench(N):
x = torch.randn(16, N, d)
for _ in range(3): # ウォームアップ
_ = x @ W
# (a) 全位置を一度に(CNN型の一括処理)
t0 = time.perf_counter()
for _ in range(20):
_ = x @ W
t_par = (time.perf_counter() - t0) / 20 * 1000
# (b) 時刻ごとに逐次(RNN型の逐次処理)
t0 = time.perf_counter()
for _ in range(5):
outs = [x[:, t, :] @ W for t in range(N)]
t_seq = (time.perf_counter() - t0) / 5 * 1000
return t_par, t_seq
for N in [64, 256, 1024, 2048]:
p, s = bench(N)
print(f"N={N:4d} 一括={p:6.3f}ms 逐次={s:7.3f}ms 比={s/p:4.1f}x")
手元のCPUでは、系列長 $N=2048$ でも一括処理は数ミリ秒で終わるのに対し、逐次ループはそのおよそ4〜7倍遅いという結果になりました。行列積そのものの演算量は同じなのに、1ステップずつ実行すると「次のステップに進む前に前を待つ」オーバーヘッドが $N$ 回積み重なるからです。

グラフを見ると、逐次ループは系列長にほぼ比例して時間が伸び、一括処理は同じ演算をずっと少ない壁時計で片付けています。CPUでこの差なら、数千コアのGPUではさらに開きます。CNNの畳み込みは(a)側、RNNの再帰は(b)側にあたる、というのがここでの要点です。
ここまでで「CNNが速い理由=出力どうしに依存が無く並列化できるから」がわかりました。ところが、この「依存が無い」という同じ性質が、次に見る長距離依存の弱点を生みます。
なぜCNNは長距離依存が苦手なのか
そもそも「長距離依存」とは何か
先に言葉のイメージを掴んでおきましょう。長距離依存(long-range dependency)とは、系列の中で遠く離れた要素どうしが強く関係し合う状況を指します。言語なら、「私が昨日会った、背の高い、眼鏡をかけた人は……だった」のように、主語と述語が長い修飾句を挟んで離れていても文法的に呼応します。時系列なら、あるバルブを閉じた影響が数百ステップ後の温度上昇として現れる、といった「原因と遅れた結果」の関係です。
こうした関係を捉えるには、モデルが離れた2点の情報を1か所に集めて突き合わせる必要があります。ところがCNNの各出力は局所的な窓しか見ていません。遠くの情報を突き合わせるには、局所の窓を何段も積み重ねて、少しずつ情報を運んでこなければならない。ここに「距離」と「必要な層数」の関係が生まれます。それを定量化するのが、次に見る受容野と最大パス長という2つのものさしです。
受容野 — 1つの出力が「見えている」入力範囲
畳み込みの各出力は、局所的な窓(幅 $k$)からしか計算されません。では層を重ねると、最終的に1つの出力はどれだけ広い入力範囲を「見て」いるのでしょうか。この「見えている範囲」を受容野(receptive field)と呼びます。
幅 $k$ の畳み込みを $L$ 層積んだときの受容野は、次式で表せます。
$$ \begin{equation} \text{RF} = 1 + L \,(k – 1) \end{equation} $$
導出は素直です。1層目の出力は幅 $k$ の窓、つまり $1 + (k-1)$ 個の入力を見ます。2層目の出力は「幅 $k$ の1層目の出力」を見ますが、その各出力がさらに $(k-1)$ ずつ横に広がった入力を見ているので、受容野は $(k-1)$ だけ増えます。これを $L$ 回繰り返すと、$1 + L(k-1)$ になります。

図は $k=3$ の畳み込みを積んだときの、中央ニューロンの受容野(緑)の広がりです。1層で幅3、2層で幅5、3層で幅7……と、層数に対して線形にしか広がりません。ここに問題が潜んでいます。系列長 $N$ の両端どうしの関係を1つのニューロンで捉えたければ、受容野が $N$ に届くまで、およそ $N/(k-1)$ 層も積まなければならないのです。
具体的な数を入れてみましょう。$k=3$ の畳み込みだけで長さ $N=1000$ の系列の両端を結ぶには、$\text{RF}=1+L(k-1)\ge 1000$ より $L \ge 499.5$、つまり約500層が必要です。カーネルを $k=7$ に広げても $L \ge 166.5$ で約167層。いずれも現実的な深さとは言えません。プーリングやストライドで系列を段階的に縮めれば実効的な受容野はもっと速く広がりますが、その代償に時間解像度を失います。「局所窓を積む」という素朴なやり方では、長距離依存のコストがいかに高くつくかが数字で見て取れます。
最大パス長 — 離れた2点が影響し合うまでの最短ステップ数
同じことを別の角度から測るのが最大パス長(maximum path length)です。これは「入力のどの2点も、互いに影響を及ぼし合えるようになるまでに通らなければならない層の最小数」を表します。Transformerの原論文 Attention Is All You Need が3アーキテクチャを比較した際の中心的な指標でもあります。

- RNN: 情報は時刻を1つずつ伝わるので、端から端まで $N-1$ ホップ。最大パス長は $O(N)$。
- CNN: 1層で $k-1$ だけ距離を稼げるので、端から端をつなぐには $O(N/k)$ 層が必要。
- Self-Attention: どの2点も1層で直接内積を取るので、最大パス長は $O(1)$。
なぜパス長が長いと学習が難しくなるのでしょうか。2点の情報が影響し合うには、その間の全ての層を勾配が逆向きに伝わる必要があります。各層は活性化関数の微分やパラメータ行列を勾配に掛け合わせるので、経由する層が多いほど、勾配は途中で小さく(または大きく)なりやすい。これは深いRNNで悪名高い勾配消失・勾配爆発と同じ現象です。加えて、離れた2点の情報は途中の非線形変換やプーリングを何度もくぐるうちに混ざり、薄まっていきます。パス長が短いほど、この「劣化」を受けずに直接的な関係を学べるわけです。
CNNの $O(N/k)$ は、RNNの $O(N)$ よりはマシですが、attentionの $O(1)$ には遠く及びません。離れた位置ほど、多くの層を経由しないと関係を捉えられない —— これがCNNの構造的な弱点です。しかもその「多くの層」は、上で述べた勾配の劣化を招く諸刃の剣でもあります。
実験1: 受容野が距離を超えた瞬間に、長距離依存が解ける
この弱点を、きれいな相転移として実測してみましょう。次のような合成課題を作ります。長さ $N=64$ の $\pm1$ 系列を用意し、中央から左右対称に距離 $D$ だけ離れた2点に「印」を置きます。2点が一致していれば正解ラベル1、不一致なら0。残りはすべてランダムなノイズです。中央位置の特徴だけで判定させれば、その受容野が2点を両方カバーできない限り、原理的に解けません。
import numpy as np, torch, torch.nn as nn
def make_batch(bs, N, dist, rng):
x = rng.choice([-1., 1.], size=(bs, N)).astype(np.float32)
c = N // 2; p = c - dist // 2; q = c + (dist - dist // 2)
a = rng.choice([-1., 1.], size=bs)
same = rng.random(bs) < 0.5
x[:, p] = a
x[:, q] = np.where(same, a, -a) # 一致/不一致を仕込む
return torch.tensor(x).unsqueeze(1), torch.tensor(same.astype(np.int64))
class CNN1D(nn.Module):
def __init__(self, L, k=3, ch=32):
super().__init__()
layers, cin = [], 1
for _ in range(L):
layers += [nn.Conv1d(cin, ch, k, padding=k // 2), nn.ReLU()]; cin = ch
self.conv = nn.Sequential(*layers); self.head = nn.Linear(ch, 2)
def forward(self, x):
h = self.conv(x); c = h.shape[-1] // 2
return self.head(h[:, :, c]) # 中央位置の特徴で判定
def train_eval(L, N, dist, k=3, steps=350, seed=0):
torch.manual_seed(seed); rng = np.random.default_rng(seed)
m = CNN1D(L, k); opt = torch.optim.Adam(m.parameters(), 3e-3); lf = nn.CrossEntropyLoss()
for _ in range(steps):
xb, yb = make_batch(128, N, dist, rng)
opt.zero_grad(); lf(m(xb), yb).backward(); opt.step()
xb, yb = make_batch(2000, N, dist, rng)
return (m(xb).argmax(1) == yb).float().mean().item(), 1 + L * (k - 1)
まず、2点の距離を $D=24$ に固定し、2層CNNのカーネル幅 $k$ を広げて受容野を変えていきます(受容野は $1+2(k-1)$)。
for k in [3, 5, 9, 13, 17]:
acc, rf = train_eval(2, 64, 24, k=k)
print(f"カーネル幅k={k:2d} 受容野={rf:2d} 精度={acc:.3f}")
出力はおよそ次のようになります。
カーネル幅k= 3 受容野= 5 精度=0.496
カーネル幅k= 5 受容野= 9 精度=0.496
カーネル幅k= 9 受容野=17 精度=0.496
カーネル幅k=13 受容野=25 精度=1.000
カーネル幅k=17 受容野=33 精度=1.000

結果は劇的です。受容野が17までは精度0.5(でたらめ=2点の関係がまったく見えていない)だったのが、受容野が距離24を超えた25の瞬間に精度1.0へ跳ね上がります。受容野が2点を両方カバーできて初めて、その依存関係を学習できるという、原理そのものが数値に表れています。中間の値がほとんど無い「相転移」なのは、片方の印しか見えないニューロンには一致・不一致を判定する情報が物理的に存在しないからです。
実験2: 受容野を固定すると、距離が伸びた瞬間に解けなくなる
逆向きの実験も見ましょう。今度はネットワークを4層・カーネル幅3(受容野 $1+4\times2=9$)に固定し、2点の距離 $D$ のほうを変えます。
for dist in [2, 4, 6, 8, 10, 14, 20]:
acc, rf = train_eval(4, 64, dist, k=3, seed=1)
print(f"距離={dist:2d} 精度={acc:.3f} (受容野={rf})")
距離= 2 精度=1.000 (受容野=9)
距離= 6 精度=1.000 (受容野=9)
距離= 8 精度=1.000 (受容野=9)
距離=10 精度=0.515 (受容野=9)
距離=14 精度=0.515 (受容野=9)
距離=20 精度=0.515 (受容野=9)

距離が受容野の限界(この設定では8)以内なら精度1.0で完璧に解けますが、距離が10に伸びて受容野からはみ出した瞬間、精度は0.5に崩壊します。同じモデルでも、捉えたい依存が受容野より遠いと、学習の巧拙以前に原理的に不可能なのです。実験1と2は表裏一体で、「受容野 ≥ 依存の距離」が長距離依存を扱えるかどうかの絶対条件だと示しています。
ここまでで、CNNの並列性と長距離依存の弱さが、どちらも「局所的な窓」という同じ設計から来ていることが見えてきました。次に、この特徴をRNN・attentionと1枚の表で比べて位置づけましょう。
3アーキテクチャの計算特性を並べて比べる
Attention Is All You Need の Table 1 は、系列を処理する3つの方式を「層あたり計算量」「逐次操作数」「最大パス長」で比較しています。系列長 $N$、特徴次元 $d$、カーネル幅 $k$ として整理すると次のようになります。
| 方式 | 層あたり計算量 | 逐次操作数 | 最大パス長 |
|---|---|---|---|
| RNN(再帰) | $O(N d^2)$ | $O(N)$ | $O(N)$ |
| CNN(畳み込み) | $O(k N d^2)$ | $O(1)$ | $O(N/k)$ |
| Self-Attention | $O(N^2 d)$ | $O(1)$ | $O(1)$ |

3つの列を読み解きましょう。
- 逐次操作数(並列しにくさ): RNNだけが $O(N)$ で、系列長に比例して「待ち」が発生します。CNNとSelf-Attentionは $O(1)$、つまり系列の長さに関係なく1段で全位置を処理でき、両方とも並列計算に向いています。CNNが速い理由がここに集約されています。
- 最大パス長(長距離の届きにくさ): Self-Attentionは $O(1)$ で最強、RNNは $O(N)$ で最弱、CNNはその中間の $O(N/k)$。CNNは並列性ではattentionと同格なのに、長距離依存ではattentionに負けます。
- 層あたり計算量: Self-Attentionは $O(N^2 d)$ で、系列が長いと二乗で重くなります(この点はself-attentionの計算量の記事で深掘りしています)。CNNは $O(kNd^2)$ で $N$ に線形。長い系列では、実はCNNのほうが計算量的には軽いのです。
つまりCNNは「並列性はattention並みに良く、計算量も系列長に線形で軽いが、長距離依存だけが弱い」という、はっきりした個性を持っています。逐次操作数が $O(1)$ なのに最大パス長が $O(N/k)$ という組み合わせが、この記事の主題そのものです。
なお、上の表でCNNの最大パス長を $O(N/k)$ としましたが、原論文では $O(\log_k N)$ と書かれています。これは次に述べる拡張畳み込みを使った場合の値です。
対策と、それでも残る限界
拡張畳み込み — 受容野を指数的に広げる
普通の畳み込みでは受容野が層数に線形($1+L(k-1)$)にしか広がりませんでした。ここで、カーネルの要素を1つおき、2つおき……と間隔を空けて配置する拡張畳み込み(dilated convolution)を使います。拡張率(dilation)を $1, 2, 4, 8, \dots$ と倍々にしていくと、受容野は指数的に広がります。

図のように $k=3$ で拡張率を倍々にすると、受容野は $3, 7, 15, 31, \dots$ と $2^{L+1}-1$ で伸びます。系列長 $N$ をカバーするのに必要な層数は $O(\log N)$ で済み、最大パス長が $O(N/k)$ から $O(\log_k N)$ に改善します。これが原論文の表の値です。音声生成のWaveNetや、時系列のTCN(Temporal Convolutional Network)はこの拡張畳み込みを積み重ね、CNNのまま長い依存を扱えるように設計されています。
ただし拡張畳み込みには「歯抜け(gridding)」という副作用があります。間隔を空けて参照するため、途中の位置を飛ばしてしまい、拡張率とちょうど噛み合わない距離の情報を取りこぼすことがあります。実際に本記事の実験でも、拡張率を大きくしすぎると、受容野は広いのに特定の距離の印を飛び越えてしまい精度が落ちる、という現象が観測できます。拡張率の設計(積み方)に注意が要るということです。
深く積む代償と、attentionへの流れ
拡張畳み込みを使わず、普通の畳み込みを大量に積んで受容野を稼ぐ手もあります。しかし層を深くすると、勾配消失や最適化の難しさという別のコストが生じます(実際、非常に深いプレーンなCNNは残差接続なしでは学習が不安定になります)。「長距離を捉えるために深くする」こと自体が、新たな困難を呼ぶわけです。
一方Self-Attentionは、1層でどの2点も直接結ぶので、受容野を広げるための深さがそもそも要りません。長距離依存を素直に、しかも並列に扱える —— これが、長い文脈を扱うタスクでCNN・RNNからTransformerへ主役が移った理由です。詳しくはAttention Is All You Needの解読記事を参照してください。
時系列を扱うときの実務的な含意
この受容野の話は、時系列データを畳み込みで扱うときに直接効いてきます。たとえば時系列の異常検知や予測で「どのくらい過去まで遡って現在を説明するか」は、そのままモデルの受容野の設計問題です。周期の長いパターンや、原因と結果が大きく離れた事象を捉えたいなら、受容野がその時間差をカバーしていなければなりません。実験2で見たとおり、受容野が足りなければ、どれだけ学習を工夫しても原理的に届かないからです。
現実の設計では、(1) カーネル幅を広げる、(2) 層を深くする、(3) 拡張畳み込みで指数的に受容野を稼ぐ、(4) プーリングやストライドで系列を粗くして相対的に受容野を広げる、といった手が使われます。どれも「並列性は保ったまま受容野をどう広げるか」という同じ問いへの答えです。逆に、依存の距離が読めないほど長い・可変な場合は、最初からパス長 $O(1)$ のattentionを選ぶほうが素直、という判断につながります。
トレードオフを1枚の地図にまとめる
最後に、ここまでの議論を「並列計算のしやすさ」と「長距離依存の届きやすさ」の2軸で1枚の地図にまとめます。

- RNNは左下。逐次依存で並列化しにくく、パス長 $O(N)$ で長距離も苦しい。
- CNNは右下。並列性は抜群だが、標準の畳み込みでは長距離の届きにくさが残る。
- 拡張CNNはCNNの上。並列性を保ったまま、受容野を指数的に広げて長距離側へ寄せる。
- Self-Attentionは右上。並列性と長距離依存の両方を手に入れた(その代償が計算量 $O(N^2)$)。
CNNが「並列◎・長距離△」の位置にいるのは、局所的な窓を全位置に一斉に当てるという1つの設計の、表と裏だとわかります。速さと引き換えに、遠くを見るには層を積む必要がある。この理解があれば、扱う依存の距離に応じて、畳み込み・拡張畳み込み・attentionを使い分ける判断ができます。
大事なのは、この地図に「絶対的な勝者」はいないという点です。短距離のパターンが支配的で速度が命なら、素朴なCNNが最良の選択になり得ます。中程度の長さの依存を並列性を保って捉えたいなら拡張CNN、依存の距離が読めず長文脈が要るならattention、というように、タスクの性質がどの象限を選ぶかを決めます。近年は畳み込みとattentionを組み合わせ、局所は畳み込みで安く、大域はattentionで捉えるハイブリッド構成も広く使われています。それぞれの手法が地図のどこにいるかを知っていれば、こうした設計の意図も読み解けるはずです。
まとめ
本記事では、CNNの並列性と長距離依存の弱さを、同じ構造の表裏として解説しました。
- CNNが並列計算できる理由は、各出力が局所的な入力窓だけから決まり、出力どうしに逐次依存が無いから。逐次操作数は $O(1)$ で、RNNの $O(N)$ と対照的。実測でも一括処理は逐次ループより数倍速かった。
- CNNが長距離依存を苦手とする理由は、受容野が層数に線形($1+L(k-1)$)にしか広がらず、最大パス長が $O(N/k)$ だから。受容野が依存の距離を超えないと、その関係は原理的に学習できない(実験で相転移として確認)。
- 拡張畳み込みなら受容野を指数的に広げ、最大パス長を $O(\log_k N)$ に改善できるが、歯抜けや深さのコストが残る。長距離と並列性を両取りしたのがSelf-Attentionで、その代償が $O(N^2)$ の計算量。
次のステップとして、以下の記事もおすすめです。