PPy3/NumPy
Spis treści
NumPy: rachunki numeryczne na tablicach liczb
Pakiet Numpy
Moduł Numpy jest podstawowym zestawem narzędzi dla języka Python umożliwiającym zaawansowane obliczenia matematyczne, w szczególności do zastosowań naukowych (tzw. obliczenia numeryczne, jak mnożenie i dodawanie macierzy, diagonalizacja czy odwrócenie, całkowanie, rozwiązywanie równań, itd.). Daje on nam do dyspozycji specjalizowane typy danych, operacje i funkcje, których nie ma w typowej instalacji Pythona. Natomiast moduł Scipy pozwala na dostęp do bardziej złożonych i różnorodnych algorytmów wykorzystujących narzędzia dostarczone w Numpy.
Przedstawimy tutaj tylko wstęp do Numpy. Wynika to z faktu, że opisanie licznych funkcji dostępnych w bibliotece Numpy jest ogromną pracą, która zupełnie nie ma sensu — równie dobrze można zajrzeć bezpośrednio do źródła, http://docs.scipy.org/doc/numpy/reference/.
Najważniejszym obiektem, na którym bazuje pakiet Numpy i szereg pakietów z niego korzystających jest klasa ndarray wprowadzająca obiekty array. Obiekty array możemy traktować jako uniwersalne pojemniki na dane w postaci macierzy (czyli wektorów lub tablic). W porównaniu ze standardowymi typami sekwencji Pythonowych (lista, krotka) jest kilka różnic w operowaniu tymi obiektami:
- obiekty przechowywane w tablicy array muszą być wszystkie tego samego typu;
- obiekty array zachowują swój rozmiar; przy zmianie rozmiaru takiego obiektu powstaje nowy obiekt, a obiekt sprzed zmiany zostaje usunięty;
- obiekty array wyposażone są w bogaty zestaw funkcji operujących na wszystkich przechowywanych w obiekcie danych, specjalnie optymalizowanych do przetwarzania dużych ilości danych. Jak to działa zostanie zaprezentowane poniżej.
Tworzenie tablic
Najprostszym sposobem stworzenia tablicy Numpy jest wywołanie funkcji array z argumentem w postaci listy liczb. Jeśli zamiast listy liczb użyjemy listy zawierającej inne listy (tzw. listy zagnieżdżone), to otrzymamy tablicę wielowymiarową. Na przykład jeśli listy są podwójnie zagnieżdzone, to otrzymujemy tablicę dwuwymiarową (macierz).
# przykład wykorzystania Numpy
>>> import numpy
>>> A = numpy.array([1, 3, 7, 2, 8])
array([1, 3, 7, 2, 8])
>>> B = numpy.array([[1, 2, 3], [4, 5, 6]])
>>> B
array([[1, 2, 3],
[4, 5, 6]])
>>> B.transpose()
array([[1, 4],
[2, 5],
[3, 6]])
Innym sposobem tworzenia tablicy jest funkcja numpy.arange, która działa analogicznie do range, tyle tylko, że zwraca tablicę NumPy zamiast listy, i dopuszcza parametry ułamkowe — a nie tylko całkowite.
Argumenty są takie same:
- indeks początkowy [opcjonalnie, domyślnie 0]
- indeks następny po końcowym
- krok [opcjonalnie, domyślnie 1]
>>> numpy.arange(1000000)
array([ 0, 1, 2, ..., 999997, 999998, 999999])
>>> numpy.arange(0.1, 0.2, 0.01)
array([ 0.1 , 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19])
>>> numpy.arange(0.9, 0.0, -0.1)
array([ 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1])
Jak było już wspomniane, w przypadku tablicy array typowe operacje matematyczne możemy przeprowadzić dla wszystkich elementów tablicy przy użyciu jednego operatora lub funkcji. Zachowanie takie jest odmienne niż w przypadku list czy innych sekwencji Pythona. Jeśli chcielibyśmy na przykład pomnożyć wszystkie elementy listy L przez liczbę a, musimy użyć pętli:
L = [1, 3, 5, 2, 3, 1]
for k, x in enumerate(L):
L[k] = a * x
Można też zapisać to zwięźlej, używając wyrażenia generatorowego:
L = [1, 3, 5, 2, 3, 1]
L = [a * x for x in L]
jest to jednak poniekąd tylko uproszczony zapis pętli. Natomiast mnożenie wszystkich elementów tablicy M przez liczbę a wygląda tak:
M = numpy.array([1, 3, 5, 2, 3, 1])
M = a * M
Operacje wykonywane od razu na całych macierzach mają wiele zalet. Kod programu jest prostszy i krótszy, przez co mniej podatny na błędy. Poza tym nie musimy przejmować się konkretną realizacją danej operacji — robi to za nas funkcja pakietu Numpy, która jest specjalnie optymalizowana, żeby działała jak najszybciej.
- Inne
- zob.numpy.mgrid, numpy.ogrid, numpy.linspace, numpy.zeros, numpy.ones, numpy.r_.
Wydobywanie danych
Pojedyncze liczby
Dostęp do elementów (i pod-tablic) jest możliwy poprzez użycie notacji indeksowej (tablica[i]) jak i wycinkowej (tablica[i:j]).
Dostęp do pojedynczego elementu:
>>> A = array([[1, 2, 3],[4,5,6]])
>>> A
array([[1, 2, 3],
[4, 5, 6]])
>>> A[0][2] # podobnie jak w Pythonie,numeracja od 0
3
>>> A[0, 2]
3
Indeksy dotyczące poszczególnych wymiarów można podać w pojedynczej parze nawiasów oddzielone przecinkami.
Macierz A jest tablicą dwuwymiarową, i sposób numerowania zawartych w niej obiektów jest następujący: pierwszy indeks przebiega pierwszy wymiar (wybiera wiersz), drugi indeks przebiega drugi wymiar (wybiera kolumnę).
Pod-tablice
Dostęp do pod-tablic:
>>> A[1] # wiersz 1
array([4, 5, 6])
>>> A[1, :] # wiersz 1, wszystkie kolumny
array([4, 5, 6])
>>> A[:, 1] # wszystkie wiersze, kolumna 1
array([2, 5])
Jak widać, ograniczenie się do pojedynczego punktu w danym wymiarze, powoduje degenerację tego wymiaru. Uzyskuje się tablicę, w której liczba wymiarów jest mniejsza o jeden.
>>> A[:, 1:]
array([[2, 3],
[5, 6]])
W pierwszym wymiarze (wiersze) bierzemy wszystko, natomiast w drugim od 1 do końca. Efektywnie wycinamy kolumnę 0.
Indeksowanie tablic tablicami
Do wybrania elementów z tablicy można tez użyć innej tablicy. Może to być
- tablica liczb — wówczas są one traktowane jako indeksy. Wybieramy te elementy, które uzyskalibyśmy indeksując każdym z indeksów z osobna
- tablica wartości logicznych (boolean) rozmiaru macierzy z danymi. Wybieramy te elementy, którym odpowiada True w macierzy indeksującej.
Uwaga: W wyniku dostajemy macierz jedno wierszową.
- Przykład
>>> print A
[[1 2 3]
[4 5 6]]
>>> print A > 2
[[False False True]
[ True True True]]
>>> print A[A > 2]
[3 4 5 6]
>>> print A[A % 2 == 0]
[2 4 6]
Dlaczego warto używać Numpy?
Pierwsza przyczyna, zazwyczaj najmniej istotna, to wydajność. Jeśli mamy pomnożyć 100 elementów, to szybkość operacji na pojedynczym elemencie nie ma znaczenia. Podobnie jest z rozmiarem pojedynczego elementu. Jeśli elementów jest 106, to również wtedy narzut nie ma większego znaczenia. Policzmy: 1000000 razy 12 bajtów, to 12 MB. Typowy komputer ma obecnie 1-4 GB pamięci, czyli używamy od 1,2% do 0,27% dostępnej pamięci — jaki problem? Dopiero gdy miejsce zajmowane przez dane jest tego samego rzędu co całość dostępnej pamięci, to czy pojedyncza komórka zajmuje 8 czy 16 bajtów, zaczyna mieć znaczenie.
Druga przyczyna, istotna ze względu na przyjemność pracy, to notacja obiektowa i infixowa. Ta pierwsza to oczywiście „notacja z kropką” — dostęp do metod i atrybutów na obiekcie. Jej użycie, zwłaszcza w połączeniu z dopełnieniem TAB-em upraszcza pisanie. Przykład notacji obiektowej:
a.transpose().min()
# zamiast
numpy.min(numpy.transpose(a))
Ta druga (infixowa) to stara dobra „notacja matematyczna” — umiejscowienie operatorów dwuargumentowych pomiędzy obiektami na które działają. Przykład notacji infixowej:
a + b*c
# zamiast
numpy.add(a, numpy.multiply(b, c))
Oczywiście notacja obiektowa i infixowa jest używane wszędzie w Pythonie, ale warto wspomnieć, że Numpy od niej nie odchodzi. Niemniej Numpy odchodzi od Pythonowej interpretacji niektórych działań. W Pythonie takie operacje jak mnożenie list wywodzą się z działań na ciągach znaków. W obliczeniach numerycznych podstawą są działania na elementach, tak więc w Numpy wszystkie operatory domyślnie działają na indywidualnych parach elementów.
Trzecia przyczyna, chyba najważniejsza, to biblioteka funkcji numerycznych. Odejście od obiektowości danych pozwala na eksport wartości i komunikację z bibliotekami napisanymi w zupełnie innych językach programowania. Na przykład Scipy może korzystać z biblioteki LAPACK (Linear Algebra PACKage, napisanej w Fortranie 77). To że funkcje napisane w różnych językach mogą wymieniać się danymi w pamięci bez skomplikowanego tłumaczenia danych, wynika z faktu, że tak jak to w poprzednim podrozdziale było opisane, ostatecznie wszystkie liczby są w formacie akceptowanym przez procesor.
Możliwość użycia kodu napisanego w C czy Fortranie pozwala na wykorzystanie starych, zoptymalizowanych, sprawdzonych rozwiązań.
Ćwiczenia
1. Napisz funkcję zastap_zera(A, x), która zwraca tablicę utworzoną z tablicy A (o dowolnym kształcie) poprzez zastąpienie wszystkich elementów równych zero liczbą x. Sama tablica A powinna pozostać niezmieniona.
2. Napisz funkcję wysrodkuj(A), która modyfikuje tablicę A (o dowolnym kształcie) w taki sposób, że od każdego jej elementu odejmuje średnią arytmetyczną wszystkich elementów A.
3. Dla (dowolnego rozmiaru większego niż 1) tablicy kwadratowej A stworzyć tablicę jednowymiarową, której k-ty element to suma elementów k-tej kolumny tablicy A leżących poniżej głównej przekątnej.
4. Napisz funkcję, która w minimalnej liczbie kroków tworzy (i zwraca) kwadratową tablicę NumPy zawierającą na przemian jedynki i zera, o dowolnym - zadanym przez argument wywołania - rozmiarze, w postaci:
In [1]: from naprzemian import naprzemian
In [2]: naprzemian(2)
Out[2]:
array([[ 1., 0.],
[ 0., 1.]])
In [3]: naprzemian(3)
Out[3]:
array([[ 1., 0., 1.],
[ 0., 1., 0.],
[ 1., 0., 1.]])
In [4]: naprzemian(4)
Out[4]:
array([[ 1., 0., 1., 0.],
[ 0., 1., 0., 1.],
[ 1., 0., 1., 0.],
[ 0., 1., 0., 1.]])
In [5]: naprzemian(5)
Out[5]:
array([[ 1., 0., 1., 0., 1.],
[ 0., 1., 0., 1., 0.],
[ 1., 0., 1., 0., 1.],
[ 0., 1., 0., 1., 0.],
[ 1., 0., 1., 0., 1.]])