6 Ekim 2014 Pazartesi

Numpy: Vektörleştirme ve Dilimleme


Bilimsel hesaplamalarda ekseriyetle büyük veri kümeleri üzerinden işlem yapılır. Klasik programlamada, bu türden hesaplamalar liste halindeki sayısal değerlerin döngü içerisinde birer birer çağrılmasıyla yapılır. Ancak günümüzde ise mümkün mertebe döngü kullanımından kaçınılmaktadır. Hatta büyük firmalar tarafından bu amaca yönelik optimize edilmiş program arayüzleri ve kütüphaneler ticari olarak geliştirilmektedir (Matlab, Intel MKL). Bu yazımızda da Python'un Numpy kütüphanesi sayesinde dizi halindeki sayısal veriler üzerinden nasıl hızlı ve basit işlemlerimizi gerçekleştirebileceğimizden bahsedeceğiz. 

Vektörleştirme

Girişte de bahsettiğimiz gibi bir matematiksel işlemi çoğunlukla dizi halindeki verinin tüm elemanları üzerinden gerçekleştirmek isteriz. Bu durumda işlemi for döngüsü içerisinde dizinin herbir elemanı için teker teker yapmak yerine Numpy kütüphanesi sayesinde tek değer üzerinden işlem yapıyormuş gibi gerçekleştirebiliriz. Şimdi aşağıdaki örnekte, polinom ve trigonometri ifadeleri içeren bir hesaplamayı hem döngü ile hem de vektörel formda yapıp karşılaştıralım.
>>> import time as tm   # süre tutma için gerekli olan modül
>>> import numpy as np  # Numpy kütüphanesi 'np' ön adıyla ekliyoruz
>>>
>>> N = 10000               # dizinin eleman uzunluğu
>>> x = np.random.random(N) # argüman dizisi
>>> y = np.zeros_like(x)    # sonuç dizisi
>>> 
>>> t0 = tm.clock()         # kronometre başlangıcı
>>> 
>>> # döngü ile hesap
>>> for i in np.arange(N):
...     y[i] = x[i]**2 + 3*x[i] - x[i]/5 + np.sin(x[i])
>>> t1 = tm.clock()
>>> 
>>> # vektörel hesap
>>> y = x**2 + 3*x - x/5 + np.sin(x)
>>> t2 = tm.clock()
>>> 
>>> print("Döngü ile hesaplama: %5.2f ms" %((t1-t0)*1E3))
>>> print("Vektörel hesaplama: %5.2f ms" %((t2-t1)*1E3))
Döngü ile hesaplama: 106.15 ms
Vektörel hesaplama:  0.67 ms
Görüldüğü gibi vektörel hesaplamayla basit bir hesaplamada bile en az 100 kat daha hızlı işlem yapılabilmektedir. Ayrıca kod metnimiz daha sade ve anlaşılır hale gelmiştir.
Numpy kendi dizi yapıları için temel aritmetik işlemleri (+,-,x,/) ve standart math/cmath kütüphanelerindeki fonksiyonları vektörel formda sağlamaktadır. Eğer harici fonksiyonlar da aynı verimlilikle hesap yapabilmek için vektörleştirmek istenirse bu, vectorize fonksiyonu kullanarak sağlanabilir. 
>>> from math import sqrt           # standart kütüphanedeki kök alma fonksiyonu
>>> def afunc(x):
...    if x<0:
...        return sqrt(-x) + x**2
...    else:
...        return sqrt(x) + x**2
>>> 
>>> afunc_v = np.vectorize(afunc)   # fonksiyonu vektörel hale getiriyoruz
>>> 
>>> N = 10000                       # dizinin eleman uzunluğu
>>> x = np.random.uniform(-1,+1,N)  # argüman dizisi
>>> y = np.zeros_like(x)            # sonuç dizisi
>>> 
>>> t0 = tm.clock()                 # kronometre başlangıcı
>>> 
>>> # döngü ile hesap
>>> for i in np.arange(N):
...    y[i] = afunc(x[i])
>>> t1 = tm.clock()
>>> 
>>> # vektörel hesap
>>> y = afunc_v(x)
>>> t2 = tm.clock()
>>> 
>>> print("Döngü ile hesaplama: %5.2f ms" %((t1-t0)*1E3))
>>> print("Vektörel hesaplama: %5.2f ms" %((t2-t1)*1E3))
Döngü ile hesaplama: 45.30 ms
Vektörel hesaplama:  10.99 ms
Eğer harici fonksiyonumuz sadece temel aritmetik işleçler ve standart kütüphanedeki fonksiyonları içeriyorsa yukarıdaki gibi vektörleştirmeye gerek kalmadan fonksiyonun direk kendisiyle çalışılabilir.

Dilimleme
Önceki konu başlığında tüm dizi elemanları üzerinden nasıl hızlı işlem yapabileceğimizden bahsettik. Ancak bazı durumlarda dizinin tamamıyla değil de belli kısımlarıyla işlem yapmak isteyebiliriz. Mesela; dizinin yarıdan sonrası, çift indisli elemanları gibi... Dilimleme operatörü de işte dizilerdeki elemanlara bu açıdan erişmemizi sağlar. Kullanılışı köşeli parantez içerisindeki iki nokta üst üste işaretleriyle sağlanır [ilk:son:adım]. Parantez içerisindeki tüm değerler tam sayı olmalıdır. Python'daki standart indisleme kuralları geçerlidir. Yani; son indisteki değer aralığa dahil değildir ve adım aralığı geriye doğru ilerlemek için negatif değer alabilir. Aşağıdaki kısa örneklerle kullanışını açıklamaya çalışalım.
>>> N = 10
>>> dizi = np.arange(N, dtype='int')
>>> print(dizi[::])         # dizinin tüm elemanları
[0 1 2 3 4 5 6 7 8 9]
>>> print(dizi[0:N/2:2])    # dizinin yarısına kadar 2'şer aralıklarla
[0 2 4]
>>> print(dizi[::-1])       # tersten geriye doğru birer adımlama
[9 8 7 6 5 4 3 2 1 0]
Dilimleme tekniği özellikle iteratif işlem gerektiren algoritmaların programlanmasında çok önemlidir. Şimdi bize nasıl bir kolaylık sağladığını iyice görmek açısından gerçekçi bir örnek yapalım. Bilindiği gibi analitik türevi mümkün olmayan fonksiyonların ilgili aralıktaki örnek değerleri üzerinden sayısal türevi sonlu farklar yöntemiyle belli doğrulukta hesaplanabilmektedir. Algortimayla alakalı detaylı bilgiyi http://tr.wikipedia.org/wiki/Sonlu_fark referansına bırakıp nasıl kodlandığına bakalım. Karşılaştırmayı mümkün kılmak için analitik türevini bildiğimiz sin fonksiyonunun türevini sayısal olarak üç farklı sonlu fark formülüyle aşağıdaki gibi hesaplıyoruz.
>>> # adım aralığı bilgisini de alacak şekilde eş aralıklı örnekleme
>>> N = 10  # örnekleme sayısı
>>> x, dx = np.linspace(0,+np.pi,N,retstep=True) # [0,+pi]
>>> 
>>> # test fonksiyonu ve analitik türevi
>>> y = np.sin(x)
>>> yp_analitik = np.cos(x)
>>> 
>>> # sonlu farklar
>>> yp_merkezi = ( y[2:N]-y[0:N-2] )/(2*dx) # merkezi fark
>>> yp_ileri = ( y[1:N]-y[0:N-1] )/dx       # ileri fark
>>> yp_geri = ( y[1:N]-y[0:N-1] )/dx        # geri fark
Görüldüğü gibi her bir sonlu fark hesabını dilimleme operatörü sayesinde birer satırda gerçekleştirebildik. Not olarak belirtmek gerekir ki, ileri ve geri fark ifadeleri birbirine eşit görünmekle birlikte açı dizisinde (x) karşılık geldiği noktalar farklıdır.
Yazımızı, bulduğumuz sonuçları görselleştirip, çıktılarını inceleyerek bitirelim. Aşağıdaki kodda Matplotlib kütüphanesi kullanılarak analitik türevle birlikte sayısal türevler tek bir grafik üzerinde gösterilmek istenmiştir. İleri ve geri fark sonuçları ise birbirleriyle benzer hata oranıyla analitik değere iyi bir biçimde yakınsamaktadır. Ancak beklediğimiz gibi, merkezi fark yöntemiyle hesaplanan türev diğer ikisinden daha az yanılmayla sonuca yaklaşmıştır. Örnekleme sayısını arttıracak olsaydık sayısal türev değerleri giderek daha fazla analitik değere benzeyecek, grafik eğrileri üzerinde ayırt edilemez hale gelecekti.
>>> import matplotlib.pyplot as plt # matplotlib kütüphanesini çizdiricisi
>>> 
>>> fig = plt.figure()              # yeni grafik ekranı
>>> 
>>> plt.plot(x,yp_analitik, color='blue', label='analitik')
>>> plt.plot(x[1:N-1],yp_merkezi, color='red', label='merkezi')
>>> plt.plot(x[0:N-1],yp_ileri, color='yellow', label='ileri')
>>> plt.plot(x[1:N],yp_geri, color='green', label='geri')
>>> plt.legend(loc='upper right')
>>> plt.title('Sinüs Fonksiyonunun Türevi')
>>> plt.xlabel('x')
>>> plt.ylabel('y\'(x)')
>>> plt.grid()
>>> plt.xlim(0,+np.pi)
>>> plt.ylim(-1,1)
>>> plt.show()

Kaynaklar:
[1] Langtangen H. P., Python Scripting for Computational Science, Springer, 2007

0 yorum :

Yorum Gönder