dersblog



Genel Optimizasyon, Paketler, Autograd

Otomatik türevin nasıl işlediğini [1] yazısında gördük. Programlama dilinde yazılmış, içinde if, case, hatta döngüler bile içerebilen herhangi bir kod parçasının türevini alabilmemizi sağlayan otomatik türev almak pek çok alanda işimize yarar. Optimizasyon alanı bunların başında geliyor. Düşünürsek, eğer sembolik olarak türev alması çok çetrefil bir durum varsa, tasaya gerek yok; bir fonksiyonu kodlayabildiğimiz anda onun türevini de alabiliriz demektir.

Autograd

Çok boyutlu bir fonksiyonun gradyani ve Hessian'ı,

from autograd import grad, hessian

def objective(X): 
    x, y, z = X
    return x**2 + y**2 + z**2

x,y,z = 1.0,1.0,1.0

h = hessian(objective, 0)
res = h(np.array([x, y, z]))
print (res)

g = grad(objective, 0)
res = g(np.array([x, y, z]))
print (res)
[[2. 0. 0.]
 [0. 2. 0.]
 [0. 0. 2.]]
[2. 2. 2.]

Ya da

Hessian

Mesela $f(x_1,x_2) = x_2^3 + x_2^3 + x_1^2x_2^2$ gibi bir fonksiyon var diyelim. Belli bir noktadaki Hessian

$$ H = \left[\begin{array}{rr} \frac{\partial f}{\partial x_1x_1} & \frac{\partial f}{\partial x_1x_2} \\ \frac{\partial f}{\partial x_2x_1} & \frac{\partial f}{\partial x_2x_2} \end{array}\right] $$

hesaplatmak için autograd.hessian kullanırız,

import autograd 

def f(x):
    x1,x2=x[0],x[1]
    return x1**3 + x2**3 + (x1**2)*(x2**2)

print
xx = np.array([1.0,1.0])
h = autograd.hessian(f)
print (h(xx))
[[8. 4.]
 [4. 8.]]

Şimdi bazı genel optimizasyon konularını işleyelim.

Sınırlanmamış optimizasyonda (unconstrained optimization) $f(x)$ fonksiyonunu minimum değerde tutacak $x$ değerini bulmaya uğraşıyoruz, ki $x$ tek boyutlu skalar, ya da çok boyutlu $x \in \mathbb{R}^n$ olabilir. Yani yapmaya uğraştığımız

$$ \min_x f(x) $$

işlemi. Peki minimumu nasıl tanımlarız? Bir nokta $x^\ast$ global minimize edicidir eğer tüm $x$'ler için $f(x^\ast) \le f(x)$ ise, ki $x \in \mathbb{R}^n$, en azından $x$ modelleyeni ilgilendiren tüm küme öğeleri için.

Fakat çoğu zaman bir global $f$'i kullanmak mümkün olmayabilir, fonksiyon çok çetrefil, çok boyutlu, bilinmez durumdadır, ve elimizde sadece yerel bilgi vardır. Bu durumda üstteki tanımı "bir $N$ bölgesi içinde" olacak şekilde değiştiririz ki bölge, $x^\ast$ etrafındaki, yakınındaki bölgedir.

Üstteki tanımı okuyunca $x^\ast$'in yerel minimum olup olmadığını anlamanın tek yolunun yakındaki diğer tüm noktalara teker teker bakmak olduğu anlamı çıkabilir, fakat eğer $f$ pürüzsüz bir fonksiyon ise yerel minimumu doğrulamanın çok daha hızlı bir yöntemi vardır. Hatta ve hatta eğer fonksiyon $f$ iki kez türevi alınabilir haldeyse $x^\ast$'in yerel minimum olduğunu ispatlamak daha kolaylaşır, $\nabla f(x^\ast)$ ve Hessian $\nabla^2 f(x^\ast)$'e bakarak bunu yapabiliriz.

Minimallik için 1. ve 2. derece şartlar var. 1. derece gerekli şart (ama yeterli değil) $\nabla f = 0$ olması. Bu standard Calculus'tan bildiğimiz bir şey, minimum ya da maksimumda birinci türev sıfırdır. Ama türevin sıfır olup minimum ya da maksimum olmadığı durum da olabilir, mesela $f(x) = x^3$. $f'(0) = 0$'dir fakat $x=0$ ne maksimum ne de minimumdur. Daha iyi bir termioloji $\nabla f = 0$ noktalarını {\em kritik nokta} olarak tanımlamaktır. $x=0$ noktasında bir değişim oluyor, bu değişim kritik bir değişim, her ne kadar minimum ya da maksimum olmasa da.

x = np.linspace(-3,3,100)
plt.plot(x,x**3)
plt.grid(True)
plt.savefig('func_40_autograd_01.png')

Bir kritik noktanın yerel maksimum ya da yerel minimum olup olmadığını anlamak için fonksiyonun ikinci türevine bakabiliriz. Bir $f: \mathbb{R}^n \to \mathbb{R}$ var ve $x^\ast$ noktasının kritik nokta olduğunu düşünelim, yani $\nabla f(x^\ast) = 0$. Şimdi çok ufak bir $h$ adımı için $f(x^\ast + h)$'a ne olduğuna bakalım. Burada Taylor açılımı kullanabiliriz [2],

$$ f(x + h^\ast) = f(x^\ast) + \nabla f(x^\ast) h + \frac{1}{2} h^T f(x^\ast) \nabla^2 (x^\ast) f(x^\ast) h + O(3) $$

$\nabla^2 (x^\ast)$ bir matristır içinde $f$'nin ikinci derece türevleri vardır [6]. Şimdi, kritik noktada olduğumuz için $\nabla f(x^\ast) = 0$, ve $O(3)$ terimlerini iptal edersek, üstteki

$$ f(x^\ast + h^\ast) - f(x^\ast) = \frac{1}{2} h^T \nabla^2 (x^\ast) h + O(3) $$

haline gelir. Simdi "bir noktanın mesela yerel maksimum olması" sözünü $f(x^\ast + h^\ast) - f(x^\ast) < 0$ ile ifade edebiliriz, çünkü $x^\ast$ etrafındaki tüm $x$'lerin $f$'in daha az değerlerinden olma şartını aramış oluyoruz (adım atılıyor, çıkartma yapılıyor, sonuç sıfırdan küçük). Tabii bu "tüm" söylemi yaklaşıksal, o sebeple minimumluk ifadesi yerel.

Devam edersek $f(x^\ast + h^\ast) - f(x^\ast) < 0$ olması şartı aynı zamanda $\frac{1}{2} h^T \nabla^2 (x^\ast) h < 0$ anlamına gelir, bu da $\nabla^2 (x^\ast )$ negatif kesin demektir. Çünkü $A$ simetrik bir matris olduğu zaman

$x^TAx < 0$ ise matris negatif kesin

$x^TAx \le 0$ ise matris negatif yarı-kesin (negatif semi-definite)

$x^TAx > 0$ ise matris pozitif kesin

$x^TAx \ge 0$ ise matris pozitif yarı-kesin (positive semi-definite)

Gradyan Inisi

Optimizasyonun mekaniğine gelelim. Diyelim ki basit, tek boyutlu bir $f(x) = x^2$ fonksiyonumuz var. Tek boyutlu bu ortamda bir noktadan başlayıp gradyanın (1. türev) işaret ettiği yönde ufak bir adım atmak bizi minimuma daha yaklaştırır, ve bunu ardı ardına yaparak yerel bir minimuma erisebiliriz. Örnek $f(x)$ dışbükey (convex) olduğu için bu bizi global minimuma götürür [3]. Formül

$$ x_{i+1} = x_i + \alpha \nabla f(x_i) $$

Başlangıç $x_0$ herhangi bir nokta, üstteki formülle adım ata ata ilerliyoruz, adım boyutunu bizim tanımladığımız bir $\alpha$ sabitiyle ayarlayabiliyoruz.

import autograd

def fun(x):
    return x**2

def grad_desc(x, fun, alpha=0.1, max_iter=100):
    xs = np.zeros(1 + max_iter)
    xs[0] = x
    grad = autograd.grad(fun)

    for step in range(max_iter):
        x = x - alpha * grad(x)
        xs[step + 1] = x

![](func_40_autograd_02.png)
    return xs

alpha = 0.1
x0 = 1.

x_opt = grad_desc(x0, fun, alpha = alpha, max_iter = 10)
y_opt = fun(x_opt)

x_true = np.linspace(-1.2, 1.2, 100)
y_true = fun(x_true)

plt.plot(x_true, y_true)
plt.plot(x_opt, y_opt, 'o-', c='red')

for i, (x, y) in enumerate(zip(x_opt, y_opt), 1):
      plt.text(x - 0.1, y + 0.1, i, fontsize=15)

plt.show()

Türevi autograd ile aldık, bu örnekte sembolik türev kolaydı, elle $f'(x)=2x$ diyebilirdik ama gösterim amaçlı direk yazılımla türevi aldık.

Kısıtlanmış Optimizasyon

Mühendislik problemlerinde kısıtlanmış optimizasyon çok ortaya çıkar. Prototipik örnek bir düzlem üzerindeki orijine en yakın noktayı bulmak. Mesela düzlem $2x - y + z = 3$ olsun, ve mesafeyi minimize etmek istiyoruz, bunu $x^2+y^2+z^2$ ile hesaplayabiliriz. Yani optimizasyon problemi düzlem denklemi ile sınırlanan mesafe formülünün minimal noktasını bulmak [5].

Problemi direk scipy.optimize.minimize ile çözelim.

from scipy.optimize import minimize

def objective(X): # hedef
    x, y, z = X
    return x**2 + y**2 + z**2

def cons(X): # kisitlama
    x, y, z = X
    return 2 * x - y + z - 3

x0 = [1, 1, 1]
sol = minimize(objective, x0, constraints={'type': 'eq', 'fun': cons})
print (sol)
     fun: 1.5000000035790053
     jac: array([ 1.99997392, -1.00010441,  0.99994774])
 message: 'Optimization terminated successfully.'
    nfev: 22
     nit: 4
    njev: 4
  status: 0
 success: True
       x: array([ 0.99998696, -0.50005221,  0.49997386])

Fonksiyon minimize için kısıtlamalar eq ile sıfıra eşit olma üzerinden tanımlanır. Eğer ineq kullanılırsa sıfırdan büyük olma tanımlanıyor o zaman mesela $x>0$ ve $x<5$ kısıtlamalarını getirmek istersek,

cons=({'type': 'ineq','fun': lambda xvec: 5.0-xvec[1]}, # y<5
      {'type': 'ineq','fun': lambda xvec: xvec[1]}) # y>0
sol = minimize(objective, x0, method = 'SLSQP', constraints=cons)
print (sol)

Not: SLSQP metotu gradyana ihtiyaç duymuyor.

     fun: 1.1090612774580318e-16
     jac: array([7.79817877e-12, 1.49011612e-08, 7.79860898e-12])
 message: 'Optimization terminated successfully.'
    nfev: 20
     nit: 4
    njev: 4
  status: 0
 success: True
       x: array([-7.44668151e-09,  2.73897702e-24, -7.44668129e-09])

Bazen her şeyi kendimiz yaparak tüm adımların ne yaptığından emin olmak isteyebiliriz. Mesela kısıtlama şartlarını kendimiz bir Lagrange çarpanı $f(x) f(x) - \lambda g(x)$ ifadesi üzerinden tanımlayıp, türevi alıp sıfıra eşitleyip, $f_x(x)=f_y(x)=f_z(x)=g(x)=0$ ile, elde edilen kısıtsız optimizasyonu çözmeyi tercih edebiliriz. Türevin alınmasını direk autograd'a yaptırırız.

import autograd.numpy as np
from autograd import grad

def F(L):
    x, y, z, _lambda = L
    return objective([x, y, z]) - _lambda * eq([x, y, z])

dfdL = grad(F, 0)

# Find L that returns all zeros in this function.
def obj(L):
    x, y, z, _lambda = L
    dFdx, dFdy, dFdz, dFdlam = dfdL(L)
    return [dFdx, dFdy, dFdz, eq([x, y, z])]

from scipy.optimize import fsolve
x, y, z, _lam = fsolve(obj, [0.0, 0.0, 0.0, 1.0])
print (x,y,z)
1.0 -0.5 0.5

Aynı sonuç bulundu. Şimdi merak ediyoruz, bu sonuç gerçekten minimum mu? Üstteki noktada Hessian'ın pozitif kesin olup olmadığını kontrol edebiliriz. Hessian'ı da autograd hesaplar! Once gradyan,

from autograd import hessian
h = hessian(objective, 0)
res = h(np.array([x,y,z]))
print (res)
[[2. 0. 0.]
 [0. 2. 0.]
 [0. 0. 2.]]

Bu matris pozitif kesin, ama çıplak gözle bariz değilse, tüm özdeğerleri pozitif olup olmadığına bakabiliriz,

print (np.linalg.eig(h(np.array([x, y, z])))[0])
[2. 2. 2.]

Birden Fazla Gradyan Değişkeni

Diyelim ki elimizde

$$ g(w_1,w_2) = \tanh (w_1w_2) $$

fonksiyonu var, bu üç boyutlu bir fonksiyon, ve optimizasyon amaçlı gradyan gerekiyor, gradyanın iki değişken üzerinden alınması gerekli [7].

import autograd
from autograd import numpy as anp

def g(w_1,w_2):
    return anp.tanh(w_1*w_2)

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
x = np.linspace(-4,4,20)
y = np.linspace(-4,4,20)
xx,yy = np.meshgrid(x,y)
zz = g(xx,yy)
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(xx, yy, zz, cmap=cm.coolwarm)
plt.savefig('func_40_autograd_03.png')

$g$'nin her iki kısmi türevini ve gradyanını,

$$ \nabla g(w_1,w_2) = \left[\begin{array}{r} \frac{\partial }{\partial w_1} g(w_1,w_2) \\ \frac{\partial }{\partial w_2} g(w_1,w_2) \end{array}\right] $$

autograd ile hesaplamak için

dgdw1 = autograd.grad(g,0)
dgdw2 = autograd.grad(g,1)

Dikkat edersek, 0 ve 1 parametreleri geçildi, bunlar sırasıyla $w_1$ ve $w_2$ değişkenlerine tekabül ediyorlar (g tanımındaki sıralarına göre, 0. ve 1. parametreler). Şimdi mesela (1.0,2.0) noktasındaki gradyanı hesaplayabiliriz,

gradg = [dgdw1(1.0,2.0), dgdw2(1.0,2.0)]
print (gradg)
[0.14130164970632894, 0.07065082485316447]

Tabii çok boyutlu ortamda yazının başındaki teknikleri kullanmak daha iyi, üstteki bir seçenek.

Kaynaklar

[1] Bayramlı, Ders Notları, Otomatik Türev Almak (Automatic Differentiation -AD-)

[2] Schrimpf, http://faculty.arts.ubc.ca/pschrimpf/526/526.html

[3] Stoyanov, https://nikstoyanov.me/post/2019-04-14-numerical-optimizations

[5] Kitchin, http://kitchingroup.cheme.cmu.edu/blog/2018/11/03/Constrained-optimization-with-Lagrange-multipliers-and-autograd/

[6] Bayramlı, Cok Boyutlu Calculus, Vektör Calculus, Kurallar, Matris Türevleri

[7] Watt, Automatic Differentiation, https://jermwatt.github.io/machine_learning_refined/notes/3_First_order_methods/3_5_Automatic.html


Yukarı