Нейронная сеть учится понимать сигналы светофора

в 12:48, , рубрики: TensorFlow, машинное обучение, нейронные сети, светофор

Введение

Сегодня нейронные сети всё чаще используются для решения различных задач – от распознавания лиц до управления беспилотными автомобилями. Однако для тех, кто только начинает знакомиться с этой технологией, может показаться, что процесс обучения нейросети — это что-то сложное и непонятное.

Задача проста: у нас есть светофор, и мы хотим научить модель решать, можно ли продолжать движение на перекрестке в зависимости от того, какой сигнал светофора горит. В статье использованы базовые принципы машинного обучения и простой код на 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) # Вызов функции построения графика

Нейронная сеть учится понимать сигналы светофора - 1

Теперь получена модель, которая уже может распознавать сигналы светофора и предсказывать, разрешено ли движение. Однако, важным аспектом машинного обучения является возможность дальнейшего улучшения модели, тестирования и экспериментов.

Тестирование модели

Проверим, как созданная нейронная сеть работает на тех же данных, которые использовались для обучения. Протестируем ее, проведем прямой проход через сеть и посмотрим на предсказания.

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

})

Нейронная сеть учится понимать сигналы светофора - 2

Метрики качества модели

Основные метрики для оценки качества модели в задачах классификации:

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 эпох:

Нейронная сеть учится понимать сигналы светофора - 3

Можно еще продолжить на 3000 эпох:

Нейронная сеть учится понимать сигналы светофора - 4

Теперь результаты качества модели:

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.

Вот результат такого изменения:

Нейронная сеть учится понимать сигналы светофора - 5
Нейронная сеть учится понимать сигналы светофора - 6

Результаты очевидны:

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

В остальном код без изменений.

Результаты:

Нейронная сеть учится понимать сигналы светофора - 7
Нейронная сеть учится понимать сигналы светофора - 8

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()

Нейронная сеть учится понимать сигналы светофора - 9

Модель обучается на данных, используя небольшие батчи (по 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)

Нейронная сеть учится понимать сигналы светофора - 10

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]   # Ничего не горит: разрешено

])

С их помощью пытаемся предсказать, как модель будет интерпретировать такие необычные показания светофора.

Интереснее всего посмотреть на таблицу с результатами прогноза модели.

Нейронная сеть учится понимать сигналы светофора - 11

Из таблицы видно, что правильные метки, когда горит только зеленый или красный сигнал светофора, модель с высокой вероятностью классифицировала действие. А вот на других сигналах модель начинает сомневаться и не знает что ей делать, но все же в таких случаях, когда светофор вообще без сигналов, модель говорит, что ехать можно, что вполне разумно.

Оценка качества модели теми же метриками:

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. На простом примере со светофором показали, как нейронная сеть обучается, разобрали принципы работы сети на интуитивном уровне и проанализировали её поведение при столкновении с необычными, нелогичными сигналами.

Эксперименты продемонстрировали, что, несмотря на некоторую сложность задач, нейронные сети могут эффективно распознавать корректные комбинации сигналов и предоставлять полезные результаты. Однако тестирование на «чокнутом светофоре» также выявило ограничения модели, которые дают основания для дальнейшего улучшения её точности и устойчивости. Далее можно продолжить исследования, направленные на улучшение архитектуры моделей, использование больших объемов данных и развитие более устойчивых алгоритмов к аномалиям.

Источники

  1. Trask, A. (2019). Глубокое обучение. Погружение в мир нейронных сетей с Keras. Питер.

  2. TensorFlow Documentation. (n.d.). Sequential Model. https://www.tensorflow.org/guide/keras/sequential_model

  3. 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

  4. Habr.com. (n.d.). Машинное обучение и нейронные сети. https://habr.com/ru/hub/machine_learning/

Автор: AV_Tar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js