NEAT进化错误:种群清空后的分析与解决
2024-12-27 10:38:52
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_transaction
和balance
的阈值。 - 保留精英个体: 在每次进化中,保留一小部分适应度较高的个体,不进行删除。这样可以确保即使大部分个体表现不佳被删除,至少还会有一些有竞争力的个体存活,以引导下一代的进化。
- 重新引入随机个体: 当种群个体数量过低时,可以考虑引入一定数量的随机个体。 这确保了即便种群规模缩减,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 进化算法的稳定性和健壮性。
在实践中, 务必结合实际应用需求和代码逻辑,灵活使用这些方案,才能更好地应对进化过程中的挑战。