返回

NEAT进化错误:种群清空后的分析与解决

python

NEAT 进化中清空种群后的错误分析与解决

当使用 Python NEAT 库进行进化算法开发时,有时可能会遇到这样的情况:在某些条件触发下,整个种群被删除,导致程序报错并停止进化。 这篇博文分析了这个问题的原因,并提供了相应的解决方案。

错误原因分析

报错信息 ValueError: The truth value of a Series is ambiguous. 表明在比较过程中使用了 Pandas Series 对象,但是 Pandas 不支持直接对 Series 对象使用布尔运算符 (>, <, == 等)。在 NEAT 的 population.py 模块中,对适应度进行比较时(if best is None or g.fitness > best.fitness:),如果 g.fitness 返回的是 Pandas Series,就会产生上述错误。

进一步观察,问题发生的时间点是所有种群个体都被移除之后,当种群被清空后,由于没有存活的个体,best 变量可能会返回 None 值,这时候NEAT内部试图寻找fitness,由于fitness值为pandas.Series,它无法被判断大小比较。NEAT默认认为有一个适应度最高(Best Fitness)的值去更新其信息。这时进行的大小判断,在Pandas DataFrame/Series上比较,无法解析出大小关系,引发了 ValueError

同时, NEAT 内部在判断 if best is None 的时候没有及时处理空的最佳值而导致的错误。因为在代码层面,if语句会隐式将一个布尔操作返回给最佳值判断,而pandas.Series无法隐式进行类型转化,所以会导致逻辑判断错误。

简单来讲, 导致此问题发生的原因是,种群因为某种原因被清空(可能设置了一些比较激进的删除规则),然后NEAT无法处理为空的种群和相关变量。

解决方案

解决这类问题的思路主要有以下几种:

方案一:确保种群在进化中不为空

这个方案着重避免种群为空,可以确保进化过程不会中断。

  • 删除种群成员时谨慎: 检查所有移除种群个体的条件。 在实际代码中,检查导致所有交易员被移除的代码逻辑(trader.since_last_transaction > 250 or trader.balance < 5500 or istooconseq)。确保移除条件不是过于激进,以避免整个种群过早被删除。可以对交易员移除条件进行微调,比如降低 since_last_transactionbalance 的阈值。
  • 保留精英个体: 在每次进化中,保留一小部分适应度较高的个体,不进行删除。这样可以确保即使大部分个体表现不佳被删除,至少还会有一些有竞争力的个体存活,以引导下一代的进化。
  • 重新引入随机个体: 当种群个体数量过低时,可以考虑引入一定数量的随机个体。 这确保了即便种群规模缩减,NEAT 也依然可以运作。
  • 逐步缩小删除条件 当发现程序即将删除全部个体,应有容错逻辑,防止出现空值情况,比如当个体数量接近删除全部时,减少删除数量,当仅剩余一个个体的时候则跳过删除逻辑。
   for x, close in enumerate(df['close']):
       ...

        if x > 7 and len(traders) > 0:
            for i, trader in enumerate(reversed(traders)):
                
                index = len(traders) - 1 -i

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

                output = nets[index].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:  # Buy
                    trader.buy(close)
                    trader.since_last_transaction = 0
                    trader.consq2.append(0)
                elif action == 1: #Hold
                    trader.since_last_transaction += 1
                    trader.consq2.append(0)
                elif action == 2 and trader.buy_price != 0:  # Sell
                    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[index].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 len(traders) > 1 and (trader.since_last_transaction > 250 or trader.balance < 5500 or istooconseq) :
                    traders.pop(index)
                    ge.pop(index)
                    nets.pop(index)

                    if trader.balance < 5500:
                        print("too low balance", index, trader.balance)
                    elif trader.since_last_transaction > 250:
                        print("not making trades", index, trader.since_last_transaction)
                    elif istooconseq:
                        print("too consequtive", index, trader.consq.count(1))

                istooconseq = False

            #   balance_labels[i].config(text=f"Balance: {trader.balance:.2f}")
            
            canvas.draw()

        if len(traders) == 0:
            break

在这个代码片段中,如果只剩一个 traders,就会跳过删除,从而避免种群为空的问题。同时为了代码安全性,for i, trader in enumerate(reversed(traders)) 被使用了 reversed 遍历列表来规避pop产生的数组下标错乱问题。

方案二:调整NEAT种群选择逻辑,处理空种群情况

  • 检查 best 的有效性: 在NEAT的 population.py 文件中,修改 if best is None or g.fitness > best.fitness: 代码逻辑,确保在 best 为 None 的时候,不会尝试使用 Series 值做大小判断,避免直接对 best.fitness 做判断, 添加安全检查。在main 函数内部修改,确保函数不会在找不到best时产生异常。
def main(genomes, config):
    ...
    try:
          for x, close in enumerate(df['close']):
           ...
    except Exception as e:
        print(f"Error: {e}")
        # 返回一些值 让neat继续运行
        return  None 
    

这个修改在try catch语句内部运行进化算法主逻辑,一旦出现问题直接捕获异常,然后跳过并继续进行下一代种群进化。这样做可以确保程序不会因为空种群直接终止运行。

  • 使用条件表达式: 使用类似 if best and g.fitness > best.fitness 的逻辑, 保证只有当 best 存在,而且g.fitness可以和best.fitness进行对比时才进行下一步比较,而不是直接做比较, 可以使用 try/catch 和逻辑条件组合, 如果存在最佳适应度函数, 就在有最佳适应度的基础上进行更新,如果不存在则直接进行下一轮。
   winner = None
   try:
        winner = p.run(main, 1000)

   except Exception as e:
         print(f"NEAT runtime Error {e}, it means all traders are eliminated") 
   finally: 
       # 进行退出或者一些记录

在此段代码里, 程序使用try语句尝试运行主进化函数,如果在main函数内部发生异常会被捕获, 并继续运行下一代的NEAT程序,不会中断整体运行。在finally里面可以做一些其他操作比如记录等,用于分析代码失败的原因。

安全建议

  • 细致的日志记录: 在关键代码部分添加日志,输出每次迭代中种群大小,以及每个个体适应度和被删除的原因,方便问题追溯和代码逻辑完善。
  • 详细注释: 为每个代码段添加注释,以便在未来查看或修改代码时更容易理解。 尤其在有风险删除种群的代码片段,更应该进行注释解释。
  • 测试覆盖率: 添加测试用例覆盖不同的情况,包括边界情况。例如,特别模拟种群被快速清空的情况,以及高适应度和低适应度个体,保证即使存在少量存活个体的情况下也能完成正常更新,覆盖所有潜在的错误路径,保证整体运行的安全性。

总结

当 NEAT 种群出现 ValueError: The truth value of a Series is ambiguous. 时, 这通常是由于在种群清空后, NEAT 尝试对空的或非法 fitness 值进行操作导致的。 通过优化种群删除策略, 使用错误处理逻辑和增加对异常值的判断和预防,即可解决这类问题。 代码调整需在具体应用场景进行修改, 以上策略提供了一些可行的方向,应按需选择和组合使用, 提高 NEAT 进化算法的稳定性和健壮性。

在实践中, 务必结合实际应用需求和代码逻辑,灵活使用这些方案,才能更好地应对进化过程中的挑战。