Na dnešní lekci si do virtuálního prostředí nainstalujte následující balíčky:
$ python -m pip install --upgrade pip
$ python -m pip install notebook numpy scipy imageio matplotlib pillow
Mezitím, co se to bude instalovat, si stáhněte do adresáře static
tyto soubory:
A až bude nainstalováno, spusťte si nový Notebook. (Viz lekce o Notebooku.)
NumPy je základní knihovna pro vědce a analytiky, kteří pracují s Pythonem. Definuje typ pro n-rozměrné homogenní pole (nejčastěji čísel) a API pro práci s takovým polem.
Téměř všechny knihovny, kde se objevují větší matice či tabulky, jsou buď postavené na NumPy, nebo podporují numpy.array
: od pandas
pro datovou analýzu a matplotlib
pro grafy, přes scipy
, kde najdete základní algoritmy pro interpolaci, integraci aj., astrofyzikální astropy
, librosa
pro analýzu hudby, až po integraci v knihovnách jako Pillow
nebo Tensorflow
.
Podobně jako „Djangonauti” kolem webového frameworku Django tvoří vědci a datoví analytici podskupinu pythonní komunity s vlastními konferencemi (PyData), organizacemi (NumFocus, Continuum Analytics) a knihovnami jako NumPy, Pandas, SciPy, Matplotlib či Astropy. Potřeby této komunity se samozřejmě odrážejí i v Pythonu samotném (např. ...
a @
, které si ukážeme dále, byly do jazyka přidány pro ulehčení výpočtů) a naopak (na rozdíl od specializovaných jazyků jako R nebo Matlab se tu stále indexuje od nuly). Většina těchto knihoven ale má jednu zvláštnost, kterou ve zbytku pythonního světa tolik nevidíme: důraz na použití v interaktivním režimu.
Čísla můžeme buď prozkoumávat, hrát si s nimi, zjišťovat zajímavé souvislosti; anebo můžeme připravovat programy, které nějaké výpočty provedou automaticky.
Na obojí se používají podobné nástroje.
Automaticky pouštěné skripty musí být samozřejmě robustní. Nástroje ke zkoumání dat ale bývají přívětivé k vědcům, často na úkor robustnosti nebo „dobrých programátorských mravů”. Například některé funkce tak trochu „hádají”, co uživatel chtěl, a v tutoriálech se setkáte se zkratkami jako import numpy as np
či dokonce from numpy import *
.
Toto je kurz programovací, kde nám záleží více na znovupoužitelném kódu než na jednom konkrétním výsledku. Budeme proto preferovat explicitní a jednoznačné operace. Ty jsou v použitých knihovnách vždy vedle zkratek k dispozici a popsány v dokumentaci.
Jak s polem pracovat? Nejprve si ho vytvoříme, nejlépe ze seznamu čísel:
import numpy
array = numpy.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array
Každé pole má dvě základní, neměnné vlastnosti: tvar (shape
), neboli velikost, a datový typ (dtype
).
array.shape
array.dtype
Tvar je n-tice, kde n je počet dimenzí pole; shape=(3, 3) dtype='int64'
znamená pole 3×3 celých čísel.
Dimenzí může být libovolně mnoho, např. trojrozměrnou matici můžeme vytvořit z trojnásobně vnořených seznamů a NumPy ji „rozumně” vypíše:
cube = numpy.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(cube.shape)
cube
Základní vlastnost NumPy polí je to, že aritmetické operace se skalárními hodnotami (např. čísly) fungují po prvcích.
array
array - 1
array / 2
-(array % 4)
Kromě aritmetických operací takto funguje i porovnávání. Následujícím výrazem dostanete pravdivostní tabulku, která má True
na místech, kde pro příslušný prvek platí podmínka:
array > 5
Protože Python neumožňuje předefinovat chování operátorů and
a or
, logické spojení operací se tradičně dělá přes bitové operátory &
(a) a |
(nebo). Ty mají ale neintuitivní prioritu, proto se jednotlivé výrazy hodí uzavřít do závorek:
(array > 3) & (array < 7)
Operace s jinými poli pracují po prvcích. Obě pole musí být stejně velké.
array + array
array * numpy.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
Sekvence (jako seznamy) jsou před operací převedeny na pole.
array * [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
NumPy pole jde různými způsoby indexovat. Základní způsoby známe už ze samotného Pythonu – pole se chová jako sekvence menších polí:
array
array[0]
array[0:-1]
array[0][1]
Na rozdíl od Pythonu se ale dá vícerozměrné pole indexovat přímo n-ticí. Toto je dokonce preferovaný způsob – přehlednější a mnohem rychlejší:
array[0, 1]
array[0:-1, 1:]
Chceme-li vybrat určitý sloupec, řekneme si „kompletním intervalem“ (:
) o všechny řádky:
array[:, 2]
Další způsob specifický pro NumPy je indexování pravdivostní tabulkou.
Když potřebujete z matice vybrat prvky, pro které platí nějaká podmínka, napřed si vytvořte pole hodnot True
/False
:
array > 4
To pak použijte jako index:
array[array > 4]
Výsledek je jednorozměrný – informace o původních pozicích prvků se ztratí.
Pro úplnost uvedu ještě dva způsoby indexování. První je seznamem indexů, kterým můžete vybírat, přehazovat nebo i duplikovat konkrétní řádky:
array[[0, 2, 1, 1]] # Řádky 0, 2, 1 a 1
array[:, [2, 2, 0, 0]] # Podobně pro sloupce
Druhý je indexování pomocí n-tice „řádkových souřadnic“ a n-tice odpovídajících „sloupcových souřadnic“:
array[(0, 1, 2), (1, 2, 0)] # Vybere prvky (0, 1), (1, 2), (2, 0)
Trochu specifické je indexování vícerozměrných polí. U nich se často využije „kompletní interval“ (:
):
cube
cube[0, :, :] # První „vrstva“ - to samé jako cube[0]
cube[:, 0, :] # První řádky - to samé jako cube[:, 0]
cube[:, :, 0] # První sloupce
Má-li pole hodně rozměrů, je psaní spousty :,
zdlouhavé a nepřehledné. Existuje proto speciální hodnota ...
(Ellipsis
), která doplní tolik „kompletních intervalů“ (:
), aby souhlasil počet dimenzí:
cube[..., 0] # První sloupce – ekvivalent [:, :, 0]
Už jsme si ukázali, že aritmetické operace se skalárními hodnotami se provede pro všechny prvky, zatímco operace mezi dvěma stejně velkými poli se provede po prvcích:
array
array * 3
array * [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
Jak je to ale s různě velkými poli?
Nemá-li sekvence, se kterou pracujeme, dost dimenzí, poslední dimenze se „rozšíří“, jako bychom pracovali v každém sloupci se skalární hodnotou. Tomuto „rozšiřování” se obecně říká broadcasting.
array
array * [0, 1, 10] # vynásobí 1. sloupec nulou, 2. jedničkou, 3. deseti
Podobné rozšiřování nastane, má-li některá dimenze velikost 1:
array * [[0], [1], [10]] # vynásobí 1. *řádek* nulou, atd.
Jednotlivé hodnoty v poli lze měnit:
array[0, 0] = 123
array
...a i na měnění se vztahuje broadcasting:
array[0] = 123
array
array[:] = 123
array
Obecně platí, že lze-li něčím vybírat prvky, lze tím i pole měnit:
array[(1, 2, 0), (0, 2, 1)] = 0
array
Další způsob, jak pole měnit, je rozšířeným přiřazením.
array *= 2
array
Tato operace není totéž co array = array * 2
, ačkoli má stejný výsledek.
array *= 2
změní existující pole, zatímco array = array * 2
vytvoří nové pole, které pak přiřadí do původní proměnné.
Pozor na to, že není možné měnit typ pole:
try:
array /= 2
except Exception as e:
print("Chyba!!", type(e), e)
Časté druhy matic se dají vytvořit pomocí pomocných funkcí. Výsledky se dají použít přímo nebo naplnit vypočítanými daty:
numpy.zeros((4, 4))
numpy.ones((4, 4))
numpy.full((4, 4), 12.34)
numpy.eye(4) # Jednotková matice (je čtvercová – n×n)
numpy.diag([1, 2, 3, 4]) # Diagonální matice
U těchto funkcí lze obecně použít argument dtype
, kterým specifikujeme datový typ:
int_zeros = numpy.zeros((4, 4), dtype='int8')
print(int_zeros.dtype)
int_zeros
Další funkce tvoří jednorozměrné matice. Základní je arange
, která bere stejné argumenty jako range
v Pythonu:
numpy.arange(50) # Celočíselné – argumenty jako range() v Pythonu
Navíc umí pracovat s reálnými čísly (float
). Pozor ale na to, že reálná čísla jsou nepřesná! arange
k začátku sekvence postupně přičítá „krok”, takže chyba narůstá celkem rychle:
numpy.arange(0.0, 50.0, 0.3)[-1]
V krajních případech takto dokonce můžeme dostat pole jiné velikosti, než jsme zamýšleli. Proto arange
používejte jen pro celá čísla; pro reálná je tu linspace
, která bere začátek a konec intervalu, plus počet prvků:
numpy.linspace(0, 50, num=11) # vždy 11 prvků
Ačkoli indexování polí v NumPy je dost mocné, v paměti jsou jednotlivé hodnoty reprezentovány jako (metadata a) jednorozměrné pole, známé z jazyka C (ačkoli samotné rozmístění prvků může být jiné než po řádcích, jak jsme zvyklí u C).
Je jednoduché změnit tvar pole, nezmění-li se tím celkový počet prvků:
array = numpy.arange(12)
array
reshaped = array.reshape((3, 4))
reshaped
Pozor na to, že reshape
sice vrací nový objekt, ale může (ale nemusí!) to být jen nový pohled na existující data. Změny v pohledu se projeví i v původním poli:
reshaped[2, 2] = -99
reshaped
array
Podobně tvoří pohledy i jednoduché indexování:
a_slice = array[2:4]
a_slice[:] = -99, -99
array
Potřebujete-li kopii dat, použijte metodu copy
:
array.reshape((3, 4)).copy()
Podobně jako reshape
funguje transpozice, což je tak častá operace, že má jednopísmennou zkratku – atribut T
. (Tohle hodně napomáhá tomu, že zápis maticových výpočtů v NumPy se podobá odpovídajícím matematickým vzorcům.)
reshaped.T
Až budete NumPy zkoušet, doporučuji si u nových funkcí najít, zda tvoří nová pole, vracejí pohled nebo modifikují původní pole. U některých funkcí najdete pojmenovaný argument inplace
(modifikuje původní pole), případně out
, („naplní“ jiné, existující pole).
Jak už bylo řečeno, matice v NumPy mají určené datové typy. Ty jdou nastavit ve většině funkcí, které matice tvoří, argumentem dtype
:
numpy.zeros(4, dtype=int)
numpy.zeros(4, dtype=float)
numpy.zeros(4, dtype=bool)
Nejobecnější typ je object
(jehož použitím ale přicházíme o většinu výhod NumPy).
numpy.array([0, 1.3, "foobar"], dtype=object)
Kromě pythonních typů bere dtype
i řetězcové specifikace typu:
numpy.array([1, 8, 500], dtype='int8') # 8bitové celé číslo
Znáte-li modul array
ze standardní knihovny, můžete jeho specifikaci použít i tady:
numpy.zeros(4, dtype='<I')
Navíc dtype
umí řetězcové a bytestring typy. Tyto mají danou maximální velikost a nesmí obsahovat \0
(resp. znakem '\0'
jsou ukončeny):
numpy.full(4, 'abcdef', dtype=('U', 10)) # Unicode
numpy.full(4, 'abcdef', dtype=('a', 3)) # "ASCII"
Typy v NumPy můžou být poměrně složité; např. existují i složené datové typy (records
). Ty nebudeme používat, ale je dobré o nich aspoň tušit:
record_type = numpy.dtype([('a', int), ('b', float), ('c', ('U', 3))])
numpy.array([(1, 2, 'abc')] * 4, record_type)
Kromě základních aritmetických operací se u vícerozměrných polí často setkáme s maticovým násobením. Předpokládám, že jako bakaláři jste se s ním už setkali a tušíte co dělá – jestli ne, tuto sekci ignorujte.
V Pythonu 3.5 byl na výzvu vědecké komunity do jazyka přidán operátor @
(mATrix multiplication), který je vyhrazen pro maticové násobení. V samotném Pythonu ani ve standardní knihovně není typ, který ho podporuje, ale matice v NumPy tuto operaci samozřejmě umí.
array1 = numpy.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array2 = numpy.array([[1, 0, 0], [0, 2, 0], [0, 0, 3]])
array1 @ array2
Ve starších verzích Pythonu je potřeba používat metodu nebo funkci dot
.
Důvod přidání operátoru @
byl prostý – zjednodušení zápisu maticových operací. Jako příklad uvedený v návrhu je uveden tento vzorec pro testování hypotéz v lineárním regresním modelu:
$ S=(H\beta-r)^T(HVH^T)^{-1} (H\beta-r) $
V NumPy se dá přepsat jako:
from numpy import dot
from numpy.linalg import inv, solve
Pomocí funkce dot
:
S = dot((dot(H, beta) - r).T,
dot(inv(dot(dot(H, V), H.T)), dot(H, beta) - r))
Pomocí metoody dot
:
S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)
Pomocí operátoru @
:
S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)
Poslední varianta nápadně připomíná původní vzorec; u prvních dvou se člověk snadno ztratí ve změti závorek.
Použijeme-li pole v příkazu if, NumPy nám vynadá. Standardní pythonní seznam je „pravdivý“ pokud obsahuje nějaké prvky, ale u pole, které má fixní velikost, je tahle informace téměř vždy zbytečná.
try:
if numpy.eye(3):
pass
except ValueError as e:
print("Chyba!", type(e), e)
Musíme říct přesně, co chceme.
if numpy.eye(3).any():
print('Alespoň jeden prvek je nenulový')
if numpy.eye(3).all():
print('Všechny prvky jsou nenulové')
if numpy.eye(3).size:
print('Pole obsahuje nějaké prvky')
Z historických důvodů existují dvě výjimky: pole s právě jedním prvkem má pravdivostní hodnotu podle daného prvku a prázdné pole je „nepravdivé“:
if numpy.ones((1, 1, 1, 1)):
print('Ano')
if numpy.zeros((1, 1, 1, 1)):
print('Ne')
if numpy.ones((0, 0)):
print('Ano')
Modul numpy
obsahuje spoustu základních funkcí, které pracují s maticemi; mimo jiné většinu funkcií z pythonního modulu math
. Oproti math
zvládají funkce z NumPy broadcasting.
array = numpy.linspace(0, numpy.pi, num=1000)
array[:10]
sine = numpy.sin(array)
sine[:10]
Další operace doporučuji hledat buď v Notebooku přes tab, v dokumentaci, nebo obecně na Internetu (kde najdete i případné knihovny, které implementují operace, které v NumPy nejsou).
Dost teorie. Tahle n-rozměrná pole se používají na spoustu zajímavých věcí. Podívejme se na některé příklady.
Jak se používají matice, jistě znáte z matematiky a cílem tohoto kurzu není vás to naučit. Ukážu ale pár ochutnávek.
Použijeme knihovnu Matplotlib, která vykresluje grafy. Jak ji použít dohledáte v dokumentaci nebo – často efektivněji – v galerii příkladů.
Matplotlib nemá automatickou integraci s Jupyter Notebookem, proto ji je potřeba po importu zapnout:
from matplotlib import pyplot
# Zapnutí integrace s notebookem – `%` je "magický" příkaz IPythonu, podobně jako `!` pro shell
%matplotlib inline
A teď můžeme nakreslit třeba graf funkce:
x = numpy.linspace(0, numpy.pi * 4) # definiční obor
y = numpy.sin(x) # odpovídající hodnoty funkce
pyplot.plot(x, y)
s = numpy.linspace(-10, 10, num=100)
# meshgrid vrátí dvě 100×100 matice:
# - jednu, kde v každém řádku jsou čísla od -10 do 10,
# - druhou, kde v každém sloupci jsou čísla od -10 do 10.
x, y = numpy.meshgrid(s, s)
# vyhodnotíme (x**2 + y**2) pro každý prvek
z = x ** 2 + y ** 2
# výsledek vykreslíme jako obrázek
pyplot.imshow(z)
# přidáme legendu
pyplot.colorbar()
# Ta samá data můžeme vykreslit i ve 3D
from mpl_toolkits.mplot3d import Axes3D
fig = pyplot.figure()
axes = fig.gca(projection='3d')
surf = axes.plot_surface(x, y, z, cmap='viridis')
Typický barevný obrázek není nic než matice $m \times n \times 3$ čísel: $m \times n$ pixelů na šířku a výšku a 3 kanály pro červenou, zelenou a modrou barvu.
Knihovna pillow
(nástupce knihovny PIL, který se stále importuje jako PIL) obsahuje nástroje na práci s obrázky, např. „nakresli čáru“ nebo „převeď na černobílý obrázek“ nebo „načti PNG“. Není postavena přímo na NumPy, ale umí obrázky převádět z a na NumPy pole, pokud máme NumPy nainstalované.
Knihovna imageio
slouží k načítání a ukládání různých (njen obrázkových) formátů do/z NumPy matic.
Nás bude zajímat funkce imageio.imread
, která pomocí Pillow/PIL načte obrázek jako 3D matici 8bitových čísel. Já načtu obrázek hada, vy najděte na internetu jakýkoli RGB obrázek a načtěte si ten.
Použitý obrázek je stažený z Wikimedia Commons a je pod licencí CC-BY-SA 3.0. Autor je uživatel Mokele na anglické Wikipedii.
import imageio
img = imageio.imread('static/python.jpg')
img
Pomocí nám už známé knihovny matplotlib
takovou matici můžeme zobrazit:
pyplot.imshow(img)
Podívejme se teď na strukturu matice:
img.shape
První rozměr jsou řádky (y); můj obrázek je 887 pixelů vysoký. Druhý jsou sloupce (x); tento obrázek má na šířku 1037 px. Třetí rozměr jsou barevné kanály.
Pomocí indexování se můžeme na jednotlivé barevné kanály dostat: je to poslední index, takže řádky a sloupce nahradíme buď dvěma kompletními intervaly (:, :
) nebo vynechávkou (...
). Červený kanál tedy bude [..., 1]
, modrý [..., -1]
.
Zobrazení chceme černobílé; na to má matplotlib pojmenovaný argument cmap
. Výchozí způsob obarvování je vhodný spíše pro grafy funkcí.
blue_channel = img[..., -1]
pyplot.imshow(blue_channel, cmap='gray')
Zajímavé využití obrázku jako matice je steganografie: ukrytí informace v obrazových datech.
Načteme jiný obrázek stejné velikosti, tentokrát černobílý (s módem L
). Informace v něm schováme do posledního bitu modrého kanálu.
secret = imageio.imread('static/secret.png', pilmode='L')
img[..., -1] = (img[..., -1] & 0b11111110) + (secret.astype(bool))
Obrázek vypadá na první pohled stejně...
pyplot.imshow(img)
... ale v posledím modrém bitu se skrývá tajná informace.
pyplot.imshow(img[..., -1] & 1, cmap='gray')
Výsledek je dobré uložit v bezztrátovém formátu (PNG), aby se informace neztratila:
imageio.imsave('python.png', img)
Jako pole lze reprezentovat i zvukový záznam. Mám záznam, na kterém zkouším zpívat; pomocí funkce scipy.io.wavfile
ho můžu načíst jako NumPy pole:
import scipy.io.wavfile
sample_rate, sound = scipy.io.wavfile.read('static/sample.wav')
print(sample_rate)
sound
Zvuk je stereo, má dvě stopy; jednu z nich si vykreslím:
channel = sound[..., 1]
pyplot.plot(channel)
Případně můžu využít možností Jupyter Notebooku a HTML a zvuk si přehrát:
from IPython.display import Audio
Audio(data=channel, rate=sample_rate)
print('(Zkuste si to sami; tento print vymažte)')
Podívám se na detail první „noty”, kde je patrná vlna s nějakou frekvencí:
segment = channel[50000:55000]
pyplot.plot(segment)
from IPython.display import Audio
Audio(data=segment, rate=sample_rate)
Jaká to je frekvence? Znáte-li analýzu signálů, tušíte, že na podobné otázky odpovídá Fourierova transformace.
NumPy obsahuje diskrétní Fourierovu transformaci v modulu numpy.fft
spolu s funkcí, která spočítá odpovídající frekvence v Hz:
spectrum = numpy.fft.fft(segment)
freqs = numpy.fft.fftfreq(len(spectrum), 1/sample_rate)
pyplot.xlabel('Frekvence (Hz)')
pyplot.plot(freqs, numpy.abs(spectrum))
V tomto grafu hledám maximum. Můžu se zaměřit na prvních pár hodnot spektra:
pyplot.xlabel('Frekvence (Hz)')
pyplot.plot(freqs[:100], numpy.abs(spectrum[:100]))
Maximum je někde kolem 120 Hz; abych to zjistil přesně, použiji funkci argmax
:
amax = numpy.argmax(abs(spectrum))
amax
... a najdu odpovídající frekvenci:
freqs[amax]
Což je podle seznamu not skoro H$_2$ (123,5 Hz).