zuma/lab/1a_Podstawowe_narzędzia.ipynb

45 KiB

Uczenie maszynowe — laboratoria

1a. Podstawowe narzędzia uczenia maszynowego

Elementy języka Python przydatne w uczeniu maszynowym

Listy składane (_List comprehension)

lista = [i**2 for i in range(50)]

print(lista)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401]
slownik = {k: k**2 for k in range(10) if k % 2 == 0}

print(slownik)
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

Przypuśćmy, że mamy dane zdanie i chcemy utworzyć listę, która będzie zawierać długości kolejnych wyrazów tego zdania. Możemy to zrobić w następujący sposób:

zdanie = 'tracz tarł tarcicę tak takt w takt jak takt w takt tarcicę tartak tarł'
wyrazy = zdanie.split()
dlugosci_wyrazow = []
for wyraz in wyrazy:
    dlugosci_wyrazow.append(len(wyraz))
    
print(dlugosci_wyrazow)
[5, 4, 7, 3, 4, 1, 4, 3, 4, 1, 4, 7, 6, 4]

Możemy to też zrobić bardziej „pythonicznie”, przy użyciu list składanych:

zdanie = 'tracz tarł tarcicę tak takt w takt jak takt w takt tarcicę tartak tarł'
wyrazy = zdanie.split()
dlugosci_wyrazow = [len(wyraz) for wyraz in wyrazy]

print(dlugosci_wyrazow)
[5, 4, 7, 3, 4, 1, 4, 3, 4, 1, 4, 7, 6, 4]

Jeżeli chcemy, żeby był sprawdzany dodatkowy warunek, np. chcemy pomijać wyraz „takt”, to wciąż możemy użyć list składanych:

zdanie = 'tracz tarł tarcicę tak takt w takt jak takt w takt tarcicę tartak tarł'
wyrazy = zdanie.split()

# Ta konstrukcja:
dlugosci_wyrazow = []
for wyraz in wyrazy:
    if wyraz != 'takt':
        dlugosci_wyrazow.append(wyraz)
        
# ...jest równoważna tej jednolinijkowej:
dlugosci_wyrazow = [len(wyraz) for wyraz in wyrazy if wyraz != 'takt']

print(dlugosci_wyrazow)
[5, 4, 7, 3, 1, 3, 1, 7, 6, 4]

Indeksowanie

Wszystkie listy i krotki w Pythonie, w tym łańcuchy (które trakowane są jak krotki znaków), są indeksowane od 0:

napis = 'abcde'
print(napis[0])  # 'a'
print(napis[4])  # 'e'
a
e

Indeksy możemy liczyć również „od końca”:

napis = 'abcde'
print(napis[-1])  # 'e' („ostatni”)
print(napis[-2])  # 'd' („drugi od końca”)
print(napis[-5])  # 'a' („piąty od końca”)
e
d
a

Łańcuchy możemy też „kroić na plasterki” (_slicing):

napis = 'abcde'
print(napis[1:4])  # 'bcd' („znaki od 1. włącznie do 4. wyłącznie”)
print(napis[1:2])  # 'b' (to samo co `napis[1]`)
print(napis[-3:-1])  # 'cd' (kroić można też stosując indeksowanie od końca)
print(napis[1:-1])  # 'bcd' (możemy nawet mieszać te dwa sposoby indeksowania)
print(napis[3:])  # 'de' (jeżeli koniec przedziału nie jest podany, to kroimy do samego końca łańcucha)
print(napis[:3])  # 'abc' (jeżeli początek przedziału nie jest podany, to kroimy od początku łańcucha)
print(napis[:])  # 'abcde' (kopia całego napisu)
bcd
b
cd
bcd
de
abc
abcde

Biblioteka _NumPy

Tablice

Głównym obiektem w NumPy jest jednorodna, wielowymiarowa tablica. Przykładem takiej tablicy jest macierz x.

Macierz $x = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix}$ można zapisać jako:

import numpy as np

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

Najczęsciej używane metody tablic typu array:

x.shape  # wymiary macierzy x
(3, 3)
x.sum(axis=0)  # wektor złożony z sum elementów w każdej kolumnie
array([12, 15, 18])
x.min(axis=1)  # wektor złożony ze średnich elementów w każdym wierszu
array([1, 4, 7])

Do tworzenia sekwencji liczbowych jako obiekty typu array należy wykorzystać funkcję arange.

np.array(list(range(10)))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(5, 10)
array([5, 6, 7, 8, 9])
np.arange(5, 10, 2)
array([5, 7, 9])

Kształt tablicy można zmienić za pomocą metody reshape:

x = np.arange(1, 13)
print(x)
y = x.reshape(3, 4)
print(y)
[ 1  2  3  4  5  6  7  8  9 10 11 12]
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

Funkcją podobną do arange jest linspace, która wypełnia wektor określoną liczbą elementów z przedziału o równych automatycznie obliczonych odstępach (w arange należy podać rozmiar kroku):

x = np.linspace(0, 1, 10)
print(x)
[0.         0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
 0.66666667 0.77777778 0.88888889 1.        ]

Dodatkowe informacje o funkcjach NumPy uzyskuje się za pomocą polecenia help(nazwa_funkcji):

help(np.shape)
Help on function shape in module numpy:

shape(a)
    Return the shape of an array.
    
    Parameters
    ----------
    a : array_like
        Input array.
    
    Returns
    -------
    shape : tuple of ints
        The elements of the shape tuple give the lengths of the
        corresponding array dimensions.
    
    See Also
    --------
    alen
    ndarray.shape : Equivalent array method.
    
    Examples
    --------
    >>> np.shape(np.eye(3))
    (3, 3)
    >>> np.shape([[1, 2]])
    (1, 2)
    >>> np.shape([0])
    (1,)
    >>> np.shape(0)
    ()
    
    >>> a = np.array([(1, 2), (3, 4)], dtype=[('x', 'i4'), ('y', 'i4')])
    >>> np.shape(a)
    (2,)
    >>> a.shape
    (2,)

Tablice mogą składać się z danych różnych typów (ale tylko jednego typu danych równocześnie, stąd jednorodność).

x = np.array([1, 2, 3])
print(x, "- typ: ", x.dtype)

x = np.array([0.1, 0.2, 0.3])
print(x, "- typ: ", x.dtype)

x = np.array([1, 2, 3], dtype='float64')
print(x, "- typ: ", x.dtype)
[1 2 3] - typ:  int32
[0.1 0.2 0.3] - typ:  float64
[1. 2. 3.] - typ:  float64

Tworzenie tablic składających się z samych zer lub jedynek umożliwiają funkcje zeros oraz ones:

x = np.zeros([3,4])
print(x)
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
x = np.ones([3,4])
print(x)
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]

Podstawowe operacje arytmetyczne

Operatory arytmetyczne na tablicach w NumPy działają element po elemencie.

import numpy as np

a = np.array([3, 4, 5])
b = np.ones(3)
print(a - b)
[2. 3. 4.]

Za mnożenie macierzy odpowiadają funkcje dot i matmul (nie operator *):

a = np.array([[1, 2], [3, 4]])
print(a)
[[1 2]
 [3 4]]
b = np.array([[1, 2], [3, 4]])
print(b)
[[1 2]
 [3 4]]
a * b  # mnożenie element po elemencie
array([[ 1,  4],
       [ 9, 16]])
np.dot(a,b)  # mnożenie macierzowe
array([[ 7, 10],
       [15, 22]])
np.matmul(a,b)  # mnożenie macierzowe
array([[ 7, 10],
       [15, 22]])

Przykłady innych operacji dodawania i mnożenia:

a = np.zeros((2, 2), dtype='float')
a += 5
a
array([[5., 5.],
       [5., 5.]])
a *= 5
a
array([[25., 25.],
       [25., 25.]])
a + a
array([[50., 50.],
       [50., 50.]])

Sklejanie tablic:

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

Typowe funkcje matematyczne:

x = np.arange(1, 5)
np.sqrt(x) * np.pi
array([3.14159265, 4.44288294, 5.44139809, 6.28318531])
2**4
16
np.power(2, 4)
16
np.log(np.e)
1.0
x = np.arange(5)
x.max() - x.min()
4

Indeksy i zakresy

Tablice jednowymiarowe zachowują sie podobnie do zwykłych list pythonowych.

a = np.arange(10)
a[2:4]
array([2, 3])
a[:10:2]  # elementy do 10., co drugi element
array([0, 2, 4, 6, 8])
a[::-1]  # wszytkie elementy tablicy `a`, ale w odwróconej kolejności
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

Tablice wielowymiarowe mają po jednym indeksie na wymiar:

x = np.arange(12).reshape(3, 4)
x
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
x[2, 3]  # wiersz 2, kolumna 3
x[:, 1]  # kolumna 1
array([1, 5, 9])
x[1, :]  # wiersz 1
array([4, 5, 6, 7])
x[1:3, :]  # wiersze od 1. włącznie do 3. wyłącznie
array([[ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

Warunki

Warunki pozwalają na selekcję elementów tablicy.

a = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])
a[a > 1]
array([2, 2, 2, 3, 3, 3])
a[a == 3]
array([3, 3, 3])
np.where(a < 3)
(array([0, 1, 2, 3, 4, 5], dtype=int64),)
np.where(a < 3)[0]
array([0, 1, 2, 3, 4, 5], dtype=int64)
np.where(a > 9)
(array([], dtype=int64),)

Pętle i wypisywanie

for row in x:
    print(row)
[0 1 2 3]
[4 5 6 7]
[ 8  9 10 11]
# Operacja `.flat` "spłaszcza" macierz
for element in x.flat:
    print(element) 
0
1
2
3
4
5
6
7
8
9
10
11

Liczby losowe

np.random.randint(0, 10, 5)  # Tablica złożona z 5 liczb całkowitych wylosowanych z zakresu od 0 do 10
array([2, 0, 7, 3, 5])
np.random.normal(0, 1, 5)  # Tablica złożona z 5 liczb wylosowanych z rozkładu normalnego (0, 1)
array([-0.7907838 , -0.65971486,  0.0375355 ,  2.00045956,  0.32631216])
np.random.uniform(0, 2, 5)  # Tablica złożona z 5 liczb wylosowanych z rozkładu jednostajnego na przedziale (0, 1)
array([1.50130054, 1.20710594, 0.45451505, 0.70098876, 0.90371663])

Macierze

NumPy jest pakietem wykorzystywanym do obliczeń w dziedzinie algebry liniowej, co jeszcze szczególnie przydatne w uczeniu maszynowym.

Transpozycja to operacja zamiany wierszy na kolumny i na odwrót.

Wektor o wymiarach $1 \times N$ $$ x = \begin{pmatrix} x_{1} \\ x_{2} \\ \vdots \\ x_{N} \end{pmatrix} $$

i jego transpozycję  $x^\top = (x_{1}, x_{2},\ldots,x_{N})$ można wyrazić w Pythonie w następujący sposób:

import numpy as np

x = np.array([[1, 2, 3]]).T
x.shape
(3, 1)
xt = x.T
xt.shape
(1, 3)

Macierz kolumnowa w NumPy. $$X = \begin{pmatrix} 3 \\ 4 \\ 5 \\ 6
\end{pmatrix}$$

x = np.array([[3,4,5,6]]).T
x
array([[3],
       [4],
       [5],
       [6]])

Macierz wierszowa w NumPy. $$ X = \begin{pmatrix} 3 & 4 & 5 & 6 \end{pmatrix}$$

x = np.array([[3,4,5,6]])
x
array([[3, 4, 5, 6]])

Oprócz obiektów typu array istnieje wyspecjalizowany obiekt matrix, dla którego operacje * (mnożenie) oraz **-1 (odwracanie) są określone w sposób właściwy dla macierzy (w przeciwieństwie do operacji elementowych dla obiektów array).

x = np.array([1,2,3,4,5,6,7,8,9]).reshape(3,3)
print(x)
[[1 2 3]
 [4 5 6]
 [7 8 9]]
y = np.array([4,6,3,8,7,1,3,0,3]).reshape(3,3)
print(y)
[[4 6 3]
 [8 7 1]
 [3 0 3]]
X = np.matrix(x)
Y = np.matrix(y)
print(x * y)  # Tablice np.array mnożone są element po elemencie
[[ 4 12  9]
 [32 35  6]
 [21  0 27]]
print(X * Y)  # Macierze np.matrix mnożone są macierzowo
[[ 29  20  14]
 [ 74  59  35]
 [119  98  56]]
print(np.matmul(x, y))
[[ 29  20  14]
 [ 74  59  35]
 [119  98  56]]

Wyznacznik macierzy

a = np.array([[3,-9],[2,5]])
np.linalg.det(a)
33.000000000000014

Macierz odwrotna

A = np.array([[-4,-2],[5,5]])
A
array([[-4, -2],
       [ 5,  5]])
invA = np.linalg.inv(A)
invA
array([[-0.5, -0.2],
       [ 0.5,  0.4]])
B = np.matrix(A)
invB = B**-1
print(invB)
[[-0.5 -0.2]
 [ 0.5  0.4]]
np.round(np.dot(A, invA))
array([[1., 0.],
       [0., 1.]])

(ponieważ $AA^{-1} = A^{-1}A = I$).

Wartości i wektory własne

a = np.diag((1, 2, 3))
a
array([[1, 0, 0],
       [0, 2, 0],
       [0, 0, 3]])
w, v = np.linalg.eig(a)
print(w)  # wartości własne
print(v)  # wektory własne
[1. 2. 3.]
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Biblioteka PyTorch

Biblioteka PyTorch została stworzona z myślą o uczeniu maszynowym. Oprócz wykonywania rozmaitych działań matematycznych takich jak te, które można wykonywać w bibliotece NumPy, dostarcza metod przydatnych w uczeniu maszynowym, z których chyba najbardziej charakterystyczną jest automatyczne różniczkowanie (moduł autograd).

Ale o tym później.

Instalacja

pip install torch torchvision

lub

conda install pytorch torchvision torchaudio cudatoolkit=10.2 -c pytorch

Tensory

Podstawowym typem danych dla pakietu pytorch jest tensor (torch.tensor). Tensor to uogólnienie macierzy na dowolną liczbę wymiarów. Można powiedzieć, że macierze są dwuwymiarowymi tensorami.

import torch

x = torch.tensor([[1,2,3],[4,5,6],[7,8,9]])
print(x)
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

Operacje na tensorach

Działania na tensorach w bibliotece PyTorch wykonuje się bardzo podobnie do działań na miacierzach w bibliotece NumPy. Czasami nazwy metod się trochę różnią.

# Wymiary (rozmiar) tensora

print(x.shape)
print(x.size())  # Można użyć `size()` zamiast `shape`
torch.Size([3, 3])
torch.Size([3, 3])
# Typy elementów

x = torch.tensor([1, 2, 3])
print(x, "- type:", x.dtype)

x = torch.tensor([0.1, 0.2, 0.3])
print(x, "- type:", x.dtype)

x = torch.tensor([1, 2, 3], dtype=torch.float64)  # Uwaga: inaczej niż w NumPy
print(x, "- type:", x.dtype)
tensor([1, 2, 3]) - type: torch.int64
tensor([0.1000, 0.2000, 0.3000]) - type: torch.float32
tensor([1., 2., 3.], dtype=torch.float64) - type: torch.float64
x = torch.zeros([3,4])
print(x)
tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]])
x = torch.ones([3,4])
print(x)
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])
x = torch.rand([3,4])
print(x)
tensor([[0.9863, 0.9173, 0.5301, 0.4279],
        [0.7708, 0.4671, 0.2965, 0.0578],
        [0.6684, 0.4432, 0.9817, 0.1521]])
# Iterowanie po elementach tensora

for i, row in enumerate(x):
    print(f"\nWiersz {i}:")
    for element in row:
        print(element.item())  # `item()` zamienia jednoelementowy (bezwymiarowy) tensor na liczbę
Wiersz 0:
0.9863188862800598
0.917273998260498
0.53009432554245
0.42788761854171753

Wiersz 1:
0.7708230018615723
0.46713775396347046
0.2964947819709778
0.057803571224212646

Wiersz 2:
0.6684107780456543
0.4432327151298523
0.9817106127738953
0.15205740928649902
# Przykładowe macierze

A = torch.rand([3, 4])
print(A)

B = torch.rand([3, 4])
print(B)

C = torch.rand([4, 2])
print(C)
tensor([[0.1572, 0.4592, 0.7481, 0.6673],
        [0.1138, 0.9820, 0.4452, 0.5775],
        [0.7510, 0.3174, 0.6937, 0.8904]])
tensor([[0.0661, 0.6596, 0.7498, 0.5254],
        [0.3271, 0.8968, 0.3188, 0.9255],
        [0.2099, 0.5828, 0.4611, 0.6856]])
tensor([[0.0738, 0.3776],
        [0.2646, 0.5449],
        [0.6779, 0.0567],
        [0.0348, 0.3072]])
# Działania "element po elemencie"

print(A + B)
print(A - B)
print(A * B)
print(A / B)
tensor([[0.2232, 1.1188, 1.4978, 1.1928],
        [0.4409, 1.8788, 0.7640, 1.5029],
        [0.9609, 0.9002, 1.1548, 1.5760]])
tensor([[ 0.0911, -0.2005, -0.0017,  0.1419],
        [-0.2133,  0.0853,  0.1264, -0.3480],
        [ 0.5410, -0.2654,  0.2326,  0.2048]])
tensor([[0.0104, 0.3029, 0.5609, 0.3506],
        [0.0372, 0.8807, 0.1419, 0.5344],
        [0.1577, 0.1850, 0.3199, 0.6105]])
tensor([[2.3777, 0.6961, 0.9977, 1.2701],
        [0.3478, 1.0951, 1.3966, 0.6240],
        [3.5770, 0.5446, 1.5044, 1.2988]])
# Mnożenie macierzowe

print(torch.matmul(A, C))
tensor([[0.6635, 0.5570],
        [0.5902, 0.7807],
        [0.6406, 0.7694]])

Konwersja między PyTorch i NumPy

# Konwersja z PyTorch do NumPy

print(A)

A_numpy = A.numpy()
print(A_numpy)
tensor([[0.1572, 0.4592, 0.7481, 0.6673],
        [0.1138, 0.9820, 0.4452, 0.5775],
        [0.7510, 0.3174, 0.6937, 0.8904]])
[[0.15715027 0.45915365 0.7480644  0.66733134]
 [0.11377418 0.98203135 0.4451999  0.5774748 ]
 [0.7509776  0.3174067  0.69367564 0.8904279 ]]
# Konwersja z numpy do PyTorch

X = np.random.rand(3, 5)
print(X)

X_pytorch = torch.from_numpy(X)
print(X_pytorch)
[[0.84580006 0.49270934 0.67969751 0.27546956 0.10600392]
 [0.84610871 0.11680263 0.3535065  0.83725955 0.07995571]
 [0.4586334  0.64818257 0.53201793 0.77786372 0.8584107 ]]
tensor([[0.8458, 0.4927, 0.6797, 0.2755, 0.1060],
        [0.8461, 0.1168, 0.3535, 0.8373, 0.0800],
        [0.4586, 0.6482, 0.5320, 0.7779, 0.8584]], dtype=torch.float64)

Przydatne materiały