Глава 18 Обчислюємо з NumPy

⏱️ Час на опанування теми: 15 хвилин

🤷 Для чого ми це вивчаємо:

🔑 Результати навчання:

  • Розуміння що таке програма, додаток та програмне забезпечення
  • Розуміння що таке алгоритм, кодування та програмування

🎈 Увага: Наразі ця глава знаходиться у стані активної розробки і ймовірно буде змінюватись і доповнюватись!


У цій главі…


NumPy є вже класичним Python пакетом, і мабудь якщо ви спитаєте будь-кого, хто працює з DS, ML та AI, він чи вона 100% чули про цей пакет. Назва NumPy – це скорочення від Numerical **Python, тобто числовий Python і цей пакет відповідно до свого ім’я використовується для швидких та ефективних числових обчіслень у Python.

Перед тим як пірнути у море практичних навичок, давайте спершу розберемось чому ми будемо використовувати пакет NumPy. По-перше, NumPy містить велику кількість корисних функцій, які нам не треба будувати самим з нуля. По-друге, NumPy набагато швидший ніж звичайний Python і об’єкти створені в NumPy змаймають набагато меньше пам’яті. І останній, але дуже важливий пункт – такі бібліотеки, як scikit-learn і keras які ми будемо використовувати для machine learning та deep learning використовують об’єкти з NumPy.

18.1 Розбираємося що таке масиви

Ключовим об’єктом NumPy є багатомірний масив, який називається ndarray (вимовляється як ен-ді-ерей). ndarray – це скорочення від англійського n-dimensional array, тобто n-вимірний масив. Ми будемо використовувати терміни масив та ndarray пліч-о-пліч. Зараз терміни ці не зрозумілі, тому розбираємось. Якщо говорити формально, то масив – це впорядкований набір однотипних елементів. Але давайте, як завжди, подивимось що це таке на прикладі.

Розглянемо випадок коли нам треба проаналізувати річні оцінки у школі. Скажімо учениця Інна отримала дев’ятку з математики. Ми можемо створити нову змінну з ім’ям inna у яку ми запишемо дев’ять:

Якщо у нас з’явились нові дані про оцінки з біології та укрїнської, ми звичайно ж можемо створити ще дві окремі змінні для цих предметів (і також переіменуємо оцінку з математики заради узгодженності):

А що тоді робити з літературою, географією, англійською, фізрою, тощо? Якщо ми будемо створювати окрему змінну для кожного предмету, то нам буде дуже легко заплутатись та зробити помилку у коді. Замість цього ми можемо “зклеїти” оцінки з математики, української та біології між собою і створити масив. Масив дозволяє зберігати пов’язані між собою значення в одній змінній.

Цей масив – одновимірний, який ми можемо візуалізувати як ланцюжок значень. Цих значень у ланцюжку може бути скільки завгодно і обмежується тільки пам’яттю вашого комп’ютера…

Стоп, стоп, стоп. Ми ж здається вже таке робили, коли працювали зі списками list у Главі 7? І це майже вірно, тому що одновимірні масиви дуже схожі на списки. Є одна ключова відмінність: елементи масивів повинні мати один і той самий тип, на відміну від list, в якому елементи можуть мати різні типи.

Їдемо далі – Інна не єдина учениця в класі. Також є, наприклад, Андірй, Марія та Оксана. І тут така сама історія, ми звичайно ж можемо створити окрему змінну для кожного учня, але створювати змінні для кожного учня у школі буде теж якось незграбно.

І тут ми можемо використати такуж ідею, коли ми з окремих змінних “зліпили” одновимірний масив. Приліпляючи одновимірні масиви однакової довжини один до одного ми отримаємо двовимірний масив. Наш новий масив міститиме оцінки з різних предметів для кожного учня в класі.

До тривимірних та чотиривимірних масивів ми повернемось трохи згодом. Зараз нам треба пам’ятати, що одномірний масив виглядає як ланцюжок або рядок значень, а двомірний - як таблиця. Двомірний масив нам дуже знадобиться в курсі Machine Learning. Перейдемо ж до більш практичинх штук.

18.2 Завантажуємо бібліотеку NumPy

Так як ми працюємо у Google Colab, то NumPy вже встановленно і нам не треба нічого робити. Перед тим як використовувати пакет NumPy у Google Colab записнику, нам треба його завантажити. Це можна зробити використовуючи інструкцію import. За конвенцією NumPy, ми переіменуємо її до np. Це дозволить нам не писати кожного разу п’ять літер numpy, а тільки дві np:

import numpy as np

18.3 Створюємо масив ndarray

Найчастіше ми будемо створювати ndarray за допомогою функції np.array(). Аргументом цієї функції можуть бути об’єкти різних типів, але найчастіше ми будемо використовувати наш улюблений список list.

Якщо ви ще пам’ятаєте як створювати list в Python (Глава 7), вважайте що ви вже вмієте створювати ndarray. Якщо ви не пам’ятаєте або не знаєте що це таке, це не проблема, тому що синтаксис дуже простий, зрозумілий та природній. Щоб створити list, нам просто треба “загорнути” значення у квадратні дужки.

Давайте зараз створемо list, який буде містити оцінки у школі і потім використаємо як аргумент до нашої функції np.array():

inna = np.array([9, 8, 10])

inna
## array([ 9,  8, 10])

Увага: клас об’єкту називається ndarray, a функція щоб його створити просто np.array().

Фунцкію np.array() ми також використовуємо для створення двовимірних масивів. Так само ми будемо створювати використовуючи list. Але цього разу, кожен елемент цього list буде також list. Знов ж таки, якщо ви не пам’ятаєте що таке list, просто загорніть значення кожного рядку в квадратні дужки, а потім і самі рядки:

class10b = np.array([[9, 8, 10], [7, 12, 9], [12, 11, 11], [10, 12, 9]])
class10b
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])

Також існує більше сорока допоміжних функцій, які дозволяють створити різні ndarray. Ми не будемо розглядати кожну з них, а розглянемо тільки кілька корисних функцій. Наприклад, np.arange(). Вона створює ndarray з рівномірно розподілениими значеннями в заданому інтервалі. Якщо ви пам’ятаєте функцію range() (Глава ??), то це її NumPy еквівалент.

Скажімо я хочу створити ndarray який міститеме числа від нуля до дев’яти:

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

Далі, дуже корисні функції np.zeros() та np.ones(). Вони створюють ndarray, усі елементи яких нулі або одиниці, відповідно. Нам треба зазначити тільки розмір, тобто якщо це двовимірний масив, то кількість рядків та стовпчиків.

ones = np.ones(shape=(4, 3))
ones
## array([[1., 1., 1.],
##        [1., 1., 1.],
##        [1., 1., 1.],
##        [1., 1., 1.]])
zero = np.zeros(shape=(4, 3))
zero
## array([[0., 0., 0.],
##        [0., 0., 0.],
##        [0., 0., 0.],
##        [0., 0., 0.]])

18.4 Працюємо з атребутами ndarray

Коли ми будемо більш конкретно працювати з ndarray, рано чи пізно нам знадобиться інформація про нього. Ну наприклад, який це ndarray, одновимірний чи двовимірний? Щоб дізнатись кількість вимірів, ми можемо подивитись на значення атрібуту ndim:

inna.ndim
## 1

Змінна inna дійсно є одновимірним ndarray. Давайте переконаємось, що class10b є двовимірним масивом:

class10b.ndim
## 2

Йдемо далі. В нашому ndarray inna ми маємо три значення. Іншими словами, довжина цього одновимірного масива дорівню трьом. Щоб отримати це значення, ми роздрукуємо занчення атрибуту shape:

inna.shape
## (3,)

Ndarray class10b має три стовпчики та чотири рядки. Кількість рядків та стовпчиків теж знаходиться в атрибуті shape:

class10b.shape
## (4, 3)

Останній атрибут який ми розглянемо – це тип даних. Як і скаляр, NumPy ndarray має свій тип даних, який до речі, нагадую, один і той самий для усіх елементів масиву. Значення цього атрібуту можна отримати, якщо зазначити атрибут dtype після крапки. Давайте подивимось на тип даних нашої змінної inna:

inna.dtype
## dtype('int64')

Тип даних innaint, тобто ціле число. Також у нас є float для дійсних чисел. Давайте швиденько подивимось:

temp = np.array([36.6])
temp.dtype
## dtype('float64')

Так само як і у скалярів, у нас ще є тип bool, який приймає або True або False значення:

good_student = np.array([True])
good_student.dtype
## dtype('bool')

Ми також можемо зберігати тексти та символи у ndarray, елементи якого будуть мати тип str. Але це досить рідкісний випадок, тому ми не будемо його розглядати.

18.5 Використовуємо оператори для ndarray

NumPy дуже зручний коли йдеться про арефметичні операції. Під арефметичними операціями, ми маємо на увазі додавання, віднімання, множення і ділення. Ці операції виконуються поелементно. Наприклад, якщо ми додамо два ndarray поелементно, це означає що ми додамо перший елемент до першого, другий - до другого, і так далі.

Давайте порахуємо скільки Інна та Андрій отримали балів разом з кожного предмету:

inna = np.array([9, 8, 10])
andrii = np.array([7, 12, 9])

inna + andrii
## array([16, 20, 19])

Такм же чином, ми можемо помножити ці ndarray. Це не буде мати фізичного сенсу, проте продемонструє як працює арифметика:

inna * andrii
## array([63, 96, 90])

Це правило також поширюється і на масиви з більшою кількістю вимірів:

class10b 
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])
ones
## array([[1., 1., 1.],
##        [1., 1., 1.],
##        [1., 1., 1.],
##        [1., 1., 1.]])
class10b + ones
## array([[10.,  9., 11.],
##        [ 8., 13., 10.],
##        [13., 12., 12.],
##        [11., 13., 10.]])

Якщо ми замість другого або першого ndarray підставимо скаляр, то кожний елемент з цього ndarray буде, наприклад, помноженно на цей скаляр:

class10b * 2
## array([[18, 16, 20],
##        [14, 24, 18],
##        [24, 22, 22],
##        [20, 24, 18]])

Увага! У математиці двовимірні масиви мають спеціальну назву – матриці. Математичне множення матриць виконується зовсім по іншому ніж поелементне множення і в NumPy існує спеціальна функція, яка називається np.matmul(). До речі, NumPy також може транспонувати матрицю за допомогою функції np.transpose(). Взагалі, NumPy містить в собі величезну кількість функцій пов’язаних з лінійною алгеброю, про які ви можете почитати в офіційній документації.

18.6 Використовуємо універсальні функції ufunc

Також NumPy має великий набір корисних функцій, які застосовуються до кожного елементу. Давайте подивимось на декілька з них:

inna
## array([ 9,  8, 10])
np.sqrt(inna)
## array([3.        , 2.82842712, 3.16227766])
class10b
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])
np.log(class10b)
## array([[2.19722458, 2.07944154, 2.30258509],
##        [1.94591015, 2.48490665, 2.19722458],
##        [2.48490665, 2.39789527, 2.39789527],
##        [2.30258509, 2.48490665, 2.19722458]])

Перелік цих фунцкій включає експоненційну функцію, синус, косинус, та багато іншого.

18.7 Узагалнюємо із статистичними методами

Дуже часто нам буде цікаво подивитись на узагальнення наших даних. Наприклад, на середній бал, мінімальну оцінку, максимальну оцінку тощо. Такі значення називаються статичтичними показниками, і NumPy має цілу ниску цих фунцкій. Давайте подивимось на показники, які ви зразу ж можете зрозуміти та використовувати.

Наприклад, щоб знайти середній бал Інни, нам достатньо прописати настпну інструкцію:

np.mean(class10b)
## 10.0

Мінімальний та максимальний бали, відповідно:

np.min(class10b)
## 7
np.max(class10b)
## 12

18.8 Звертаємось до елементів

В багатьох випадках нам буде необхідно отримати значення якогось елементу або елементів з ndarray. Наприклад, нас цікавить оцінка Інни з біології – то нам треба отримати елемент з індексом два. Індексація в ndarray починається з нуля, так само як і у списка list. Щоб отримати елемент одновимірного масиву за заданим ідексом, нам достатьно написати ім’я змінної і індекс загорнутий в квадратні дужки. Давайте спробуємо:

inna[2]
## 10
zero_to_nine[4]
## 4

У NumPy теж реалізован слайсінг. Як ми вже казали у Главі 7, слайсінг дозволяє отримати декілька елементів (тобто зріз) однією командою. Для цього треба зазначити початковий індекс, двокрапку і останній індекс.

Використаємо слайсінг на zero_to_nine:

zero_to_nine
## array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
zero_to_nine[1:5]
## array([1, 2, 3, 4])

Як бачимо, у результаті цієї команди ми отримали елементи із індексами 1, 2, 3 та 4. Тобто усі числа з індексу один включно і до самої п’ятірки. При чому елемент з індексом 5 ми не включили.

Давайте ще раз спробуємо отримати елементи з індексами 3, 4 та 5. Треба зазначити спочатку 3, тому що перший елемент до двокрапок ми включаємо, далі двокрапку, і шість – тому що шість ми вже не включимо

zero_to_nine[3:6]
## array([3, 4, 5])

До речі, усі трюки які ми бачили коли дивились на слайсігу списків, як наприклад від’ємні індекси або дві двокрапки, можна використати і з ndarray.

Якщо ми хочемо прикзначити нове значення якомусь елементу в ndarray, ми також можемо використати індексацію та слайсінг:

zero_to_nine[2] = 5
zero_to_nine
## array([0, 1, 5, 3, 4, 5, 6, 7, 8, 9])
zero_to_nine[4:6] = 5
zero_to_nine
## array([0, 1, 5, 3, 5, 5, 6, 7, 8, 9])

В двовимірних масивах, кожен елемент має два індекси. Щоб отримати значення елементу за індексами, треба написати і’мя змінної, потім викдрити квадратні дужки, зазначити перший індекс, кома, другий індекс, та зачинити квадратну дужку. Давайте спробуємо на class10b:

class10b
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])
class10b[1, 2]
## 9

Ми також можемо використати slicing для першого або другого індексу. Я хочу отримати оцінки з української для Андрія та Оксани:

class10b
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])
class10b[0:2, 1]
## array([ 8, 12])

Аналогічно до одновимірного масиву, ми можемо змінити елементи ndarray. Наприклад, ми зробили помилку, і Інна отримала одинадцять з математики. Щоб скорегувати цю оцінку, достатньо призначити елементу з індексами нуль та нуль оцінку в одинадцять балів.

class10b
## array([[ 9,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])
class10b[0, 0] = 11
class10b
## array([[11,  8, 10],
##        [ 7, 12,  9],
##        [12, 11, 11],
##        [10, 12,  9]])

18.9 Опановуємо масиви, які мають три та більше вимірів

Пам’ятаєте як ми отримали з одновимірного масиву двовимірний? Правильно, ми взяли декілька одновимірних, та приєднали один до одного. Так само ми можемо зробити і з тривимірними – взяти декілька двовимірних та приклеїти один до одного. Йдемо далі, так само можна зробити і з тривимірними, беремо кілька тривимірних, приклеюємо їх один до одного, і вуаля – отримаємо чотиривимірний.

Тривимірні масиви нам знадобляться для збереження зображень. Коли цих зображень буде декілька і нам треба буде помістити їх в один об’єкт, тоді ми використаємо чотиривимірні масиви. Взагалі, якщо не брати до уваги відео дані, то чотирьох вимірів нам як правило буде достатньо. Але все одно ви вже знаєте як утворити масиви з більшою кількістю вимірів. А ще в deep learning, масиви прийнято називати тензорами.

Давайте шивденько подивимось, як працювати з тривимірними та чотиривимірними масивами.

Наприклад, давайте створимо один трьохвимірний і один чотиривимірний масив:

array3d = np.arange(27)
array3d.shape = (3, 3, 3)

array3d
## array([[[ 0,  1,  2],
##         [ 3,  4,  5],
##         [ 6,  7,  8]],
## 
##        [[ 9, 10, 11],
##         [12, 13, 14],
##         [15, 16, 17]],
## 
##        [[18, 19, 20],
##         [21, 22, 23],
##         [24, 25, 26]]])
array3d[0, 1, 2]
## 5
array4d = np.arange(81)
array4d.shape = (3, 3, 3, 3)
array4d[0, 0, 0, 2]
## 2


🤸 Вправи
1. Оберіть правильні твердження щодо масивів ndarrays:
2. Котрий рядок коду правильно створить масив ndarray?
3. Який атрибут дозволяє дізнатись кількість вимірів ndarray?
4. Що показує значення атрибуту .shape для одновимірного ndarray?
5. Які арефметичні операції можна виконати з двома ndarray?
6. Як отримати елемент з індексом 3 з одновимірного ndarray?
7. Яким індексом в ndarrayпочинається індексація?
8. Що буде результатом виконання коду zero_to_nine[1:5]?
9. Що буде результатом виконання коду class10b[0, 0] = 8?