Σημειωματάριο Τρίτης 24 Μαρ. 2015

Εισαγωγή στη βιβλιοθήκη numpy

Η βιβλιοθήκη numpy είναι μια συλλογή από συναρτήσεις και τύπους δεδομένων για τη γλώσσα python οι οποία έχει φτιαχτεί για να χρησιμοποιείται σε περιπτώσεις πολλών και επαναλαμβανόμενων αριθμητικών υπολογισμών, όπως αυτοί συναντώνται σε μαθηματικές ή επιστημονικές εφαρμογές. Η numpy έχει φτιαχτεί με σκοπό την αποτελεσματική υλοποίηση κάποιων κοινών υπολογιστικών πράξεων και ο τρόπος που τη χρησιμοποιούμε είναι ότι προσπαθούμε όσο μπορούμε να χρησιμοποιούμε τους τύπους δεδομένων και τις συναρτήσεις της βιβλιοθήκης αυτής και όπου μπορούμε να αποφεύγουμε να υλοποιούμε με δικό μας κώδικα πράγματα που είναι ήδη υλοποιημένα, και μάλιστα με πολύ πιο αποτελεσματικό τρόπο, στη βιβλιοθήκη numpy.

Ένα παράδειγμα με numpy και χωρίς

Παρακάτω βλέπουμε δύο διαφορετικές υλοποιήσεις, τη μια χωρίς τη numpy (συνάρτηση nonumpy_version) και την άλλη με τη numpy (numpy_version) των εξής μαθηματικών πράξεων:

  1. Δημιουργούμε δύο ίδια διανύσματα \(X, Y\) μήκους \(10^7\) με περιεχόμενα τους αριθμούς \(0, 1, 2, \ldots, 10^7-1\) με αύξουσα σειρά.
  2. Προσθέτουμε αυτά τα δύο διανύσματα και παίρνουμε ένα τρίτο διάνυσμα \(Z\).

Και οι δύο αυτές συναρτήσεις κάνουν τα παραπάνω 1 και 2 και επίσης χρονομετρούν τον εαυτό τους και επιστρέφουν το χρόνο που τους πήρε να τρέξουν σε δευτερόλεπτα. Παρατηρείστε ότι η μορφή χωρίς numpy είναι πολλές φορές πιο αργή από τη μορφή με numpy (το πόσες φορές ακριβώς είναι πιο αργή εξαρτάται και από τον υπολογιστή, αλλά 10 ως 50 φορές είναι συνηθισμένες τιμές).

Για τη συνάρτηση nonumpy_version δεν έχουμε κάτι νέο να εξηγήσουμε, απλά να πούμε ότι τα διανύσματα \(X, Y, Z\) υλοποιούνται με τις συνηθισμένες μας λίστες της python.

Η συνάρτηση numpy_version και τα arrays.

Στη συνάρτηση numpy_version παρατηρούμε κατ' αρχήν ότι χρησιμοποιούμε μια παραλλαγή της γνωστής μας range τη συνάρτηση numpy.arange.

Παρατηρούμε επίσης τη γραμμή Z = X+Y που αν τα \(X, Y\) μας ήταν εδώ λίστες (δεν είναι) τότε το αποτέλεσμα \(Z\) θα ήταν όχι το κατά συντεταγμένη άθροισμα των δύο λιστών αλλά η συνένωσή τους (η Y μετά τη X).

Δεν είναι λοιπόν λίστα αυτό που επιστρέφει η συνάρτηση numpy.arange αλλά ένας νέος τύπος δεδομένων, το numpy.array (το όνομα array δηλ. ορίζεται κάπου μέσα στη numpy). Μπορούμε να σκεφτόμαστε τον τύπο αυτό δεδομένων, όπως έχει υλοποιηθεί στη numpy, ως μια γενίκευση και ταυτόχρονα διαφορετική υλοποίηση της γνωστής μας λίστας.

Αν λοιπόν τα X και Y είναι arrays τότε η εντολή Z = X+Y παράγει ένα νέο array (στη μεταβλητή Z) του οποίου τα περιεχόμενα είναι τα κατά συντεταγμένες αθροίσματα των περιεχομένων των X και Y. Το γεγονός ότι η python γνωρίζει πώς τα προσθέτει δύο arrays κατά σημείο οφείλεται στο ότι έχουμε κάνει import numpy.

In [2]:
import numpy
import time

def nonumpy_version():
    t1 = time.time()
    X = range(10000000)
    Y = range(10000000)
    Z = []
    for i in range(len(X)):
        Z.append(X[i] + Y[i])
    return time.time() - t1

def numpy_version():
    t1 = time.time()
    X = numpy.arange(10000000)
    Y = numpy.arange(10000000)
    Z = X + Y
    return time.time() - t1

print nonumpy_version()

print numpy_version()
1.62276411057
0.140839099884

Εδώ βλέπουμε ένα νέο τρόπο να κάνουμε import τη βιβλιοθήκη μας δίνοντάς της ταυτόχρονα ένα μικρότερο όνομα, συνήθως np, για να μη γράφουμε συνέχει το μακρύτερο όνομα numpy. Έτσι αν ένα όνομα είναι δηλωμένο μέσα στη numpy, π.χ. το όνομα array ή η συνάρτηση arange, ο τρόπος να αναφερθούμε σε αυτό το όνομα είναι με το πρόθεμα np., γράφοντας δηλ. np.array και np.arange.

Έπειτα δημιουργούμε μια λίστα l με περιεχόμενα 1, 2, 3 και χρησιμοποιούμε τη συνάρτηση np.array για να φτιάξουμε από τη λίστα l ένα array a που περιέχει τις ίδιες τιμές. Παρατηρείστε ότι όταν τυπώνουμε το l και το a αυτά τυπώνονται κάπως διαφορετικά από την python αλλά έχουν τα ίδια περιεχόμενα.

Στον ορισμό της μεταβλητής b βλέπουμε το πώς ορίζουμε ένα διδιάστατο πίνακα με τη numpy. Περιγράφουμε τις γραμμές του πίνακα σε μια λίστα την κάθε μία και ολόκληρο τον πίνακα ως μια λίστα των γραμμών του. Σε αυτή τη λίστα από λίστες εφαρμόζουμε τη συνάρτηση array και παράγουμε το διδιάστατο πίνακα b, τον οποίο και τυπώνουμε.

Για να ορίσουμε τη μεταβλητή c με τον ανάστροφο πίνακα του b καλούμε τη μέθοδο transpose() του πίνακα b, η οποία επιστρέφει ένα άλλο array που περιέχει τον ανάστροφο του b. Έπειτα τυπώνουμε το c.

Τέλος καλούμε τη μέθοδο sum() του array c η οποία μας επιστρέφει το άθροισμα όλων των στοιχείων του c.

In [14]:
import numpy as np

l = [1, 2, 3]
a = np.array(l)

print "l is", l
print "a is", a

b = np.array( [ [1, 2, 3], [4, 5, 6] ] )
print "b is\n", b

c = b.transpose()
print "c is\n", c

print "Sum of elements of c is", np.sum(c)
l is  [1, 2, 3]
a is  [1 2 3]
b is 
[[1 2 3]
 [4 5 6]]
c is 
[[1 4]
 [2 5]
 [3 6]]
Sum of elements of c is  21

Εδώ δημιουργούμε ένα array a από τη λίστα [1, 2, 3] και προσδιορίζουμε ότι θέλουμε ο τύπος των αριθμών στο array να είναι float64 (αυτό είναι ένα όνομα δηλωμένο στη numpy άρα πρέπει να γράψουμε np.float64). Το float64 είναι ένας τύπος δεδομένων (αριθμός κινητής υποδιαστολής με 64 αντί για 32 bits, για να παριστάνουμε πραγματικούς αριθμούς) που δεν είναι ορισμένος στην ίδια την python.

Όταν στο τέλος λέμε στην python να τυπώσει τη μεταβλητή a παρατηρούμε ότι οι αριθμοί 1, 2, 3 εμφανίζονται με υποδιαστολή, έχουν δηλ. μετατραπεί εσωτερικά στον υπολογιστή σε αριθμούς κινητής υποδιαστολής, παρά το ότι τυχαίνει να είναι και ακέραιοι.

In [18]:
import numpy as np

a = np.array([1, 2, 3], dtype=np.float64)
a
Out[18]:
array([ 1.,  2.,  3.])

Παρένθεση: παράμετροι σε μια συνάρτηση με default τιμές και προσδιορισμός των παραμέτρων όχι με τη θέση τους αλλά με το όνομά τους

Παρακάτω ορίζουμε τη συνάρτηση powersum(x, y, power). Παρατηρείστε στον ορισμό της συνάρτησης ότι η παράμετρος power παίρνει την default τιμή 1. Αν ο χρήστης δε δώσει τιμή γι' αυτή την παράμετρο σε μια κλήση της συνάρτησης powersum τότε στην εκτέλεση του κώδικα της συνάρτησης αυτή θα έχει την τιμή 1. Αυτό ακριβώς κάνουμε αμέσως μετά τον ορισμό της συνάρτησης.

Παρατηρείστε επίσης (επόμενη γραμμή) ότι στην python μπορούμε να δώσουμε τις παραμέτρους μιας συνάρτησης με οποιαδήποτε σειρά θέλουμε, αρκεί να δώσουμε και το όνομά τους μαζί.

In [21]:
def powersum(x, y, power=1):
    return x**power + y**power

print powersum(2, 3)

print powersum(power=2, x=2, y=3.0)
5
13.0

Πράξεις πάνω σε πίνακες κατά στοιχείο

Στη δεύτερη γραμμή παρακάτω from numpy import array as ar βλέπουμε ένα μηχανισμό της python ώστε να χρησιμοποιούμε ότι όνομα θέλουμε για να καλέσουμε τη συνάρτηση array (στην περίπτωση αυτή το όνομα ar).

Ορίζουμε δύο μονοδιάστατα arrays a και b και τους δίνουμε τιμές (η συνάρτηση ones της numpy επιστρέφει ένα array που έχει παντού 1), υπολογίζουμε το κατά σημείο αθροισμά τους c και το τυπώνουμε. Το ίδιο κάνουμε στις επόμενες γραμμές και για το κατά σημείο γινόμενο και διαίρεση των διανυσμάτων a και b.

Ορίζουμε έπειτα το διδιάστατο πίνακα A με διαστάσεις \(2 \times 3\), τον τυπώνουμε και επίσης τυπώνουμε το shape (μορφή) του πίνακα που είναι ένα tuple με τόσες θέσεις όσες η διάσταση του array (δύο στην περίπτωση αυτή). Βλέπουμε ότι το shape είναι το tuple (2,3), πρόκειται δηλ. για ένα πίνακα με 2 γραμμές και 3 στήλες (διδιάστατος πίνακας). Έπειτα ορίζουμε το διάνυσμα (μονοδιάστατος πίνακας) b με τιμή [1, 1, 1] και παρατηρούμε ότι το shape του b είναι (3,), πρόκειται δηλ. για ένα μονοδιάστατο πίνακα με 3 θέσεις.

Τέλος χρησιμοποιούμε τη συνάρτηση dot της numpy η οποία υλοποιεί ακριβώς τον πολλαπλασιασμό πινάκων. Πολλαπλασιάζουμε τον πίνακα A με διαστάσεις \(2\times 3\) με το διάνυσμα b και παίρνουμε το αποτέλεσμα στο διάνυσμα c.

In [42]:
import numpy as np
from numpy import array as ar

a = ar([1, 2, 3, 4], float); print "a is", a
b = np.ones(4); print "b is", b
c = a+b
print "c=a+b is", c

c = a*b
print "c=a*b is", c

c = b/a
print "c=a/b is", c

A = ar([ [1, 2, 3], [5, 2, 1] ])
print "A is\n", A
print "The shape of A is", A.shape

b = np.ones(3)
print "b is", b
print "The shape of b is", b.shape

c = np.dot(A, b)
print "c=A*b is", c
print "The shape of c is", c.shape
a is [ 1.  2.  3.  4.]
b is [ 1.  1.  1.  1.]
c=a+b is [ 2.  3.  4.  5.]
c=a*b is [ 1.  2.  3.  4.]
c=a/b is [ 1.          0.5         0.33333333  0.25      ]
A is
[[1 2 3]
 [5 2 1]]
The shape of A is (2, 3)
b is [ 1.  1.  1.]
The shape of b is (3,)
c=A*b is [ 6.  8.]
The shape of c is (2,)

Εφαρμογή μιας συνάρτησης σε ένα πίνακα, σημείο προς σημείο

Μέσα στη numpy βρίσκονται υλοποιημένες πολλές συναρτήσεις που είναι φτιαγμένες ώστε να εφαρμόζονται στα στοιχεία ενός πίνακα, αν τους περάσουμε πίνακα ως παράμετρο. Παρακάτω βλέπουμε τη συνάρτηση sin (ημίτονο) της numpy να εφαρμόζεται σε όλα τα στοιχεία του διανύσματος x, το οποίο δημιουργήσαμε προηγουμένως με την arange περιέχει τα στοιχεία μιας αριθμητικής προόδου με πρώτο στοιχείο το 0, βήμα το 0.1 και μέχρι αυτά να είναι \(< 0\). Οι τιμές που τυπώνονται ως τιμές του array y είναι τα ημίτονα των αριθμών που είναι στο array x, με τη σειρά αυτή.

In [46]:
import numpy as np
import math

x = np.arange(0, 10, 0.1)
y = np.sin(x)
print y
[ 0.          0.09983342  0.19866933  0.29552021  0.38941834  0.47942554
  0.56464247  0.64421769  0.71735609  0.78332691  0.84147098  0.89120736
  0.93203909  0.96355819  0.98544973  0.99749499  0.9995736   0.99166481
  0.97384763  0.94630009  0.90929743  0.86320937  0.8084964   0.74570521
  0.67546318  0.59847214  0.51550137  0.42737988  0.33498815  0.23924933
  0.14112001  0.04158066 -0.05837414 -0.15774569 -0.2555411  -0.35078323
 -0.44252044 -0.52983614 -0.61185789 -0.68776616 -0.7568025  -0.81827711
 -0.87157577 -0.91616594 -0.95160207 -0.97753012 -0.993691   -0.99992326
 -0.99616461 -0.98245261 -0.95892427 -0.92581468 -0.88345466 -0.83226744
 -0.77276449 -0.70554033 -0.63126664 -0.55068554 -0.46460218 -0.37387666
 -0.2794155  -0.1821625  -0.0830894   0.0168139   0.1165492   0.21511999
  0.31154136  0.40484992  0.49411335  0.57843976  0.6569866   0.72896904
  0.79366786  0.85043662  0.8987081   0.93799998  0.96791967  0.98816823
  0.99854335  0.99894134  0.98935825  0.96988981  0.94073056  0.90217183
  0.85459891  0.79848711  0.7343971   0.66296923  0.58491719  0.50102086
  0.41211849  0.31909836  0.22288991  0.12445442  0.02477543 -0.07515112
 -0.17432678 -0.27176063 -0.36647913 -0.45753589]