5.6 Dropout

Trumpai pamąstykime, ko tikimės iš gero prognozavimo modelio. Norime, kad jis gerai veiktų su nematytais duomenimis. Klasikinė apibendrinimo teorija teigia, kad norint sumažinti atotrūkį tarp apmokymo ir testavimo našumo, turėtume siekti paprasto modelio. Paprastumas gali reikštis nedideliu dimensijų skaičiumi. Tai nagrinėjome aptardami tiesinių modelių monominius bazines funkcijas 3.6 skyrelis. Be to, kaip matėme aptardami svorių nykimą (ℓ2\ell_2 reguliarizavimą) 3.7 skyrelis, parametrų (atvirkštinė) norma taip pat yra naudingas paprastumo matas. Kita naudinga paprastumo samprata yra glotnumas, t. y. kad funkcija neturėtų būti jautri nedideliems jos įvesčių pokyčiams. Pavyzdžiui, klasifikuodami vaizdus, tikėtumės, kad šiek tiek atsitiktinio triukšmo pridėjimas prie pikselių daugiausia turėtų būti nekenksmingas.

(Bishop, 1995) suformalizavo šią idėją, įrodydamas, kad apmokymas su įvesties triukšmu yra ekvivalentus Tichonovo reguliarizavimui. Šis darbas nubrėžė aiškų matematinį ryšį tarp reikalavimo, kad funkcija būtų glotni (taigi ir paprasta), ir reikalavimo, kad ji būtų atspari įvesties trikdžiams.

Vėliau (Srivastava ir kt., 2014) sukūrė gudrią idėją, kaip taikyti Bishopo idėją ir vidiniams tinklo sluoksniams. Jų idėja, vadinama dropout, apima triukšmo injektavimą skaičiuojant kiekvieną vidinį sluoksnį tiesioginio sklidimo metu, ir ji tapo standartine neuroninių tinklų apmokymo technika. Metodas vadinamas dropout (angl. „iškritimas“), nes apmokymo metu mes pažodžiui išmetame kai kuriuos neuronus. Viso apmokymo metu, kiekvienoje iteracijoje, standartinis dropout susideda iš tam tikros kiekvieno sluoksnio mazgų dalies nuliavimo prieš skaičiuojant kitą sluoksnį.

Kad būtų aišku, ryšiu su Bishopu mes primetame savo pasakojimą. Originalus straipsnis apie dropout pateikia intuiciją per stebinančią analogiją su lytiniu dauginimusi. Autoriai teigia, kad neuroninio tinklo persimokymą apibūdina būsena, kai kiekvienas sluoksnis remiasi tam tikru ankstesnio sluoksnio aktyvacijų dėsningumu, šią būseną pavadindami bendra adaptacija. Jų teigimu, dropout sutraukia bendrą adaptaciją taip, kaip, manoma, lytinis dauginimasis sutraukia kartu prisitaikiusius genus. Nors tokio šios teorijos pagrindimo tikrai galima diskutuoti, pati dropout technika pasirodė esanti ilgaamžė, ir įvairios dropout formos yra įgyvendintos daugumoje giliojo mokymosi bibliotekų.

Pagrindinis iššūkis yra, kaip injektuoti šį triukšmą. Viena idėja yra injektuoti jį nešališku būdu, kad kiekvieno sluoksnio laukiamoji reikšmė – fiksavus kitus – lygiavertų reikšmei, kurią jis būtų įgijęs be triukšmo. Bishopo darbe jis pridėjo Gauso triukšmą prie tiesinio modelio įvesčių. Kiekvienoje apmokymo iteracijoje jis pridėdavo triukšmą, paimtą iš skirstinio su nuliniu vidurkiu ϵ∼N(0,σ2)\epsilon \sim \mathcal{N}(0,\sigma^2), prie įvesties x\mathbf{x}, gaudamas sutrikdytą tašką x′=x+ϵ\mathbf{x}' = \mathbf{x} + \epsilon. Laukiamoji reikšmė E[x′]=xE[\mathbf{x}'] = \mathbf{x}.

Standartiniame dropout reguliarizavime nuliuojama tam tikra kiekvieno sluoksnio mazgų dalis, o tada kiekvienas sluoksnis pašalinamas iš šališkumo, normalizuojant pagal išlaikytų (neišmestų) mazgų dalį. Kitaip tariant, su dropout tikimybe pp kiekviena tarpinė aktyvacija hh pakeičiama atsitiktiniu dydžiu h′h' taip:

h′={0 with probability ph1−p otherwise\begin{aligned} h' = \begin{cases} 0 & \textrm{ with probability } p \\ \frac{h}{1-p} & \textrm{ otherwise} \end{cases} \end{aligned}

Pagal konstrukciją laukiamoji reikšmė lieka nepakitusi, t. y. E[h′]=hE[h'] = h.

from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
from d2l import torch as d2l
import torch
from torch import nn
from d2l import tensorflow as d2l
import tensorflow as tf
from d2l import jax as d2l
from flax import linen as nn
from functools import partial
import jax
from jax import numpy as jnp
import optax

5.6.1 Dropout praktikoje

Prisiminkite MLP su paslėptuoju sluoksniu ir penkiais paslėptaisiais vienetais iš 5.1.1 pav.. Kai taikome dropout paslėptajam sluoksniui, nuliuodami kiekvieną paslėptąjį vienetą su tikimybe pp, rezultatą galima vertinti kaip tinklą, kuriame yra tik pradinių neuronų poaibis. 5.6.1 pav. pašalinti h2h_2 ir h5h_5. Todėl išvesčių skaičiavimas nebepriklauso nuo h2h_2 ar h5h_5, ir jų atitinkamas gradientas taip pat išnyksta atliekant atgalinį sklidimą. Tokiu būdu išvesties sluoksnio skaičiavimas negali pernelyg priklausyti nuo nė vieno iš h1,…,h5h_1, \ldots, h_5 elementų.

MLP prieš dropout ir po jo.

5.6.1 pav. MLP prieš dropout ir po jo.

Paprastai testavimo metu dropout išjungiame. Turint apmokytą modelį ir naują pavyzdį, neišmetame jokių mazgų, todėl normalizuoti nereikia. Tačiau yra keletas išimčių: kai kurie tyrėjai naudoja dropout testavimo metu kaip euristiką neuroninio tinklo prognozių netikrumui įvertinti: jei prognozės sutampa per daugelį skirtingų dropout išvesčių, galime sakyti, kad tinklas yra labiau pasitikintis.

5.6.2 Įgyvendinimas nuo nulio

Kad įgyvendintume dropout funkciją vienam sluoksniui, turime paimti tiek imčių iš Bernulio (dvejetainio) atsitiktinio dydžio, kiek mūsų sluoksnis turi dimensijų, kur atsitiktinis dydis įgyja reikšmę 11 (palikti) su tikimybe 1−p1-p ir 00 (išmesti) su tikimybe pp. Vienas lengvas būdas tai įgyvendinti yra pirmiausia paimti imtis iš tolygiojo skirstinio U[0,1]U[0, 1]. Tada galime palikti tuos mazgus, kurių atitinkama imtis yra didesnė nei pp, o likusius išmesti.

Šiame kode (įgyvendiname funkciją dropout_layer, kuri išmeta tenzoriaus įvesties X elementus su tikimybe dropout), likusius perskalindami, kaip aprašyta aukščiau: išlikusius dalydami iš 1.0-dropout.

def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    if dropout == 1: return np.zeros_like(X)
    mask = np.random.uniform(0, 1, X.shape) > dropout
    return mask.astype(np.float32) * X / (1.0 - dropout)
def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    if dropout == 1: return torch.zeros_like(X)
    mask = (torch.rand(X.shape) > dropout).float()
    return mask * X / (1.0 - dropout)
def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    if dropout == 1: return tf.zeros_like(X)
    mask = tf.random.uniform(
        shape=tf.shape(X), minval=0, maxval=1) < 1 - dropout
    return tf.cast(mask, dtype=tf.float32) * X / (1.0 - dropout)
def dropout_layer(X, dropout, key=d2l.get_key()):
    assert 0 <= dropout <= 1
    if dropout == 1: return jnp.zeros_like(X)
    mask = jax.random.uniform(key, X.shape) > dropout
    return jnp.asarray(mask, dtype=jnp.float32) * X / (1.0 - dropout)

Galime išbandyti funkciją dropout_layer keliais pavyzdžiais. Šiose kodo eilutėse perduodame savo įvestį X per dropout operaciją su tikimybėmis atitinkamai 0, 0,5 ir 1.

if tab.selected('mxnet'):
    X = np.arange(16).reshape(2, 8)
if tab.selected('pytorch'):
    X = torch.arange(16, dtype = torch.float32).reshape((2, 8))
if tab.selected('tensorflow'):
    X = tf.reshape(tf.range(16, dtype=tf.float32), (2, 8))
if tab.selected('jax'):
    X = jnp.arange(16, dtype=jnp.float32).reshape(2, 8)
print('dropout_p = 0:', dropout_layer(X, 0))
print('dropout_p = 0.5:', dropout_layer(X, 0.5))
print('dropout_p = 1:', dropout_layer(X, 1))

5.6.2.1 Modelio apibrėžimas

Žemiau pateiktas modelis taiko dropout kiekvieno paslėptojo sluoksnio išvesčiai (po aktyvacijos funkcijos). Kiekvienam sluoksniui dropout tikimybes galime nustatyti atskirai. Įprastas pasirinkimas yra nustatyti mažesnę dropout tikimybę arčiau įvesties sluoksnio. Užtikriname, kad dropout būtų aktyvus tik apmokymo metu.

class DropoutMLPScratch(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.lin1 = nn.Dense(num_hiddens_1, activation='relu')
        self.lin2 = nn.Dense(num_hiddens_2, activation='relu')
        self.lin3 = nn.Dense(num_outputs)
        self.initialize()

    def forward(self, X):
        H1 = self.lin1(X)
        if autograd.is_training():
            H1 = dropout_layer(H1, self.dropout_1)
        H2 = self.lin2(H1)
        if autograd.is_training():
            H2 = dropout_layer(H2, self.dropout_2)
        return self.lin3(H2)
class DropoutMLPScratch(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.lin1 = nn.LazyLinear(num_hiddens_1)
        self.lin2 = nn.LazyLinear(num_hiddens_2)
        self.lin3 = nn.LazyLinear(num_outputs)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((X.shape[0], -1))))
        if self.training:  
            H1 = dropout_layer(H1, self.dropout_1)
        H2 = self.relu(self.lin2(H1))
        if self.training:
            H2 = dropout_layer(H2, self.dropout_2)
        return self.lin3(H2)
class DropoutMLPScratch(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.lin1 = tf.keras.layers.Dense(num_hiddens_1, activation='relu')
        self.lin2 = tf.keras.layers.Dense(num_hiddens_2, activation='relu')
        self.lin3 = tf.keras.layers.Dense(num_outputs)

    def forward(self, X):
        H1 = self.lin1(tf.reshape(X, (X.shape[0], -1)))
        if self.training:
            H1 = dropout_layer(H1, self.dropout_1)
        H2 = self.lin2(H1)
        if self.training:
            H2 = dropout_layer(H2, self.dropout_2)
        return self.lin3(H2)
class DropoutMLPScratch(d2l.Classifier):
    num_hiddens_1: int
    num_hiddens_2: int
    num_outputs: int
    dropout_1: float
    dropout_2: float
    lr: float
    training: bool = True

    def setup(self):
        self.lin1 = nn.Dense(self.num_hiddens_1)
        self.lin2 = nn.Dense(self.num_hiddens_2)
        self.lin3 = nn.Dense(self.num_outputs)
        self.relu = nn.relu

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape(X.shape[0], -1)))
        if self.training:
            H1 = dropout_layer(H1, self.dropout_1)
        H2 = self.relu(self.lin2(H1))
        if self.training:
            H2 = dropout_layer(H2, self.dropout_2)
        return self.lin3(H2)

5.6.2.2 Apmokymas

Toliau pateikta panašu į anksčiau aprašytą MLP apmokymą.

hparams = {'num_outputs':10, 'num_hiddens_1':256, 'num_hiddens_2':256,
           'dropout_1':0.5, 'dropout_2':0.5, 'lr':0.1}
model = DropoutMLPScratch(**hparams)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

5.6.3 Glaustas įgyvendinimas

Su aukšto lygio API viskas, ko mums reikia, yra pridėti Dropout sluoksnį po kiekvieno pilnai sujungto sluoksnio, perduodant dropout tikimybę kaip vienintelį jo konstruktoriaus argumentą. Apmokymo metu Dropout sluoksnis atsitiktinai išmes ankstesnio sluoksnio išvestis (arba, lygiavertiškai, kito sluoksnio įvestis) pagal nurodytą dropout tikimybę. Kai ne apmokymo režimu, Dropout sluoksnis testavimo metu tiesiog praleidžia duomenis.

class DropoutMLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential()
        self.net.add(nn.Dense(num_hiddens_1, activation="relu"),
                     nn.Dropout(dropout_1),
                     nn.Dense(num_hiddens_2, activation="relu"),
                     nn.Dropout(dropout_2),
                     nn.Dense(num_outputs))
        self.net.initialize()
class DropoutMLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.Flatten(), nn.LazyLinear(num_hiddens_1), nn.ReLU(), 
            nn.Dropout(dropout_1), nn.LazyLinear(num_hiddens_2), nn.ReLU(), 
            nn.Dropout(dropout_2), nn.LazyLinear(num_outputs))
class DropoutMLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = tf.keras.models.Sequential([
            tf.keras.layers.Flatten(),
            tf.keras.layers.Dense(num_hiddens_1, activation=tf.nn.relu),
            tf.keras.layers.Dropout(dropout_1),
            tf.keras.layers.Dense(num_hiddens_2, activation=tf.nn.relu),
            tf.keras.layers.Dropout(dropout_2),
            tf.keras.layers.Dense(num_outputs)])
class DropoutMLP(d2l.Classifier):
    num_hiddens_1: int
    num_hiddens_2: int
    num_outputs: int
    dropout_1: float
    dropout_2: float
    lr: float
    training: bool = True

    @nn.compact
    def __call__(self, X):
        x = nn.relu(nn.Dense(self.num_hiddens_1)(X.reshape((X.shape[0], -1))))
        x = nn.Dropout(self.dropout_1, deterministic=not self.training)(x)
        x = nn.relu(nn.Dense(self.num_hiddens_2)(x))
        x = nn.Dropout(self.dropout_2, deterministic=not self.training)(x)
        return nn.Dense(self.num_outputs)(x)

Atkreipkite dėmesį, kad turime iš naujo apibrėžti nuostolių funkciją, nes tinklui su dropout sluoksniu, naudojant Module.apply(), reikia PRNGKey, o šis ATS sėklos raktas turi būti aiškiai pavadintas dropout. Šį raktą dropout sluoksnis Flax naudoja atsitiktinei dropout kaukei viduje generuoti. Svarbu kiekvienai apmokymo ciklo epochai naudoti unikalų dropout_rng raktą, kitaip sugeneruota dropout kaukė nebus stochastinė ir skirtingų epochų paleidimuose bus ta pati. Šį dropout_rng galima saugoti TrainState objekte (klasėje d2l.Trainer, apibrėžtoje 3.2.4 skyrelis) kaip atributą, o kiekvienoje epochoje jis pakeičiamas nauju dropout_rng. Tai jau sutvarkėme metodu fit_epoch, apibrėžtu 3.4 skyrelis.

@d2l.add_to_class(d2l.Classifier)  #@save
@partial(jax.jit, static_argnums=(0, 5))
def loss(self, params, X, Y, state, averaged=True):
    Y_hat = state.apply_fn({'params': params}, *X,
                           mutable=False,  # To be used later (e.g., batch norm)
                           rngs={'dropout': state.dropout_rng})
    Y_hat = d2l.reshape(Y_hat, (-1, Y_hat.shape[-1]))
    Y = d2l.reshape(Y, (-1,))
    fn = optax.softmax_cross_entropy_with_integer_labels
    # The returned empty dictionary is a placeholder for auxiliary data,
    # which will be used later (e.g., for batch norm)
    return (fn(Y_hat, Y).mean(), {}) if averaged else (fn(Y_hat, Y), {})

Toliau apmokome modelį.

model = DropoutMLP(**hparams)
trainer.fit(model, data)

5.6.4 Santrauka

Be dimensijų skaičiaus ir svorių vektoriaus dydžio kontroliavimo, dropout yra dar vienas įrankis persimokymui išvengti. Dažnai įrankiai naudojami kartu. Atkreipkite dėmesį, kad dropout naudojamas tik apmokymo metu: jis pakeičia aktyvaciją hh atsitiktiniu dydžiu, kurio laukiamoji reikšmė yra hh.

5.6.5 Pratimai

  1. Kas nutinka, jei pakeičiate pirmojo ir antrojo sluoksnių dropout tikimybes? Visų pirma, kas nutinka, jei sukeičiate abiejų sluoksnių tikimybes? Suplanuokite eksperimentą šiems klausimams atsakyti, aprašykite savo rezultatus kiekybiškai ir apibendrinkite kokybines išvadas.
  2. Padidinkite epochų skaičių ir palyginkite rezultatus, gautus naudojant dropout, su gautais jo nenaudojant.
  3. Kokia yra aktyvacijų dispersija kiekviename paslėptajame sluoksnyje, kai dropout taikomas ir netaikomas? Nubraižykite grafiką, rodantį, kaip šis dydis kinta laikui bėgant abiem modeliams.
  4. Kodėl dropout paprastai nenaudojamas testavimo metu?
  5. Naudodami šiame skyrelyje pateiktą modelį kaip pavyzdį, palyginkite dropout ir svorių nykimo poveikį. Kas nutinka, kai dropout ir svorių nykimas naudojami vienu metu? Ar rezultatai adityvūs? Ar yra mažėjanti grąža (ar dar blogiau)? Ar jie vienas kitą panaikina?
  6. Kas nutinka, jei dropout taikome atskiriems svorių matricos svoriams, o ne aktyvacijoms?
  7. Išrasti kitą atsitiktinio triukšmo injektavimo kiekviename sluoksnyje techniką, besiskiriančią nuo standartinės dropout technikos. Ar galite sukurti metodą, kuris pralenktų dropout su Fashion-MNIST duomenų rinkiniu (fiksuotai architektūrai)?

Diskusijos

Diskusijos

Diskusijos

Diskusijos