Сегодня нейронные сети всё чаще используются для решения различных задач – от распознавания лиц до управления беспилотными автомобилями. Однако для тех, кто только начинает знакомиться с этой технологией, может показаться, что процесс обучения нейросети — это что-то сложное и непонятное.
Задача проста: у нас есть светофор, и мы хотим научить модель решать, можно ли продолжать движение на перекрестке в зависимости от того, какой сигнал светофора горит. В статье использованы базовые принципы машинного обучения и простой код на Python, чтобы построить модель, которая способна интерпретировать показания светофора, закодированные в виде числовых данных. Приведен как «рукописный» код обучения нейронной сети, так и с применением библиотеки TensorFlow. Проведено несколько экспериментов с разными параметрами сети. В конце на десерт самое интересное – дадим обученной сети показания светофоров, которых не бывает в реальной жизни, такие как зеленый и красный горящие одновременно, посмотрим, что на это скажет сеть.
Нейронная сеть — это система, которая учится и адаптируется, подобно тому, как учится и растет ребенок, обрабатывая информацию и накапливая опыт. Идея нейронных сетей была вдохновлена тем, как работает человеческий мозг, но, конечно, гораздо проще и математически точнее.
В нейронной сети основным строительным блоком является нейрон. Представьте его как маленький вычислительный элемент, который принимает входные данные, обрабатывает их и передает результат дальше. Нейрон получает несколько входов (например, закодированные сигналы светофора), умножает их на свои веса (коэффициенты, которые помогают нейрону решить, какие входы важнее), а затем складывает эти произведения.
Нейронная сеть состоит из множества таких нейронов, организованных в слои. Входной слой нейронов получает исходные данные — в нашем случае это сигналы светофора. Затем эти данные проходят через один или несколько скрытых слоев, где нейроны выполняют вычисления, основываясь на своих весах и функциях активации. Наконец, данные доходят до выходного слоя, который выдаёт результат — в нашем случае, разрешено движение или нет (1 или 0).
Чем больше слоев в сети и чем сложнее данные, тем более сложные зависимости сеть может выявлять. Но даже простая сеть с одним скрытым слоем способна решать множество практических задач.
Чтобы нейронная сеть могла моделировать сложные зависимости, просто линейных комбинаций входных данных недостаточно. Здесь на помощь приходят функции активации. Это математические функции, которые «активируют» нейрон, трансформируя его выходное значение. Одна из самых популярных функций активации – это сигмоида. Она сглаживает выходные значения нейрона, что помогает обучению сети.
Перейдём к практике. Полный код представлен на GitHub Crazy_traffic_light . Напишем простую нейронную сеть с нуля на Python. Эта сеть будет обучена распознавать сигналы светофора и принимать решение, можно ли продолжать движение (ПДД РФ, п. 6.2). Для этого нам понадобится всего несколько библиотек: numpy для работы с массивами, matplotlib для визуализации результатов и pickle для сохранения обученной модели.
import numpy as np
import matplotlib.pyplot as plt
import pickle
import pandas as pd
Начнём с представления сигналов светофора. Применим One-Hot кодирование для каждого сигнала. Всего у нас есть 6 вариантов сигналов светофора: 1 — зеленый, 2 - зеленый мигающий, 3 - желтый, 4 - желтый мигающий, 5 – красный, 6 - красный мигающий. Для каждого сигнала светофора есть метка: 1 (разрешено движение) или 0 (запрещено). Таким образом, если горит только Зеленый, то численно этот сигнал будет представлен так: [1, 0, 0, 0, 0, 0, 0]. Зеленый мигающий: [0, 1, 0, 0, 0, 0, 0]. Зеленый и желтый: [0, 0, 1, 0, 0, 1].
X = np.array([
[1, 0, 0, 0, 0, 0], # Зеленый
[0, 1, 0, 0, 0, 0], # Зеленый мигающий
[0, 0, 1, 0, 0, 0], # Желтый
[0, 0, 0, 1, 0, 0], # Желтый мигающий
[0, 0, 0, 0, 1, 0], # Красный
[0, 0, 0, 0, 0, 1], # Красный мигающий
[0, 0, 1, 0, 1, 0], # Красный и желтый
])
y = np.array([
1, # Зеленый: движение разрешено
1, # Зеленый мигающий: движение разрешено
0, # Желтый: движение запрещено
1, # Желтый мигающий: движение разрешено
0, # Красный: движение запрещено
0, # Красный мигающий: движение запрещено
0 # Красный и желтый: движение запрещено
])
Создадим простую нейронную сеть с одним скрытым слоем. В этой сети будет 6 входных нейронов (по числу сигналов светофора), 8 нейронов в скрытом слое (их количество можно поменять) и 1 выходной нейрон, который скажет нам, разрешено ли движение.
Опишем «вручную» функцию, которая обучает нейронную сеть методом градиентного спуска. В каждом цикле (эпохе) она выполняет прямой проход для расчета предсказаний, затем вычисляет ошибку между предсказанными и реальными значениями. После этого выполняется обратный проход для расчета градиентов, чтобы корректировать веса сети на основе ошибки. Веса обновляются с использованием производной функции активации и заданной скорости обучения.
Прямой проход — это процесс, при котором данные последовательно передаются через сеть, начиная с входного слоя, затем через скрытый слой, и, наконец, к выходному. На выходе мы получаем предсказание.
Обратный проход (или Backpropagation) — это ключевой процесс, который используется для обучения нейронной сети. Он заключается в корректировке весов на основе ошибки между реальным результатом и предсказанием сети.
def train_model(epochs, X, y, weights_input_hidden, weights_hidden_output, learning_rate, activation_function, activation_derivative):
errors = [] # Список для хранения ошибок на каждой эпохе
for epoch in range(epochs):
# Прямой проход
hidden_layer_input = np.dot(X, weights_input_hidden)
hidden_layer_output = activation_function(hidden_layer_input)
final_input = np.dot(hidden_layer_output, weights_hidden_output)
final_output = activation_function(final_input)
# Расчет ошибки
error = y.reshape(-1, 1) — final_output # Приводим y к нужной форме
# Сохранение ошибки для визуализации
errors.append(np.mean(np.abs(error)))
# Обратный проход (Backpropagation)
d_output = error * activation_derivative(final_output) # Ошибка на выходном слое
error_hidden_layer = d_output.dot(weights_hidden_output.T) # Ошибка на скрытом слое
d_hidden_layer = error_hidden_layer * activation_derivative(hidden_layer_output) # Градиенты скрытого слоя
# Обновление весов
weights_hidden_output += hidden_layer_output.T.dot(d_output) * learning_rate
weights_input_hidden += X.T.dot(d_hidden_layer) * learning_rate
return weights_input_hidden, weights_hidden_output, errors
Функция активации (например, сигмоида) преобразует входы сети, а её производная помогает понять, насколько быстро сеть должна учиться. Сигмоида преобразует значения в диапазон от 0 до 1. Эта функция хорошо подходит для задач, где результатом является вероятность.
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 — x)
Градиентный спуск — это метод, который помогает нейронной сети учиться, постепенно улучшая свои прогнозы. Представь, что ты стоишь на вершине горы и хочешь спуститься в долину. Но видишь только участок перед собой, поэтому делаешь маленькие шаги вниз по склону. Каждый шаг направлен в сторону уменьшения высоты — это как уменьшение ошибки в прогнозах нейронной сети.
Производная функции — это «склон» или «наклон», который показывает, в какую сторону нужно сделать шаг.
Обучение сети начинается с инициализации весов случайными значениями. Если произошло чудо и веса, расставленные случайным образом, оказались наилучшие, то модели даже учиться не придется. Если веса уже почти оптимальны, то модель не будет значительно изменять их во время обучения, так как градиентный спуск не сможет внести существенных улучшений. Градиенты будут малы, и обновления весов будут минимальными, что приведет к быстрому завершению процесса обучения. Однако, это идеализированный сценарий, который редко встречается на практике, так как вероятность случайного нахождения оптимальных весов с самого начала крайне мала. Обычно инициализация весов используется для того, чтобы помочь модели начать обучение, а не для нахождения оптимального решения сразу.
np.random.seed(2024)
weights_input_hidden = np.random.rand(6, 8) # Входной слой -> Скрытый слой (6 нейронов входа, 8 нейронов скрытого слоя)
weights_hidden_output = np.random.rand(8, 1) # Скрытый слой -> Выходной слой (8 нейронов скрытого слоя, 1 нейрон выхода)
Теперь, когда есть функции для прямого и обратного прохода, можно приступить к обучению созданной сети. Установим начальную скорость обучения (learning_rate) и количество эпох (итераций обучения, epochs), чтобы сеть смогла постепенно улучшать свои прогнозы.
learning_rate = 0.1 # Скорость обучения
epochs = 100 # Количество итераций обучения
weights_input_hidden, weights_hidden_output, errors = train_model(epochs, X, y, weights_input_hidden, weights_hidden_output, learning_rate, sigmoid, sigmoid_derivative)
Для того чтобы оценить, как хорошо обучалась модель, можно построить график ошибок по эпохам. Это поможет нам понять, насколько эффективно сеть минимизировала ошибку в процессе обучения.
import matplotlib.pyplot as plt
def train_plot(errors):
plt.figure(figsize=(10, 3))
plt.plot(errors)
plt.title(»Ошибка модели по эпохам»)
plt.xlabel(»Эпохи»)
plt.ylabel(»Ошибка»)
plt.grid(True)
plt.show()
train_plot(errors) # Вызов функции построения графика
Теперь получена модель, которая уже может распознавать сигналы светофора и предсказывать, разрешено ли движение. Однако, важным аспектом машинного обучения является возможность дальнейшего улучшения модели, тестирования и экспериментов.
Проверим, как созданная нейронная сеть работает на тех же данных, которые использовались для обучения. Протестируем ее, проведем прямой проход через сеть и посмотрим на предсказания.
def predict(input_data, weights_input_hidden, weights_hidden_output):
hidden_layer_input = np.dot(input_data, weights_input_hidden) # Прямой проход через скрытый слой
hidden_layer_output = sigmoid(hidden_layer_input) # Активация скрытого слоя
final_input = np.dot(hidden_layer_output, weights_hidden_output) # Прямой проход к выходу
final_output = sigmoid(final_input) # Активация выходного слоя
return final_output
Тестовые данные представлены так же, как и те, на которых модель училась, это просто для проверки. Поменяем их потом.
test_data = np.array([
[1, 0, 0, 0, 0, 0], # Зеленый
[0, 1, 0, 0, 0, 0], # Зеленый мигающий
[0, 0, 1, 0, 0, 0], # Желтый
[0, 0, 0, 1, 0, 0], # Желтый мигающий
[0, 0, 0, 0, 1, 0], # Красный
[0, 0, 0, 0, 0, 1], # Красный мигающий
[0, 0, 1, 0, 1, 0], # Красный и желтый
])
Вызовем функцию predict определения меток на основе тестовых данных (test_data) и полученных весов в результате обучения (weights_input_hidden, weights_hidden_output), результат запишем в predictions, округлим в rounded_predictions:
predictions = predict(test_data, weights_input_hidden, weights_hidden_output)
rounded_predictions = np.round(predictions) # Округление предсказаний до 0 или 1
Сформируем таблицу результатов для наглядности:
df = pd.DataFrame({
«Показания светофора (One-Hot)»: [str(row) for row in test_data],
«Прогноз (вероятность)»: predictions.flatten(),
«Результат модели (разрешено движение/запрещено)»: rounded_predictions.flatten(),
«Правильный результат»: y
})
Основные метрики для оценки качества модели в задачах классификации:
Accuracy (Точность) — доля правильных предсказаний модели от общего числа. Оценивает, насколько модель в целом верно предсказывает метки классов.
Recall (Полнота) — доля верно предсказанных положительных классов среди всех реальных положительных. Показывает, насколько хорошо модель находит все положительные примеры.
Precision (Точность предсказания) — доля верно предсказанных положительных классов среди всех предсказанных положительных. Оценивает точность предсказания положительных классов.
F1 Score — гармоническое среднее между Precision и Recall, даёт сбалансированную оценку между этими двумя метриками, особенно полезно при несбалансированных классах.
Каждая метрика оценивает разные аспекты работы модели, и их комбинация помогает лучше понять её производительность.
Можно, конечно, использовать метрики из библиотек, но в этот раз, пропишем метрики «вручную», также как нейронную сеть.
def accuracy(y_true, y_pred):
y_pred = np.round(y_pred) # Округление предсказаний до 0 или 1
correct_predictions = np.sum(y_true == y_pred)
total_predictions = len(y_true)
return correct_predictions / total_predictions
def recall(y_true, y_pred):
y_pred = np.round(y_pred)
true_positive = np.sum((y_true == 1) & (y_pred == 1))
false_negative = np.sum((y_true == 1) & (y_pred == 0))
return true_positive / (true_positive + false_negative)
def precision(y_true, y_pred):
y_pred = np.round(y_pred)
true_positive = np.sum((y_true == 1) & (y_pred == 1))
false_positive = np.sum((y_true == 0) & (y_pred == 1))
return true_positive / (true_positive + false_positive)
def f1_score(y_true, y_pred):
prec = precision(y_true, y_pred)
rec = recall(y_true, y_pred)
return 2 * (prec * rec) / (prec + rec)
acc = accuracy(y_test, predictions.flatten())
print(f'Accuracy: {acc:.3f}')
rec = recall(y_test, predictions.flatten())
print(f'Recall: {rec:.3f}')
prec = precision(y_test, predictions.flatten())
print(f'Precision: {prec:.3f}')
f1 = f1__score(y_test, predictions.flatten())
print(f'F1 Score: {f1:.3f}')
Результаты метрик для базовой модели, обученной и протестированной на одних и тех же данных:
Accuracy: 0.857
Recall: 0.667
Precision: 1.000
F1 Score: 0.800
Accuracy (Точность) — 0.857 (или 85,7%). Эта метрика показывает, какую долю всех прогнозов модель сделала правильно. Значение 85,7% означает, что более 8 из 10 предсказаний были верными.
Recall (Полнота) — 0.667 (или 66.7%). Полнота показывает, какую долю объектов положительного класса модель корректно распознала. В данном случае, модель правильно определила 66.7% случаев, где движение было разрешено. Это важно для ситуаций, где важно минимизировать пропуск разрешенных сигналов.
Precision (Точность предсказания) — 1.000 (или 100%). Эта метрика отражает, насколько модель уверенно предсказывает разрешенные сигналы светофора, без ложных срабатываний. Значение 100% указывает на то, что все предсказания модели на положительный класс были правильными, то есть модель не делала ошибок, предсказывая, что движение разрешено.
F1 Score — 0.800 (или 80%). F1-метрика объединяет значения точности и полноты, представляя собой гармоническое среднее между ними. Значение 85,7% говорит о том, что модель сбалансировано учитывает как правильные распознавания положительных сигналов, так и общую точность их предсказаний.
После обучения модели ее обычно сохраняют для дальнейшего использования – предсказаний на новых данных, или дополнительного обучения.
def save_model(weights_input_hidden, weights_hidden_output, filename=«model.pkl»):
with open(filename, 'wb') as f:
pickle.dump((weights_input_hidden, weights_hidden_output), f)
print(f»Модель сохранена на диске в файл: {filename}»)
save_model(weights_input_hidden, weights_hidden_output, 'model.pkl')
Если модель недостаточно обучена и (или) ее нужно дообучить на новых данных, используется загрузка параметров модели. Опять-таки, есть стандартные методы из библиотек, но у нас будет своя функция load_model:
def load_model(filename=«model.pkl»):
with open(filename, 'rb') as f:
weights_input_hidden, weights_hidden_output = pickle.load(f)
print(f»Модель загружена из файла: {filename}»)
return weights_input_hidden, weights_hidden_output
weights_input_hidden, weights_hidden_output = load_model('model.pkl')
additional_epochs = 1000 # Количество дополнительных эпох
weights_input_hidden, weights_hidden_output, errors = train_model(
additional_epochs,
X,
y,
weights_input_hidden,
weights_hidden_output,
learning_rate,
sigmoid,
sigmoid_derivative
)
save_model(weights_input_hidden, weights_hidden_output, «model_continued.pkl»)
train_plot(errors)
Результат дообучения на 1000 эпох:
Можно еще продолжить на 3000 эпох:
Теперь результаты качества модели:
Accuracy: 1.000
Recall: 1.000
Precision: 1.000
F1 Score: 1.000
Если результаты всех метрик равны 1 — это означает, что модель правильно классифицировала все тестовые примеры. Такой результат говорит о том, что на данном тестовом наборе ошибок нет, и все предсказания полностью соответствуют истинным меткам. Обычно так не бывает, но ввиду малого объема тренировочных данных, недостаточного разнообразия в данных и очень простой задачи – этот результат нормальный, ожидаемый. Таким образом модель переобучилась, т.е. просто запомнила тренировочные данные. Чуть дальше внесем «огонька» в данных, заставим модель задуматься.
Базовую модель нейронной сети уже есть, теперь попробуем ее улучшить. Увеличив количество нейронов, увеличивается её сложность, при этом можно повысить точность предсказаний. Увеличение числа нейронов в скрытом слое позволяет сети выявлять более сложные паттерны в данных.
В нашей базовой модели было 8 нейронов в скрытом слое. В новой версии увеличили их число до 20. Для этого достаточно изменить инициализацию слоев:
np.random.seed(2024)
weights_input_hidden = np.random.rand(6, 20) # Входной слой -> Скрытый слой (6 нейронов входа, 20 нейронов скрытого слоя)
weights_hidden_output = np.random.rand(20, 1) # Скрытый слой -> Выходной слой (20 нейронов скрытого слоя, 1 нейрон выхода)
Остальная часть кода остается без изменений. Вызываем в том же порядке функции train_model, train_plot, predict. Количество эпох в обучении зададим сразу 3000.
Вот результат такого изменения:
Результаты очевидны:
Accuracy: 1.000
Recall: 1.000
Precision: 1.000
F1 Score: 1.000
Иногда одного скрытого слоя недостаточно, особенно если задача требует более сложного анализа входных данных. Для улучшения модели можно добавить ещё один скрытый слой, который позволит нейронной сети выявлять более глубокие зависимости.
Как это работает
Сеть с двумя скрытыми слоями работает следующим образом: входные данные сначала проходят через первый скрытый слой, затем результаты передаются во второй скрытый слой, и только после этого — в выходной слой. Каждый слой использует функцию активации для нелинейного преобразования данных.
Инициализация слоев будет выглядеть по-другому:
np.random.seed(2024)
w_input_hidden1 = np.random.rand(6, 8) # Входной слой -> Первый скрытый слой (6 нейронов входа, 8 нейронов в первом скрытом слое)
w_hidden1_hidden2 = np.random.rand(8, 8) # Первый скрытый слой -> Второй скрытый слой (8 нейронов первого скрытого слоя, 8 во втором)
w_hidden2_output = np.random.rand(8, 1) # Второй скрытый слой -> Выходной слой (8 нейронов во втором скрытом слое, 1 нейрон выхода)
Процесс обучения аналогичен предыдущей сети, но теперь добавляется больше шагов для прямого и обратного прохода через два скрытых слоя. После каждого шага сеть корректирует веса, используя метод обратного распространения ошибки.
Функция для обучения с двумя скрытыми слоями будет выглядеть иначе:
def train_model_2(
epochs, X, y,
w_input_hidden1, w_hidden1_hidden2, w_hidden2_output,
learning_rate, activation_function, activation_derivative):
errors = [] # Список для хранения ошибок на каждой эпохе
for epoch in range(epochs):
# Прямой проход через первый скрытый слой
hidden_layer1_input = np.dot(X, w_input_hidden1)
hidden_layer1_output = activation_function(hidden_layer1_input)
# Прямой проход через второй скрытый слой
hidden_layer2_input = np.dot(hidden_layer1_output, w_hidden1_hidden2)
hidden_layer2_output = activation_function(hidden_layer2_input)
# Прямой проход через выходной слой
final_input = np.dot(hidden_layer2_output, w_hidden2_output)
final_output = activation_function(final_input)
# Расчет ошибки
error = y.reshape(-1, 1) — final_output # Приводим y к нужной форме
# Сохранение ошибки для визуализации
errors.append(np.mean(np.abs(error)))
# Обратный проход (Backpropagation)
d_output = error * activation_derivative(final_output) # Ошибка на выходном слое
error_hidden_layer2 = d_output.dot(w_hidden2_output.T) # Ошибка на втором скрытом слое
d_hidden_layer2 = error_hidden_layer2 * activation_derivative(hidden_layer2_output) # Градиенты второго скрытого слоя
error_hidden_layer1 = d_hidden_layer2.dot(w_hidden1_hidden2.T) # Ошибка на первом скрытом слое
d_hidden_layer1 = error_hidden_layer1 * activation_derivative(hidden_layer1_output) # Градиенты первого скрытого слоя
# Обновление весов
w_hidden2_output += hidden_layer2_output.T.dot(d_output) * learning_rate
w_hidden1_hidden2 += hidden_layer1_output.T.dot(d_hidden_layer2) * learning_rate
w_input_hidden1 += X.T.dot(d_hidden_layer1) * learning_rate
return w_input_hidden1, w_hidden1_hidden2, w_hidden2_output, errors
Функция получения предиктов (предсказаний модели) тоже поменяется:
def predict_2(input_data, w_input_hidden1, w_hidden1_hidden2, w_hidden2_output):
hidden_layer1_input = np.dot(input_data, w_input_hidden1) # Прямой проход через первый скрытый слой
hidden_layer1_output = sigmoid(hidden_layer1_input) # Активация первого скрытого слоя
hidden_layer2_input = np.dot(hidden_layer1_output, w_hidden1_hidden2) # Прямой проход через второй скрытый слой
hidden_layer2_output = sigmoid(hidden_layer2_input) # Активация второго скрытого слоя
final_input = np.dot(hidden_layer2_output, w_hidden2_output) # Прямой проход к выходу
final_output = sigmoid(final_input) # Активация выходного слоя
return final_output
В остальном код без изменений.
Результаты:
Accuracy: 1.000
Recall: 1.000
Precision: 1.000
F1 Score: 1.000
Для ускорения разработки и упрощения работы с нейронными сетями часто используют специализированные библиотеки, такие как TensorFlow. Она позволяет автоматизировать многие процессы создания и обучения модели, избавляя от необходимости вручную прописывать математику прямого и обратного прохода.
Создание модели с TensorFlow
С помощью TensorFlow легко создать модель. В данном примере используется последовательная модель (Sequential), где слои добавляются последовательно:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
model = Sequential()
model.add(Dense(16, input_dim=6, activation='relu')) # Скрытый слой с 16 нейронами
model.add(Dense(1, activation='sigmoid')) # Выходной слой
Здесь добавляется скрытый слой с 16 нейронами и функцией активации ReLU, а также выходной слой с функцией активации — сигмоидой.
Компиляция и обучение модели
После создания структуры модели её необходимо скомпилировать. Будем использовать функцию потерь binary_crossentropy и оптимизатор Adam, который адаптивно подстраивает скорость обучения:
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()
Модель обучается на данных, используя небольшие батчи (по 1 примеру за итерацию) и за 200 эпох:
epochs = 200
batch_size = 1
history = model.fit(X, y, epochs=epochs, batch_size=batch_size, verbose=0)
Визуализация результатов обучения
После завершения обучения можно посмотреть на графики изменения точности (accuracy) и ошибки (loss) по мере того, как модель обучалась:
def plot_model_accuracy_loss(history):
'''
функция визуализации процесса обучения: точности Accuracy и ошибки Loss
'''
plt.figure(figsize=(12, 4))
# график точности
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Accuracy')
plt.title('Model Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
# график ошибки
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Loss')
plt.title('Model Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
plot_model_accuracy_loss(history)
Accuracy: 1.000
Recall: 1.000
Precision: 1.000
F1 Score: 1.000
Этот график наглядно демонстрирует, как улучшалась точность предсказаний и как уменьшалась ошибка на протяжении всех эпох. Метрики ожидаемо выдают 1 даже на таком малом процессе обучения.
Использование TensorFlow значительно упрощает процесс создания и обучения нейронных сетей. Благодаря этой библиотеке можно быстро экспериментировать с разными архитектурами сетей, оптимизаторами и функциями потерь, не погружаясь в детали реализации обучения.
Чтобы проверить устойчивость нашей модели, мы вводим не существующие комбинации сигналов светофора — такие, которые противоречат логике, например, одновременно горят зелёный и красный свет. Такие тесты помогают выявить, как модель справляется с некорректными или непредсказуемыми данными.
Нестандартные данные
Подаем на вход модели тестовые данные с нелогичными комбинациями сигналов:
X_test_data = np.array([
[1, 0, 0, 0, 1, 0], # Зеленый и Красный
[0, 1, 0, 0, 0, 1], # Зеленый мигающий и Красный мигающий
[1, 0, 1, 0, 1, 0], # Зеленый, Желтый и Красный
[0, 1, 0, 1, 0, 1], # Желтый мигающий, Зеленый мигающий и Красный мигающий
[0, 0, 0, 0, 1, 0], # Красный
[1, 0, 0, 0, 0, 0], # Зеленый
[0, 0, 0, 0, 0, 0] # Ничего не горит
])
y_test_data = np.array([
[0], # Зеленый и Красный: запрещено
[0], # Зеленый мигающий и Красный мигающий: запрещено
[0], # Зеленый, Желтый и Красный: запрещено
[1], # Желтый мигающий, Зеленый мигающий и Красный мигающий: запрещено
[0], # Красный: запрещено
[1], # Зеленый: разрешено
[1] # Ничего не горит: разрешено
])
С их помощью пытаемся предсказать, как модель будет интерпретировать такие необычные показания светофора.
Интереснее всего посмотреть на таблицу с результатами прогноза модели.
Из таблицы видно, что правильные метки, когда горит только зеленый или красный сигнал светофора, модель с высокой вероятностью классифицировала действие. А вот на других сигналах модель начинает сомневаться и не знает что ей делать, но все же в таких случаях, когда светофор вообще без сигналов, модель говорит, что ехать можно, что вполне разумно.
Оценка качества модели теми же метриками:
Accuracy: 1.000
Recall: 1.000
Precision: 1.000
F1 Score: 1.000
Опять-таки модель показывает единицу и даже на таких непонятных данных выдает правильный результат.
Модель хорошо распознает «чокнутые» сигналы светофора, особенно в предсказаниях, где движение разрешено. Однако, её способность не пропускать положительные сигналы могла бы быть выше. Метрики показывают, что модель может столкнуться с трудностями при обработке необычных сигналов светофора, но всё ещё делает разумные предсказания в большинстве случаев.
Теперь представим, что у нас есть реальный светофор, и мы хотим применить нашу модель для принятия решений в реальном времени. Для этого мы можем подключить камеру, которая будет считывать сигналы светофора, и использовать нашу сеть для анализа изображений. В этом случае вместо вручную закодированных сигналов светофора нам понадобятся данные с камеры, которые можно обработать с помощью технологий компьютерного зрения, таких как OpenCV.
Общий алгоритм принятия изображения с камеры и принятия решения о движении может выглядеть примерно так:
import cv2
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
# Предположим, что мы можем детектировать цвет сигнала и преобразовать его в One-Hot вектор
one_hot_signal = detect_traffic_light(frame)
# Прогноз модели
prediction = predict(one_hot_signal, weights_input_hidden, weights_hidden_output)
print(f»Предсказание модели: {'Разрешено движение' if prediction > 0.5 else 'Движение запрещено'}»)
# Вывод изображения
cv2.imshow(«Traffic Light Detection», frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
Но это уже другая история, требующая более глубокого погружения человека в глубокое обучение нейронных сетей.
В статье был рассмотрен процесс создания и обучения нейронной сети для распознавания сигналов светофора с использованием различных подходов — от базовой модели с одним скрытым слоем до более сложных архитектур с несколькими слоями и библиотекой TensorFlow. На простом примере со светофором показали, как нейронная сеть обучается, разобрали принципы работы сети на интуитивном уровне и проанализировали её поведение при столкновении с необычными, нелогичными сигналами.
Эксперименты продемонстрировали, что, несмотря на некоторую сложность задач, нейронные сети могут эффективно распознавать корректные комбинации сигналов и предоставлять полезные результаты. Однако тестирование на «чокнутом светофоре» также выявило ограничения модели, которые дают основания для дальнейшего улучшения её точности и устойчивости. Далее можно продолжить исследования, направленные на улучшение архитектуры моделей, использование больших объемов данных и развитие более устойчивых алгоритмов к аномалиям.
Trask, A. (2019). Глубокое обучение. Погружение в мир нейронных сетей с Keras. Питер.
TensorFlow Documentation. (n.d.). Sequential Model. https://www.tensorflow.org/guide/keras/sequential_model
LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324. https://doi.org/10.1109/5.726791
Habr.com. (n.d.). Машинное обучение и нейронные сети. https://habr.com/ru/hub/machine_learning/