Pytorchをインストールするには、公式Webサイトの指示に従ってください。
pip install torch torchvision
新しい記事を追加して、このシリーズを徐々に拡張し、Pytorch APIの最新リリースでコンテンツを最新に保つことを目指しています。このシリーズを改善する方法や説明を曖昧にする方法についての提案がある場合は、問題を作成したり、パッチを送信したり、電子メールで連絡したりしてください。
Pytorchは数値計算で最も人気のあるライブラリの1つであり、現在、機械学習研究を実行するために最も広く使用されているライブラリの1つです。 Pytorchは多くの点でNumpyに似ており、Pytorchでコードを重大な変更せずにCPU、GPU、およびTPUで計算を実行できる追加の利点があります。 Pytorchは、複数のデバイスまたはマシンに計算を簡単に配布できます。 Pytorchの最も重要な機能の1つは、自動分化です。これにより、勾配降下法を使用して機械学習モデルをトレーニングするために重要な効率的な方法で、機能の勾配を解析的に計算できます。ここでの目標は、Pytorchへの穏やかな紹介を提供し、Pytorchを使用するためのベストプラクティスについて議論することです。
Pytorchについて最初に学ぶことは、テンソルの概念です。テンソルは、単に多次元配列です。 pytorchテンソルは、いくつかのnumpy配列に非常に似ています魔法追加の機能。
テンソルはスカラー値を保存できます。
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)または配列:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])マトリックス:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])または任意の寸法テンソル:
d = torch . rand ([ 2 , 2 , 2 ])テンソルを使用して、代数操作を効率的に実行できます。機械学習アプリケーションで最も一般的に使用される操作の1つは、マトリックス増殖です。サイズ3x5と5x4の2つのランダム行列を掛けたいとします。これは、マトリックス乗算(@)操作で実行できます。
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )同様に、2つのベクトルを追加するには、次のことができます。
z = x + yテンソルをnumpy配列に変換するには、Tensorのnumpy()メソッドを呼び出すことができます。
print ( z . numpy ())また、次をテンソルに変換できます。
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))NumpyよりもPytorchの最も重要な利点は、ニューラルネットワークの最適化パラメーターなどの最適化アプリケーションに非常に役立つ自動分化機能です。例でそれを理解してみましょう。
2つの関数のチェーンである複合関数があるとします: g(u(x)) 。 xに対してgの導関数を計算するには、次のように記載されているチェーンルールを使用できます。dg dg/dx = dg/du * du/dx 。 Pytorchは、私たちの導関数を分析的に計算できます。
Pytorchでデリバティブを計算するには、最初にテンソルを作成し、そのrequires_grad trueに設定します。テンソル操作を使用して、機能を定義できます。 u二次関数であり、 g単純な線形関数であると仮定します。
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - uこの場合、複合関数はg(u(x)) = -x*xです。したがって、 xに対する導関数は-2xです。ポイントx=1では、これは-2に等しくなります。
これを確認しましょう。これは、pytorchの段階機能を使用して実行できます。
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.)自動分化がどれほど強力であるかを理解するには、別の例を見てみましょう。曲線からのサンプル( f(x) = 5x^2 + 3 )があると仮定し、これらのサンプルに基づいてf(x)を推定する必要があります。パラメトリック関数g(x, w) = w0 x^2 + w1 x + w2を定義します。これは、入力xと潜在パラメーターwの関数であり、 g(x, w) ≈ f(x)のような潜在パラメーターを見つけることです。これは、次の損失関数を最小化することで実行できます: L(w) = Σ (f(x) - g(x, w))^2 。この単純な問題には閉じたフォームソリューションがありますが、任意の微分微分機能に適用できるより一般的なアプローチを使用することを選択します。サンプルポイントのセットを介してwに関してL(w)の平均勾配を計算し、反対方向に移動するだけです。
Pytorchでそれを行う方法は次のとおりです。
import numpy as np
import torch
# Assuming we know that the desired function is a polynomial of 2nd degree, we
# allocate a vector of size 3 to hold the coefficients and initialize it with
# random noise.
w = torch . tensor ( torch . randn ([ 3 , 1 ]), requires_grad = True )
# We use the Adam optimizer with learning rate set to 0.1 to minimize the loss.
opt = torch . optim . Adam ([ w ], 0.1 )
def model ( x ):
# We define yhat to be our estimate of y.
f = torch . stack ([ x * x , x , torch . ones_like ( x )], 1 )
yhat = torch . squeeze ( f @ w , 1 )
return yhat
def compute_loss ( y , yhat ):
# The loss is defined to be the mean squared error distance between our
# estimate of y and its true value.
loss = torch . nn . functional . mse_loss ( yhat , y )
return loss
def generate_data ():
# Generate some training data based on the true function
x = torch . rand ( 100 ) * 20 - 10
y = 5 * x * x + 3
return x , y
def train_step ():
x , y = generate_data ()
yhat = model ( x )
loss = compute_loss ( y , yhat )
opt . zero_grad ()
loss . backward ()
opt . step ()
for _ in range ( 1000 ):
train_step ()
print ( w . detach (). numpy ())このコードを実行することで、これに近い結果が表示されます。
[ 4.9924135 , 0.00040895029 , 3.4504161 ]これは、パラメーターに比較的近い近似です。
これは、Pytorchができることのための氷山の先端です。数百万のパラメーターを使用して大規模なニューラルネットワークを最適化するなど、多くの問題は、わずか数行のコードでPytorchで効率的に実装できます。 Pytorchは、複数のデバイスとスレッドのスケーリングを処理し、さまざまなプラットフォームをサポートします。
前の例では、裸の骨のテンソルとテンソル操作を使用してモデルを構築しました。コードをやや整理するには、Pytorchのモジュールを使用することをお勧めします。モジュールは、パラメーターの単なるコンテナであり、モデル操作をカプセル化します。たとえば、線形モデルy = ax + bを表現するとします。このモデルは、次のコードで表すことができます。
import torch
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . a = torch . nn . Parameter ( torch . rand ( 1 ))
self . b = torch . nn . Parameter ( torch . rand ( 1 ))
def forward ( self , x ):
yhat = self . a * x + self . b
return yhatこのモデルを実際に使用するには、モジュールをインスタンス化し、単に関数のように呼び出すだけです。
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x )パラメーターは、基本的にテンソルであり、 requires_gradセットはtrueに設定されています。モジュールのparameters()メソッドを使用してすべてを単純に取得できるため、パラメーターを使用すると便利です。
for p in net . parameters ():
print ( p )ここで、不明な関数y = 5x + 3 + some noiseを持っているとし、この関数に適合するようにモデルのパラメーターを最適化する必要があるとします。関数からいくつかのポイントをサンプリングすることから始めることができます。
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3前の例と同様に、損失関数を定義し、次のようにモデルのパラメーターを最適化できます。
criterion = torch . nn . MSELoss ()
optimizer = torch . optim . SGD ( net . parameters (), lr = 0.01 )
for i in range ( 10000 ):
net . zero_grad ()
yhat = net ( x )
loss = criterion ( yhat , y )
loss . backward ()
optimizer . step ()
print ( net . a , net . b ) # Should be close to 5 and 3 Pytorchには、事前定義された多くのモジュールが付属しています。そのようなモジュールの1つは、上記で定義したものよりも線形関数のより一般的な形式であるtorch.nn.Linearです。 torch.nn.Linearを使用して、上記のモジュールを書き換えることができます。
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . linear = torch . nn . Linear ( 1 , 1 )
def forward ( self , x ):
yhat = self . linear ( x . unsqueeze ( 1 )). squeeze ( 1 )
return yhat torch.nn.Linear 、スカラーとは対照的に、ベクターのバッチで動作するため、Squeezeとqueeezeを使用していないことに注意してください。
デフォルトでは、モジュール上のパラメーター()を呼び出します。すべてのサブモジュールのパラメーターを返します。
net = Net ()
for p in net . parameters ():
print ( p )他のモジュールのコンテナとして機能するいくつかの定義済みモジュールがあります。最も一般的に使用されるコンテナモジュールはtorch.nn.Sequentialです。その名前が示すように、互いの上に複数のモジュール(またはレイヤー)を積み重ねるために使用されます。たとえば、2つの線形層を積み重ねることで、その間にできるReLUリアリティを備えています。
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
)Pytorchは、ブロードキャストElementWise Operationsをサポートしています。通常、加算や乗算などの操作を実行する場合は、オペランドの形状が一致することを確認する必要があります。たとえば、形状のテンソル[3, 2]形状[3, 4]に追加することはできません。しかし、特別なケースがあり、それはあなたが特異な次元を持っているときです。 Pytorchは、他のオペランドの形状と一致するように、その特異な寸法全体にテンソルを暗黙的にタイル張りにします。したがって、形状のテンソル[3, 2]形状のテンソル[3, 1]に追加することは有効です。
import torch
a = torch . tensor ([[ 1. , 2. ], [ 3. , 4. ]])
b = torch . tensor ([[ 1. ], [ 2. ]])
# c = a + b.repeat([1, 2])
c = a + b
print ( c )ブロードキャストを使用すると、タイリング操作の結果を保存する必要がないため、コードを短くし、メモリ効率を高めることにより、暗黙のタイルを実行できます。これを使用できるきちんとした場所の1つは、さまざまな長さの特徴を組み合わせるときです。さまざまな長さの特徴を連結するために、一般的に入力テンソルをタイル張り、結果を連結し、何らかの非線形性を適用します。これは、さまざまなニューラルネットワークアーキテクチャにわたる一般的なパターンです。
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear = torch . nn . Linear ( 11 , 10 )
# concat a and b and apply nonlinearity
tiled_b = b . repeat ([ 1 , 3 , 1 ])
c = torch . cat ([ a , tiled_b ], 2 )
d = torch . nn . functional . relu ( linear ( c ))
print ( d . shape ) # torch.Size([5, 3, 10])しかし、これはブロードキャストでより効率的に行うことができます。 f(m(x + y)) f(mx + my)に等しいという事実を使用します。したがって、線形操作を個別に行い、放送を使用して暗黙の連結を行うことができます。
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear1 = torch . nn . Linear ( 5 , 10 )
linear2 = torch . nn . Linear ( 6 , 10 )
pa = linear1 ( a )
pb = linear2 ( b )
d = torch . nn . functional . relu ( pa + pb )
print ( d . shape ) # torch.Size([5, 3, 10])実際、このコードはかなり一般的であり、テンソル間のブロードキャストが可能である限り、任意の形状のテンソルに適用できます。
class Merge ( torch . nn . Module ):
def __init__ ( self , in_features1 , in_features2 , out_features , activation = None ):
super (). __init__ ()
self . linear1 = torch . nn . Linear ( in_features1 , out_features )
self . linear2 = torch . nn . Linear ( in_features2 , out_features )
self . activation = activation
def forward ( self , a , b ):
pa = self . linear1 ( a )
pb = self . linear2 ( b )
c = pa + pb
if self . activation is not None :
c = self . activation ( c )
return cこれまでのところ、放送の大部分について議論しました。しかし、あなたが尋ねるかもしれない醜い部分は何ですか?暗黙の仮定は、ほとんどの場合、デバッグを難しくします。次の例を考えてみましょう。
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) cの価値は評価後にどうなると思いますか? 6を推測した場合、それは間違っています。これは12になるでしょう。これは、2つのテンソルのランクが一致しない場合、Pytorchが要素ごとの動作の前にランクの低いテンソルの最初の次元を自動的に拡張するため、追加の結果は[[2, 3], [3, 4]]であり、すべてのパラメーターの削減により12が与えられます。
この問題を回避する方法は、可能な限り明確にすることです。どのディメンションを削減したいかを指定していたら、このバグをキャッチするのはずっと簡単だったでしょう。
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c )ここでは、 cの値は[5, 7]であり、すぐに結果の形状に基づいて何か間違っていると推測します。一般的な経験則は、削減操作の寸法を常に指定し、 torch.squeezeを使用する場合です。
Numpyと同じように、Pytorchは多くのPython演算子を過負荷にして、Pytorchコードをより短く読みやすくします。
スライスOPは、インデックス作成テンソルを非常に簡単にすることができる過負荷の演算子の1つです。
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)ただし、このOPを使用する場合は非常に注意してください。スライスOPは、他のOPと同様に、オーバーヘッドがあります。それは一般的な操作であり、無邪気な見た目であるため、それは過剰に使用され、非効率性につながる可能性があります。このOPがどれほど非効率的であるかを理解するには、例を見てみましょう。マトリックスの行全体で手動で削減を実行したい:
import torch
import time
x = torch . rand ([ 500 , 10 ])
z = torch . zeros ([ 10 ])
start = time . time ()
for i in range ( 500 ):
z += x [ i ]
print ( "Took %f seconds." % ( time . time () - start ))これは非常に遅くなり、その理由は、私たちが500回スライスOPを呼び出しているため、多くのオーバーヘッドが追加されます。より良い選択は、 torch.unbind opを使用してマトリックスを一度にベクトルのリストにスライスすることでした。
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iこれは大幅に(マシンで約30%)速くなります。
もちろん、この簡単な削減を行う正しい方法は、 torch.sum opを1つのopで使用することです。
z = torch . sum ( x , dim = 0 )これは非常に高速です(マシンでは約100倍高速)。
Pytorchはまた、さまざまな算術演算子と論理演算子を過負荷にします。
z = - x # z = torch.neg(x)
z = x + y # z = torch.add(x, y)
z = x - y
z = x * y # z = torch.mul(x, y)
z = x / y # z = torch.div(x, y)
z = x // y
z = x % y
z = x ** y # z = torch.pow(x, y)
z = x @ y # z = torch.matmul(x, y)
z = x > y
z = x >= y
z = x < y
z = x <= y
z = abs ( x ) # z = torch.abs(x)
z = x & y
z = x | y
z = x ^ y # z = torch.logical_xor(x, y)
z = ~ x # z = torch.logical_not(x)
z = x == y # z = torch.eq(x, y)
z = x != y # z = torch.ne(x, y)これらのOPSの拡張バージョンを使用することもできます。たとえばx += yおよびx **= 2も有効です。
Pythonは過負荷and 、 orキーワードnot許可していないことに注意してください。
Pytorchは、大きなテンソルで操作を実行するために最適化されています。 Pytorchでは、小さなテンソルで多くの操作を行うことは非常に非効率的です。そのため、可能な限り、オーバーヘッドを減らしてパフォーマンスを向上させるために、バッチ形式で計算を書き換える必要があります。手動で操作をバッチすることができない場合は、Torchscriptを使用するとコードのパフォーマンスが向上する可能性があります。 Torchscriptは、Pytorchによって認識されるPython関数の単なるサブセットです。 Pytorchは、Just In Time(JIT)コンパイラを使用してTorchscriptコードを自動的に最適化し、オーバーヘッドを減らすことができます。
例を見てみましょう。 MLアプリケーションの非常に一般的な操作は、「バッチギャザー」です。この操作はoutput[i] = input[i, index[i]]として単純に記述できます。これは、次のようにPytorchで単純に実装できます。
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Torchscriptを使用して同じ関数を実装するには、 torch.jit.scriptデコレータを使用するだけです。
@ torch . jit . script
def batch_gather_jit ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output )私のテストでは、これは約10%高速です。
しかし、手動で操作をバッチすることに勝るものはありません。私のテストでのベクトル化された実装は、100倍高速です。
def batch_gather_vec ( tensor , indices ):
shape = list ( tensor . shape )
flat_first = torch . reshape (
tensor , [ shape [ 0 ] * shape [ 1 ]] + shape [ 2 :])
offset = torch . reshape (
torch . arange ( shape [ 0 ]). cuda () * shape [ 1 ],
[ shape [ 0 ]] + [ 1 ] * ( len ( indices . shape ) - 1 ))
output = flat_first [ indices + offset ]
return output 最後のレッスンでは、効率的なPytorchコードの作成について説明しました。ただし、コードを最大の効率で実行するには、データをデバイスのメモリに効率的にロードする必要があります。幸いなことに、Pytorchはデータの読み込みを簡単にするためのツールを提供しています。 DataLoaderと呼ばれています。 DataLoader 、複数のワーカーを使用してDatasetからデータを同時にロードし、オプションでSamplerを使用してデータエントリをサンプリングし、バッチを形成します。
データをランダムにアクセスできる場合は、 DataLoaderを使用することは非常に簡単です。__ __getitem__ (各データ項目を読むため)と__len__ (データセットのアイテムの数を返す)を実装するDatasetクラスを実装する必要があります。たとえば、特定のディレクトリから画像をロードする方法は次のとおりです。
import glob
import os
import random
import cv2
import torch
class ImageDirectoryDataset ( torch . utils . data . Dataset ):
def __init__ ( path , pattern ):
self . paths = list ( glob . glob ( os . path . join ( path , pattern )))
def __len__ ( self ):
return len ( self . paths )
def __item__ ( self ):
path = random . choice ( paths )
return cv2 . imread ( path , 1 )特定のディレクトリからすべてのJPEG画像をロードするには、次のことを行うことができます。
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataここでは、8人の労働者を使用して、ディスクからデータを同時に読み取ります。最適な結果を得るために、マシン上の労働者の数を調整できます。
DataLoaderを使用して、ランダムアクセスを使用してデータを読み取ることは、高速ストレージがある場合、またはデータ項目が大きい場合は問題なくなる場合があります。ただし、接続が遅いネットワークファイルシステムがあることを想像してください。この方法で個々のファイルをリクエストすることは非常に遅く、おそらくトレーニングパイプラインのボトルネックになることになります。
より良いアプローチは、データを連続的に読み取ることができる連続的なファイル形式に保存することです。たとえば、画像の大規模なコレクションがある場合は、TARを使用して単一のアーカイブを作成し、Pythonのアーカイブからファイルを抽出できます。これを行うには、PytorchのIterableDataset使用できます。 IterableDatasetクラスを作成するには、データセットからデータ項目を順次読み取り、生成する__iter__メソッドを実装する必要があります。
素朴な実装はこれを望みます:
import tarfile
import torch
def tar_image_iterator ( path ):
tar = tarfile . open ( self . path , "r" )
for tar_info in tar :
file = tar . extractfile ( tar_info )
content = file . read ()
yield cv2 . imdecode ( content , 1 )
file . close ()
tar . members = []
tar . close ()
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , path ):
super (). __init__ ()
self . path = path
def __iter__ ( self ):
yield from tar_image_iterator ( self . path )しかし、この実装には大きな問題があります。 Dataloaderを使用して、複数のワーカーを使用してこのデータセットから読み取ろうとすると、多くの重複した画像が観察されます。
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items問題は、各ワーカーがデータセットの個別のインスタンスを作成し、それぞれがデータセットの先頭から開始することです。これを回避する1つの方法は、1つのTARファイルを使用する代わりに、データをnum_workers個別のタールファイルに分割し、それぞれを別のワーカーにロードすることです。
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , paths ):
super (). __init__ ()
self . paths = paths
def __iter__ ( self ):
worker_info = torch . utils . data . get_worker_info ()
# For simplicity we assume num_workers is equal to number of tar files
if worker_info is None or worker_info . num_workers != len ( self . paths ):
raise ValueError ( "Number of workers doesn't match number of files." )
yield from tar_image_iterator ( self . paths [ worker_info . worker_id ])これは、データセットクラスを使用する方法です。
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with data重複したエントリの問題を回避するための簡単な戦略について説明しました。 Tfrecord Packageは、わずかに洗練された戦略を使用して、その場でデータを破壊します。
NumpyやPytorchなどの数値計算ライブラリを使用する場合、数学的に正しいコードを書くことは必ずしも正しい結果につながるわけではないことに注意することが重要です。また、計算が安定していることを確認する必要があります。
簡単な例から始めましょう。数学的には、 xの非ゼロ値に対してx * y / y = x見るのは簡単です。しかし、それが実際に常に真実かどうかを見てみましょう。
import numpy as np
x = np . float32 ( 1 )
y = np . float32 ( 1e-50 ) # y would be stored as zero
z = x * y / y
print ( z ) # prints nan間違った結果の理由は、 yがFloat32タイプには単純に小さすぎるためです。 yが大きすぎると同様の問題が発生します。
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanFloat32タイプが表すことができる最小の正の値は1.4013E-45であり、それ以下のものはゼロとして保存されます。また、3.40282E+38を超える任意の数はINFとして保存されます。
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38計算が安定していることを確認するには、小さいまたは非常に大きな絶対値で値を回避する必要があります。これは非常に明白に聞こえるかもしれませんが、この種の問題は、特にPytorchで勾配降下を行う場合にデバッグが非常に困難になる可能性があります。これは、フォワードパス内のすべての値がデータ型の有効な範囲内にあることを確認する必要があるだけでなく、バックワードパスで同じことを確認する必要があるためです(勾配計算中)。
本当の例を見てみましょう。ロジットのベクトル上でソフトマックスを計算したいと考えています。素朴な実装は次のようになります:
import torch
def unstable_softmax ( logits ):
exp = torch . exp ( logits )
return exp / torch . sum ( exp )
print ( unstable_softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ nan, 0.]比較的少ない数のロジットの指数を計算すると、float32範囲外の巨大な結果が得られることに注意してください。ナイーブソフトマックスの実装の最大の有効なロジットはln(3.40282e+38) = 88.7です。
しかし、どうすればこれをより安定させることができますか?ソリューションはかなり単純です。 exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x)簡単にわかります。したがって、ロジットから任意の定数を差し引くことができ、結果は同じままです。この定数を選択して、ロジットの最大値にします。このようにして、指数関数のドメインは[-inf, 0]に限定され、その結果、その範囲は[0.0, 1.0]に望ましいものになります。
import torch
def softmax ( logits ):
exp = torch . exp ( logits - torch . reduce_max ( logits ))
return exp / torch . sum ( exp )
print ( softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ 1., 0.]より複雑なケースを見てみましょう。分類の問題があると考えてください。 SoftMax関数を使用して、ロジットから確率を生成します。次に、損失関数を予測とラベルの間の交差エントロピーであると定義します。カテゴリ分布の交差エントロピーはxe(p, q) = -Σ p_i log(q_i)として単純に定義できることを思い出してください。したがって、クロスエントロピーの素朴な実装は次のようになります。
def unstable_softmax_cross_entropy ( labels , logits ):
logits = torch . log ( softmax ( logits ))
return - torch . sum ( labels * logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = unstable_softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints infこの実装では、SoftMax出力がゼロに近づくにつれて、ログの出力がインフィニティに近づき、計算に不安定性を引き起こすことに注意してください。 SoftMaxを拡大し、いくつかの単純化を行うことで、これを書き直すことができます。
def softmax_cross_entropy ( labels , logits , dim = - 1 ):
scaled_logits = logits - torch . max ( logits )
normalized_logits = scaled_logits - torch . logsumexp ( scaled_logits , dim )
return - torch . sum ( labels * normalized_logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints 500.0勾配も正しく計算されていることを確認することもできます。
logits . requires_grad_ ( True )
xe = softmax_cross_entropy ( labels , logits )
g = torch . autograd . grad ( xe , logits )[ 0 ]
print ( g . numpy ()) # prints [0.5, -0.5]勾配降下を行うときは、関数の範囲と各層の勾配が有効な範囲内にあることを確認するために、余分な注意を払わなければならないことをもう一度思い出させてください。指数関数と対数関数は、単純に使用する場合に特に問題があります。なぜなら、それらは少数数に巨大な数にマッピングされる可能性があるため、その他の方法です。
デフォルトでは、Pytorchのテンソルとモデルパラメーターは、32ビットの浮動小数点精度に保存されます。 32ビットフロートを使用したニューラルネットワークのトレーニングは通常安定しており、大きな数値の問題を引き起こしませんが、ニューラルネットワークは16ビットおよびさらに低い精度で非常にうまく機能することが示されています。より低い精度での計算は、最新のGPUで大幅に高速になる可能性があります。また、より少ないメモリを使用することで、より大きなモデルを可能にすることを可能にし、より大きなバッチサイズを使用して、パフォーマンスをさらに向上させるという特別な利点があります。問題は、精度が蓄積のようないくつかの操作を実行するには通常十分ではないため、16ビットでのトレーニングが非常に不安定になることが多いことです。
この問題を支援するために、Pytorchは混合精度のトレーニングをサポートしています。一言で言えば、混合精度トレーニングは、32ビットで蓄積などの他の数値的に敏感な操作を実行しながら、入力をキャストすることにより、16ビットでいくつかの高価な操作(畳み込みやマトリックスの多重化など)を実行することにより行われます。このようにして、16ビットの計算のすべての利点を、その欠点なしで取得します。次に、オートキャストとグラッドスカラーを使用して自動混合精度トレーニングを行うことについて説明します。
autocast 、一部の計算のためにデータを16ビットに自動的にキャストすることにより、ランタイムパフォーマンスを改善するのに役立ちます。それがどのように機能するかを理解するには、例を見てみましょう。
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 xとyの両方が32ビットテンソルであることに注意してくださいが、 autocast 32ビットで追加操作を維持しながら、16ビットでマトリックス増殖を実行します。オペランドの1つが16ビットにある場合はどうなりますか?
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda (). half ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16再びautocast 、32ビットオペランドを16ビットにキャストしてマトリックス増殖を実行しますが、追加操作は変更されません。デフォルトでは、Pytorchに2つのテンソルを追加すると、キャストが高くなります。
実際には、ランタイムの効率を向上させるために適切なキャストを行うためにautocastを信頼することができます。重要なことは、すべてのフォワードパス計算をautocastコンテキストの下に保つことです。
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )これは、比較的安定した最適化の問題があり、比較的低い学習率を使用している場合に必要なすべてです。この1行の追加コードを追加すると、最新のハードウェアでトレーニングを半分に減らすことができます。
このセクションの冒頭で述べたように、16ビットの精度では、いくつかの計算に対して必ずしも十分ではないかもしれません。関心のある特定のケースの1つは、勾配値を表すことであり、その大部分は通常小さな値です。 16ビットのフロートでそれらを表すことは、しばしばバッファーのアンダーフローにつながります(つまり、それらはゼロとして表されます)。これにより、トレーニングニューラルネットワークは非常に不安定になります。 GradScalar 、この問題を解決するように設計されています。それはあなたの損失値を入力するときにかかり、それを大きなスカラーで乗算し、勾配値を膨らませるため、それらを16ビットの精度で表現可能にします。次に、勾配更新中にそれらを縮小して、パラメーターが正しく更新されるようにします。これは一般的にGradScalarが行うことです。しかし、フードの下では、 GradScalarそれよりも少し賢いです。勾配を膨らませると、実際にはオーバーフローが発生する可能性がありますが、これは同様に悪いことです。したがって、 GradScalar実際に勾配値を監視し、オーバーフローを検出すると更新をスキップし、設定可能なスケジュールに応じてスカラーファクターをスケーリングします。 (通常、デフォルトのスケジュールは機能しますが、ユースケースに対してそれを調整する必要がある場合があります。)
GradScalarの使用は非常に簡単です。
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update ()最初にGradScalarのインスタンスを作成することに注意してください。トレーニングループでは、 GradScalar.scaleを呼び出して損失をスケーリングしてから、後方に呼び出して膨らんだ勾配を生成し、 GradScalar.stepを使用してモデルパラメーターを更新します。次に、必要に応じてスカラーアップデートを実行するGradScalar.updateに電話します。それだけです!
以下は、画像座標からチェッカーボードを生成するために学習する合成問題に関する精度トレーニングを混合したケースを示すサンプルコードです。 Googleコラブに貼り付け、バックエンドをGPUに設定し、シングルと混合精度のパフォーマンスを比較できます。これは小さなおもちゃの例であることに注意してください。実際には、より大きなネットワークがある場合は、混合精度を使用してパフォーマンスが大きくなることがあります。
import torch
import matplotlib . pyplot as plt
import time
def grid ( width , height ):
hrange = torch . arange ( width ). unsqueeze ( 0 ). repeat ([ height , 1 ]). div ( width )
vrange = torch . arange ( height ). unsqueeze ( 1 ). repeat ([ 1 , width ]). div ( height )
output = torch . stack ([ hrange , vrange ], 0 )
return output
def checker ( width , height , freq ):
hrange = torch . arange ( width ). reshape ([ 1 , width ]). mul ( freq / width / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
vrange = torch . arange ( height ). reshape ([ height , 1 ]). mul ( freq / height / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
output = hrange . logical_xor ( vrange ). float ()
return output
# Note the inputs are grid coordinates and the target is a checkerboard
inputs = grid ( 512 , 512 ). unsqueeze ( 0 ). cuda ()
targets = checker ( 512 , 512 , 8 ). unsqueeze ( 0 ). unsqueeze ( 1 ). cuda () class Net ( torch . jit . ScriptModule ):
def __init__ ( self ):
super (). __init__ ()
self . net = torch . nn . Sequential (
torch . nn . Conv2d ( 2 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 1 , 1 ))
@ torch . jit . script_method
def forward ( self , x ):
return self . net ( x ) net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
loss . backward ()
opt . step ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu ()); plt . show () net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
scaler = torch . cuda . amp . GradScaler ()
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
with torch . cuda . amp . autocast ():
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
scaler . scale ( loss ). backward ()
scaler . step ( opt )
scaler . update ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu (). float ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu (). float ()); plt . show ()