В чём ошибка при обучении нейронной сети с помощью q-обучения?

Поставил себе цель, вникнуть в q-обучение для нейронных сетей. Для простоты решил сделать бота для игры в пинг-понг. Для начала написал класс для работы с нейронными сетями:

#dnn.py
from os import error
import numpy as np

def activation(x):
    return (np.exp(2 * x ) - 1) / (np.exp(2 * x) + 1)

def activation_prime(fx):
    return 1 - fx * fx

class NeuralNetwork:
    def __init__(self, neuros_in_layers:list):
        self.layers = []
        for i in range(0, len(neuros_in_layers) - 1):
            neurons_num = neuros_in_layers[i+1]
            weights_num = neuros_in_layers[i]
            self.layers.append(2*np.random.random((weights_num, neurons_num)) - 1)

    def forward(self, input):
        input = np.array([input])
        output = input
        for layer in self.layers:
            output = activation(np.dot(output,layer))
        return output

    def learn(self, inputs, outputs, epochs, teta=0.1):
        for epoch in range(epochs):
            # print('================================================================')
            inputs = np.array(inputs)
            outputs = np.array(outputs)
            outs = []
            outs.append(inputs)
            for layer in self.layers:
                outs.append(activation(np.dot(outs[-1], layer)))
            error = outputs - outs[-1]

            deltas = [error * activation_prime(outs[-1])]
            # if epoch == 0:
            #     print("Error:" + str(np.mean(np.abs(error))))
            errors = [error]
            for i in range(len(self.layers) - 1, -1, -1):
                layer = self.layers[i]
                errors.append(
                    deltas[-1].dot(layer.T)
                )
                deltas.append(errors[-1] * activation_prime(outs[i]))

            # меняем веса
            for i, layer in enumerate(reversed(self.layers)):
                layer += teta*outs[len(self.layers) - i - 1].T.dot(deltas[i])

Проверил сеть на функции XOR - всё работает. Далее, написал логику для взаимодействия бота и нейронной сети:

#pongbrain.py
import numpy as np
import random as rd

from dnn import NeuralNetwork

# Input:
# ballx, bally, ballvx, ballvy, paddlex
# variations: 4 * 1024 * 1024 * 768
class PongBrain:
    def __init__(self):
        self.qfunction = NeuralNetwork([6, 6, 6, 1])
        self.no_action = 0
        self.left = -1
        self.right = 1
        self.epsilon = 1
        self.delta_epsilon = 0.9999
        self.gamma = 0.1
        self.q_t = 0


    def get_action(self, ballx, bally, ballvx, ballvy, paddlex):
        # q_value = self.qfunction.forward([ballx, bally, ballvx, ballvy, paddlex])[0][0]
        if rd.random() < self.epsilon:
            action = rd.choice([-1, 0, 1])
        else:
            action = max([-1, 0, 1], key=lambda action: self.qfunction.forward([ballx, bally, ballvx, ballvy, paddlex, action])[0][0])
        return action

    def learn(self, reward, current_state, current_action, next_state, next_action):
        if reward != 0:
            print(f'{reward=}')
        next_action = max([-1, 0, 1], key=lambda action: self.qfunction.forward(next_state + [action])[0][0])
        q_hat = self.qfunction.forward(next_state + [next_action])
        learn_output = np.array(q_hat * self.gamma + reward)
        learn_input = [np.array(current_state + [current_action])]
        self.qfunction.learn(learn_input, learn_output, 1)
        self.epsilon *= self.delta_epsilon

    


Логика такая. При запросе действия возвращается либо случайное действие, либо действие, для которого Q функция будет максимальна. Чем дальше тренировка, тем ниже вероятность того, что будет взято случайное действие.

Для обучения я по формуле q_hat * self.gamma + reward получаю значение Q функции.

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

Я пробовал менять коэффициенты в нейронной сети и q функции, менять количество слоёв и нейронов, в пределах от -1 до +1 менять вознаграждения, ибо выход Q функции ограничен этими значениями. Ничего не помогало.

На всякий случай ниже код самой игры. Чтобы всё заработало, нужно поставить pygame, numpy и Python версии 3.8+

#pong.py
import pygame
import random
import sys

from pongbrain import PongBrain

pygame.init()
WIDTH = 300
HEIGHT = 200
screen = pygame.display.set_mode([WIDTH, HEIGHT])
running = True
brain = PongBrain()

PLAYER = 1
AI = 2

RULLER = AI

FPS = 1000

score = 0

score_font = pygame.font.Font(None, 20)
score_pos = [10, 10]

last_reward = 0
current_reward = 0
current_action = 0
next_action = 0
current_state = [0, 0, 0, 0, 0]
next_state = [0, 0, 0, 0, 0]


class Ball(pygame.sprite.Sprite):
    radius = 10
    def __init__(self):
        self.x_speed = 5
        self.y_speed = 5
        self.rect = pygame.Rect(0, 0, self.radius * 2, self.radius * 2)
        self.rect.center = (WIDTH / 2, HEIGHT / 2)

        self.x_speed *= random.choice([-1, 1])
        self.y_speed *= random.choice([-1, 1])

        self.was_reflect = False # чтобы не было бага с застреванием мяча в подложке

    def update(self):
        global score, current_reward
        if self.rect.left + self.x_speed <= 0 or self.rect.right + self.x_speed >= WIDTH:
            self.x_speed *= -1
            self.was_reflect = False
        if self.rect.top + self.y_speed <= 0 or self.rect.bottom + self.y_speed >= HEIGHT:
            if self.rect.bottom + self.y_speed >= HEIGHT:
                score -= 1
                current_reward = -1
            self.y_speed *= -1
            self.was_reflect = False
        self.rect.centerx += self.x_speed
        self.rect.centery += self.y_speed

    def draw(self, screen):
        pygame.draw.circle(screen, (0, 0, 255), self.rect.center, self.radius)

class Paddle:
    def __init__(self):
        self.width = 90
        self.height = 10
        self.speed = 90
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.bottom, self.rect.centerx = HEIGHT - self.height / 2, WIDTH / 2

    def move_left(self):
        if self.rect.left - self.speed > 0:
            self.rect.left -= self.speed
    
    def move_right(self):
        if self.rect.right + self.speed < WIDTH:
            self.rect.right += self.speed

    def draw(self, screen):
        pygame.draw.rect(screen, (255, 0, 0), self.rect)

clock = pygame.time.Clock()

balls = [Ball()]
paddle = Paddle()

while running:
    last_reward = current_reward
    current_reward = 0
    for event in pygame.event.get():
        if event.type == pygame.QUIT or (
            event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
        ):
            running = False
    keys = pygame.key.get_pressed()
    screen.fill((255, 255, 255))
    ball = balls[0]
    if ball.rect.colliderect(paddle.rect):
        bottom_penetration = ball.rect.bottom - paddle.rect.top
        left_penetration = ball.rect.right - paddle.rect.left
        right_penetration = paddle.rect.right - ball.rect.left
        if bottom_penetration > left_penetration or bottom_penetration > right_penetration:
            if not ball.was_reflect:
                ball.x_speed *= -1
                ball.was_reflect = True
        else:
            if not ball.was_reflect:
                score += 1
                current_reward = 1
                ball.y_speed *= -1
                ball.was_reflect = True
        
    ball.update()
    ball.draw(screen)
    score_surf = score_font.render(f"{score=}, eps={round(brain.epsilon, 5)}, delta={brain.delta_epsilon}", 1, (255, 0, 0))
    screen.blit(score_surf, score_pos)
    if RULLER == PLAYER:
        if keys[pygame.K_d]:
            paddle.move_right()
        if keys[pygame.K_a]:
            paddle.move_left()
    if RULLER == AI:
        x, y = ball.rect.center
        current_state = next_state
        next_state = [x / 1000, y / 1000, ball.x_speed / 1000, ball.y_speed / 1000, paddle.rect.centerx / 1000]
        action = brain.get_action(x / 1000, y / 1000, ball.x_speed / 1000, ball.y_speed / 1000, paddle.rect.centerx / 1000)
        current_action = next_action
        if action == -1:
            paddle.move_left()
        if action == 1:
            paddle.move_right()
        next_action = action
        brain.learn(last_reward, current_state, current_action, next_state, next_action)
    paddle.draw(screen)
    pygame.display.flip()
    clock.tick(FPS)

print('========================Нейросеть========================')
print('[')
for layer in brain.qfunction.layers:
    print(repr(layer)+',')
print(']')
pygame.quit()
sys.exit(0)

P.S. Я новичок на сайте, в искусственном интеллекте и далеко не сильный программист. Поэтому, прошу не бить палками за косяки, а если и бить, то не сильно. В любом случае буду рад услышать любое мнение. Спасибо


Ответы (1 шт):

Автор решения: Vladimir Kurbatov

После некоторого времени, проведенного в поисках я понял в чём проблема. Дело в том, что уравнение Белмана для нейронной сети отличается от стандартного дополнительным множителем, который значительно меньше нуля. В моём случае это было 10 в -36 степени. Подробнее: https://habr.com/ru/post/443240/

P.S. В ходе разработки обнаружился целый ворох проблем. Q функция должна была быть Q(s,a) = r + y*maxQ(s', a) где y строго меньше единицы. Кроме того, свою роль сыграло количество слоёв. В начале, когда было 3 слоя, бот забивался в угол и не понимал, что делать. После увеличения слоёв до 10, программа начала стабильно выигрывать

→ Ссылка