PyTorch 2.0 チュートリアル : 学習 : 例題による PyTorch の学習

PyTorch 2.0 チュートリアル : 学習 : 例題による PyTorch の学習 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 03/24/2023 (2.0.0)

* 本ページは、PyTorch 2.0 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

  • 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
  • sales-info@classcat.com  ;  Website: www.classcat.com  ;   ClassCatJP

 

PyTorch 2.0 チュートリアル : 学習 : 例題による PyTorch の学習

このチュートリアルは自己完結的なサンプルを通して PyTorch の基礎的な概念を紹介します。

その中核として、PyTorch は2つの主要な特徴を提供しています :

  • n-次元テンソル、numpy に類似していますが GPU 上で動作可能です。
  • ニューラルネットワークを構築して訓練するための自動微分

実行サンプルとして $y=\sin(x)$ を 3 次多項式で適合させる問題を使用します。ネットワークは 4 つのパラメータを持ち、そしてネットワーク出力と真の出力の間のユークリッド距離を最小化するすることによりランダムデータに適合させるために勾配降下で訓練されます。

 

テンソル

ウォームアップ: numpy

PyTorch を紹介する前に、最初に numpy を使用してネットワークを実装します。

Numpy は n-次元配列オブジェクト、そしてこれらの配列を操作するための多くの関数を提供します。Numpy は科学計算のための一般的なフレームワークです ; それは計算グラフや、深層学習や、勾配については何も知りません。けれどもサイン関数に 3 次元多項式を適合させるために、numpy 演算を使用してネットワークを通して forward と backward パスを手動で実装することにより、容易に numpy を利用できます :

# -*- 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)

# 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(2000):
    # Forward pass: compute predicted y
    # y = a + b x + c x^2 + d x^3
    y_pred = a + b * x + c * x ** 2 + d * x ** 3

    # Compute and print loss
    loss = np.square(y_pred - y).sum()
    if t % 100 == 99:
        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
    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} + {b} x + {c} x^2 + {d} x^3')

 

PyTorch: テンソル

Numpy は素晴らしいフレームワークですが、それは数値計算を加速するために GPU を利用することができません。現代的な深層ニューラルネットワークについては、GPU は 50 倍またはそれ以上 のスピードアップを提供することも多いので、残念ながら numpy は現代的な深層学習のためには十分ではありません。

ここでは最も基礎的な PyTorch 概念: テンソル を紹介します。PyTorch Tensor は概念的には numpy 配列と等値です : テンソルは n-次元配列で、そして PyTorch はこれらのテンソル上で演算するための多くの関数を提供します。内部的には、テンソルは計算グラフと勾配を追跡することができますが、それらは科学計算のための一般的なツールとしても有用です。

また numpy とは違い、PyTorch テンソルはそれらの数値計算を加速するために GPU を活用することができます。PyTorch テンソルを GPU 上で実行するためには、単純に正しいデバイスを指定する必要があるだけです。

ここでは 3 次元多項式をサイン関数に適合させるために PyTorch テンソルを使用します。上の numpy サンプルのようにネットワークを通して forward と backward パスを手動で実装する必要があります :

# -*- 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 random input and output data
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 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(2000):
    # 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 % 100 == 99:
        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')

 

Autograd

PyTorch: テンソルと autograd

上のサンプルでは、ニューラルネットワークの forward と backward パスの両方を手動で実装しなければなりませんでした。backward パスを手動で実装することは小さな 2-層ネットワークに対しては大したことはありませんが、大規模で複雑なネットワークに対しては非常に困難なものに直ちになり得るでしょう。

ありがたいことに、ニューラルネットワークの backward パスの計算を自動化する 自動微分 を利用できます。PyTorch の autograd パッケージは正確にこの機能を提供します。autograd を使用するとき、貴方のネットワークの forward パスは 計算グラフ を定義します ; グラフのノードはテンソルで、エッジは入力テンソルから出力テンソルを生成する関数です。それからこのグラフを通して逆伝播すれば、貴方に簡単に勾配を計算することを可能にします。

これは複雑に聞こえますが、実際に利用することは非常に単純です。各テンソルは計算グラフのノードを表します。もし x が “x.requires_grad=True” を持つテンソルであれば、x.grad はあるスカラー値に関する x の勾配を保持する別のテンソルです。

ここではフィッティングさせているサイン波を 3 次多項式サンプルで実装するために PyTorch テンソルと autograd を使用します ; 今ではネットワークを通して backward パスを手動で実装する必要はもはやありません :

# -*- 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)

# 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(2000):
    # 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 % 100 == 99:
        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')

 

PyTorch: 新しい autograd 関数を定義する

内部的には、各々のプリミティブな autograd 演算子は実際にはテンソル上で作用する2つの関数です。forward 関数は入力テンソルから出力テンソルを計算します。backward 関数はあるスカラー値に関する出力テンソルの勾配を受け取り、同じスカラー値に関する入力テンソルの勾配を計算します。

PyTorch では torch.autograd.Function のサブクラスを定義して forward と backward 関数を実装することにより私達自身の autograd 演算子を簡単に定義できます。そしてインスタンスを構築してそれを関数のように呼び出して、入力データを含むテンソルを渡すことにより新しい autograd 演算子を利用できます。

このサンプルでは私達のモデルを \(y=a+bx+cx^2+dx^3\) の代わりに \(y=a+b P_3(c+dx)\) として定義します、ここで \(P_3(x)=\frac{1}{2}\left(5x^3-3x\right)\) は 3 次の ルジャンドル多項式 です。\(P_3\) の forward と backward を計算するために私達自身の autograd 関数を書いてそれをモデルを実装するために利用します :

# -*- 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)

# 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(2000):
    # 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 % 100 == 99:
        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)')

 

nn モジュール

PyTorch: nn

計算グラフと autograd は複雑な演算子を定義して導関数を自動的に取るための非常にパワフルなパラダイムです ; けれども大規模なニューラルネットワークのためには raw autograd は少し低位過ぎます。

ニューラルネットワークを構築するとき私達は計算を 内に配置することを考えることが多いです、それらの幾つかは学習の間に最適化される 学習可能なパラメータ を持ちます。

TensorFlow では、Keras, TensorFlow-Slim と TFLearn のようなパッケージが raw 計算グラフを越える高位な抽象を提供します、これらはニューラルネットワークを構築するために有用です。

PyTorch では、nn パッケージがこの同じ目的のために役立ちます。nn パッケージは モジュール のセットを定義し、これはニューラルネットワーク層におおよそ同値です。モジュールは入力テンソルを受け取り出力テンソルを計算しますが、学習可能なパラメータを含むテンソルのような内部状態もまた保持することができます。nn パッケージはまた、ニューラルネットワークを訓練するとき一般に使用される有用な損失関数のセットも定義します。

このサンプルでは私達の多項式モデルネットワークを実装するために nn パッケージを利用します :

# -*- 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)

# 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(2000):

    # 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 % 100 == 99:
        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')

 

PyTorch: optim

ここまで torch.no_grad() を持つ学習可能なパラメータを保持するテンソルを手動で変更することによりモデルの重みを更新してきました。これは確率的勾配降下のような単純な最適化アルゴリズムのためには大きな重荷ではありませんが、実際には私達は AdaGrad, RMSProp, Adam 等のようなより洗練された optimizer を使用してニューラルネットワークを訓練することが多いです。

PyTorch の optim パッケージは最適化アルゴリズムの考えを抽象化して一般に使用される最適化アルゴリズムの実装を提供します。

このサンプルでは前のように私達のモデルを定義するために nn パッケージを使用しますが、optim パッケージで提供される RMSprop アルゴリズムを使用してモデルを最適化します :

# -*- 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)

# 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(2000):
    # 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 % 100 == 99:
        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')

 

PyTorch: カスタム nn Module

時に既存の Module のシークエンスよりも複雑なモデルを指定することを望むでしょう ; それらのケースのために、nn.Module をサブクラス化して (入力テンソルを受け取り、他のモジュールやテンソル上の他の autograd 演算を使用して出力テンソルを生成する) forward を定義することにより貴方自身の Module を定義できます。

このサンプルでは私達の 3 次多項式をカスタム Module サブクラスとして実装します :

# -*- 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)

# 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 (defined 
# with torch.nn.Parameter) which are members of the model.
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
for t in range(2000):
    # 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 % 100 == 99:
        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()}')

 

PyTorch: 制御フロー + 重み共有

動的グラフと重み共有のサンプルとして、非常に奇妙なモデルを実装します : 3-5 次多項式で、これは各 forward パス上で 3 と 5 の間のランダム数を選択してその数を利用し、4 と 5 次を計算するために同じ重みを複数回再利用します。

このモデルのために、このループを実装するために通常の Python フロー制御を使用できます、そして forward パスを定義するとき同じパラメータを単純に複数回再利用することにより重み共有を実装できます。

このモデルを Module サブクラスとして容易に実装することができます :

# -*- 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)

# 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(30000):
    # 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 % 2000 == 1999:
        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()}')

 

以上