Gradyan inişi
Bir modeli eğitmek, kayıp fonksiyonunun (loss function)kayıp fonksiyonu (loss function): Modelin tahminlerinin ne kadar yanlış olduğunu tek bir sayıyla ölçen fonksiyon. verdiği sayıyı, yani kaybı (loss), en küçük yapan parametreleri bulmak demektir. Gradyan inişi (gradient descent)gradyan inişi (gradient descent): Kaybı azaltmak için parametreleri gradyanın tersine küçük adımlarla güncelleyen yöntem. bunu küçük adımlarla yapar. Eğimin ters yönüne bir adım at, sonra tekrarla.
Bu derste şunları yapacaksın:
- Kuralı tek parametreli bir loss’la kuracaksın.
- Öğrenme oranının (learning rate)öğrenme oranı (learning rate): Gradyan inişinde atılan adımın büyüklüğü. Çok küçükse eğitim yavaşlar, çok büyükse kayıp büyür. neden bu kadar önemli olduğunu göreceksin.
- İki parametreli bir yüzeyde gradient descent’in nasıl ilerlediğine bakacaksın.
- Gradyanı (gradient)gradyan (gradient): Bir fonksiyonun en hızlı arttığı yönü ve bu artışın dikliğini gösteren türev vektörü. bilmediğin durumlar için sayısal bir yol öğreneceksin.
Eğimi takip et
Eğim ve gradyan
Bir model minimumun yerini bilmez; bildiği tek şey bulunduğu noktadaki eğimdir. En basit loss olsun. Minimumun ‘da olduğunu biz biliyoruz.
‘nin türevi (derivative) şudur:
Tek parametrede gradyan, türevin kendisidir. onu gösterir.
İki durum var:
- Eğim pozitifse loss sağa doğru artıyor demektir. ‘yi küçült.
- Eğim negatifse ‘yi büyüt.
Aynı türevi iki noktada hesapla:
w = 4.0print(2 * w) # → 8.0 eğim pozitif: w'yi küçültw = -4.0print(2 * w) # → -8.0 eğim negatif: w'yi büyüt
Güncelleme kuralı
İki durumu tek kuralda toplayabilirsin: her adımda gradyanıngradyan (gradient): Bir fonksiyonun en hızlı arttığı yönü ve bu artışın dikliğini gösteren türev vektörü. tersine git.
Burada (eta) learning rate’tiröğrenme oranı (learning rate): Gradyan inişinde atılan adımın büyüklüğü. Çok küçükse eğitim yavaşlar, çok büyükse kayıp büyür.. Adımın ne kadar büyük olacağını belirler.
ve ile başla. İlk adım şöyle çıkar:
- Gradyanı bul: .
- Adımı bul: .
- ‘yi güncelle: .
Aynı üç adım kodda:
w, lr = 4.0, 0.1grad = 2 * w # → 8.0 gradyanı buldelta = lr * grad # → 0.8 adımı bulw = w - delta # → 3.2 w'yi güncelle
Adımları tekrarlamak
Bir sonraki adımda gradyan ‘e düşer. Minimuma yaklaştıkça adımlar kendiliğinden küçülür.
Kodda bu kural bir fonksiyondur. Bir döngü onu tekrar tekrar çağırır:
def step(w, lr): grad = 2 * w # loss'un w noktasındaki eğimi return w - lr * grad # gradyanın tersine bir adımw = 4.0for i in range(4): new_w = step(w, 0.1) print(i + 1, round(new_w, 4), round(w - new_w, 4)) w = new_w# adım no, yeni w, adım büyüklüğü# → 1 3.2 0.8# → 2 2.56 0.64# → 3 2.048 0.512# → 4 1.6384 0.4096
Aşağıda learning rate’i değiştir ve topun nasıl indiğine bak.
Kayıp eğrisi · loss curve
adım 0 / 12 · w = 4,000 · kayıp = 16,000
İpucu
‘yı yap: her adım minimumun öbür tarafına bir öncekinden daha uzağa düşer ve loss büyür.
Öğrenme oranı
Adım bir çarpandır
Her adım ‘yi aynı sayıyla çarpar. için bir adımı açarsan şunu elde edersin:
Çarpan ‘dır. Aynı sonucu iki yoldan bul:
w, lr = 4.0, 0.1print(w - lr * 2 * w) # → 3.2 gradyan adımıprint((1 - 2 * lr) * w) # → 3.2 çarpanla aynı sonuçprint(round(w * (1 - 2 * lr) ** 3, 3)) # → 2.048 üç adım: çarpan üç kez
Çarpana göre beş durum
Çarpana bakarak ne olacağını önceden söyleyebilirsin:
- : çarpan 0 ile 1 arasında. hep aynı tarafta kalarak sıfıra yaklaşır. Buna yakınsama (convergence) denir.
- : çarpan 0. Tek adımda minimuma ulaşırsın. Bu yalnızca bu loss’a özgü bir şanstır.
- : çarpan ile 0 arasında. minimumun iki yanında salınarak yaklaşır ve yine yakınsar.
- : çarpan . her adımda yalnızca işaret değiştirir. Loss hiç değişmez: ne yakınsar ne de ıraksar.
- : çarpanın mutlak değeri 1’den büyük. Her adım minimumu bir öncekinden daha çok aşar ve loss büyür. Buna ıraksama (divergence) denir.
Beş durumu ‘ten başlayarak dört adım izle:
import numpy as npfor lr in [0.2, 0.5, 0.9, 1.0, 1.05]: factor = 1 - 2 * lr path = 4.0 * factor ** np.arange(5) print(lr, round(factor, 2), path.round(3).tolist())# lr, çarpan, w'nin beş değeri# → 0.2 0.6 [4.0, 2.4, 1.44, 0.864, 0.518]# → 0.5 0.0 [4.0, 0.0, 0.0, 0.0, 0.0]# → 0.9 -0.8 [4.0, -3.2, 2.56, -2.048, 1.638]# → 1.0 -1.0 [4.0, -4.0, 4.0, -4.0, 4.0]# → 1.05 -1.1 [4.0, -4.4, 4.84, -5.324, 5.856]
Dört oranı karşılaştır
Yukarıdaki grafikte dört oranı da dene:
- : minimuma çok yavaş yaklaşırsın.
- : minimuma aynı taraftan hızla yaklaşırsın.
- : minimuma iki yanda salınarak yaklaşırsın.
- : her adımda biraz daha uzaklaşırsın.
Aynı dört oranla 10 adım sonraki loss:
for lr in [0.05, 0.2, 0.9, 1.05]: w = 4.0 * (1 - 2 * lr) ** 10 # 10 adım sonra w print(lr, round(w**2, 4)) # loss# → 0.05 1.9452# → 0.2 0.0006# → 0.9 0.1845# → 1.05 107.64
Gerçek modellerde learning rate
Gerçek modellerde loss bu kadar basit değildir ve en iyi learning rate’i bir formülle bulamazsın. Oranı yanlış seçersen iki şey olabilir:
- Çok küçük bir oran eğitimi gereksiz yere yavaşlatır.
- Çok büyük bir oran loss’u patlatır.
Bu yüzden learning rate, eğitimde ilk ayarlanan değerlerden biridir.
İki parametrede gradyan
Kayıp yüzeyi
İki parametreli bir modelin loss’u, gibi bir yüzeydir. Bunu bir önceki derste gördün: doğrusal modelde bu yüzey bir çanaktı. Gerçek modellerin parametresi de birden çoktur.
Parametrelere ve diyelim. Bilerek uzun bir çanak seçelim:
Her çifti bir loss değeri verir. Bu değerlerin oluşturduğu yüzeye kayıp yüzeyi (loss surface) denir. Formüle göre olduğunda , loss’a ‘in on katı kadar katkıda bulunur.
Formülü fonksiyon yap ve üç noktada dene:
import numpy as npdef loss(w): return 0.5 * w[0]**2 + 5 * w[1]**2print(loss(np.array([1.0, 0.0]))) # → 0.5 yalnızca w1'in katkısıprint(loss(np.array([0.0, 1.0]))) # → 5.0 w2'nin katkısı: on katprint(loss(np.array([-4.5, 2.0]))) # → 30.125 aşağıdaki başlangıç noktası
Kontur çizgileri
Yüzeyi tepeden görmek için kontur çizgisi (contour line) çizeriz. Her çizgi aynı loss değerindeki noktaları birleştirir, tıpkı haritadaki yükselti çizgileri gibi.
Loss’un 2 olduğu çizgiyi bulalım. Bir parametreyi 0 yap ve öbürünü çöz:
Çanak yönünde uzun, yönünde dardır. Çizgi yönünde 2, yönünde yalnızca 0,63 birim uzanır.
Çizgiler yönünde, yönündekinden yaklaşık kat daha sıktır. Aynı loss’a yönünde çok daha kısa mesafede varırsın. Yani yüzey o yönde daha diktir.
# np: önceki bloklardanw1_edge = np.sqrt(2 / 0.5) # w2 = 0 iken 0.5 * w1**2 = 2w2_edge = np.sqrt(2 / 5) # w1 = 0 iken 5 * w2**2 = 2print(w1_edge, round(w2_edge, 2)) # → 2.0 0.63print(round(w1_edge / w2_edge, 1)) # → 3.2
Kısmi türev
Tek parametrede gradyan, loss’un türeviydi. İki parametrede her parametre için ayrı bir türev gerekir. Öbür parametreyi sabit sayıp yalnızca birine göre aldığın türeve kısmi türev (partial derivative)kısmi türev (partial derivative): Birden çok değişkenli bir fonksiyonun, öbür değişkenler sabit tutulup yalnızca bir değişkene göre alınan türevi. denir.
İki kısmi türev şöyle çıkar:
- sabitken ilk terimin türevi ‘dir. İkinci terim ‘e bağlı olmadığından türevi 0’dır.
- sabitken aynı hesap için verir.
Gradyan bu iki kısmi türevi bir vektörde toplar:
Birinci bileşen yönündeki eğimi, ikincisi yönündeki eğimi verir.
# np: önceki bloklardandef grad(w): return np.array([w[0], 10 * w[1]]) # [kısmi türev w1, kısmi türev w2]print(grad(np.array([-4.5, 2.0]))) # → [-4.5 20. ]
Gradyan vektörünün iki yanı
Vektörün iki önemli yanı var:
- Loss’un en hızlı arttığı yönü gösterir.
- O noktadan geçen contour line’ı dik keser.
Gradyanın tersine gitmek, bulunduğun noktada en dik inişi seçmektir. Ama en dik iniş, minimuma giden en kısa yol demek değildir.
İki parametrede bir adım
Başlangıç noktası ve olsun. İlk adım şöyle çıkar:
- Gradyanı bul: . Eğim yönünde çok daha diktir.
- Adımı bul: .
- Yeni noktayı bul: .
# np, loss ve grad: önceki bloklardanw = np.array([-4.5, 2.0])lr = 0.18g = grad(w) # → array([-4.5, 20. ])delta = -lr * g # → array([ 0.81, -3.6 ])w_new = w + delta # → array([-3.69, -1.6 ])print(loss(w), round(loss(w_new), 3)) # → 30.125 19.608
yalnızca ilerledi. ise birim sıçrayıp işaret değiştirdi; yani top vadinin öbür yamacına geçti. Loss yine de ‘ten ‘e düştü.
Uzun çanakta zikzak
Her yönün kendi çarpanı
Bu çanakta her kısmi türev yalnızca kendi parametresine bağlıdır ( ve ). Bu yüzden güncelleme iki ayrı çarpmaya ayrılır:
Tek parametrede çarpan idi. Burada her yönün kendi çarpanı var. Çarpanın mutlak değeri iki şeyi söyler:
- 1’e ne kadar yakınsa o yönde ilerleme o kadar yavaştır.
- 1’i aşarsa o yön ıraksar.
için çarpanlar ve ‘dir:
- her adımda %18 küçülür.
- her adımda işaret değiştirir ve mutlak değeri %20 küçülür.
Çarpanlarla atılan adım, gradyanla atılan adımın aynısıdır:
lr = 0.18w = np.array([-4.5, 2.0])# np ve grad: önceki bloklardanfactors = np.array([1 - lr, 1 - 10 * lr])print(factors) # → [ 0.82 -0.8 ]print(factors * w) # → [-3.69 -1.6 ] çarpanlarlaprint(w - lr * grad(w)) # → [-3.69 -1.6 ] gradyanla
Başlangıç noktasının etkisi
Aşağıdaki grafikte learning rate ‘de sabittir. Değiştireceğin şey başlangıç noktasıdır.
Topu sürükle ya da grafikte bir yere dokun: 30 adımlık yolun tamamı yeniden çizilir.
Top önce noktasından başlar. İlk sıçraması az önce hesapladığımız adımdır. Şunları dene:
Kayıp yüzeyi · loss surface · η = 0,18
adım 0 / 30 · w₁ = -4,500 · w₂ = 2,000 · kayıp = 30,125
- Topu farklı yerlere koy ve yolun contour line’larla kesişmesine bak. Her sıçrama, başladığı noktadaki contour line’a diktir.
- Topu ekseninin dışında herhangi bir yere koy. Top dar yönde zikzak çizer ve yine de yakınsar.
- her adımda ile çarpılır ve işaret değiştirir. ise ile çarpılır ve işareti korur. Durum satırında bunu kontrol et.
- Topu eksenine, yani çizgisine yaklaştır. Zikzak küçülür.
- Topu tam eksene koy: 0 çarpı yine 0’dır, top hiç sıçramaz ve eksen boyunca düz iner.
İki başlangıçtan ilk üç adım: biri eksenin dışında, biri eksenin üstünde.
# np ve grad: önceki bloklardanfor start in ([-4.5, 2.0], [-4.5, 0.0]): w = np.array(start) path = [w.round(2).tolist()] for _ in range(3): w = w - 0.18 * grad(w) path.append(w.round(2).tolist()) print(path)# → [[-4.5, 2.0], [-3.69, -1.6], [-3.03, 1.28], [-2.48, -1.02]]# → [[-4.5, 0.0], [-3.69, 0.0], [-3.03, 0.0], [-2.48, 0.0]]
Learning rate’e göre üç iniş
Bu grafik başka learning rate’leri göstermiyor, ama çarpanlar ne olacağını söyler. ‘den başlayan üç inişin 30 adım sonrası:
| çarpanı | çarpanı | 30 adım sonra | |
|---|---|---|---|
| 0,05 | 0,95 | 0,5 | , loss 0,466: tabanda sürünür |
| 0,18 | 0,82 | −0,8 | loss neredeyse 0: zikzak küçülür |
| 0,21 | 0,79 | −1,1 | her adımda %10 büyür: ıraksar |
- : top önce dar yönde hızla tabana iner, sonra uzun yönde sürünür.
- : çarpanların mutlak değeri 1’den küçüktür, sıçramalar küçülür.
- : yönü diye büyür. Tek bir yönün ıraksaması bütün inişi bozmaya yeter.
Üç inişi 30 adım boyunca çalıştır:
# np, loss ve grad: önceki bloklardandef descend_2d(w, lr, steps): for _ in range(steps): w = w - lr * grad(w) return wfor lr in [0.05, 0.18, 0.21]: w = descend_2d(np.array([-4.5, 2.0]), lr, 30) print(lr, w.round(3).tolist(), f"{loss(w):.3g}")# lr, 30 adım sonra w, loss# → 0.05 [-0.966, 0.0] 0.466# → 0.18 [-0.012, 0.002] 9.89e-05# → 0.21 [-0.004, 34.899] 6.09e+03
Zikzağın nedeni
Zikzak için iki şey birlikte gerekir.
- Birincisi: gradyanın tersi minimumu değil, bulunduğun noktadaki en dik inişi gösterir. Uzun bir çanakta bu yön çoğunlukla vadinin tabanına, yani dar yöne doğrudur.
- İkincisi: dar yönde adım fazla büyükse top tabanı aşıp karşı yamaca düşer. Bu çanakta bu, olduğunda, yani olduğunda gerçekleşir.
‘te ikinci koşul sağlanmaz, bu yüzden zikzak olmaz. Top önce tabana kayar, sonra uzun yönde sürünür.
İkinci koşulu dar yöndeki çarpanın işaretine bakarak denetle:
for lr in [0.05, 0.1, 0.18, 0.21]: narrow = 1 - 10 * lr print(lr, round(narrow, 2), narrow < 0)# lr, dar yöndeki çarpan, işaret değiştirir mi# → 0.05 0.5 False# → 0.1 0.0 False# → 0.18 -0.8 True# → 0.21 -1.1 True
Tek learning rate iki yöne uymaz
Tek bir learning rate iki yöne birden uymaz.
- Dar yöne uyan küçük bir oranla top uzun yönde yavaş ilerler.
- Uzun yönü hızlandıracak büyük bir oranla ise dar yönde zikzak çizer ya da ıraksar.
Aynı başlangıçtan loss’u ‘in altına indiren adım sayısına bak:
# np, loss ve grad: önceki bloklardandef steps_to_converge(lr, tol=0.01, max_steps=200): w = np.array([-4.5, 2.0]) for n in range(1, max_steps + 1): w = w - lr * grad(w) if loss(w) < tol: # loss tol'un altına indi mi return n return None # inemedifor lr in [0.05, 0.1, 0.18, 0.21]: print(lr, steps_to_converge(lr))# → 0.05 68# → 0.1 33# → 0.18 19# → 0.21 None
Learning rate’in üst sınırını da dar yön koyar. olması için gerekir. Uzun yönde hızlanmak için ‘yı bu sınırın üstüne çıkaramazsın, çünkü dar yön ıraksar.
Gerçek loss’larda
Gerçek loss’ların contour line’ları bu kadar düzgün elipsler değildir:
- Eksenleri eğik olabilir. O zaman her kısmi türev iki parametreye birden bağlıdır ve güncelleme iki ayrı çarpmaya ayrılmaz.
- Parametre sayısı ikiden çoktur.
Ama dar ve uzun vadiler orada da bulunur ve aynı zikzağa yol açar. Doğrusal regresyon dersinde böyle bir çanağı daha yuvarlak yapmanın bir yolunu göreceksin.
Ne zaman durmalı?
Gradient descent minimuma yaklaşır, ama varmaz. Pratikte iki durma koşulu kullanırsın:
- Bir adımda loss’un değişimi küçükse dur: . Mutlak değer yönü değil, büyüklüğü sayar.
- Belirli bir adım sayısına ulaştıysan dur. Bu, ıraksayan ya da çok yavaş ilerleyen bir eğitimin sonsuza kadar sürmesini engeller.
Dikkat
Sabit bir loss her zaman minimum demek değildir: ‘de loss hiç değişmez, oysa minimuma hiç yaklaşmaz.
İkisini birlikte kullanmak güvenlidir: hangisi önce gerçekleşirse orada durursun.
İki koşulu tek fonksiyonda birleştir ve dört oranla dene:
def descend(w0, lr, tol=1e-6, max_steps=100): w = w0 for steps in range(1, max_steps + 1): new_w = w - lr * 2 * w if abs(new_w**2 - w**2) < tol: # loss değişimi küçük return new_w, steps w = new_w return w, max_steps # adım sınırına ulaştınfor lr in [0.2, 1.0, 1.05, 0.01]: w, steps = descend(4.0, lr) print(lr, round(w, 4), steps)# lr, son w, adım sayısı# → 0.2 0.0007 17# → 1.0 -4.0 1# → 1.05 55122.4494 100# → 0.01 0.5305 100
Sonuçlar:
- : loss değişimi ‘nın altına inince 17. adımda durur.
- : loss değişmediği için ilk adımda durur, ama minimumdan uzaktır.
- ve : loss değişimi küçülmez, adım sınırı durdurur.
Gradyanı bilmiyorsan
Merkezi fark
Gradyanı bilmiyorsan, türevin tanımından yaklaşık değerini hesaplayabilirsin. ‘nin türevini tek satırda yazdık. Daha karmaşık bir fonksiyonda türevi elle çıkarmak zor olabilir.
Türevin tanımına dönersen, küçük bir için şunu yazabilirsin:
Buna merkezi fark (central difference) denir. Noktanın iki yanına eşit uzaklıkta bakar. Örnek olarak ve alalım. Kesin türev ‘dir.
def f(w): return w**3h = 1e-5print((f(2.0 + h) - f(2.0 - h)) / (2 * h)) # → 12.00000000021184, 12'den ancak 2e-10 uzak
İleri fark (forward difference) yalnızca bir yana bakar:
Aynı ile daha kötüdür.
print((f(2.0 + h) - f(2.0)) / h) # → 12.000060000261213, 12'den 6e-5 uzak‘yi küçültmek hatayı önce azaltır, sonra artırır. Büyük ‘de hata yaklaşma farkından gelir. Çok küçük ‘de ile birbirine çok yakın iki sayıdır ve farkları yuvarlama hatasıyla dolar.
- Merkezi farkta en iyi yaklaşık ‘tir.
- İleri farkta en iyi yaklaşık ‘dir ve hatası yine de çok daha büyüktür.
Aşağıda ‘yi 1’den ‘ye doğru kaydır.
L(w) = w³ · w = 2 · kesin türev L′(2) = 3w² = 12 · float64 ile hesaplanır
Secant teğete yaklaşır
Mutlak hata · log–log
h = 1 · merkezi fark − 12 = 1 · ileri fark − 12 = 7 h büyük: ileri fark 12 + 6h + h² verir, secant teğetten açıkça ayrılır. Merkezi secant teğete neredeyse paralel; farkı yalnızca h² kadar.
Birden çok parametrede
Birden çok parametrede aynı hesabı her parametre için ayrı ayrı yaparsın:
- Bir parametre seç ve öbürlerine dokunma.
- Parametreyi ilk değerinden kadar artır ve loss’u hesapla.
- Parametreyi ilk değerinden kadar azalt ve loss’u hesapla.
- Merkezi farkı uygula. Sonuç, o parametrenin kısmi türevinin yaklaşık değeridir.
Dört adım, bir numerical_gradient fonksiyonudur:
import numpy as npdef numerical_gradient(f, x, h=1e-5): x = np.array(x, dtype=float) grad = np.zeros_like(x) for i in np.ndindex(x.shape): # 1. bir parametre seç old = x[i] x[i] = old + h # 2. artır f_plus = f(x) x[i] = old - h # 3. azalt f_minus = f(x) x[i] = old # x[i]'yi eski değerine döndür grad[i] = (f_plus - f_minus) / (2 * h) # 4. merkezi fark return grad
Bu yöntem yavaştır, çünkü her parametre için loss’u iki kez hesaplar. Milyonlarca parametreli bir modeli bununla eğitemezsin.
Elle yazılmış gradyanı kontrol et
Sayısal gradyan, elle yazdığın gradyanı kontrol etmek için çok değerlidir. Elle türettiğin gradyanı sayısal gradyanla karşılaştır. İkisi birbirine çok yakınsa türevin doğrudur.
Çanağın gradyanını önce doğru, sonra yanlış yazılmış haliyle dene:
# np ve numerical_gradient: önceki bloklardandef bowl(w): return 0.5 * w[0]**2 + 5 * w[1]**2w = np.array([-4.5, 2.0])numeric = numerical_gradient(bowl, w)print(numeric.round(4)) # → [-4.5 20. ]right = np.array([w[0], 10 * w[1]]) # doğru türevwrong = np.array([w[0], 5 * w[1]]) # 10 yerine 5 yazdınprint(np.abs(right - numeric).max().round(6)) # → 0.0print(np.abs(wrong - numeric).max().round(6)) # → 10.0
Özet
- Gradient descent, parametreyi her adımda gradyanın tersine taşır: .
- Learning rate adımın büyüklüğüdür. Çok küçükse eğitim yavaşlar, çok büyükse loss büyür.
- Birden çok parametrede gradyan, kısmi türevlerden oluşan bir vektördür.
- Uzun bir çanakta tek bir learning rate iki yöne birden uymaz: dar yöne göre seçilen küçük oran uzun yönde yavaş kalır, büyük oran dar yönde zikzağa ya da ıraksamaya yol açar.
- Bir adımdaki loss değişiminin mutlak değeri çok küçüldüğünde ya da adım sınırına ulaştığında durursun.
- Gradyanı merkezi farkla yaklaşık olarak hesaplayabilir, elle yazdığın türevi bununla kontrol edebilirsin.
Terimler
| Türkçe | English |
|---|---|
| kayıp fonksiyonu | loss function |
| kayıp | loss |
| gradyan inişi | gradient descent |
| öğrenme oranı | learning rate |
| gradyan | gradient |
| türev | derivative |
| yakınsama | convergence |
| ıraksama | divergence |
| kayıp yüzeyi | loss surface |
| kontur çizgisi | contour line |
| kısmi türev | partial derivative |
| merkezi fark | central difference |
Şimdi sıra sende
Ders tamamlandı. Sıradaki: ilk problem.
- Gradyan inişi adımıZorluk: KolayBaşla →
- Yakınsayana kadar inZorluk: OrtaBaşla →
- Sayısal gradyanZorluk: OrtaBaşla →
Notların
Metinden bir bölüm seçip Not al'a basarak da ekleyebilirsin.
Bu derse dair söylemek istediğin bir şey var mı?