12.3 Gradientinis nusileidimas
Šiame skyrelyje pristatysime pagrindines gradientinio nusileidimo sąvokas. Nors giliajame mokymesi jis naudojamas retai tiesiogiai, gradientinio nusileidimo supratimas yra raktas į stochastinio gradientinio nusileidimo algoritmų supratimą. Pavyzdžiui, optimizavimo uždavinys gali diverguoti dėl pernelyg didelio mokymosi greičio. Šį reiškinį galima pastebėti jau gradientiniame nusileidime. Panašiai pradinių sąlygų tikrinimas yra įprastas gradientinio nusileidimo metodas ir perkeliamas į pažangesnius algoritmus. Pradėkime nuo paprasto ypatingo atvejo.
12.3.1 Vienmatis gradientinis nusileidimas
Gradientinis nusileidimas vienoje dimensijoje yra puikus pavyzdys paaiškinti, kodėl gradientinio nusileidimo algoritmas gali sumažinti tikslo funkcijos reikšmę. Panagrinėkime tam tikrą tolydžiai diferencijuojamą realiųjų reikšmių funkciją . Naudodami Teiloro skleidinį gauname
Tai yra, pirmos eilės aproksimacijoje nusako funkcijos reikšmė ir pirmoji išvestinė taške . Nėra nepagrįsta daryti prielaidą, kad mažiems judėjimas neigiamo gradiento kryptimi sumažins . Kad viskas būtų paprasta, pasirenkame fiksuotą žingsnio dydį ir parenkame . Įstatę tai į aukščiau pateiktą Teiloro skleidinį, gauname
Jei išvestinė neišnyksta, darome pažangą, nes . Be to, visada galime pasirinkti pakankamai mažą , kad aukštesnės eilės nariai taptų nesvarbūs. Taigi gauname
Tai reiškia, kad jei naudojame
iteracijoms, funkcijos reikšmė gali mažėti. Todėl gradientiniame nusileidime pirmiausia pasirenkame pradinę reikšmę ir konstantą , o paskui jas naudojame nuolat iteruodami , kol pasiekiama sustojimo sąlyga, pavyzdžiui, kai gradiento dydis yra pakankamai mažas arba iteracijų skaičius pasiekė tam tikrą reikšmę.
Paprastumo dėlei pasirenkame tikslo funkciją , kad iliustruotume, kaip įgyvendinti gradientinį nusileidimą. Nors žinome, kad yra minimizavimo sprendinys, vis tiek naudojame šią paprastą funkciją stebėti, kaip kinta .
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import np, npx
npx.set_np()
%matplotlib inline
from d2l import torch as d2l
import numpy as np
import torch
%matplotlib inline
from d2l import tensorflow as d2l
import numpy as np
import tensorflow as tf
def f(x): # Objective function
return x ** 2
def f_grad(x): # Gradient (derivative) of the objective function
return 2 * x
Toliau kaip pradinę reikšmę naudojame ir tarkime, kad . Naudodami gradientinį nusileidimą iteruoti 10 kartų, matome, kad galiausiai reikšmė artėja prie optimalaus sprendinio.
def gd(eta, f_grad):
x = 10.0
results = [x]
for i in range(10):
x -= eta * f_grad(x)
results.append(float(x))
print(f'epoch 10, x: {x:f}')
return results
results = gd(0.2, f_grad)
Optimizavimo per eigą galima nubraižyti taip.
def show_trace(results, f):
n = max(abs(min(results)), abs(max(results)))
f_line = d2l.arange(-n, n, 0.01)
d2l.set_figsize()
d2l.plot([f_line, results], [[f(x) for x in f_line], [
f(x) for x in results]], 'x', 'f(x)', fmts=['-', '-o'])
show_trace(results, f)
12.3.1.1 Mokymosi greitis
Mokymosi greitį gali nustatyti algoritmo kūrėjas. Jei naudojame pernelyg mažą mokymosi greitį, atnaujinamas labai lėtai, todėl reikia daugiau iteracijų geresniam sprendiniui gauti. Kad parodytume, kas nutinka tokiu atveju, panagrinėkime pažangą tame pačiame optimizavimo uždavinyje, kai . Kaip matome, net po 10 žingsnių vis dar esame labai toli nuo optimalaus sprendinio.
show_trace(gd(0.05, f_grad), f)
Ir atvirkščiai, jei naudojame pernelyg didelį mokymosi greitį, gali būti per didelis pirmos eilės Teiloro skleidinio formulei. Tai yra, narys (12.3.2) gali tapti reikšmingas. Tokiu atveju negalime garantuoti, kad iteravimas sumažins reikšmę. Pavyzdžiui, kai mokymosi greitį nustatome , peršoka optimalų sprendinį ir palaipsniui diverguoja.
show_trace(gd(1.1, f_grad), f)
12.3.1.2 Lokalieji minimumai
Kad iliustruotume, kas nutinka neiškiliosioms funkcijoms, panagrinėkime atvejį su tam tikra konstanta . Ši funkcija turi be galo daug lokaliųjų minimumų. Priklausomai nuo mokymosi greičio pasirinkimo ir nuo to, kiek gerai sąlygotas uždavinys, galime baigti viename iš daugelio sprendinių. Toliau pateiktas pavyzdys iliustruoja, kaip (nerealiai) didelis mokymosi greitis lems prastą lokalųjį minimumą.
c = d2l.tensor(0.15 * np.pi)
def f(x): # Objective function
return x * d2l.cos(c * x)
def f_grad(x): # Gradient of the objective function
return d2l.cos(c * x) - c * x * d2l.sin(c * x)
show_trace(gd(2, f_grad), f)
12.3.2 Daugiamatis gradientinis nusileidimas
Dabar, kai geriau intuityviai suprantame vienmatį atvejį, panagrinėkime situaciją, kai . Tai yra, tikslo funkcija atvaizduoja vektorius į skaliarus. Atitinkamai jos gradientas taip pat yra daugiamatis. Tai vektorius, sudarytas iš dalinių išvestinių:
Kiekvienas gradiento dalinės išvestinės elementas nurodo kitimo spartą taške įvesties atžvilgiu. Kaip ir anksčiau vienmačiu atveju, galime naudoti atitinkamą daugelio kintamųjų funkcijų Teiloro aproksimaciją, kad gautume supratimą, ką turėtume daryti. Visų pirma turime, kad
Kitaip tariant, iki antros eilės narių atžvilgiu staigiausio nusileidimo kryptį nusako neigiamas gradientas . Pasirinkus tinkamą mokymosi greitį , gaunamas prototipinis gradientinio nusileidimo algoritmas:
Kad pamatytume, kaip algoritmas elgiasi praktikoje, sukonstruokime tikslo funkciją , kurios įvestis yra dvimatis vektorius , o išvestis – skaliaras. Gradientas yra . Stebėsime trajektoriją gradientinio nusileidimo metu, pradedant nuo pradinės padėties .
Pirmiausia mums reikia dar dviejų pagalbinių funkcijų. Pirmoji naudoja atnaujinimo funkciją ir taiko ją 20 kartų pradinei reikšmei. Antroji pagalbinė funkcija vizualizuoja trajektoriją.
def train_2d(trainer, steps=20, f_grad=None): #@save
"""Optimize a 2D objective function with a customized trainer."""
# `s1` and `s2` are internal state variables that will be used in Momentum, adagrad, RMSProp
x1, x2, s1, s2 = -5, -2, 0, 0
results = [(x1, x2)]
for i in range(steps):
if f_grad:
x1, x2, s1, s2 = trainer(x1, x2, s1, s2, f_grad)
else:
x1, x2, s1, s2 = trainer(x1, x2, s1, s2)
results.append((x1, x2))
print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}')
return results
def show_trace_2d(f, results): #@save
"""Show the trace of 2D variables during optimization."""
d2l.set_figsize()
d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e')
x1, x2 = d2l.meshgrid(d2l.arange(-55, 1, 1),
d2l.arange(-30, 1, 1))
x1, x2 = x1.asnumpy()*0.1, x2.asnumpy()*0.1
d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
def show_trace_2d(f, results): #@save
"""Show the trace of 2D variables during optimization."""
d2l.set_figsize()
d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e')
x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1),
d2l.arange(-3.0, 1.0, 0.1))
d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
def show_trace_2d(f, results): #@save
"""Show the trace of 2D variables during optimization."""
d2l.set_figsize()
d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e')
x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1),
d2l.arange(-3.0, 1.0, 0.1), indexing='ij')
d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
Toliau stebime optimizavimo kintamojo trajektoriją, kai mokymosi greitis . Matome, kad po 20 žingsnių reikšmė artėja prie savo minimumo . Pažanga gana tvarkinga, nors ir gana lėta.
def f_2d(x1, x2): # Objective function
return x1 ** 2 + 2 * x2 ** 2
def f_2d_grad(x1, x2): # Gradient of the objective function
return (2 * x1, 4 * x2)
def gd_2d(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
return (x1 - eta * g1, x2 - eta * g2, 0, 0)
eta = 0.1
show_trace_2d(f_2d, train_2d(gd_2d, f_grad=f_2d_grad))
12.3.3 Adaptyvieji metodai
Kaip matėme 12.3.1.1 skyrelis, mokymosi greitį pasirinkti „tiksliai tinkamą“ yra sudėtinga. Jei pasirenkame per mažą, darome mažai pažangos. Jei pasirenkame per didelį, sprendinys svyruoja, o blogiausiu atveju gali net diverguoti. Ką, jei galėtume nustatyti automatiškai arba išvis atsikratytume poreikio rinktis mokymosi greitį? Antros eilės metodai, kurie žiūri ne tik į tikslo funkcijos reikšmę ir gradientą, bet ir į jos kreivumą, gali padėti šiuo atveju. Nors šių metodų negalima tiesiogiai taikyti giliajam mokymuisi dėl skaičiavimo kainos, jie suteikia naudingos intuicijos, kaip projektuoti pažangius optimizavimo algoritmus, imituojančius daugelį toliau aprašytų algoritmų pageidaujamų savybių.
12.3.3.1 Niutono metodas
Peržiūrėdami kokios nors funkcijos Teiloro skleidinį, nematome priežasties sustoti po pirmojo nario. Iš tikrųjų galime jį užrašyti kaip
Kad išvengtume sudėtingo žymėjimo, apibrėžiame kaip Hesės matricą, kuri yra matrica. Mažiems ir paprastiems uždaviniams lengva apskaičiuoti. Tačiau giliesiems neuroniniams tinklams gali būti nepriimtinai didelė dėl elementų saugojimo kainos. Be to, ją gali būti pernelyg brangu apskaičiuoti atgaliniu sklidimu. Kol kas nekreipkime dėmesio į tokius svarstymus ir pažiūrėkime, koks algoritmas gautųsi.
Juk minimumas tenkina . Laikydamiesi diferencialinio skaičiavimo taisyklių 2.4.3 skyrelis, išvestinę (12.3.4) atžvilgiu ir ignoruodami aukštesnės eilės narius, gauname
Tai yra, kaip optimizavimo uždavinio dalį turime invertuoti Hesės matricą .
Kaip paprastas pavyzdys, kai , turime ir . Taigi bet kuriam gauname . Kitaip tariant, vieno žingsnio pakanka tobulai konverguoti, nereikia jokio koregavimo! Deja, čia mums šiek tiek pasisekė: Teiloro skleidinys buvo tikslus, nes .
Pažiūrėkime, kas nutinka kituose uždaviniuose. Turint iškilią hiperbolinio kosinuso funkciją su tam tikra konstanta , matome, kad globalusis minimumas taške pasiekiamas po kelių iteracijų.
c = d2l.tensor(0.5)
def f(x): # Objective function
return d2l.cosh(c * x)
def f_grad(x): # Gradient of the objective function
return c * d2l.sinh(c * x)
def f_hess(x): # Hessian of the objective function
return c**2 * d2l.cosh(c * x)
def newton(eta=1):
x = 10.0
results = [x]
for i in range(10):
x -= eta * f_grad(x) / f_hess(x)
results.append(float(x))
print('epoch 10, x:', x)
return results
show_trace(newton(), f)
Dabar panagrinėkime neiškiliąją funkciją, pavyzdžiui, su tam tikra konstanta . Juk atkreipkite dėmesį, kad Niutono metode galiausiai dalijame iš Hesės matricos. Tai reiškia, kad jei antroji išvestinė yra neigiama, galime eiti didėjančios reikšmės kryptimi. Tai lemtinga algoritmo yda. Pažiūrėkime, kas nutinka praktikoje.
c = d2l.tensor(0.15 * np.pi)
def f(x): # Objective function
return x * d2l.cos(c * x)
def f_grad(x): # Gradient of the objective function
return d2l.cos(c * x) - c * x * d2l.sin(c * x)
def f_hess(x): # Hessian of the objective function
return - 2 * c * d2l.sin(c * x) - x * c**2 * d2l.cos(c * x)
show_trace(newton(), f)
Tai nepavyko spektakliškai. Kaip tai ištaisyti? Vienas būdas būtų „ištaisyti“ Hesės matricą vietoj jos paėmus jos absoliutinę reikšmę. Kita strategija yra grąžinti mokymosi greitį. Atrodo, kad tai paneigia tikslą, bet ne visai. Turėdami antros eilės informaciją, galime būti atsargūs, kai kreivumas didelis, ir daryti ilgesnius žingsnius, kai tikslo funkcija plokštesnė. Pažiūrėkime, kaip tai veikia su kiek mažesniu mokymosi greičiu, tarkime, . Kaip matome, turime gana efektyvų algoritmą.
show_trace(newton(0.5), f)
12.3.3.2 Konvergavimo analizė
Analizuojame tik Niutono metodo konvergavimo greitį kokiai nors iškiliajai ir tris kartus diferencijuojamai tikslo funkcijai , kurios antroji išvestinė nelygi nuliui, t. y. . Daugiamatis įrodymas yra paprastas toliau pateikto vienmačio argumento plėtinys ir praleidžiamas, nes jis mums daug nepadeda intuicijos požiūriu.
Pažymėkime reikšmę -ojoje iteracijoje ir tegul yra atstumas nuo optimumo -ojoje iteracijoje. Pagal Teiloro skleidinį sąlygą galima užrašyti kaip
kuri galioja tam tikram . Padalijus aukščiau pateiktą skleidinį iš , gauname
Prisiminkite, kad turime atnaujinimą . Įstatę šią atnaujinimo lygtį ir paėmę abiejų pusių absoliutinę reikšmę, turime
Todėl, kai tik esame srityje, kurioje aprėžta, turime kvadratiškai mažėjančią paklaidą
Pastebėkime, kad optimizavimo tyrėjai tai vadina tiesiniu konvergavimu, o tokia sąlyga kaip būtų vadinama pastovia konvergavimo sparta. Atkreipkite dėmesį, kad ši analizė turi keletą išlygų. Pirma, iš tikrųjų neturime didelės garantijos, kada pasieksime greito konvergavimo sritį. Vietoj to žinome tik tai, kad kai ją pasieksime, konvergavimas bus labai greitas. Antra, ši analizė reikalauja, kad būtų gerai besielgianti iki aukštesnės eilės išvestinių. Tai susiveda į tai, kad neturėtų jokių „netikėtų“ savybių, susijusių su tuo, kaip ji gali keisti savo reikšmes.
12.3.3.3 Pradinių sąlygų tikrinimas
Visai nenuostabu, kad pilnos Hesės matricos skaičiavimas ir saugojimas yra labai brangus. Todėl pageidautina rasti alternatyvų. Vienas būdas pagerinti padėtį yra pradinių sąlygų tikrinimas. Jis vengia skaičiuoti visą Hesės matricą ir apskaičiuoja tik įstrižainės elementus. Tai veda prie tokios formos atnaujinimo algoritmų:
Nors tai nėra visai taip gerai kaip pilnas Niutono metodas, vis tiek tai daug geriau nei jo nenaudoti. Kad pamatytume, kodėl tai gali būti gera idėja, panagrinėkime situaciją, kai vienas kintamasis žymi aukštį milimetrais, o kitas – aukštį kilometrais. Darant prielaidą, kad abiem natūralus mastelis yra metrai, turime baisų parametrizacijų neatitikimą. Laimė, pradinių sąlygų tikrinimas tai pašalina. Efektyviai pradinių sąlygų tikrinimas su gradientiniu nusileidimu reiškia skirtingo mokymosi greičio pasirinkimą kiekvienam kintamajam (vektoriaus koordinatei). Kaip pamatysime vėliau, pradinių sąlygų tikrinimas lemia dalį naujovių stochastinio gradientinio nusileidimo optimizavimo algoritmuose.
12.3.3.4 Gradientinis nusileidimas su tiesine paieška
Viena pagrindinių gradientinio nusileidimo problemų yra ta, kad galime peršokti tikslą arba padaryti nepakankamą pažangą. Paprastas problemos sprendimas yra naudoti tiesinę paiešką kartu su gradientiniu nusileidimu. Tai yra, naudojame kryptį, kurią nurodo , o tada atliekame dvejetainę paiešką, kuris mokymosi greitis minimizuoja .
Šis algoritmas konverguoja sparčiai (analizės ir įrodymo žr., pvz., (Stephen Boyd ir Lieven Vandenberghe, )). Tačiau giliojo mokymosi tikslais tai nėra visai įgyvendinama, nes kiekvienam tiesinės paieškos žingsniui reikėtų įvertinti tikslo funkciją visame duomenų rinkinyje. Tai pernelyg brangu.
12.3.4 Santrauka
- Mokymosi greičiai svarbūs. Per dideli – diverguojame, per maži – nedarome pažangos.
- Gradientinis nusileidimas gali įstrigti lokaliuosiuose minimumuose.
- Didelėse dimensijose mokymosi greičio koregavimas yra sudėtingas.
- Pradinių sąlygų tikrinimas gali padėti koreguoti mastelį.
- Niutono metodas yra daug greitesnis, kai jis pradeda tinkamai veikti iškiliuosiuose uždaviniuose.
- Būkite atsargūs naudodami Niutono metodą be jokių koregavimų neiškiliesiems uždaviniams.
12.3.5 Pratimai
- Eksperimentuokite su skirtingais mokymosi greičiais ir tikslo funkcijomis gradientiniam nusileidimui.
- Įgyvendinkite tiesinę paiešką iškiliajai funkcijai minimizuoti intervale .
- Ar dvejetainei paieškai reikia išvestinių, t. y. nuspręsti, ar rinktis , ar .
- Kokia sparti yra algoritmo konvergavimo sparta?
- Įgyvendinkite algoritmą ir pritaikykite jį minimizavimui.
- Suprojektuokite tikslo funkciją, apibrėžtą , kuriai gradientinis nusileidimas yra nepaprastai lėtas. Užuomina: skirtingas koordinates keiskite skirtingu masteliu.
- Įgyvendinkite lengvą Niutono metodo versiją naudodami pradinių sąlygų tikrinimą:
- Kaip pradinių sąlygų tikrinimą naudokite įstrižainės Hesės matricą.
- Naudokite jos absoliutines reikšmes, o ne tikrąsias (galbūt su ženklu) reikšmes.
- Pritaikykite tai aukščiau pateiktam uždaviniui.
- Pritaikykite aukščiau pateiktą algoritmą daugeliui tikslo funkcijų (iškiliųjų ar ne). Kas nutinka, jei pasukate koordinates laipsniais?
