- Sinir ağı eğitiminin temelindeki otomatik türevleme, skalarlar için bir
Tensor sınıfı doğrudan uygulanarak; değer hesaplama ile türev hesaplamanın aynı hesaplama grafiği üzerinde nasıl ilerlediğini gösteriyor
- Normal Python değişkenlerinde
z = x + y ifadesinden sonra yalnızca sonuç değeri kalır ve ilişki kaybolur; bu yüzden Tensorun değerle birlikte işlem geçmişini de saklaması gerekir
Children(a, b, op) ve forward() özyinelemeli çağrılarıyla bir ikili ağaç hesaplama grafiği oluşturuluyor; toplama ve çarpmanın yeniden tanımlanması sayesinde değerler sonradan verilse bile ifade yeniden hesaplanabiliyor
grad(deriv_to), kendisine göre türevi 1, başka bir skalara göre türevi 0 alıyor ve temel işlemlerin türev kurallarını özyinelemeli biçimde uygulayarak yeni bir hesaplama grafiği oluşturuyor
- Uygulama yalnızca skalarları işliyor ve yavaş olabilir; bu nedenle dizi işlemleri, 0 çarpımı dallarını budama, sabit düğüm işleme ve tekrar eden hesaplamaları azaltan önbellek gelecekte iyileştirilecek alanlar olarak öne çıkıyor
Normal Python değişkenlerinde ilişki kaybolur
x = 3, y = 5, z = x + y gibi bir hesaplama yapıldığında z içinde yalnızca sonuç değeri 8 kalır
- Daha sonra
x ya da y değerleri değişse bile znin hangi değişkenlerden üretildiği izlenemez
- Değişkenler arasındaki ilişki korunmadığı için, belirli bir değişkene göre türevi otomatik hesaplamak zordur
Tensor ile işlem geçmişini korumak
- Yeni
Tensor tipi değeri (value) saklar ve operatörleri yeniden tanımlayarak Tensorlar arasında hesaplama yapıldığında yeni bir Tensor döndürür
- İlk uygulama yalnızca
__add__ metodunu yeniden tanımlar; böylece Tensor(3) + Tensor(5) ifadesi T:8 üretebilir
- Ancak bu aşamada
znin x + y sonucunda oluştuğuna dair işlem geçmişi henüz korunmaz
Hesaplama grafiği ve forward()
- İşlem geçmişini korumak için
Children = namedtuple('Children', ['a', 'b', 'op']) tanıtılır
a: sol giriş tensörü
b: sağ giriş tensörü
op: np.add, np.multiply gibi gerçek işlem
- Her
Tensor, sayısal değerin yanı sıra children da taşıyabilir ve bunun üzerinden ikili ağaç biçiminde bir hesaplama grafiği kurulur
forward(), alt düğümleri özyinelemeli olarak ziyaret ederek gerçek değeri hesaplar
x = Tensor(3), y = Tensor(5) iken z1 = x + y, z2 = z1 * y sonucu T:40 olur
- Grafik önce
x = Tensor(None), y = Tensor(None) ile kurulup daha sonra x.value = 3, y.value = 5 atanarak z2.forward() çağrıldığında da T:40 hesaplanır
Otomatik türevlemeyi hesaplama grafiği olarak kurmak
- Otomatik türevleme,
Tensorun desteklediği her temel işleme bir türev kuralı eklenerek uygulanır
grad(self, deriv_to), hesaplama grafiğini özyinelemeli olarak dolaşır ve karmaşık bir fonksiyonu basit fonksiyonların birleşimine ayırır
- Temel kurallar şunlardır
- Bir tensörün kendisine göre türevi
Tensor(1)
- Çocuğu olmayan bir skalerin başka bir tensöre göre türevi
Tensor(0)
- Toplama:
(a + b)' = a' + b'
- Çarpma:
(ab)' = a'b + ab'
z2 = (x + y) * y ifadesi yye göre türevlendiğinde sonuç g, basit bir değer değil kısmi türevi gösteren yeni bir hesaplama grafiği olur
- Denklem olarak
g = ∂z2/∂y = x + 2*y
x = 3, y = 5 iken gnin değeri 13 olur
Çıkarma, bölme ve üstel fonksiyona kadar genişletme
- Daha karmaşık ifadeleri ele almak için
Tensora çıkarma, bölme, üstel fonksiyon ve negatif alma işlemleri eklenir
grad() içine her işleme karşılık gelen türev kuralları konur
- Çıkarma:
(a - b)' = a' - b'
- Bölme:
(a/b)' = (a'b - ab') / b²
- Üstel fonksiyon:
exp(a)' = a' * exp(a)
forward() da yalnızca tek terim gerektiren işlemleri ele alacak şekilde değiştirilir
- Örnek:
exp(a) için ikinci terim b gerekmez
-x, 0 - x biçiminde işlenir
Örnek ifade ve Sympy ile doğrulama
- Aşağıdaki ifade
Tensor ile yazılır ve x, y için kısmi türevleri hesaplanır
z = (12 - (x * e^y)) / (45 + x * y * e^-x)
- Kodda şu şekilde ifade edilir
x = Tensor(3)
y = Tensor(5)
z = (Tensor(12) - (x * y.exp())) / (Tensor(45) + x * y * (-x).exp())
- Hesaplanan kısmi türev değerleri şöyledir
z.grad(x) → T:-3.34729777301069
z.grad(y) → T:-9.70176956641438
- Aynı ifadenin
Sympy içindeki diff() ve evalf() ile hesaplanan sonucu da aynıdır
xs = 3, ys = 5 iken xe göre türev değeri -3.34729777301069
yye göre türev değeri -9.70176956641438
Basit uygulamanın kısıtları ve optimizasyon noktaları
- Bu uygulama, en basit otomatik türevleme sistemlerinden birine yakın ve aynı zamanda oldukça yavaş olabilir
- Mevcut sınıf yalnızca skalarları işler
- Daha kullanışlı bir kütüphane olması için rastgele boyutlu dizi işlemlerinin eklenmesi gerekir
- Hesaplama grafiğine bakıldığında çeşitli optimizasyonlar mümkündür
- Çarpma düğümünde çocuklardan biri
0 ise daha derine inmeye gerek kalmaz
- Bir düğüm ve çocukları, türevi alınan
x tensörüne bağlı değilse o düğüm sabit kabul edilip dolaşım durdurulabilir
- Aynı işlemler tekrarlandığında önbellek kullanılarak aynı hesaplamanın birden fazla kez yapılması önlenebilir
1 yorum
Hacker News yorumları
Bu tür küçük ve zarif kod demolarını seviyorum. Çünkü kavramı ellerinizi kirleterek anlamanızı sağlıyorlar.
Sasha Rush’ın GPU bulmacaları ve tensör bulmacaları da benzer örnekler.
https://github.com/srush/GPU-Puzzles
https://github.com/srush/Tensor-Puzzles
Orijinal kod burada: https://github.com/jaymody/picoGPT/blob/main/gpt2.py
Sadece bununla otomatik türevlemeyi tamamen anladığınıza inanıyorsanız kendinizi kandırıyorsunuz.
Grafik bir ağaç olduğunda, bu yazıdaki gibi her şey çok basittir. Ama grafik daha genel bir yönlü çevrimsiz grafikse; örneğin
x = 5; y = 2x; z = xyise, uygulama hâlâ çok basit olsa da neden doğru olduğunu anlamak kolay değildir. Bunun “sadece normal zincir kuralı” olduğunu düşünüyorsanız yine kendinizi kandırıyorsunuz.İlk açıklamalardan birini Paul Werbos yaptı; gereken kurala sıralı türevler için zincir kuralı adını verdi ve bunu normal zincir kuralından tümevarımla kanıtladı. Yine de normal zincir kuralından hemen apaçık çıkmaz. Aksini düşünen biri varsa, yanıldığımı kanıtlamasını isterim; bu beni çok mutlu eder.
Hesaplama grafiğinin, yani yönlü çevrimsiz grafiğin kenarları boyunca normal zincir kuralını uygularsanız her adımda doğru değeri elde edersiniz. Gereken ek kural, “bir değişken hesaplamada birden fazla kez kullanılırsa, yani aynı düğümden birden çok kenar çıkıyorsa veya ters yönde birden çok kenar giriyorsa, ayrı ayrı hesaplanan gradyanlar toplanmalıdır” gibi bir şeydir; bunu da oldukça temel ve sezgisel buluyorum.
Örneğin
f(x, y)içinde hemxhem deyyerinezkoyarsanızd/dz f(z, z) = f_x(z, z) + f_y(z, z)olur; alt indisler kısmi türevleri ifade eder. Bana göre bu yaklaşım, ikisini karıştırıp “zincir kuralının ötesinde” bir şeymiş gibi göstermekten matematiksel olarak daha basit; ayrıca gerçek uygulamaya, özellikle de en aşina olduğum PyTorch’un yaptığı işe daha yakın görünüyor.Otomatik türevleme sihir gibi geliyor.
Birçok bilgisayar bilimci buna kapılıp tekniği daha geniş bir perspektiften tanıtan yazılar yazdı. Benim yazım da onlardan biri; ayrıca operatör overloading olmadan karmaşık sayılar kullanan “fakir işi” bir varyant da içeriyor.
https://pizzaseminar.speicherleck.de/automatic-differentiati...
[0]: https://doi.org/10.1145/838250.838251
Benim yaptığım 26 satırlık Python otomatik türevleme uygulaması var: https://gist.github.com/sradc/d9d66e3898ffe3a02e0b6b266629b0...
Bilgi tabanlı mühendislik sistemlerinde kullanılan tekniğe çok benziyor; orada buna bağımlılık izleme deniyor. Düğüm ya da tensör önbellekleme ile birlikte kullanıldığında hesaplama miktarını azaltabilir; özellikle büyük parametrik 3D modeller için yararlıdır.
Bir değeri alırken ikili/bağımlılık ağacını özyinelemeli olarak çağırıp hangi değişkenlerin değiştiğini kontrol eder ve yalnızca gerekenleri yeniden hesaplar.
__set__,__get__metodlarına sahip özel Python nesneleri ve özellikler kullanırsanız bunu nesne yönelimli modelin yerleşik bir özelliği gibi yapabilirsiniz.x = Tensor(3)y = Tensor(5)z = x + yprint(x, y) # 3, 5print(z) # 8x.value = 4 # değer ayarlanırken hiçbir şey yeniden hesaplanmazprint(z) # 9, çünkü değer alındığı anda değişen bağımlılık yeniden hesaplanırAndrej Karpathy’nin bir autograd motoru oluşturduğu ilginç bir videosu var ve oldukça ufuk açıcı.
https://youtu.be/VMj-3S1tku0?si=wuKhELwOwoYbzpt7
Depo:
https://github.com/karpathy/micrograd
Bildiğim otomatik türev alma varyasyonu bir işlem grafiği oluşturmaz. Bunun yerine ilgili değeri anında hesaplar
İkisi de çalışır, ancak duruma göre biri daha verimlidir. “Sinir ağı eğitimi” gibi durumlarda, çok sayıda hedef için tek bir kayıp çıktısını optimize etmek sık görüldüğünden genelde geri mod kullanılır
Otomatik türev almaya keşke sadece sayısal zincir kuralı dense ya da en azından öyle açıklansa. Kelimenin tam anlamıyla bundan ibaret; belirli işlemlerde Jacobian matrisini açıkça hesaplamamak için eklenmiş birkaç numara var sadece, bu yüzden çok daha anlaşılır olur
Gerçekten de bir hesaplama grafiği üzerinden gradyanları yaymak için zincir kuralını kullanmanız istendiğinde, çoğu kişi sezgisel olarak varsayılan şekilde ileri modu düşünür gibi geliyor. Ben de öyle yapardım.
https://en.wikipedia.org/wiki/Automatic_differentiation#Beyo...
Bu açıdan bakınca, zincir kuralının sağladığı ifadeleri dolaşıp gradyanları biriktirmenin belirli bir yöntemini ifade eden bir terim olarak kullanmak yararlı görünüyor
“Diğer yöntemlerle karşılaştırma” bölümüne bakabilirsiniz: https://en.m.wikipedia.org/wiki/Automatic_differentiation
Yakındaki yorumlarda denildiği gibi, asıl nokta uygulamanın gerçekten önemli ve öğrenmeye değer olması. Otomatik türev almayı zincir kuralını uygulama yöntemlerinin bir bütünü olarak adlandırmak sorun değil, ama buna “sadece” sayısal zincir kuralı demek yanlış
Otomatik türev alma, düzgün fonksiyonlar kategorisinde Jacobian matrisi ve toplam türevin Kartezyen lensinden ibaret; bunda sorun ne? https://www.youtube.com/watch?v=ne99laPUxN4
Sınıf adının neden Tensor olduğunu merak ediyorum. Bir ifadeyi ya da onun türevini tensör olarak düşünmenin bir yolu var mı? Yoksa skalerler de tensör olduğu ve bu yapı başka tensör türlerini de destekleyecek şekilde genişletilebildiği için mi?
Açıklanan otomatik türev alma algoritması keyfi yüksek boyutlu nesneler için çalıştığından, bu nesnelere tensör demek mantıklı görünüyor