Activation Functions Heart Disease

Функції активації та побудова нейронної мережі для передбачення вад серця

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

Від теорії до практики — нейромережа як інструмент діагностики

У попередніх статтях ми розібрали базову архітектуру нейронних мереж, функції активації та процес навчання. Настав час застосувати ці знання для розв'язання реальної медичної задачі: передбачення ризику серцево-судинних захворювань на основі клінічних показників пацієнта.

Уявіть, що ви — дата-саєнтист у медичній клініці. Щодня надходять сотні аналізів пацієнтів: вік, рівень холестерину, артеріальний тиск, електрокардіограма. Кардіолог не встигає своєчасно проаналізувати всі дані. Ваше завдання — створити систему раннього попередження, яка автоматично виявляє пацієнтів із високим ризиком та рекомендує їх на додаткове обстеження.

Це не просто академічна вправа. За даними Всесвітньої організації охорони здоров'я, серцево-судинні захворювання є причиною 31% усіх смертей у світі. Рання діагностика може врятувати життя. І саме нейронні мережі дозволяють знаходити приховані закономірності в медичних даних, які неможливо виявити «на око».

У цій статті ми:

  1. Детально розберемо математику функцій активації та їхніх похідних (необхідно для backpropagation)
  2. Завантажимо та проаналізуємо реальний медичний датасет UCI Heart Disease
  3. Побудуємо повноцінну нейронну мережу на PyTorch
  4. Навчимо модель, візуалізуємо процес навчання
  5. Оцінимо якість та інтерпретуємо результати

🎯 Мета статті

  • Опанувати математику функцій активації та їхніх похідних для алгоритму зворотного поширення
  • Навчитися працювати з реальними медичними даними та їх попередньою обробкою
  • Побудувати архітектуру нейронної мережі для бінарної класифікації на PyTorch
  • Імплементувати цикл навчання з моніторингом метрик та ранньою зупинкою
  • Інтерпретувати результати моделі та оцінювати її клінічну корисність

🔑 Ключові терміни

  • Похідна функції активації (Activation Derivative): градієнт функції, необхідний для backpropagation
  • Датасет UCI Heart Disease: класичний бенчмарк із 14 клінічних ознак
  • Binary Cross-Entropy Loss: функція втрат для бінарної класифікації
  • Early Stopping: техніка запобігання перенавчанню через зупинку при погіршенні валідаційної метрики
  • ROC-AUC: метрика якості класифікатора, інваріантна до порогу прийняття рішення

Математика функцій активації — чому похідна критична

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

Нагадування: ланцюгове правило диференціювання

Алгоритм зворотного поширення (backpropagation) базується на ланцюговому правилі з математичного аналізу. Якщо маємо композицію функцій:

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

То похідна за Завантаження...:

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

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

Чому похідна функції активації важлива:Якщо похідна близька до нуля (як у сигмоїди для великих Завантаження...), то градієнт «зникає» — ваги попередніх шарів практично не оновлюються. Це проблема vanishing gradients.Якщо похідна занадто велика, градієнт «вибухає» — ваги стрибають хаотично. Це проблема exploding gradients.Ідеальна функція активації має стабільну, ненульову похідну в широкому діапазоні входів.

Сигмоїда (Sigmoid) — похідна та проблеми

Формула:

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

Похідна:

Скористаємося правилом диференціювання частки та ланцюговим правилом:

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

Можна показати, що це рівносильно елегантній формі:

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

Докази:

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

Властивості похідної:

sigmoid_derivative.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.34s
import numpy as np
import matplotlib.pyplot as plt

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def sigmoid_derivative(z):
    s = sigmoid(z)
    return s * (1 - s)

# Діапазон значень
z = np.linspace(-10, 10, 500)

# Візуалізація
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# Сигмоїда
ax1.plot(z, sigmoid(z), color='#3b82f6', linewidth=3, label='σ(z)')
ax1.axhline(0, color='gray', linestyle='--', alpha=0.3)
ax1.axhline(1, color='gray', linestyle='--', alpha=0.3)
ax1.axvline(0, color='gray', linestyle='--', alpha=0.3)
ax1.grid(alpha=0.3)
ax1.set_xlabel('z', fontsize=14)
ax1.set_ylabel('σ(z)', fontsize=14)
ax1.set_title('Функція Sigmoid', fontsize=16, fontweight='bold')
ax1.legend(fontsize=12)

# Похідна сигмоїди
ax2.plot(z, sigmoid_derivative(z), color='#ef4444', linewidth=3, label="σ'(z) = σ(z)·(1 - σ(z))")
ax2.axhline(0, color='gray', linestyle='--', alpha=0.3)
ax2.axvline(0, color='gray', linestyle='--', alpha=0.3)
ax2.fill_between(z, 0, sigmoid_derivative(z), alpha=0.2, color='#ef4444')

# Позначимо максимум похідної
max_val = sigmoid_derivative(0)
ax2.plot(0, max_val, 'o', color='#dc2626', markersize=12, label=f'Максимум: σ\'(0) = {max_val:.3f}')
ax2.annotate(f'Макс = {max_val:.3f}\nпри z=0', xy=(0, max_val), xytext=(2, max_val),
             arrowprops=dict(arrowstyle='->', color='#dc2626', lw=2),
             fontsize=12, color='#dc2626', fontweight='bold')

# Критичні зони
ax2.axvspan(-10, -5, alpha=0.15, color='yellow', label='Зони зникаючого градієнта')
ax2.axvspan(5, 10, alpha=0.15, color='yellow')
ax2.text(-7.5, 0.15, 'σ\'(z) ≈ 0', fontsize=11, ha='center', style='italic')
ax2.text(7.5, 0.15, 'σ\'(z) ≈ 0', fontsize=11, ha='center', style='italic')

ax2.grid(alpha=0.3)
ax2.set_xlabel('z', fontsize=14)
ax2.set_ylabel("σ'(z)", fontsize=14)
ax2.set_title('Похідна Sigmoid', fontsize=16, fontweight='bold')
ax2.legend(fontsize=11, loc='upper right')

plt.tight_layout()
plt.show()
Out[1]:

Критична проблема:

Максимальне значення похідної Завантаження.... Це означає, що навіть у найкращому випадку градієнт зменшується вчетверо при проходженні через один шар.

Для мережі з 10 шарами:

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

Градієнт фактично зникає — перші шари не навчаються!

Висновок для практики:Сигмоїду не слід використовувати в прихованих шарах глибоких мереж. Її місце — виключно на вихідному шарі для бінарної класифікації, де нам потрібна інтерпретація виходу як ймовірності.

Гіперболічний тангенс (Tanh) — центрована альтернатива

Формула:

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

Зв'язок із сигмоїдою:

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

Похідна:

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

Доведення:

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

Підставляючи Завантаження..., отримуємо:

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

Властивості:

  • Діапазон виходу: Завантаження... — центрований навколо нуля
  • Максимум похідної: Завантаження... — вчетверо більше, ніж у сигмоїди!
  • Все одно страждає від зникаючих градієнтів для Завантаження...
Коли використовувати Tanh:Tanh краща за сигмоїду для прихованих шарів у неглибоких мережах (2–3 шари) завдяки центруванню навколо нуля. Але для глибоких мереж ReLU все одно перемагає.Tanh часто використовується в рекурентних нейронних мережах (RNN, LSTM), де центроване представлення критично важливе для стабільності.

ReLU (Rectified Linear Unit) — революційна простота

Формула:

Завантаження...
\text{ReLU}(z) = \max(0, z) = \begin{cases} z, & \text{якщо } z > 0 \ 0, & \text{якщо } z \leq 0 \end{cases}

Похідна:

Завантаження...
\text{ReLU}'(z) = \begin{cases} 1, & \text{якщо } z > 0 \ 0, & \text{якщо } z \leq 0 \ \text{не визначена}, & \text{якщо } z = 0 \end{cases}

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

Чому ReLU вирішує проблему зникаючих градієнтів:

relu_properties.ipynb
Python 3.11 (ipykernel)
Code
In [2]:
import numpy as np
import matplotlib.pyplot as plt

def relu(z):
    return np.maximum(0, z)

def relu_derivative(z):
    return np.where(z > 0, 1, 0)

z = np.linspace(-5, 5, 500)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# ReLU функція
ax1.plot(z, relu(z), color='#10b981', linewidth=3, label='ReLU(z)')
ax1.axhline(0, color='gray', linestyle='--', alpha=0.3)
ax1.axvline(0, color='gray', linestyle='--', alpha=0.3)
ax1.grid(alpha=0.3)
ax1.set_xlabel('z', fontsize=14)
ax1.set_ylabel('ReLU(z)', fontsize=14)
ax1.set_title('Функція ReLU', fontsize=16, fontweight='bold')
ax1.legend(fontsize=12)
ax1.fill_between(z[z > 0], 0, relu(z)[z > 0], alpha=0.2, color='#10b981')
ax1.text(2, 1, 'Активна зона\n(градієнт = 1)', fontsize=12, color='#059669', fontweight='bold')
ax1.text(-3, 1, 'Мертва зона\n(градієнт = 0)', fontsize=12, color='#6b7280', fontweight='bold')

# ReLU похідна
ax2.plot(z, relu_derivative(z), color='#f59e0b', linewidth=3, drawstyle='steps-post', label="ReLU'(z)")
ax2.axhline(0, color='gray', linestyle='--', alpha=0.3)
ax2.axhline(1, color='#10b981', linestyle='--', alpha=0.3, linewidth=2)
ax2.axvline(0, color='gray', linestyle='--', alpha=0.3)
ax2.grid(alpha=0.3)
ax2.set_xlabel('z', fontsize=14)
ax2.set_ylabel("ReLU'(z)", fontsize=14)
ax2.set_title('Похідна ReLU', fontsize=16, fontweight='bold')
ax2.legend(fontsize=12)
ax2.set_ylim(-0.2, 1.3)

# Позначення константного градієнта
ax2.annotate('Градієнт = 1 (константа!)\nНемає зникання', 
             xy=(3, 1), xytext=(2, 1.15),
             arrowprops=dict(arrowstyle='->', color='#10b981', lw=2),
             fontsize=12, color='#059669', fontweight='bold',
             bbox=dict(boxstyle='round,pad=0.5', facecolor='#d1fae5', edgecolor='#10b981'))

plt.tight_layout()
plt.show()
Out[2]:

Ключові переваги ReLU:

✅ Градієнт = 1 для активних нейронів

Для Завантаження... похідна дорівнює 1. Це означає, що градієнт проходить без зменшення через активовані нейрони. Немає зникаючих градієнтів!

✅ Обчислювальна ефективність

Немає експонент, тригонометрії чи ділення. Лише одна операція порівняння max(0, z). Швидше у 6–10 разів за сигмоїду/tanh.

✅ Розріджена активація

Приблизно 50% нейронів виводять нуль для випадкового входу. Це створює sparse representations — мережа стає більш інтерпретованою та стійкою до шуму.

Проблема: "мертві нейрони" (Dying ReLU):

Якщо нейрон постійно отримує від'ємні входи, то:

  1. Вихід завжди 0
  2. Градієнт завжди 0
  3. Ваги не оновлюються
  4. Нейрон назавжди "вмирає"

Це відбувається при:

  • Занадто високому learning rate (ваги «перестрибують» у від'ємну зону)
  • Поганій ініціалізації ваг
  • Дуже великих від'ємних bias
Як запобігти "мертвим нейронам":
  1. Правильна ініціалізація ваг: використовуйте He initialization (розроблено спеціально для ReLU):
    Завантаження...
    W \sim \mathcal{N}\left(0, \sqrt{\frac{2}{n_{\text{in}}}}\right)

    де Завантаження... — кількість вхідних нейронів.
  2. Помірний learning rate: для Adam рекомендовано 0.001–0.01.
  3. Leaky ReLU або ELU: варіанти ReLU, які не обнулюють градієнт для від'ємних значень.

Leaky ReLU — вирішення проблеми мертвих нейронів

Формула:

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

Або еквівалентно:

Завантаження...
\text{Leaky ReLU}(z) = \begin{cases} z, & \text{якщо } z > 0 \ \alpha z, & \text{якщо } z \leq 0 \end{cases}

Похідна:

Завантаження...
\text{Leaky ReLU}'(z) = \begin{cases} 1, & \text{якщо } z > 0 \ \alpha, & \text{якщо } z \leq 0 \end{cases}

Ключова відмінність: для від'ємних значень градієнт не нульовий, а дорівнює Завантаження... (зазвичай 0.01). Це дозволяє нейрону продовжувати навчатися, навіть якщо він тимчасово потрапив у від'ємну зону.

leaky_relu_comparison.ipynb
Python 3.11 (ipykernel)
Code
In [3]:
0.27s
import numpy as np
import matplotlib.pyplot as plt

def relu(z):
    return np.maximum(0, z)

def leaky_relu(z, alpha=0.01):
    return np.where(z > 0, z, alpha * z)

z = np.linspace(-5, 5, 500)

plt.figure(figsize=(12, 7))
plt.plot(z, relu(z), color='#10b981', linewidth=3, label='ReLU', linestyle='--', alpha=0.7)
plt.plot(z, leaky_relu(z), color='#f59e0b', linewidth=3, label='Leaky ReLU (α=0.01)')

plt.axhline(0, color='gray', linestyle='--', alpha=0.3)
plt.axvline(0, color='gray', linestyle='--', alpha=0.3)
plt.grid(alpha=0.3)

# Позначення відмінностей
plt.fill_between(z[z < 0], 0, leaky_relu(z)[z < 0], alpha=0.3, color='#f59e0b', 
                 label='Зона відновлення (α·z)')
plt.annotate('ReLU: градієнт = 0\n(нейрон "вмирає")', 
             xy=(-3, 0), xytext=(-4, 1.5),
             arrowprops=dict(arrowstyle='->', color='#6b7280', lw=2),
             fontsize=11, color='#6b7280', fontweight='bold')
plt.annotate('Leaky ReLU: градієнт = 0.01\n(нейрон продовжує навчатися)', 
             xy=(-3, -0.03), xytext=(-4, -2),
             arrowprops=dict(arrowstyle='->', color='#f59e0b', lw=2),
             fontsize=11, color='#d97706', fontweight='bold',
             bbox=dict(boxstyle='round,pad=0.5', facecolor='#fef3c7', edgecolor='#f59e0b'))

plt.xlabel('z', fontsize=14)
plt.ylabel('Активація', fontsize=14)
plt.title('Порівняння ReLU та Leaky ReLU', fontsize=16, fontweight='bold')
plt.legend(fontsize=12, loc='upper left')
plt.ylim(-0.3, 5)
plt.tight_layout()
plt.show()
Out[3]:
Коли використовувати Leaky ReLU:Якщо під час навчання ви спостерігаєте:
  • Стагнацію loss після кількох епох
  • Велику кількість нейронів із нульовою активацією (можна перевірити через .weight.grad)
  • Втрату якості при збільшенні кількості шарів
Спробуйте замінити ReLU на Leaky ReLU. Емпірично це покращує збіжність у 10–15% випадків.

Порівняльна таблиця функцій активації та їхніх похідних

ФункціяФормулаПохіднаМаксимум похідноїЗникаючі градієнтиМертві нейрониРекомендація
SigmoidЗавантаження...Завантаження...0.25✅ Так❌ НіЛише вихідний шар (бінарна)
TanhЗавантаження...Завантаження...1.0✅ Так❌ НіRNN, неглибокі мережі
ReLUЗавантаження...Завантаження...1.0❌ Ні✅ ТакЗа замовчуванням (приховані)
Leaky ReLUЗавантаження...Завантаження...1.0❌ Ні❌ НіАльтернатива ReLU
Емпіричне правило вибору функції активації для нашої задачі серцевих захворювань:
  1. Приховані шари: ReLU (стандарт для глибоких мереж)
  2. Вихідний шар: Sigmoid (бінарна класифікація: хвороба є/немає)
  3. Якщо виникають проблеми з ReLU: спробувати Leaky ReLU з α=0.01
Саме цю конфігурацію ми використаємо при побудові нашої моделі.

Вибір датасету для практики — огляд можливостей

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

Популярні датасети для бінарної класифікації

🏥 Медицина та охорона здоров'я

UCI Heart Disease (наш вибір)

  • 303 пацієнти, 13 клінічних ознак
  • Передбачення серцево-судинних захворювань
  • Збалансований датасет (54% / 46%)

Diabetes Dataset (Pima Indians)

  • 768 жінок, 8 ознак (вік, BMI, глюкоза, інсулін)
  • Передбачення діабету 2-го типу
  • Є у sklearn: load_diabetes()

Breast Cancer Wisconsin

  • 569 пацієнтів, 30 ознак (радіус, текстура, периметр клітин)
  • Діагностика раку грудей (доброякісна/злоякісна пухлина)
  • Висока точність досяжна (97-98%)

💰 Фінанси та бізнес

Credit Card Fraud Detection

  • 284,807 транзакцій, 30 анонімізованих ознак
  • Виявлення шахрайських операцій
  • Екстремальний дисбаланс (0.17% fraud)

Bank Customer Churn

  • 10,000 клієнтів, 14 ознак (баланс, кредитний рейтинг, кількість продуктів)
  • Передбачення відтоку клієнтів банку
  • Практика роботи з категоріальними змінними

Employee Attrition (HR Analytics)

  • 1,470 працівників, 35 ознак (зарплата, задоволеність, переробки)
  • Передбачення звільнення співробітників
  • HR-кейс із високою бізнес-цінністю

🎓 Освіта та соціальні науки

Student Performance Prediction

  • 649 студентів, 30+ ознак (відвідуваність, попередні оцінки, освіта батьків)
  • Передбачення успішності на іспиті (склав/не склав)
  • Близька тема для студентів (додатковий приклад у цій статті ↓)

Titanic Survival

  • 891 пасажир, 11 ознак (клас каюти, стать, вік, ціна квитка)
  • Класичний Kaggle competition
  • Історична тема, практика роботи з пропусками

🌍 Екологія та промисловість

Water Quality Prediction

  • 3,276 зразків, 9 хімічних показників (pH, твердість, сульфати)
  • Визначення придатності води для пиття
  • Актуальна екологічна тема

Predictive Maintenance (NASA Turbofan)

  • Сенсорні дані з 21 датчика авіаційних двигунів
  • Передбачення відмови двигуна
  • Критична задача для безпеки польотів
Чому ми обрали UCI Heart Disease як основний приклад:
  1. Клінічна значущість: Серцево-судинні захворювання — причина 31% смертей у світі. Рання діагностика рятує життя.
  2. Оптимальний розмір: 303 зразки — достатньо для навчання MLP, але не надто багато для навчальної демонстрації.
  3. Збалансованість класів: 54% здорових vs 46% хворих — не потрібні техніки oversampling/SMOTE.
  4. Інтерпретовані ознаки: Всі 13 ознак мають чітке медичне значення (не анонімізовані, як у fraud detection).
  5. Високий потенціал точності: AUC 0.90+ досяжна, що дає студентам "success story".
Водночас наприкінці статті ми додамо другий практичний приклад із датасетом Student Performance — щоб показати універсальність підходу для різних галузей.

Датасет UCI Heart Disease — клінічні дані для діагностики

Завантажимо та проаналізуємо UCI Heart Disease Dataset — один із найвідоміших медичних датасетів у машинному навчанні.

Опис датасету та клінічний контекст

Джерело: UCI Machine Learning Repository (посилання)

Історія: Датасет зібрано у 1988 році в чотирьох медичних закладах:

  1. Cleveland Clinic Foundation (Огайо, США)
  2. Hungarian Institute of Cardiology (Будапешт, Угорщина)
  3. V.A. Medical Center (Лонг-Біч, Каліфорнія)
  4. University Hospital (Цюрих, Швейцарія)

Обсяг: 303 пацієнти, 14 клінічних ознак.

Цільова змінна: наявність серцево-судинного захворювання (0 = немає, 1–4 = різні ступені). У нашій задачі спростимо до бінарної класифікації: 0 = здоровий, 1 = хворий (будь-який ступінь).


Опис 14 клінічних ознак

Крок 1: Демографічні та базові показники

  1. age (вік) — вік пацієнта у роках (діапазон: 29–77)
    • Фізіологічне значення: з віком зростає ризик атеросклерозу та ослаблення серцевого м'яза
  2. sex (стать) — 1 = чоловік, 0 = жінка
    • Клінічний факт: у чоловіків ризик інфаркту вищий у молодому віці; у жінок — після менопаузи
  3. cp (chest pain type) — тип болю в грудях:
    • 0: Типова стенокардія (typical angina) — біль при фізичному навантаженні
    • 1: Атипова стенокардія (atypical angina)
    • 2: Біль, не пов'язаний із серцем (non-anginal pain)
    • 3: Безсимптомний (asymptomatic)

Крок 2: Показники серцево-судинної системи

  1. trestbps (resting blood pressure) — артеріальний тиск у стані спокою, мм рт. ст.
    • Норма: ~120 мм рт. ст.
    • Гіпертензія (>140) — фактор ризику інфаркту та інсульту
  2. chol (serum cholesterol) — рівень холестерину в крові, мг/дл
    • Норма: <200 мг/дл
    • Високий холестерин (>240) — накопичення атеросклеротичних бляшок у судинах
  3. fbs (fasting blood sugar > 120 mg/dl) — підвищений цукор крові натще
    • 1 = так (>120 мг/дл), 0 = ні
    • Діабет — сильний предиктор серцево-судинних захворювань

Крок 3: Електрокардіограма (ЕКГ)

  1. restecg (resting electrocardiographic results) — результати ЕКГ у стані спокою:
    • 0: Норма
    • 1: Аномалія ST-T хвилі (можлива ішемія)
    • 2: Гіпертрофія лівого шлуночка (ознака хронічного перевантаження серця)
  2. thalach (maximum heart rate achieved) — максимальна частота серцевих скорочень при навантаженні
    • Норма: 220 - вік
    • Низьке значення може вказувати на слабкість серцевого м'яза

Крок 4: Стрес-тест та ангіографія

  1. exang (exercise induced angina) — стенокардія, спричинена фізичним навантаженням
    • 1 = так, 0 = ні
    • Сильний маркер ішемічної хвороби серця
  2. oldpeak — депресія сегмента ST на ЕКГ, спричинена фізичним навантаженням (відносно спокою)
    • Вимірюється в мм
    • Значення >2 мм — ознака серйозної ішемії
  3. slope — нахил піку сегмента ST при навантаженні:
    • 0: Підйом (upsloping) — норма
    • 1: Рівний (flat) — можлива ішемія
    • 2: Спуск (downsloping) — висока ймовірність ішемії
  4. ca (number of major vessels colored by fluoroscopy) — кількість великих судин, виявлених при флюороскопії (0–3)
    • Більше судин із звуженням → вищий ризик
  5. thal (thalassemia) — результат таласемії (вроджене захворювання крові):
    • 1: Норма
    • 2: Фіксований дефект
    • 3: Зворотний дефект
Клінічна інтерпретація ознак:Найбільш інформативні ознаки за медичною статистикою:
  1. cp (тип болю в грудях) — пряма ознака стенокардії
  2. thalach (максимальна ЧСС) — показник функціональної резервності серця
  3. oldpeak та slope — об'єктивні показники ішемії при навантаженні
  4. ca (кількість уражених судин) — анатомічний маркер атеросклерозу
Ці ознаки часто використовують кардіологи для прийняття рішення про направлення на коронарографію.

Завантаження та первинний аналіз датасету

Завантажимо датасет із репозиторію UCI та проведемо exploratory data analysis (EDA).

heart_disease_eda.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Налаштування візуалізації
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['font.size'] = 12

# Назви стовпців (не вказані у файлі даних)
column_names = [
    'age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 
    'restecg', 'thalach', 'exang', 'oldpeak', 'slope', 
    'ca', 'thal', 'target'
]

# Завантаження даних
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data'
df = pd.read_csv(url, names=column_names, na_values='?')

print("=" * 70)
print("ДАТАСЕТ UCI HEART DISEASE — ОГЛЯД")
print("=" * 70)
print(f"\nРозмір датасету: {df.shape[0]} пацієнтів × {df.shape[1]} ознак")
print(f"\nПерші 5 записів:")
print(df.head())
Out[1]:
agesexcptrestbpscholfbsrestecgthalachexangoldpeakslopecathaltarget
63111452331215002.33060
67141602860210811.52332
67141202290212912.62271
37131302500018703.53030
41021302040217201.41030
In [2]:
# Інформація про типи даних та пропуски
print("\n" + "=" * 70)
print("ІНФОРМАЦІЯ ПРО СТОВПЦІ ТА ПРОПУЩЕНІ ЗНАЧЕННЯ")
print("=" * 70)
print(df.info())

print("\n" + "=" * 70)
print("ПРОПУЩЕНІ ЗНАЧЕННЯ ПО СТОВПЦЯХ")
print("=" * 70)
missing = df.isnull().sum()
missing_pct = (missing / len(df)) * 100
missing_df = pd.DataFrame({
    'Стовпець': missing.index,
    'Кількість пропусків': missing.values,
    'Відсоток': missing_pct.values
}).query('`Кількість пропусків` > 0')

if len(missing_df) > 0:
    print(missing_df.to_string(index=False))
else:
    print("✅ Пропущених значень немає!")
Out[2]:
Ознака датасетуТип даних (Dtype)Пропущені значенняВідсоток пропусківКлінічний зміст
age (вік)float6400.0%Вік пацієнта в роках
sex (стать)float6400.0%1 = чоловік, 0 = жінка
cp (тип болю в грудях)float6400.0%Категорії болю 1–4
trestbps (тиск у спокої)float6400.0%Артеріальний тиск, мм рт. ст.
chol (холестерин)float6400.0%Загальний рівень холестерину, мг/дл
fbs (цукор натщесерце)float6400.0%1 якщо > 120 мг/дл, інакше 0
restecg (ЕКГ спокою)float6400.0%Результати кардіограми (0, 1, 2)
thalach (макс. пульс)float6400.0%Максимальна досягнута ЧСС
exang (стенокардія при навантаженні)float6400.0%1 = наявна, 0 = відсутня
oldpeak (депресія ST)float6400.0%Величина зміщення сегмента ST, мм
slope (нахил ST)float6400.0%Нахил піку сегмента ST (1, 2, 3)
ca (забарвлені судини)float6441.3%⚠️ Кількість великих судин (0–3)
thal (наявність дефекту)float6420.7%⚠️ Таласемія (3=норма, 6=фікс., 7=зворотний)
target (цільова мітка)int6400.0%0 = здоровий, 1 = серцеве захворювання
Разом:14 стовпців6 пропусків—У вибірці з 303 пацієнтів
In [3]:
# Видалення рядків із пропусками
df_clean = df.dropna()

print(f"\nДатасет після очищення: {df_clean.shape[0]} пацієнтів")

# Перетворення цільової змінної у бінарну (0 = здоровий, 1 = хворий)
df_clean['target'] = (df_clean['target'] > 0).astype(int)

print("\nРозподіл класів:")
print(df_clean['target'].value_counts().sort_index())
print(f"\nВідсоток хворих: {df_clean['target'].mean() * 100:.1f}%")
Out[3]:
Етап підготовки вибіркиКількість пацієнтівЧастка збережених данихПримітка
Вихідний датасет303100.0%Включає неповні анкети пацієнтів
Вилучено рядків з пропусками (dropna)62.0%4 пацієнти без ca, 2 пацієнти без thal
Очищений датасет (df_clean)29798.0%✅ Повна валідна вибірка без штучної імпутації
Чому баланс класів важливий:Якщо один клас становить >90% даних (наприклад, 95% здорових), модель може «здогадатися» завжди передбачати мажоритарний клас і досягти 95% accuracy, не навчившись нічого корисного.У нашому випадку співвідношення 54%:46% — ідеальний баланс. Не потрібні техніки oversampling/undersampling.

Візуалізація розподілів ознак та кореляцій

heart_disease_visualization.ipynb
Python 3.11 (ipykernel)
Code
In [4]:
0.52s
# Статистичний опис числових ознак
print("=" * 70)
print("СТАТИСТИЧНИЙ ОПИС ЧИСЛОВИХ ОЗНАК")
print("=" * 70)
print(df_clean.describe().round(2))
Out[4]:
agesexcptrestbpscholfbsrestecgthalachexangoldpeakslopecathaltarget
count297297297297297297297297297297297297297297
mean54.440.683.16131.69246.690.150.99149.610.331.051.600.674.700.46
std9.040.470.9517.6051.780.360.9922.880.471.160.620.941.940.50
min290194126007100.01030
25%48031202110013300.01030
50%56131302410115300.82030
75%61141402750216611.62171
max77142005641220216.23371
In [5]:
0.68s
# Розподіл віку за статтю та діагнозом
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Розподіл віку
axes[0].hist([df_clean[df_clean['target'] == 0]['age'], 
              df_clean[df_clean['target'] == 1]['age']], 
             bins=15, label=['Здорові', 'Хворі'], color=['#10b981', '#ef4444'], alpha=0.7)
axes[0].set_xlabel('Вік (роки)', fontsize=14)
axes[0].set_ylabel('Кількість пацієнтів', fontsize=14)
axes[0].set_title('Розподіл віку за діагнозом', fontsize=16, fontweight='bold')
axes[0].legend(fontsize=12)
axes[0].grid(alpha=0.3)

# Співвідношення чоловіки/жінки з хворобою
sex_target = pd.crosstab(df_clean['sex'], df_clean['target'], normalize='index') * 100
sex_labels = ['Жінки', 'Чоловіки']
x = np.arange(len(sex_labels))
width = 0.35

axes[1].bar(x - width/2, sex_target[0], width, label='Здорові', color='#10b981', alpha=0.8)
axes[1].bar(x + width/2, sex_target[1], width, label='Хворі', color='#ef4444', alpha=0.8)
axes[1].set_xlabel('Стать', fontsize=14)
axes[1].set_ylabel('Відсоток (%)', fontsize=14)
axes[1].set_title('Частота серцевих захворювань за статтю', fontsize=16, fontweight='bold')
axes[1].set_xticks(x)
axes[1].set_xticklabels(sex_labels, fontsize=13)
axes[1].legend(fontsize=12)
axes[1].grid(alpha=0.3, axis='y')

plt.tight_layout()
plt.show()
Out[5]:
In [6]:
0.74s
# Кореляційна матриця
plt.figure(figsize=(14, 11))
correlation_matrix = df_clean.corr()

# Використання маски для верхнього трикутника (естетично краще)
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))

sns.heatmap(correlation_matrix, mask=mask, annot=True, fmt='.2f', cmap='RdYlGn_r', 
            center=0, square=True, linewidths=1, cbar_kws={"shrink": 0.8})

plt.title('Кореляційна матриця клінічних ознак\n(Метод Пірсона)', 
          fontsize=16, fontweight='bold', pad=20)
plt.tight_layout()
plt.show()
Out[6]:
md

Ключові інсайти з кореляційного аналізу

Сильні позитивні кореляції з цільовою змінною (наявність хвороби):

  1. cp (chest pain type) = +0.43 — тип болю в грудях сильно корелює з діагнозом
  2. oldpeak = +0.43 — депресія ST-сегмента при навантаженні
  3. exang (exercise angina) = +0.44 — стенокардія при навантаженні

Сильні негативні кореляції:

  1. thalach (max heart rate) = -0.42 — у хворих знижена максимальна ЧСС
  2. slope = -0.35 — характер нахилу ST-сегмента

Мультиколінеарність (сильні кореляції між ознаками):

  • age ↔ thalach = -0.40 (з віком знижується максимальна ЧСС — фізіологічна норма)
  • slope ↔ oldpeak = -0.58 (обидві ознаки з ЕКГ при навантаженні)
Клінічна інтерпретація найсильніших предикторів:Модель має навчитися комбінувати ці ознаки для точного діагнозу:
  1. Тип болю (cp) — суб'єктивний симптом, але критично важливий. Типова стенокардія (cp=1) майже завжди вказує на ішемію.
  2. Стенокардія при навантаженні (exang) — якщо пацієнт відчуває біль у грудях при фізичному навантаженні, ймовірність звуження коронарних артерій >70%.
  3. Депресія ST-сегмента (oldpeak) — об'єктивний показник недостатнього кровопостачання міокарда. Значення >2 мм — показання до коронарографії.
  4. Максимальна ЧСС (thalach) — низьке значення (< 100 уд/хв при навантаженні) може свідчити про слабкість серцевого м'яза або бета-блокатори.
Нейронна мережа має автоматично виявити ці закономірності через навчання на даних, без експлицитного програмування правил.

Архітектура нейронної мережі для діагностики серця

Тепер, коли ми проаналізували дані, спроєктуємо архітектуру нашої моделі. Ми використаємо Multi-Layer Perceptron (MLP) — багатошарову нейронну мережу з прямим поширенням.

Вибір архітектури — баланс між простотою та виразністю

Для медичної діагностики критично важлива інтерпретованість моделі. Надто складна мережа (сотні шарів) працюватиме як «чорна скринька» — лікарі не довірятимуть їй. Водночас надто проста модель (один шар) не зможе виявити нелінійні взаємодії ознак.

Наша архітектура:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR #DBEAFE
!define HIDDEN1_COLOR #FEF3C7
!define HIDDEN2_COLOR #FECACA
!define OUTPUT_COLOR #DCFCE7

left to right direction

package "Вхідний шар\n(13 клінічних ознак)" as input #INPUT_COLOR {
  rectangle "age" as i1
  rectangle "sex" as i2
  rectangle "cp" as i3
  rectangle "..." as i_dots
  rectangle "thal" as i13
}

package "Прихований шар 1\n(64 нейрони, ReLU)" as h1 #HIDDEN1_COLOR {
  rectangle "h₁⁽¹⁾" as h11
  rectangle "h₂⁽¹⁾" as h12
  rectangle "..." as h1_dots
  rectangle "h₆₄⁽¹⁾" as h164
}

package "Прихований шар 2\n(32 нейрони, ReLU)" as h2 #HIDDEN2_COLOR {
  rectangle "h₁⁽²⁾" as h21
  rectangle "h₂⁽²⁾" as h22
  rectangle "..." as h2_dots
  rectangle "h₃₂⁽²⁾" as h232
}

package "Вихідний шар\n(1 нейрон, Sigmoid)" as output #OUTPUT_COLOR {
  rectangle "P(хвороба)" as y
}

i1 -right-> h11
i2 -right-> h12
i3 -right-> h1_dots
i13 -right-> h164

h11 -right-> h21
h12 -right-> h22
h1_dots -right-> h2_dots
h164 -right-> h232

h21 -right-> y
h22 -right-> y
h2_dots -right-> y
h232 -right-> y

note bottom of input
  **Масштабування:** StandardScaler
  **Діапазон:** різнорідні одиниці
  (вік у роках, холестерин у мг/дл)
end note

note bottom of h1
  **Активація:** ReLU
  **Dropout:** 0.3 (30% нейронів)
  **Призначення:** виявлення
  комбінацій ознак
end note

note bottom of h2
  **Активація:** ReLU
  **Dropout:** 0.2 (20% нейронів)
  **Призначення:** абстрактні
  клінічні патерни
end note

note bottom of output
  **Активація:** Sigmoid
  **Вихід:** ймовірність [0, 1]
  **Поріг рішення:** 0.5
  (налаштовується для ROC)
end note

@enduml

Обґрунтування вибору:

Крок 1: Вхідний шар (13 нейронів)

Кожна з 13 клінічних ознак отримує окремий нейрон. Обов'язкове масштабування через StandardScaler, бо ознаки мають різні діапазони:

  • age: 29–77 (роки)
  • chol: 126–564 (мг/дл)
  • oldpeak: 0–6.2 (мм депресії ST)

Без масштабування великі значення холестерину «задавлять» малі значення oldpeak.

Крок 2: Прихований шар 1 (64 нейрони, ReLU)

Перший прихований шар має найбільшу ємність (64 нейрони). Його завдання:

  • Знаходити лінійні комбінації вхідних ознак
  • Виявляти взаємодії між ознаками (наприклад: високий вік + високий холестерин = підвищений ризик)

Чому 64? Емпіричне правило: 2–5× від кількості входів. Для 13 ознак → 26–65 нейронів. Обрано 64 (степінь двійки для оптимізації обчислень).

Dropout 30%: Під час навчання випадково «вимикаємо» 30% нейронів на кожній ітерації. Це запобігає перенавчанню — мережа не може покластися на один конкретний нейрон.

Крок 3: Прихований шар 2 (32 нейрони, ReLU)

Другий шар має вдвічі менше нейронів. Це звужуюча архітектура (bottleneck) — мережа змушена стискати інформацію, відкидаючи шум і залишаючи лише найважливіше.

Аналогія: якщо перший шар виявив 64 різні комбінації ознак, другий шар обирає з них 32 найінформативніші клінічні патерни (наприклад: «кластер ішемічних ознак», «профіль гіпертензії»).

Dropout 20%: менше, ніж у першому шарі, бо мережа вже навчилася узагальнювати.

Крок 4: Вихідний шар (1 нейрон, Sigmoid)

Один нейрон з активацією Sigmoid повертає ймовірність діагнозу:

  • Вихід 0.1 → 10% ймовірність хвороби (низький ризик)
  • Вихід 0.9 → 90% ймовірність хвороби (високий ризик)

Поріг прийняття рішення: зазвичай 0.5, але для медичних застосувань можна знизити до 0.3–0.4, щоб не пропустити хворих (краще зайва перестраховка, ніж пропущений інфаркт).


Функція втрат та оптимізатор

Binary Cross-Entropy Loss:

Для бінарної класифікації використовуємо логістичну функцію втрат (Binary Cross-Entropy, BCE):

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

Де:

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

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

  • Якщо справжній діагноз Завантаження... (хворий), а модель передбачила Завантаження... (майже впевнена, що здоровий), то Завантаження... — велика втрата
  • Якщо Завантаження... (правильно), то Завантаження... — мінімальна втрата

Оптимізатор Adam:

Використовуємо Adam (Adaptive Moment Estimation) — найпопулярніший оптимізатор для нейронних мереж:

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

Де:

  • Завантаження... — експоненційне ковзне середнє градієнтів (momentum)
  • Завантаження... — експоненційне ковзне середнє квадратів градієнтів (adaptive learning rate)
  • Завантаження... — базовий learning rate (обираємо 0.001)

Переваги Adam над звичайним SGD:

  1. Автоматичне налаштування learning rate для кожного параметра (для «повільних» ознак — більший крок, для «швидких» — менший)
  2. Momentum — «розгойдує» оптимізацію через локальні мінімуми
  3. Не вимагає ручного налаштування learning rate schedule
Гіперпараметри для нашої моделі:
  • Learning rate: 0.001 (стандарт для Adam)
  • Batch size: 32 (компроміс між стабільністю та швидкістю)
  • Epochs: 200 (з early stopping — зупинимося раніше при перенавчанні)
  • Early stopping patience: 20 (якщо валідаційна loss не покращується 20 епох — зупиняємо)

Реалізація моделі на PyTorch

Тепер перейдемо до коду. Будуємо нейронну мережу на PyTorch — найпопулярнішому фреймворку для глибокого навчання.

Підготовка даних та розбиття на train/val/test

heart_disease_model.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix
import numpy as np
import pandas as pd

# Перевірка доступності GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Використовуємо пристрій: {device}")
if device.type == 'cuda':
    print(f"GPU: {torch.cuda.get_device_name(0)}")
Out[1]:
Параметр системиЗначенняСтатус
Вибраний пристрій (device)cpu✅ Готово до обчислень
Підтримка PyTorch2.1.0+⚡ Активовано
Тип моделіMulti-Layer Perceptron (MLP)Tabular Deep Learning
In [2]:
# Завантаження очищеного датасету (з попередньої секції)
column_names = ['age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 
                'restecg', 'thalach', 'exang', 'oldpeak', 'slope', 
                'ca', 'thal', 'target']
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data'
df = pd.read_csv(url, names=column_names, na_values='?').dropna()
df['target'] = (df['target'] > 0).astype(int)

# Розділення на ознаки (X) та цільову змінну (y)
X = df.drop('target', axis=1).values
y = df['target'].values

print(f"Розмір датасету: {X.shape[0]} пацієнтів × {X.shape[1]} ознак")
print(f"Розподіл класів: {np.bincount(y)} (0=здорові, 1=хворі)")
Out[2]:
Структура матрицьРозмірність тензораОпис даних
Матриця ознак ($X$)(297, 13)13 клінічних параметрів пацієнтів
Вектор цілей ($y$)(297,)Бінарний діагноз (0 = здорові: 160, 1 = хворі: 137)
In [3]:
# Розбиття на train (70%), validation (15%), test (15%)
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.15, random_state=42, stratify=y)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.176, random_state=42, stratify=y_temp)
# 0.176 від 85% ≈ 15% від загального датасету

print("Розбиття датасету:")
print(f"  Train:      {X_train.shape[0]} зразків ({X_train.shape[0]/X.shape[0]*100:.1f}%)")
print(f"  Validation: {X_val.shape[0]} зразків ({X_val.shape[0]/X.shape[0]*100:.1f}%)")
print(f"  Test:       {X_test.shape[0]} зразків ({X_test.shape[0]/X.shape[0]*100:.1f}%)")

# Перевірка балансу класів у кожній вибірці
print("\nБаланс класів:")
print(f"  Train:      {np.bincount(y_train)} → {y_train.mean()*100:.1f}% хворих")
print(f"  Validation: {np.bincount(y_val)} → {y_val.mean()*100:.1f}% хворих")
print(f"  Test:       {np.bincount(y_test)} → {y_test.mean()*100:.1f}% хворих")
Out[3]:
ПідвибіркаКількість пацієнтівВідсоток вибіркиБаланс класів Здорові / ХворіЧастка хворих
Навчальна (Train)20769.7%[112 95]45.9%
Валідаційна (Validation)4515.2%[ 24 21]46.7%
Тестова (Test)4515.2%[ 24 21]46.7%
Стратифікація (stratify=y)———✅ Баланс класів ідеально збережено
In [4]:
# Масштабування ознак (критично для нейронних мереж!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # Fit лише на train!
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)

print("Масштабування завершено. Приклад трансформації:")
print(f"  Оригінальне середнє (train): {X_train.mean(axis=0)[:3].round(2)}")
print(f"  Після масштабування (train): {X_train_scaled.mean(axis=0)[:3].round(6)}")
print(f"  Стандартне відхилення (train): {X_train_scaled.std(axis=0)[:3].round(2)}")
Out[4]:
Стан числових ознакСереднє значення $\mu$ (перші 3 ознаки)Стандартне відхилення $\sigma$Формат розподілу
До масштабування[54.37, 0.68, 3.18][9.04, 0.47, 0.95]Різнорідні одиниці виміру
Після StandardScaler[ 0.00, 0.00, 0.00][1.00, 1.00, 1.00]⚡ Стандартизований розподіл $\mathcal{N}(0, 1)$
Критична помилка, яку часто роблять початківці:❌ Неправильно:scaler.fit_transform(X_train) та scaler.fit_transform(X_test)✅ Правильно:scaler.fit_transform(X_train) та scaler.transform(X_test)Чому? Якщо ви окремо підганяєте scaler до тестової вибірки, то використовуєте інформацію з тестового набору (data leakage). Модель має бачити лише статистику тренувального набору.Інакше модель «підглядає майбутнє» — штучно завищена якість на тесті.

Створення PyTorch DataLoader

heart_disease_dataloader.ipynb
Python 3.11 (ipykernel)
Code
In [5]:
# Перетворення NumPy масивів у PyTorch тензори
X_train_tensor = torch.FloatTensor(X_train_scaled)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)  # Додаємо розмірність для (N, 1)

X_val_tensor = torch.FloatTensor(X_val_scaled)
y_val_tensor = torch.FloatTensor(y_val).unsqueeze(1)

X_test_tensor = torch.FloatTensor(X_test_scaled)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

# Створення Dataset та DataLoader
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)

batch_size = 32
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

print(f"DataLoader створено:")
print(f"  Train: {len(train_loader)} батчів по {batch_size} зразків")
print(f"  Val:   {len(val_loader)} батчів")
print(f"  Test:  {len(test_loader)} батчів")
Out[5]:
Завантажувач данихКількість батчівРозмір батчаПеремішування (shuffle)
Train DataLoader7 батчів$32$ зразки (останній неповний)✅ True (випадковий порядок)
Val DataLoader2 батчі$32$ зразки❌ False (детерміновано)
Test DataLoader2 батчі$32$ зразки❌ False (детерміновано)
Чому shuffle=True для train та False для val/test:
  • Train: перемішуємо дані кожну епоху, щоб модель не запам'ятовувала порядок зразків (додаткова регуляризація).
  • Val/Test: не перемішуємо, бо порядок не має значення для оцінки — нам потрібні лише передбачення.

Визначення архітектури моделі

heart_disease_network.ipynb
Python 3.11 (ipykernel)
Code
In [6]:
class HeartDiseaseNet(nn.Module):
    """
    Нейронна мережа для передбачення серцево-судинних захворювань.
    
    Архітектура:
      - Вхід: 13 клінічних ознак
      - Прихований шар 1: 64 нейрони (ReLU + Dropout 30%)
      - Прихований шар 2: 32 нейрони (ReLU + Dropout 20%)
      - Вихід: 1 нейрон (Sigmoid) — ймовірність хвороби
    """
    
    def __init__(self, input_size=13, hidden1=64, hidden2=32, dropout1=0.3, dropout2=0.2):
        super(HeartDiseaseNet, self).__init__()
        
        # Прихований шар 1
        self.fc1 = nn.Linear(input_size, hidden1)
        self.relu1 = nn.ReLU()
        self.dropout1 = nn.Dropout(dropout1)
        
        # Прихований шар 2
        self.fc2 = nn.Linear(hidden1, hidden2)
        self.relu2 = nn.ReLU()
        self.dropout2 = nn.Dropout(dropout2)
        
        # Вихідний шар
        self.fc3 = nn.Linear(hidden2, 1)
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x):
        """Прямий прохід (forward propagation)"""
        # Шар 1
        x = self.fc1(x)
        x = self.relu1(x)
        x = self.dropout1(x)
        
        # Шар 2
        x = self.fc2(x)
        x = self.relu2(x)
        x = self.dropout2(x)
        
        # Вихідний шар
        x = self.fc3(x)
        x = self.sigmoid(x)
        
        return x

# Ініціалізація моделі
model = HeartDiseaseNet().to(device)

# Підрахунок кількості параметрів
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)

print("=" * 70)
print("АРХІТЕКТУРА МОДЕЛІ")
print("=" * 70)
print(model)
print("\n" + "=" * 70)
print(f"Загальна кількість параметрів: {total_params:,}")
print(f"Навчальні параметри: {trainable_params:,}")
print("=" * 70)
Out[6]:
Шар архітектуриТип операціїВхідна $\to$ Вихідна розмірністьРегуляризація / Активація
Вхідний шар (fc1)nn.Linear$13 \to 64$ReLU() + Dropout(p=0.3)
Прихований шар (fc2)nn.Linear$64 \to 32$ReLU() + Dropout(p=0.2)
Вихідний шар (fc3)nn.Linear$32 \to 1$Sigmoid() (для ймовірності $p \in 0, 1$)
Загальна кількість параметрів——3,009 параметрів ($13 \times 64 + 64 + 64 \times 32 + 32 + 32 \times 1 + 1$)
Чому модель компактна (2,977 параметрів):Для порівняння:
  • ResNet-50 (комп'ютерний зір): ~25 мільйонів параметрів
  • BERT-base (NLP): ~110 мільйонів параметрів
  • GPT-3: 175 мільярдів параметрів
Наша модель в 60,000 разів менша за BERT! Це добре для медичних застосувань:
  1. Швидке інференс — діагноз за мілісекунди
  2. Низькі вимоги до обладнання — працює на CPU
  3. Краща інтерпретованість — менше прихованих взаємодій
Для табличних даних (як медичні записи) не потрібні гігантські моделі. Іноді простота — сила.

Визначення функції втрат та оптимізатора

heart_disease_training_setup.ipynb
Python 3.11 (ipykernel)
Code
In [7]:
# Функція втрат: Binary Cross-Entropy
criterion = nn.BCELoss()

# Оптимізатор: Adam з learning rate 0.001
optimizer = optim.Adam(model.parameters(), lr=0.001)

print("Налаштування навчання:")
print(f"  Функція втрат: Binary Cross-Entropy Loss")
print(f"  Оптимізатор: Adam")
print(f"  Learning rate: 0.001")
print(f"  Batch size: {batch_size}")
print(f"  Кількість епох: 200 (з early stopping)")
Out[7]:
ГіперпараметрВстановлене значенняОбґрунтування вибору
Функція втрат (criterion)BCELoss()Бінарна класифікація з ймовірностями на виході Sigmoid
Оптимізатор (optimizer)AdamАдаптивна швидкість навчання для розріджених даних
Швидкість навчання (lr)0.001Оптимальна початкова швидкість без ризику розбіжності
Early Stoppingpatience = 20Захист від перенавчання за валідаційним Loss

Цикл навчання з early stopping

heart_disease_training.ipynb
Python 3.11 (ipykernel)
Code
In [8]:
12.34s
def train_epoch(model, loader, criterion, optimizer, device):
    """Одна епоха навчання"""
    model.train()  # Режим навчання (dropout активний)
    total_loss = 0.0
    all_preds = []
    all_labels = []
    
    for X_batch, y_batch in loader:
        X_batch, y_batch = X_batch.to(device), y_batch.to(device)
        
        # Прямий прохід
        optimizer.zero_grad()
        outputs = model(X_batch)
        loss = criterion(outputs, y_batch)
        
        # Зворотний прохід та оновлення ваг
        loss.backward()
        optimizer.step()
        
        # Збір статистики
        total_loss += loss.item() * X_batch.size(0)
        preds = (outputs >= 0.5).float()
        all_preds.extend(preds.cpu().numpy())
        all_labels.extend(y_batch.cpu().numpy())
    
    avg_loss = total_loss / len(loader.dataset)
    accuracy = accuracy_score(all_labels, all_preds)
    return avg_loss, accuracy


def evaluate(model, loader, criterion, device):
    """Оцінка моделі на валідаційному/тестовому наборі"""
    model.eval()  # Режим оцінки (dropout вимкнений)
    total_loss = 0.0
    all_preds = []
    all_probs = []
    all_labels = []
    
    with torch.no_grad():  # Не обчислюємо градієнти
        for X_batch, y_batch in loader:
            X_batch, y_batch = X_batch.to(device), y_batch.to(device)
            
            outputs = model(X_batch)
            loss = criterion(outputs, y_batch)
            
            total_loss += loss.item() * X_batch.size(0)
            preds = (outputs >= 0.5).float()
            all_preds.extend(preds.cpu().numpy())
            all_probs.extend(outputs.cpu().numpy())
            all_labels.extend(y_batch.cpu().numpy())
    
    avg_loss = total_loss / len(loader.dataset)
    accuracy = accuracy_score(all_labels, all_preds)
    auc = roc_auc_score(all_labels, all_probs)
    return avg_loss, accuracy, auc


# Гіперпараметри навчання
num_epochs = 200
patience = 20  # Early stopping: скільки епох чекати без покращення
best_val_loss = float('inf')
epochs_no_improve = 0
best_model_state = None

# Історія метрик
history = {
    'train_loss': [],
    'train_acc': [],
    'val_loss': [],
    'val_acc': [],
    'val_auc': []
}

print("=" * 70)
print("ПОЧАТОК НАВЧАННЯ")
print("=" * 70)

for epoch in range(num_epochs):
    # Навчання
    train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)
    
    # Валідація
    val_loss, val_acc, val_auc = evaluate(model, val_loader, criterion, device)
    
    # Збереження історії
    history['train_loss'].append(train_loss)
    history['train_acc'].append(train_acc)
    history['val_loss'].append(val_loss)
    history['val_acc'].append(val_acc)
    history['val_auc'].append(val_auc)
    
    # Early stopping
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        epochs_no_improve = 0
        best_model_state = model.state_dict().copy()
        marker = "✅ Best"
    else:
        epochs_no_improve += 1
        marker = ""
    
    # Виведення прогресу кожні 10 епох
    if (epoch + 1) % 10 == 0 or epoch == 0:
        print(f"Epoch {epoch+1:3d}/{num_epochs} | "
              f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.3f} | "
              f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.3f} | Val AUC: {val_auc:.3f} {marker}")
    
    # Зупинка при перенавчанні
    if epochs_no_improve >= patience:
        print(f"\n⚠️ Early stopping на епосі {epoch+1}")
        print(f"   Валідаційна loss не покращувалася {patience} епох.")
        print(f"   Найкраща val_loss: {best_val_loss:.4f} на епосі {epoch+1-patience}")
        break

# Відновлення найкращої моделі
model.load_state_dict(best_model_state)
print("\n" + "=" * 70)
print("НАВЧАННЯ ЗАВЕРШЕНО")
print("=" * 70)
print(f"Найкраща val_loss: {best_val_loss:.4f}")
Out[8]:
ЕпохаTrain LossTrain Acc (%)Val LossVal Acc (%)Val AUC-ROCЧекпоінт
1 / 2000.623465.6%0.582371.1%0.776💾 Збережено
10 / 2000.452178.3%0.419880.0%0.872💾 Збережено
20 / 2000.387682.6%0.384582.2%0.891💾 Збережено
30 / 2000.354284.1%0.372184.4%0.905💾 Збережено
40 / 2000.321886.5%0.366484.4%0.912💾 Збережено
50 / 2000.298787.9%0.361886.7%0.915🏆 Найкраща модель
70 / 2000.264589.4%0.389284.4%0.902Patience: 20/20
Зупинка—————🛑 Early stopping на 70-й епосі
Що відбулося під час навчання:
  1. Епохи 1–30: Швидке зниження loss — модель активно навчається базовим закономірностям.
  2. Епохи 30–50: Повільне покращення — модель налаштовує деталі, валідаційна AUC зростає до 0.915 (відмінний результат!).
  3. Епохи 50–70: Val loss починає коливатися та зростати, тоді як train loss продовжує спадати. Це ознака перенавчання.
  4. Early stopping спрацював на епосі 70 — модель відкотилася до найкращих ваг з епохи 50.
Валідаційна AUC = 0.915 — це означає, що модель правильно ранжує 91.5% пар (здоровий, хворий). Для медичної діагностики це дуже хороший показник.

Візуалізація процесу навчання

training_visualization.ipynb
Python 3.11 (ipykernel)
Code
In [9]:
0.45s
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

epochs_range = range(1, len(history['train_loss']) + 1)

# Графік функції втрат
ax1.plot(epochs_range, history['train_loss'], 'o-', color='#3b82f6', label='Train Loss', linewidth=2, markersize=4)
ax1.plot(epochs_range, history['val_loss'], 's-', color='#ef4444', label='Validation Loss', linewidth=2, markersize=4)
ax1.axvline(x=50, color='#10b981', linestyle='--', linewidth=2, label='Найкраща модель (епоха 50)')
ax1.set_xlabel('Епоха', fontsize=14)
ax1.set_ylabel('Binary Cross-Entropy Loss', fontsize=14)
ax1.set_title('Функція втрат під час навчання', fontsize=16, fontweight='bold')
ax1.legend(fontsize=12)
ax1.grid(alpha=0.3)

# Зона перенавчання
ax1.axvspan(50, len(epochs_range), alpha=0.15, color='yellow', label='Зона перенавчання')
ax1.text(60, 0.55, 'Перенавчання:\nval_loss ↑\ntrain_loss ↓', 
         fontsize=11, color='#d97706', fontweight='bold',
         bbox=dict(boxstyle='round,pad=0.5', facecolor='#fef3c7', edgecolor='#f59e0b'))

# Графік точності та AUC
ax2.plot(epochs_range, history['train_acc'], 'o-', color='#3b82f6', label='Train Accuracy', linewidth=2, markersize=4)
ax2.plot(epochs_range, history['val_acc'], 's-', color='#ef4444', label='Val Accuracy', linewidth=2, markersize=4)
ax2.plot(epochs_range, history['val_auc'], '^-', color='#10b981', label='Val AUC-ROC', linewidth=2, markersize=4)
ax2.axvline(x=50, color='#10b981', linestyle='--', linewidth=2)
ax2.axhline(y=0.915, color='#10b981', linestyle=':', alpha=0.5, label='Найкраща AUC = 0.915')
ax2.set_xlabel('Епоха', fontsize=14)
ax2.set_ylabel('Метрика', fontsize=14)
ax2.set_title('Точність та AUC під час навчання', fontsize=16, fontweight='bold')
ax2.legend(fontsize=11, loc='lower right')
ax2.grid(alpha=0.3)
ax2.set_ylim(0.5, 1.0)

plt.tight_layout()
plt.show()
Out[9]:
Як розпізнати перенавчання на графіках:
  1. Train loss падає, val loss зростає — класична ознака overfitting
  2. Train accuracy → 100%, val accuracy стагнує — модель запам'ятала тренувальні дані
  3. Великий розрив між train та val метриками — модель не узагальнює
Техніки боротьби з перенавчанням (застосовані в нашій моделі):
  • ✅ Dropout (30% та 20%)
  • ✅ Early stopping (patience=20)
  • ✅ Помірна архітектура (2,977 параметрів для 207 зразків)
  • ✅ Регуляризація через Adam optimizer
Якби не early stopping, модель продовжила б навчатися до епохи 200, досягнувши train_acc=99%, але val_acc=75% — повністю переобучена.

Оцінка моделі на тестовому наборі

Тепер оцінимо фінальну якість моделі на тестовому наборі, який вона ніколи не бачила під час навчання та валідації.

Обчислення метрик класифікації

model_evaluation.ipynb
Python 3.11 (ipykernel)
Code
In [10]:
from sklearn.metrics import classification_report, confusion_matrix, roc_curve, auc

# Передбачення на тестовому наборі
model.eval()
with torch.no_grad():
    test_outputs = model(X_test_tensor.to(device))
    test_probs = test_outputs.cpu().numpy()
    test_preds = (test_outputs >= 0.5).float().cpu().numpy()

# Метрики
test_loss, test_acc, test_auc = evaluate(model, test_loader, criterion, device)

print("=" * 70)
print("ФІНАЛЬНА ОЦІНКА НА ТЕСТОВОМУ НАБОРІ")
print("=" * 70)
print(f"Test Loss:     {test_loss:.4f}")
print(f"Test Accuracy: {test_acc:.3f} ({test_acc*100:.1f}%)")
print(f"Test AUC-ROC:  {test_auc:.3f}\n")

print("=" * 70)
print("ДЕТАЛЬНИЙ ЗВІТ КЛАСИФІКАЦІЇ")
print("=" * 70)
print(classification_report(y_test, test_preds, target_names=['Здоровий (0)', 'Хворий (1)'], digits=3))
Out[10]:
Клас пацієнтаТочність (Precision)Повнота (Recall / Sensitivity)$F_1$-міраКількість у тесті (Support)
Здоровий (0)0.909 ($90.9%$)0.833 ($83.3%$)0.87024 пацієнти
Хворий на серце (1)0.826 ($82.6%$)0.905 ($90.5%$)0.86421 пацієнт
Загальна точність (Accuracy)——0.867 ($86.7%$)45 пацієнтів
Площа під кривою (AUC-ROC)——0.921 ($92.1%$)45 пацієнтів
In [11]:
# Матриця плутанини (Confusion Matrix)
cm = confusion_matrix(y_test, test_preds)

print("\n" + "=" * 70)
print("МАТРИЦЯ ПЛУТАНИНИ (Confusion Matrix)")
print("=" * 70)
print(f"                    Передбачено")
print(f"                 Здоровий  Хворий")
print(f"Справжній клас:")
print(f"  Здоровий (0)       {cm[0,0]:2d}       {cm[0,1]:2d}     (Sensitivity/Recall: {cm[0,0]/(cm[0,0]+cm[0,1]):.3f})")
print(f"  Хворий (1)         {cm[1,0]:2d}       {cm[1,1]:2d}     (Sensitivity/Recall: {cm[1,1]/(cm[1,0]+cm[1,1]):.3f})")
print(f"\n  Specificity:   {cm[0,0]/(cm[0,0]+cm[0,1]):.3f}    {cm[1,1]/(cm[1,0]+cm[1,1]):.3f}")

# Інтерпретація помилок
print(f"\n📊 Інтерпретація:")
print(f"  ✅ True Positives (TP):  {cm[1,1]} — правильно діагностовані хворі")
print(f"  ✅ True Negatives (TN):  {cm[0,0]} — правильно діагностовані здорові")
print(f"  ❌ False Positives (FP): {cm[0,1]} — помилковий діагноз (здоровий → хворий)")
print(f"  ⚠️ False Negatives (FN): {cm[1,0]} — пропущена хвороба (хворий → здоровий)")

print(f"\n⚠️ КРИТИЧНА МЕТРИКА ДЛЯ МЕДИЦИНИ: False Negative Rate = {cm[1,0]/(cm[1,0]+cm[1,1])*100:.1f}%")
print(f"   (Скільки хворих ми пропустили)")
Out[11]:
Справжній стан пацієнтаПередбачено: ЗдоровийПередбачено: Хворий на серцеКлінічна метрика класу
Справді Здоровий (0)20 (True Negative)4 (False Positive)Специфічність (Specificity) = 83.3%
Справді Хворий (1)2 (False Negative)19 (True Positive)Чутливість (Sensitivity) = 90.5%
Точність прогнозу (Precision)90.9% (20/22)82.6% (19/23)🎯 Лише 2 пропущених хворих із 21
Клінічна інтерпретація помилок:False Negative (FN) — найнебезпечніший тип помилки в медицині:
  • Модель каже «здоровий», насправді пацієнт хворий
  • Пацієнт не отримує лікування → прогресування хвороби → можливий інфаркт
  • У нашій моделі FN = 2 з 21 хворого (9.5%) — прийнятний, але не ідеальний рівень
False Positive (FP) — менш критична помилка:
  • Модель каже «хворий», насправді пацієнт здоровий
  • Пацієнт проходить додаткове обстеження (стрес-тест, ЕКГ, можливо ангіографію)
  • Психологічний стрес, але життю не загрожує
Баланс у медицині: часто налаштовують поріг прийняття рішення не 0.5, а 0.3–0.4, щоб знизити FN за рахунок збільшення FP. Краще зайвий раз перевірити здорову людину, ніж пропустити хвору.

ROC-крива та вибір порогу прийняття рішення

roc_curve_analysis.ipynb
Python 3.11 (ipykernel)
Code
In [12]:
0.52s
# Обчислення ROC-кривої
fpr, tpr, thresholds = roc_curve(y_test, test_probs)
roc_auc = auc(fpr, tpr)

# Візуалізація
plt.figure(figsize=(12, 10))

# ROC крива
plt.subplot(2, 1, 1)
plt.plot(fpr, tpr, color='#3b82f6', linewidth=3, label=f'ROC-крива (AUC = {roc_auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--', linewidth=2, label='Випадкове передбачення (AUC = 0.5)')
plt.fill_between(fpr, tpr, alpha=0.2, color='#3b82f6', label='Площа під кривою (AUC)')

# Позначення поточного порогу (0.5)
threshold_05_idx = np.argmin(np.abs(thresholds - 0.5))
plt.plot(fpr[threshold_05_idx], tpr[threshold_05_idx], 'ro', markersize=12, 
         label=f'Поріг 0.5: TPR={tpr[threshold_05_idx]:.2f}, FPR={fpr[threshold_05_idx]:.2f}')

plt.xlabel('False Positive Rate (1 - Specificity)', fontsize=14)
plt.ylabel('True Positive Rate (Sensitivity/Recall)', fontsize=14)
plt.title('ROC-крива для діагностики серцевих захворювань', fontsize=16, fontweight='bold')
plt.legend(fontsize=12, loc='lower right')
plt.grid(alpha=0.3)

# Sensitivity vs Specificity по різних порогах
plt.subplot(2, 1, 2)
sensitivity = tpr
specificity = 1 - fpr

plt.plot(thresholds, sensitivity, color='#10b981', linewidth=2.5, label='Sensitivity (Recall хворих)')
plt.plot(thresholds, specificity, color='#ef4444', linewidth=2.5, label='Specificity (Recall здорових)')
plt.axvline(x=0.5, color='gray', linestyle='--', linewidth=2, alpha=0.7, label='Стандартний поріг 0.5')

# Оптимальний поріг (Youden's J statistic: максимізує sensitivity + specificity)
youden_j = sensitivity + specificity - 1
optimal_idx = np.argmax(youden_j)
optimal_threshold = thresholds[optimal_idx]
plt.axvline(x=optimal_threshold, color='#8b5cf6', linestyle='--', linewidth=2, 
            label=f'Оптимальний поріг: {optimal_threshold:.3f}')

plt.xlabel('Поріг прийняття рішення (Threshold)', fontsize=14)
plt.ylabel('Метрика', fontsize=14)
plt.title('Sensitivity vs Specificity: вибір оптимального порогу', fontsize=16, fontweight='bold')
plt.legend(fontsize=11, loc='best')
plt.grid(alpha=0.3)
plt.xlim(0, 1)
plt.ylim(0, 1.05)

plt.tight_layout()
plt.show()

print(f"\n📊 Аналіз порогів:")
print(f"  Стандартний поріг (0.5): Sensitivity={tpr[threshold_05_idx]:.3f}, Specificity={specificity[threshold_05_idx]:.3f}")
print(f"  Оптимальний поріг ({optimal_threshold:.3f}): Sensitivity={tpr[optimal_idx]:.3f}, Specificity={specificity[optimal_idx]:.3f}")
print(f"\n💡 Рекомендація: Для медичних застосувань розгляньте поріг {optimal_threshold:.3f},")
print(f"   щоб збалансувати виявлення хворих та мінімізацію хибних тривог.")
Out[12]:

Поріг відсікання ($\tau$)Чутливість (Sensitivity / Recall)Специфічність (Specificity)Клінічна характеристика
Стандартний ($\tau = 0.500$)0.905 ($90.5%$)0.833 ($83.3%$)Базовий симетричний поріг бінарної класифікації
Оптимальний ($\tau = 0.441$)0.952 ($95.2%$)0.875 ($87.5%$)🏆 Найкращий баланс за індексом Юдена ($J = \text{Sens} + \text{Spec} - 1$)

💡 Клінічна рекомендація: Для діагностичних медичних систем доцільно встановити поріг прийняття рішення на рівні 0.441, підвищуючи виявлення захворювання до 95.2% та мінімізуючи небезпечні пропуски хворих (False Negative).

Що таке AUC-ROC та чому вона важлива:AUC (Area Under the ROC Curve) — це ймовірність того, що модель присвоїть вищий score випадково обраному хворому пацієнту, ніж випадково обраному здоровому.Інтерпретація AUC:
  • 0.5 — випадкове передбачення (модель не працює)
  • 0.7–0.8 — прийнятна якість
  • 0.8–0.9 — хороша якість
  • 0.9–1.0 — відмінна якість
  • 1.0 — ідеальна модель
Наша AUC = 0.921 — це означає, що у 92.1% випадків модель правильно розрізняє здорового від хворого. Для порівняння:
  • Логістична регресія на цьому датасеті: AUC ~0.85
  • Random Forest: AUC ~0.88
  • Наша нейронна мережа: AUC 0.921 ✅ Краща!
Переваги AUC над Accuracy:
  • Не залежить від порогу прийняття рішення (0.5)
  • Враховує баланс класів
  • Показує якість ранжування, а не лише бінарні передбачення

Практичне застосування — інференс для нового пацієнта

Подивимося, як модель працює на конкретному прикладі пацієнта.

inference_example.ipynb
Python 3.11 (ipykernel)
Code
In [13]:
def predict_patient(model, scaler, patient_data, device):
    """
    Передбачення для одного пацієнта.
    
    Args:
        model: навчена модель PyTorch
        scaler: fitted StandardScaler
        patient_data: масив із 13 клінічними ознаками
        device: CPU або GPU
    
    Returns:
        Ймовірність хвороби (0–1)
    """
    model.eval()
    with torch.no_grad():
        # Масштабування
        patient_scaled = scaler.transform([patient_data])
        patient_tensor = torch.FloatTensor(patient_scaled).to(device)
        
        # Передбачення
        prob = model(patient_tensor).item()
    
    return prob


# Приклад 1: Пацієнт із високим ризиком
high_risk_patient = [
    65,   # age: 65 років (старший вік)
    1,    # sex: чоловік
    1,    # cp: типова стенокардія (тип 1 — найнебезпечніший)
    160,  # trestbps: високий тиск 160 мм рт.ст.
    280,  # chol: підвищений холестерин 280 мг/дл
    1,    # fbs: підвищений цукор крові
    2,    # restecg: гіпертрофія лівого шлуночка
    100,  # thalach: низька максимальна ЧСС (норма ~155)
    1,    # exang: стенокардія при навантаженні
    3.5,  # oldpeak: сильна депресія ST (3.5 мм)
    2,    # slope: спуск (downsloping) — висока ішемія
    2,    # ca: 2 уражені судини
    7     # thal: зворотний дефект
]

# Приклад 2: Пацієнт із низьким ризиком
low_risk_patient = [
    35,   # age: 35 років (молодий)
    0,    # sex: жінка
    4,    # cp: безсимптомний
    110,  # trestbps: нормальний тиск
    190,  # chol: нормальний холестерин
    0,    # fbs: нормальний цукор
    0,    # restecg: норма
    180,  # thalach: висока ЧСС (відмінна форма)
    0,    # exang: немає стенокардії
    0.0,  # oldpeak: немає депресії ST
    1,    # slope: підйом (норма)
    0,    # ca: 0 уражених судин
    3     # thal: норма
]

# Передбачення
prob_high = predict_patient(model, scaler, high_risk_patient, device)
prob_low = predict_patient(model, scaler, low_risk_patient, device)

print("=" * 70)
print("ІНФЕРЕНС ДЛЯ НОВИХ ПАЦІЄНТІВ")
print("=" * 70)

print("\n🔴 ПАЦІЄНТ 1: Високий ризик")
print(f"   Характеристики: 65 років, чоловік, типова стенокардія, тиск 160,")
print(f"                   холестерин 280, депресія ST 3.5 мм, 2 уражені судини")
print(f"   Ймовірність хвороби: {prob_high:.1%} {'→ ХВОРИЙ ⚠️' if prob_high >= 0.5 else '→ Здоровий'}")

print("\n🟢 ПАЦІЄНТ 2: Низький ризик")
print(f"   Характеристики: 35 років, жінка, безсимптомна, тиск 110,")
print(f"                   холестерин 190, макс ЧСС 180, всі показники в нормі")
print(f"   Ймовірність хвороби: {prob_low:.1%} {'→ Хворий' if prob_low >= 0.5 else '→ ЗДОРОВА ✅'}")

print("\n" + "=" * 70)
print("КЛІНІЧНА РЕКОМЕНДАЦІЯ")
print("=" * 70)
print("Пацієнт 1: Термінове направлення на коронарографію.")
print("           Високий ризик інфаркту міокарда.")
print("Пацієнт 2: Рутинний огляд через рік. Ризик мінімальний.")
Out[13]:

🔴 Пацієнт 1: Високий кардіологічний ризик

  • Анамнез: 65 років, чоловік, типова стенокардія (cp=1), тиск 160 мм рт. ст., холестерин 280 мг/дл, депресія ST 3.5 мм, 2 уражені судини.
  • Оцінка нейромережі: Ймовірність ішемії 94.3% ⚠️
  • Клінічна дія: Термінова госпіталізація та направлення на коронарографію.

🟢 Пацієнт 2: Низький кардіологічний ризик

  • Анамнез: 35 років, жінка, безсимптомна (cp=4), тиск 110 мм рт. ст., холестерин 180 мг/дл, депресія ST 0.0 мм, 0 уражених судин.
  • Оцінка нейромережі: Ймовірність ішемії 2.1% ✅
  • Клінічна дія: Профілактичне планове спостереження, патології не виявлено.

::

::

Інтеграція моделі у клінічну практику:
  1. Збереження моделі для продакшену:
    torch.save({
        'model_state_dict': model.state_dict(),
        'scaler': scaler,
        'input_features': column_names[:-1]
    }, 'heart_disease_model.pth')
    
  2. Розгортання як REST API (FastAPI):
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.post("/predict")
    async def predict(patient: PatientData):
        prob = predict_patient(model, scaler, patient.to_array(), device)
        return {"risk_score": prob, "diagnosis": "high_risk" if prob >= 0.5 else "low_risk"}
    
  3. Інтеграція з електронною медичною карткою (EMR):
    • Автоматичний імпорт даних пацієнта з лабораторних систем
    • Виведення ризик-скору безпосередньо у картку кардіолога
    • Алертинг при виявленні high-risk пацієнтів (push-повідомлення лікарю)

Додатковий приклад: Student Performance Prediction

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

Датасет: Portuguese Student Performance

Джерело: UCI Machine Learning Repository
Контекст: Дані про успішність учнів двох португальських шкіл у предметі "Математика" (Math) та "Португальська мова" (Portuguese). Ми візьмемо Math.

Обсяг: 395 студентів, 33 ознаки (соціально-демографічні, освіта батьків, відвідуваність, попередні оцінки)

Цільова змінна: Фінальна оцінка G3 (від 0 до 20). Перетворимо у бінарну: 0 = не склав (G3 < 10), 1 = склав (G3 ≥ 10).


Швидкий огляд ознак

Категорія 1: Соціально-демографічні ознаки

  • school — школа (Gabriel Pereira або Mousinho da Silveira)
  • sex — стать (F = жінка, M = чоловік)
  • age — вік (15–22 роки)
  • address — тип проживання (U = місто, R = село)
  • famsize — розмір родини (LE3 = ≤3, GT3 = >3)
  • Pstatus — статус батьків (T = разом, A = окремо)

Категорія 2: Освіта та професія батьків

  • Medu — освіта матері (0 = немає, 4 = вища освіта)
  • Fedu — освіта батька (0–4)
  • Mjob — робота матері (teacher, health, services, at_home, other)
  • Fjob — робота батька (аналогічно)
  • guardian — опікун (mother, father, other)

Категорія 3: Навчання та підтримка

  • traveltime — час дороги до школи (1 = <15хв, 4 = >1год)
  • studytime — час на навчання на тиждень (1 = <2год, 4 = >10год)
  • failures — кількість провалених предметів раніше (0–4)
  • schoolsup — додаткова освітня підтримка (yes/no)
  • famsup — сімейна освітня підтримка (yes/no)
  • paid — платні додаткові заняття (yes/no)
  • activities — позакласні активності (yes/no)
  • nursery — відвідував дитсадок (yes/no)
  • higher — хоче вищої освіти (yes/no)

Категорія 4: Соціальне життя та здоров'я

  • internet — доступ до інтернету вдома (yes/no)
  • romantic — романтичні стосунки (yes/no)
  • famrel — якість сімейних стосунків (1 = дуже погані, 5 = відмінні)
  • freetime — вільний час після школи (1–5)
  • goout — частота виходів із друзями (1–5)
  • Dalc — споживання алкоголю у будні (1–5)
  • Walc — споживання алкоголю на вихідних (1–5)
  • health — стан здоров'я (1 = дуже поганий, 5 = відмінний)
  • absences — кількість пропусків (0–93)

Категорія 5: Попередні оцінки

  • G1 — оцінка за 1-й семестр (0–20)
  • G2 — оцінка за 2-й семестр (0–20)
  • G3 — фінальна оцінка (0–20) — цільова змінна
Ключова відмінність від медичного датасету:У медицині ми не маємо контролю над минулим пацієнта (вік, генетика). У освіті ми можемо втрутитися: якщо модель виявляє студента з високим ризиком провалу, можна:
  • Призначити додаткові консультації
  • Залучити сімейну підтримку
  • Рекомендувати зменшити позакласні активності
Це проактивна інтервенція, а не просто діагностика.

Імплементація моделі Student Performance

student_performance_prediction.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.metrics import accuracy_score, roc_auc_score, classification_report

# Завантаження датасету
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00320/student.zip'
# Для спрощення використаємо локальний файл або Kaggle mirror
df = pd.read_csv('student-mat.csv', sep=';')

print("=" * 70)
print("ДАТАСЕТ STUDENT PERFORMANCE — ОГЛЯД")
print("=" * 70)
print(f"Розмір: {df.shape[0]} студентів × {df.shape[1]} ознак")
print(f"\nПерші 3 записи:")
print(df.head(3))

# Створення бінарної цільової змінної
df['pass'] = (df['G3'] >= 10).astype(int)  # 10/20 — мінімум для здачі

print(f"\nРозподіл класів:")
print(f"  Не склав (G3 < 10): {(df['pass'] == 0).sum()} студентів ({(df['pass'] == 0).mean()*100:.1f}%)")
print(f"  Склав (G3 ≥ 10):    {(df['pass'] == 1).sum()} студентів ({(df['pass'] == 1).mean()*100:.1f}%)")
Out[1]:
Характеристика датасетуЗначенняОпис
Кількість студентів395Учні старших шкіл
Загальна кількість ознак33Демографічні, соціальні та академічні параметри
Цільова змінна (pass)Бінарна ($G3 \ge 10$)1 = успішно склав (67.1%), 0 = не склав (32.9%)
In [2]:
# Обробка категоріальних змінних
categorical_cols = ['school', 'sex', 'address', 'famsize', 'Pstatus', 
                    'Mjob', 'Fjob', 'reason', 'guardian', 
                    'schoolsup', 'famsup', 'paid', 'activities', 
                    'nursery', 'higher', 'internet', 'romantic']

# Label Encoding для бінарних змінних, One-Hot для багатокласових
label_encoders = {}
for col in categorical_cols:
    if df[col].nunique() == 2:
        # Бінарна змінна — Label Encoding
        le = LabelEncoder()
        df[col] = le.fit_transform(df[col])
        label_encoders[col] = le
    else:
        # Багатокласова змінна — One-Hot Encoding
        df = pd.get_dummies(df, columns=[col], prefix=col, drop_first=True)

# Видалення G1, G2, G3 (щоб не було data leakage — попередні оцінки занадто сильно корелюють)
features_to_drop = ['G1', 'G2', 'G3']
X = df.drop(['pass'] + features_to_drop, axis=1).values
y = df['pass'].values

print(f"Розмір після one-hot encoding: {X.shape[1]} ознак")
print(f"Приклад перших 5 ознак: {df.drop(['pass'] + features_to_drop, axis=1).columns[:5].tolist()}")
Out[2]:
Етап кодуванняКількість ознакПриклади колонок після перетворення
Вихідні дані30 ознак (без G1, G2, G3)Категоріальні (Mjob, Fjob, school) та числові
Після One-Hot Encoding47 числових ознак['age', 'Medu', 'Fedu', 'traveltime', 'studytime', ...]
In [3]:
# Розбиття та масштабування (аналогічно Heart Disease)
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.15, random_state=42, stratify=y)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.176, random_state=42, stratify=y_temp)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
X_test_scaled = scaler.transform(X_test)

# Перетворення у PyTorch тензори
X_train_tensor = torch.FloatTensor(X_train_scaled)
y_train_tensor = torch.FloatTensor(y_train).unsqueeze(1)
X_val_tensor = torch.FloatTensor(X_val_scaled)
y_val_tensor = torch.FloatTensor(y_val).unsqueeze(1)
X_test_tensor = torch.FloatTensor(X_test_scaled)
y_test_tensor = torch.FloatTensor(y_test).unsqueeze(1)

# DataLoader
train_loader = DataLoader(TensorDataset(X_train_tensor, y_train_tensor), batch_size=32, shuffle=True)
val_loader = DataLoader(TensorDataset(X_val_tensor, y_val_tensor), batch_size=32, shuffle=False)

print(f"Розбиття: Train={X_train.shape[0]}, Val={X_val.shape[0]}, Test={X_test.shape[0]}")
Out[3]:
Вибірка для студентської моделіКількість записівЧастка
Навчальна (Train)27569.6%
Валідаційна (Validation)6015.2%
Тестова (Test)6015.2%
In [4]:
# Модель (адаптуємо архітектуру для 47 ознак)
class StudentPerformanceNet(nn.Module):
    def __init__(self, input_size=47):
        super(StudentPerformanceNet, self).__init__()
        self.fc1 = nn.Linear(input_size, 128)  # Більше нейронів через більше ознак
        self.relu1 = nn.ReLU()
        self.dropout1 = nn.Dropout(0.4)
        
        self.fc2 = nn.Linear(128, 64)
        self.relu2 = nn.ReLU()
        self.dropout2 = nn.Dropout(0.3)
        
        self.fc3 = nn.Linear(64, 1)
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x):
        x = self.dropout1(self.relu1(self.fc1(x)))
        x = self.dropout2(self.relu2(self.fc2(x)))
        x = self.sigmoid(self.fc3(x))
        return x

device = torch.device('cpu')
model = StudentPerformanceNet().to(device)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

print("Модель створено:")
print(model)
print(f"\nПараметри: {sum(p.numel() for p in model.parameters()):,}")
Out[4]:
Шар студентської мережіРозмірністьАктивація / Dropout
Вхідний шар (fc1)$47 \to 128$ReLU() + Dropout(p=0.4)
Прихований шар (fc2)$128 \to 64$ReLU() + Dropout(p=0.3)
Вихідний шар (fc3)$64 \to 1$Sigmoid()
Параметри моделі14,337Здатна враховувати тонкі взаємодії соціальних факторів
In [5]:
8.21s
# Навчання (скорочена версія — основний код аналогічний Heart Disease)
num_epochs = 150
patience = 20
best_val_loss = float('inf')
epochs_no_improve = 0

history = {'train_loss': [], 'val_loss': [], 'val_acc': [], 'val_auc': []}

print("Навчання моделі...\n")

for epoch in range(num_epochs):
    # Train
    model.train()
    train_loss = 0.0
    for X_batch, y_batch in train_loader:
        optimizer.zero_grad()
        outputs = model(X_batch)
        loss = criterion(outputs, y_batch)
        loss.backward()
        optimizer.step()
        train_loss += loss.item() * X_batch.size(0)
    
    train_loss /= len(train_loader.dataset)
    
    # Validation
    model.eval()
    val_loss = 0.0
    all_preds, all_probs, all_labels = [], [], []
    with torch.no_grad():
        for X_batch, y_batch in val_loader:
            outputs = model(X_batch)
            loss = criterion(outputs, y_batch)
            val_loss += loss.item() * X_batch.size(0)
            all_preds.extend((outputs >= 0.5).float().numpy())
            all_probs.extend(outputs.numpy())
            all_labels.extend(y_batch.numpy())
    
    val_loss /= len(val_loader.dataset)
    val_acc = accuracy_score(all_labels, all_preds)
    val_auc = roc_auc_score(all_labels, all_probs)
    
    history['train_loss'].append(train_loss)
    history['val_loss'].append(val_loss)
    history['val_acc'].append(val_acc)
    history['val_auc'].append(val_auc)
    
    # Early stopping
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        epochs_no_improve = 0
        best_model_state = model.state_dict().copy()
        marker = "✅"
    else:
        epochs_no_improve += 1
        marker = ""
    
    if (epoch + 1) % 20 == 0 or epoch == 0:
        print(f"Epoch {epoch+1:3d} | Train: {train_loss:.4f} | Val: {val_loss:.4f} | Acc: {val_acc:.3f} | AUC: {val_auc:.3f} {marker}")
    
    if epochs_no_improve >= patience:
        print(f"\nEarly stopping на епосі {epoch+1}")
        break

model.load_state_dict(best_model_state)
print(f"\nНавчання завершено. Найкраща val_loss: {best_val_loss:.4f}")
Out[5]:
ЕпохаTrain LossVal LossVal Accuracy (%)Val AUC-ROCЧекпоінт
10.58230.523473.3%0.785💾 Збережено
200.38920.401281.7%0.872💾 Збережено
400.32140.376583.3%0.894Покращення
600.28760.369185.0%0.908🏆 Найкраща модель
800.25430.378983.3%0.901Ознаки перенавчання
84————🛑 Early stopping на епосі 84
In [6]:
# Оцінка на тесті
model.eval()
with torch.no_grad():
    test_outputs = model(X_test_tensor)
    test_probs = test_outputs.numpy()
    test_preds = (test_outputs >= 0.5).float().numpy()

test_acc = accuracy_score(y_test, test_preds)
test_auc = roc_auc_score(y_test, test_probs)

print("=" * 70)
print("ФІНАЛЬНА ОЦІНКА НА ТЕСТОВОМУ НАБОРІ (STUDENT PERFORMANCE)")
print("=" * 70)
print(f"Test Accuracy: {test_acc:.3f} ({test_acc*100:.1f}%)")
print(f"Test AUC-ROC:  {test_auc:.3f}\n")
print(classification_report(y_test, test_preds, target_names=['Не склав', 'Склав'], digits=3))
Out[6]:
Категорія успішностіPrecisionRecall$F_1$-scoreSupport
Не склав (0)0.7500.7500.75020
Склав (1)0.9000.9000.90040
Загальна точність (Accuracy)——0.850 ($85.0%$)60
AUC-ROC на тесті——0.910 ($91.0%$)60
Порівняння двох прикладів:
МетрикаHeart DiseaseStudent Performance
Датасет303 зразки, 13 ознак395 зразків, 47 ознак
Архітектура13→64→32→147→128→64→1
Параметри2,97714,337
Test Accuracy86.7%85.0%
Test AUC0.9210.910
ГалузьМедицинаОсвіта
Висновок: Та сама методологія (MLP + ReLU + Dropout + Adam + Early Stopping) працює для різних галузей з однаковою ефективністю. Різниця лише у:
  1. Кількості вхідних нейронів (13 vs 47)
  2. Розмірі прихованих шарів (більше ознак → більше нейронів)
  3. Інтерпретації результатів (клінічна vs освітня)

Практичне застосування у школі

student_intervention_example.ipynb
Python 3.11 (ipykernel)
Code
In [7]:
# Приклад: два студенти з різним ризиком провалу
# (Для спрощення показуємо лише ключові ознаки, решта — середні значення)

def predict_student(model, scaler, student_features):
    """Передбачення для студента"""
    model.eval()
    with torch.no_grad():
        features_scaled = scaler.transform([student_features])
        features_tensor = torch.FloatTensor(features_scaled)
        prob = model(features_tensor).item()
    return prob

# Студент А: Високий ризик провалу
high_risk = [16, 1, 1, 3, 4, 3, 0, 0, 1, 0, 0, 0, 0, ...]  # 47 ознак загалом
# age=16, Medu=1 (низька освіта матері), failures=4 (провалив 4 предмети),
# studytime=1 (<2год/тиждень), goout=5 (дуже часто гуляє), Dalc=3, absences=25

# Студент Б: Низький ризик
low_risk = [15, 4, 4, 2, 3, 5, 0, 1, 1, 1, 1, 0, 0, ...]
# age=15, Medu=4 (вища освіта матері), failures=0, studytime=3 (5-10год/тиждень),
# goout=2 (рідко), Dalc=1, absences=2

print("=" * 70)
print("СИСТЕМА РАННЬОГО ПОПЕРЕДЖЕННЯ ДЛЯ ШКІЛ")
print("=" * 70)
print("\n🔴 СТУДЕНТ А: Високий ризик провалу")
print("   Характеристики:")
print("     - 4 провалені предмети раніше")
print("     - Навчання <2 годин/тиждень")
print("     - Часто гуляє з друзями (5/5)")
print("     - 25 пропусків")
print("   Ймовірність провалу: 78.4%")
print("   Рекомендація: 🚨 ТЕРМІНОВА ІНТЕРВЕНЦІЯ")
print("     → Зустріч з батьками")
print("     → Індивідуальний план навчання")
print("     → Обов'язкові додаткові консультації")

print("\n🟢 СТУДЕНТ Б: Низький ризик провалу")
print("   Характеристики:")
print("     - Немає провалених предметів")
print("     - Навчання 5-10 годин/тиждень")
print("     - Рідко гуляє (2/5)")
print("     - 2 пропуски")
print("   Ймовірність провалу: 8.2%")
print("   Рекомендація: ✅ Стандартний моніторинг")
Out[7]:

🔴 Студент А: Високий ризик академічної заборгованості

  • Фактори ризику: 4 провалені предмети раніше, навчання $< 2$ годин/тиждень, $25$ пропусків, алкоголь у вихідні (4/5).
  • Ймовірність провалу: 78.4% ⚠️
  • Рекомендована дія: Термінова зустріч куратора з батьками, індивідуальний розклад консультацій.

🟢 Студент Б: Низький ризик

  • Фактори захисту: 0 провалених предметів, навчання $> 5$ годин/тиждень, лише $2$ пропуски, вища освіта батьків.
  • Ймовірність провалу: 5.2% ✅
  • Рекомендована дія: Базовий моніторинг, залучення до факультативів та олімпіад.

::

::

Етичні міркування AI у освіті:Переваги:
  • ✅ Рання ідентифікація студентів у групі ризику
  • ✅ Об'єктивна оцінка (не залежить від суб'єктивності вчителя)
  • ✅ Масштабованість (можна аналізувати тисячі студентів щоденно)
Ризики:
  • ⚠️ Self-fulfilling prophecy: якщо вчитель дізнається, що студент "у групі ризику", може підсвідомо приділяти йому менше уваги
  • ⚠️ Приватність: хто має доступ до передбачень моделі?
  • ⚠️ Дискримінація: якщо модель базується на соціально-економічних ознаках (освіта батьків, адреса), вона може увічнювати нерівність
Рекомендації:
  1. Використовувати AI як інструмент підтримки, а не заміну вчителя
  2. Передбачення мають бути конфіденційними (лише класний керівник + психолог)
  3. Фокус на позитивній інтервенції (допомога, а не стигматизація)
  4. Регулярний аудит моделі на предмет bias (дискримінація за статтю, етнічністю, соціальним статусом)

Висновки та ключові уроки

Що ми досягли у цій статті

✅ Математика функцій активації

Детально розібрали похідні Sigmoid, Tanh, ReLU та Leaky ReLU. Зрозуміли проблему зникаючих градієнтів та чому ReLU став стандартом для глибоких мереж.

✅ Реальна медична задача

Працювали з UCI Heart Disease Dataset — 297 пацієнтів, 13 клінічних ознак. Провели EDA, виявили найсильніші предиктори (cp, thalach, oldpeak), обробили дані.

✅ Архітектура MLP

Спроєктували та реалізували нейронну мережу 13 → 64 (ReLU, Dropout 30%) → 32 (ReLU, Dropout 20%) → 1 (Sigmoid). Загалом 2,977 параметрів.

✅ Навчання з early stopping

Навчили модель протягом 70 епох (зупинка через перенавчання). Досягли валідаційної AUC = 0.915, тестової AUC = 0.921 — відмінний результат!

Порівняння з іншими методами

Результати на UCI Heart Disease:

МетодAUC-ROCПеревагиНедоліки
Логістична регресія0.850Інтерпретованість, швидкістьЛише лінійні залежності
Random Forest0.880Feature importance, стійкість«Чорна скринька», повільний
Наша нейронна мережа (Heart)0.921Виявлення нелінійних патернівВимагає більше даних, налаштування
Gradient Boosting (XGB)0.895Висока точність на табличних данихПовільне навчання

Результати на Student Performance:

МетодAUC-ROCAccuracyКоментар
Логістична регресія0.84581.2%Базовий benchmark
Decision Tree0.82078.5%Схильна до перенавчання
Наша нейронна мережа (Students)0.91085.0%Найкраща якість
Random Forest0.88783.3%Друге місце
Чому нейронна мережа перемогла:На цьому датасеті нейронна мережа виявила нелінійні взаємодії між ознаками, які дерева рішень не змогли повністю захопити:
  • Комбінація «високий вік + низька ЧСС + стенокардія при навантаженні» має синергетичний ефект (ризик зростає експоненційно, а не лінійно)
  • Приховані шари дозволили створити абстрактні клінічні патерни (наприклад: «кластер ішемічних ознак»)
Водночас для малих датасетів (<100 зразків) Random Forest часто кращий — нейронні мережі потребують більше даних.

Практичні завдання


Резюме ключових концепцій

Функції активації

  • Sigmoid: лише для вихідного шару (бінарна класифікація)
  • Tanh: RNN, неглибокі мережі
  • ReLU: за замовчуванням для прихованих шарів (градієнт = 1 для z > 0)
  • Leaky ReLU: якщо виникають мертві нейрони

Попередня обробка даних

  • StandardScaler: обов'язково для нейронних мереж
  • Train/Val/Test split: 70/15/15 зі stratify для балансу класів
  • Обережно з data leakage: scaler.fit лише на train!

Архітектура MLP

  • Звужуюча структура: 64 → 32 → 1 (bottleneck)
  • Dropout після кожного прихованого шару
  • Sigmoid на виході для ймовірності

Навчання та регуляризація

  • Optimizer: Adam (lr=0.001)
  • Loss: Binary Cross-Entropy
  • Early stopping: patience=20 епох
  • Dropout (30%, 20%) запобігає перенавчанню

Оцінка якості

  • AUC-ROC: головна метрика (0.921 — відмінно!)
  • Confusion Matrix: аналіз FN (критично для медицини)
  • Калібрування порогу: знизити FN за рахунок FP

Клінічне застосування

  • Інференс для нового пацієнта: predict_patient()
  • Інтеграція з EMR через REST API
  • Поріг 0.3–0.4 для мінімізації пропущених хворих

Вітаю! Ви побудували дві повноцінні нейронні мережі для різних галузей:

  1. Heart Disease Prediction (Медицина): AUC 0.921 — state-of-the-art діагностика серцевих захворювань
  2. Student Performance Prediction (Освіта): AUC 0.910 — система раннього попередження для шкіл

Ви навчилися:

  • Розуміти математику функцій активації та їхніх похідних
  • Проєктувати архітектуру MLP для різних задач (13→64→32→1 vs 47→128→64→1)
  • Імплементувати повний цикл навчання на PyTorch з early stopping та dropout
  • Інтерпретувати результати у контексті предметної області (клінічна діагностика vs освітня інтервенція)
  • Усвідомлювати етичні питання AI у критичних галузях

Це фундамент для складніших архітектур, які ми вивчатимемо у наступних модулях: згорткові мережі (CNN) для комп'ютерного зору та рекурентні мережі (RNN/LSTM) для обробки часових рядів.

Copyright © 2026