概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめになります。PyTorchの1次元畳み込みにパディングを追加して、時系列データから時間軸の流れに従った特徴量を抽出する方法、因果畳み込みについてまとめてみたいと思います。
1. 因果畳み込みの解説
1次元畳み込み(Conv1d)は、音声、株価、センサーデータなど、時間と共に変化するデータの特徴を捉えるのに有効なネットワーク層です。しかし、通常の1次元畳み込みには、時系列データを扱う上で注意すべき点があります。「未来の情報を見てしまう?」という問題です。
1.1 具体例
例1. 1次元畳み込み
平均気温の時系列データを例として考察してみましょう。7月1日から7月5日までの5日間の気温データを入力情報として考えましょう。下図のように、カーネルサイズ3の1次元畳み込みを適用してみます。最初の出力$y_1$を計算するには、7月1日、7月2日、7月3日の3日分の気温情報を使っています。
出力$y_1$を「どの時点の特徴量とみなすか」で同じ数値であっても解釈が大きく変わってきます。
-
$y_1$を「7月1日時点での特徴量」として扱う場合
7月1日の時点では、まだ発生していない7月2日と7月3日の未来の情報を使って特徴量を計算しているように解釈できます。つまり特徴量を抽出する際に 「未来の情報を利用している」 というわけです。 -
$y_1$を「7月3日時点での特徴量」として扱う場合
過去と現在の3日分の情報を使った計算と解釈できます。この解釈だと7月3日より前を起点とした特徴量が得られません。
対象としている時点よりも未来の情報を使わず、過去の情報だけを使って特徴量を抽出するにはどうすればよいのか?というモチベーションが因果畳み込み(Causal Convolution) です。因果畳み込みでは、ある時点の出力を計算する際に、その時点より前の情報(過去)のみを使用します。これにより、時系列データの予測モデルに求められる「因果律」を守った、より信頼性の高い特徴抽出が可能になりそうです![]()
例2. 因果畳み込み
「対象としている時点よりも未来の情報を使わず、過去の情報だけを使って特徴量を抽出する」素朴な解決方法が、左側にゼロ「0」を挿入して解釈の2番目を使うというアイディアです。下図のように「0」を入力データのベクトルの左側に挿入します。2つゼロを左側に追加することで7月1日時点での特徴量を求める時に、7月2日と7月3日の情報を使わずに$y_1$を求めることができます。
7月2日時点での特徴量は、0と7月1日、7月2日の情報を利用して求めることになります。
| 対象時点 | 利用データ | 出力値 |
|---|---|---|
| 7月1日 | 7月1日の情報のみ利用 | $y_1$ |
| 7月2日 | 7月1日と7月2日の情報のみ利用 | $y_2$ |
| 7月3日 | 7月1日、7月2日、7月3日の情報のみ利用 | $y_3$ |
| 7月4日 | 7月2日、7月3日、7月4日の情報のみ利用 | $y_4$ |
| 7月5日 | 7月3日、7月4日、7月5日の情報のみ利用 | $y_5$ |
素朴ですが入力データの左側のみに0パディングすることで、「対象としている時点よりも未来の情報を使わず、過去の情報だけを使って特徴量を抽出する」ことが可能になります。
実装例
次のような設定で因果畳み込みを実装してみます。
設定
- 入力データ : 5期分の時系列データ (1, 2, 3, 4, 5)
- カーネルサイズ : 3
- カーネルの重み : (1, 1, 1)
PyTorchのConv1dのオプション、paddingでパディングする数を指定できます。実際に挿入される値は、padding_modeで設定できます。zeros (0でパディング)がデフォルト値になっています。
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
# 入力データは(バッチサイズ、チャンネル数、系列長)
x = torch.FloatTensor([1, 2, 3, 4, 5]).reshape(1,1,5)
# Conv1dの設定
padding_size = 2 # カーネルサイズが3なので、パディングは2個
cnn1 = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=3, padding=padding_size, bias=False)
# カーネルの値を(1, 1, 1)に指定
param = torch.FloatTensor([1,1,1]).reshape(1,1,3)
cnn1.weight = Parameter(param)
# 実際に計算
y = cnn1(x)
print(y)
# conv1dのpaddingは左右におこなわれることがわかる
# tensor([[[ 1., 3., 6., 9., 12., 9., 5.]]], grad_fn=<ConvolutionBackward0>)
# 右から2個、余分な部分を切り取ることで因果畳み込みの形になる
print(y[:,:,:-padding_size])
# tensor([[[ 1., 3., 6., 9., 12.]]], grad_fn=<SliceBackward0>)
コードのポイント
- PyTorchのConv1dにおけるpadding=2は、入力データの左右に2個「0」を挿入することになります
- cnn1(x)の出力は、[ 1., 3., 6., 9., 12., 9., 5.]で、右側の0まで含めて計算されています
- 因果畳み込みにするには、cnn1(x)の出力値を右から2個削除することになります
-
y[:,:,:-padding_size]と右端を削除したものが因果畳み込みの出力値となります
1.2 ダイレーション (dilation・膨張・拡張)
1次元畳み込みのバリエーションについてです。畳み込みに利用するカーネルは通常、連続した並びに対して計算されます。計算の間隔を広げるのがdilationと呼ばれるオプションになります。畳み込みカーネルの要素間に隙間を作ることで、広い範囲の情報(受容野)を効率的に獲得することが可能となります。
例3. ダイレーションが2のケース
入力データが、(1,2, 3, 4, 3, 2, 1)という7日分のデータを使って、カーネルサイズが3で、(1, 1, 1)とします。dilationが2の場合の計算と効果を確認してみます。
設定
- 入力データ : 7期分の時系列データ (1, 2, 3, 4, 3, 2, 1)
- カーネルサイズ : 3
- カーネルの重み : (1, 1, 1)
- dilation : 2
dilationが2なので入力データを2個おき(1個とばし)で演算を行います。カーネルサイズが3ですが、図のように1個とばしのカーネルで、見た目カーネルサイズ5の(1, 0, 1, 0, 1)と考えることもできます。カーネルの値が1の部分が学習によって値が変更していくと考えます。
- 1期目:図の黄色の部分とカーネルの(1, 1, 1)を使って、1×1+3×1+3×1=7と計算されます
- 2期目:図のオレンジ色の部分とカーネルから、2×1+4×1+2×1=8と計算されます
- 3期目:図の緑色の部分とカーネルから、3×1+3×1+1×1=7と計算されます
実際に、コードで確認してみます。Parameterを利用して、カーネルの値を(1, 1, 1)に固定します。
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
# 入力データは(バッチサイズ、チャンネル数、系列長)
x = torch.FloatTensor([1, 2, 3, 4, 3, 2 ,1]).reshape(1,1,7)
# Conv1dの設定
cnn1 = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=3, dilation=2, bias=False)
# カーネルの値を(1, 1, 1)に指定
param = torch.FloatTensor([1,1,1]).reshape(1,1,3)
cnn1.weight = Parameter(param)
# 実際に計算
y = cnn1(x)
print(y)
# tensor([[[7., 8., 7.]]], grad_fn=<ConvolutionBackward0>)
例4. ダイレーションが3のケース
例3と同様の設定で、dilation=3のケースを扱います。入力データが、(1,2, 3, 4, 3, 2, 1)という7日分のデータ、カーネルサイズが3で、(1, 1, 1)とします。
設定
- 入力データ : 7期分の時系列データ (1, 2, 3, 4, 3, 2, 1)
- カーネルサイズ : 3
- カーネルの重み : (1, 1, 1)
- dilation : 3
dilationが3なので入力データを3個おき(2個とばし)で演算を行います。カーネルサイズが3ですが、カーネルサイズ7の(1, 0, 0, 1, 0, 0, 1)として振る舞います。計算結果は1×1+4×1+1×1=6と計算されます。カーネルサイズは3ですが、dilationを3にすることで、より広範囲から情報を抽出していると解釈されます。
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
# 入力データは(バッチサイズ、チャンネル数、系列長)
x = torch.FloatTensor([1, 2, 3, 4, 3, 2 ,1]).reshape(1,1,7)
# Conv1dの設定
cnn1 = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=3, dilation=3, bias=False)
# カーネルの値を(1, 1, 1)に指定
param = torch.FloatTensor([1,1,1]).reshape(1,1,3)
cnn1.weight = Parameter(param)
# 実際に計算
y = cnn1(x)
print(y)
# tensor([[[6.]]], grad_fn=<ConvolutionBackward0>)
例5. 因果畳み込みとダイレーション
最後にdilation (膨張) を用いた因果畳み込みの計算例を見ていきましょう。dilationによって、カーネルの受容野が広がり、広い範囲の情報を効率的に捉えられていると解釈できることを確認します。
設定
- 入力データ : 7期分の時系列データ (1, 2, 3, 4, 3, 2, 1)
- カーネルサイズ : 3
- カーネルの重み: (1, 1, 1)
- dilation : 2
dilation=2に設定すると、カーネルは1つおきの入力値に作用します。カーネルサイズが3なので、カーネルは(1, 0, 1, 0, 1)のように、間に1つずつ空白が空いた状態として振る舞います。過去の情報のみを利用する「因果性」を保つために、左側に0を4つパディングします。
計算の流れ
- 1期目:図の緑色の部分とカーネルの(1, 1, 1)を使って、0×1+0×1+1×1=1と計算されます。1期目の値「1」のみが利用されています
- 2期目:0×1+0×1+2×1=2と計算されます。2期目までの値だけで計算されています
- 3期目:0×1+1×1+3×1=4と計算されます
︙ - 7期目:図の薄緑色とカーネルの(1, 1, 1)を利用して、3×1+3×1+1×1=7と計算されます。3期目、5期目、7期目の情報が利用されています。dilation=1の場合は、5期、6期、7期の情報から特徴量を抽出するので、dilation=2とすることで、広い範囲から情報を抽出していると解釈できます
このようにdilationを用いることで、連続したデータではなく、より広い範囲(この場合は5期分の情報)から特徴量を抽出することができます。より遠い過去の情報も考慮に入れた、特徴表現が可能になります。
次回
因果畳み込みを利用した時系列分析を行ってみたいと思います。
目次ページ




