Przejdź do treści

NumPy

logo NumPy

Wprowadzenie

NumPy (Numerical Python) to bardzo wydajna biblioteka obliczeniowa dla języka Python, przewidziana do obliczeń wykonywanych na CPU. Umożliwia tworzenie obiektów reprezentujących wielowymiarowe tablice oraz dostarcza funkcje do szybkich operacji na tablicach wielowymiarowych. Zakres wspieranych funkcji jest bardzo szeroki i obejmuje m.in. podstawowe operacje algebry liniowej na wektorach i macierzach.

Głównym założeniem NumPy jest ułatwienie użytkownikom korzystania z operacji tablicowych, które zwiększają wydajność programu. Biblioteka dostarcza podstawowe operacja na tablicach, takie jak dodawanie i mnożenie tablic (np. wektorów, macierzy). Są one łatwe w użyciu, a ich implementacja jest efektywna (wykorzystuje m.in. wektoryzację). Oprócz tego biblioteka dostarcza wyspecjalizowane funkcje służące m.in. do:

  • różnych operacji matematycznych (podobnych do tych ze standardowej biblioteki math; np. funkcje trygonometryczne),
  • operacji logicznych (np. dzielenie tablicy na części ze względu na wybrane kryterium),
  • podstawowych operacji statystycznych,
  • manipulacji kształtem tablicy (zmiana wymiarów, dodawania/usuwanie wierszy i kolumn),
  • sortowania,
  • operacji I/O (tj. zapisu i odczytu danych),
  • dyskretnej transformaty Fouriera,
  • algebry liniowej,
  • … i wiele więcej.

Dostępność

Biblioteka NumPy dostępna jest tylko dla języka Python. Jest przystosowana do wykonywania obliczeń na CPU. Do pracy na GPU potrzeba skorzystać z innej biblioteki np. CuPy️. Można ją zainstalować przy użyciu standardowych menedżerów oprogramowania (pip, conda) lub budować ręcznie ze źródeł.

Biblioteka korzysta pod spodem z implementacji BLAS/LAPACK️ ale standardowa instalacja NumPy nie wymaga ich wcześniejszego posiadania. Jest tak dlatego, że wraz z pakietem NumPy automatycznie dostarczana jest implementacja OpenBLAS️ (w przypadku pip) lub MKL️ (w przypadku domyślnego kanału conda). Dodatkowo conda umożliwia wybranie innej implementacji, jest to również możliwe w przypadku ręcznego budowania.


Szczegóły

W standardowym Pythonie lista może przechowywać zmienne różnego typu i jej elementy nie są umieszczone w spójnym bloku fizycznej pamięci. Zajmuje to dodatkową pamięć i zwiększa czas dostępu do poszczególnych elementów. Z tego powodu wszelkie pętle po listach wyrażone w języku Python nie są efektywne. NumPy rozwiązuje ten problem, implementując klasyczną strukturę tablicy jako obiekt ndarray (N-dimensional array). Jest to wielowymiarowa tablica elementów o jednakowym typie, przechowywana w spójnym bloku pamięci. Każdy element takiej tablicy zajmuje tyle samo pamięci.

NumPy jako implementacja tablicy wielowymiarowej

Dostarczenie wydajnej implementacji tablicy wielowymiarowej jest główną motywacją i celem projektu NumPy. Jest ona powszechnie przyjęta i stosowana przez inne pakiety obliczeniowe jako podstawowa składowa. Przykładem może być pakiet SciPy️, który dostarcza dużo więcej funkcji obliczeniowych niż NumPy, bazując właśnie na ndarray.

Większość funkcji dostarczanych przez numpy dotyczy wykonania operacji dotyczącej całej tablicy lub tej samej operacji dla każdego z elementów tablicy z osobna. Operacje te zostały zaimplementowane w języku C i podlegają wektoryzacji, dzięki czemu obliczenia są bardzo efektywne. Należy jednak zwrócić uwagę, że biblioteka jest przewidziana do pracy na jednym rdzeniu CPU, tj. nie korzysta z wielowątkowości i nie zrównolegla obliczeń. Z drugiej strony funkcje dotyczące algebry liniowej są realizowane za pomocą biblioteki BLAS/LAPACK, która może korzystać z wielowątkowości. W efekcie, operacje algebry liniowej mogą zostać zrównoleglone na wiele rdzeni CPU w obrębie jednej maszyny. Ich wydajność i zachowanie zależy od zastosowanej implementacji BLAS/LAPACK.

Standardowy moduł array

Moduł array w bazowym Pythonie pozwala wykorzystać strukturę pamięci języka C do przechowywania danych tego samego typu, co usprawnia częściowo pracę w porównaniu z listami Python. Niemniej nie udostępnia on operacji matematycznych na tych tablicach, co sprawia że praca na nich będzie mniej wydajna niż praca z tablicami NumPy (przykładowo nie zostanie wykorzystana wektoryzacja). Ponadto NumPy w przeciwieństwie do modułu array jest zintegrowany z wieloma innymi narzędziami.

Tablice i skalary

Tablica wielowymiarowa, może wydawać się nieco zbyt ogólnym typem do prowadzenia obliczeń np. na samych wektorach albo macierzach. Nie należy jednak ulegać takiemu wrażeniu. Obiekt ndarray służy do wyrażania zarówno wektorów (tablica 1-wymiarowa), macierzy (tablica 2-wymiarowa), jak i wyżej wymiarowych tablic. Co więcej, pojedyncze wartości – czyli skalary – w NumPy są również reprezentowane przez ndarray. Skalar to tablica 0-wymiarowa, która przechowuje tylko jeden element. Takie zunifikowane podejście sprawia, że wszystkie obiekty posiadają te same metody i atrybuty.

Zabroniona klasa matrix

W NumPy istnieje przestarzała klasa numpy.matrix, przewidziana do reprezentowania macierzy. ⚠️ Nie należy z niej korzystać, klasa jest wycofywana z użytku i jest przewidziana do usunięcia. Ponieważ jest to trochę inny obiekt, może być źródłem niekompatybilności z kodem, który działa na tablicach ndarray.

Tablica ndarray przechowuje elementy jednakowego typu, do opisu którego służy obiekt dtype. Pozwala on na dokładniejsze wyrażenie typu danych niż sam język Python. W szczególności można określić rozmiar i precyzję, np. czy pracujemy z liczbami całkowitymi 32- czy 64-bitowymi, albo czy pracujemy z liczbami zmiennoprzecinkowymi pojedynczej czy podwójnej precyzji. Typy danych w większości odpowiadają typom znanym z języka C. Dla czytelności odpowiedniki typów podstawowych Python mają sufiks “_” (np. numpy.int_ odpowiada int). Pełna lista wraz z opisem jest dostępna w specyfikacji typów.

typy danych dostępne w NumPy [źródło]

Operacje na tablicach

Dla wygody programowania i czytelności kodu, NumPy dostarcza podstawowe operatory arytmetyczne (+, -, /, *) dla tablic wielowymiarowych. Reprezentują one wykonanie działania między elementami tablic, które sobie odpowiadają. Przykładowo dla wektorów A i B:

  • C = A + B, to tablica w której ,
  • C = A * B, to tablica w której .

Operatory arytmetyczne (+, *, -) są równoważne konkretnym funkcją z NumPy, odpowiednio: numpy.add, numpy.multiply, numpy.divide.

Mnożenie macierzy

Należy zwrócić uwagę, że mnożenie macierzy nie jest realizowane przez operator *. Zamiast tego należy użyć funkcji numpy.dot().

przykład operacji arytmetycznych między dwoma wektorami [źródło]

Zasadniczo operację między tablicami można zrealizować gdy kształt obydwóch tablic jest taki sam (te same wymiary) ale również między obiektami o różnych wymiarach, gdy odpowiadające sobie wymiary mają ten sam rozmiar. Na przykładzie mnożenia:

  • b = a * x (wektor * skalar) - każdy element wektora zostaje pomnożony przez skalar; w efekcie powstaje wektor, w którym ,
  • c = a * b (wektor * wektor) - każdy element pomnożony przez swój odpowiednik; w efekcie powstaje wektor, w którym ,
  • C = A * b (macierz * wektor) - każdy wiersz macierzy zostaje potraktowany jak wektor i pomnożony tak jak wyżej wektor * wektor; w efekcie powstaje macierz, w której .

operacja macierz + wektor [źródło]

W przypadku innych funkcji niż podstawowe operacje, ich zachowanie może zależeć od wymiarów podanych tablic. Na przykładzie funkcji dot() wywołanie numpy.dot(A,B), to

  • iloczyn skalarny, gdy A i B to wektory (tablice 1-wymiarowe),
  • mnożenie macierzy, gdy A i B to macierze (tablice 2-wymiarowe),
  • mnożenie tablicy przez skalar, gdy A to skalar, a B to dowolna tablica,
  • …jeszcze inne zachowanie gdy jedna z tablic ma wymiar większy niż 2.

Taka uniwersalność funkcji niesie ze sobą ryzyko pomylenia, co jest liczone w danej chwili. Przykładowo jeżeli chcemy mnożyć wektory, a pomnożymy wektor przez macierz – kod nie zwróci błędu. Aby się przed tym ustrzec można stosować bardziej dopasowane funkcje. Przykładowo funkcja inner() służy do wykonania operacji iloczynu skalarnego lub mnożenia wektora przez skalar. Zwraca błąd, jeśli wejściowa tablica ma więcej niż 1 wymiar, co jest przydatne w pisaniu bezpieczniejszego i czytelniejszego kodu.

Indeksowanie

Numpy dostarcza wygodne i rozbudowane indeksowanie tablic ndarray. Dostęp do pojedynczego elementu macierzy jest realizowany przez:

  • A[0, 1] (równoważnie A[(0, 1)]) – element z pierwszego wiersza, drugiej kolumny.

W przypadku macierzy zwróci pojedynczy element (skalar). Indeksowanie dla większej liczby wymiarów działa analogicznie, np. A[0, 5, 3] zwraca pojedynczy element dla tablicy trójwymiarowej.

W przypadku tablicy więcej wymiarowej A[0, 1] zwróci tzw. widok tablicy (view). Wskazuje on na te same dane, nie są one kopiowane, dzięki czemu tworzenie widoków to bardzo szybka operacja. Zmiany dokonane na oryginalnej tablicy będą widoczne w widoku i odwrotnie, zmiany na widoku tablicy będą widoczne w oryginalnej tablicy. Przykładowo dla tablicy 4-wymiarowej A, gdy B = A[0,1] to B będzie macierzą-widokiem gdzie B[i,j] wskazuje na ten sam element co A[0,1,i,j]. Obiekt zwrócony zależy od ilości podanych indeksów względem ilości wymiarów tablicy. A[i] zwróci widok o 1 wymiar mniejszy, a A[i,j] zwróci widok o 2 wymiary mniejszy niż A.

Bezpośredni dostęp do elementu tablicy

Na przykładzie 2-wymiarowej tablicy, należy zwrócić uwagę na różnicę między A[(1,2)] a A[1][2]. Obydwa wywołania zwracają ten sam element, jednak A[1][2] jest mniej wydajne, ponieważ tworzy tymczasowy widok (A[1]) i dopiero z niego odczytuje konkretny element.

Dostępne są również bardziej zaawansowane formy indeksowania:

  • A[[0, 2, 4]] – pierwszy, trzeci i piąty element tablicy A.
  • A[[0, 0, 1], [1, 2, 3]] – elementy A[0,1], A[0,2], A[1,3] W pierwszym nawiasie znajdują się numery wierszy, a w drugim kolumny.
  • A[:, 0] – pierwsza kolumna w postaci 1-wymiarowej tablicy.
  • A[A < 5] – wszystkie wartości z tablicy mniejsze niż 5 w postaci 1-wymiarowej tablicy. Działa na dowolnym rozmiarze tablicy.

Składnia indeksowania

Warto zwrócić uwagę, że A[[0, 2, 4]] ma zupełnie inne znaczenie niż A[(0, 2, 4)].

przykładowe formy indeksowania w NumPy [źródło]

Możliwości zrównoleglania

Jak było już wspomniane, NumPy sam w sobie nie udostępnia możliwości prowadzenia obliczeń równoległych. Wyjątek dotyczy funkcji, które są realizowane przez bibliotekę BLAS/LAPACK (numpy.linalg). W efekcie tylko operacje algebry liniowej mogą zostać zrównoleglone na wiele rdzeni (w obrębie jednej maszyny).

Powodem ograniczonych możliwości zrównoleglenia programów na wiele rdzeni są wysokopoziomowe właściwości i szczegóły implementacji języka Python. Główna przeszkoda w tym zakresie to GIL (Global Interpreter Lock). Niemniej powstały specjalne narzędzia pozwalające na implementację równoległych obliczeń, które m.in. wykorzystują NumPy.

  • Dask Arrays ─ dzieli tablice NumPy na mniejsze części i przekazuje je do równoległego wykonania. Sposób przekazywania jest podyktowany poprzez tzw. Task Graph (pol. schemat zadań), który przedstawia program bezpośrednio w strukturze danych. Jest to mniej czytelne dla programisty, ale pozwala na szybszą obróbkę danych przez maszynę.
  • Numexpr ─ biblioteka dobrze zintegrowana z NumPy. Przyspiesza znacznie obliczenia takie jak całkowanie numeryczne dla dużych ilości danych.

Informacje o wydaniu

Obecna wersja 1.24.3 pochodzi z kwietnia 2023. NumPy jest dostępny open source. Zarówno pip oraz conda udostępniają NumPy, a instalując w ten sposób, nie ma potrzeby ręcznego ustawiania biblioteki BLAS. W przypadku zintegrowanych środowisk programistycznych (IDE) często jest domyślnie zainstalowany wraz z tym środowiskiem. Biblioteka NumPy jest regularnie rozwijana i stanowi podstawowe narzędzie w obliczeniach naukowych.


Linki


Ostatnia aktualizacja: 15 września 2023