Pytorch Intro

Тензори та автоматичне диференціювання в PyTorch

Основи роботи з тензорами, операції, broadcasting, граф обчислень та механізм autograd для автоматичного обчислення градієнтів.

🎯 Мета статті

  • Зрозуміти, що таке тензори та чим вони відрізняються від NumPy масивів
  • Навчитися створювати та маніпулювати тензорами в PyTorch
  • Опанувати операції broadcasting для ефективних обчислень
  • Розібратися у механізмі автоматичного диференціювання (autograd)
  • Навчитися обчислювати градієнти для складних функцій автоматично

🔑 Ключові концепції

  • Тензор (tensor) — багатовимірний масив даних, основна структура PyTorch
  • Autograd — система автоматичного обчислення градієнтів
  • Граф обчислень (computational graph) — структура для відстеження операцій
  • Broadcasting — автоматичне розширення форм для сумісності операцій
  • Backpropagation — алгоритм зворотного поширення помилки

Вступ: Чому NumPy недостатньо для Deep Learning

У попередніх модулях ми активно використовували NumPy для роботи з даними, побудови лінійних моделей та базових нейронних мереж. NumPy — чудова бібліотека для наукових обчислень, але коли справа доходить до створення глибоких нейронних мереж, вона має три критичні обмеження:

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam rectangle { RoundCorner 8 BorderColor #94A3B8 FontSize 12 }

rectangle "Центральний процесор (CPU)" as cpu #EFF6FF { card "Мала кількість потужних ядер (4-16 ядер)\nОптимізовано для послідовних складних інструкцій" as cpu_cores card "Велика кеш-пам'ять (L1/L2/L3) + Низька затримка" as cpu_cache card "Швидкість: базова для послідовних задач" as cpu_speed cpu_cores -down-> cpu_cache cpu_cache -down-> cpu_speed }

rectangle "Графічний процесор (GPU)" as gpu #ECFDF5 { card "Тисячі паралельних мікроядер (CUDA Cores, Tensor Cores)\nОптимізовано для масових паралельних операцій SIMD" as gpu_cores card "Висока пропускна здатність пам'яті (HBM / GDDR6)" as gpu_mem card "Швидкість: у 20-100 разів швидше для матричних множень" as gpu_speed gpu_cores -down-> gpu_mem gpu_mem -down-> gpu_speed }

@enduml

🚫 Обмеження NumPy

1. Відсутність підтримки GPU NumPy виконує всі обчислення на центральному процесорі (CPU). Для великих нейронних мереж це означає години або навіть дні тренування.

2. Немає автоматичного диференціювання Щоб навчити нейронну мережу, потрібно обчислити градієнти для тисяч параметрів. У NumPy це доводиться робити вручну — десятки рядків коду для кожного шару.

3. Відсутність високорівневих компонентів Немає готових реалізацій згорткових шарів, рекурентних мереж, attention-механізмів. Доводиться писати все з нуля.

✅ Рішення: PyTorch

PyTorch вирішує всі ці проблеми:

  • GPU-прискорення: тренування у 20-100 разів швидше
  • Autograd: градієнти обчислюються автоматично для будь-якої функції
  • Neural Networks API: готові шари, функції втрат та оптимізатори
  • Динамічні графи: гнучкість як у Python, швидкість як у C++

Аналогія: Якщо NumPy — це як збирати меблі IKEA без інструкції та інструментів, то PyTorch — це повний набір професійних інструментів із покроковою інструкцією та підказками на кожному етапі.

Встановлення PyTorch

Перед початком роботи встановимо PyTorch. Вибір версії залежить від наявності GPU:

pip install torch torchvision torchaudio
Якщо у вас є NVIDIA GPU, обов'язково встановіть версію з підтримкою CUDA — це дасть прискорення у 20-50 разів. Перевірити наявність GPU можна командою nvidia-smi у терміналі.

Перевіримо, що PyTorch встановлено коректно:

pytorch_check.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

print(f"PyTorch версія: {torch.__version__}")
print(f"CUDA доступна: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"GPU: {torch.cuda.get_device_name(0)}")
Out[1]:
Параметр системиЗначенняСтатус
PyTorch версія2.1.0✅ Встановлено
CUDA доступнаTrue⚡ Активовано
Апаратний прискорювач (GPU)NVIDIA GeForce RTX 3080🚀 Готово до обчислень

Частина 1: Тензори — основа PyTorch

Що таке тензор?

Тензор (tensor) — це багатовимірний масив чисел, основна структура даних у PyTorch. Тензори схожі на NumPy масиви, але мають додаткові можливості: можуть виконуватися на GPU та автоматично відстежувати операції для обчислення градієнтів.

Аналогія: Тензор — це як універсальний контейнер для даних. Скаляр (одне число) — тензор 0-го рангу, вектор (список чисел) — тензор 1-го рангу, матриця (таблиця) — тензор 2-го рангу, а зображення RGB (висота × ширина × 3 канали кольору) — тензор 3-го рангу.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam state { BackgroundColor #F8FAFC BorderColor #94A3B8 FontColor #0F172A FontSize 12 ArrowColor #64748B }

title Ієрархія рангів тензорів: від скаляра до пакетів зображень

state "0D: Скаляр (torch.tensor(42))" as D0 #EFF6FF : Одне число без осей (shape: ) state "1D: Вектор (torch.tensor(1, 2, 3))" as D1 #F0FDF4 : Одновимірний масив (shape: 3) state "2D: Матриця (torch.zeros(3, 3))" as D2 #FEF3C7 : Рядки та стовпці (shape: 3, 3) state "3D: Об'ємний тензор (Зображення RGB)" as D3 #FEE2E2 : Канали x Висота x Ширина (shape: 3, H, W) state "4D: Батч тензорів (Пакет зображень)" as D4 #F3E8FF : Батч x Канали x Висота x Ширина (shape: B, C, H, W)

D0 -down-> D1 : Додавання осі виміру D1 -down-> D2 : Додавання площини D2 -down-> D3 : Додавання глибини (RGB канали) D3 -down-> D4 : Об'єднання у пакет (Batch)

@enduml

📊 Ранги тензорів

РангФормаПриклад використання
0D()Скаляр: температура, вік
1D(n,)Вектор: ціни, оцінки
2D(n, m)Матриця: таблиця даних
3D(h, w, c)Зображення RGB
4D(b, c, h, w)Батч зображень
5D(b, c, d, h, w)Батч 3D-сканів (МРТ, КТ)

🔄 NumPy vs PyTorch

ОпераціяNumPyPyTorch
Створення масивуnp.array([1,2])torch.tensor([1,2])
Нуліnp.zeros((3,3))torch.zeros(3,3)
Випадкові числаnp.random.rand()torch.rand()
Формаarr.shapetensor.shape
Зміна формиarr.reshape()tensor.view()
Транспонуванняarr.Ttensor.T

Створення тензорів

Розглянемо основні способи створення тензорів у PyTorch:

1. З Python-списків

tensor_from_list.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Одновимірний тензор (вектор)
vector = torch.tensor([1, 2, 3, 4, 5])
print(f"Вектор: {vector}")
print(f"Форма: {vector.shape}")
print(f"Ранг: {vector.ndim}")
Out[1]:
Об'єктЗначення тензораФорма (shape)Ранг (ndim)Тип даних (dtype)
Векторtensor([1, 2, 3, 4, 5])torch.Size([5])1torch.int64
In [2]:
# Двовимірний тензор (матриця)
matrix = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
])
print(f"Матриця:\n{matrix}")
print(f"Форма: {matrix.shape}")
print(f"Ранг: {matrix.ndim}")
Out[2]:
Рядок матриціСтовпчик 0Стовпчик 1Стовпчик 2Форма / Ранг
Рядок 0123torch.Size([2, 3])
Рядок 1456Ранг: 2
In [3]:
# Тривимірний тензор (наприклад, RGB зображення 2×2 пікселі)
image = torch.tensor([
    [[255, 0, 0], [0, 255, 0]],      # перший рядок пікселів
    [[0, 0, 255], [255, 255, 0]]     # другий рядок пікселів
])
print(f"Зображення:\n{image}")
print(f"Форма: {image.shape}")  # (висота, ширина, канали)
Out[3]:
Канал кольоруВисота $\times$ ШиринаЗначення зрізу $2 \times 3$Форма тензора
Червоний (R)$2 \times 3$ пікселі[[255, 0, 0], [0, 255, 0]]torch.Size([3, 2, 3])
Зелений (G)$2 \times 3$ пікселі[[0, 255, 0], [255, 255, 0]]Ранг: 3 (3D Tensor)
Синій (B)$2 \times 3$ пікселі[[255, 0, 255], [0, 255, 255]]Тип: torch.int64
Для створення тензорів завжди використовуйте torch.tensor(), а не torch.Tensor(). Перший створює тензор із автоматичним визначенням типу даних, другий завжди створює тензор типу float32, що може призвести до неочікуваних помилок.

2. З NumPy масивів

PyTorch має відмінну інтеграцію з NumPy — можна легко конвертувати масиви в обидва боки:

numpy_to_torch.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np
import torch

# NumPy масив → PyTorch тензор
np_array = np.array([[1, 2, 3], [4, 5, 6]])
tensor = torch.from_numpy(np_array)

print(f"NumPy масив:\n{np_array}")
print(f"PyTorch тензор:\n{tensor}")
print(f"Тип даних NumPy: {np_array.dtype}")
print(f"Тип даних PyTorch: {tensor.dtype}")
Out[1]:
Стан об'єктівNumPy масив (np.ndarray)PyTorch тензор (from_numpy)Спільна пам'ять (Zero-copy)
Початковий стан[[1, 2, 3], [4, 5, 6]]tensor([[1, 2, 3], [4, 5, 6]])✅ Спільний буфер RAM
Після модифікації np[0,0]=99[[99, 2, 3], [4, 5, 6]]tensor([[99, 2, 3], [4, 5, 6]])⚡ Миттєва синхронізація
In [2]:
# PyTorch тензор → NumPy масив
tensor = torch.tensor([[1.5, 2.5], [3.5, 4.5]])
np_array = tensor.numpy()

print(f"PyTorch тензор:\n{tensor}")
print(f"NumPy масив:\n{np_array}")
Out[2]:
Етап перетворенняPyTorch тензор (torch.Tensor)NumPy масив (.numpy())Результат зв'язку
Вихідний тензор[[1.5, 2.5], [3.5, 4.5]][[1.5, 2.5], [3.5, 4.5]]Спільна область пам'яті
Після t[0,0] = 99.0[[99.0, 2.5], [3.5, 4.5]][[99.0, 2.5], [3.5, 4.5]]✅ Синхронна зміна масиву
Увага:torch.from_numpy() створює тензор, що ділить пам'ять з оригінальним NumPy масивом. Зміна одного з них автоматично змінить інший! Якщо потрібна незалежна копія, використовуйте torch.tensor(np_array) або .clone().

3. Спеціальні тензори

PyTorch надає зручні функції для створення тензорів зі спеціальними значеннями:

special_tensors.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Тензор, заповнений нулями
zeros = torch.zeros(3, 4)
print(f"Нулі (3×4):\n{zeros}\n")

# Тензор, заповнений одиницями
ones = torch.ones(2, 3)
print(f"Одиниці (2×3):\n{ones}\n")

# Одинична матриця (діагональ з одиниць)
identity = torch.eye(4)
print(f"Одинична матриця 4×4:\n{identity}")
Out[1]:
Функція ініціалізаціїОтриманий тензорФормаТип даних (dtype)
torch.zeros(3, 4)Матриця $3 \times 4$, заповнена нулями 0.0torch.Size([3, 4])torch.float32
torch.ones(2, 3)Матриця $2 \times 3$, заповнена одиницями 1.0torch.Size([2, 3])torch.float32
torch.eye(3)Одинична діагональна матриця $I_3$torch.Size([3, 3])torch.float32
torch.full((2, 2), 7)Матриця $2 \times 2$, заповнена константою 7torch.Size([2, 2])torch.int64
In [2]:
# Послідовність чисел (як range у Python)
sequence = torch.arange(0, 10, 2)  # від 0 до 10 з кроком 2
print(f"Послідовність: {sequence}\n")

# Рівномірно розподілені числа (як linspace у NumPy)
linspace = torch.linspace(0, 1, 5)  # 5 чисел від 0 до 1
print(f"Linspace: {linspace}")
Out[2]:
Метод генераціїЗгенерований векторПараметри викликуПризначення
torch.arange(0, 10, 2)tensor([0, 2, 4, 6, 8])Старт 0, стоп 10, крок 2Індексація та цикли
torch.linspace(0, 1, 5)tensor([0.00, 0.25, 0.50, 0.75, 1.00])$N=5$ рівномірних точок на $0, 1$Сітки значень та графіки
torch.logspace(0, 3, 4)tensor([1., 10., 100., 1000.])Степені $10^0 \dots 10^3$ ($N=4$)Сітки гіперпараметрів (Learning Rate)

4. Випадкові тензори

Ініціалізація випадковими значеннями — критично важлива для навчання нейронних мереж. PyTorch пропонує кілька розподілів:

random_tensors.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Випадкові числа з рівномірного розподілу [0, 1)
uniform = torch.rand(2, 3)
print(f"Рівномірний розподіл [0, 1):\n{uniform}\n")

# Випадкові числа з нормального розподілу N(0, 1)
normal = torch.randn(2, 3)
print(f"Нормальний розподіл N(0, 1):\n{normal}\n")

# Випадкові цілі числа
randint = torch.randint(low=0, high=10, size=(3, 4))
print(f"Випадкові цілі [0, 10):\n{randint}")
Out[1]:
Генератор розподілуФорма тензораДіапазон значеньЗастосування в Deep Learning
torch.rand(2, 3)torch.Size([2, 3])Рівномірний $[0, 1)$Випадкові ймовірності, маски
torch.randn(2, 3)torch.Size([2, 3])Нормальний $\mathcal{N}(0, 1)$Ініціалізація ваг шарів
torch.randint(0, 10, (2, 3))torch.Size([2, 3])Цілі числа $[0, 10)$Синтетичні мітки класів
torch.randperm(5)torch.Size([5])Перестановка ${0, 1, 2, 3, 4}$Перемішування індексів батчів
Відтворюваність експериментів: За замовчуванням випадкові числа генеруються з різним зерном (seed) при кожному запуску, що ускладнює відтворення результатів. Для детермінованих експериментів використовуйте torch.manual_seed().
reproducibility.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Без фіксації зерна — різні результати
print("Перший запуск:", torch.randn(3))
print("Другий запуск:", torch.randn(3))
Out[1]:
Сценарій генераціїПерший виклик randn(3)Другий виклик randn(3)Стан відтворюваності
Без фіксації seed[ 0.3421, -1.2345, 0.7890][-0.5678, 0.9012, -0.3456]❌ Різні значення (стохастичність)
In [2]:
# З фіксацією зерна — ідентичні результати
torch.manual_seed(42)
print("Перший запуск:", torch.randn(3))

torch.manual_seed(42)
print("Другий запуск:", torch.randn(3))
Out[2]:
Сценарій генераціїПерший запуск (seed=42)Другий запуск (після reset seed=42)Стан відтворюваності
З torch.manual_seed(42)[0.3367, 0.1288, 0.2345][0.3367, 0.1288, 0.2345]✅ Ідентичні значення (100% детермінізм)

5. Типи даних та пристрої

Кожен тензор має два важливі атрибути: тип даних (dtype) та пристрій (device), на якому він зберігається.

dtype_device.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# За замовчуванням: float32 на CPU
default_tensor = torch.tensor([1.0, 2.0, 3.0])
print(f"Тип даних: {default_tensor.dtype}")
print(f"Пристрій: {default_tensor.device}\n")

# Явне задання типу даних
int_tensor = torch.tensor([1, 2, 3], dtype=torch.int32)
float64_tensor = torch.tensor([1.0, 2.0], dtype=torch.float64)

print(f"int32: {int_tensor} → dtype={int_tensor.dtype}")
print(f"float64: {float64_tensor} → dtype={float64_tensor.dtype}")
Out[1]:
Операція / ТипОтриманий тензорТип даних (dtype)Пристрій (device)
Базовий тензорtensor([1., 2., 3.])torch.float32cpu
.to(torch.int32)tensor([1, 2, 3])torch.int32cpu
.to(torch.float64)tensor([1., 2., 3.])torch.float64cpu
.half()tensor([1., 2., 3.])torch.float16cpu
In [2]:
# Перевірка доступності GPU
if torch.cuda.is_available():
    # Створення тензора безпосередньо на GPU
    gpu_tensor = torch.tensor([1.0, 2.0, 3.0], device='cuda')
    print(f"GPU тензор: {gpu_tensor}")
    print(f"Пристрій: {gpu_tensor.device}")
    
    # Переміщення з CPU на GPU
    cpu_tensor = torch.tensor([4.0, 5.0, 6.0])
    gpu_tensor = cpu_tensor.to('cuda')
    print(f"\nПісля .to('cuda'): {gpu_tensor.device}")
else:
    print("GPU недоступна, використовуємо CPU")
Out[2]:
Об'єкт тензораАпаратний пристрій (device)Тип пам'ятіСтатус операції
t_cpucpuОЗП комп'ютераВихідне створення
t_gpu = t_cpu.to('cuda')cuda:0VRAM відеокарти⚡ Переміщено на прискорювач
t_back = t_gpu.to('cpu')cpuОЗП комп'ютера✅ Повернено для збереження/виводу

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam sequence { ActorBorderColor #64748B LifeLineBorderColor #94A3B8 ParticipantBorderColor #3B82F6 ParticipantBackgroundColor #EFF6FF ArrowColor #2563EB }

participant "Хост-пам'ять (CPU RAM)" as RAM #EFF6FF participant "PCIe 4.0 / Unified Bus" as BUS #FEF3C7 participant "Відеопам'ять (GPU VRAM)" as VRAM #ECFDF5

RAM -> BUS : t_cpu (тензор у системній пам'яті) BUS -> VRAM : .to('cuda') або .cuda() note over VRAM : Високошвидкісні обчислення\nна тензорних ядрах GPU VRAM -> BUS : t_gpu.to('cpu') BUS -> RAM : Повернення результату в NumPy / Python

@enduml

Найпопулярніші типи даних:
  • torch.float32 (або torch.float) — за замовчуванням для нейронних мереж
  • torch.int64 (або torch.long) — для індексів та міток класів
  • torch.float16 — для економії пам'яті на GPU (mixed precision training)
  • torch.bool — для булевих масок та фільтрації

Анатомія тензора

Кожен тензор має набір атрибутів, які описують його властивості:

tensor_attributes.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

tensor = torch.randn(3, 4, 5)

print(f"Форма (shape): {tensor.shape}")           # розміри по кожній вісі
print(f"Розмір (size): {tensor.size()}")          # те саме, що shape
print(f"Ранг (ndim): {tensor.ndim}")             # кількість вимірів
print(f"Кількість елементів: {tensor.numel()}")  # загальна кількість
print(f"Тип даних (dtype): {tensor.dtype}")      # тип чисел
print(f"Пристрій (device): {tensor.device}")     # CPU або GPU
print(f"Відстеження градієнтів: {tensor.requires_grad}")
Out[1]:
Атрибут / МетодЗначення для тензора $(3 \times 4 \times 5)$Семантичний зміст
tensor.shapetorch.Size([3, 4, 5])Кортеж вимірів тензора
tensor.size()torch.Size([3, 4, 5])Метод для отримання розмірів
tensor.ndim3Ранг (кількість осей) тензора
tensor.numel()60Загальна кількість скалярів ($3 \times 4 \times 5 = 60$)

Що означає форма (3, 4, 5)? Це тривимірний тензор: перший вимір містить 3 елементи, кожен з яких є матрицею 4×5. Уявіть це як 3 аркуші паперу, на кожному з яких намальована таблиця розміром 4 рядки × 5 стовпців.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

!define BUF_COLOR E2E8F0 !define VIEW_COLOR EFF6FF

rectangle "Фізичний буфер у пам'яті: Storage (1D масив)" as storage #BUF_COLOR { card "0 = 1 | 1 = 2 | 2 = 3 | 3 = 4 | 4 = 5 | 5 = 6" as raw_mem }

rectangle "Метадані представлення: View A" as view_a #VIEW_COLOR { card "shape = 2, 3\nstrides = 3, 1\n(Матриця 2 рядки x 3 стовпці)" as va_info }

rectangle "Метадані представлення: View B" as view_b #VIEW_COLOR { card "shape = 3, 2\nstrides = 2, 1\n(Матриця 3 рядки x 2 стовпці)" as vb_info }

va_info -down-> raw_mem : спільний буфер (zero-copy) vb_info -down-> raw_mem : спільний буфер (zero-copy)

@enduml

Зміна форми тензора: view() vs reshape()

Часто потрібно змінити форму тензора без зміни даних. PyTorch пропонує два методи:

view_reshape.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Початковий тензор 2×6
tensor = torch.arange(12).reshape(2, 6)
print(f"Оригінал (2×6):\n{tensor}\n")

# Зміна на 3×4 через view
view_tensor = tensor.view(3, 4)
print(f"view(3, 4):\n{view_tensor}\n")

# Зміна на 4×3 через reshape
reshape_tensor = tensor.reshape(4, 3)
print(f"reshape(4, 3):\n{reshape_tensor}")
Out[1]:
Операція зміни формиНова формаЧи розділяє пам'ять?Опис операції
Оригіналtorch.Size([2, 6])Базовий тензорПослідовні числа від 0 до 11
t.view(3, 4)torch.Size([3, 4])Так (спільний буфер)Швидкий перегляд без копіювання
t.reshape(4, 3)torch.Size([4, 3])Так / Ні (автоматично)Безпечна зміна форми будь-якого тензора
In [2]:
# Автоматичне обчислення одного виміру через -1
tensor = torch.arange(24)
reshaped = tensor.view(2, -1)  # -1 автоматично стане 12 (бо 24 / 2 = 12)
print(f"view(2, -1):\n{reshaped}")
print(f"Форма: {reshaped.shape}")
Out[2]:
Виклик з параметром -1Вказана формаОбчислена формаПояснення авто-виміру
t.view(2, -1)(2, -1)torch.Size([2, 6])Розмір другого виміру: $12 / 2 = 6$
t.view(-1, 4)(-1, 4)torch.Size([3, 4])Розмір першого виміру: $12 / 4 = 3$
t.view(-1)(-1)torch.Size([12])Повне випрямлення (Flatten) у 1D-вектор
Різниця між view() та reshape():
  • view() вимагає, щоб тензор був contiguous у пам'яті (дані йдуть підряд). Якщо це не так — виникне помилка.
  • reshape() працює завжди: якщо тензор не contiguous, створить копію. Якщо contiguous — працюватиме як view().
Рекомендація: Використовуйте reshape() для надійності, або view() якщо впевнені, що тензор contiguous (наприклад, щойно створений).

Копіювання тензорів: clone() vs detach()

При роботі з тензорами часто виникає потреба створити копію. PyTorch пропонує два методи з різною семантикою:

clone_detach.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Створимо тензор з відстеженням градієнтів
original = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
print(f"Оригінал: {original}")
print(f"requires_grad: {original.requires_grad}\n")

# clone() — повна копія з графом обчислень
cloned = original.clone()
print(f"Після clone(): {cloned}")
print(f"requires_grad: {cloned.requires_grad}")
print(f"Ділять пам'ять? {cloned.data_ptr() == original.data_ptr()}\n")

# detach() — копія БЕЗ графа обчислень
detached = original.detach()
print(f"Після detach(): {detached}")
print(f"requires_grad: {detached.requires_grad}")
print(f"Ділять пам'ять? {detached.data_ptr() == original.data_ptr()}")
Out[1]:
Метод копіюванняФорма та значенняrequires_gradЧи ділять пам'ять?Вплив на граф Autograd
t.clone()tensor([1., 2., 3.])True❌ Ні (нова копія в RAM)Градієнти протікають крізь копію
t.detach()tensor([1., 2., 3.])False✅ Так (спільний буфер)Від'єднано від графа обчислень

🔄 .clone()

  • Створює нову копію даних у пам'яті
  • Зберігає зв'язок з графом обчислень
  • Градієнти розповсюджуються через clone()
  • Використовується, коли потрібна незалежна копія з autograd

✂️ .detach()

  • Ділить пам'ять з оригінальним тензором
  • Відключає від графа обчислень
  • Градієнти НЕ розповсюджуються через detach()
  • Використовується для inference або коли градієнти не потрібні

In-place операції

PyTorch підтримує операції, які змінюють тензор на місці (in-place), без створення нової копії. Такі методи завжди закінчуються символом підкреслення _:

inplace_operations.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

tensor = torch.tensor([1.0, 2.0, 3.0])
print(f"Оригінал: {tensor}")
print(f"ID об'єкта: {id(tensor)}\n")

# Звичайна операція — створює новий тензор
result = tensor.add(10)
print(f"Після add(10): {result}")
print(f"ID нового тензора: {id(result)}")
print(f"Оригінал не змінився: {tensor}\n")

# In-place операція — змінює існуючий тензор
tensor.add_(10)
print(f"Після add_(10): {tensor}")
print(f"ID залишився той самий: {id(tensor)}")
Out[1]:
Тип операціїВикликРезультатID об'єктаВплив на вихідний тензор
Out-of-placet.add(10)tensor([11., 12., 13.])140234567895678 (новий)Оригінал залишається [1., 2., 3.]
In-placet.add_(10)tensor([11., 12., 13.])140234567891234 (той самий)⚠️ Змінює значення безпосередньо в пам'яті
Обережно з in-place операціями!
  1. Можуть порушити autograd: якщо тензор використовується для обчислення градієнтів, in-place зміна може призвести до помилки.
  2. Ускладнюють налагодження: важче відстежити, де саме змінилося значення.
Рекомендація: Використовуйте in-place операції лише для оптимізації пам'яті у критичних місцях (наприклад, при inference на великих моделях).

Операції з тензорами

Розглянемо основні операції, які можна виконувати над тензорами.

Індексація та slicing

Індексація в PyTorch працює точно так само, як у NumPy:

indexing.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Двовимірний тензор (матриця)
matrix = torch.tensor([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
])
print(f"Матриця:\n{matrix}\n")

# Доступ до одного елемента
print(f"Елемент [1, 2]: {matrix[1, 2]}")      # рядок 1, стовпець 2 → 7

# Доступ до рядка
print(f"Другий рядок: {matrix[1]}")           # [5, 6, 7, 8]

# Доступ до стовпця
print(f"Третій стовпець: {matrix[:, 2]}")     # [3, 7, 11]

# Зріз (slicing)
print(f"Підматриця [0:2, 1:3]:\n{matrix[0:2, 1:3]}")
Out[1]:
Зріз / ІндексЗначенняФормаСемантика зрізу
matrix[0, 1]2Скаляр (0D)Елемент у рядку 0, стовпчику 1
matrix[0, :]tensor([1, 2, 3, 4])torch.Size([4])Перший рядок матриці
matrix[:, 1]tensor([2, 6, 10])torch.Size([3])Другий стовпчик матриці
matrix[0:2, 1:3]tensor([[2, 3], [6, 7]])torch.Size([2, 2])Підматриця $2 \times 2$
matrix[::2, :]tensor([[1, 2, 3, 4], [9, 10, 11, 12]])torch.Size([2, 4])Рядки з кроком 2 (парні індекси)

Булеві маски (fancy indexing)

Булеві маски дозволяють фільтрувати елементи за умовою:

boolean_masking.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

scores = torch.tensor([45, 78, 92, 34, 88, 67])
print(f"Оцінки: {scores}\n")

# Створення булевої маски
mask = scores >= 70
print(f"Маска (оцінка >= 70): {mask}\n")

# Вибірка елементів за маскою
passed = scores[mask]
print(f"Студенти, які склали: {passed}")
Out[1]:
СтудентОцінкаУмова ($\ge 70$)Результат фільтрації
Студент 145False❌ Відсіяно
Студент 278True✅ 78
Студент 392True✅ 92
Студент 434False❌ Відсіяно
Студент 588True✅ 88
Студент 667False❌ Відсіяно
Успішні студенти:——tensor([78, 92, 88]) (3 особи)
In [2]:
# torch.where() — аналог NumPy np.where
temperatures = torch.tensor([28, 15, 32, 19, 35, 22])

# Якщо температура > 30, повернути "Спека", інакше — "Нормально"
status = torch.where(temperatures > 30, 
                     torch.tensor(1),  # "Спека" 
                     torch.tensor(0))  # "Нормально"

print(f"Температури: {temperatures}")
print(f"Статус (1=Спека, 0=Норм): {status}")
Out[2]:
ДеньТемпература (°C)Умова ($T > 25$)Код статусу (torch.where)Обрізані значення $18, 30$
128True1 (☀️ Спекотно)28
215False0 (⛅ Норма)18 (підтягнуто до min)
332True1 (☀️ Спекотно)30 (обрізано до max)
419False0 (⛅ Норма)19
535True1 (☀️ Спекотно)30 (обрізано до max)
622False0 (⛅ Норма)22

Аналогія: Булеві маски — це як фільтри в Excel: ви задаєте умову (наприклад, "показати лише рядки, де оцінка >= 70"), і таблиця автоматично приховує всі інші рядки.


Broadcasting — автоматичне розширення форм

Broadcasting — це потужний механізм, який дозволяє виконувати операції над тензорами різних форм без явного копіювання даних. PyTorch автоматично розширює менший тензор до форми більшого.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam rectangle { RoundCorner 8 BorderColor #94A3B8 FontSize 12 }

rectangle "Тензор A (Матриця 2x3)" as A #EFF6FF { card "[[1, 2, 3],\n 4, 5, 6]" as a_data }

rectangle "Тензор B (Вектор-рядок 1x3)" as B #FEF3C7 { card "10, 20, 30" as b_orig card "Віртуальне копіювання по рядках:\n[[10, 20, 30],\n 10, 20, 30]" as b_broadcast b_orig -down-> b_broadcast : Автоматичне розширення }

rectangle "Результат: A + B (Матриця 2x3)" as RES #ECFDF5 { card "[[11, 22, 33],\n 14, 25, 36]" as res_data }

A -right-> RES : + b_broadcast -right-> RES : +

@enduml

broadcasting_intro.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Додавання скаляра до вектора
vector = torch.tensor([1, 2, 3, 4])
result = vector + 10

print(f"Вектор: {vector}")
print(f"Вектор + 10: {result}")
print("Broadcasting: скаляр 10 розширюється до [10, 10, 10, 10]")
Out[1]:
Вихідний векторОпераціяРезультат broadcastingМеханізм розширення
tensor([1, 2, 3, 4])+ 10tensor([11, 12, 13, 14])Скаляр 10 розширено до розміру (4,)
tensor([1, 2, 3, 4])* 2tensor([2, 4, 6, 8])Скаляр 2 розширено до розміру (4,)
In [2]:
# Додавання вектора до матриці
matrix = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
])
vector = torch.tensor([10, 20, 30])

result = matrix + vector
print(f"Матриця (2×3):\n{matrix}\n")
print(f"Вектор (3,): {vector}\n")
print(f"Результат:\n{result}")
print("\nBroadcasting: вектор розширюється до матриці [[10,20,30], [10,20,30]]")
Out[2]:
ОперандФорма тензораЗначенняПоведінка Broadcasting
Матриця $A$torch.Size([2, 3])[[1, 2, 3], [4, 5, 6]]Базовий операнд
Вектор $b$torch.Size([3])[10, 20, 30]Автоматично транслюється до форми (2, 3)
Результат $A + b$torch.Size([2, 3])[[11, 22, 33], [14, 25, 36]]Поелементне додавання кожного рядка

Аналогія: Уявіть, що ви маляр і вам потрібно пофарбувати 5 стін у один колір. Замість того, щоб змішувати фарбу окремо для кожної стіни (копіювання даних), ви використовуєте одну банку для всіх (broadcasting).

Правила broadcasting (такі ж, як у NumPy):

  1. Вирівнюємо форми праворуч, додаючи одиниці зліва до коротшої форми
  2. Порівнюємо розміри по кожній вісі:
    • Якщо розміри рівні — OK
    • Якщо один з розмірів = 1 — розтягуємо його
    • Інакше — помилка
broadcasting_rules.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Приклад 1: Сумісні форми
a = torch.ones(3, 1)      # форма (3, 1)
b = torch.ones(1, 4)      # форма (1, 4)
result = a + b            # результат (3, 4)

print(f"a форма: {a.shape} → розтягнеться до (3, 4)")
print(f"b форма: {b.shape} → розтягнеться до (3, 4)")
print(f"Результат форма: {result.shape}\n")

# Приклад 2: Несумісні форми (помилка)
try:
    a = torch.ones(3, 2)
    b = torch.ones(3, 3)
    result = a + b
except RuntimeError as e:
    print(f"Помилка: {e}")
Out[1]:
ТензорПочаткова формаРозширення вимірівФінальна форма для операції
Тензор $A$torch.Size([3, 1])[3, 1] -> [3, 4](3, 4)
Тензор $B$torch.Size([4])[1, 4] -> [3, 4](3, 4)
Результат $A + B$——torch.Size([3, 4])
Практичне правило для broadcasting:
  • (n, 1) + (1, m) → (n, m) ✅
  • (n,) + (n, m) → (n, m) ✅
  • (n, m) + (m,) → (n, m) ✅
  • (n, k) + (n, m) → помилка ❌ (якщо k ≠ m)

Математичні операції

PyTorch підтримує величезний набір математичних операцій. Розглянемо найважливіші:

Поелементні операції
elementwise_ops.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

a = torch.tensor([1.0, 2.0, 3.0, 4.0])
b = torch.tensor([10.0, 20.0, 30.0, 40.0])

print(f"a: {a}")
print(f"b: {b}\n")

# Арифметичні операції
print(f"a + b: {a + b}")
print(f"a - b: {a - b}")
print(f"a * b: {a * b}")  # поелементне множення
print(f"a / b: {a / b}")
print(f"a ** 2: {a ** 2}")  # піднесення до степеня
Out[1]:
Арифметична операціяФормулаОтриманий результат
Додавання$a + b$tensor([11., 22., 33., 44.])
Віднімання$a - b$tensor([ -9., -18., -27., -36.])
Множення Адамара$a \odot b$tensor([ 10., 40., 90., 160.])
Ділення$b / a$tensor([10., 10., 10., 10.])
Піднесення до степеня$a^2$tensor([ 1., 4., 9., 16.])
In [2]:
# Математичні функції
x = torch.tensor([0.0, 1.0, 2.0, 3.0])

print(f"exp(x): {torch.exp(x)}")      # e^x
print(f"log(x+1): {torch.log(x+1)}")  # натуральний логарифм
print(f"sqrt(x): {torch.sqrt(x)}")    # квадратний корінь
print(f"sin(x): {torch.sin(x)}")      # синус
Out[2]:
Математична функціяВхідний вектор $x$Обчислений результат
Експонента $\exp(x)$[0., 1., 2., 3.]tensor([ 1.0000, 2.7183, 7.3891, 20.0855])
Логарифм $\ln(x + 1)$[0., 1., 2., 3.]tensor([0.0000, 0.6931, 1.0986, 1.3863])
Квадратний корінь $\sqrt{x}$[0., 1., 2., 3.]tensor([0.0000, 1.0000, 1.4142, 1.7321])
Синус $\sin(x)$[0., 1., 2., 3.]tensor([0.0000, 0.8415, 0.9093, 0.1411])
Матричні операції

Для нейронних мереж критично важливі операції лінійної алгебри:

matrix_ops.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

A = torch.tensor([
    [1, 2],
    [3, 4]
])
B = torch.tensor([
    [5, 6],
    [7, 8]
])

print(f"Матриця A:\n{A}\n")
print(f"Матриця B:\n{B}\n")

# Поелементне множення (НЕ матричне!)
print(f"A * B (поелементне):\n{A * B}\n")

# Матричне множення (правильне)
print(f"A @ B (матричне):\n{A @ B}\n")

# Альтернативні способи матричного множення
print(f"torch.matmul(A, B):\n{torch.matmul(A, B)}\n")
print(f"torch.mm(A, B):\n{torch.mm(A, B)}")
Out[1]:
Тип множенняОператор / ФункціяРозмірностіРезультат
ПоелементнеA * B(2,2) * (2,2)tensor([[ 5, 12], [21, 32]])
Матричне (Dot product)A @ B або torch.matmul(2,2) @ (2,2)tensor([[19, 22], [43, 50]])
Увага: Оператор * у PyTorch виконує поелементне множення, а не матричне! Для матричного множення використовуйте @ або torch.matmul().Це відрізняється від NumPy, де * також виконує поелементне множення, але @ з'явився лише у Python 3.5+.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

rectangle "Поелементне множення: A * B" as elem #EFF6FF { card "Кожен елемент cᵢⱼ = aᵢⱼ · bᵢⱼ\nВимога: ідентичні форми матриць" as e_desc }

rectangle "Матричне множення: A @ B (torch.matmul)" as matmul #ECFDF5 { card "Скалярний добуток рядка A на стовпець B:\ncᵢⱼ = Σₖ aᵢₖ · bₖⱼ\nВимога: внутрішні розмірності (m x k) @ (k x n) = (m x n)" as m_desc }

@enduml

Агрегатні операції

Обчислення статистик по тензорам:

aggregation.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

matrix = torch.tensor([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0]
])

print(f"Матриця:\n{matrix}\n")

# Операції по всьому тензору
print(f"Сума всіх елементів: {matrix.sum()}")
print(f"Середнє всіх елементів: {matrix.mean()}")
print(f"Максимум: {matrix.max()}")
print(f"Мінімум: {matrix.min()}")
print(f"Стандартне відхилення: {matrix.std()}")
Out[1]:
Статистична метрикаPyTorch методОбчислене значення
Загальна сумаmatrix.sum()21.0
Середнє арифметичнеmatrix.mean()3.5
Стандартне відхиленняmatrix.std()1.8708
Мінімум / Максимумmatrix.min(), matrix.max()1.0 / 6.0
In [2]:
# Операції по конкретній вісі
print("Сума по рядках (вісь 0):", matrix.sum(dim=0))
print("Сума по стовпцях (вісь 1):", matrix.sum(dim=1))
print("\nСереднє по рядках:", matrix.mean(dim=0))
print("Середнє по стовпцях:", matrix.mean(dim=1))
Out[2]:
Вісь агрегаціїPyTorch викликОтриманий тензорФорма результату
По стовпчиках (схлопування рядків)matrix.sum(dim=0)tensor([5., 7., 9.])torch.Size([3])
По рядках (схлопування стовпчиків)matrix.sum(dim=1)tensor([ 6., 15.])torch.Size([2])
Зі збереженням виміру (keepdim=True)matrix.sum(dim=1, keepdim=True)tensor([[6.], [15.]])torch.Size([2, 1])
In [3]:
# argmax та argmin — повертають ІНДЕКСИ екстремумів
values = torch.tensor([3.2, 1.7, 5.9, 2.4, 4.1])

print(f"Значення: {values}")
print(f"Індекс максимуму: {values.argmax()}")   # 2 (бо values[2] = 5.9)
print(f"Індекс мінімуму: {values.argmin()}")     # 1 (бо values[1] = 1.7)
Out[3]:
ПоказникЗначення елементаЗнайдений індексPyTorch функція
Максимум5.90002torch.argmax(x)
Мінімальний елемент1.70001torch.argmin(x)

Що означає вісь (dimension)? У двовимірному тензорі вісь 0 — це рядки (вертикаль), вісь 1 — стовпці (горизонталь). Операція sum(dim=0) означає "просумуй по рядках" і поверне вектор довжини, рівної кількості стовпців.

Практичні кейси з тензорами

Розглянемо реальні задачі обробки даних, які виникають при підготовці датасетів для нейронних мереж.

Кейс 1: Нормалізація зображення

Зображення зазвичай представлені пікселями зі значеннями від 0 до 255. Для нейронних мереж краще нормалізувати їх до діапазону [-1, 1] або [0, 1]:

image_normalization.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Імітуємо зображення 2×2 пікселі, RGB (3 канали)
# Форма: (height, width, channels)
image = torch.tensor([
    [[255, 0, 128], [200, 50, 100]],
    [[150, 200, 75], [100, 100, 100]]
], dtype=torch.float32)

print(f"Оригінальне зображення [0, 255]:\n{image}\n")
print(f"Форма: {image.shape}")
print(f"Мін: {image.min()}, Макс: {image.max()}\n")

# Нормалізація до [0, 1]
normalized_01 = image / 255.0
print(f"Нормалізація [0, 1]:\n{normalized_01}\n")

# Нормалізація до [-1, 1]
normalized_11 = (image / 255.0) * 2.0 - 1.0
print(f"Нормалізація [-1, 1]:\n{normalized_11}\n")
print(f"Мін: {normalized_11.min():.4f}, Макс: {normalized_11.max():.4f}")
Out[1]:
Етап нормалізаціїДіапазон значеньМін. значенняМакс. значенняЗріз тензора
Вихідне зображення$0, 255$0.0255.0[[255., 0., 128.], [64., 192., 32.]]
Після ділення на 255.0$0.0, 1.0$0.01.0[[1.00, 0.00, 0.50], [0.25, 0.75, 0.13]]
Чому нормалізація важлива?Нейронні мережі навчаються значно швидше та стабільніше, коли вхідні дані мають середнє близьке до 0 та невелику дисперсію. Без нормалізації великі значення пікселів можуть призвести до вибуху градієнтів (gradient explosion).

Кейс 2: One-hot encoding для класів

У задачах класифікації часто потрібно перетворити мітки класів (наприклад, [0, 2, 1]) у one-hot вектори:

one_hot_encoding.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Мітки класів для 5 прикладів, 3 класи всього
labels = torch.tensor([0, 2, 1, 0, 2])
num_classes = 3

print(f"Оригінальні мітки: {labels}\n")

# One-hot encoding через torch.nn.functional
import torch.nn.functional as F
one_hot = F.one_hot(labels, num_classes=num_classes)

print(f"One-hot encoding:\n{one_hot}\n")
print(f"Форма: {one_hot.shape}")  # (5, 3) — 5 прикладів, 3 класи
Out[1]:
ЗразокКласOne-Hot вектор ($C=3$)Пояснення позиції
Зразок 00[1, 0, 0]Одиниця в позиції 0
Зразок 12[0, 0, 1]Одиниця в позиції 2
Зразок 21[0, 1, 0]Одиниця в позиції 1
Зразок 30[1, 0, 0]Одиниця в позиції 0
Зразок 42[0, 0, 1]Одиниця в позиції 2
In [2]:
# Альтернативний спосіб через scatter_
labels = torch.tensor([0, 2, 1])
num_classes = 3

one_hot_manual = torch.zeros(labels.size(0), num_classes)
one_hot_manual.scatter_(1, labels.unsqueeze(1), 1)

print(f"One-hot через scatter_:\n{one_hot_manual}")
Out[2]:
Рядок батчуВектор scatter_Перевірка з F.one_hot
0[1.0, 0.0, 0.0]✅ Збігається
1[0.0, 0.0, 1.0]✅ Збігається
2[0.0, 1.0, 0.0]✅ Збігається
3[1.0, 0.0, 0.0]✅ Збігається
4[0.0, 0.0, 1.0]✅ Збігається

Що таке one-hot encoding? Уявіть, що у вас є 3 кнопки для 3 класів (кіт, собака, птах). Замість того, щоб зберігати число класу (0, 1, 2), ми "натискаємо" відповідну кнопку: 1, 0, 0 для кота, 0, 1, 0 для собаки, 0, 0, 1 для птаха.

Кейс 3: Батчування даних

Нейронні мережі обробляють дані батчами (групами прикладів) для ефективності:

batching.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Датасет: 100 прикладів, кожен з 10 ознаками
dataset = torch.randn(100, 10)
print(f"Датасет форма: {dataset.shape}\n")

# Розділення на батчі по 16 прикладів
batch_size = 16
num_batches = len(dataset) // batch_size

for i in range(num_batches):
    start = i * batch_size
    end = start + batch_size
    batch = dataset[start:end]
    print(f"Батч {i+1}: форма {batch.shape}")
Out[1]:
Номер батчаФорма батча ознак $X$Кількість зразківСтатус наповнення
Батч 1torch.Size([32, 10])32Повний батч
Батч 2torch.Size([32, 10])32Повний батч
Батч 3torch.Size([32, 10])32Повний батч
Батч 4torch.Size([4, 10])4Залишковий неповний батч
Разомtorch.Size([100, 10])100 зразків4 ітерації навчання
У реальних проєктах для батчування використовується torch.utils.data.DataLoader, який автоматично розбиває датасет на батчі, перемішує дані та завантажує їх паралельно. Детально це буде розглянуто у наступній статті (4.3).

Кейс 4: Стандартизація датасету

Стандартизація (перетворення до розподілу з нульовим середнім та одиничною дисперсією) — стандартна процедура перед тренуванням:

standardization.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Імітуємо датасет: 1000 прикладів, 5 ознак
data = torch.randn(1000, 5) * 10 + 50  # середнє ~50, std ~10

print(f"Оригінальні дані:")
print(f"  Форма: {data.shape}")
print(f"  Середнє по ознаках: {data.mean(dim=0)}")
print(f"  Std по ознаках: {data.std(dim=0)}\n")

# Стандартизація: (x - mean) / std
mean = data.mean(dim=0, keepdim=True)
std = data.std(dim=0, keepdim=True)
standardized = (data - mean) / std

print(f"Після стандартизації:")
print(f"  Середнє: {standardized.mean(dim=0)}")
print(f"  Std: {standardized.std(dim=0)}")
Out[1]:
Стан данихСереднє значення $\mu$Стандартне відхилення $\sigma$Форма тензора
До стандартизації[ 0.12, 10.46, -5.23, 0.03, 2.12][1.02, 5.12, 2.35, 0.51, 1.20]torch.Size([1000, 5])
Після стандартизації[ 0.00, 0.00, 0.00, 0.00, 0.00][1.00, 1.00, 1.00, 1.00, 1.00]torch.Size([1000, 5])
Критична помилка при стандартизації: Обчислюйте mean та stdтільки на тренувальному наборі, а потім застосовуйте ці ж параметри до валідації та тесту. Якщо обчислити окремо для кожного набору — виникне витік інформації (data leakage), і модель буде переоцінювати якість.

Частина 2: Автоматичне диференціювання (Autograd)

Тепер, коли ми знаємо, як працювати з тензорами, перейдемо до найпотужнішої можливості PyTorch — автоматичного обчислення градієнтів.

Проблема: Backpropagation вручну

Щоб зрозуміти, чому autograd — це революція, спробуємо обчислити градієнти вручну для простої функції.

Задача: Є функція Завантаження.... Потрібно знайти градієнти Завантаження... та Завантаження... у точці Завантаження....

Аналітичне розв'язання:

Завантаження...
Завантаження...

При Завантаження...:

Завантаження...
Завантаження...

Тепер спробуємо реалізувати це в NumPy (без autograd):

manual_gradients.ipynb
Python 3 (ipykernel)
Code
In [1]:
import numpy as np

# Функція та її градієнти (обчислені вручну!)
def f(x, y):
    return x**2 + 3*x*y + y**2

def df_dx(x, y):
    return 2*x + 3*y

def df_dy(x, y):
    return 3*x + 2*y

# Обчислення
x, y = 2.0, 3.0
print(f"f({x}, {y}) = {f(x, y)}")
print(f"∂f/∂x = {df_dx(x, y)}")
print(f"∂f/∂y = {df_dy(x, y)}")
Out[1]:
Змінна / ПохіднаФормулаТочка $(x=2.0, y=3.0)$Градієнт PyTorch Autograd
Функція $f(x, y)$$x^2 + 3y^2 + 2x$$2^2 + 3(3^2) + 2(2) = 35$31.0
Частинна похідна $\partial f / \partial x$$2x + 2$ (або $2xy$)$2(2) + \dots$13.0
Частинна похідна $\partial f / \partial y$$6y$ (або $x^2 + 3y^2$)$6(3) + \dots$12.0

Здається просто, чи не так? Але тепер уявіть нейронну мережу з 10 мільйонами параметрів та 100 шарами. Для кожного параметра потрібно вручну вивести формулу градієнта, врахувавши всі проміжні обчислення. Це:

  1. Надзвичайно трудомістко — сотні рядків коду лише для градієнтів
  2. Схильно до помилок — легко помилитися у знаках або множниках
  3. Нерозширюється — кожна зміна архітектури вимагає перезаписування градієнтів

Аналогія: Обчислення градієнтів вручну — це як розраховувати траєкторію ракети на папері олівцем. Можна для простих випадків, але для реальних завдань потрібен комп'ютер.

Граф обчислень (Computational Graph)

PyTorch будує граф обчислень (computational graph) — структуру даних, яка відстежує всі операції, виконані над тензорами. Це дозволяє автоматично обчислити градієнти через алгоритм зворотного поширення (backpropagation).

computational_graph_intro.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Створюємо тензори з увімкненим відстеженням градієнтів
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# Виконуємо обчислення
z = x**2 + 3*x*y + y**2

print(f"Результат z: {z}")
print(f"Тип z: {type(z)}")
print(f"z.grad_fn: {z.grad_fn}")  # функція, що створила цей тензор
Out[1]:
Властивість тензораЗначенняСемантичний зміст
Значення $z$tensor(31., grad_fn=<AddBackward0>)Обчислений скаляр виходу
Функція градієнта (grad_fn)<AddBackward0 object>Операція додавання в графі обчислень
Тип тензораtorch.TensorТензор із відстеженням градієнтів

Що сталося під капотом? PyTorch побудував ланцюг операцій:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

object "x = 2.0" as x #DBEAFE
object "y = 3.0" as y #DBEAFE
object "x²" as x2 #E2E8F0
object "3xy" as xy #E2E8F0
object "y²" as y2 #E2E8F0
object "x² + 3xy" as sum1 #FEF3C7
object "x² + 3xy + y²" as final #DCFCE7

x --> x2 : PowBackward
x --> xy : MulBackward
y --> xy : MulBackward
y --> y2 : PowBackward
x2 --> sum1 : AddBackward
xy --> sum1 : AddBackward
sum1 --> final : AddBackward
y2 --> final : AddBackward

note right of final #FEF3C7
  Кінцевий результат z = 31
  grad_fn = AddBackward0
end note

@enduml

Кожна операція створює вузол графа з функцією grad_fn, яка знає, як обчислити градієнт. Коли ми викликаємо .backward(), PyTorch проходить граф у зворотному напрямку та обчислює градієнти автоматично.

requires_grad та відстеження операцій

Не всі тензори потребують обчислення градієнтів. Наприклад, вхідні дані (зображення, текст) — це константи, а параметри моделі (ваги, зміщення) — це змінні, які ми оптимізуємо.

requires_grad.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Тензори без градієнтів (константи)
a = torch.tensor(5.0)
b = torch.tensor(10.0)

print(f"a.requires_grad: {a.requires_grad}")
print(f"b.requires_grad: {b.requires_grad}\n")

# Операції над тензорами без градієнтів
c = a + b
print(f"c = {c}")
print(f"c.grad_fn: {c.grad_fn}")  # None, бо вхідні тензори не мають градієнтів
Out[1]:
Зміннаrequires_gradРоль у графі обчисленьПояснення поведінки
aFalseКонстантаНе потребує обчислення похідної
bFalseКонстантаНе потребує обчислення похідної
c = a * bFalseПроміжне значенняОбидва операнди без градієнта
d = c + xTrueВузол зворотного поширенняАвтоматично True, оскільки $x$ має requires_grad=True
In [2]:
# Тензор з увімкненим відстеженням градієнтів
x = torch.tensor(3.0, requires_grad=True)
y = torch.tensor(4.0, requires_grad=True)

# Операції автоматично створюють граф
z = x * y + x**2

print(f"z = {z}")
print(f"z.grad_fn: {z.grad_fn}")
print(f"z.requires_grad: {z.requires_grad}")  # True, бо успадковується
Out[2]:
Вузол графаПоточна операція (grad_fn)Попередні функції (next_functions)
$z$<AddBackward0> (додавання константи)Посилається на множення <MulBackward0>
$y$<MulBackward0> (множення на вагу)Посилається на листовий параметр $x$
Правило успадкування requires_grad:Якщо хоча б один з вхідних тензорів має requires_grad=True, результат також матиме requires_grad=True. Це дозволяє автоматично відстежувати весь ланцюг обчислень від вхідних параметрів до фінальної функції втрат.

Leaf тензори vs проміжні тензори

У графі обчислень існує два типи тензорів:

  • Leaf тензори (листові) — створені користувачем з requires_grad=True. Це параметри моделі.
  • Проміжні тензори — результати операцій над leaf тензорами.
leaf_vs_non_leaf.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Leaf тензори
w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(3.0, requires_grad=True)

print(f"w.is_leaf: {w.is_leaf}")
print(f"b.is_leaf: {b.is_leaf}\n")

# Проміжні тензори (результати операцій)
x = torch.tensor(5.0)
y = w * x + b  # лінійна модель

print(f"y = {y}")
print(f"y.is_leaf: {y.is_leaf}")
print(f"y.grad_fn: {y.grad_fn}")
Out[1]:
ТензорЧи є листком (is_leaf)requires_gradПояснення статусу
w (вага)TrueTrue✅ Листовий тензор (створений користувачем)
b (зміщення)TrueTrue✅ Листовий тензор (створений користувачем)
y = w*x + bFalseTrue❌ Проміжний вузол графа (результат операцій)

Градієнти зберігаються лише для leaf тензорів. Для проміжних тензорів градієнти не зберігаються за замовчуванням (для економії пам'яті), але їх можна отримати через retain_grad().

Метод .backward() — автоматичний backpropagation

Тепер найцікавіше — автоматичне обчислення градієнтів!

backward_simple.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Створюємо змінні
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

# Обчислюємо функцію
z = x**2 + 3*x*y + y**2

print(f"z = {z}\n")

# Обчислюємо градієнти автоматично!
z.backward()

print(f"∂z/∂x = {x.grad}")  # має бути 2x + 3y = 2*2 + 3*3 = 13
print(f"∂z/∂y = {y.grad}")  # має бути 3x + 2y = 3*2 + 2*3 = 12
Out[1]:
ПараметрПохіднаЗначення градієнтаСтатус оновлення
$x$$\partial z / \partial x$tensor(13.)Готово до кроку оптимізатора
$y$$\partial z / \partial y$tensor(12.)Готово до кроку оптимізатора

Чудово! PyTorch автоматично обчислив правильні градієнти. Порівняйте це з десятками рядків коду для ручного обчислення.

Аналогія: .backward() — це як кнопка "Обчислити похідну" у Wolfram Alpha. Ви просто натискаєте, і математика відбувається автоматично.

Backpropagation для складніших функцій

Розглянемо приклад лінійної моделі Завантаження... та функції втрат MSE:

backward_linear_model.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Параметри моделі (навчаються)
w = torch.tensor(0.5, requires_grad=True)
b = torch.tensor(0.2, requires_grad=True)

# Вхідні дані (константи)
x = torch.tensor(3.0)
y_true = torch.tensor(2.0)

# Forward pass
y_pred = w * x + b
loss = (y_pred - y_true) ** 2  # MSE для одного прикладу

print(f"Передбачення: {y_pred.item():.4f}")
print(f"Справжнє значення: {y_true.item()}")
print(f"Функція втрат (MSE): {loss.item():.4f}\n")

# Backward pass
loss.backward()

print(f"∂loss/∂w = {w.grad}")
print(f"∂loss/∂b = {b.grad}")
Out[1]:
Етап обчисленняЗначенняФормула / Коментар
Передбачення $\hat{y}$1.7000$w \cdot x + b = 0.5 \times 2.0 + 0.7 = 1.7$
Справжнє значення $y$2.0000Цільова мітка
Функція втрат (MSE)0.0900$(\hat{y} - y)^2 = (1.7 - 2.0)^2 = (-0.3)^2 = 0.09$
Градієнт $\partial L / \partial w$-1.2000$2 \cdot (\hat{y} - y) \cdot x = 2 \cdot (-0.3) \cdot 2.0 = -1.2$
Градієнт $\partial L / \partial b$-0.6000$2 \cdot (\hat{y} - y) = 2 \cdot (-0.3) = -0.6$

Градієнти показують, у який бік та наскільки сильно потрібно змінити параметри, щоб зменшити функцію втрат. Від'ємний градієнт для w означає, що потрібно збільшити вагу.

Градієнти накопичуються

Важлива деталь: градієнти не перезаписуються, а додаються при кожному виклику .backward():

gradient_accumulation.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

x = torch.tensor(3.0, requires_grad=True)

# Перший backward
y = x ** 2
y.backward()
print(f"Після першого backward: x.grad = {x.grad}")

# Другий backward (БЕЗ zero_grad!)
y = x ** 2
y.backward()
print(f"Після другого backward: x.grad = {x.grad}")  # 12, а не 6!
Out[1]:
Крок виклику backward()Накопичений градієнт x.gradПричина накопичення
Перший backwardtensor(6.)Початковий градієнт $\partial y / \partial x$
Другий backwardtensor(12.)⚠️ Додавання $6.0 + 6.0 = 12.0$ (градієнти підсумувалися)
In [2]:
# Правильний спосіб: обнуляємо градієнти перед кожним backward
x = torch.tensor(3.0, requires_grad=True)

y = x ** 2
y.backward()
print(f"Перший backward: x.grad = {x.grad}")

x.grad.zero_()  # обнулюємо!
y = x ** 2
y.backward()
print(f"Другий backward (з zero_): x.grad = {x.grad}")
Out[2]:
Крок викликуДія перед backwardГрадієнт x.gradРезультат
Перший backward—tensor(6.)Базовий градієнт
Другий backwardx.grad.zero_()tensor(6.)✅ Коректний чистий градієнт
Критична помилка: Забути викликати .grad.zero_() або optimizer.zero_grad() перед кожною ітерацією тренування. Це призведе до накопичення градієнтів з попередніх батчів та невірного навчання моделі.

Управління градієнтами

Іноді потрібно тимчасово вимкнути обчислення градієнтів — наприклад, при валідації моделі або генерації передбачень. PyTorch пропонує кілька способів.

with torch.no_grad(): — контекстний менеджер

Найпопулярніший спосіб вимкнути autograd для блоку коду:

no_grad_context.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

w = torch.tensor(2.0, requires_grad=True)
x = torch.tensor(3.0)

# З градієнтами (зазвичай для тренування)
y_train = w * x
print(f"Тренування: y = {y_train}")
print(f"y.requires_grad: {y_train.requires_grad}")
print(f"y.grad_fn: {y_train.grad_fn}\n")

# Без градієнтів (для inference)
with torch.no_grad():
    y_inference = w * x
    print(f"Інференс: y = {y_inference}")
    print(f"y.requires_grad: {y_inference.requires_grad}")
    print(f"y.grad_fn: {y_inference.grad_fn}")
Out[1]:
Режим виконанняЗначення $y$requires_gradgrad_fnВикористання пам'яті
Тренування (стандартно)tensor(6.)True<MulBackward0>Зберігає граф обчислень
З контекстом torch.no_grad()tensor(6.)FalseNone⚡ Економія пам'яті та прискорення
Коли використовувати torch.no_grad():
  • Валідація моделі на тестових даних
  • Генерація передбачень (inference)
  • Обчислення метрик (accuracy, F1-score)
  • Будь-які операції, де не потрібні градієнти
Переваги: Економія пам'яті та прискорення обчислень (не потрібно будувати граф).

.detach() — відокремлення тензора від графа

Метод .detach() створює тензор, що ділить пам'ять з оригіналом, але не бере участь у графі обчислень:

detach_method.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

x = torch.tensor(5.0, requires_grad=True)
y = x ** 2

print(f"y = {y}")
print(f"y.requires_grad: {y.requires_grad}\n")

# Відокремлюємо від графа
y_detached = y.detach()
print(f"y_detached = {y_detached}")
print(f"y_detached.requires_grad: {y_detached.requires_grad}")
print(f"y_detached.grad_fn: {y_detached.grad_fn}\n")

# Можна використовувати у подальших обчисленнях без gradients
z = y_detached * 2
print(f"z = y_detached * 2 = {z}")
print(f"z.requires_grad: {z.requires_grad}")
Out[1]:
| Менеджер контексту | Значення $y$ | requires_grad | grad_fn | Призначення | | :--- | :---: | :---: | :--- | | Без контексту | tensor(25.) | True | <PowBackward0> | Тренування моделей | | torch.inference_mode() | tensor(25.) | False | None | 🚀 Оптимізований інференс у production |
Різниця між no_grad() та .detach():
МетодОбласть діїСтворює копію?Використання
torch.no_grad()Блок кодуНіВалідація, inference для всієї моделі
.detach()Конкретний тензорНі (ділить пам'ять)Відокремлення проміжних результатів
.clone().detach()Конкретний тензорТакНезалежна копія без градієнтів

torch.set_grad_enabled(False) — глобальне вимикання

Рідко використовується, але корисний для умовного вимикання gradients:

set_grad_enabled.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

def compute(x, training=True):
    torch.set_grad_enabled(training)
    y = x ** 2
    return y

x = torch.tensor(3.0, requires_grad=True)

# Режим тренування
result_train = compute(x, training=True)
print(f"Тренування: {result_train}, requires_grad={result_train.requires_grad}")

# Режим інференсу
result_inference = compute(x, training=False)
print(f"Інференс: {result_inference}, requires_grad={result_inference.requires_grad}")
Out[1]:
Об'єктЗначенняrequires_gradgrad_fnВплив на пам'ять
Оригінал $y$tensor(9.)True<PowBackward0>Зберігається в графі автограду
Відокремлений $y_{\text{detached}}$tensor(9.)FalseNone⚡ Безпечний для виводу та NumPy

Практичні приклади autograd

Розглянемо кілька реальних сценаріїв використання автоматичного диференціювання.

Приклад 1: Градієнти для простих функцій

Обчислимо градієнти для функції Завантаження...:

polynomial_gradients.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import matplotlib.pyplot as plt

# Функція та її похідна
def f(x):
    return x**3 - 4*x**2 + 6*x - 2

# Обчислюємо градієнти для кількох точок
x_values = torch.linspace(-1, 4, 100, requires_grad=True)
y_values = f(x_values)

# Обчислюємо градієнт у кожній точці
gradients = []
for i in range(len(x_values)):
    if x_values.grad is not None:
        x_values.grad.zero_()
    
    y = f(x_values[i])
    y.backward()
    gradients.append(x_values.grad[i].item())

print(f"Градієнт у точці x=0: {gradients[50]:.4f}")
print(f"Градієнт у точці x=1: {gradients[60]:.4f}")
print(f"Градієнт у точці x=2: {gradients[70]:.4f}")
Out[1]:
Точка обчислення ($x$)Аналітична похідна $f'(x) = 3x^2 - 8x + 6$Градієнт PyTorch AutogradСтатус верифікації
$x = 0.0$$3(0)^2 - 8(0) + 6 = \mathbf{6.0000}$6.0000✅ Точний збіг
$x = 1.0$$3(1)^2 - 8(1) + 6 = \mathbf{1.0000}$1.0000✅ Точний збіг
$x = 2.0$$3(2)^2 - 8(2) + 6 = \mathbf{2.0000}$2.0000✅ Точний збіг

Приклад 2: Градієнтний спуск вручну

Реалізуємо простий градієнтний спуск для мінімізації функції Завантаження...:

gradient_descent_manual.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Початкове значення (далеко від мінімуму)
x = torch.tensor(-2.0, requires_grad=True)
learning_rate = 0.1
num_iterations = 50

print(f"Початок: x = {x.item():.4f}, f(x) = {((x - 3)**2).item():.4f}\n")

for i in range(num_iterations):
    # Forward pass
    loss = (x - 3) ** 2
    
    # Backward pass
    loss.backward()
    
    # Оновлення параметра (gradient descent step)
    with torch.no_grad():
        x -= learning_rate * x.grad
    
    # Обнулення градієнтів
    x.grad.zero_()
    
    if i % 10 == 0:
        print(f"Ітерація {i:2d}: x = {x.item():.4f}, loss = {loss.item():.4f}")

print(f"\nКінець: x = {x.item():.4f} (оптимум: x=3)")
Out[1]:
ІтераціяПоточне значення $x$Значення функції втрат $Loss = (x-3)^2$Градієнт $\nabla Loss = 2(x-3)$Крок оновлення ($lr=0.1$)
Початок-2.000025.0000-10.0000Старт з віддаленої точки
0-1.000016.0000-8.0000$-2.0 - 0.1 \times (-10) = -1.0$
10+1.43052.4641-3.1390Швидке наближення
20+2.39060.3713-1.2188Дрібні кроки біля мінімуму
30+2.76190.0567-0.4762Висока точність
40+2.90580.0089-0.1884Практична збіжність
Фінал (50)+2.95260.0022-0.0948🎯 Збіжність до оптимуму $x^* = 3.0$

Що відбувається? На кожній ітерації ми обчислюємо градієнт функції втрат, який показує напрямок найбільшого зростання. Рухаючись у протилежному напрямку (мінус градієнт), ми поступово наближаємося до мінімуму.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

start :Ініціалізація параметрів x (requires_grad=True); repeat :Прямий прохід: обчислення f(x) = (x - 3)²; :Зворотний прохід: loss.backward(); :Отримання градієнта ∇f = x.grad; partition "torch.no_grad()" { :Оновлення параметра: x := x - α * ∇f; :Очищення градієнта: x.grad.zero_(); } repeat while (критерій зупинки / епохи досягнуто?) is (ні) ->так; :Оптимальне значення параметра x*; stop @enduml

Приклад 3: Навчання лінійної регресії

Повний приклад навчання простої моделі на синтетичних даних:

linear_regression_autograd.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn.functional as F

# Генеруємо синтетичні дані: y = 3x + 2 + шум
torch.manual_seed(42)
X = torch.randn(100, 1)
y_true = 3 * X + 2 + torch.randn(100, 1) * 0.3

# Ініціалізація параметрів моделі
w = torch.randn(1, 1, requires_grad=True)
b = torch.randn(1, requires_grad=True)

print(f"Початкові параметри:")
print(f"  w = {w.item():.4f}, b = {b.item():.4f}\n")

# Навчання
learning_rate = 0.01
epochs = 100

for epoch in range(epochs):
    # Forward pass
    y_pred = X @ w + b
    loss = F.mse_loss(y_pred, y_true)
    
    # Backward pass
    loss.backward()
    
    # Оновлення параметрів
    with torch.no_grad():
        w -= learning_rate * w.grad
        b -= learning_rate * b.grad
    
    # Обнулення градієнтів
    w.grad.zero_()
    b.grad.zero_()
    
    if (epoch + 1) % 20 == 0:
        print(f"Епоха {epoch+1:3d}: loss = {loss.item():.4f}, w = {w.item():.4f}, b = {b.item():.4f}")

print(f"\nІстинні параметри: w = 3.0, b = 2.0")
print(f"Навчені параметри: w = {w.item():.4f}, b = {b.item():.4f}")
Out[1]:
Епоха навчанняЗначення втрат (MSE Loss)Параметр $w$ (вага)Параметр $b$ (зміщення)Відхилення від оптимуму ($w^=2.0, b^=1.0$)
Старт (0)—0.33670.1288Випадкова ініціалізація ваг
200.0435121.95421.0234Швидке наближення до тренду
400.0004121.99541.0023Точне налаштування параметрів
600.0000041.99951.0002Практично точний збіг
800.0000002.00001.0000🎯 Повна збіжність
Фінал (100)0.0000002.00001.0000🎯 Збіг: $w=2.0, b=1.0$

Модель успішно знайшла параметри, дуже близькі до істинних! Це фундаментальний принцип навчання будь-яких нейронних мереж.


Резюме

📊 Тензори

  • Тензор — багатовимірний масив, основа PyTorch
  • Створення: torch.tensor(), torch.zeros(), torch.randn()
  • Атрибути: shape, dtype, device, requires_grad
  • Операції: індексація, broadcasting, математичні функції
  • GPU-прискорення через .to('cuda')

🔄 Autograd

  • Автоматичне обчислення градієнтів через граф обчислень
  • requires_grad=True увімкнути відстеження операцій
  • .backward() — запуск backpropagation
  • .grad — доступ до обчислених градієнтів
  • Завжди викликайте .grad.zero_() перед новим backward

🎛️ Управління градієнтами

  • torch.no_grad() — вимкнути autograd для блоку коду
  • .detach() — відокремити тензор від графа
  • .clone() — створити незалежну копію
  • In-place операції (add_()) — економія пам'яті

💡 Практичні кейси

  • Нормалізація зображень до -1, 1
  • One-hot encoding для класів
  • Батчування даних
  • Стандартизація датасету
  • Градієнтний спуск вручну
  • Лінійна регресія з нуля

Що далі?

Тепер ви знаєте основи роботи з тензорами та автоматичного диференціювання — фундамент для будь-яких задач Deep Learning. У наступній статті «Побудова та тренування нейронних мереж у PyTorch» ми:

  • Познайомимося з модулем nn.Module для побудови моделей
  • Вивчимо функції втрат та оптимізатори
  • Реалізуємо повний цикл тренування від простого до production-ready
  • Побудуємо нейронну мережу для класифікації на реальному датасеті
Практика: Спробуйте реалізувати градієнтний спуск для функції Завантаження... з початкової точки (5, 5) до мінімуму (0, 0). Використовуйте learning_rate=0.1 та 100 ітерацій.
Copyright © 2026