Математика за ИИ: начнем с простого
Как бы вы обычно написали программу для сложения двух чисел меньше 100? Легко, верно? Вы бы, как правило, написали что-то простое, например:
#include <iostream>
using namespace std;
int main() {
int a, b, sum;
cout << "Введите первое число: ";
cin >> a;
cout << "Введите второе число: ";
cin >> b;
sum = a + b;
cout << "Сумма равна: " << sum << endl;
return 0;
}
Но давайте выберем более захватывающий путь — давайте научим машину самостоятельно складывать, используя нейронную сеть. Нейронные сети лежат в основе современного ИИ, обеспечивая работу всего, от распознавания изображений и перевода языков до сложных систем, таких как ChatGPT.
В этой статье мы покажем, как нейронная сеть может "научиться" складывать два числа через обучение, а не следуя жестко закодированному правилу. Вы увидите, как нейронная сеть учится "складывать", распознавая шаблоны. Мы даже раскроем реальные числа (магия за кулисами), которые это делают.
Вот взгляд на внутреннюю структуру обученной нейронной сети:
// Веса и смещения скрытого слоя
double weight1_node1 = 0.658136;
double weight2_node1 = 0.840666;
double bias_node1 = -0.893218;
double weight1_node2 = -0.720667;
double weight2_node2 = -0.369172;
double bias_node2 = 0.036762;
double weight1_node3 = 0.512252;
double weight2_node3 = 0.292342;
double bias_node3 = -0.0745917;
// Веса и смещение выходного слоя
double weight_out_node1 = 1.93108;
double weight_out_node2 = -0.718584;
double weight_out_node3 = 0.589741;
double bias_out = -0.467899;
На первый взгляд, эти значения могут показаться случайными, но они представляют собой крошечный искусственный мозг, способный научиться складывать два числа.
Используйте эту формулу для расчета выхода. Не переживайте, если это сейчас кажется сложным — просто следуйте за нами:
Здесь, x1
и x2
— это два числа, которые вы хотите сложить (нормализованные путем деления на 100):
double x1 = 0.3; // 30 / 100
double x2 = 0.5; // 50 / 100
// Скрытый слой
double z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
double h1 = sigmoid(z1);
double z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
double h2 = sigmoid(z2);
double z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
double h3 = sigmoid(z3);
// Выходной слой
double sum = (h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out) * 200.0f;
Давайте разберем, почему мы умножаем на 200: мы изначально масштабировали наши входные и выходные данные, чтобы они находились между 0 и 1 для более легкого обучения. Поскольку максимальная сумма двух чисел меньше 100 равна 200, мы масштабируем выход обратно, умножая его на 200.
Функция сигмоида:
double sigmoid(double x) {
return 1.0 / (1.0 + exp(-x));
}
Давайте попробуем с реальными числами:
Давайте используем x1 = 30
и x2 = 50
. Сначала нормализуем их:
x1 = 30 / 100 = 0.3
x2 = 50 / 100 = 0.5
Теперь мы пройдем через математику:
z1 = (0.3 × 0.658136) + (0.5 × 0.840666) - 0.893218
≈ 0.1974408 + 0.420333 - 0.893218
≈ -0.2754442
h1 = sigmoid(z1) ≈ 0.431844
z2 = (0.3 × -0.720667) + (0.5 × -0.369172) + 0.036762
≈ -0.2162001 - 0.184586 + 0.036762
≈ -0.3640241
h2 = sigmoid(z2) ≈ 0.409872
z3 = (0.3 × 0.512252) + (0.5 × 0.292342) - 0.0745917
≈ 0.1536756 + 0.146171 - 0.0745917
≈ 0.2252549
h3 = sigmoid(z3) ≈ 0.556078
// Финальный выходной слой
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
≈ (0.834389) + (-0.294320) + (0.328013) - 0.467899
≈ 0.400183
Предсказанная сумма = 0.400183 × 200 ≈ 80.0366
// Почему умножаем на 200? Потому что во время обучения мы нормализовали все входные и выходные данные, чтобы они оставались между 0 и 1 для лучшей стабильности обучения. Поскольку максимальная сумма двух входов равна 100 + 100 = 200, мы масштабируем выход сети обратно, умножая его на 200.
Бум! Результат примерно 80, что является правильной суммой 30 и 50.
Если вы не верите, что эта маленькая сеть действительно может складывать, давайте протестируем ее снова с другими числами: 20 и 30.
Сначала нормализуем входные данные:
x1 = 20 / 100 = 0.2
x2 = 30 / 100 = 0.3
Затем вычисления:
z1 = (0.2 × 0.658136) + (0.3 × 0.840666) - 0.893218
≈ 0.1316272 + 0.2521998 - 0.893218
≈ -0.509391
h1 = sigmoid(z1) ≈ 0.375241
z2 = (0.2 × -0.720667) + (0.3 × -0.369172) + 0.036762
≈ -0.1441334 - 0.1107516 + 0.036762
≈ -0.218123
h2 = sigmoid(z2) ≈ 0.445714
z3 = (0.2 × 0.512252) + (0.3 × 0.292342) - 0.0745917
≈ 0.1024504 + 0.0877026 - 0.0745917
≈ 0.1155613
h3 = sigmoid(z3) ≈ 0.528860
// Финальный выходной слой
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
≈ (0.724688) + (-0.320095) + (0.311644) - 0.467899
≈ 0.248338
Предсказанная сумма = 0.248338 × 200 ≈ 49.6676
Она предсказала около 49.67. Это очень близко к реальной сумме: 50!
Довольно здорово, не правда ли?
Итак... Что такое нейронная сеть?
Краткое введение: Нейронная сеть — это как математическая версия вашего мозга. Она состоит из единиц, называемых нейронами, которые обрабатывают входные данные, умножая их на веса, добавляя смещение и затем применяя функцию активации. Одна из распространенных функций активации — это сигмоида:
sigmoid(x) = 1 / (1 + e^(-x))
Это сжимает любой вход в значение между 0 и 1, позволяя сети захватывать сложные шаблоны.
Вот график функции сигмоида, наглядно показывающий, как входные данные плавно преобразуются в значения между 0 и 1:

Существуют и другие функции активации, такие как ReLU (Rectified Linear Unit), tanh и softmax, каждая из которых имеет свои случаи использования и поведение. Но сигмоида — отличное место для начала, потому что она проста и плавна — идеально подходит для небольших нейронных сетей, таких как наша.
Настройка: Обучаем нашу сеть складывать
Для этого эксперимента мы создали небольшую сеть с:
- 2 входных узла (для двух чисел, которые нужно сложить)
- 3 скрытых узла (выполняющих тяжелую работу с магией сигмоида)
- 1 выходной узел (дающий нам сумму)
Вот диаграмма, показывающая структуру нейронной сети, которую мы используем для сложения двух чисел. Вы можете увидеть, как каждый входной узел соединяется со всеми узлами скрытого слоя, как применяются веса и смещения, и как все движется к финальному выходу:

И да, мы все масштабировали. Поскольку наши числа меньше 100, мы делим входные данные на 100 и в конце масштабируем выход обратно на 200.
Вот код, который мы пишем для сложения двух чисел с помощью нейронной сети:
#include <iostream>
#include <cmath>
using namespace std;
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
int main() {
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;
float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f;
float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f;
float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000;
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
float x1 = a / 100.0f;
float x2 = b / 100.0f;
float target = (a + b) / 200.0f; // Нормализуем цель в диапазоне 0-1
// Прямой проход для скрытого слоя
float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
float h1 = sigmoid(z1);
float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
float h2 = sigmoid(z2);
float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
float h3 = sigmoid(z3);
// Выходной слой с линейной активацией
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float error = target - y_pred;
total_loss += error * error;
// Обратный проход (выходной слой - линейный)
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;
// Обратный проход (скрытый слой)
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;
float grad_w1_node2 = delta_h2 * x1;
float grad_w2_node2 = delta_h2 * x2;
float grad_b_node2 = delta_h2;
float grad_w1_node3 = delta_h3 * x1;
float grad_w2_node3 = delta_h3 * x2;
float grad_b_node3 = delta_h3;
// Обновление весов
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
weight1_node2 += learning_rate * grad_w1_node2;
weight2_node2 += learning_rate * grad_w2_node2;
bias_node2 += learning_rate * grad_b_node2;
weight1_node3 += learning_rate * grad_w1_node3;
weight2_node3 += learning_rate * grad_w2_node3;
bias_node3 += learning_rate * grad_b_node3;
}
}
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[Итог] Эпоха " << epoch + 1 << ": Потери = " << total_loss << endl;
}
// Тестируем модель
float a, b;
cout << "\nТестирование предсказания суммы (a + b)\nВведите a: ";
cin >> a;
cout << "Введите b: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float predicted_sum = y_pred * 200.0f;
cout << "w1_node1: " << weight1_node1 << ", w2_node1: " << weight2_node1
<< ", w1_node2: " << weight1_node2 << ", w2_node2: " << weight2_node2
<< ", w1_node3: " << weight1_node3 << ", w2_node3: " << weight2_node3
<< ", out_w1: " << weight_out_node1 << ", out_w2: " << weight_out_node2
<< ", out_w3: " << weight_out_node3 << ", bias_out: " << bias_out
<< ", bias_node1: " << bias_node1 << ", bias_node2: " << bias_node2 << ", bias_node3: " << bias_node3 << endl;
cout << "Предсказанная сумма: " << predicted_sum << "\nФактическая сумма: " << (a + b) << endl;
return 0;
}
Итак... Как она научилась?
Вот интересная часть: мы не закодировали правила для сложения. Мы просто дали сети кучу пар чисел и их суммы, и она постепенно подстраивала эти веса и смещения, пока предсказания не становились все лучше и лучше.
Это как проба и ошибка, но умнее — с добавлением немного исчисления.
Она действительно научилась складывать?
Эм, не совсем так, как мы. Она не понимает, что значит "плюс". Но она действительно учится очень хорошей аппроксимации. Она видит шаблоны в числах и предсказывает суммы, которые часто оказываются точными.
Это больше похоже на обучение через ощущение шаблонов, а не знание правил. Если вам интересно, вы можете изменить входные числа и протестировать это. Чем больше вы играете, тем больше вы поймете, как работают эти маленькие мозги.
Заключение
Обучение нейронной сети складывать два числа может показаться глупым на первый взгляд, но это отличный способ заглянуть внутрь разума ИИ. За всей этой шумихой стоят просто веса, смещения и немного умной математики.
В следующей части мы углубимся в то, как работают нейронные сети: мы исследуем слои, функции активации, производные и то, что на самом деле происходит во время обучения.