====== Design Orientat pe Obiecte pentru Implementare ======
În introducerea noastră în regresia liniară, am parcurs diverse componente, inclusiv datele, modelul, funcția de pierdere și algoritmul de optimizare. Într-adevăr, regresia liniară este unul dintre cele mai simple modele de machine learning. Antrenarea acestuia, însă, utilizează multe dintre aceleași componente pe care le necesită alte modele din această carte. Prin urmare, înainte de a intra în detaliile de implementare, merită să proiectăm unele dintre API-urile pe care le folosim pe parcurs. Tratând componentele din deep learning ca obiecte, putem începe prin a defini clase pentru aceste obiecte și interacțiunile lor. Acest design orientat pe obiecte pentru implementare va simplifica considerabil prezentarea și s-ar putea chiar să doriți să îl utilizați în proiectele dumneavoastră.
Inspirat de biblioteci open-source precum [[https://www.pytorchlightning.ai/|PyTorch Lightning]], la un nivel înalt dorim să avem trei clase: (i) ''%%Module%%'' conține modele, funcții de pierdere și metode de optimizare; (ii) ''%%DataModule%%'' oferă încărcătoare de date (loaders) pentru antrenare și validare; (iii) ambele clase sunt combinate folosind clasa ''%%Trainer%%'', care ne permite să antrenăm modele pe o varietate de platforme hardware. Majoritatea codului din această carte adaptează ''%%Module%%'' și ''%%DataModule%%''. Vom aborda clasa ''%%Trainer%%'' doar atunci când vom discuta despre GPU-uri, CPU-uri, antrenare paralelă și algoritmi de optimizare.
import time
import numpy as np
from d2l import torch as d2l
import torch
from torch import nn
===== Utilități =====
Avem nevoie de câteva utilități pentru a simplifica programarea orientată pe obiecte în notebook-urile Jupyter. Una dintre provocări este că definițiile claselor tind să fie blocuri destul de lungi de cod. Lizibilitatea notebook-urilor cere fragmente scurte de cod, intercalate cu explicații, o cerință incompatibilă cu stilul de programare obișnuit pentru bibliotecile Python. Prima funcție utilitară ne permite să înregistrăm funcții ca metode într-o clasă //după// ce clasa a fost creată. De fapt, putem face acest lucru //chiar și după// ce am creat instanțe ale clasei! Aceasta ne permite să împărțim implementarea unei clase în mai multe blocuri de cod.
def add_to_class(Class): #@save
"""Register functions as methods in created class."""
def wrapper(obj):
setattr(Class, obj.__name__, obj)
return wrapper
Să aruncăm o privire rapidă asupra modului de utilizare. Planificăm să implementăm o clasă ''%%A%%'' cu o metodă ''%%do%%''. În loc să avem codul atât pentru ''%%A%%'', cât și pentru ''%%do%%'' în același bloc de cod, putem mai întâi să declarăm clasa ''%%A%%'' și să creăm o instanță ''%%a%%''.
class A:
def __init__(self):
self.b = 1
a = A()
În continuare, definim metoda ''%%do%%'' așa cum am face-o în mod normal, dar nu în domeniul (scope) clasei ''%%A%%''. În schimb, decorăm această metodă cu ''%%add_to_class%%'' având clasa ''%%A%%'' ca argument. Procedând astfel, metoda poate accesa variabilele membre ale clasei ''%%A%%'' exact așa cum ne-am aștepta dacă ar fi fost inclusă ca parte a definiției clasei ''%%A%''. Să vedem ce se întâmplă când o invocăm pentru instanța ''%%a%%''.
@add_to_class(A)
def do(self):
print('Class attribute "b" is', self.b)
a.do()
A doua este o clasă utilitară care salvează toate argumentele din metoda ''%%__init__%%'' a unei clase ca atribute ale clasei. Aceasta ne permite să extindem semnăturile apelurilor constructorilor în mod implicit, fără cod suplimentar.
class HyperParameters: #@save
"""Clasa de bază a hiperparametrilor."""
def save_hyperparameters(self, ignore=[]):
raise NotImplemented
Amânăm implementarea sa în . Pentru a o utiliza, definim clasa noastră care moștenește din ''%%HyperParameters%%'' și apelează ''%%save_hyperparameters%%'' în metoda ''%%__init__%%''.
# Call the fully implemented HyperParameters class saved in d2l
class B(d2l.HyperParameters):
def __init__(self, a, b, c):
self.save_hyperparameters(ignore=['c'])
print('self.a =', self.a, 'self.b =', self.b)
print('There is no self.c =', not hasattr(self, 'c'))
b = B(a=1, b=2, c=3)
Utilitatea finală ne permite să trasăm progresul experimentului în mod interactiv în timp ce acesta are loc. Din respect pentru mult mai puternicul (și complexul) [[https://www.tensorflow.org/tensorboard|TensorBoard]], o numim ''%%ProgressBoard%%''. Implementarea este amânată pentru . Deocamdată, să o vedem pur și simplu în acțiune.
Metoda ''%%draw%%'' trasează un punct ''%%(x, y)%%'' în figură, cu ''%%label%%'' specificat în legendă. Argumentul opțional ''%%every_n%%'' netezește linia prin afișarea a doar $1/n$ puncte în figură. Valorile lor sunt mediate din cele $n$ puncte vecine din figura originală.
class ProgressBoard(d2l.HyperParameters): #@save
"""Tabloul care trasează punctele de date în animație."""
def __init__(self, xlabel=None, ylabel=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
ls=['-', '--', '-.', ':'], colors=['C0', 'C1', 'C2', 'C3'],
fig=None, axes=None, figsize=(3.5, 2.5), display=True):
self.save_hyperparameters()
def draw(self, x, y, label, every_n=1):
raise NotImplemented
În următorul exemplu, trasăm ''%%sin%%'' și ''%%cos%%'' cu o netezime diferită. Dacă rulați acest bloc de cod, veți vedea liniile crescând în animație.
board = d2l.ProgressBoard('x')
for x in np.arange(0, 10, 0.1):
board.draw(x, np.sin(x), 'sin', every_n=2)
board.draw(x, np.cos(x), 'cos', every_n=10)
===== Modele =====
Clasa ''%%Module%%'' este clasa de bază pentru toate modelele pe care le vom implementa. Cel puțin, avem nevoie de trei metode. Prima, ''%%__init__%%'', stochează parametrii învățabili, metoda ''%%training_step%%'' acceptă un lot (batch) de date pentru a returna valoarea pierderii și, în final, ''%%configure_optimizers%%'' returnează metoda de optimizare, sau o listă a acestora, care este utilizată pentru a actualiza parametrii învățabili. Opțional, putem defini ''%%validation_step%%'' pentru a raporta măsurile de evaluare. Uneori punem codul pentru calcularea ieșirii într-o metodă separată ''%%forward%%'' pentru a o face mai reutilizabilă.
class Module(d2l.nn_Module, d2l.HyperParameters): #@save
"""Clasa de bază a modelelor."""
def __init__(self, plot_train_per_epoch=2, plot_valid_per_epoch=1):
super().__init__()
self.save_hyperparameters()
self.board = ProgressBoard()
def loss(self, y_hat, y):
raise NotImplementedError
def forward(self, X):
assert hasattr(self, 'net'), 'Neural network is defined'
return self.net(X)
def plot(self, key, value, train):
"""Trasează un punct în animație."""
assert hasattr(self, 'trainer'), 'Trainer is not inited'
self.board.xlabel = 'epoch'
if train:
x = self.trainer.train_batch_idx / \
self.trainer.num_train_batches
n = self.trainer.num_train_batches / \
self.plot_train_per_epoch
else:
x = self.trainer.epoch + 1
n = self.trainer.num_val_batches / \
self.plot_valid_per_epoch
self.board.draw(x, d2l.numpy(d2l.to(value, d2l.cpu())),
('train_' if train else 'val_') + key,
every_n=int(n))
def training_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=True)
return l
def validation_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=False)
def configure_optimizers(self):
raise NotImplementedError
Puteți observa că ''%%Module%%'' este o subclasă a ''%%nn.Module%%'', clasa de bază a rețelelor neuronale în PyTorch. Aceasta oferă caracteristici convenabile pentru gestionarea rețelelor neuronale. De exemplu, dacă definim o metodă ''%%forward%%'', cum ar fi ''%%forward(self, X)%%'', atunci pentru o instanță ''%%a%%'' putem invoca această metodă prin ''%%a(X)%%''. Acest lucru funcționează deoarece apelează metoda ''%%forward%%'' în metoda încorporată ''%%__call__%%''. Puteți găsi mai multe detalii și exemple despre ''%%nn.Module%%'' în .
===== Date =====
Clasa ''%%DataModule%%'' este clasa de bază pentru date. Destul de frecvent, metoda ''%%__init__%%'' este utilizată pentru a pregăti datele. Aceasta include descărcarea și preprocesarea, dacă este necesar. ''%%train_dataloader%%'' returnează încărcătorul de date pentru setul de antrenament. Un încărcător de date este un generator (Python) care produce un lot de date de fiecare dată când este utilizat. Acest lot este apoi introdus în metoda ''%%training_step%%'' a clasei ''%%Module%%'' pentru a calcula pierderea. Există un ''%%val_dataloader%%'' opțional pentru a returna încărcătorul setului de date de validare. Acesta se comportă în același mod, cu excepția faptului că produce loturi de date pentru metoda ''%%validation_step%%'' din ''%%Module%%''.
class DataModule(d2l.HyperParameters): #@save
"""Clasa de bază a datelor."""
def get_dataloader(self, train):
raise NotImplementedError
def train_dataloader(self):
return self.get_dataloader(train=True)
def val_dataloader(self):
return self.get_dataloader(train=False)
===== Antrenare =====
Clasa ''%%Trainer%%'' antrenează parametrii învățabili din clasa ''%%Module%%'' cu datele specificate în ''%%DataModule%%''. Metoda cheie este ''%%fit%%'', care acceptă două argumente: ''%%model%%'', o instanță a ''%%Module%%'', și ''%%data%%'', o instanță a ''%%DataModule%%''. Aceasta iterează apoi peste întregul set de date de ''%%max_epochs%%'' ori pentru a antrena modelul. Ca și înainte, vom amâna implementarea acestei metode pentru capitolele ulterioare.
class Trainer(d2l.HyperParameters): #@save
"""Clasa de bază pentru antrenarea modelelor cu date."""
def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0):
self.save_hyperparameters()
assert num_gpus == 0, 'No GPU support yet'
def prepare_data(self, data):
self.train_dataloader = data.train_dataloader()
self.val_dataloader = data.val_dataloader()
self.num_train_batches = len(self.train_dataloader)
self.num_val_batches = (len(self.val_dataloader)
if self.val_dataloader is not None else 0)
def prepare_model(self, model):
model.trainer = self
model.board.xlim = [0, self.max_epochs]
self.model = model
def fit_epoch(self):
raise NotImplementedError
===== Rezumat =====
Pentru a evidenția designul orientat pe obiecte pentru viitoarea noastră implementare de deep learning, clasele de mai sus arată pur și simplu cum obiectele lor stochează date și interacționează între ele. Vom continua să îmbogățim implementările acestor clase, de exemplu prin intermediul ''%%@add_to_class%%'', în restul cărții. Mai mult, aceste clase complet implementate sunt salvate în [[https://github.com/d2l-ai/d2l-en/tree/master/d2l|biblioteca D2L]], un //set de instrumente ușor// care facilitează modelarea structurată pentru deep learning. În special, acesta facilitează reutilizarea multor componente între proiecte fără a schimba prea mult. De exemplu, putem înlocui doar optimizatorul, doar modelul, doar setul de date etc.; acest grad de modularitate aduce beneficii pe tot parcursul cărții în ceea ce privește concizia și simplitatea (acesta este motivul pentru care l-am adăugat) și poate face același lucru pentru propriile proiecte.
===== Exerciții =====
- Localizați implementările complete ale claselor de mai sus care sunt salvate în [[https://github.com/d2l-ai/d2l-en/tree/master/d2l|biblioteca D2L]]. Vă recomandăm cu tărie să examinați implementarea în detaliu odată ce ați dobândit mai multă familiaritate cu modelarea în deep learning.
- Eliminați instrucțiunea ''%%save_hyperparameters%%'' din clasa ''%%B%%''. Mai puteți afișa ''%%self.a%%'' și ''%%self.b%%''? Opțional: dacă ați studiat implementarea completă a clasei ''%%HyperParameters%%'', puteți explica de ce?
[[https://discuss.d2l.ai/t/6646|Discuții]]