Код доступен здесь.

В части 1 этой серии мы получили скрипт на Python, чтобы видеть мир и (несмотря на это) ехать прямо, как правило, в стены. В этой части мы заставим водителя действовать немного умнее.

Когда дело доходит до автономных гонок, есть масса вариантов, и в теории легко запутаться. Начнем с простой парадигмы машинного обучения: обучения с учителем. Это означает, что мы будем обучать водителя, сначала показывая ему, как мы водим, а затем позволяя ему повторно наблюдать за нашим вождением, чтобы он мог скопировать наш стиль вождения и применить его сам.

Прежде чем мы начнем, нам нужно выбрать модель. Простой ответ для любого классификатора изображений — сверточная нейронная сеть. Это тип модели машинного обучения, которая особенно хороша для классификации изображений, и это именно то, что мы делаем — мы берем изображение, а затем решаем, двигаться ли вперед, влево, вправо или тормозить/назад.

Когда вы создаете нейронную сеть, у вас есть много вариантов настройки модели: сколько слоев, сколько нейронов, сколько сверточных фильтров, размер шага… список можно продолжить. Для нашей первой попытки мы постараемся уменьшить количество параметров. В колледже меня учили, что вы всегда хотите чрезмерно ограничивать модель, то есть иметь больше обучающих примеров, чем параметров. Обучающим примером в нашем случае является один снимок экрана из игры. Количество параметров модели определяется архитектурой нейронной сети, и оно может быстро исчисляться миллионами даже для довольно простых архитектур. Давайте попробуем удержать нашу цифру примерно до 10 000. Наш игровой цикл работает со скоростью около 20 кадров в секунду, поэтому мы можем сделать как минимум 10 000 снимков экрана чуть более чем за 8 минут.

Это определяется с помощью keras:

model = keras.Sequential([
        keras.Input(shape=input_shape),
         layers.Conv2D(2, kernel_size=(30, 30), activation="relu"),
         layers.Flatten(),
         layers.Dropout(0.2),
         layers.Dense(num_outputs, activation="sigmoid"),
         layers.Dense(num_outputs, activation="sigmoid"),
     ])

С уменьшенной входной формой (300, 400, 1), что соответствует изображению в градациях серого 400x300, это дает нам около 13 000 параметров. Довольно близко к нашему идеалу ~ 10 000.

Полный обучающий код доступен в файле train_model.py.

Вау, мы сильно опередили себя. У нас есть прекрасная модель, но нам все еще нужно собрать данные для обучения. Полный код доступен в gather_training_data.py, но я дам вам краткую идею.

Нам нужно захватить ввод модели (т.е. экран) и вывод модели (т.е. наши нажатия клавиш). Мы установим его в цикле, в котором мы опрашиваем экран и наш ввод и сохраняем каждый в массиве. Затем, когда скрипт завершит работу, мы сохраним оба массива в файлы, чтобы мы могли получить доступ к данным при обучении нашей модели.

Перед запуском цикла мы создадим два массива (технически списки Python, но идея та же):

screen_captures = []
labels = []

Затем внутри цикла мы захватим нужные нам данные и сохраним их в массивах:

while frames_processed < MAX_FRAMES:
    frame_number = frames_processed
        
    # grab screen
    screen = np.array(ImageGrab.grab(bbox=(0, 40, 800, 640)))
    # get user input, then store as labelled data
    user_input = utils.get_user_input()
    
    screen_captures.append(processed_screen)
    labels.append(user_input[:4])

utils.get_user_input() использует модуль с именем keyboard для захвата ввода пользователя, а затем кодирует его по той же схеме, что описана в части 1 этой серии, с добавлением пробела и c для приостановки и выхода из обучения.

import keyboard
def get_user_input():
    """
    Returns np.array with boolean values for whether w, a, s, d, space are pressed.
    indices: 0=w, 1=a, 2=s, 3=d, 4=space, 5=c
    """
    bool_list = [keyboard.is_pressed("w"), keyboard.is_pressed("a"),
                 keyboard.is_pressed("s"), keyboard.is_pressed("d"),
                 keyboard.is_pressed("space"), keyboard.is_pressed("c")]
    float_array = np.array([float(1.0) if k else float(0.0) for k in bool_list])
    
    return float_array

Теперь просто запустите collect_training_data.py, пока катаетесь в своей видеоигре. Время от времени контролируйте вывод, чтобы убедиться, что все выглядит правильно. Теперь, когда у нас есть данные для обучения, мы вернемся и обучим модель с помощью train_model.py, прежде чем перейти к хорошей части: позволить гонщику участвовать в гонке!

Код этой части находится в run_supervised.py.

model = keras.models.load_model('models/basic_cnn')
    model.summary()
    # game loop
    while frames_processed < MAX_FRAMESt:
        frame_number = frames_processed
        
        # grab screen
        screen = np.array(ImageGrab.grab(bbox=(0, 40, 800, 640)))
        # process image and display resulting image
        processed_screen = utils.process_image(screen)
        cv2.imshow('window', processed_screen)
        user_input = utils.get_user_input()
            
        # get model prediction 
        model_input = np.expand_dims(np.array(processed_screen), -1).reshape((1, 300, 400, 1))
        prediction = model.predict(model_input)[0]
        prediction_str = " ".join([f"{p:2.2}" for p in prediction])
        print(prediction_str)
        # send input
        utils.send_input(prediction)

И вот мы идем! Просто запустите скрипт, чтобы увидеть, как работает ваш водитель!

Благодаря этой архитектуре и примерно 15-минутным тренировочным данным я заставил ИИ в основном передвигаться по трассе в Grid Autosport. Это не сумасшедший быстрый гонщик, но обычно он некоторое время едет прямо и рулит, когда видит приближающийся поворот. Тем не менее, он часто застревает. В следующий раз мы рассмотрим трюки, чтобы заставить водителя водить машину немного умнее.