Python – Wprowadzenie do biblioteki NumPy

Gdy rozpoczynamy pracę związaną z uczeniem maszynowym lub cyfrowym przetwarzaniem sygnałów często potrzebujemy rozwiązać wiele zaawansowanych równań matematycznych. Do tego celu warto skorzystać z bibliotek, które umożliwiają obliczenia numeryczne w całkiem szybki sposób. Jedną z nich jest NumPy, która jest polecana szczególnie na początku. Przejdźmy zatem do rzeczy!

Co to jest NumPy?

NumPy jest fundamentalną biblioteką do obliczeń naukowych. NumPy jest skrótem od „Numerical Python”. Pakiet składa się z wielowymiarowych obiektów tablicowych i zbioru procedur do przetwarzania tablicy oraz posiada narzędzia do algebry liniowej, transformaty Fouriera oraz generowania liczb losowych. Często jest używana z pakietami takimi jak SciPy „Scientific Python” oraz Mat-plotlib (biblioteka do rysowania wykresów). Taki zestaw jest praktycznie zawsze używany w celu zastąpienia MATLABa. Ponadto NumPy jest biblioteką open source, co jest niezwykłym plusem.

Jeśli miałeś okazję poznać MATLABa, ten poradnik umożliwi Ci całkiem proste wprowadzenie do biblioteki.

Jak zacząć pracę z NumPy?

Zaczynamy od importowania biblioteki. Najczęściej robi się to korzystając z skrótu np. Jest to dobra praktyka, która zmniejsza ilość liter w linii, każdy programista dobrze wie, że chodzi o NumPy.

import numpy as np

Tablica w NumPy

Podstawowa tablica

Tablica ndarray jest główną strukturą danych w pakiecie NumPy, której dokładna nazwa to n-wymiarowa tablica. Wszystkie jej elementy muszą być tego samego typu, indeksowane przez typ tuple lub nieujemne liczby całkowite. Każdy element takiej tablicy nosi nazwę dtype.

arr = np.array([1, 2, 3]) # tworzy 1-wymiarowa tablice
print(type(arr)) # drukuje <class 'numpy.ndarray'>
print(arr.shape) # drukuje (3,) - zwraca ksztalt tablicy
print(arr.size) # drukuje 3 - zwraca ilosc elementow tablicy

arr2 = np.array([[1, 2, 3], 
                [4, 5, 6],
                [7, 8, 9]]) # tworzy 2-wymiarowa tablice 3x3
print(type(arr2)) # drukuje <class 'numpy.ndarray'>
print(arr2.shape) # drukuje (3, 3)
print(arr2.size) # drukuje 9

Zauważ, że dwu-wymiarowa tablica składa się z 3 tablic umieszczonych w jednej głównej tablicy. Zobaczcie poniższy przykład, który dużo lepiej zobrazuje tworzenie n-wymiarowej tablicy:

elem_1 = 1
elem_2 = 2
elem_3 = 3

arr_1 = [1, 2, 3]
arr_2 = [4, 5, 6]
arr_3 = [7, 8, 9]

l1 = [elem_1, elem_2, elem_3]
l2 = [arr_1, arr_2, arr_3]

a = np.array(l1)
b = np.array(l2)

print(type(a)) # drukuje <class 'numpy.ndarray'>
print(a.shape) # drukuje (3,) - zwraca ksztalt tablicy
print(a.size) # drukuje 3 - zwraca ilosc elementow tablicy

print(type(b)) # drukuje <class 'numpy.ndarray'>
print(b.shape) # drukuje (3, 3)
print(b.size) # drukuje 9

Tablice specjalne

W pracy z wektorami lub macierzami przydadzą się nam funkcje, które pozwalają na wstępną inicjalizację tablicy zerami, jedynkami i innymi liczbami. Kilka przykładów metod pozwalających na takie operacje:

shape = (3, 3)

a = np.zeros(shape) # tworzy tablice skladajaca sie z samych zer

a = np.ones(shape) # tworzy tablice skladajaca sie z samych jedynek

a = np.eye(shape[0]) # tworzy tablice w postaci macierzy jednostkowej
# nalezy podac tylko jedna liczbe jako parametr

a = np.empty(shape) # tworzy tablice skladajaca sie z losowych wartosci
# zaleznie od pamieci komputera (szybsza od np.zeros())

a = np.random.random(shape) # tworzy tablice wartosci losowych na przedziale [0, 1]
# nalezy podac tylko jedna liczbe jako parametr

const_value = 10
a = np.full(shape, const_value) # tworzy tablice o stalej zadanej wartosci
# nalezy podac tylko jedna liczbe jako parametr

Tablice z wartościami w zadanym przedziale

Warto znać dwie metody: np.arange oraz np.linspace, które są często wykorzystywane podczas generowania zmiennych w zadanym przedziale.

# tworzy tablice rownomiernie rozmieszczonych wartosci w przedziale
# z krokiem rownym step
a = np.arange(start=0, stop=100, step=11) 
print(a) # drukuje [ 0 11 22 33 44 55 66 77 88 99]

# tworzy tablice rownomiernie rozmieszczonych num elementow w przedziale
a = np.linspace(start=0, stop=100, num=11) 
print(a) # drukuje [  0.  10.  20.  30.  40.  50.  60.  70.  80.  90. 100.]

Indeksowanie i wycinanie tablic

Generalnie NumPy pozwala na bardzo dużo sposobów indeksowania i wycinania elementów w tablicach. Postaram się pokazać najpopularniejsze metody.

a = np.array([[1, 2, 3], 
              [4, 5, 6],
              [7, 8, 9]])

b = a[0, 2] # [numer wiersza, numer kolumny]
print(b) # drukuje 3

# [od początku do wiersza 2, od kolumny 1 do konca]
c = a[:2, 1:] # [wiersze 0 i 1, kolumny 1 i 2]
print(c) # drukuje [[2 3]
                  # [5 6]]

b = 123
print(a[0, 2]) # drukuje 3
c[0, 1] = 123
print(a[0, 2]) # drukuje 123

# wez wszystko z kolumny pierwszej
print(a[:, 1]) # drukuje [2 5 8]
# wez wszystko z wiersza pierwszego
print(a[1, :]) # drukuje [4 5 6]

Żeby zrozumieć, dlaczego a[0, 2] wyświetla 123 należy wiedzieć, że wycinanie to operacja „wglądu” do oryginalnej tablicy. Jak w C++ mamy referencję, tak tutaj mamy wgląd do oryginału i modyfikujemy zawartość głównej tablicy.

Innym ciekawym trikiem jest indeksowanie przy pomocy innych tablic.

a = np.array([[1, 2, 3], 
              [4, 5, 6],
              [7, 8, 9]])

b = [0, 0, 1]
c = np.arange(3) # tablica [0, 1, 2]

# na wierszach tablicy operuje b, na kolumnach c
# pierwszy element b to 0, pierwszy element c to 0, wiec jest a[0, 0] to 1
# drugi element b to 0, drugi element c to 1, wiec a[0, 1] to 2
# trzeci element b to 1, trzeci element c to 2, wiec a[1, 2] to 6
print(a[b, c]) # drukuje [1 2 6]

# sprobuj opanowac skad bierze sie taki wyniki
print(a[c, b]) # drukuje [1 4 8]

Można też korzystać z zapisów warunkowych, żeby sprawdzić gdzie jest spełniony dany warunek w tablicy.

a = np.array([[1, 2, 3], 
              [4, 5, 6],
              [7, 8, 9]])

# zwroci wartosc True dla kazdego elementu tablicy a wiekszego od 5
b = (a > 5)
print(b) # drukuje [[False False False]
                  # [False  True  True]
                  # [ True  True  True]]
print(a[b]) # drukuje [6 7 8 9] 
print(a[a > 5]) # drukuje [6 7 8 9]

Może powstać pytanie, a co jak chcemy więcej niż jeden warunek? Przydatna będzie funkcja, która pozwala na takie operacje – np.logical_and():

a = np.array([[1, 2, 3], 
              [4, 5, 6],
              [7, 8, 9]])

b = (a > 5)
c = (a <= 8)
print(a[np.logical_and(b, c)])

Gdy pracujemy w uczeniu maszynowym może się przydać dodanie kolumny bądź wiersza jedynek (jak chociażby w regresji liniowej podczas wyliczania współczynników), dlatego pokażę jak to zrobić w formie ciekawostki:

a = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])

b = np.ones(3)
b = b[np.newaxis, :]
print(b, '\n')

new_row = np.concatenate((a, b), axis=0) # dodanie b jako wiersz
new_column = np.concatenate((a, b.T), axis=1) # dodanie b jako kolumna
print(new_row, '\n') # [[1. 2. 3.]
                      # [4. 5. 6.]
                      # [7. 8. 9.]
                      # [1. 1. 1.]] 
                      
print(new_column, '\n') # [[1. 2. 3. 1.]
                         # [4. 5. 6. 1.]
                         # [7. 8. 9. 1.]]

Na początek dodajemy nowy wymiar do wektora jedynek, jest to operacja wymagana. Pamiętasz z czego składa się tablica dwuwymiarowa – z wektorów 1D. Następnie wykonujemy konkatenację dwóch tablic, czyli ich dodanie. W pierwszym przypadku dodajemy tablice jedynek jako nowy wiersz, w kolejnym przypadku jako kolumnę. Należy pamiętać o transpozycji. Możemy to zrobić na 2 sposoby:

  • <nazwa tablicy>.T
  • np.transpose(<nazwa tablicy>)

Typy danych w NumPy

Generalnie typów danych jest bardzo dużo i zalecam samodzielnie się przekonać o ich liczbie. Poniżej pokazałem dwa podstawowe, z nimi będziemy pracować zdecydowanie najczęściej. Odnośnik do pozostałych typów znajdziecie w linkach referencyjnych.

x = np.array([1, 2])
print(x.dtype) # wyswietla "int64"

x = np.array([1.0, 2.0])
print(x.dtype) # wyswietla "float64"

x = np.array([1, 2], dtype=np.int64) # przekonaj do danego typu
print(x.dtype) # wyswietla "int64"

Operacje arytmetyczne

Mamy dostęp do wszystkich operacji arytmetycznych. O przeciążonych operatorach +, -, * lub / myślę, że każdy słyszał. Nowością dla mnie są gotowe metody do wspomnianych operacji.

x = np.array([[1, 2], [3, 4]], dtype=np.float64)
y = np.array([[5, 6], [7, 8]], dtype=np.float64)

# Dodawanie elementow kazdego z osobna
# [[ 6.0  8.0]
#  [10.0 12.0]]
print(x + y)
print(np.add(x, y))

# Odejmowanie elementow kazdego z osobna
# [[-4.0 -4.0]
#  [-4.0 -4.0]]
print(x - y)
print(np.subtract(x, y))

# Mnozenie elementow kazdego z osobna
# [[ 5.0 12.0]
#  [21.0 32.0]]
print(x * y)
print(np.multiply(x, y))

# Dzielenie elementow kazdego z osobna
# [[ 0.2         0.33333333]
#  [ 0.42857143  0.5       ]]
print(x / y)
print(np.divide(x, y))

# Pierwiastek kazdego z elementow
# [[ 1.          1.41421356]
#  [ 1.73205081  2.        ]]
print(np.sqrt(x))

Ważną różnicą między NumPy i MATLABem jest fakt, że dla MATLABa mnożenie przy pomocy * to operacja iloczynu skalarnego wektorów oraz iloczynu macierzy, natomiast wykorzystanie .* jest iloczynem każdego elementu z osobna. Natomiast w NumPy jest odwrotnie – * to iloczyn każdego elementu z osobna, na obliczenie iloczynu macierzy lub iloczynu skalarnego pozwala funkcja np.dot(), którą możemy wykorzystywać jako funkcję biblioteki jak i funkcję instancji obiektu:

x = np.array([[1, 2],[3, 4]])
y = np.array([[5, 6],[7, 8]])

v = np.array([9, 10])
u = np.array([11, 12])

# Iloczyn skalarny wektorow, 219 jest wynikiem
print(v.dot(u))
print(np.dot(v, u))

# Mnozenie macierzy, wynikiem jest [[19 22]
#                                   [43 50]]
print(x.dot(y))
print(np.dot(x, y))

Rozmiary tablic

Ważnym zagadnieniem są długość, szerokość czy ilość wymiarów tablic w NumPy. Przy zaawansowanych obliczeniach możemy nieraz złapać się na braku jednego elementu, bądź zapomnieć, że tablice w Python iterują się od 0. Dlatego pokażę jak sprawdzić rozmiary elementów.

arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])

print('arr.ndim:', arr.ndim) # 2 - ilosc wymiarow
print('arr.size:', arr.size) # 9 - ilosc elementow
print('arr.shape:', arr.shape) # (3, 3) - (ilosc wierszy, ilosc kolumn)

Inne pomocne funkcje w NumPy

  • np.min / np.max – zwraca minimum / maximum w tablicy
  • np.sum – zwraca sumę wszystkich elementów w tablicy
  • np.sort – sortuje tablicę
  • np.unique – zwraca wartości unikalne w tablicy
  • np.all / np.any – sprawdza, czy wszystkie / jakiś element tablicy w danej są równe True
  • np.isnan – sprawdza, czy są elementy NaN
  • np.isinf – sprawdza, czy elementy dążą do nieskończoności
  • np.isfinite – sprawdza, czy elementy są skończone
  • np.exp – liczy funkcję eksponencjalną z danego parametru
  • np.log / np.log10 – liczy logarytm naturalny / z podstawą 10 wszystkich elementów w tablicy

Podsumowanie

Podsumowując, udało nam się dziś nauczyć bardzo dużo i z zdobytą wiedzą możemy przejść do pracy z algorytmami numerycznymi. Warto zaznaczyć, że jest to jedynie wstęp do tej jakże obszernej biblioteki wykorzystywanej pośród programistów.

Jeśli chciałbyś poznać podstawy języka Python, zapraszam tutaj!

Linki referencyjne:

Data types

NumPy Tutorial

NumPy Docs