4.1. Array Nesneleri ve Vektörizasyon

Kayıt Tarihi:

Son Güncelleme:

Özet:

Bu derste NumPy paketini ve array nesnelerini tanıyacaksınız. Bir fonksiyonu vektörize etmenin çeşitli yollarını da göreceksiniz, vektörizasyon işlemi bir sonraki bölümde ele alacağımız grafik çizme işlemi için bir gerekliliktir.

Anahtar Kelimeler: array · clock · linspace · logical_and · ndarray · numpy · ones · ones_like · time · vectorize · vektörizasyon · where · zeros · zeros_like

NumPy Array Nesneleri

NumPy paketinin bize sunduğu en temel öge, array nesneleridir (numpy.ndarray, n-dimensional array). Bu nesneler daha önce çalıştığımız list nesnesine çok benzerdir, fakat aşağıdaki temel farklılıklara sahiptir.

  • Her elemanı aynı tipte bir nesne olmalıdır; sıklıkla int, float ve complex.
  • Array nesnesi oluşturulurken eleman sayısı belirli olmalıdır.
  • Array nesneleri pythonda standart olarak bulunmaz, bu nesnelere Numerical Python (NumPy) paketinden ulaşırız.

Bu özellikler üzerine şunları da vurgulamamız gerekir. Öncelikle Python'da standart olarak gelen bir array nesnesi daha vardır, fakat bilimsel hesaplamalarda NumPy array nesneleri kadar kullanışlı değildir ve bunları bu metinde hiç kullanmayacağız. İkincisi de, aslında array nesnelerinin eleman sayıları daha sonra değiştirilebilir fakat bu durumda nesnede saklanan tüm veriler kaybedilir.

Anlaşılacağı üzere array nesneleri aslında list nesneleri kadar esnek değil, bazı ciddi kısıtlamaları var. O halde neden bu nesnelere ihtiyacımız olabilir? Çok yakında gözlemleyeceğimiz gibi list veya tuple verileri yerine bu nesneler üzerinde hesaplama yapmak programımızı çok daha verimli yapar. Bilimsel hesaplamalar yapan bir programda genellikle sayılardan oluşan çok büyük uzunlukta listelerle çalışırız, bunların üzerinde matematiksel operasyonlar yapıp sonuçları manipüle ederiz. Örnek olarak içinde sayılar olan ve 100.000 elemandan oluşan bir list nesnesi üzerinde matematiksel operasyonlar ($\sin, \exp, \log$ gibi) yardımıyla hesaplamalar yaptığınızı düşünün. Bunları yaparken 100.000 adımdan oluşan döngüler kuracaksınız ve bu hesaplamaları tamamlamak uzun bir süre alabilir. Bu verileri array nesnelerinde saklayarak aynı hesaplamaları tekrar denerseniz çok daha kısa sürede tamamlandığına şahit olursunuz.

Programlarımızda bir fonksiyon ile çalışırken fonksiyon verilerini bir nesne olarak kaydedip üzerinde işlemler yaparız. Örneğin $$f(x):=\text{e}^{-x^2}\sin(x)$$ fonksiyonunun $[-4, 4]$ aralığındaki verilerini hesaplayıp saklayalım. Bir fonksiyonun verisi demek belirli sayıdaki $x$ koordinatları ve bunlara karşılık gelen $y=f(x)$ değerleri demektir. Örnek olarak 10 nokta kullanarak bu verileri hesaplayıp saklayalım. Bunun için iki ayrı liste kullanacağız, daha sonra da bu verileri yazdıracağız.


from math import exp, sin

f = lambda x: exp(-x**2)*sin(x)
a = -4.0; b = 4.0; h = (b - a)/10.0
X = [-4 + i*h for i in range(11)]
Y = [f(x) for x in X]

for x, y in zip(X, Y):
    print "x=%4.1f, f(x)=%g" % (x, y)

Bu programı çalıştırırsak aşağıdaki çıktıyı alırız.


Terminal > python array1.py
x=-4.0, f(x)=8.51669e-08
x=-3.2, f(x)=2.08471e-06
x=-2.4, f(x)=-0.00212846
x=-1.6, f(x)=-0.0772718
x=-0.8, f(x)=-0.378256
x= 0.0, f(x)=0
x= 0.8, f(x)=0.378256
x= 1.6, f(x)=0.0772718
x= 2.4, f(x)=0.00212846
x= 3.2, f(x)=-2.08471e-06
x= 4.0, f(x)=-8.51669e-08

Şimdi aynı işlemleri bir de array nesneleri kullanarak yapacağız. Bir numpy.ndarray nesnesini oluşturmanın çeşitli yolları vardır. Bunlardan birisi, önce bir list nesnesi oluşturup sonra bunu numpy.array() fonksiyonunu kullanarak bir array nesnesine dönüştürmektir.


>>> a = -4.0
>>> b = 4.0
>>> h = (b - a)/10.0
>>> X = [-4 + i*h for i in range(11)]
>>>
>>> type(X)
type 'list'>
>>> import numpy as np
>>> X1 = np.array(X)
>>> type(X1)
type 'numpy.ndarray'>

Bir aralıkta düzgün dağılmış sayıların bir array nesnesini oluşturmak çok sık kullanılan bir işlemdir ve bunun kısa bir yolu vardır. NumPy içindeki linspace(a, b, n) komutuyla $[a,b]$ aralığı içinde düzgün dağılımlı $n$ tane nokta içeren bir array nesnesi oluşturulur.


>>> X2 = np.linspace(-4, 4, 10)
>>> type(X2)
type 'numpy.ndarray'>

Şimdi bu array nesnesini kullanarak $f(x)$ değerlerini içeren bir array oluşturalım. Bunun için şöyle bir yol izlenebilir, önce bir array nesnesi oluşturup bunun elemanlarını bir döngü ile değiştirebiliriz. Bu en pratik yol değildir ama array nesnelerini anlamamıza yardımcı olacak. Python'da boş array nesnesi oluşturup sonradan eleman ekleyemeyiz, çünkü array nesnelerinin uzunlukları değişirse verileri kaybolur. Bunun yerine belirli uzunlukta sabit bir array oluşturup elemanları güncellemeliyiz, bu array oluşturma işini np.zeros(n) komutuyla yaparız. Bu komut, $n$ tane 0.0 (float) sayısından oluşan bir array nesnesi üretir. Farklı tipten verilerle bunu yapmak için np.zeros(10, dtype=int) gibi bunu belirtiriz. Eğer önceden oluşturduğumuz bir array nesnesi ile aynı yapıda ve uzunlukta sıfır array oluşturmak istersek bunu np.zeros_like(x) komutuyla yaparız, bu komut x array nesnesi ile aynı veri yapısına ve uzunluğuna sahip olan fakat sıfırla dolu bir array üretir. Bunları sıfır yerine bir sayılarıyla yapan ve aynı şekilde çalışan np.ones() ve np.ones_like() komutları da vardır.


>>> Y2 = np.zeros(10)
>>> print Y2
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
>>> A = np.zeros(5, dtype=int)
>>> print A
[0 0 0 0 0]
>>> B = np.ones_like(Y2)
>>> print B
[ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]
>>> C = np.ones_like(A)
>>> print C
[1 1 1 1 1]

Şimdi bu array nesnesinin elemanlarını güncelleyeceğiz. Bir array nesnesini tıpkı bir list nesnesi gibi indisleyebilir ve dilimleyebiliriz. Fakat dilimleme konusunda bir uyarı yapmamız gerekiyor. Array nesnesinden alınan dilimler yeni bir nesnede saklanmaz, sadece orijinal nesneye atıf verir. Yani alınan dilimde bir değişiklik yaparsak bundan dilimi aldığımız nesne de etkilenir. Bunun dışında listelerdeki dilimleme ve indisleme ilgili her işlem array nesneleri için de geçerlidir. Diğer yandan array nesnelerinin list nesnelerinde olmayan çok daha gelişmiş dilimleme ve indisleme yöntemleri de mevcuttur, bunları daha sonra öğreneceğiz.


>>> x = np.linspace(0, 1, 11)
>>> print x
[ 0.   0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  1. ]
>>> x[5]
0.5
>>> x[2:7]
array([ 0.2,  0.3,  0.4,  0.5,  0.6])
>>> A = x[3:]
>>> A
[ 0.3  0.4  0.5  0.6  0.7  0.8  0.9  1. ]
>>> A[-1] = 0.0
>>> A
[ 0.3  0.4  0.5  0.6  0.7  0.8  0.9  0. ]
>>> x
[ 0.   0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  0. ]

Sonuç olarak yukarıdaki programı list yerine array nesneleriyle yeniden yazarsak aşağıdaki gibi olacaktır ve aynı çıktıyı üretecektir.


from math import exp, sin
import numpy as np

f = lambda x: exp(-x**2)*sin(x)
a = -4.0; b = 4.0; h = (b - a)/10.0
X = np.linspace(-4, 4, 10)
Y = np.zeros_like(X)

for i in xrange(10):
    Y[i] = f(X[i])

for x, y in zip(X, Y):
    print "x=%4.1f, f(x)=%g" % (x, y)

Vektörizasyon

Programlarımızda matematiksel fonksiyonları NumPy array nesnelerine kayıt ederken kullandığımız döngüler verimliliği düşürür. En az sayıda döngü kullanmalı, mümkünse hiç kullanmamalıyız. Array nesneleri üzerinde işlem yaparken döngülere başvurmak yerine NumPy paketinde bulunan array nesneleri ile çalışmak için tanımlanmış hazır fonksiyonları kullanmalıyız.

Aşağıdaki Python oturumunda da göreceğimiz gibi bir $f$ fonksiyoununun bir x array nesnesindeki sayılardaki değerlerinden oluşan bir y array nesnesi oluşturmak için döngü kullanmak zorunda değiliz. Çoğu durumda bu işlemi yapmak için sadece y = f(x) komutu yeterlidir.


>>> import numpy as np
>>> f = lambda x: x**2 -3*x + 2
>>> x = np.linspace(0, 3, 10)
>>> x
>>> x = np.linspace(0, 3, 5)
>>> x
array([ 0.  ,  0.75,  1.5 ,  2.25,  3.  ])
>>> y = f(x)
>>> y
array([ 2.    ,  0.3125, -0.25  ,  0.3125,  2.    ])

Yani bir fonksiyona argüman olarak bir array nesnesi verdiğimizde o array nesnesinin her bir elemanına fonksiyon uygulanır ve sonuçta bir array nesnesi döndürülür. Bu durum, array nesnelerinin büyük bir avantajıdır. Fakat bu yöntemi kullanırken dikkat edilmesi gereken bir kaç husus var. Örnek olarak bu bölümün başında ele aldığımız $f(x):=\text{e}^{-x^2}\sin(x)$ fonksiyonunu düşünelim.


>>> import numpy as np
>>> from math import exp, sin
>>> f = lambda x: exp(-x**2)*sin(x)
>>> x = np.linspace(-4, 4, 100)
>>> y = f(x)
Traceback (most recent call last):
File "", line 1, in 
File "", line 1, in 
TypeError: only length-1 arrays can be converted to Python scalars

Gördüğümüz gibi bu fonksiyonda bu işlem işe yaramadı. Bunun sebebi, math modülündeki fonksiyonların array nesneleri ile çalışacak şekilde tanımlanmamış olmasıdır. Onlar float nesneleri ile çalışacak şekilde tanımlanmıştır. Bu sorunun oldukça basit bir çözümü vardır. NumPy paketi içinde de sin, cos, sqrt, exp, log gibi matematiksel fonksiyonlar tanımlanmıştır ve bunlar array nesneleri ile uyumludur. Üstelik bu fonksiyonlar sadece array nesneleri ile değil, aynı zamanda float nesneleri ile de çalışabilir. Sonuç olarak bu örnekte exp ve sin fonksiyonlarını math yerine numpy paketinden almalıyız.


>>> import numpy as np
>>>
>>> f = lambda x: np.exp(-x**2)*np.sin(x)
>>> x = np.linspace(-4, 4, 10)
>>> y = f(x)
>>> y
array([  8.51669010e-08,  -1.90733964e-06,  -5.69932230e-03,
	-1.64270447e-01,  -3.52888753e-01,   3.52888753e-01,
         1.64270447e-01,   5.69932230e-03,   1.90733964e-06,
	-8.51669010e-08])

Bir fonksiyon değerlerini bu örnekte olduğu gibi hiç döngü kullanmadan array nesnelerinde saklamak işine fonksiyonu vektörize etmek denir. Ayrıca bir işlemi döngü kullanmadan sadece array operasyonları ile yapan kodlar vektörize kodlamaya örnektir, aksi duruma skaler kodlama denir. Programların maksimum verimde ve hızda çalışmaları için vektörize kodlama ile yazılması gerekir.

Şimdi skaler kodlama ile vektörize kodlama arasındaki performans farkını gözlemleyelim. Bunun için skaler kodlanmış fonksiyon ile vektörize kodlanmış fonksiyonun çalışmasında bilgisayar işlemcisinin ne kadar süre kullanıldığını ölçeceğiz. Python'da time modülü içinde bulunan clock() fonksiyonu ile programın başlatıldığı andan itibaren merkezi işlemcinin (CPU) saniye cinsinden ne kadar süre meşgul edildiği öğrenilebilir. Dolayısıyla ilgili kodlar öncesinde ve sonrasında bu CPU zamanlarını alıp birbirinden çıkarırsak o kodların ne kadar CPU süresinde çalıştığını anlamış oluruz.


import numpy as np
from math import exp, sin

def h1(f, a, b, k):                 #skaler kodlama
    h = (b - a)/k
    x = [a + i*h for i in range(k)]
    y = [f(a) for a in x]
    s = sum(y)

def h2(f, a, b, k):                 #vektorize kodlama
    x = np.linspace(a, b, k)
    y = f(x)
    s = np.sum(y)

Yukarıda tanımlanan iki fonksiyon da aynı işlemi yapıyor, f fonksiyonunun $[a,b]$ aralığında k tane değerinin toplamını hesaplıyor. Toplama işlemini sum() fonksiyonu yardımıyla yapıyoruz, bu fonksiyon verilen list nesnesinin tüm elemanlarının toplamını döndürür. Bu fonksiyon array nesneleri ile de çalışır fakat NumPy paketinde bulunan np.sum() fonksiyonu array nesneleri üzerinde daha verimli çalışır.


import time

k = 10000000
f1 = lambda x: exp(-x**2)*sin(x)
f2 = lambda x: np.exp(-x**2)*np.sin(x)

t0 = time.clock()       #cpu zaman baslangic
h1(f1, -4, 4, k)        #skaler hesap
t1 = time.clock()       #cpu zaman bitis
print "Skaler Kodlama\ncpu zaman: %f" % (t1 - t0)

t2 = time.clock()       #cpu zaman baslangic
h2(f2, -4, 4, k)        #vektorize hesap
t3 = time.clock()       #cpu zaman bitis
print "Vektorize Kodlama\ncpu zaman: %f" % (t3 - t2)

Yukarıdaki kodlarda bu bölümde ele aldığımız fonksiyonun $[-4, 4]$ aralığında 10.000.000 noktada değerlerinin toplamını hesaplıyoruz ve geçen zamanı hesaplatıyoruz. Programın çıktısı aşağıdaki gibi olacaktır.


Terminal python array3.py
Skaler Kodlama
cpu zaman: 7.236636
Vektorize Kodlama
cpu zaman: 0.641086

Aradaki bariz performans farkını görüyorsunuz. Bu kadar büyük array nesneleri ile çalışmayabilirsiniz, ama ortalama büyüklükteki arraylar ile yapılan hesapları üst üste koyarsanız sıradan bir programda benzer bir iş yükü oluşacaktır. Burada ölçülen zaman bilgisi çalıştığınız bilgisayarın donanımına, işletim sistemine ve anlık olarak çalışan yazılımlara bağlı olarak değişebilir.

Gelişmiş Vektörizasyon Teknikleri

Şu ana kadar öğrendiğimiz yöntemlerle if-else bloklarını içeren fonksiyonları vektörize edemeyiz. Örnek olarak daha önce incelediğimiz ve Heaviside fonksiyonu olarak bilinen $$ H(x)= \left\{ \begin{array}{ll} 0, & \quad x < 0\\ 1, & \quad x\geq 0 \end{array} \right. $$ fonksiyonunu düşünelim.


>>> import numpy as np
>>> def H(x):
...     return (0 if x < 0 else 1)
...
>>> x = np.linspace(-2, 2, 5)
>>> x
array([-2., -1.,  0.,  1.,  2.])
>>> y = H(x)
Traceback (most recent call last):
File "", line 1, in 
File "", line 2, in H
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Burada karşılaştığımız sorunun kaynağı şudur, H fonksiyonundaki if-else kısmında bulunan x < 0 bool ifadesini sonucu bir tek bool nesnesi değildir. Bu ifadenin sonucunda, x bir array nesnesi olduğundan, True veya False değerlerinden oluşan bir array nesnesi oluşur.


>>> import numpy as np
>>> x = np.linspace(-2, 2, 5)
>>> x
array([-2., -1.,  0.,  1.,  2.])
>>> x < 0
array([ True,  True, False, False, False], dtype=bool)
>>> x >= 0
array([False, False,  True,  True,  True], dtype=bool)

Dolayısıyla if-else blokları içeren fonksiyonları vektörize etmek için bunları uygun şekilde düzenlemeliyiz. Bunun akla gelen ilk yöntemi bir döngü ile array nesnesinin tüm elemanlarını güncellemektir.


import numpy as np

def H(x):
    return (0 if x < 0 else 1)

def Hv1(x):
    r = np.zeros_like(x)
    for i in range(len(x)):
        r[i] = H(x[i])
    return r

x = np.linspace(-2, 2, 5)
print x
print Hv1(x)

Yukarıdaki programda H fonksiyonunu manuel olarak vektörize eden başka bir Hv1 fonksiyonu tanımladık. Programın çıktısı aşağıdaki gibi olacaktır.


Terminal > python array5.py
[-2. -1.  0.  1.  2.]
[ 0.  0.  1.  1.  1.]

Bu yaklaşımın hiç bir zorluğu olmasa da kodlarda açık olarak bulunan döngü programımızın verimliliğini düşürür. Döngüler yerine aynı işi yapan hazır fonksiyonları kullanmalıyız. NumPy modülünde bulunan where() fonksiyonunu bu iş için kullanabiliriz. Bir kosul array nesnesi bool değerler içersin (x < 0 gibi), A ve B de iki array nesnesi olsun. Bu üç array nesnesi aynı sayıda eleman içeriyorsa r = where(kosul, A, B) kullanımı ile yeni bir r array nesnesi oluşturulur; eğer kosul[i] değeri True ise r[i]=A[i], aksi durumda r[i]=B[i] olur. Burada A ve B nesneleri array yerine float olarak da verilebilir. Aşağıdaki program da aynı çıktıyı üretecektir.


import numpy as np

def Hv2(x):
    return np.where(x < 0, 0.0, 1.0)

x = np.linspace(-2, 2, 5)
print x
print Hv2(x)

Bu işlemin daha pratik bir yolu daha vardır; buna bool indisleme (boolean indexing) denir. Genel olarak bir array nesnesi başk bir bool array nesnesi ile indislenebilir. Bu yöntemin ana fikri şudur; bir a array verisi başka bir b array verisi ile a[b] biçiminde indislenebilir. Bunun sonucunda yeni bir array verisi oluşur, bunun elemanları b[i] True olan a[i] elemanlarıdır. Dolayısıyla a[b]= c gibi atama ile a array nesnesinin b[i]=True olan elemanlarını a nesnesine yerleştiririz.


>>> import numpy as np
>>> x = np.linspace(-2, 2, 5)
>>> x
array([-2., -1.,  0.,  1.,  2.])
>>> a = x >= 0
>>> a
array([False, False,  True,  True,  True], dtype=bool)
>>> x[a]
array([ 0.,  1.,  2.])
>>> x[a] = 3
>>> x
array([-2., -1.,  3.,  3.,  3.])

Sonuç olarak Heaviside fonksiyonunu aşağıdaki biçimde de vektörize edebiliriz. Bool indisleme yöntemi ile vektörizasyon diğerlerine göre daha hızlı çalışır.


import numpy as np

def Hv3(x):
    r = np.zeros_like(x)
    r[x >= 1] = 1
    return r

x = np.linspace(-2, 2, 5)
print x
print Hv3(x)

Başka bir örnek olarak daha önce incelediğimiz düzeltilmiş Heaviside fonksiyonu olarak adlandırılan ve $$ H_\epsilon(x)= \left\{ \begin{array}{ll} 0, & \quad x<-\epsilon\\ \frac{1}{2}+\frac{x}{2\epsilon}+\frac{1}{2\pi}\sin\left( \frac{\pi x}{\epsilon} \right), & \quad -\epsilon\leq x\leq\epsilon\\ 1, & \quad x>\epsilon \end{array} \right. $$ olarak tanımlanan fonksiyonu ele alalım. Bu fonksiyonu aşağıdaki gibi tanımlayabiliriz.


import numpy as np

def H_eps(x, epsilon=1E-5):
    if x < -epsilon:
        return 0
    elif -epsilon <= x <= epsilon:
        return 0.5+x/(2.0*epsilon)+1/(2*np.pi)*np.sin(np.pi*x/epsilon)
    else:
        return 1

Bu fonksiyonu farklı şekillerde aşağıdaki vektörize edebiliriz. Öncelikle açık bir döngü kullanarak aşağıdaki kodları yazabiliriz.


def H_eps_v1(x, epsilon=1E-5):
    r = np.zeros_like(x)
    for i in range(len(x)):
        r[i] = H_eps(x[i])
    return r

İkinci bir seçenek olarak np.where() fonksiyonunu kullanabiliriz. Fakat burada dikkat etmemiz gereken bir nokta var. Koşullardan birini kodlarken -epsilon <= x <= epsilon ifadesini yazmalıyız, bu ise -epsilon <= x and x <= epsilon komutunu çalıştırır. Fakat and operatörü standart olarak array nesneleriyle çalışmaz, bunun yerine NumPy içinde bulunan logical_and() fonksiyonunu kullanmalıyız. np.logical_and(a, b) ifadesi a and b demektir.


def H_eps_v2(x, epsilon=1E-5):
    k1 = x < -epsilon
    k2 = np.logical_and(-epsilon <= x, x <= epsilon)
    k3 = x > epsilon

    v = 0.5 + x/(2.0*epsilon) + 1/(2*np.pi)*np.sin(np.pi*x/epsilon)

    r = np.zeros_like(x)
    r = np.where(k1, 0, r)
    r = np.where(k2, v, r)
    r = np.where(k3, 1, r)

    return r

Son olarak bool indisleme yöntemini kullanalım. Aşağıdaki programı inceleyin, yukarıdaki ile aynı işi aynı mantıkla yapar, çok ufak bir farklılığı var.


def H_eps_v3(x, epsilon=1E-5):
    k1 = x < -epsilon
    k2 = x > epsilon

    r = 0.5 + x/(2.0*epsilon) + 1/(2*np.pi)*np.sin(np.pi*x/epsilon)

    r[k1] = 0
    r[k2] = 1

    return r

Bunların çıktısını aşağıdaki gibi kontrol edersek hepsinin aynı çıktıyı verdiğini gözlemleriz.


x = np.linspace(-0.5, 0.5, 10)
print [H_eps(xx) for xx in x]
print H_eps_v1(x)
print H_eps_v2(x)
print H_eps_v3(x)

Son olarak bir vektörizasyon tekniğinden daha bahsedeceğiz. Bir fonksiyonu otomatik olarak vektörize etmek için aslında sadece np.vectorize(H_eps) komutu yeterlidir.


H_eps_v = np.vectorize(H_eps)
x = np.linspace(-0.5, 0.5, 10)
print H_eps_v(x)

Peki, madem bu kadar basit bir yolu var, neden diğer vektörizasyon tekniklerine ihtiyaç duyarız? Otomatik vektörizasyon çok pratik olmasına rağmen diğerleri gibi verimli değildir, özellikle bool indisleme ve where() tekniğine göre belirgin şekilde yavaş çalışır. Bu yöntemle vektörize edilen fonksiyonlarda array nesnelerinin getirdiği verimlilikten faydalanamayız. Dolayısıyla bu yöntemi en son çare olarak kullanmalıyız.

Önceki Ders Notu:
3.3. Python'da Modüller
Dersin Ana Sayfası:
Python ve Bilimsel Hesaplama
Sonraki Ders Notu:
4.2. Grafik Çizimi