«Hello world» у світі нейронних мереж: як навчити мережу додавати числа
Про що ця стаття: будучи ІТ-ментором в українській компанії, я мав нагоду виявити особливу рису розробників. Вони відчувають, що у сфері машинного навчання та LLM відбувається щось важливе. Використовується багато слів на кшталт нейронних мереж та генеративних алгоритмів й завдяки цьому зʼявляється інтерес зʼясувати, що криється за цим поняттям, проте...
І це «проте» може стати значним бар’єром, адже якщо ви почнете читати посібник з машинного навчання, то швидко зрозумієте, що перш ніж ви дійдете до привабливого розділу «Нейронні мережі», вам доведеться пройти через досить довгий список розділів, таких як лінійна регресія, персептрон і кілька інших. В результаті у вас виникає відчуття, що для того, щоб зрозуміти, що таке нейронна мережа, і отримати спокусливий імпульс заглибитися в неї самостійно, ви дійсно повинні знати всі ці речі.
З академічної точки зору це, безсумнівно, необхідно. Якщо ви хочете отримати цілісне розуміння того, що таке машинне навчання, або, наприклад, вам потрібно скласти іспити в університеті, боюся, у вас немає іншого вибору, окрім як прочитати якийсь підручник повністю. Але якщо ви хочете відчути і доторкнутися до того, що робить вся ця магія (точніше, не магія, звичайно), я пропоную вам спробувати прийняти мої пояснення, які я використовую в своєму курсі машинного навчання протягом 2 років. За попередніми результатами я можу сказати, що після курсу більшість інженерів можуть більш-менш описати, що таке нейронна мережа, дуже близько до того, як вона працює насправді.
Я не можу сказати, що це пояснення має строгі визначення або академічний стиль. Скоріше воно базується на принципі, який знайомий більшості розробників: коли вони хочуть почати вивчати щось нове — вони хочуть побачити «Hello World». Іншими словами, вони хочуть побачити щось відносно просте, але таке, що проливає світло на суть і дає можливість зрозуміти технологію.
Що ми збираємося робити
Існує поширена проблема, що коли ви намагаєтеся знайти якийсь простий приклад в курсах машинного навчання, щось на кшталт «Hello World» — ви стикаєтеся, швидше за все, з прикладом розпізнавання зображень, де вам пропонується розрізнити собак і котів на зображеннях. Я не кажу, що цей приклад обов’язково поганий, але, на мою думку, він дає вам досить багато деталей, які спочатку маскують суть нейронних мереж.
Отже, в цій статті ми розглянемо реальний приклад простої нейронної мережі, написаної на Python, яка навчиться знаходити суму двох цілих чисел. Підкреслюю — ми будемо писати код, який не міститиме точної операції «плюс». Натомість цей код зможе знаходити суму цілих чисел на основі певного досвіду, який ми дамо йому можливість отримати.
Не можу сказати, що це пояснення нічого від вас не вимагає. Точніше, мій курс складається з інженерів-програмістів, які ще не мають досвіду в машинному навчанні, але вже не падають в непритомність від слів на кшталт «лінійна регресія». Вони також не думають, що для написання корпоративних застосунків потрібна лише одна головна навичка — вміння добре шукати на GitHub і питати у ChatGPT, що робити далі задля...
Лінійна регресія
Я б сказав, що це дійсно необхідний крок, щоб наблизитися до розуміння нейронних мереж, тому що, по суті, нейронна мережа — це безліч маленьких завдань, кожне з яких є лінійною регресією.
Пояснення, яке ви знайдете нижче, не має на меті бути суто академічним. Навпаки, воно розроблене спеціально для того, щоб дати вам комфортне відчуття розуміння. Після цієї статті я наполегливо рекомендую звернутися до інших академічних посібників.
Що ж ми називаємо лінійною регресією?
Припустимо, що ми маємо табличне представлення деякої залежності. На своїх курсах я маю підозру, що чим більше людина вчиться, тим вищі бали вона отримує на випускному іспиті (я дуже сподіваюся, що цей приклад інтуїтивно зрозумілий). Щоб підтвердити це, я збирав статистику серед своїх студентів і отримав щось на зразок цього:

Якщо ваша уява, як і моя, залишає бажати кращого, можливо, ви захочете побачити цю залежність на графіку, навіть якщо ви якимось відчуваєте, що час, витрачений на навчання, повинен якось впливати на кінцевий результат.
Отже, давайте побудуємо графік, на якому зобразимо всі ці точки, вважаючи, що по осі X будуть відкладені значення з «Часу, який студент витратив на вивчення розробки програмного забезпечення», назвемо ці значення X, а по осі Y будуть відкладені значення з «Балів, які студенти отримали на іспиті».

Я вважаю, що досить легко помітити, дивлячись на цей графік, що ці точки дуже нагадують геометричний об’єкт — лінію. Зазвичай ми можемо думати про це таким чином: агов, якщо у нас є деякі табличні дані, які, очевидно, створюють залежність, що може бути описана лінією, можливо, ми можемо зробити згортку даних до чогось більш компактного і зручного, наприклад, рівняння лінії? Окрім зручності використання формули замість табличних даних ми також отримуємо можливість відповісти на питання іншого типу: що, якби людина витратила на навчання не 10 днів, а 20, який бал вона змогла б отримати?
Отже, що нам потрібно зробити? Ми повинні провести лінію таким чином, щоб вона була максимально наближена до цих точок. Зазвичай більшість студентів зізнаються, що це твердження інтуїтивно зрозуміле, але я маю підкреслити, що існує суворе академічне визначення того, що означає «близька». Цим критерієм «близькості» є метод найменших квадратів. Якщо ви читаєте це вперше, після того, як закінчите, вам варто звернути увагу на цей метод, але поки що, якщо ви згодні з тим, що лінія «близька» до точок, то поки що все гаразд.
Отже, ми знаємо, що алгебраїчне представлення прямої має такий вигляд:

де βs — коефіцієнти, які визначають положення прямої на цьому графіку. Отже, ми можемо переформулювати твердження «Нам потрібно провести пряму так, щоб вона була максимально наближеною до цих точок» на нове: знайти такі значення обох βs, щоб рівняння прямої зайняло положення, яке ми описали словом «наближена».
Звичайно, у нас є хороший спосіб досягти цього, і цей підхід називається «лінійна регресія». Я переконаний, що вам варто прочитати про деталі цього процесу в більш академічних джерелах, але тут я лише надам код на Python, який бере нашу таблицю даних на вхід і виконує лінійну регресію. Якщо ви читаєте цю фразу і відчуваєте, що вона нічого не додає до вашого розуміння, я пропоную вам подумати про це таким чином: у нас є вхідні дані, тобто табличні дані, і є вихідні дані, тобто коефіцієнти β. Ще раз:
ВХІД -> тут щось відбувається -> ВИХІД
Я пропоную вам подумати про це так: щось відбувається в середині цього процесу обчислення. Як би дивно це не звучало, це і є концепція того, що ми називаємо нейронною мережею, але в дуже тривіальному сенсі, звичайно.
Ще один момент, який ми повинні обговорити для подальшого розуміння: тут ми маємо симуляцію процесу навчання. Слід уточнити, що ми маємо на увазі в цьому випадку, коли говоримо «навчання». Спрощено я б сказав, що навчання тут — це зміна коефіцієнтів лінійного рівняння. Але не просто зміна, тому що навчання зазвичай передбачає, що ми повинні «порівнювати» наші знання і розуміння з чимось, що ми називаємо істиною. Згадайте, будь ласка, що коли ви відповідали на запитання в школі чи університеті, суть цього процесу полягала в порівнянні сенсу, який ви намагалися донести, з розумінням вашого вчителя чи професора. Подібно до цього процесу, ми також маємо цей критерій істини. Якщо ми якимось чином (неважливо як, наприклад, просто навмання взявши значення — це теж добре, але абсолютно неефективно) отримали коефіцієнти лінійного рівняння, намалювали цю лінію і з’ясували, що положення лінії не є «близьким» до точок, то ми повинні визнати, що наші коефіцієнти, безумовно, слід покращити. Я дійсно думаю, що це друга концепція, яку ви повинні прийняти в своєму розумі, щоб зрозуміти ідею нейронних мереж: у нас повинна бути можливість перевірити наш результат в тому чи іншому випадку. Цей процес перевірки — це саме те, що ми маємо на увазі, кажучи «навчання».
Щоб бути послідовним, я пропоную вам перевірити деякий код на Python, який в основному робить саме те, що я описав: він обчислює коефіцієнти рівняння прямої за допомогою методу градієнта спуску (якщо ви не знаєте, що це таке, можете погуглити пізніше), і виявляється, що після деяких обчислень наша пряма досить «близько» до наших точок.
import numpy as np
import matplotlib.pyplot as plt
# Our data
X = np.array([0.00, 0.53, 1.05, 1.58, 2.11, 2.63, 3.16, 3.68, 4.21, 4.74,
5.26, 5.79, 6.32, 6.84, 7.37, 7.89, 8.42, 8.95, 9.47, 10.00])
Y = np.array([0.50, 2.93, 3.13, 3.96, 5.54, 6.60, 7.42, 8.29, 9.69, 10.70,
11.56, 12.75, 13.94, 14.31, 16.18, 17.21, 17.85, 18.78, 20.13, 21.26])
# Normalize the features
X_norm = (X - np.mean(X)) / np.std(X)
# Add a column of ones to X for the intercept term
X_norm = np.column_stack((np.ones_like(X_norm), X_norm))
# Initialize parameters
theta = np.zeros(2)
# Set hyperparameters
alpha = 0.01 # Learning rate
num_iters = 1000
# Gradient Descent
for _ in range(num_iters):
h = np.dot(X_norm, theta)
gradient = np.dot(X_norm.T, (h - Y)) / len(Y)
theta -= alpha * gradient
# Convert parameters back to original scale
m = theta[1] / np.std(X)
b = theta[0] - m * np.mean(X)
print(f"Equation of the line: y = {m:.2f}x + {b:.2f}")
# Calculate R-squared
y_pred = m * X + b
ss_tot = np.sum((Y - np.mean(Y))**2)
ss_res = np.sum((Y - y_pred)**2)
r_squared = 1 - (ss_res / ss_tot)
print(f"R-squared value: {r_squared:.4f}")
# Plotting
plt.figure(figsize=(10, 6))
plt.scatter(X, Y, color='blue', label='Data points')
plt.plot(X, y_pred, color='red', label='Regression line')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Linear Regression using Gradient Descent')
plt.legend()
plt.grid(True)
plt.show()
Код надрукує наступне:
Equation of the line: y = 2.01x + 1.11
І нарешті у нас є шанс переконатися, що ця лінія дійсно досить близька до наших точок.

Так навіщо це нам?
Чи наближає це нас до розуміння нейронних мереж? Моя відповідь — так. І дозвольте мені ще раз повернутися до того, що ми щойно зробили.
- У нас був вхідний набор даних, у нашому випадку точок.
- Ми щось зробили з цими даними й на виході отримали деякий результат.
Ми досягли цього, змінюючи коефіцієнти рівняння ліній більш-менш хитромудрим способом. - Наші вихідні дані кардинально відрізняються від вхідних.
Що, якщо я скажу, що точно такий же принцип лежить в основі нейронної мережі? Буквально, у нас є вхідні дані, ми щось з ними робимо (що саме ми робимо — гарне питання, але це не тема цієї статті) і, нарешті, ми маємо оброблені деяким чином дані на виході.
І зараз я покажу вам щось дуже схоже на те, що я описав вище. Отже, ми навчимо наш код додавати два числа без використання оператора «+» у Python. Я не жартую! Ми напишемо код, який навчиться додавати два цілих числа майже так само, як це робить лінійна регресія. Вперед!
- Ми матимемо внесок.
- Ми збираємося щось зробити з цими даними. Деталі дуже важливі, але не зараз: «щось зробити» означає, що ми збираємося змінити коефіцієнти деякої математичної моделі, використовуючи певний підхід, досить велику кількість разів. Згадайте, що ми робили, коли мали справу з лінійною регресією: ми порівнювали, наскільки близькою є наша лінія. У нашому випадку все набагато простіше: якщо ми намагаємося попросити когось — будь ласка, підрахуйте суму — ми точно знаємо відповідь або принаймні можемо порахувати.
- На виході ми матимемо деякі значення коефіцієнтів деякої математичної моделі, яка здатна приймати два цілих числа і без їх підсумовування давати нам відповідь.
Звичайно, цей результат буде приховано від нас, і ми зможемо використовувати навчену модель лише тоді, коли попросимо її підсумувати наші цілі числа.
Ось код нейронної мережі, яка вміє додавати числа, не знаючи, що таке арифметична операція «+» взагалі.
import tensorflow as tf
import numpy as np
# Generate training data with a wider range
np.random.seed(0)
X = np.random.randint(0, 1000, size=(10000, 2)) # 10000 pairs of numbers between 0 and 999
y = np.sum(X, axis=1)
# New normalization strategy
X_max = 1000.0
y_max = X_max * 2
X_norm = X / X_max
y_norm = y / y_max
# Define model
model = tf.keras.Sequential([
tf.keras.layers.Dense(64, activation='relu', input_shape=(2,)),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1)
])
# Compile the model
model.compile(optimizer='adam', loss='mse')
# Train the model
history = model.fit(X_norm, y_norm, epochs=100, validation_split=0.2, verbose=0)
# Test the model
test_input = np.array([[400, 200]])
test_input_norm = test_input / X_max
prediction = model.predict(test_input_norm)
predicted_sum = prediction[0][0] * y_max
print(f"Input: {test_input[0]}")
print(f"Predicted sum: {predicted_sum:.2f}")
print(f"Actual sum: {np.sum(test_input[0])}")
# Print final loss
print(f"Final loss: {history.history['loss'][-1]:.6f}")
# Test with more examples
test_cases = [[5, 7], [100, 200], [400, 200], [999, 999]]
for case in test_cases:
test_input = np.array([case])
test_input_norm = test_input / X_max
prediction = model.predict(test_input_norm)
predicted_sum = prediction[0][0] * y_max
print(f"\nInput: {case}")
print(f"Predicted sum: {predicted_sum:.2f}")
print(f"Actual sum: {sum(case)}")
Ця стаття призначена для розробників, але якщо ви не розробник або читаєте її з цікавості, я пробіжуся по цьому коду, щоб підкреслити ключову концепцію того, що ми маємо в ньому.
Перш за все, запам’ятайте основну концепцію: у нас є вхідні дані, які дають можливість навчати нашу модель. Коли я кажу «навчати», я маю на увазі, що ми можемо щоразу порівнювати те, що дає наша модель, з правильним значенням і вносити певні корективи в неї. Це саме те, що ми маємо на увазі, коли говоримо «навчання». Хороша новина полягає в тому, що вся ця складність прихована завдяки гарній інкапсуляції сучасних бібліотек Python.
Отже, крок за кроком. Підготуємо дані для навчання:
# Generate training data with a wider range np.random.seed(0) X = np.random.randint(0, 1000, size=(10000, 2)) # 10000 pairs of numbers between 0 and 999 y = np.sum(X, axis=1)
Думаю, цей код не потребує пояснень, мабуть, окрім останнього рядка: якщо X — тензор з парами цілих чисел, обмежених 1000, то в y ми матимемо суму цілих чисел у кожній такій парі.
А все, що після цього рядка і до:
# Train the model history = model.fit(X_norm, y_norm, epochs=100, validation_split=0.2, verbose=0)
.... — це те, що ми називаємо навчанням. Якщо ви читаєте це вперше і ніколи не стикалися з цим раніше, я дуже пропоную вам тимчасово (тому що пізніше вам обов’язково потрібно буде повернутися до більш академічних пояснень) подумати над кодом таким чином:
- Гаразд, у нас є 1000 пар цілих чисел та їх сум.
- Кожна з цих пар проходить через деяке лінійне рівняння, де невідомими змінними є наші пари, а результатом рівняння є сума цих чисел.
- Ми підставляємо ці пари і суми в рівняння 1000 разів і кожного разу розв’язуємо це рівняння.
- Наша мета — знайти такі коефіцієнти рівняння, які, підставивши в це рівняння наші пари цілих чисел замість невідомих змінних, ми зможемо порівняти результат з відомим нам, і прийняти рішення — чи варто покращити наші коефіцієнти, чи вони й так досить хороші.
Дозвольте мені проілюструвати це графічно:
- У нас є деяке лінійне рівняння, де x та y невідомі.
- У нас є ще невідомі коефіцієнти a та b.
- Ми присвоїмо їм деякі значення за замовчуванням.
- У це рівняння ми підставляємо нашу першу пару цілих чисел і робимо прогноз. Простіше кажучи, ми обчислюємо суму цієї функції.
- Оскільки ми знаємо, яка сума в цьому випадку, завдяки нашому коду на Python ми можемо порівняти, наскільки великою є наша помилка, або, як на цьому графіку, ми обчислюємо втрати.
- Якщо ми бачимо, що наші втрати занадто великі, давайте якось змінимо наші a і b.
- Так можна продовжувати досить довго.
- І є ненульовий шанс, що ми знайдемо такі коефіцієнти, які принаймні для будь-якої пари наших цілих чисел дадуть нам деяке число, яке буде досить близьким до їх суми.
- Ми закінчили.

Звичайно, можна задати безліч питань: про тип рівняння, про математичний метод, який використовується для розрахунку, які кроки в коді Python, але я думаю, що на даний момент важливіше зрозуміти основну ідею нейронних мереж: ми можемо обчислити результат деякої функції, порівняти з відомим значенням і налаштувати параметри цієї функції. Що, по суті, і є навчання.
А тепер настав час перевірити те, що у нас вийшло. Якщо ви запустите свій код, то отримаєте щось схоже на:
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 85ms/step Input: [400 200] Predicted sum: 597.78 Actual sum: 600 Final loss: 0.000002 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 28ms/step Input: [5, 7] Predicted sum: 11.58 Actual sum: 12 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step Input: [100, 200] Predicted sum: 297.75 Actual sum: 300 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step Input: [400, 200] Predicted sum: 597.78 Actual sum: 600 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 35ms/step Input: [999, 999] Predicted sum: 1989.17 Actual sum: 1998
Як бачите, у всіх тестових прикладах (це, звісно, хард-код, було б дуже корисно, якби ви спробували свої власні цифри) наші відповіді досить близькі до точних відповідей. Наприклад:
Input: [100, 200] Predicted sum: 297.75 Actual sum: 300
Отже, ми можемо зробити обережний висновок, що, незважаючи на те, що наша мережа робить деякі помилки, загалом вона обчислює досить добре для чогось, що не знає, що таке сума, і була навчена цьому за допомогою наших обчислень. Ви можете поставити слушне запитання: чому ми не отримуємо точних результатів? Але відповідь буде надто довгою, щоб розповідати всі деталі тут. Коротко кажучи, оскільки ми не робимо підсумовування цілих чисел, ми робимо наближені обчислення, які за визначенням не можуть бути точними на комп’ютері.
Як тепер навчити нашу мережу віднімати цілі числа?
Звичайно, не тільки цілі числа, але й будь-які інші, з якими ви хочете, щоб ваша нейромережа навчилася працювати. Я впевнений, що ви вже вловили ідею — ви генеруєте свої пари чисел і результат віднімання цих пар. В цьому випадку ви навіть не заплутаєтесь: іноді деякі мої студенти кажуть щось на кшталт — «Ей, звичайно, адже в рівнянні прямої у нас у формулі стоїть „плюс“, то це не зовсім чесно, тут може бути якийсь обман». Щоб бути абсолютно впевненим, що нейромережа не чаклує і чесно обчислює віднімання, ви можете поекспериментувати. Можете навіть не зупинятися на відніманні: не соромтеся навчити нейромережу робити все, що дозволяє ваша уява: множити, ділити, розв’язувати рівняння та інші речі. Це буде гарним кроком для початку.
Можливо, ви вже готові зрозуміти, як нейронні мережі розпізнають зображення. Наприклад, уявімо, що у вас є 1000 фотографій котів і 1000 фотографій собак. І з якихось причин ви хочете навчити свою нейромережу визначати, чи містить будь-яка довільна фотографія кота або собаку.
Ви можете подумати про концепцію цього процесу таким чином. Гаразд, у мене є зображення котів. Це мій input. Для кожного такого зображення я абсолютно точно знаю, що на ньому зображено. Тож, можливо, комбінація пікселів, яка в певному випадку є котом, може дати мені числову характеристику, яка ідентифікує кота? Це дійсно гарне питання, як знайти таку характеристику, але якщо ми її знайшли, це означає, що ми можемо описати будь-якого кота деякою послідовністю значень. Якщо ми отримаємо будь-яке невідоме зображення і знову обчислимо цю характеристику, ми можемо з певною часткою впевненості порівняти наші результати і сказати: якщо тензор невідомого зображення відносно близький до тензора значень, які я отримую, навчаючи свою нейромережу, то, можливо, на невідомому зображенні теж є кіт!
Очевидно, що цей підхід можна поширити майже на все, де можна виявити такі закономірності: ми маємо вхідні дані, ми маємо бажаний результат і ми можемо перевірити якість нашого результату. Зазвичай після такого пояснення я пропоную студентам: всі (майже) знають, що таке квадратне рівняння. Тепер ніщо не заважає вам навчити вашу нейромережу так, щоб вона навчилася розв’язувати такі рівняння. Все, що вам потрібно зробити, це надати список коефіцієнтів і коренів квадратних рівнянь, що відповідають цим коефіцієнтам. Звичайно, це трохи складніше, ніж тривіальне підсумовування цілих чисел, але більш ніж реально. Тож, будь ласка, починайте!
9 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівЦе не Hello World, це мікроскопом цвяхи забивати.
Зважаючи що для цього Hello World десь так гігабайт залежностей треба завантажити то порівняння не далеко від правди
у вас дуже непоганий стиль викладання, нагадує чимось Тімоті Снайдера: ви можете подумати про ... , отже ми можемо зробити .., можливо вивже готові зрозуміти ..
Цікаво, який буде результат якщо в цю модель вкинути такі пари?
[10002, 555555], [44444, 33333]
підказка: ви не навчили ніякої регрессії вашу модель, ви просто запамятали датасет.
Cитуація: корпоративна розсилка, питання — хто хоче вивчати ML. Всі бажаючи (можливо вони останній раз чули про регресії в универі, якщо взагалі чули) приходять на мітап, де в мене є три години, щоб їм розповісти як це все працює й що ця «магія» значить. В мене немає на перших заняттях взагалі такої мети — розповідати про оверфіттінг або всі інші підводні камені, які можна зустріти. Моя задача — дати хелло ворлд в контексті ML.
Simple computation проблема це поганий приклад, що ви могли побачити самі по майже стовідсотковій точності моделі (і про метріки ви взагалі нічого не сказали, а було б цікаво). Розробники мають також знати, для яких задач ML не потрібен. Hello world це був би класичний приклад розпізнавання цифр на зображеннях
була б чудова ідея для наступної статті
Дуже гарно це зроблено в www.youtube.com/watch?v=aircAruvnKk
дякую, візуал чудовий
а от алгоритми зрозуміти складно, передивлявся по кілька разів так і недопетрав всього =)
PS виявилось що я добре знаю цей канал — раніше там дивився про «популярні математичні» штуки