Untuk menginstal Pytorch, ikuti instruksi di situs web resmi:
pip install torch torchvision
Kami bertujuan untuk secara bertahap memperluas seri ini dengan menambahkan artikel baru dan menjaga konten tetap up to date dengan rilis terbaru Pytorch API. Jika Anda memiliki saran tentang cara meningkatkan seri ini atau menemukan penjelasan yang ambigu, jangan ragu untuk membuat masalah, mengirim tambalan, atau menjangkau melalui email.
Pytorch adalah salah satu perpustakaan paling populer untuk perhitungan numerik dan saat ini adalah salah satu perpustakaan yang paling banyak digunakan untuk melakukan penelitian pembelajaran mesin. Dalam banyak hal, Pytorch mirip dengan Numpy, dengan manfaat tambahan yang Pytorch memungkinkan Anda untuk melakukan perhitungan Anda pada CPU, GPU, dan TPU tanpa perubahan material pada kode Anda. Pytorch juga memudahkan untuk mendistribusikan perhitungan Anda di beberapa perangkat atau mesin. Salah satu fitur terpenting dari Pytorch adalah diferensiasi otomatis. Ini memungkinkan menghitung gradien fungsi Anda secara analitik dengan cara yang efisien yang sangat penting untuk pelatihan model pembelajaran mesin menggunakan metode keturunan gradien. Tujuan kami di sini adalah untuk memberikan pengantar yang lembut kepada Pytorch dan membahas praktik terbaik untuk menggunakan Pytorch.
Hal pertama yang harus dipelajari tentang Pytorch adalah konsep tensor. Tensor hanyalah array multidimensi. Tensor Pytorch sangat mirip dengan array yang tidak bagus dengan beberapa gaib fungsi tambahan.
Tensor dapat menyimpan nilai skalar:
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)atau array:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])matriks:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])atau tensor dimensi sewenang -wenang:
d = torch . rand ([ 2 , 2 , 2 ])Tensor dapat digunakan untuk melakukan operasi aljabar secara efisien. Salah satu operasi yang paling umum digunakan dalam aplikasi pembelajaran mesin adalah multiplikasi matriks. Katakanlah Anda ingin melipatgandakan dua matriks acak ukuran 3x5 dan 5x4, ini dapat dilakukan dengan operasi multiplikasi matriks (@):
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )Demikian pula, untuk menambahkan dua vektor, Anda dapat melakukannya:
z = x + yUntuk mengubah tensor menjadi array numpy, Anda dapat memanggil metode Tensor Numpy ():
print ( z . numpy ())Dan Anda selalu dapat mengubah array yang tidak bisa menjadi tensor dengan:
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))Keuntungan paling penting dari Pytorch dibandingkan Numpy adalah fungsi diferensiasi otomatisnya yang sangat berguna dalam aplikasi optimasi seperti mengoptimalkan parameter jaringan saraf. Mari kita coba memahaminya dengan sebuah contoh.
Katakanlah Anda memiliki fungsi komposit yang merupakan rantai dua fungsi: g(u(x)) . Untuk menghitung turunan dari g sehubungan dengan x kita dapat menggunakan aturan rantai yang menyatakan bahwa: dg/dx = dg/du * du/dx . Pytorch dapat secara analitis menghitung turunan untuk kami.
Untuk menghitung turunan di Pytorch pertama kami membuat tensor dan mengatur requires_grad menjadi true. Kami dapat menggunakan operasi tensor untuk menentukan fungsi kami. Kami menganggap u adalah fungsi kuadratik dan g adalah fungsi linier sederhana:
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - u Dalam hal ini fungsi komposit kami adalah g(u(x)) = -x*x . Jadi turunannya sehubungan dengan x adalah -2x . Pada titik x=1 , ini sama dengan -2 .
Mari kita pastikan ini. Ini dapat dilakukan dengan menggunakan fungsi lulusan di Pytorch:
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.) Untuk memahami betapa kuatnya diferensiasi otomatis, mari kita lihat contoh lain. Asumsikan bahwa kami memiliki sampel dari kurva (katakanlah f(x) = 5x^2 + 3 ) dan kami ingin memperkirakan f(x) berdasarkan sampel ini. Kami mendefinisikan fungsi parametrik g(x, w) = w0 x^2 + w1 x + w2 , yang merupakan fungsi dari input x dan parameter laten w , tujuan kami adalah untuk menemukan parameter laten sedemikian rupa sehingga g(x, w) ≈ f(x) . Ini dapat dilakukan dengan meminimalkan fungsi kerugian berikut: L(w) = Σ (f(x) - g(x, w))^2 . Meskipun ada solusi bentuk tertutup untuk masalah sederhana ini, kami memilih untuk menggunakan pendekatan yang lebih umum yang dapat diterapkan pada fungsi yang dapat dibedakan secara sewenang -wenang, dan itu menggunakan keturunan gradien stokastik. Kami hanya menghitung gradien rata -rata L(w) sehubungan dengan w pada satu set titik sampel dan bergerak ke arah yang berlawanan.
Begini cara dilakukan di 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 ())Dengan menjalankan sepotong kode ini, Anda akan melihat hasil yang dekat dengan ini:
[ 4.9924135 , 0.00040895029 , 3.4504161 ]Yang merupakan perkiraan yang relatif dekat dengan parameter kami.
Ini hanya ujung gunung es untuk apa yang bisa dilakukan Pytorch. Banyak masalah seperti mengoptimalkan jaringan saraf besar dengan jutaan parameter dapat diimplementasikan secara efisien di Pytorch hanya dalam beberapa baris kode. Pytorch menangani penskalaan di beberapa perangkat, dan utas, dan mendukung berbagai platform.
Dalam contoh sebelumnya kami menggunakan tensor tulang telanjang dan operasi tensor untuk membangun model kami. Untuk membuat kode Anda sedikit lebih terorganisir, disarankan untuk menggunakan modul Pytorch. Modul hanyalah sebuah wadah untuk parameter Anda dan merangkum operasi model. Misalnya mengatakan Anda ingin mewakili model linier y = ax + b . Model ini dapat diwakili dengan kode berikut:
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 yhatUntuk menggunakan model ini dalam praktiknya, Anda instantiate modul dan cukup menyebutnya seperti fungsi:
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x ) Parameter pada dasarnya adalah tensor dengan requires_grad diatur ke true. Lebih mudah menggunakan parameter karena Anda dapat dengan mudah mengambil semuanya dengan metode parameters() :
for p in net . parameters ():
print ( p ) Sekarang, katakanlah Anda memiliki fungsi yang tidak diketahui y = 5x + 3 + some noise , dan Anda ingin mengoptimalkan parameter model Anda agar sesuai dengan fungsi ini. Anda dapat mulai dengan mencicipi beberapa poin dari fungsi Anda:
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3Mirip dengan contoh sebelumnya, Anda dapat mendefinisikan fungsi kerugian dan mengoptimalkan parameter model Anda sebagai berikut:
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 hadir dengan sejumlah modul yang telah ditentukan. Salah satu modul tersebut adalah torch.nn.Linear yang merupakan bentuk yang lebih umum dari fungsi linier daripada apa yang kita tentukan di atas. Kami dapat menulis ulang modul kami di atas menggunakan torch.nn.Linear seperti ini:
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 Perhatikan bahwa kami menggunakan Squeeze dan tidak cukup karena torch.nn.Linear beroperasi pada batch vektor yang bertentangan dengan skalar.
Secara default panggilan parameter () pada modul akan mengembalikan parameter semua submodulnya:
net = Net ()
for p in net . parameters ():
print ( p ) Ada beberapa modul yang telah ditentukan sebelumnya yang bertindak sebagai wadah untuk modul lain. Modul wadah yang paling umum digunakan adalah torch.nn.Sequential . Seperti namanya, ia digunakan untuk menumpuk beberapa modul (atau lapisan) di atas satu sama lain. Misalnya untuk menumpuk dua lapisan linier dengan ReLU nonlinieritas di antara Anda dapat melakukannya:
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
) Pytorch mendukung operasi penyiaran elemental. Biasanya ketika Anda ingin melakukan operasi seperti penambahan dan perkalian, Anda perlu memastikan bahwa bentuk operan cocok, misalnya Anda tidak dapat menambahkan tensor bentuk [3, 2] ke tensor bentuk [3, 4] . Tapi ada kasus khusus dan saat itulah Anda memiliki dimensi tunggal. Pytorch secara implisit ubin tensor melintasi dimensi tunggal agar sesuai dengan bentuk operan lainnya. Jadi valid untuk menambahkan tensor bentuk [3, 2] ke tensor bentuk [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 )Penyiaran memungkinkan kami untuk melakukan ubin implisit yang membuat kode lebih pendek, dan lebih efisien memori, karena kami tidak perlu menyimpan hasil operasi ubin. Salah satu tempat yang rapi yang dapat digunakan adalah ketika menggabungkan fitur dengan panjang yang bervariasi. Untuk menggabungkan fitur yang bervariasi, kami biasanya ubin tensor input, menggabungkan hasilnya dan menerapkan beberapa nonlinier. Ini adalah pola umum di berbagai arsitektur jaringan saraf:
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]) Tetapi ini dapat dilakukan dengan lebih efisien dengan penyiaran. Kami menggunakan fakta bahwa f(m(x + y)) sama dengan f(mx + my) . Jadi kita dapat melakukan operasi linier secara terpisah dan menggunakan penyiaran untuk melakukan penggabungan implisit:
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])Faktanya sepotong kode ini cukup umum dan dapat diterapkan pada tensor bentuk sewenang -wenang selama penyiaran antar tensor dimungkinkan:
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 cSejauh ini kami membahas bagian baik dari penyiaran. Tapi apa bagian jelek yang mungkin Anda tanyakan? Asumsi implisit hampir selalu membuat debugging lebih sulit dilakukan. Pertimbangkan contoh berikut:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) Menurut Anda apa nilai c setelah evaluasi? Jika Anda menebak 6, itu salah. Ini akan menjadi 12. Ini karena ketika peringkat dua tensor tidak cocok, Pytorch secara otomatis memperluas dimensi pertama tensor dengan peringkat yang lebih rendah sebelum operasi elemental, sehingga hasil penambahan akan [[2, 3], [3, 4]] , dan pengurangan semua parameter akan memberi kami 12.
Cara untuk menghindari masalah ini adalah dengan eksplisit mungkin. Seandainya kami menentukan dimensi mana yang ingin kami kurangi, menangkap bug ini akan jauh lebih mudah:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c ) Di sini nilai c akan menjadi [5, 7] , dan kami segera akan menebak berdasarkan bentuk hasil bahwa ada sesuatu yang salah. Aturan praktis umum adalah untuk selalu menentukan dimensi dalam operasi reduksi dan saat menggunakan torch.squeeze .
Sama seperti Numpy, Pytorch membebani sejumlah operator Python untuk membuat kode Pytorch lebih pendek dan lebih mudah dibaca.
Slicing OP adalah salah satu operator yang kelebihan beban yang dapat membuat tensor pengindeksan sangat mudah:
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)Berhati -hatilah saat menggunakan OP ini. OP pengiris, seperti OP lainnya, memiliki overhead. Karena ini adalah OP yang umum dan terlihat tidak bersalah mungkin banyak digunakan secara berlebihan yang dapat menyebabkan inefisiensi. Untuk memahami betapa tidak efisiennya OP ini, mari kita lihat contohnya. Kami ingin melakukan pengurangan secara manual melintasi baris matriks:
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 )) Ini berjalan cukup lambat dan alasannya adalah bahwa kami memanggil Slice Op 500 kali, yang menambah banyak overhead. Pilihan yang lebih baik adalah dengan menggunakan torch.unbind .
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iIni secara signifikan (~ 30% pada mesin saya) lebih cepat.
Tentu saja, cara yang tepat untuk melakukan pengurangan sederhana ini adalah dengan menggunakan torch.sum op untuk ini dalam satu op:
z = torch . sum ( x , dim = 0 )yang sangat cepat (~ 100x lebih cepat pada mesin saya).
Pytorch juga membebani berbagai operator aritmatika dan logis:
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) Anda juga dapat menggunakan versi augmented dari OPS ini. Misalnya x += y dan x **= 2 juga valid.
Perhatikan bahwa Python tidak mengizinkan kelebihan beban and , or , dan not kata kunci.
Pytorch dioptimalkan untuk melakukan operasi pada tensor besar. Melakukan banyak operasi pada tensor kecil cukup tidak efisien di Pytorch. Jadi, bila memungkinkan, Anda harus menulis ulang perhitungan Anda dalam bentuk batch untuk mengurangi overhead dan meningkatkan kinerja. Jika tidak ada cara Anda dapat secara manual mengumpulkan operasi Anda, menggunakan Torchscript dapat meningkatkan kinerja kode Anda. Torchscript hanyalah subset dari fungsi Python yang diakui oleh Pytorch. Pytorch dapat secara otomatis mengoptimalkan kode Torchscript Anda menggunakan kompiler Just In Time (JIT) dan mengurangi beberapa overhead.
Mari kita lihat sebuah contoh. Operasi yang sangat umum dalam aplikasi ML adalah "Batch Gather". Operasi ini dapat dengan mudah ditulis sebagai output[i] = input[i, index[i]] . Ini dapat dengan mudah diimplementasikan di Pytorch sebagai berikut:
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Untuk mengimplementasikan fungsi yang sama menggunakan Torchscript cukup gunakan torch.jit.script Decorator:
@ 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 )Pada tes saya, ini sekitar 10% lebih cepat.
Tapi tidak ada yang mengalahkan operasi Anda secara manual. Implementasi vektor dalam tes saya 100 kali lebih cepat:
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 Dalam pelajaran terakhir kami berbicara tentang menulis kode pytorch yang efisien. Tetapi untuk membuat kode Anda berjalan dengan efisiensi maksimum, Anda juga perlu memuat data Anda secara efisien ke dalam memori perangkat Anda. Untungnya Pytorch menawarkan alat untuk memudahkan data. Ini disebut DataLoader . DataLoader menggunakan banyak pekerja untuk secara bersamaan memuat data dari Dataset dan secara opsional menggunakan Sampler untuk sampel entri data dan membentuk batch.
Jika Anda dapat mengakses data Anda secara acak, menggunakan DataLoader sangat mudah: Anda hanya perlu mengimplementasikan kelas Dataset yang mengimplementasikan __getitem__ (untuk membaca setiap item data) dan __len__ (untuk mengembalikan jumlah item dalam metode dataset). Misalnya, inilah cara memuat gambar dari direktori yang diberikan:
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 )Untuk memuat semua gambar jpeg dari direktori yang diberikan, Anda kemudian dapat melakukan hal berikut:
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataDi sini kami menggunakan 8 pekerja untuk membaca data kami secara bersamaan dari disk. Anda dapat menyetel jumlah pekerja di mesin Anda untuk hasil yang optimal.
Menggunakan DataLoader untuk membaca data dengan akses acak mungkin baik -baik saja jika Anda memiliki penyimpanan cepat atau jika item data Anda besar. Tetapi bayangkan memiliki sistem file jaringan dengan koneksi lambat. Meminta masing -masing file dengan cara ini bisa sangat lambat dan mungkin akan berakhir menjadi hambatan pipa pelatihan Anda.
Pendekatan yang lebih baik adalah menyimpan data Anda dalam format file yang berdekatan yang dapat dibaca secara berurutan. Misalnya jika Anda memiliki banyak koleksi gambar yang dapat Anda gunakan TAR untuk membuat arsip tunggal dan mengekstrak file dari arsip secara berurutan dalam Python. Untuk melakukan ini, Anda dapat menggunakan IterableDataset Pytorch. Untuk membuat kelas IterableDataset , Anda hanya perlu mengimplementasikan metode __iter__ yang secara berurutan membaca dan menghasilkan item data dari dataset.
Implementasi yang naif akan menyukai ini:
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 )Tetapi ada masalah besar dengan implementasi ini. Jika Anda mencoba menggunakan Dataloader untuk membaca dari dataset ini dengan lebih dari satu pekerja, Anda akan mengamati banyak gambar yang digandakan:
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items Masalahnya adalah bahwa setiap pekerja membuat instance terpisah dari dataset dan masing -masing akan dimulai dari awal dataset. Salah satu cara untuk menghindari ini adalah dengan alih -alih memiliki satu file tar, membagi data Anda menjadi file tar terpisah num_workers dan memuat masing -masing dengan pekerja yang terpisah:
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 ])Beginilah kelas dataset kami dapat digunakan:
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with dataKami membahas strategi sederhana untuk menghindari masalah entri yang digandakan. Paket Tfrecord menggunakan strategi yang sedikit lebih canggih untuk menghiasi data Anda dengan cepat.
Saat menggunakan pustaka komputasi numerik seperti Numpy atau Pytorch, penting untuk dicatat bahwa menulis kode yang benar secara matematis tidak selalu mengarah pada hasil yang benar. Anda juga perlu memastikan bahwa perhitungannya stabil.
Mari kita mulai dengan contoh sederhana. Secara matematis, mudah untuk melihat bahwa x * y / y = x untuk nilai non nol dari x . Tapi mari kita lihat apakah itu selalu benar dalam praktiknya:
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 Alasan untuk hasil yang salah adalah bahwa y terlalu kecil untuk tipe float32. Masalah serupa terjadi ketika y terlalu besar:
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanNilai positif terkecil yang dapat diwakili oleh float32 adalah 1.4013E-45 dan apa pun di bawah yang akan disimpan sebagai nol. Juga, berapa pun angka di luar 3.40282e+38, akan disimpan sebagai INF.
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38Untuk memastikan bahwa perhitungan Anda stabil, Anda ingin menghindari nilai dengan nilai absolut yang kecil atau sangat besar. Ini mungkin terdengar sangat jelas, tetapi masalah semacam ini bisa menjadi sangat sulit untuk debug terutama ketika melakukan penurunan gradien di Pytorch. Ini karena Anda tidak hanya perlu memastikan bahwa semua nilai di Forward Pass berada dalam kisaran yang valid dari tipe data Anda, tetapi juga Anda perlu memastikan hal yang sama untuk pass mundur (selama perhitungan gradien).
Mari kita lihat contoh nyata. Kami ingin menghitung softmax di atas vektor log. Implementasi yang naif akan terlihat seperti ini:
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.] Perhatikan bahwa menghitung eksponensial logit untuk jumlah yang relatif kecil menghasilkan hasil raksasa yang berada di luar kisaran float32. Logit valid terbesar untuk implementasi Softmax naif kami adalah ln(3.40282e+38) = 88.7 , apa pun di luar yang mengarah ke hasil NAN.
Tapi bagaimana kita bisa membuat ini lebih stabil? Solusinya agak sederhana. Mudah untuk melihat bahwa exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x) . Oleh karena itu kami dapat mengurangi konstanta apa pun dari log dan hasilnya akan tetap sama. Kami memilih konstan ini untuk menjadi maksimum log. Dengan cara ini domain fungsi eksponensial akan terbatas pada [-inf, 0] , dan akibatnya kisarannya adalah [0.0, 1.0] yang diinginkan:
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.] Mari kita lihat kasus yang lebih rumit. Pertimbangkan kami memiliki masalah klasifikasi. Kami menggunakan fungsi softmax untuk menghasilkan probabilitas dari login kami. Kami kemudian mendefinisikan fungsi kehilangan kami untuk menjadi entropi silang antara prediksi kami dan label. Ingatlah bahwa entropi silang untuk distribusi kategorikal dapat dengan mudah didefinisikan sebagai xe(p, q) = -Σ p_i log(q_i) . Jadi implementasi naif dari entropi silang akan terlihat seperti ini:
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 infPerhatikan bahwa dalam implementasi ini ketika output softmax mendekati nol, output log mendekati infinity yang menyebabkan ketidakstabilan dalam perhitungan kami. Kita dapat menulis ulang ini dengan memperluas softmax dan melakukan beberapa penyederhanaan:
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.0Kami juga dapat memverifikasi bahwa gradien juga dihitung dengan benar:
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]Izinkan saya mengingatkan lagi bahwa perawatan ekstra harus diambil saat melakukan keturunan gradien untuk memastikan bahwa kisaran fungsi Anda serta gradien untuk setiap lapisan berada dalam kisaran yang valid. Fungsi eksponensial dan logaritmik ketika digunakan secara naif sangat bermasalah karena mereka dapat memetakan jumlah kecil ke yang sangat besar dan sebaliknya.
Secara default tensor dan parameter model di pytorch disimpan dalam presisi titik mengambang 32-bit. Pelatihan jaringan saraf menggunakan pelampung 32-bit biasanya stabil dan tidak menyebabkan masalah numerik utama, namun jaringan saraf telah terbukti berkinerja cukup baik dalam presisi 16-bit dan bahkan lebih rendah. Perhitungan dalam presisi yang lebih rendah dapat secara signifikan lebih cepat pada GPU modern. Ini juga memiliki manfaat ekstra menggunakan lebih sedikit memori yang memungkinkan pelatihan model yang lebih besar dan/atau dengan ukuran batch yang lebih besar yang dapat meningkatkan kinerja lebih lanjut. Masalahnya adalah bahwa pelatihan dalam 16 bit sering menjadi sangat tidak stabil karena presisi biasanya tidak cukup untuk melakukan beberapa operasi seperti akumulasi.
Untuk membantu masalah ini, Pytorch mendukung pelatihan dalam presisi beragam. Singkatnya, pelatihan presisi campuran dilakukan dengan melakukan beberapa operasi mahal (seperti konvolusi dan matriks multplikasi) dalam 16-bit dengan mencatat input sambil melakukan operasi sensitif numerik lainnya seperti akumulasi dalam 32-bit. Dengan cara ini kita mendapatkan semua manfaat dari komputasi 16-bit tanpa kelemahannya. Selanjutnya kita berbicara tentang menggunakan AutoCast dan GradScaler untuk melakukan pelatihan presisi campuran otomatis.
autocast membantu meningkatkan kinerja runtime dengan secara otomatis memasukkan data ke 16-bit untuk beberapa perhitungan. Untuk memahami cara kerjanya, mari kita lihat sebuah contoh:
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 Perhatikan baik x dan y adalah tensor 32-bit, tetapi autocast melakukan multiplikasi matriks dalam 16-bit sambil menjaga operasi penambahan dalam 32-bit. Bagaimana jika salah satu operan dalam 16-bit?
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 Sekali lagi autocast dan menurunkan operan 32-bit menjadi 16-bit untuk melakukan multiplikasi matriks, tetapi tidak mengubah operasi penambahan. Secara default, penambahan dua tensor dalam Pytorch menghasilkan gips ke presisi yang lebih tinggi.
Dalam praktiknya, Anda dapat mempercayai autocast untuk melakukan casting yang tepat untuk meningkatkan efisiensi runtime. Yang penting adalah menjaga semua perhitungan lulus ke depan Anda di bawah konteks autocast :
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )Ini mungkin yang Anda butuhkan jika Anda memiliki masalah optimisasi yang relatif stabil dan jika Anda menggunakan tingkat belajar yang relatif rendah. Menambahkan satu baris kode tambahan ini dapat mengurangi pelatihan Anda hingga setengah pada perangkat keras modern.
Seperti yang kami sebutkan di awal bagian ini, presisi 16-bit mungkin tidak selalu cukup untuk beberapa perhitungan. Salah satu kasus yang menarik adalah mewakili nilai -nilai gradien, sebagian besar yang biasanya merupakan nilai kecil. Mewakili mereka dengan float 16-bit sering menyebabkan underflow buffer (yaitu mereka akan diwakili sebagai nol). Ini membuat pelatihan jaringan saraf sangat tidak stabil. GradScalar dirancang untuk menyelesaikan masalah ini. Dibutuhkan sebagai input nilai kerugian Anda dan melipatgandakannya dengan skalar besar, menggembungkan nilai gradien, dan karenanya membuatnya diwakili dalam presisi 16-bit. Kemudian menskalakannya selama pembaruan gradien untuk memastikan parameter diperbarui dengan benar. Ini umumnya apa yang dilakukan GradScalar . Tapi di bawah kap GradScalar sedikit lebih pintar dari itu. Mengembangkan gradien sebenarnya dapat menghasilkan luapan yang sama buruknya. Jadi GradScalar sebenarnya memantau nilai -nilai gradien dan jika mendeteksi meluap, ia melewatkan pembaruan, menurunkan faktor skalar sesuai dengan jadwal yang dapat dikonfigurasi. (Jadwal default biasanya berfungsi tetapi Anda mungkin perlu menyesuaikannya untuk kasus penggunaan Anda.)
Menggunakan GradScalar sangat mudah dalam praktik:
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update () Perhatikan bahwa pertama -tama kita membuat instance dari GradScalar . Dalam pelatihan loop kami memanggil GradScalar.scale untuk skala kerugian sebelum menelepon ke belakang untuk menghasilkan gradien yang meningkat, kami kemudian menggunakan GradScalar.step yang (mungkin) memperbarui parameter model. Kami kemudian menghubungi GradScalar.update yang melakukan pembaruan skalar jika diperlukan. Itu saja!
Berikut ini adalah kode sampel yang menunjukkan kasus pelatihan presisi campuran pada masalah belajar sintetis untuk menghasilkan kotak -kotak dari koordinat gambar. Anda dapat menempelkannya di Google Colab, mengatur backend ke GPU dan membandingkan kinerja single dan presisi campuran. Perhatikan bahwa ini adalah contoh mainan kecil, dalam praktiknya dengan jaringan yang lebih besar Anda mungkin melihat peningkatan yang lebih besar dalam kinerja menggunakan presisi campuran.
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 ()