Veri Yönetimi

Birşey yapılması için veri depolamak ve veri işlemeye bir yol bulmamız gerekir. Genel olarak veri ile yapmamız gereken iki önemli şey vardır: (i) veriyi elde etmek, (ii) bilgisayara girdikten sonra verileri işlemek. Veri depolamanın bir yolu olmadan elde etmenin bir anlamı yok. Bu yüzden önce sentetik verileri ele alarak başlayalım. Başlamak için n-boyutlu dizi ortaya koyuyoruz. Numpy ve MXNet'te böyle bir diziye ndarray PyTorch ve TensorFlow da Tensor denir. Bu kitap boyunca ndarray ismini kullanıyoruz. ndarray bir sınıftır ve her örneğe bir ndarray diyoruz.

MXNet

Python'da en çok kullanılan bilimsel hesaplama paketi olan Numpy çalıştıysanız, bu bölümü tanıdık bulacaksınız. MXNet de ndarray bir sınıftır ve her hangi bir tensor(n boyurlu dizi) bir ndarray örneğidir. MXNet'in tensorleri, Numpy'ın tensorlerine(n boyutlu dizilerine) birkaç önemli özellik ile birlikte bir genişlemesidir. İlk olarak MXNet'in tensorleri CPU, GPU ve dağıtılmış bulut mimarileri üzerinde asenkron hesaplamaları desteklerken, Numpy sadece CPU hesaplamasını destekler. İkinci olarak MXNet'in tensorleri otomatik türevi destekler. Bu özellikler derin öğrenme için MXNet'in tensorlerini uygun hale getirir. Bu kitap boyunca tensorler dediğimizde aksini ifade etmedikçe MXNet'in ndarray örneklerinden söz ediyoruz.

Başlangıç

Bu bölümde, bu kitabın başından sonuna kadar ilerledikçe inşa edeceğiniz temel matematik ve sayısal hesaplama araçları ile sizi hazırlamak ve çalıştırmayı amaçlıyoruz. Matematik kavramlarının veya kütüphane fonksiyonlarının bazılarını anlamaya çalışıyorsanız eğer endişelenmeyin. Aşağıdaki bölümler pratik örnekler ile birlikte bu materyali yeniden ele alacaktır. Diğer yandan eğer biraz geçmişiniz varsa ve matematiksel içeriğin daha derinine inmek istiyorsanız, bu bölümü geçebilirsiniz.

Başlamak için MXNet'den np(numpy) ve npx(numpy_extension) modüllerini yükleyelim. Burada npx modülü Numpy gibi bir ortamda derin öğrenmeyi güçlendirmek için geliştirilmiş bir genişleme kümesi içerirken, np modülü Numpy tarafından desteklenen fonksiyonları içermektedir. Tensorleri kullanırken her zaman set_np() fonksiyonunu çağırırız. Bu MXNet'in diğer bileşenleri ile tensor işleme uyumu içindir.

pip install mxnet==1.6.0 #Öncelikle MXNet'i indiriyoruz.
from mxnet import np, npx
npx.set_np()

Bir tensor sayısal değerlere sahip bir dizi(muhtemelen çok boyutlu) temsil eder. Matematikte bir eksenli tensor bir vektöre karşılık gelir. İki eksenli bir tensor ise matrise karşılık gelir. İkiden fazla eksenli diziler matematiksel olarak bir isme sahip değildir. Biz bunlara tensor diyoruz.

Varsayılan değeri float olarak oluşturmasına rağmen, 0 ile başlayan ilk 12 tam sayıyı içeren bir satır vektörü olan x'i oluşturmak için 'arrange' fonksiyonunu kullanabiliriz. Bir tensordeki değerlerin hepsi tensorün bir elemanı olarak adlandırılır. Örneğin, x tensoründe 12 eleman vardır. Aksi belirtilmediği sürece yeni bir tensor ana hafızada depolanacak ve CPU tabanlı hesaplama için tasarlanacak.

x = np.arange(12)
x

Bir tensorün satır ve sutün sayısını öğrenmek için shape özelliğini kullanabiliriz.

x.shape

Sadece bir tensorün elemenlarının toplam sayısını bilmek istersek, size özelliğini kullanabiliriz. Burada bir vektörle uğraştığımız için shape fonksiyonunun değeri ile size fonksiyonunun değeri aynı sayıyı döndürür.

x.size

Bir tensorün eleman sayısını veya değerini değiştirmeden shape'ini değiştirmek için reshape fonksiyonunu kullanabiliriz. Örneğin, (12,) shape sahip x vektörünü, (3,4) shape li bir matrise dönüştürebiliriz. Bu yeni tensör tamamen aynı değerleri içerir. Ancak 3 satır ve 4 sütun olarak düzenlenmiş bir matris olarak görünürler. Yani tensörün shape i değişmesine rağmen içindeki değerler değişmez. Boyutun yeniden şekillendirilerek değiştirilmediğine dikkat edin.

x.reshape(3,4)

Her boyutu manuel olarak belirterek yeniden şekillendirmeye gerek yoktur. Hedef şeklimiz (yükseklik,genişlik) olan bir matris ise genişliği bildikten sonra yükseklik kapalı bir şekilde verilir. Neden bölmeyi kendimiz yapmak zorundayız? Yukarıdaki örnekte 3 satırlık bir martis almak için hem 3 satır hemde 4 sütunlu olması gerektiğini belirttik. Neyse ki tensörler satır sayısı verildiğinde sütun sayısını,sütun sayısı verildiğinde de satır sayısını hesaplayabilir. Bunu, tensörün otomatik olarak çıkarım yapmasını istediğimiz boyutu -1 ile değiştirerek sağlıyoruz. Yani, x.reshape(3,4) olarak şekillendirmek yerine x.reshape(3,-1) veya x.reshape(-1,4) olarak yeniden şekillendirebiliriz. Hepsinde de sonuç aynıdır. empty metodu bir yığın hafızayı tutar ve bize girdilerinin herhangi birinin değerini değiştirmek için uğraşmadan bir matris geri verir.

np.empty((3,4))

Tipik olarak, biz ya sıfırlar, birler, bazı diğer sabitler ya da belirli bir dağılımdan rasgele örneklenmiş sayılar ile başlatılmış matrisler isteyeceğiz. Tüm elemanları 0 ve (2,3,4) şeklindeki bir tensörü temsil eden bir tensörü aşağıdaki gibi oluşturabiliriz:

np.zeros((2,3,4))

Benzer şekilde, her elemanı 1 olan tensörleri aşağıdaki gibi oluşturabiliriz:

np.ones((2,3,4))

Çoğunlukla, bazı olasılık dağılımlarından bir tensördeki her elemanın değerini rasgele örneklemek isteriz. Örneğin, bir sinir ağındaki parametreler olarak kullanılacak dizileri oluşturduğumuzda, parametrelerin değerlerini alışıla geldik bir şekilde rasgele oluşturacağız. Aşağıdaki kod parçasında (3,4) şekle sahip bir tensör oluşur. Tensörün her elemanı ortalaması 0 ve standart sapması 1 olan Gaussian(normal) dağılım dan rasgele örneklenmiştir.

np.random.normal(0, 1, size= (3,4))

Sayısal değerler içeren bir Python listesi veya listeleri oluşturarak istenen tensördeki her eleman için kesin değerleri de belirtebiliriz. Burada en dıştaki liste 0 eksenine, içteki liste ise 1 eksenine karşılık gelir.

np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

İşlemler

Bu kitap yazılım mühendisliği ile ilgili değildir. İlgi alanlarımız sadece dizilerden veri okumak ve dizilerden veri yazmakla sınırlı değildir. Biz diziler üzerinde matematiksel işlemler yapmak istiyoruz. En kolay ve en kullanışlı işlemlerden biri elementwise işlemidir. Bu bir dizinin her elemanını standart bir skaler işleme tabi tutar. Girdi olarak iki dizi alan fonksiyonlar için elementwise işlemi iki dizideki her bir karşılık gelen eleman çiftine bazı standart ikili operatörler uygulanır. Skalerden skalere giden her bir fonksiyondan elementwise fonksiyon oluşturabiliriz.

Matematik notasyonu olarak bir girdili skaler operatorü $f:R→R$ şeklinde tanımlarız. Bu reel sayılardan reel sayılara giden bir fonksiyondur. Bunun aksine iki girdili ve bir çıktıya sahip binary skaler operatörü $f:R,R→R$ şeklinde tanımlarız. Aynı boyutlu $u$ ve $v$ vektörleri verilsin. $u_i$ ve $v_i$, $u$ ve $v$ vektörlerinin $i.$ elemanı olmak üzere her $i$ için $f(u_i,v_i)→c_i$ olacak şekilde $F(u,v)=c$ vektörünü oluştururuz. Burada skaler bir fonksiyonu elementwise vektör işlemine genişleterek $F:R^d,R^d→R^d$ vektör değerli operatörünü tanımladık. Standart aritmatik işlemler(+,-,*,/ ve **) herhangi özdeş boyutlu tensörler için elementwise işlemlerine genişletilmiştir. Aynı boyutlu iki vektör üzerinde elementwise işlemini uygulayabiliriz. Aşağıdaki örnekte, 5 elementwise işleminin sonucunu virgül kullanarak ayırdık.

x = np.array([1, 2, 4, 8])
y = np.array([2, 2, 2, 2])

x + y, x - y, x * y, x / y, x ** y

Elementwise olarak daha birçok işlem uygulanabilir. Örneğin exponansiyel alma işlemi.

np.exp(x)

Elemetwise hesaplamalarına ek olarak, vektör nokta çarpımını ve matris çarpımını içeren lineer cebir işlemlerinide ayrıca uygulayabiliriz. Lineer cebirin önemli kısımlarını Bölüm 2.3'de açıklayacağız. Ayrıca birden fazla tensörü, daha büyük bir tensör oluşturmak için onları uçtan uca ekleyerek birleştirebiliriz. Sadece tensörlerin listesini vermemiz ve sisteme hangi eksende birleştirileceğini söylememiz gereklidir. Aşağıdaki örnek satır ve sutun boyunca iki matris birleştirdiğimizde neyin meydana geldiğini gösterir. İlk çıktı tensörünün eksen-0 uzunluğu(6) iki girdi tensörlerinin eksen-0 uzunluklarının toplamı(3 + 3) olduğunu, ikinci çıktı tensörünün eksen-1 uzunluğu(8) iki girdi tensörlerinin eksen-1 uzunlukları toplamı(4 + 4) olduğunu görürüz.

X = np.arange(12).reshape(3,4)
Y = np.array([[2, 1, 4, 3],[1, 2, 3, 4],[4, 3, 2, 1]])

np.concatenate([X, Y], axis=0), np.concatenate([X, Y], axis=1)

Bazen mantıksal ifadeler aracılığıyla ikili tensör oluşturmak isteriz. Örnek olarak $X == Y$ 'yi alalım. Her konum için, $X$ ve $Y$ bu konumda eşitse yeni tensördeki ilgili giriş $1$ değerini alır. Yani $X == Y$ mantıksal ifadesi bu konumda true olur. Yoksa bu konumda $0$ değerini alır.

X == Y

Tensördeki bütün elemanları toplamak, bir elemanlı bir tensör verir.

X.sum()

Broadcasting Mekanizması

Yukarıdaki bölümlerde, aynı boyutlu iki tensör üzerinde elementwise işlemlerinin nasıl uygulandığını gördük. Belirli şartlar altında, boyutlar farklı olduğunda bile broadcasting mekanizmasından yardım alarak hala elementwise işlemlerini uygulayabiliriz. Bu mekanizma şu şekilde çalışır: İlk olarak, elemanları uygun şekilde kopyalayarak dizilerden birini veya her ikisini genişletin, böylece bu dönüşümden sonra iki tensör aynı boyuta sahip olur. İkinci olarak sonuç dizisi üzerinde elementwise işlemlerini uygulayabiliriz.

a = np.arange(3).reshape(3, 1)
b = np.arange(2).reshape(1, 2)
a, b

a ve b sırasıyla $3 × 1$ ve $2 × 1$ matrisler olduğundan dolayı biz onları toplamak istersek onların boyutları eşleşmez. Her iki matrisin girişlerini aşağıdaki gibi daha büyük bir $3 × 2$ matrise broadcast ediyoruz. a ve b matrislerini elementwise toplamadan önce a matrisi için sütunları, b matrisi için satırları kopyalar.

a + b

İndexleme ve Dilimleme

Tıpkı diğer herhangi bir Python dizisinde olduğu gibi bir tensördeki elemanlara indeks ile ulaşılabilir. Herhangi bir Python dizisinde olduğu gibi ilk elemanın indeksi 0'dır ve aralıklar, ilk ancak son elemandan önce olacak şekilde belirtilir. Standart Python listelerinde olduğu gibi negatif indisler kullanarak göreceli olarak listenin sonunda bulunan elemanlara erişebiliriz.

Böylece aşağıdaki gibi $[-1]$ son elemanı ve $[1,3]$ ise ikinci ve üçüncü elamanı seçer.

X[-1], X[1:3]

Okumanın ötesinde, indeksler belirterek matrisin elemanlarını da yazdırabiliriz.

X[1, 2] = 9
X

Birden çok elemana aynı değeri atamak istiyorsak, onların hepsini kolaylıkla indeksleriz ve daha sonra değeri atarız. Örneğin; $[0:2, :]$ birinci ve ikinci satıra erişir. ':' işareti eksen-1(sütun) boyunca bütün elemanları alır. Matrisler için indekslemeyi tartışırken, bu tabi ki vektörler ve 2'den fazla boyuttaki tensörler için de işe yarar.

X[0:2, :] = 12
X

Hafıza Tasarrufu

Çalışan işlemler, sonuçları barındırmak için yeni belleğin tahsis edilmesine neden olabilir. Örneğin; eğer $Y = X + Y$ yazarsak, $Y$ 'nin işaret ettiği tensörü kaldıracağız ve bunun yerine yeni ayrılan bellekteki $Y$ 'yi işaret edeceğiz. Aşağıdaki örnekte, bunu Python daki $id()$ fonksiyonu ile gösteriyoruz. Bu fonksiyon bize bellekte referans verilen nesnenin tam adresini verir. $Y = X + Y$ tanımlandıktan sonra $id(Y)$ 'nin farklı bir lokasyona işaret ettiğini göreceğiz. Bunun nedeni, Python'un önce $Y + X$ 'i değerlendirmesindendir, sonuç için yeni bellek ayırır ve ardından $Y$ 'yi bellekteki bu yeni konuma koyar.

before = id(Y)
Y = Y + X
id(Y) == before

Bu iki nedenden dolayı istenmeyen olabilir. İlk olarak, her zaman gereksiz yere bellek ayıtmakla uğraşmak istemiyoruz. Makine öğrenmesinde, yüzlerce megabayt parametrelereye sahip olabiliriz ve bunların hepsini her saniyede birçok kez güncelleyebiliriz. Alışılan şekilde bu güncellemeleri yerinde yapmak isteyeceğiz. İkinci olarak, birden çok değişkenden aynı parametrelere işaret edebiliriz. Eğer yerinde güncellemezsek, referanslar hala eski bellek konumuna işaret edecek ve bu kodumuzun bazı kısımlarının yanlışlıkla eski parametrelere başvurmasını mümkün kılacak. Neyse ki, yerinde işlem yapmak kolaydır. Dilim notasyonu ile önceden ayrılmış diziyi bir işlemin sonucuna atayabiliriz. Örneğin; $Y[:]=<ifade>$. Bu kavramı örneklemek için ilk olarak başka bir $Y$ ile aynı boyuta sahip yeni bir $Z$ matrisi oluştururuz. Bu Z matrisini 'zeros_like' fonksiyonunu kullanarak oluştururuz ve bu bize $0$ girdili bir blok tahsis eder.

Z = np.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))

$X$ 'in değeri sonraki hesaplamalarda tekrar kullanılmazsa işlemin bellek yükünü azaltmak için $X[:] = X + Y$ veya $X += Y$ notasyonları da ayrıca kullanılabilir.

before = id(X)
X += Y
id(X) == before

Diğer Python Nesnelerine Dönüştürme

Numpy tensörüne dönüştürmek veya tam tersi kolaydır. Dönüştürülen sonuç hafızayı paylaşmaz. Bu küçük sıkıntı aslında oldukça önemli: CPU veya GPU'lar üzerinde işlemler gerçekleştirdiğinizde, Python'un Numpy paketinin aynı bellek parçasına sahip başka birşey yapmak isteyip istemediğini görmeyi bekleyerek hesaplamayı durdurmak istemezisin.

A = X.asnumpy()
B = np.array(A)
type(A), type(B)

1-boyutlu bir tensörü Python skalerine dönüştürmek için, $item$ fonksiyonunu veya Python'un gömülü fonksiyonlarını çağırabiliriz.

a = np.array([3.5])
a, a.item(), float(a), int(a)

Özet

Derin öğrenmede veri depolamak ve yönetmek için ana arayüz tensördür(n-boyutlu dizi). Bu, temel matematik işlemleri, broadcasting, indeksleme, dilimleme, hafıza tasarrufu ve diğer Python nesnelerine dönüştürme gibi birçok işlevsellik sağlar.

Alıştırmalar

$1)$ Bu bölümdeki kodları çalıştır. Bu bölümdeki $X == Y$ koşullu ifadesini $X < Y$ veya $X > Y$ ile değiştir ve daha sonra ne çeşit bir tensör elde ettiğinizi görün.

$2)$ Broadcasting mekanizmasındaki elemanla çalışan iki tensörü başka boyutlu tensörler ile değiştirin. Örneğin; 3-boyutlu tensötler ile. Sonuç beklendiği gibi mi?