【pytorch入門】 例を使ったPytorchの学習で遊んでみた♬

深層学習を始めてずっといだいていたわだかまりがやっと解決しそうです。
つまり、
「深層学習って、逆伝播手法と関数としてテンソル使うということを除けば、単に通常の[最急降下法(Steepest Descent)](https://ja.wikipedia.org/wiki/%E6%9C%80%E6%80%A5%E9%99%8D%E4%B8%8B%E6%B3%95)でのフィッティングと同じだよな」
という思い。
残念ながら、力が無いというか証明は出来ていなかったけど、以下の学習サイトのコードを動かして、ますます上記の思いは確信に変わりました。
ということで、参考サイトのコードを動かして遊んでみます。
合わせて、Pytorchの方針が見えると思います。
【参考】
・[LEARNING PYTORCH WITH EXAMPLES](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html)
###やったこと
・pytorchの考え方
・Warm-up: numpy
・PyTorch: Tensors
・PyTorch: Tensors and autograd
・PyTorch: Defining new autograd functions
・PyTorch: nn
・PyTorch: optim
・PyTorch: Custom nn Modules
・PyTorch: Control Flow + Weight Sharing
###・pytorchの考え方（Google翻訳＋加筆・修正）
「このチュートリアルでは、自己完結型の例を通じてPyTorchの基本的な概念を紹介します。
PyTorchは、その核となる以下の2つの主要な機能を提供します。
・numpy類似の、GPUで実行できるn次元テンソル
・ニューラルネットワークを構築およびトレーニングするための自動微分
実行例として、y = sin（x）を3次多項式で近似する問題を使用します。

フィッティング関数は4つのパラメーターを持つ3次多項式とし、フィッティング関数の出力と学習データの出力の間のユークリッド距離を最小化することにより、ランダムデータに適合するように勾配降下法でトレーニングします。」
###[・Warm-up: numpy](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#warm-up-numpy)
この項が以下のすべての例を理解するために一番大切だと思います。
というか、この項があるので、昔からの関数フィッティングとの連続性が担保されたと感じています。
「PyTorchを紹介する前に、まずnumpyを使用してネットワークを実装します。Numpyは、n次元の配列オブジェクトと、これらの配列を操作するための多くの関数を提供します。Numpyは、科学計算のための一般的なフレームワークです。計算グラフ、深層学習、または勾配については何も知りません。ただし、numpyを使用して、ネットワークを介した順方向パスと逆方向パスを手動で実装することにより、numpyを使用して3次多項式を正弦関数に簡単に適合させることができます。」
コードは、以下のとおりです。なお、コメント等はオリジナルを参照願います。ここでは気に入った別のコメントを追記しています。

```py
# -*- coding: utf-8 -*-
import numpy as np
import math

# Create random input and output data
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)
np.random.seed(0) #再現性のために追記

# Randomly initialize weights
a = np.random.randn()
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()

learning_rate = 1e-6 #学習率
for t in range(10001): #原文2000⇒収束性のため1000毎で10000を出力のため10001に変更
    # Forward pass: compute predicted y
    # y = a + b x + c x^2 + d x^3 a,b,c.dをフィッティング
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss
    loss = np.square(y_pred - y).sum() #二乗誤差, 和はデータ数
    if t % 1000 == 0:
        print(t, loss)

    # Backprop to compute gradients of a, b, c, d with respect to loss　最急降下法でフィッティングのため、勾配を求める
    grad_y_pred = 2.0 * (y_pred - y) #dloss/dy_pred
    grad_a = grad_y_pred.sum() #dloss/dy_pred・dy_pred/da
    grad_b = (grad_y_pred * x).sum() #dloss/dy_pred・dy_pred/db
    grad_c = (grad_y_pred * x ** 2).sum() #dloss/dy_pred・dy_pred/dc 
    grad_d = (grad_y_pred * x ** 3).sum() #dloss/dy_pred・dy_pred/dd

    # Update weights；勾配の逆方向に学習率αx勾配だけ移動させる
    a -= learning_rate * grad_a #a - α・dloss/dy_pred・dy_pred/da
    b -= learning_rate * grad_b #b - α・dloss/dy_pred・dy_pred/db
    c -= learning_rate * grad_c #c - α・dloss/dy_pred・dy_pred/dc
    d -= learning_rate * grad_d #d - α・dloss/dy_pred・dy_pred/dd

print(f'Result: y = {a} + {b} x + {c} x^2 + {d} x^3')
```
df/dx⇒$\frac{\partial f}{\partial x}$の意味
結果は、グラフ表示コードを追加して以下のように求められる。

```py
import matplotlib.pyplot as plt

def plot_data(x,y,y_pred,sk):
    fig, (ax1,ax2) = plt.subplots(2,1,figsize=(1.6180 * 4, 4*2))
    lns1=ax1.plot(x, y, "o-", color="red",label = "data")
    lns2=ax1.plot(x, y_pred, "o-", color="blue",label = "pred")
    lns_ax1 = lns1+lns2
    labs_ax1 = [l.get_label() for l in lns_ax1]
    ax1.legend(lns_ax1, labs_ax1, loc=0)
    lns3=ax2.plot(x, y_pred-y, "o-", color="red",label = "pred-data")
    ax2.legend(loc=0)
    ax1.set_title(sk)
    plt.pause(1)
    plt.savefig('./fig_{}_.png'.format(sk)) 
    plt.close()
    
plot_data(x,y,y_pred,'numpy')
```
以下のようにフィッティング出来ました。
![fig_numpy_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233744/3259b3c5-e7b6-3165-6597-6294b5ff8e9d.png)

```numpy.標準出力.
>python fit_numpy.py
0 1485660.0264192733
1000 86.10126410316815
2000 11.09498140389957
3000 8.88640586815202
4000 8.819306126163202
5000 8.817232190310014
6000 8.81716750293456
7000 8.817165475756841
8000 8.817165412075084
9000 8.817165410072112
10000 8.817165410009073
Result: y = 4.7907191316485336e-08 + 0.8567408422692111 x + -8.26478768023003e-09 x^2 + -0.09333038892615529 x^3
```
###[・PyTorch: Tensors](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-tensors)
「Numpyは優れたフレームワークですが、GPUを利用して数値計算を高速化することはできません。最新のディープニューラルネットワークの場合、GPUは50倍以上のスピードアップを提供することが多いため、残念ながら、最新のディープラーニングにはnumpyでは不十分です。

ここでは、最も基本的なPyTorchの概念であるTensorを紹介します。

PyTorch Tensorは、概念的にはnumpy配列と同じです。Tensorはn次元配列であり、PyTorchはこれらのTensorを操作するための多くの関数を提供します。Tensorsは計算グラフと勾配を追跡できると共に、科学計算の汎用ツールとしても役立ちます。
また、numpyとは異なり、PyTorchTensorsはGPUを利用して数値計算を高速化できます。GPUでPyTorchTensorを実行するには、正しいデバイスを指定するだけです。

ここでは、PyTorchテンソルを使用して、3次多項式を正弦関数に適合させます。上記のnumpyの例と同じように、ネットワークを介して順方向パスと逆方向パスを手動で実装します。」
torchの関数を使う以外は、全く同じコードです。
※対照すると、使い方が分かります

```py
# -*- coding: utf-8 -*-
import torch
import math

dtype = torch.float
device = torch.device("cpu")
#device = torch.device("cuda:0") # Uncomment this to run on GPU
#ウワンの環境はcudaが古いと怒られました

# Create random input and output data
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
torch.manual_seed(0) #再現性のために追記

# Randomly initialize weights
a = torch.randn((), device=device, dtype=dtype)
b = torch.randn((), device=device, dtype=dtype)
c = torch.randn((), device=device, dtype=dtype)
d = torch.randn((), device=device, dtype=dtype)

learning_rate = 1e-6
for t in range(10001):
    # Forward pass: compute predicted y
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss
    loss = (y_pred - y).pow(2).sum().item()
    if t % 1000 == 0:
        print(t, loss)

    # Backprop to compute gradients of a, b, c, d with respect to loss
    grad_y_pred = 2.0 * (y_pred - y)
    grad_a = grad_y_pred.sum()
    grad_b = (grad_y_pred * x).sum()
    grad_c = (grad_y_pred * x ** 2).sum()
    grad_d = (grad_y_pred * x ** 3).sum()

    # Update weights using gradient descent
    a -= learning_rate * grad_a
    b -= learning_rate * grad_b
    c -= learning_rate * grad_c
    d -= learning_rate * grad_d

print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
```
グラフは同一のものが得られました。
標準出力もほぼ同じになります。

```tensor.
>python fit_tensor.py
0 215799.875
1000 130.93563842773438
2000 12.274429321289062
3000 8.919835090637207
4000 8.820301055908203
5000 8.81726360321045
6000 8.817169189453125
7000 8.817167282104492
8000 8.817166328430176
9000 8.817167282104492
10000 8.817167282104492
Result: y = 5.429832938830259e-08 + 0.8567265868186951 x + -1.1671580146810356e-08 x^2 + -0.09332836419343948 x^3
```
###[・PyTorch: Tensors and autograd](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-tensors-and-autograd)
「上記の例では、ニューラルネットワークの順方向パスと逆方向パスの両方を手動で実装しました。後方パスを手動で実装することは、小規模な2層ネットワークでは大したことではありませんが、大規模で複雑なネットワークではすぐに非常に厄介になる可能性があります。
Pytorchを使うと自動微分を使用して、ニューラルネットワークの後方パスの計算を自動化できます。PyTorchのautogradパッケージは、まさにこの機能を提供します。autogradを使用する場合、ネットワークのフォワードパスは計算グラフを定義します。グラフのノードはテンソルになり、エッジは入力テンソルから出力テンソルを生成する関数になります。このグラフを逆伝播すると、勾配を簡単に計算できます。
これは複雑に聞こえますが、実際に使用するのは非常に簡単です。
各テンソルは、計算グラフのノードを表します。 xがx.requires_grad = Trueを持つテンソルである場合、x.gradは、あるスカラー値に関するxの勾配を保持する別のテンソルです。

ここでは、PyTorchテンソルとautogradを使用して、3次多項式の例で正弦波のフィッティングを実装します。これで、ネットワークを介したバックワードパスを手動で実装する必要がなくなりました。」

```py
# -*- coding: utf-8 -*-
import torch
import math

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0")  # Uncomment this to run on GPU

# Create Tensors to hold input and outputs.
# By default, requires_grad=False, which indicates that we do not need to
# compute gradients with respect to these Tensors during the backward pass.
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
torch.manual_seed(0)

# Create random Tensors for weights. For a third order polynomial, we need
# 4 weights: y = a + b x + c x^2 + d x^3
# Setting requires_grad=True indicates that we want to compute gradients with
# respect to these Tensors during the backward pass.
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(10001):
    # Forward pass: compute predicted y using operations on Tensors.
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss using operations on Tensors.
    # Now loss is a Tensor of shape (1,)
    # loss.item() gets the scalar value held in the loss.
    loss = (y_pred - y).pow(2).sum()
    if t % 1000 == 0:
        print(t, loss.item())

    # Use autograd to compute the backward pass. This call will compute the
    # gradient of loss with respect to all Tensors with requires_grad=True.
    # After this call a.grad, b.grad. c.grad and d.grad will be Tensors holding
    # the gradient of the loss with respect to a, b, c, d respectively.
    loss.backward()

    # Manually update weights using gradient descent. Wrap in torch.no_grad()
    # because weights have requires_grad=True, but we don't need to track this
    # in autograd.
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad

        # Manually zero the gradients after updating weights
        a.grad = None
        b.grad = None
        c.grad = None
        d.grad = None

print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
```
つまり、loss.backward()でa.grad（このテンソルに勾配が格納される）などを自動計算して、それを使って、with torch.no_grad():のところでパラメータa,b,c,dを更新します。ここがPytorchのおいしいところです。
loss.[backward()](https://pytorch.org/docs/stable/autograd.html#torch.Tensor.backward)関数の詳細はリンク先を参照しましょう。
結果は、当然ですが、以下のように上記のtorch.tensorと同じになります。

```autograd.
>python fit_autograd.py
0 215799.875
1000 130.93563842773438
2000 12.274429321289062
3000 8.919835090637207
4000 8.820301055908203
5000 8.81726360321045
6000 8.817169189453125
7000 8.817167282104492
8000 8.817166328430176
9000 8.817167282104492
10000 8.817167282104492
Result: y = 5.429832938830259e-08 + 0.8567265868186951 x + -1.1671580146810356e-08 x^2 + -0.09332836419343948 x^3
```
###[・PyTorch: Defining new autograd functions](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-defining-new-autograd-functions)
「内部的には、各プリミティブautograd演算子は、実際にはTensorを操作する2つの関数forward関数とbackward関数です。
forward関数は、入力テンソルから出力テンソルを計算します。
backward関数は、あるスカラー値に関する出力テンソルの勾配を受け取ります、そして同じスカラー値に関する入力テンソルの勾配を計算します。

PyTorchでは、torch.autograd.Functionのサブクラスを定義し、フォワード関数とバックワード関数を実装することで、独自のautograd演算子を簡単に定義できます。
次に、インスタンスを作成して関数のように呼び出し、入力データを含むTensorを渡すことで、新しいautograd演算子を使用できます。

この例では、モデルを$y = a + bx + cx^2 + dx^3$ではなく$y = a + bP_3（c + dx）$として定義します。ここで、$P_3（x）= \frac{1}{2}（5x^3-3x）$は次数3の[ルジャンドル多項式](https://ja.wikipedia.org/wiki/%E3%83%AB%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%89%E3%83%AB%E5%A4%9A%E9%A0%85%E5%BC%8F#:~:text=%E3%83%AB%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%89%E3%83%AB%E5%A4%9A%E9%A0%85%E5%BC%8F%EF%BC%88%E3%83%AB%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%89%E3%83%AB%E3%81%9F%E3%81%93%E3%81%86,%E5%A4%9A%E9%A0%85%E5%BC%8F%E3%81%AE%E4%B8%80%E7%A8%AE%E3%81%A7%E3%81%82%E3%82%8B%E3%80%82)です。」

```py
# -*- coding: utf-8 -*-
import torch
import math

class LegendrePolynomial3(torch.autograd.Function):
    """
    We can implement our own custom autograd Functions by subclassing
    torch.autograd.Function and implementing the forward and backward passes
    which operate on Tensors.
    """

    @staticmethod
    def forward(ctx, input):
        """
        In the forward pass we receive a Tensor containing the input and return
        a Tensor containing the output. ctx is a context object that can be used
        to stash information for backward computation. You can cache arbitrary
        objects for use in the backward pass using the ctx.save_for_backward method.
        """
        ctx.save_for_backward(input)
        return 0.5 * (5 * input ** 3 - 3 * input)

    @staticmethod
    def backward(ctx, grad_output):
        """
        In the backward pass we receive a Tensor containing the gradient of the loss
        with respect to the output, and we need to compute the gradient of the loss
        with respect to the input.
        """
        input, = ctx.saved_tensors
        return grad_output * 1.5 * (5 * input ** 2 - 1)

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0")  # Uncomment this to run on GPU

# Create Tensors to hold input and outputs.
# By default, requires_grad=False, which indicates that we do not need to
# compute gradients with respect to these Tensors during the backward pass.
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
torch.manual_seed(0)

# Create random Tensors for weights. For this example, we need
# 4 weights: y = a + b * P3(c + d * x), these weights need to be initialized
# not too far from the correct result to ensure convergence.
# Setting requires_grad=True indicates that we want to compute gradients with
# respect to these Tensors during the backward pass.
a = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)
c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)

learning_rate = 5e-6
for t in range(10001):
    # To apply our Function, we use Function.apply method. We alias this as 'P3'.
    P3 = LegendrePolynomial3.apply

    # Forward pass: compute predicted y using operations; we compute
    # P3 using our custom autograd operation.
    y_pred = a + b * P3(c + d * x)

    # Compute and print loss
    loss = (y_pred - y).pow(2).sum()
    if t % 1000 == 0:
        print(t, loss.item())

    # Use autograd to compute the backward pass.
    loss.backward()

    # Update weights using gradient descent
    with torch.no_grad():
        a -= learning_rate * a.grad
        b -= learning_rate * b.grad
        c -= learning_rate * c.grad
        d -= learning_rate * d.grad

        # Manually zero the gradients after updating weights
        a.grad = None
        b.grad = None
        c.grad = None
        d.grad = None

print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
```
結果は、グラフは同じ。残差も以下の通りほぼ同一です。

```newautograd.
>python fit_newautograd.py
0 461.902587890625
1000 14.854462623596191
2000 8.94315242767334
3000 8.819803237915039
4000 8.817222595214844
5000 8.817169189453125
6000 8.817169189453125
7000 8.817169189453125
8000 8.817169189453125
9000 8.817169189453125
10000 8.817169189453125
Result: y = -7.459941109289048e-09 + -2.2340033054351807 * P3(2.3496515844101395e-09 + 0.2556593716144562 x)
```
因みに、P3のbackward関数を削除してみると、以下のエラーがでました。
NotImplementedError: You must implement the backward function for custom autograd.Function.
やはり定義しないと自動計算しないんですね。
###[・PyTorch: nn](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-nn)
「計算グラフとautogradは、複雑な演算子を定義し、導関数を自動的に取得するための非常に強力なパラダイムです。ただし、大規模なニューラルネットワークの場合、生のautogradは少し低レベルになる可能性があります。
ニューラルネットワークを構築するとき、私たちは計算を層に配置することをよく考えます。そのいくつかには、学習中に最適化される学習可能なパラメーターがあります。
TensorFlowでは、Keras、TensorFlow-Slim、TFLearnなどのパッケージが、ニューラルネットワークの構築に役立つ生の計算グラフに対する高レベルの抽象化を提供します。
PyTorchでは、nnパッケージがこれと同じ目的を果たします。 nnパッケージは、ニューラルネットワーク層とほぼ同等のモジュールのセットを定義します。モジュールは入力テンソルを受け取り、出力テンソルを計算しますが、学習可能なパラメーターを含むテンソルなどの内部状態を保持する場合もあります。nnパッケージは、ニューラルネットワークのトレーニング時に一般的に使用される一連の有用な損失関数も定義します。
この例では、nnパッケージを使用して、多項式モデルネットワークを実装します。」

```py
# -*- coding: utf-8 -*-
import torch
import math

# Create Tensors to hold input and outputs.
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
torch.manual_seed(0)

# For this example, the output y is a linear function of (x, x^2, x^3), so
# we can consider it as a linear layer neural network. Let's prepare the
# tensor (x, x^2, x^3).
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

# In the above code, x.unsqueeze(-1) has shape (2000, 1), and p has shape
# (3,), for this case, broadcasting semantics will apply to obtain a tensor
# of shape (2000, 3) 

# Use the nn package to define our model as a sequence of layers. nn.Sequential
# is a Module which contains other Modules, and applies them in sequence to
# produce its output. The Linear Module computes output from input using a
# linear function, and holds internal Tensors for its weight and bias.
# The Flatten layer flatens the output of the linear layer to a 1D tensor,
# to match the shape of `y`.
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)

# The nn package also contains definitions of popular loss functions; in this
# case we will use Mean Squared Error (MSE) as our loss function.
loss_fn = torch.nn.MSELoss(reduction='sum')

learning_rate = 1e-6
for t in range(10001):

    # Forward pass: compute predicted y by passing x to the model. Module objects
    # override the __call__ operator so you can call them like functions. When
    # doing so you pass a Tensor of input data to the Module and it produces
    # a Tensor of output data.
    y_pred = model(xx)

    # Compute and print loss. We pass Tensors containing the predicted and true
    # values of y, and the loss function returns a Tensor containing the
    # loss.
    loss = loss_fn(y_pred, y)
    if t % 1000 == 0:
        print(t, loss.item())

    # Zero the gradients before running the backward pass.
    model.zero_grad()

    # Backward pass: compute gradient of the loss with respect to all the learnable
    # parameters of the model. Internally, the parameters of each Module are stored
    # in Tensors with requires_grad=True, so this call will compute gradients for
    # all learnable parameters in the model.
    loss.backward()

    # Update the weights using gradient descent. Each parameter is a Tensor, so
    # we can access its gradients like we did before.
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad

# You can access the first layer of `model` like accessing the first item of a list
linear_layer = model[0]

# For linear layer, its parameters are stored as `weight` and `bias`.
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x + {linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
```
上記のコードでややこしいのは、以下のunsqueeze(-1)の部分だと思います。

```py
>>> import torch
>>> x = torch.tensor([1,2,3,4])
>>> p = torch.tensor([1,2,3])
>>> xx = x.unsqueeze(-1)
>>> xx
tensor([[1],
        [2],
        [3],
        [4]])
>>> xx = x.unsqueeze(-1).pow(p)
>>> xx
tensor([[ 1,  1,  1],
        [ 2,  4,  8],
        [ 3,  9, 27],
        [ 4, 16, 64]])
```
つまり、入力xが(2000, 1)の行列ですが、これから(2000, 3) の行列に変換するということです。こうすることにより、tensor (x, x^2, x^3)を得ていることになります。
そして、ネットワークモデルは、以下のように定義できます。

```py
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)
```
この部分のコメント訳すと以下のとおりになります。
「nnパッケージを使用して、モデルを一連のレイヤーとして定義します。 nn.Sequentialは他のモジュールを含むモジュールであり、それらを順番に適用し、その出力を生成します。
線形モジュールは、線形関数を使用して入力からの出力を計算し、その重みとバイアスの内部テンソルを保持します。 Flatten層は、線形層の出力を`y`の形に合わせ、1Dテンソルに平坦化します。」
また、loss関数はパッケージにMean Squared Error (MSE) を利用して、以下のように定義しています。
`loss_fn = torch.nn.MSELoss(reduction='sum')`

利用は、イテレーションの中で、以下のように先ほどのxxを利用して、
`y_pred = model(xx)`
を計算したあと、y_predとyを入力してlossを計算しています。
`loss = loss_fn(y_pred, y)`
そして、
`model.zero_grad()`
で勾配を0にしたあと、
`loss.backward()`
で勾配param.gradを計算しています。
最後に、以下でパラメータparamを更新しています。

```py
with torch.no_grad():
    for param in model.parameters():
        param -= learning_rate * param.grad
```
そして、
linear_layer = model[0]
として、a, b, c, dは以下で得られるんですね。

```math
\begin{eqnarray}
\left[
\begin{array}{c}
a\\
b\\
c\\
d\\
\end{array}
\right]=
\left[
\begin{array}{ccc}
\textrm{ linear_layer.bias.item()}\\
\textrm{ linear_layer.weight[:, 0].item()}\\
\textrm{ linear_layer.weight[:, 1].item()}\\
\textrm{ linear_layer.weight[:, 2].item()}\\
\end{array}
\right]
\end{eqnarray}
```
結果は、上記と変わりません。

```nn.
>python fit_nn.py
0 73111.2734375
1000 25.393537521362305
2000 9.175847053527832
3000 8.825852394104004
4000 8.817399978637695
5000 8.817174911499023
6000 8.817169189453125
7000 8.817169189453125
8000 8.817168235778809
9000 8.817169189453125
10000 8.817168235778809
Result: y = -1.4384004209944123e-08 + 0.8567266464233398 x + 3.5412108800869646e-09 x^2 + -0.09332837164402008 x^3
```
###[・PyTorch: optim](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-optim)
「ここまで、torch.no_grad（）を使用して学習可能なパラメーターを保持するテンソルを手動で変更することにより、モデルの重みを更新しました。
これは確率的勾配降下法のような単純な最適化アルゴリズムにとって大きな負担ではありませんが、実際には、AdaGrad、RMSProp、Adamなどのより高度なオプティマイザーを使用してニューラルネットワークをトレーニングすることがよくあります。
PyTorchのoptimパッケージは、最適化アルゴリズムの概念を抽象化し、一般的に使用される最適化アルゴリズムの実装を提供します。
この例では、nnパッケージを使用して以前と同じようにモデルを定義しますが、optimパッケージによって提供されるRMSpropアルゴリズムを使用してモデルを最適化します。」

```py
# -*- coding: utf-8 -*-
import torch
import math


# Create Tensors to hold input and outputs.
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
torch.manual_seed(0)

# Prepare the input tensor (x, x^2, x^3).
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

# Use the nn package to define our model and loss function.
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)
loss_fn = torch.nn.MSELoss(reduction='sum')

# Use the optim package to define an Optimizer that will update the weights of
# the model for us. Here we will use RMSprop; the optim package contains many other
# optimization algorithms. The first argument to the RMSprop constructor tells the
# optimizer which Tensors it should update.
learning_rate = 1e-3
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
for t in range(10001):
    # Forward pass: compute predicted y by passing x to the model.
    y_pred = model(xx)

    # Compute and print loss.
    loss = loss_fn(y_pred, y)
    if t % 1000 == 0:
        print(t, loss.item())

    # Before the backward pass, use the optimizer object to zero all of the
    # gradients for the variables it will update (which are the learnable
    # weights of the model). This is because by default, gradients are
    # accumulated in buffers( i.e, not overwritten) whenever .backward()
    # is called. Checkout docs of torch.autograd.backward for more details.
    optimizer.zero_grad()

    # Backward pass: compute gradient of the loss with respect to model
    # parameters
    loss.backward()

    # Calling the step function on an Optimizer makes an update to its
    # parameters
    optimizer.step()

linear_layer = model[0]
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x + {linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
```
このコードは以下のoptimizerの部分を除くと、上記とほぼ同じです。

```py
learning_rate = 1e-3
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
```
そして、iterationの中では、以下のように更新するのですね。

```py
optimizer.zero_grad()
loss.backward()
optimizer.step()
```
結果は、以下のとおり今回もほぼ同一になりました。

```optim.
>python fit_optim.py
0 73111.2734375
1000 75.7530288696289
2000 8.962709426879883
3000 8.920434951782227
4000 8.920767784118652
5000 8.920764923095703
6000 8.92076301574707
7000 8.920763969421387
8000 8.920766830444336
9000 8.920767784118652
10000 8.920764923095703
Result: y = -0.0005000222590751946 + 0.8562408089637756 x + -0.0005000336095690727 x^2 + -0.09383037686347961 x
```
###[・PyTorch: Custom nn Modules](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-custom-nn-modules)
「既存のモジュールのシーケンスよりも複雑なモデルを指定したい場合があります。このような場合、nn.Moduleをサブクラス化し、他のモジュールまたはTensorの他のautograd操作を使用して、入力Tensorを受け取り、出力Tensorを生成するフォワードを定義することで、独自のモジュールを定義できます。
この例では、3次多項式をカスタムモジュールサブクラスとして実装します。」

```py
# -*- coding: utf-8 -*-
import torch
import math

class Polynomial3(torch.nn.Module):
    def __init__(self):
        """
        In the constructor we instantiate four parameters and assign them as
        member parameters.
        """
        super().__init__()
        self.a = torch.nn.Parameter(torch.randn(()))
        self.b = torch.nn.Parameter(torch.randn(()))
        self.c = torch.nn.Parameter(torch.randn(()))
        self.d = torch.nn.Parameter(torch.randn(()))

    def forward(self, x):
        """
        In the forward function we accept a Tensor of input data and we must return
        a Tensor of output data. We can use Modules defined in the constructor as
        well as arbitrary operators on Tensors.
        """
        return self.a + self.b * x + self.c * x ** 2 + self.d * x ** 3

    def string(self):
        """
        Just like any class in Python, you can also define custom method on PyTorch modules
        """
        return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3'

# Create Tensors to hold input and outputs.
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
torch.manual_seed(0)

# Construct our model by instantiating the class defined above
model = Polynomial3()

# Construct our loss function and an Optimizer. The call to model.parameters()
# in the SGD constructor will contain the learnable parameters of the nn.Linear
# module which is members of the model.
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
for t in range(10001):
    # Forward pass: Compute predicted y by passing x to the model
    y_pred = model(x)

    # Compute and print loss
    loss = criterion(y_pred, y)
    if t % 1000 == 0:
        print(t, loss.item())

    # Zero gradients, perform a backward pass, and update the weights.
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print(f'Result: {model.string()}')
```
コードは比較的分かり易くなったと思います。
結果も、三次多項式でのフィッティングなので、変わりません。

```custom_nn.
>python fit_custom_nn.py
0 215799.875
1000 130.93563842773438
2000 12.274429321289062
3000 8.919835090637207
4000 8.820301055908203
5000 8.81726360321045
6000 8.817169189453125
7000 8.817167282104492
8000 8.817166328430176
9000 8.817167282104492
10000 8.817167282104492
Result: y = 5.429832938830259e-08 + 0.8567265868186951 x + -1.1671580146810356e-08 x^2 + -0.09332836419343948 x^3
```
###[・PyTorch: Control Flow + Weight Sharing](https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-control-flow-weight-sharing)
「動的グラフと重み共有の例として、非常に奇妙なモデルを実装します。3〜5次の多項式は、各フォワードパスで3〜5を乱数で選択し、その数の次数を使用して、同じ重みを複数回再利用して4次と5次を計算します。
このモデルでは、通常のPythonフロー制御を使用してループを実装できます。また、フォワードパスを定義するときに同じパラメーターを再利用するだけで、重みの共有を実装できます。

このモデルは、モジュールサブクラスとして簡単に実装できます。」

```py
# -*- coding: utf-8 -*-
import random
import torch
import math

class DynamicNet(torch.nn.Module):
    def __init__(self):
        """
        In the constructor we instantiate five parameters and assign them as members.
        """
        super().__init__()
        self.a = torch.nn.Parameter(torch.randn(()))
        self.b = torch.nn.Parameter(torch.randn(()))
        self.c = torch.nn.Parameter(torch.randn(()))
        self.d = torch.nn.Parameter(torch.randn(()))
        self.e = torch.nn.Parameter(torch.randn(()))

    def forward(self, x):
        """
        For the forward pass of the model, we randomly choose either 4, 5
        and reuse the e parameter to compute the contribution of these orders.

        Since each forward pass builds a dynamic computation graph, we can use normal
        Python control-flow operators like loops or conditional statements when
        defining the forward pass of the model.

        Here we also see that it is perfectly safe to reuse the same parameter many
        times when defining a computational graph.
        """
        y = self.a + self.b * x + self.c * x ** 2 + self.d * x ** 3
        for exp in range(4, random.randint(4, 6)):
            y = y + self.e * x ** exp
        return y

    def string(self):
        """
        Just like any class in Python, you can also define custom method on PyTorch modules
        """
        return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3 + {self.e.item()} x^4 ? + {self.e.item()} x^5 ?'


# Create Tensors to hold input and outputs.
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)
torch.manual_seed(0)

# Construct our model by instantiating the class defined above
model = DynamicNet()

# Construct our loss function and an Optimizer. Training this strange model with
# vanilla stochastic gradient descent is tough, so we use momentum
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9)
for t in range(30001):
    # Forward pass: Compute predicted y by passing x to the model
    y_pred = model(x)

    # Compute and print loss
    loss = criterion(y_pred, y)
    if t % 3000 == 0:
        print(t, loss.item())

    # Zero gradients, perform a backward pass, and update the weights.
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print(f'Result: {model.string()}')
```
このコードのポイントは、関数定義で以下のように最後の4次と5次の係数を共有するところにあります。

```py
y = self.a + self.b * x + self.c * x ** 2 + self.d * x ** 3
for exp in range(4, random.randint(4, 6)):
    y = y + self.e * x ** exp
return y
```
結果は、フィッティング出来て以下のようになりました。
ただし、以下から分かるように収束性が悪く、ここでは30000回のイテレーションを実施しています。
また、当然ですが、4次と5次の係数は同一になっています。

```ctlflow_w-share.
>python fit_ctlflow_w-share.py
0 21656488.0
3000 1555.84033203125
6000 529.0192260742188
9000 184.91854858398438
12000 81.51339721679688
15000 28.773971557617188
18000 15.99734878540039
21000 10.925525665283203
24000 9.43151569366455
27000 9.179972648620605
30000 8.621814727783203
Result: y = 0.011209576390683651 + 0.8553824424743652 x + -0.0025682300329208374 x^2 + -0.09357346594333649 x^3 + 0.00013947369006928056 x^4 ? + 0.00013947369006928056 x^5 ?
```
###まとめ
・Pytorch入門の例題を動かしてみて、Pytorchパッケージを利用して関数フィッティングの仕方を見た
・従来のnumpyによる最急降下法からの流れを理解できた
・Pytorchの思想というか考え方の一端を理解できた

・実際に二次元画像や時系列データの解析が出来るようになったわけではないので、今後丁寧に理解していこうと思う