PyTorchのConv1dを理解する

**【更新】より詳しく、分かりやすく解説した改訂版を[note](https://note.com/tshishou/n/ne0d8d81d1ee0?sub_rt=share_b)にて無料公開しています。
　　　　ぜひご覧ください。**

PyTorchバージョン：1.9.0

[Conv1dについての公式説明](https://pytorch.org/docs/stable/generated/torch.nn.Conv1d.html#torch.nn.Conv1d)

`Conv1d`のコンストラクターに指定しないといけないパラメータは順番に下記三つあります。

1. 入力チャネル数（`in_channels`）
1. 出力チャネル数（`out_channels`）
1. カーネルサイズ（`kernel_size`）

例えば、下記のソースコードは入力チャネル数2、出力チャネル数3、カーネルサイズ5の`Conv1d`インスタンスを作成します。

```{python}
from torch import nn

conv1d = nn.Conv1d(2,3,5)
```

オブジェクト作成できたら、初期化された重みとバイアスが確認できます。

```{python}
>>> conv1d.weight.shape
torch.Size([3, 2, 5])
>>> conv1d.weight
Parameter containing:
tensor([[[ 0.2594, -0.2927,  0.3010, -0.3144, -0.0263],
         [ 0.1818,  0.1792, -0.1513,  0.0448,  0.2669]],

        [[-0.1189,  0.1470, -0.1873, -0.1977,  0.0357],
         [ 0.1807,  0.0479,  0.2231, -0.2369, -0.1685]],

        [[ 0.0283,  0.0707,  0.0137, -0.0436, -0.2092],
         [ 0.1842,  0.2262, -0.1358, -0.1469,  0.0953]]], requires_grad=True)
>>> conv1d.bias
Parameter containing:
tensor([ 0.1507, -0.0665,  0.2158], requires_grad=True)
```

作成されたインスタンス`conv1d`を使うために、畳み込みの入力配列を用意しましょう。入力配列は三次元でなければなりません。

1. 一次元目のサイズ：[バッチサイズ](https://qiita.com/kenta1984/items/bad75a37d552510e4682#%E3%83%90%E3%83%83%E3%83%81%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%A8%E3%81%AF)（`batch_size`）
1. 二次元目のサイズ：入力チャネル数、`conv1d`インスタンスを作成したときに指定した入力チャネル数と一致しないといけません
1. 三次元目のサイズ：一件のデータの長さ、例えば、音声の場合、時間軸のサンプル（フレーム）数が一般的でしょう

```{python}
>>> import torch
>>> x = torch.rand(4,2,6)
>>> x
tensor([[[0.8598, 0.9945, 0.8397, 0.0875, 0.1347, 0.2212],
         [0.9039, 0.9663, 0.2980, 0.4002, 0.8641, 0.1295]],

        [[0.6044, 0.1435, 0.9415, 0.6749, 0.1406, 0.5504],
         [0.5129, 0.1664, 0.3843, 0.5065, 0.4144, 0.2583]],

        [[0.9566, 0.8054, 0.3213, 0.8039, 0.4228, 0.9182],
         [0.8417, 0.0937, 0.0542, 0.2004, 0.9569, 0.3480]],

        [[0.7783, 0.7377, 0.9412, 0.3135, 0.7974, 0.1117],
         [0.5227, 0.1919, 0.0875, 0.8341, 0.0841, 0.1266]]])
```

畳み込みの計算結果を確認しましょう。

```{python}
>>> y = conv1d(x)
>>> y.shape
torch.Size([4, 3, 2])
>>> y
tensor([[[ 0.8454,  0.3828],
         [-0.1566, -0.0447],
         [ 0.6581,  0.3289]],

        [[ 0.5311,  0.1668],
         [-0.4254, -0.0599],
         [ 0.2421,  0.1869]],

        [[ 0.4219,  0.4825],
         [-0.3059, -0.5375],
         [ 0.4113, -0.0433]],

        [[ 0.4764, -0.1308],
         [-0.3490, -0.0444],
         [ 0.1357,  0.1910]]], grad_fn=<SqueezeBackward1>)

```

次に上記の計算結果をどう導き出すかを説明します。

入力配列`x`のバッチサイズは4です。即ち、四件のデータがあります。畳み込み演算は`x[0]`、`x[1]`、`x[2]`と`x[3]`にそれぞれ行われて、`y[0]`、`y[1]`、`y[2]`と`y[3]`が得られました。

`conv1d.weight[0]`と`conv1d.bias[0]`は`y[:,0]`（出力チャネル0）に対応します。

```{python}
>>> conv1d.weight[0]
tensor([[ 0.2594, -0.2927,  0.3010, -0.3144, -0.0263],
        [ 0.1818,  0.1792, -0.1513,  0.0448,  0.2669]],
       grad_fn=<SelectBackward>)
>>> conv1d.weight[0,0]
tensor([ 0.2594, -0.2927,  0.3010, -0.3144, -0.0263], grad_fn=<SelectBackward>)
>>> y[0,0]  # 0件目データに対して、チャネル0の出力
tensor([0.8454, 0.3828], grad_fn=<SelectBackward>)
>>> x[0,0,0:5].dot(conv1d.weight[0,0]) + x[0,1,0:5].dot(conv1d.weight[0,1]) + conv1d.bias[0]
tensor(0.8454, grad_fn=<AddBackward0>)
>>> x[0,0,1:6].dot(conv1d.weight[0,0]) + x[0,1,1:6].dot(conv1d.weight[0,1]) + conv1d.bias[0]
tensor(0.3828, grad_fn=<AddBackward0>)
>>> y[1,0]  # 1件目データに対して、チャネル0の出力 
tensor([0.5311, 0.1668], grad_fn=<SelectBackward>)
>>> x[1,0,0:5].dot(conv1d.weight[0,0]) + x[1,1,0:5].dot(conv1d.weight[0,1]) + conv1d.bias[0]
tensor(0.5311, grad_fn=<AddBackward0>)
>>> x[1,0,1:6].dot(conv1d.weight[0,0]) + x[1,1,1:6].dot(conv1d.weight[0,1]) + conv1d.bias[0]
tensor(0.1668, grad_fn=<AddBackward0>)
```

`conv1d.weight[1]`と`conv1d.bias[1]`は`y[:,1]`（出力チャネル1）に対応します。

```{python}
>>> y[0,1]  # 0件目データに対して、チャネル1の出力
tensor([-0.1566, -0.0447], grad_fn=<SelectBackward>)
>>> x[0,0,0:5].dot(conv1d.weight[1,0]) + x[0,1,0:5].dot(conv1d.weight[1,1]) + conv1d.bias[1]
tensor(-0.1566, grad_fn=<AddBackward0>)
>>> x[0,0,1:6].dot(conv1d.weight[1,0]) + x[0,1,1:6].dot(conv1d.weight[1,1]) + conv1d.bias[1]
tensor(-0.0447, grad_fn=<AddBackward0>)
>>> y[1,1]  # 1件目データに対して、チャネル1の出力
tensor([-0.4254, -0.0599], grad_fn=<SelectBackward>)
>>> x[1,0,0:5].dot(conv1d.weight[1,0]) + x[1,1,0:5].dot(conv1d.weight[1,1]) + conv1d.bias[1]
tensor(-0.4254, grad_fn=<AddBackward0>)
>>> x[1,0,1:6].dot(conv1d.weight[1,0]) + x[1,1,1:6].dot(conv1d.weight[1,1]) + conv1d.bias[1]
tensor(-0.0599, grad_fn=<AddBackward0>)
```