Введение
Сегодня нейронные сети всё чаще используются для решения различных задач – от распознавания лиц до управления беспилотными автомобилями. Однако для тех, кто только начинает знакомиться с этой технологией, может показаться, что процесс обучения нейросети — это что-то сложное и непонятное.
Задача проста: у нас есть светофор, и мы хотим научить модель решать, можно ли продолжать движение на перекрестке в зависимости от того, какой сигнал светофора горит. В статье использованы базовые принципы машинного обучения и простой код на 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
С помощью 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/
Комментарии (12)
Lodinn
09.10.2024 13:50Что тут скажешь, у Вас получился прекрасный пример того, как НЕ надо использовать нейронки.
Ожидание: будем бороться с распознаванием сигналов светофора (см., например, https://www.autoweek.com/news/green-cars/a37114603/tesla-fsd-mistakes-moon-for-traffic-light/)
Реальность: пытаемся научить один-два скрытых слоя по уже заданному one-hot encoding восстанавливать однозначно заданную таблицу соответствия и кривовато экстраполировать вне обучающих данных.
Как учебно-методический пример, всё нормально. Но ПОЖАЛУЙСТА, не делайте вид, что так и надо, и уж тем более не фигачьте такое в продакшн.
AV_Tar Автор
09.10.2024 13:50Пожалуйста, прочтите статью полностью, а не только ее заголовок. Если ваши ожидания касаемо содержимого моей статьи не оправдались, то напишите свою статью и изложите в ней то, что вы хотели увидеть. Если есть конкретные замечания и неточности моей статьи, давайте обсудим.
Lodinn
09.10.2024 13:50У меня нет привычки писать комментарии, не осилив исходный материал.
Хотите больше конкретики - пожалуйста.
Основная проблема - из
модель, которая способна интерпретировать показания светофора, закодированные в виде числовых данных
во введении никак не следует, что вся сложная часть отдаётся на откуп магической функции
detect_traffic_light
, про которую мы _просто предположили_, что она есть и работает безошибочно.Теперь касательно той задачи, которую Вы таки решаете. Во-первых, пространство исходов совсем невелико, и правильные ответы для каждого известны; их легко сгенерировать алгоритмически. Нейросети тут - не то что просто из пушки по воробьям, но ещё и с немалой вероятностью попадания из той самой пушки себе в ногу, если плохо сформированы тестовые данные. Если бы "магическая" функция отдавала значения от 0 до 1, было бы больше смысла. Но не с one-hot encoding. Во-вторых, Вы строите всевозможные метрики, графики loss function, но никак их не анализируете. В-третьих (и это ой как важно!), совершенно отсутствуют какие-либо соображения со стороны проектирования всей системы. Во многом это перекликается с первым замечанием, но есть и практические аспекты. Мой любимый пример - задумайтесь на минутку, должна ли функция потерь быть симметрична (иными словами, верно ли, что ложноположительные срабатывания так же плохи, как ложноотрицательный результат), и подумайте, почему Вы делаете то, что Вы делаете.
Ну и ещё одно замечание по мелочи - а Вы точно по одному кадру можете понять, мигает ли светофор?
Дальнейшее рассматриваю с позиций "читатель хочет вкатиться в формате "моя первая нейросеть". Пример выбран довольно удачный и наглядный. Не хватает пояснений.
Что могло быть лучше в сутевой части:
Размышления на тему, как выбор гиперпараметров влияет на результат работы. Пусть пример "игрушечный", но в нём на кону транспортная безопасность и жизни людей, в конце концов! Если известно, что могут случаться нестандартные ситуации, достаточно ли проведённого исследования на странных сочетаниях? Может, порог принятия решения в 0.5 (=авось, пронесёт) не самый удачный? Зависит ли это от того, сколько сигналов и их сочетаний разрешающие, а сколько запрещающие? Или того, с какой вероятностью они определены неправильно? Или того, что проезд на красный чреват худшими последствиями, чем встать на зелёном и никуда не уехать?
Комментарии на тему, какие результаты работы и обучения Вы считаете более удачными и почему. Даже не погружаясь в причины того, почему, например, функции потерь так отличаются - стало хуже? Лучше? Нужен второй слой-то? Почему, кстати, функция потерь не 0, а все приведённые метрики 1?
Автоматизация тестирования. Вот вы пообучали нейросеть на семи (!) примерах и пытаетесь распространить на 2^7 = 128 возможных вариантов. Как уже говорилось, если ненадолго задуматься, можно алгоритмически сформировать таблицу исходов. Стало быть, легко показать, как точность зависит, скажем, от размеров обучающей выборки. Тоже методологически полезно.
В общем, хотелось бы немного больше осознанности, а не "вот я напитонил нейросеть прям как в учебнике, делайте так же, не работает - крутитесь как хотите".
AV_Tar Автор
09.10.2024 13:50Если вы дошли до "магической функции" detect_traffic_light, то наверняка прочитали, что раздел "Применение к реальным данным" - это лишь один из возможных вариантов дальнейшего развития и в статье никоем образом не разбирается распознавание светофоров по фото/видео с помощью OpenCV. Если коротко, то в статье разбирается "внутренность" сети, то, как обучается, базовые принципы (о чём написано прямо во введении). И да, эта статья по сути "методичка для начального понимания", а не готовый продукт в "продакшн" для запуска беспилотного поезда.
Исходя из этого, причем здесь луна и Тесла?
Причем "а Вы точно по одному кадру можете понять, мигает ли светофор", если про распознавания объектов на фото нет речи?
Какая транспортная безопасность и жизни людей?
"Почему, кстати, функция потерь не 0, а все приведённые метрики 1" - если что, то в моих результатах функция потерь при длительном обучении стремится к нулю, см. графики.Зачем писать много букв в комментарии "всё и обо всём"? Комментарий ради комментария? Не уверен, что вы поняли назначение статьи. Если хотите помочь читателям, так расскажите как надо, а не "как НЕ надо использовать нейронки"
Lodinn
09.10.2024 13:50Ясно, думать сложно.
Проехали.
IamSVP
09.10.2024 13:50Вообще я согласен с комментатором. Сложно оценить ценность статьи. Не указан размер выборки, также есть вопросы к итоговым значениям по всем метрикам единицы... Что это значит? Что модель работает идеально или что выборка небольшая и модель просто переобучилась на датасет? Также согласен с вопросом к определению мигающих сигналов по единственному кадру. Кажется, что подход выбран не верно. В качестве демонстрации проекта классификации лучше было выбрать что то более статичное, хотя бы марки авто или типы знаков
AV_Tar Автор
09.10.2024 13:50Господа, вы что издеваетесь? И вам также напишу, прочтите всю статью полностью, а не только название и конец.
В статье размер обучаемой выборки указан: X = np.array([… и X_test_data
Про результаты метрик равные единице тоже были написаны пояснения
Причем здесь "определению мигающих сигналов по единственному кадру", если в статье НЕТ разбора процесса обработки фотографий и детекции объектов на них?
"более статичное, хотя бы марки авто или типы знаков" аналогично п.3, я не разбирал процесс разбора фото
IamSVP
09.10.2024 13:50подождите... Я правильно понял, что все обучение производилось на 7 строках?
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 # Красный и желтый: движение запрещено
])
Вот это и есть ВСЯ выборка?
AV_Tar Автор
09.10.2024 13:50Да, вы совершенно верно всё поняли. Или вы считаете, что для понимания устройства и принципа обучения нейронной сети нужен датасет с миллионами строк, сотней признаков, десятки слоев, миллиарды параметров? Повторю также как и прошлому комментатору, что это "методичка для начального понимания, а не готовый продукт в "продакшн" для запуска беспилотного поезда"
IamSVP
09.10.2024 13:50ну тогда тут много вопросов) Даже по поводу 100 эпох и выбранного алгоритма)
Признаюсь честно, не с первого раза понял вашу статью. Для обегчения чтения советую оформить код в блоки
Kristaller486
09.10.2024 13:50Последнее время люди взъелись на любые упоминания нейросетей. Отсюда и такая реакция, на хорошую статью для новичков (форматирование кода только поправьте). У некоторых срабатывает триггер "нейросети -> плохо -> они неточные, глупые, так далее. Нужно написать об этом в комментариях!". Ничем другим объяснить такое не могу.
abyrvalglavryba
а если этот светофор везёт впереди идущий грузовик