User Tools

Site Tools


ro:course:linear-regression:linear-regression-scratch

Implementarea Regresiei Liniare de la Zero

Suntem acum gata să parcurgem o implementare complet funcțională a regresiei liniare. În această secțiune, (vom implementa întreaga metodă de la zero, inclusiv (i) modelul; (ii) funcția de pierdere; (iii) un optimizator de tip coborâre pe gradient stochastic de minilot; și (iv) funcția de antrenament care îmbină toate aceste piese.) În cele din urmă, vom rula generatorul nostru de date sintetice din și vom aplica modelul nostru pe setul de date rezultat. Deși framework-urile moderne de deep learning pot automatiza aproape toată această muncă, implementarea lucrurilor de la zero este singura cale de a vă asigura că știți cu adevărat ce faceți. Mai mult, atunci când va veni timpul să personalizați modelele, definind propriile straturi sau funcții de pierdere, înțelegerea modului în care funcționează lucrurile sub capotă se va dovedi utilă. În această secțiune, ne vom baza doar pe tensori și diferențiere automată. Mai târziu, vom introduce o implementare mai concisă, profitând de toate funcționalitățile framework-urilor de deep learning, păstrând în același timp structura celor ce urmează mai jos.

%matplotlib inline
from d2l import torch as d2l
import torch

Definirea Modelului

[Înainte de a putea începe să optimizăm parametrii modelului nostru] prin SGD de minilot, (avem nevoie, în primul rând, de niște parametri.) În cele ce urmează, inițializăm ponderile prin extragerea unor numere aleatorii dintr-o distribuție normală cu media 0 și o deviație standard de 0.01. Numărul magic 0.01 funcționează adesea bine în practică, dar puteți specifica o valoare diferită prin argumentul sigma. Mai mult, setăm biasul la 0. Rețineți că pentru designul orientat pe obiecte, adăugăm codul la metoda __init__ a unei subclase a d2l.Module (introdusă în ).

class LinearRegressionScratch(d2l.Module):  #@save
    """Modelul de regresie liniară implementat de la zero."""
    def __init__(self, num_inputs, lr, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.w = d2l.normal(0, sigma, (num_inputs, 1), requires_grad=True)
        self.b = d2l.zeros(1, requires_grad=True)

În continuare, trebuie să [ne definim modelul, corelând intrarea și parametrii săi cu ieșirea.] Folosind aceeași notație ca :eqref:eq_linreg-y-vec pentru modelul nostru liniar, pur și simplu luăm produsul matrice-vector al caracteristicilor de intrare $\mathbf{X}$ și al ponderilor modelului $\mathbf{w}$, și adăugăm deplasamentul $b$ la fiecare exemplu. Produsul $\mathbf{Xw}$ este un vector, iar $b$ este un scalar. Datorită mecanismului de broadcasting (vezi ), atunci când adunăm un vector și un scalar, scalarul este adăugat la fiecare componentă a vectorului. Metoda forward rezultată este înregistrată în clasa LinearRegressionScratch prin intermediul add_to_class (introdusă în ).

@d2l.add_to_class(LinearRegressionScratch)  #@save
def forward(self, X):
    return d2l.matmul(X, self.w) + self.b

Definirea Funcției de Pierdere

Deoarece [actualizarea modelului nostru necesită calcularea gradientului funcției de pierdere,] ar trebui să (definim mai întâi funcția de pierdere.) Aici folosim funcția de pierdere pătratică din :eqref:eq_mse. În implementare, trebuie să transformăm valoarea reală y în forma valorii prezise y_hat. Rezultatul returnat de următoarea metodă va avea, de asemenea, aceeași formă ca y_hat. De asemenea, returnăm valoarea medie a pierderii dintre toate exemplele din minilot.

@d2l.add_to_class(LinearRegressionScratch)  #@save
def loss(self, y_hat, y):
    l = (y_hat - y) ** 2 / 2
    return d2l.reduce_mean(l)

Definirea Algoritmului de Optimizare

După cum s-a discutat în , regresia liniară are o soluție sub formă închisă. Cu toate acestea, scopul nostru aici este de a ilustra cum se antrenează rețele neuronale mai generale, iar acest lucru necesită să vă învățăm cum să folosiți SGD de minilot. Prin urmare, vom profita de această ocazie pentru a introduce primul vostru exemplu funcțional de SGD. La fiecare pas, folosind un minilot extras aleatoriu din setul nostru de date, estimăm gradientul pierderii în raport cu parametrii. Apoi, actualizăm parametrii în direcția care ar putea reduce pierderea.

Următorul cod aplică actualizarea, având în vedere un set de parametri și o rată de învățare lr. Deoarece pierderea noastră este calculată ca o medie pe minilot, nu trebuie să ajustăm rata de învățare în funcție de dimensiunea lotului. În capitolele ulterioare vom investiga modul în care ar trebui ajustate ratele de învățare pentru miniloturi foarte mari, așa cum apar în învățarea distribuită la scară largă. Deocamdată, putem ignora această dependență.

Definim clasa noastră SGD, o subclasă a d2l.HyperParameters (introdusă în ), pentru a avea un API similar cu cel al optimizatorului SGD încorporat. Actualizăm parametrii în metoda step. Metoda zero_grad setează toți gradienții la 0, ceea ce trebuie rulat înainte de un pas de backpropagation.

class SGD(d2l.HyperParameters):  #@save
    """Coborârea pe gradient stochastic de minilot."""
    def __init__(self, params, lr):
        self.save_hyperparameters()
 
    def step(self):
        for param in self.params:
            param -= self.lr * param.grad
 
    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

Definim în continuare metoda configure_optimizers, care returnează o instanță a clasei SGD.

@d2l.add_to_class(LinearRegressionScratch)  #@save
def configure_optimizers(self):
    return SGD([self.w, self.b], self.lr)

Antrenarea

Acum că avem toate piesele la locul lor (parametrii, funcția de pierdere, modelul și optimizatorul), suntem gata să [implementăm bucla principală de antrenament.] Este crucial să înțelegeți pe deplin acest cod, deoarece veți folosi bucle de antrenament similare pentru fiecare alt model de deep learning abordat în această carte. În fiecare epocă (epoch), iterăm prin întregul set de date de antrenament, trecând o singură dată prin fiecare exemplu (presupunând că numărul de exemple este divizibil cu dimensiunea lotului). În fiecare iterație, preluăm un minilot de exemple de antrenament și îi calculăm pierderea prin metoda training_step a modelului. Apoi calculăm gradienții în raport cu fiecare parametru. În cele din urmă, vom apela algoritmul de optimizare pentru a actualiza parametrii modelului. În rezumat, vom executa următoarea buclă:

  • Inițializăm parametrii $(\mathbf{w}, b)$
  • Repetăm până când terminăm
    • Calculăm gradientul $\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)$
    • Actualizăm parametrii $(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$

Reamintiți-vă că setul de date de regresie sintetică pe care l-am generat în nu oferă un set de date de validare. În cele mai multe cazuri, însă, vom dori un set de date de validare pentru a măsura calitatea modelului nostru. Aici trecem încărcătorul de date de validare o dată în fiecare epocă pentru a măsura performanța modelului. Urmând designul nostru orientat pe obiecte, metodele prepare_batch și fit_epoch sunt înregistrate în clasa d2l.Trainer (introdusă în ).

@d2l.add_to_class(d2l.Trainer)  #@save
def prepare_batch(self, batch):
    return batch
@d2l.add_to_class(d2l.Trainer)  #@save
def fit_epoch(self):
    self.model.train()        
    for batch in self.train_dataloader:        
        loss = self.model.training_step(self.prepare_batch(batch))
        self.optim.zero_grad()
        with torch.no_grad():
            loss.backward()
            if self.gradient_clip_val > 0:  # To be discussed later
                self.clip_gradients(self.gradient_clip_val, self.model)
            self.optim.step()
        self.train_batch_idx += 1
    if self.val_dataloader is None:
        return
    self.model.eval()
    for batch in self.val_dataloader:
        with torch.no_grad():            
            self.model.validation_step(self.prepare_batch(batch))
        self.val_batch_idx += 1

Suntem aproape gata să antrenăm modelul, dar mai întâi avem nevoie de niște date de antrenament. Aici folosim clasa SyntheticRegressionData și introducem niște parametri dintr-un model real. Apoi ne antrenăm modelul cu rata de învățare lr=0.03 și setăm max_epochs=3. Rețineți că, în general, atât numărul de epoci, cât și rata de învățare sunt hiperparametri. În general, setarea hiperparametrilor este delicată și, de obicei, vom dori să folosim o împărțire în trei căi: un set pentru antrenament, un al doilea pentru selecția hiperparametrilor și al treilea rezervat pentru evaluarea finală. Omitem aceste detalii deocamdată, dar le vom revizui mai târziu.

model = LinearRegressionScratch(2, lr=0.03)
data = d2l.SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2)
trainer = d2l.Trainer(max_epochs=3)
trainer.fit(model, data)

Deoarece am sintetizat singuri setul de date, știm exact care sunt parametrii reali. Astfel, ne putem [evalua succesul în antrenament comparând parametrii reali cu cei pe care i-am învățat] prin bucla noastră de antrenament. Într-adevăr, aceștia se dovedesc a fi foarte apropiați unii de alții.

with torch.no_grad():
    print(f'error in estimating w: {data.w - d2l.reshape(model.w, data.w.shape)}')
    print(f'error in estimating b: {data.b - model.b}')

Nu ar trebui să considerăm capacitatea de a recupera exact parametrii de bază ca fiind de la sine înțeleasă. În general, pentru modelele profunde nu există soluții unice pentru parametri și, chiar și pentru modelele liniare, recuperarea exactă a parametrilor este posibilă numai atunci când nicio caracteristică nu este dependentă liniar de celelalte. Cu toate acestea, în machine learning suntem adesea mai puțin preocupați de recuperarea parametrilor reali subiacenți, cât mai degrabă de parametrii care conduc la o predicție extrem de precisă 1). Din fericire, chiar și în cazul unor probleme de optimizare dificile, coborârea pe gradient stochastic poate găsi adesea soluții remarcabil de bune, datorită parțial faptului că, pentru rețelele profunde, există multe configurații ale parametrilor care conduc la o predicție extrem de precisă.

Rezumat

În această secțiune, am făcut un pas semnificativ către proiectarea sistemelor de deep learning prin implementarea unui model de rețea neuronală complet funcțional și a unei bucle de antrenament. În acest proces, am construit un încărcător de date, un model, o funcție de pierdere, o procedură de optimizare și un instrument de vizualizare și monitorizare. Am făcut acest lucru prin compunerea unui obiect Python care conține toate componentele relevante pentru antrenarea unui model. Deși aceasta nu este încă o implementare de nivel profesional, este perfect funcțională, iar un cod ca acesta v-ar putea ajuta deja să rezolvați rapid probleme mici. În secțiunile următoare, vom vedea cum să facem acest lucru atât mai concis (evitând codul repetitiv), cât și mai eficient (folosind GPU-urile noastre la potențialul lor maxim).

Exerciții

  1. Ce s-ar întâmpla dacă am inițializa ponderile cu zero? Ar mai funcționa algoritmul? Ce s-ar întâmpla dacă am inițializa parametrii cu varianța $1000$ în loc de $0.01$?
  2. Presupunem că sunteți Georg Simon Ohm și încercați să găsiți un model pentru rezistență care să coreleze tensiunea și curentul. Puteți folosi diferențierea automată pentru a învăța parametrii modelului dumneavoastră?
  3. Puteți folosi Legea lui Planck pentru a determina temperatura unui obiect folosind densitatea energiei spectrale? Pentru referință, densitatea spectrală $B$ a radiației care emană de la un corp negru este $B(\lambda, T) = \frac{2 hc^2}{\lambda^5} \cdot \left(\exp \frac{h c}{\lambda k T} - 1\right)^{-1}$. Aici $\lambda$ este lungimea de undă, $T$ este temperatura, $c$ este viteza luminii, $h$ este constanta lui Planck și $k$ este constanta lui Boltzmann. Măsurați energia pentru diferite lungimi de undă $\lambda$ și acum trebuie să potriviți curba densității spectrale pe legea lui Planck.
  4. Care sunt problemele pe care le-ați putea întâlni dacă doriți să calculați derivatele de ordinul doi ale pierderii? Cum le-ați remedia?
  5. De ce este necesară metoda reshape în funcția loss?
  6. Experimentați folosind diferite rate de învățare pentru a afla cât de repede scade valoarea funcției de pierdere. Puteți reduce eroarea prin creșterea numărului de epoci de antrenament?
  7. Dacă numărul de exemple nu poate fi împărțit exact la dimensiunea lotului, ce se întâmplă cu data_iter la sfârșitul unei epoci?
  8. Încercați să implementați o altă funcție de pierdere, cum ar fi pierderea valorii absolute (y_hat - d2l.reshape(y, y_hat.shape)).abs().sum().
    1. Verificați ce se întâmplă pentru datele obișnuite.
    2. Verificați dacă există o diferență de comportament dacă perturbați activ unele intrări ale lui $\mathbf{y}$, cum ar fi $y_5 = 10000$.
    3. Vă puteți gândi la o soluție ieftină pentru combinarea celor mai bune aspecte ale pierderii pătratice și ale pierderii valorii absolute? Sugestie: cum puteți evita valorile gradientului foarte mari?
  9. De ce trebuie să re-amestecăm setul de date? Puteți proiecta un caz în care un set de date construit în mod malițios ar bloca algoritmul de optimizare în caz contrar?

Discuții

1)
Vapnik.1992
ro/course/linear-regression/linear-regression-scratch.txt · Last modified: by 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki