Halo, Habr!
Kami telah memesan sebelumnya buku yang sudah lama ditunggu-tunggu tentang 
perpustakaan PyTorch .

Karena Anda akan mempelajari semua materi dasar yang diperlukan tentang PyTorch dari buku ini, kami mengingatkan Anda tentang 
manfaat dari proses yang disebut "grokking" atau "pemahaman mendalam" dari topik yang ingin Anda pelajari. Dalam posting hari ini, kami akan memberi tahu Anda bagaimana Kai Arulkumaran membanting PyTorch (tidak ada gambar). Selamat datang di kucing.
PyTorch adalah kerangka pembelajaran dalam fleksibel yang secara otomatis membedakan antara objek menggunakan jaringan saraf dinamis (yaitu, jaringan menggunakan kontrol aliran dinamis, seperti 
if dan 
while loop). PyTorch mendukung akselerasi GPU, 
pelatihan terdistribusi , berbagai jenis 
optimasi dan banyak fitur menarik lainnya. Di sini saya mengemukakan beberapa pemikiran tentang bagaimana, menurut pendapat saya, harus menggunakan PyTorch; semua aspek perpustakaan dan praktik yang disarankan tidak dibahas di sini, tetapi semoga teks ini bermanfaat bagi Anda.
Jaringan saraf adalah subkelas dari grafik komputasi. Komputasi grafik menerima data sebagai input, maka data ini dirutekan (dan dapat dikonversi) pada node di mana mereka diproses. Dalam pembelajaran mendalam, neuron (node) biasanya mentransformasikan data dengan menerapkan parameter dan fungsi terdiferensiasi padanya, sehingga parameter dapat dioptimalkan untuk meminimalkan kerugian dengan metode gradient descent. Dalam arti yang lebih luas, saya perhatikan bahwa fungsi dapat menjadi stokastik dan grafik yang dinamis. Dengan demikian, sementara jaringan saraf cocok dengan paradigma pemrograman aliran data, API PyTorch berfokus pada paradigma 
pemrograman imperatif , dan cara menafsirkan program yang sedang dibuat ini jauh lebih akrab. Itulah sebabnya kode PyTorch lebih mudah dibaca, lebih mudah menilai desain program yang rumit, yang, bagaimanapun, tidak memerlukan kompromi serius pada kinerja: pada kenyataannya, PyTorch cukup cepat dan memberikan banyak optimisasi bahwa Anda, sebagai pengguna akhir, tidak dapat khawatir (Namun, jika Anda benar-benar tertarik pada mereka, Anda dapat menggali sedikit lebih dalam dan mengenal mereka).
Sisa dari artikel ini adalah analisis dari 
contoh resmi pada dataset MNIST . Di sini kita 
bermain PyTorch, jadi saya sarankan untuk memahami artikel hanya setelah berkenalan dengan manual 
pemula resmi . Untuk kenyamanan, kode disajikan dalam bentuk fragmen kecil yang dilengkapi dengan komentar, yaitu, tidak didistribusikan ke fungsi / file terpisah yang biasa Anda lihat dalam kode modular murni.
Impor
 import argparse import os import torch from torch import nn, optim from torch.nn import functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms 
Semua ini adalah impor standar, dengan pengecualian modul 
torchvision , yang secara khusus digunakan secara aktif untuk menyelesaikan tugas yang berkaitan dengan visi komputer.
Kustomisasi
 parser = argparse.ArgumentParser(description='PyTorch MNIST Example') parser.add_argument('--batch-size', type=int, default=64, metavar='N', help='input batch size for training (default: 64)') parser.add_argument('--epochs', type=int, default=10, metavar='N', help='number of epochs to train (default: 10)') parser.add_argument('--lr', type=float, default=0.01, metavar='LR', help='learning rate (default: 0.01)') parser.add_argument('--momentum', type=float, default=0.5, metavar='M', help='SGD momentum (default: 0.5)') parser.add_argument('--no-cuda', action='store_true', default=False, help='disables CUDA training') parser.add_argument('--seed', type=int, default=1, metavar='S', help='random seed (default: 1)') parser.add_argument('--save-interval', type=int, default=10, metavar='N', help='how many batches to wait before checkpointing') parser.add_argument('--resume', action='store_true', default=False, help='resume training from checkpoint') args = parser.parse_args() use_cuda = torch.cuda.is_available() and not args.no_cuda device = torch.device('cuda' if use_cuda else 'cpu') torch.manual_seed(args.seed) if use_cuda: torch.cuda.manual_seed(args.seed) 
argparse adalah cara standar untuk menangani argumen baris perintah dengan Python.
Jika Anda perlu menulis kode yang dirancang untuk bekerja pada perangkat yang berbeda (menggunakan akselerasi GPU, ketika tersedia, tetapi jika itu tidak digulirkan kembali ke perhitungan pada CPU), lalu pilih dan simpan alat 
torch.device sesuai, dengan mana Anda dapat menentukan di mana Anda harus tensor disimpan. Untuk informasi lebih lanjut tentang cara membuat kode seperti itu, lihat 
dokumentasi resmi . Pendekatan PyTorch adalah membawa pemilihan perangkat ke kontrol pengguna, yang mungkin tampak tidak diinginkan dalam contoh sederhana. Namun, pendekatan ini sangat menyederhanakan pekerjaan ketika Anda harus berurusan dengan tensor, yang a) nyaman untuk debugging b) memungkinkan Anda untuk secara efektif menggunakan perangkat secara manual.
Untuk reproduksibilitas percobaan, Anda perlu menetapkan nilai awal acak untuk semua komponen yang menggunakan pembuatan angka acak (termasuk 
random atau 
numpy , jika Anda juga menggunakannya). Harap dicatat: cuDNN menggunakan algoritma non-deterministik dan secara opsional dinonaktifkan menggunakan 
torch.backends.cudnn.enabled = False .
Data
 data_path = os.path.join(os.path.expanduser('~'), '.torch', 'datasets', 'mnist') train_data = datasets.MNIST(data_path, train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) test_data = datasets.MNIST(data_path, train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) train_loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_data, batch_size=args.batch_size, num_workers=4, pin_memory=True) 
Karena model 
torchvision disimpan di bawah 
~/.torch/models/ , saya lebih suka menyimpan set 
torchvision torchvision di bawah 
~/.torch/datasets . Ini adalah perjanjian hak cipta saya, tetapi sangat nyaman digunakan dalam proyek yang dikembangkan atas dasar MNIST, CIFAR-10, dll. Secara umum, kumpulan data harus disimpan secara terpisah dari kode jika Anda bermaksud untuk menggunakan kembali beberapa kumpulan data.
torchvision.transforms berisi banyak opsi konversi yang nyaman untuk gambar individual, seperti pemotongan dan normalisasi.
Ada banyak opsi di 
batch_size , tetapi selain 
batch_size dan 
shuffle , Anda juga harus ingat 
num_workers dan 
pin_memory , mereka membantu meningkatkan efisiensi. 
num_workers > 0 menggunakan subproses untuk memuat data asinkron, dan tidak memblokir proses utama untuk ini. Kasus penggunaan yang umum adalah memuat data (misalnya, gambar) dari disk dan, mungkin, mengubahnya; semua ini dapat dilakukan secara paralel, bersamaan dengan pemrosesan data jaringan. Tingkat pemrosesan mungkin perlu disesuaikan untuk a) meminimalkan jumlah pekerja dan, akibatnya, jumlah CPU dan RAM yang digunakan (masing-masing pekerja memuat bets terpisah, daripada sampel individu yang termasuk dalam bets) b) meminimalkan lamanya waktu data menunggu di jaringan. 
pin_memory menggunakan 
memori yang disematkan (sebagai lawan dari paged) untuk mempercepat operasi transfer data dari RAM ke GPU (dan tidak melakukan apa-apa dengan kode khusus untuk CPU).
Model
 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = self.fc2(x) return F.log_softmax(x, dim=1) model = Net().to(device) optimiser = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum) if args.resume: model.load_state_dict(torch.load('model.pth')) optimiser.load_state_dict(torch.load('optimiser.pth')) 
Inisialisasi jaringan biasanya meluas ke variabel anggota, lapisan yang berisi parameter pembelajaran dan, mungkin, parameter pembelajaran individu dan buffer yang tidak terlatih. Kemudian, dengan pass langsung, mereka digunakan dalam kombinasi dengan fungsi-fungsi dari 
F yang murni fungsional dan tidak mengandung parameter. Beberapa orang suka bekerja dengan jaringan yang berfungsi murni (mis. Menjaga parameter dan menggunakan 
F.conv2d alih-alih 
nn.Conv2d ) atau jaringan yang seluruhnya terdiri dari lapisan (mis. 
nn.ReLU alih-alih 
F.relu ).
.to(device) adalah cara mudah untuk mengirim parameter perangkat (dan buffer) ke GPU jika 
device diatur ke GPU, karena jika tidak (jika perangkat diatur ke CPU) tidak ada yang akan dilakukan. Penting untuk mentransfer parameter perangkat ke perangkat yang sesuai sebelum meneruskannya ke pengoptimal; jika tidak, pengoptimal tidak akan dapat melacak parameter dengan benar!
Baik neural networks ( 
nn.Module ) dan optimizer ( 
optim.Optimizer ) dapat menyimpan dan memuat keadaan internal mereka, dan disarankan untuk melakukan ini dengan 
.load_state_dict(state_dict) - perlu memuat ulang keadaan keduanya untuk melanjutkan pelatihan berdasarkan kamus yang disimpan sebelumnya. menyatakan. Menyimpan seluruh objek mungkin 
penuh dengan kesalahan . Jika Anda menyimpan tensor pada GPU dan ingin memuatnya ke CPU atau GPU lain, maka cara termudah adalah memuatnya langsung ke CPU menggunakan 
opsi map_location , mis. 
torch.load('model.pth' , 
map_location='cpu' ).
Berikut adalah beberapa poin lain yang tidak ditampilkan di sini, tetapi layak disebutkan, bahwa Anda dapat menggunakan aliran kontrol dengan operan langsung (misalnya, eksekusi 
if mungkin tergantung pada variabel anggota atau pada data itu sendiri. Selain itu, itu sangat valid di tengah proses untuk menghasilkan ( 
print ) tensor, yang sangat menyederhanakan debugging. Akhirnya, dengan pass langsung, banyak argumen dapat digunakan. Saya akan mengilustrasikan poin ini dengan daftar pendek yang tidak terkait dengan ide tertentu:
 def forward(self, x, hx, drop=False): hx2 = self.rnn(x, hx) print(hx.mean().item(), hx.var().item()) if hx.max.item() > 10 or self.can_drop and drop: return hx else: return hx2 
Pelatihan
 model.train() train_losses = [] for i, (data, target) in enumerate(train_loader): data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) optimiser.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() train_losses.append(loss.item()) optimiser.step() if i % 10 == 0: print(i, loss.item()) torch.save(model.state_dict(), 'model.pth') torch.save(optimiser.state_dict(), 'optimiser.pth') torch.save(train_losses, 'train_losses.pth') 
Modul jaringan dimasukkan ke dalam mode pelatihan secara default - yang sedikit banyak mempengaruhi operasi modul, sebagian besar - penipisan dan normalisasi batch. Dengan satu atau lain cara, lebih baik untuk mengatur hal-hal seperti itu secara manual menggunakan 
.train() , yang memfilter bendera "pelatihan" ke semua modul anak.
Di sini, metode 
.to() tidak hanya menerima perangkat, tetapi juga menetapkan 
non_blocking=True , sehingga memastikan penyalinan data yang tidak sinkron ke GPU dari memori yang dikomit, memungkinkan CPU untuk tetap beroperasi selama transfer data; jika tidak, 
non_blocking=True sekali bukan opsi.
Sebelum Anda membangun satu set gradien baru menggunakan 
loss.backward() dan 
optimiser.step() menggunakan 
optimiser.step() , Anda harus secara manual mengatur ulang gradien parameter yang akan dioptimalkan menggunakan 
optimiser.zero_grad() . Secara default, PyTorch mengakumulasi gradien, yang sangat nyaman jika Anda tidak memiliki sumber daya yang cukup untuk menghitung semua gradien yang Anda butuhkan dalam satu pass.
PyTorch menggunakan sistem "pita" gradien otomatis - ia mengumpulkan informasi tentang operasi apa dan dalam urutan apa yang dilakukan pada tensor, dan kemudian memutarnya dalam arah yang berlawanan untuk melakukan diferensiasi dalam urutan terbalik (reverse-mode diferensiation). Itu sebabnya sangat fleksibel dan memungkinkan grafik komputasi yang sewenang-wenang. Jika tidak ada dari tensor ini yang memerlukan gradien (Anda harus menetapkan 
requires_grad=True , membuat tensor untuk tujuan ini), maka tidak ada grafik yang disimpan! Namun, jaringan biasanya memiliki parameter yang memerlukan gradien, sehingga setiap perhitungan yang dilakukan berdasarkan output jaringan akan disimpan dalam grafik. Jadi, jika Anda ingin menyimpan data yang dihasilkan dari langkah ini, Anda harus menonaktifkan gradien secara manual atau (pendekatan yang lebih umum), menyimpan informasi ini sebagai nomor Python (menggunakan 
.item() dalam skalar PyTorch) atau array 
numpy . Baca lebih lanjut tentang 
autograd di 
dokumentasi resmi .
Salah satu cara untuk mengurangi grafik komputasi adalah dengan menggunakan 
.detach() ketika keadaan tersembunyi dilewatkan saat mempelajari RNN dengan versi terpotong dari backpropagation-through-time. Juga nyaman saat membedakan kerugian, ketika salah satu komponen adalah output dari jaringan lain, tetapi jaringan lain ini tidak boleh dioptimalkan sehubungan dengan kerugian. Sebagai contoh, saya akan mengajarkan bagian diskriminatif pada pembuatan bahan keluaran saat bekerja dengan GAN, atau pelatihan kebijakan dalam algoritme aktor-kritik menggunakan fungsi objektif sebagai fungsi dasar (misalnya, A2C). Teknik lain yang mencegah perhitungan gradien efektif dalam pelatihan GAN (pelatihan bagian pembangkit pada bahan diskriminan) dan tipikal dalam fine tuning adalah penghitungan siklus parameter jaringan yang 
param.requires_grad = False .
Penting tidak hanya untuk mencatat hasil dalam file konsol / log, tetapi juga untuk menetapkan titik kontrol dalam parameter model (dan status pengoptimal) untuk berjaga-jaga. Anda juga dapat menggunakan 
torch.save() untuk menyimpan objek Python biasa, atau menggunakan solusi standar lain - 
pickle .
Pengujian
 model.eval() test_loss, correct = 0, 0 with torch.no_grad(): for data, target in test_loader: data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) output = model(data) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.argmax(1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_data) acc = correct / len(test_data) print(acc, test_loss) 
Menanggapi 
.train() jaringan harus secara eksplisit dimasukkan ke mode evaluasi menggunakan 
.eval() .
Seperti disebutkan di atas, ketika menggunakan jaringan, grafik komputasi biasanya dikompilasi. Untuk mencegah hal ini, gunakan 
no_grad konteks 
no_grad dengan 
with torch.no_grad() .
Lagi
Ini adalah bagian tambahan, di mana saya membuat beberapa penyimpangan yang lebih bermanfaat.
Berikut adalah 
dokumentasi resmi yang menjelaskan bekerja dengan memori.
Kesalahan CUDA? Sulit untuk memperbaikinya, dan biasanya mereka terhubung dengan inkonsistensi logis, yang menurutnya pesan kesalahan yang lebih masuk akal ditampilkan pada CPU daripada pada GPU. Yang terbaik dari semuanya, jika Anda berencana untuk bekerja dengan GPU, Anda dapat dengan cepat beralih antara CPU dan GPU. Tip pengembangan yang lebih umum adalah mengatur kode sehingga dapat dengan cepat diperiksa sebelum memulai tugas penuh. Misalnya, siapkan dataset kecil atau sintetis, jalankan uji satu kereta + era, dll. Jika masalahnya adalah kesalahan CUDA, atau Anda tidak dapat beralih ke CPU sama sekali, setel CUDA_LAUNCH_BLOCKING = 1. Ini akan membuat kernel CUDA meluncurkan sinkron, dan Anda akan menerima pesan kesalahan yang lebih akurat.
Catatan tentang 
torch.multiprocessing atau hanya menjalankan beberapa skrip PyTorch secara bersamaan. Karena PyTorch menggunakan pustaka BLAS multi-threaded untuk mempercepat perhitungan aljabar linier pada CPU, biasanya beberapa core terlibat. Jika Anda ingin melakukan beberapa hal secara bersamaan, menggunakan pemrosesan multi-utas atau beberapa skrip, mungkin disarankan untuk mengurangi jumlah mereka secara manual dengan mengatur variabel lingkungan 
OMP_NUM_THREADS menjadi 1 atau nilai rendah lainnya. Dengan demikian, kemungkinan tergelincirnya prosesor berkurang. Dokumentasi resmi memiliki komentar lain mengenai pemrosesan multithread.