Python NEAT вызывает ошибку после удаления всей популяции

Вопрос или проблема

Я пытаюсь создать аккуратную торговую среду, но когда я убиваю всё население при определённых условиях, возникает эта ошибка, и эволюция не продолжается.

Ошибка:

Traceback (most recent call last):
  File "C:\Users\Administrator\TradEvolver\main.py", line 188, in <module>
    run("./config.txt")
  File "C:\Users\Administrator\TradEvolver\main.py", line 186, in run
    winner = p.run(main, 1000)
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python310\lib\site-packages\neat\population.py", line 94, in run
    if best is None or g.fitness > best.fitness:
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python310\lib\site-packages\pandas\core\generic.py", line 1577, in __nonzero__
    raise ValueError(
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

Код:

import pandas as pd
import random
import time
import matplotlib.pyplot as plt
import threading
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import messagebox
import sys
import neat

def calculate_profit(buy_price, sell_price, balance):
    c = 0.001
    profit = (sell_price - buy_price) / buy_price * balance - (balance * c)
    new_balance = balance + profit
    return new_balance

class Trader:
    def __init__(self):
        self.balance = 10000
        self.buy_price = 0
        self.since_last_transaction = 0
        self.consq = []
        self.consq2 = []

    def buy(self, buy_price):
        self.buy_price = buy_price

    def sell(self, sell_price):
        self.balance = calculate_profit(self.buy_price, sell_price, self.balance) 
        self.buy_price = 0

file = "./data/BTCUSDT_2015-01-01T00_00_00+00_00__2024-10-10T23_59_59+00_00.csv"
df = pd.read_csv(file)

root = tk.Tk()
root.title("TradEvolver - Evolution Tracker")

def on_close():
    if messagebox.askyesno("Подтверждение выхода", "Вы уверены, что хотите закрыть программу?"):
        root.quit()
        sys.exit()

root.protocol("WM_DELETE_WINDOW", on_close)

traders = []

for i in range(10):
    traders.append(Trader())

close_data = []

root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)

fig, ax = plt.subplots()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")

balance_frame = tk.Frame(root)
balance_frame.grid(row=1, column=0, sticky="nsew")

balance_labels = []

def update_plot():
    for close in df['close']:
        close_data.append(close)
        ax.clear()
        ax.plot(close_data, color="blue")
        ax.set_title('TradEvolver - Evolution Tracker')
        ax.set_xlabel('Время')
        ax.set_ylabel('Цена закрытия')

        for i, trader in enumerate(traders):
            action = random.choice(['buy', 'sell', 'hold'])
            if action == 'buy' and trader.buy_price == 0 :
                trader.buy(close)
            elif action == 'sell' and trader.buy_price != 0:
                trader.sell(close)

            balance_labels[i].config(text=f"Баланс: {trader.balance:.2f}")

        canvas.draw()
        #time.sleep(0.01)

def start_plotting():
    plot_thread = threading.Thread(target=update_plot)
    plot_thread.start()

    root.mainloop()

def main(genomes, config):
    nets = []
    ge = []
    traders = []

    for _, g in genomes:
        net = neat.nn.FeedForwardNetwork.create(g, config)
        nets.append(net)
        traders.append(Trader())
        g.fitness = 0
        ge.append(g)

    for trader in traders:
        label = tk.Label(balance_frame, text=f"Баланс: {trader.balance:.2f}")
        label.pack()
        balance_labels.append(label)

    for x, close in enumerate(df['close']):
        close_data.append(close)
        ax.clear()
        ax.plot(close_data, color="blue")
        ax.set_title('TradEvolver - Evolution Tracker')
        ax.set_xlabel('Время')
        ax.set_ylabel('Цена закрытия')            

        canvas.draw()

        if x > 7 and len(traders) > 0:
            for i, trader in enumerate(traders):
                istooconseq = False

                ehe = 1 if close > df["close"][x - 7] else 0

                output = nets[i].activate((close, trader.buy_price, df["close"][x] - df["open"][x], df["volume"][x], ehe))
                action = output.index(max(output))

                if action == 0 and trader.buy_price == 0:  # Купить
                    trader.buy(close)
                    trader.since_last_transaction = 0
                    trader.consq2.append(0)
                elif action == 1: #Держать
                    trader.since_last_transaction += 1
                    trader.consq2.append(0)
                elif action == 2 and trader.buy_price != 0:  # Продать
                    trader.since_last_transaction = 0
                    trader.sell(close)
                    trader.consq2.append(1)
                else:
                    trader.consq2.append(0)
                    trader.since_last_transaction += 1

                if trader.buy_price != 0:
                    ge[i].fitness += ((df["close"] - df["open"])/df["open"])*100

                max_index, max_trader = max(enumerate(traders), key=lambda x: x[1].balance)

                #print(max_trader.balance, max_trader.since_last_transaction, max_trader.buy_price)
                if x > 150:
                    trader.consq = trader.consq2[x-150:x]
                    istooconseq = trader.consq.count(1) > 60
                    #print(trader.consq.count(1))

                if trader.since_last_transaction > 250 or trader.balance < 5500 or istooconseq:
                    traders.pop(i)
                    ge.pop(i)
                    nets.pop(i)

                    if trader.balance < 5500:
                        print("слишком низкий баланс", i, trader.balance)
                    elif trader.since_last_transaction > 250:
                        print("не делает сделок", i, trader.since_last_transaction)
                    elif istooconseq:
                        print("слишком много последовательных", i, trader.consq.count(1))

                istooconseq = False

            #   balance_labels[i].config(text=f"Баланс: {trader.balance:.2f}")

            canvas.draw()

        if len(traders) == 0:
            break

def run(config_path):
    config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, config_path)

    p = neat.Population(config)
    p.add_reporter(neat.StdOutReporter(True))
    stats = neat.StatisticsReporter()
    p.add_reporter(stats)

    winner = p.run(main, 1000)

run("./config.txt")

То, что я не понял, так это то, что я тестировал аналогичный алгоритм NEAT для flippybird, и когда птица сталкивается с трубой, она погибала. Вот код

import pygame
import random
import neat
import os
import time
pygame.font.init()

WIN_WIDTH = 500
WIN_HEIGHT = 800

BIRD_IMGS = [pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "bird1.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "bird2.png"))), pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "bird3.png")))]
PIPE_IMG = pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "pipe.png")))
BASE_IMG = pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "base.png")))
BG_IMG = pygame.transform.scale2x(pygame.image.load(os.path.join("imgs", "bg.png")))

STAT_FONT = pygame.font.SysFont("comicsans", 50)

class Bird:
    IMGS = BIRD_IMGS
    MAX_ROTATION = 25
    ROT_VEL = 20
    ANIMATION_TIME = 5

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.tilt = 0
        self.tick_count = 0
        self.vel = 0
        self.height = self.y
        self.img_count = 0
        self.img = self.IMGS[0]

    def jump(self):
        self.vel = -10.5
        self.tick_count = 0
        self.height = self.y

    def move(self):
        self.tick_count += 1

        d = self.vel*self.tick_count + 1.5*self.tick_count**2

        if d >= 16:
            d = 16

        if d < 0:
            d  -= 2

        self.y = self.y + d

        if d < 0 or self.y < self.height + 50:
            if self.tilt < self.MAX_ROTATION:
                self.tilt = self.MAX_ROTATION

        else:
             if self.tilt > -90:
                self.tilt -= self.ROT_VEL

    def draw(self, win):
        self.img_count += 1

        if self.img_count < self.ANIMATION_TIME:
            self.img = self.IMGS[0]
        elif self.img_count < self.ANIMATION_TIME*2:
            self.img = self.IMGS[1]
        elif self.img_count < self.ANIMATION_TIME*3:
            self.img = self.IMGS[2]     
        elif self.img_count < self.ANIMATION_TIME*4:
            self.img = self.IMGS[1]
        elif self.img_count == self.ANIMATION_TIME*4 + 1:
            self.img = self.IMGS[0]
            self.img_count = 0

        if self.tilt <= -80:
            self.img = self.IMGS[1]
            self.img_count = self.ANIMATION_TIME*2

        rotated_image = pygame.transform.rotate(self.img, self.tilt)
        new_rect = rotated_image.get_rect(center=self.img.get_rect(topleft = (self.x, self.y)).center)
        win.blit(rotated_image, new_rect.topleft)

    def get_mask(self):
        return pygame.mask.from_surface(self.img)

class Pipe:
    GAP = 200
    VEL = 10

    def __init__(self, x):
        self.x = x
        self.height = 0

        self.top = 0
        self.bottom = 0
        self.PIPE_TOP = pygame.transform.flip(PIPE_IMG, False, True)
        self.PIPE_BOTTOM = PIPE_IMG

        self.passed = False
        self.set_height()

    def set_height(self):
        self.height = random.randrange(50, 450)
        self.top = self.height - self.PIPE_TOP.get_height()
        self.bottom = self.height + self.GAP

    def move(self):
        self.x -= self.VEL

    def draw(self, win):
        win.blit(self.PIPE_TOP, (self.x, self.top))
        win.blit(self.PIPE_BOTTOM, (self.x, self.bottom))

    def collide(self, bird):
        bird_mask = bird.get_mask()
        top_mask = pygame.mask.from_surface(self.PIPE_TOP)
        bottom_mask = pygame.mask.from_surface(self.PIPE_BOTTOM)

        top_offset = (self.x - bird.x, self.top - round(bird.y))
        bottom_offset = (self.x - bird.x, self.bottom - round(bird.y))

        b_point = bird_mask.overlap(bottom_mask, bottom_offset)
        t_point = bird_mask.overlap(top_mask, top_offset)

        if t_point or b_point:
            return True

        return False

class Base:
    VEL = 5
    WIDTH = BASE_IMG.get_width()
    IMG = BASE_IMG

    def __init__(self, y):
        self.y = y
        self.x1 = 0
        self.x2 = self.WIDTH

    def move(self):
        self.x1 -= self.VEL
        self.x2 -= self.VEL

        if self.x1 + self.WIDTH < 0:
            self.x1 = self.x2 + self.WIDTH

        if self.x2 + self.WIDTH < 0:
            self.x2 = self.x1 + self.WIDTH

    def draw(self, win):
        win.blit(self.IMG, (self.x1, self.y)) 
        win.blit(self.IMG, (self.x2, self.y))

def draw_window(win, birds, pipes, base, score):
    win.blit(BG_IMG, (0,0))

    for pipe in pipes:
        pipe.draw(win)

    text = STAT_FONT.render("Счет: " + str(score), 1, (255,255,255))
    win.blit(text, (WIN_WIDTH -10 - text.get_width(), 10))
    base.draw(win)

    for bird in birds:
        bird.draw(win)

    pygame.display.update()

def main(genomes, config):
    nets = []
    ge = []
    birds = []

    for _, g in genomes:
        net = neat.nn.FeedForwardNetwork.create(g, config)
        nets.append(net)
        birds.append(Bird(230,350))
        g.fitness = 0
        ge.append(g)

    base = Base(730)
    pipes = [Pipe(600)]
    win = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
    run = True
    clock = pygame.time.Clock()
    score = 0

    while run:
        clock.tick(30)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                pygame.quit()
                quit()

        pipe_ind = 0

        if len(birds) > 0:
            if len(pipes) > 1 and birds[0].x > pipes[0].x + pipes[0].PIPE_TOP.get_width():
                pipe_ind = 1
        else:
            run = False
            break

        for x, bird in enumerate(birds):
            bird.move()
            ge[x].fitness += 0.1

            output = nets[x].activate((bird.y, abs(bird.y - pipes[pipe_ind].height), abs(bird.y - pipes[pipe_ind].bottom)))

            if output[0] > 0.5:
                bird.jump()

        #bird.move()
        rem = []
        add_pipe = False

        for pipe in pipes:
            for x, bird in enumerate(birds):
                if pipe.collide(bird):
                    ge[x].fitness -= 1
                    birds.pop(x)
                    nets.pop(x)
                    ge.pop(x)

                if not pipe.passed and pipe.x < bird.x:
                    pipe.passed = True
                    add_pipe = True

            if pipe.x + pipe.PIPE_TOP.get_width() < 0:
                rem.append(pipe)

            pipe.move()

        if add_pipe:
            score += 1

            for g in ge:
                g.fitness += 5

            pipes.append(Pipe(600))

        for r in rem:
            pipes.remove(r)

        for x, bird in enumerate(birds):
            if bird.y + bird.img.get_height() >= 730 or bird.y < 0:
                birds.pop(x)
                nets.pop(x)
                ge.pop(x)

        base.move()
        draw_window(win, birds, pipes, base, score)

def run(config_path):
    config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, config_path)

    p = neat.Population(config)
    p.add_reporter(neat.StdOutReporter(True))
    stats = neat.StatisticsReporter()
    p.add_reporter(stats)

    winner = p.run(main, 50)

if __name__ == "__main__":
    local_dir = os.path.dirname(__file__)
    config_path = os.path.join(local_dir, "config.txt")
    run(config_path)

В основном, я пытался реализовать такую же логику, но возникает ошибка в моей среде, тогда как алгоритм flippybird работает идеально.

Ответ или решение

Ошибка в NEAT Python после удаления всей популяции: решение и объяснение

Введение

Проблема, с которой вы столкнулись, имеет отношение к обработке популяции в вашем NEAT алгоритме для торговли. Ошибка, указанная в вашем сообщении (ValueError: The truth value of a Series is ambiguous...), возникает, когда код пытается выполнить логическую операцию на объекте, который не может быть интерпретирован как истинный или ложный. Это случается, если объекты, с которыми вы работаете, являются недопустимыми или имеют неожиданное состояние.

Анализ ошибки

Ваша ошибка происходит в следующем фрагменте кода:

if best is None or g.fitness > best.fitness:

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

Причины возникновения проблемы

  1. Удаление всех трейдеров: Когда вы удаляете всех трейдеров из списка traders, а также из списков нейронных сетей (nets) и геномов (ge), вы можете избежать дальнейшей обработки, вызывая break в вашем коде. Однако функция, которая продолжает выполнение, возможно, не ожидает, что списки окажутся пустыми.

  2. Необходимость проверки состояния популяции: Необходимо добавить проверки состояния прежде, чем пытаться оценить или получить доступ к элементам списков, которые могут быть пустыми.

Решение проблемы

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

  1. Проверка и обработка пустой популяции:
    Внесите изменения в цикл, который удаляет трейдеров:

    if len(traders) == 0:
       print("Все трейдеры удалены, завершаем эволюцию.")
       break

    Перед тем, как вызывать g.fitness > best.fitness, убедитесь, что ge не пуст:

    if len(ge) > 0:
       # Здесь ваш код для обработки фитнеса
    else:
       print("Список геномов пуст, прекращаем текущую итерацию.")
       continue
  2. Обработка геномов в main функции:
    Убедитесь, что так же как вы проверяете на наличие трейдеров, вы также проверяете, есть ли еще геномы, с которыми нужно работать. Если список ge пуст, завершайте функцию нормально.

  3. Исключение микро-управлений:
    Пересмотрите логику вашего кода, чтобы избежать обращения к пустым или неопределенным переменным, которые могут привести к ошибкам.

Заключение

Ошибки, связанные с пустыми списками и объектами, являются обычным делом в разработке на Python, особенно в контексте сложных систем, таких как NEAT. Будьте внимательны к состоянию вашей популяции и не забывайте проверять списки на пустоту перед выполнением операций, которые могут вызвать ошибки. С применением предложенных модификаций ваш алгоритм должен начать работать корректно без неожиданных сбоев.

Оцените материал
Добавить комментарий

Капча загружается...