AIのエンジニアリング基礎1 – 最適化アルゴリズム

データの準備

import torch

# ランダムシードを固定し、実行結果を安定して再現できるようにする
torch.manual_seed(1024)
# 学習用データを生成する
x_origin = torch.linspace(100, 300, 200)
# 変数Xを正規化します。そうしないと、勾配降下法が不安定になりやすくなります
x = (x_origin - torch.mean(x_origin)) / torch.std(x_origin)
epsilon = torch.randn(x.shape)
y = 10 * x + 5 + epsilon

torch.linspace

torch.linspace関数を使用する際は、3つの主要な引数(開始値(start)、終了値(end)、ステップ数(steps))を指定する必要があります。ステップ数は、テンソルに含まれる要素の数を決定します。例えば、3から10までの範囲で5つの要素を含むテンソルを作成するには、以下のコードを使用します:

import torch

# 3から10までの範囲で、5つの要素を含むテンソルを作成
tensor = torch.linspace(3, 10, steps=5)
print(tensor)
出力は次のようになります:
tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000])

パラメータの説明

start:テンソルの開始値。

end:テンソルの終了値。

steps:テンソルの要素数。

dtype:オプションのパラメータ。返されるテンソルのデータ型を指定します。

device:オプションパラメータ。テンソルが配置されるデバイス(CPUやGPUなど)を指定します。

requires_grad:オプションパラメータ。勾配の計算が必要かどうかを指定します。

torch.std

PyTorchでは、標準偏差は torch.std 関数によって計算されます。この関数は、テンソル (tensor)の標準偏差を計算します。標準偏差は、データセット内の数値のばらつき度合いを測る統計量であり、分散の平方根です。PyTorchでは、標準偏差の計算にベッセル補正(Bessel''s correction)が考慮されています。これは、標本標準偏差を計算する際、デフォルトで分母として n ではなく n-1 が使用されることを意味します。

import torch

# テンソルを作成する
a = torch.tensor([[0.2035, 1.2959, 1.8101, -0.4644],
[1.5027, -0.3270, 0.5905, 0.6538],
[-1.5745, 1.3330, -0.5596, -0.6548],
[0.1264, -0.5080, 1.6420, 0.1992]])
# 各行の標準偏差を計算し、次元を維持する
std_a = torch.std(a, dim=1, keepdim=True)
print(std_a)

出力は、各行の要素の標準偏差を表すテンソルとなります。

パラメータの説明

input:入力テンソル。

dim:削減する次元。単一の次元、次元のリスト、または None を指定できます。

correction:標本サイズと標本の自由度の差。デフォルトはベッセル補正、つまり correction=1 です。

keepdim:出力テンソルが dim 次元を保持するかどうか。

out:出力テンソル。

モデル定義

# PyTorchの高レベルラッパー関数を使用するため、Moduleクラスを継承して関数を定義します
class Linear(torch.nn.Module):
    
    def __init__(self):
        「」"
        線形回帰モデルのパラメータ a, b を定義します
        
「」"
        super().__init__()
        self.a = torch.nn.Parameter(torch.zeros(()))
        self.b = torch.nn.Parameter(torch.zeros(()))
    
def forward(self, x):
        「」「
        現在のパラメータの推定値に基づいて、モデルの予測結果を取得する
        引数
        ----
        x :torch.tensor、変数x
        戻り値
        ----
        y_pred :torch.tensor、モデルの予測値
        」「」
        return self.a * x + self.b
    
def string(self):
        「」「
        現在のモデルの結果を出力する
        」「」
        return f『y = {self.a.item():.2f} * x + {self.b.item():.2f}』

forward

独自のネットワークを定義する際は、nn.Moduleクラスを継承し、コンストラクタ__init__とforwardという2つのメソッドを再実装する必要があります。ただし、いくつかの注意点があります:

(1)一般的には、ネットワーク内で学習可能なパラメータを持つ層(全結合層、畳み込み層など)をコンストラクタ__init__()内に配置します。もちろん、パラメータを持たない層をそこに含めることも可能です;

(2)学習可能なパラメータを持たない層(ReLU、dropout、BatchNormalization層など)は、通常、コンストラクタ内に記述することも、記述しないことも可能です。コンストラクタ__init__内に記述しない場合は、forwardメソッド内でnn.functionalを使用して代用することができます

(3)forwardメソッドはを必ずオーバーライドしなければならないものであり、モデルの機能を実現し、各層間の接続関係を実現する中核となるものです。

super().init()

super().init () は、Pythonにおいて親クラスの初期化メソッドを呼び出すための構文であり、継承の場面でよく使用され、子クラスが親クラスの属性やメソッドを正しく初期化できるようにします。

class Parent:
def __init__(self):
print(「Parent initialized」)

class Child(Parent):
def __init__(self):
super().__init__() # 親クラスの __init__ メソッドを呼び出す
print(「Child initialized」)

child = Child()

出力:

Parent initialized

Child initialized

親クラスの初期化ロジックの継承:super().init() を使用することで、子クラスは親クラスの init メソッドを呼び出すことができ、コードの重複を回避できます。

多重継承のサポート:多重継承において、super() はメソッド解決順序(MRO)に従い、すべての親クラスの初期化メソッドが正しく呼び出されることを保証します。

Pythonのf文字列

f文字列(formatted string literals)は、Python 3.6で導入された文字列フォーマットの手法であり、文字列のフォーマットをより簡便かつ効率的に行えるようにします。

基本的な使い方

f文字列では、中括弧 {} 内に任意の有効なPython式を記述でき、これらの式は実行時に評価され、その結果に置き換えられます。

name = 「Alice」
print(f「Hello, {name}!」) # 出力:Hello, Alice!

式の評価と関数の呼び出し

f文字列の中括弧{}内には、式や関数の呼び出しを記述できます。Pythonはそれらの結果を評価し、返された文字列に代入します。

import math
print(f「The value of pi is approximately {math.pi:.2f}.」) # 出力:The value of pi is approximately 3.14

複数行のf文字列

f文字列は複数行の文字列にも使用できます。

name = 「Eric」
age = 27
msg = (
f「Hello!\\n」
f「I『m {name}.\\n」
f「I』m {age}.」
)
print(msg)
# 出力:
# Hello!
# I『m Eric
# I』m 27

カスタムフォーマット

フォーマット指定子を使用して、数値の書式設定(例:揃え、幅、符号、先頭ゼロ埋め、精度など)を行うことができます。

number = 1234.5678
print(f「{number:,.2f}」) # 出力:1,234.57

インスタンスの作成、パラメータの設定、およびサードパーティ製ライブラリを使用した学習

# モデルの定義
model = Linear()
# 最適化アルゴリズムの決定
learning_rate = 0.1
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

for t in range(20):
    # 現在のパラメータ推定値に基づいて、モデルの予測結果を取得
    # つまり、forward関数を呼び出す
    y_pred = model(x)
    # 損失関数を計算
    loss = (y - y_pred).pow(2).mean()
    # 前回の勾配をリセット
    
optimizer.zero_grad()
    # 損失関数の勾配を計算する
    loss.backward()
    # モデルパラメータの推定値を反復的に更新する
    optimizer.step()
    print(f『Step {t + 1}, Loss: {loss: .2f}; Result: {model.string()}』)

SGD

torch.optim.SGD は、PyTorchにおいて確率的勾配降下法(Stochastic Gradient Descent、SGD)を実装するためのクラスであり、深層学習でよく使われる最適化アルゴリズムの一つです。SGDは、モデルパラメータを反復的に更新することで目的関数を最小化し、通常はニューラルネットワークの学習に使用されます。

PyTorchでSGDオプティマイザーを使用するには、まずSGDオブジェクトを作成し、モデルのパラメータを渡す必要があります。その後、トレーニングループ内で、順伝播を実行し、損失を計算し、逆伝播を実行し、オプティマイザーのstep()メソッドを呼び出してパラメータを更新します。例:

import torch
import torch.optim as optim

# モデルと損失関数を定義
model = torch.nn.Linear(10, 1)
criterion = torch.nn.MSELoss()
# オプティマイザーの定義
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=0.0001)
# トレーニングループ内でオプティマイザーを使用
for epoch in range(epochs):
# フォワード伝播
output = model(input_data)
loss = criterion(output, target)
# バックプロパゲーションと最適化
optimizer.zero_grad()
loss.backward()
optimizer.step()

SGDのパラメータ解説

params(必須パラメータ):最適化対象のパラメータ(テンソル)のイテレータ。例:モデルのパラメータ model.parameters()。

lr(必須パラメータ):学習率。パラメータ更新ごとのステップサイズを制御します。

momentum(デフォルト値:0):モメンタム。SGDの収束を加速するために使用され、直前の勾配の指数加重平均を導入します。

dampening(デフォルト値:0):ダンピング項。モメンタムの速度を緩めるために使用されます。

weight_decay(デフォルト値は0):重み減衰。L2正則化項とも呼ばれ、過学習を防ぐために使用されます。

nesterov(デフォルト値はFalse):ネステロフ・モーメンタム。ネステロフ・モーメンタムの更新規則を採用します。

カスタムトレーニング

# コードを用いて、PyTorchでラップされた勾配降下法を実装する
model = Linear()
for t in range(20):
    # 現在のパラメータ推定値に基づいて、モデルの予測結果を取得する
    # つまり、forward関数を呼び出す
    y_pred = model(x)
    
# 損失関数を計算
    loss = (y - y_pred).pow(2).mean()
    # 損失関数の勾配を計算
    loss.backward()
    with torch.no_grad():
        for param in model.parameters():
            # モデルパラメータの推定値を反復的に更新(optimizer.step()と同等)
            
param -= learning_rate * param.grad
            # 勾配をゼロにリセット。optimizer.zero_grad() と同等
            param.grad = None
    print(f『Step {t + 1}, Loss: {loss: .2f}; Result: {model.string()}』)

lossは小さくする必要があるため、勾配を差し引きます。

torch.no_grad()

PyTorchにおいて、with torch.no_gradは、特定のコードブロック内で自動微分機能を無効にするためのコンテキストマネージャです。これは、モデル評価や推論の際、勾配の計算が不要となるため、コードの実行効率を向上させるのに非常に役立ちます。以下は、with torch.no_gradの使用方法を示す簡単な例です:

import torch

# いくつかのテンソルを作成し、requires_grad=True を設定
x = torch.randn(10, 5, requires_grad=True)
y = torch.randn(10, 5, requires_grad=True)
z = torch.randn(10, 5, requires_grad=True)

# `with torch.no_grad` ステートメントブロック内で計算を行う
with torch.no_grad():
w = x + y + z
print(w.requires_grad) # 出力: False
print(w.grad_fn) # 出力: None

print(w.requires_grad) # 出力: False

上記のコードでは、x、y、zのrequires_grad属性がTrueであっても、with torch.no_grad文ブロック内で計算された新しいテンソルwのrequires_grad属性は自動的にFalseに設定され、grad_fnはNoneとなります。

コードの追跡と試行

再現:

import torch

torch.manual_seed(1024)
x_ori=torch.linspace(100,300,200)

x=(x_ori-torch.mean(x_ori))/torch.std (x_ori)
e=torch.randn(x.shape)
y=10*x+5+e

class Linear(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.a=torch.nn.Parameter(torch.zeros( ()))
        self.b=torch.nn.Parameter(torch.zeros(()))
    def forward(self,x):
        return self.a*x+self.b
    def string(self):
        return f『y={self.a.item():.2f}*x+{self.b.item():.2f}』

model = Linear()
lr = 0.1
optimizer = torch.optim.SGD(model.parameters(), lr=lr)

for i in range(20):
    y_p = model(x)
    loss = (y - y_p).pow(2).mean()
    optimizer.zero_grad()
    
loss.backward()
    optimizer.step()
    print(f『Step {i + 1}, Loss: {loss: .2f}; Result: {model.string()}』)

model=Linear()
for i in range(20):
    y_p=model(x)
    loss=(y-y_p).pow(2).mean()
    loss.backward()
    
with torch.no_grad():        for param in model.parameters():            param -= lr * param.grad            param.grad = None    print(f『Step {i + 1}, Loss: {loss: .2f}; Result: {model.string()}』)

結果:

Step 1, Loss: 125.25; Result: y=1.98*x+1.02Step 2, Loss: 80.72; Result: y=3.56*x+1.83Step 3, Loss: 52.16; Result: y=4.83*x+2.49Step 4, 損失: 33.84; 結果: y=5.85*x+3.01Step 5, 損失: 22.10; 結果: y=6.66*x+3.42Step 6, 損失: 14.57; 結果: y=7.31*x+3.76ステップ7、損失:9.74;結果:y=7.83×x+4.03ステップ8、損失:6.64;結果:y=8.25×x+4.24ステップ9、損失:4.66;結果:y=8.59×x+4.41ステップ10、損失:3.38;結果:y=8.85*x+4.55ステップ11、損失:2.56;結果:y=9.07*x+4.66ステップ12、損失:2.04;結果:y=9.24*x+4.74ステップ13、損失:1.71;結果:y=9.38×x+4.81ステップ14、損失:1.49;結果:y=9.49×x+4.87ステップ15、損失:1.35;結果:y=9.58×x+4.91ステップ 16、損失: 1.26; 結果: y=9.65*x+4.95ステップ 17、損失: 1.21; 結果: y=9.71*x+4.98ステップ 18、損失: 1.17; 結果: y=9.75*x+5.00ステップ 19、損失: 1.15; 結果: y=9.79*x+5.02ステップ20、損失: 1.13; 結果: y=9.82*x+5.03ステップ1、損失: 125.25; 結果: y=1.98*x+1.02ステップ2、損失:80.72;結果:y=3.56×x+1.83ステップ3、損失:52.16;結果:y=4.83×x+2.49ステップ4、損失:33.84;結果:y=5.85×x+3.01ステップ5、損失:22.10;結果:y=6.66×x+3.42ステップ6、損失:14.57;結果:y=7.31×x+3.76
ステップ7、損失:9.74;結果:y=7.83×x+4.03
ステップ8、損失:6.64;結果:y=8.25×x+4.24
ステップ9、損失:4.66;結果:y=8.59×x+4.41
ステップ10、損失:3.38;結果:y=8.85×x+4.55
ステップ11、損失:2.56;結果:y=9.07×x+4.66
ステップ12、損失:2.04;結果:y=9.24×x+4.74
ステップ13、損失:1.71;結果:y=9.38×x+4.81
ステップ14、損失:1.49;結果:y=9.49×x+4.87
ステップ15、損失:1.35;結果:y=9.58×x+4.91
ステップ16、損失:1.26;結果:y=9.65×x+4.95
ステップ17、損失:1.21;結果:y=9.71×x+4.98
ステップ 18、損失: 1.17; 結果: y=9.75*x+5.00
ステップ 19、損失: 1.15; 結果: y=9.79*x+5.02
ステップ 20、損失: 1.13; 結果: y=9.82*x+5.03

補足:ブロードキャスト機構

ブロードキャスト機構(Broadcasting)とは、演算を行う際に、データをコピーすることなく、小さいテンソルを自動的に拡張して大きいテンソルのサイズに合わせる仕組みのことです。この機構により、演算効率が向上し、メモリを節約できます。

ブロードキャスト機構により、形状の異なる2つのテンソル間で演算を行うことが可能です。その基本的なルールは以下の通りです:

各テンソルには少なくとも1つの次元がある。

末尾の次元から順に調べ、2つのテンソルの対応する各次元について、以下の条件のいずれかを満たす必要がある: 次元が等しい。 いずれかの次元が1である。 いずれかの次元が存在しない^3^。

2つのテンソルが「ブロードキャスト可能」である場合、計算プロセスは以下のルールに従う:

小さい方のテンソルの先頭に次元を追加し、大きい方のテンソルと同じ次元数にする。

計算結果の次元数は、2つのテンソルのうち大きい方の値を採用する。

次元を拡張する処理は、数値のコピーを行うものである。

以下は、ブロードキャストメカニズムを使用したサンプルコードです:
import torch

# 例1:2つのテンソルの次元が同じ場合
a = torch.rand(4, 32, 14, 14)
b = torch.rand(4, 32, 14, 14)
print((a + b).shape) # 出力:torch.Size([4, 32, 14, 14])

# 例2:1つのテンソルの特定の次元が1の場合
a = torch.rand(4, 32, 14, 14)
b = torch.rand(1, 32, 1, 1)
print((a + b).shape) # 出力:torch.Size([4, 32, 14, 14])

# 例3:ある次元が小さいテンソル
a = torch.rand(4, 32, 14, 14)
c = torch.rand(32, 1, 1)
print((a + c).shape) # 出力:torch.Size([4, 32, 14, 14])

注意事項

ブロードキャストを使用する際は、以下の点に注意してください:

テンソルの次元がブロードキャストのルールに合致していることを確認してください。そうでない場合、エラーが発生します。

インプレース操作ではテンソルの形状を変更できないため、ブロードキャストの使用は避けてください。