基于遗传算法的Flappy Bird AI无性能提升问题求助
Flappy Bird遗传算法训练无提升问题排查
以下是针对你的代码和训练无提升问题的几个关键排查点:
1. 适应度计算函数未实现(核心问题)
你提到适应度是小鸟存活时长的帧计数,但未提供calculateFitness()的实现代码。如果这个函数没有正确计算并赋值每只鸟的fitness属性,会导致:
- 轮盘赌选择(
rouletteWheelSelection)中的totalFitness为0或无意义值,选择逻辑完全随机,无法筛选出优秀个体。 - 遗传算法失去进化压力,种群无法向更优方向迭代。
修正建议:
实现calculateFitness(),比如将存活帧数作为原始适应度,再进行归一化处理(避免个别高适应度个体垄断选择):
const calculateFitness = () => { // 计算总存活帧数 let totalFrames = savedBirds.reduce((sum, bird) => sum + bird.score, 0); // 归一化适应度,让总和为1(可选,也可以用原始值但需确保差距合理) savedBirds.forEach(bird => { bird.fitness = bird.score / totalFrames; }); }
注:需确保每只鸟的score属性在存活期间正确累加帧计数
2. 交叉函数存在数组越界与参数不匹配问题
你的crossOver函数中,用同一个splitPoint对所有权重和偏置数组进行交叉,但不同参数数组的长度差异极大:
weights_ih.data:4输入×4隐藏层 = 16个元素weights_ho.data:4隐藏层×1输出 = 4个元素bias_h.data:4个元素(隐藏层偏置)bias_o.data:1个元素(输出层偏置)
当splitPoint大于4时,循环中访问weights_ho.data[i]、bias_h.data[i]会越界,导致:
- 代码报错(可能被忽略)
- 错误地覆盖不存在的数组位置,破坏神经网络参数
- 偏置
bias_o永远不会被交叉(因为splitPoint几乎必然大于0,而它只有1个元素)
修正建议:
为不同的参数数组单独生成交叉点,避免越界:
const crossOver = (bird1, bird2) => { let offspring = bird1.brain.copy(); // 输入层到隐藏层权重交叉 const ihSplit = Math.floor(Math.random() * offspring.weights_ih.data.length); for (let i = ihSplit; i < offspring.weights_ih.data.length; i++) { offspring.weights_ih.data[i] = bird2.brain.weights_ih.data[i]; } // 隐藏层到输出层权重交叉 const hoSplit = Math.floor(Math.random() * offspring.weights_ho.data.length); for (let i = hoSplit; i < offspring.weights_ho.data.length; i++) { offspring.weights_ho.data[i] = bird2.brain.weights_ho.data[i]; } // 隐藏层偏置交叉 const hBiasSplit = Math.floor(Math.random() * offspring.bias_h.data.length); for (let i = hBiasSplit; i < offspring.bias_h.data.length; i++) { offspring.bias_h.data[i] = bird2.brain.bias_h.data[i]; } // 输出层偏置交叉(单独处理,因为只有1个元素) if (Math.random() > 0.5) { offspring.bias_o.data[0] = bird2.brain.bias_o.data[0]; } return offspring; }
3. 轮盘赌选择的边界处理缺陷
当totalFitness为0时(比如所有鸟的fitness未正确赋值),Math.random() * totalFitness会得到0,此时循环中fitnessSum累加后可能永远无法满足fitnessSum >= rand,导致函数返回undefined,进而在crossOver中报错。
修正建议:
在rouletteWheelSelection开头增加边界判断,确保至少能返回一个个体:
const rouletteWheelSelection = () => { let totalFitness = savedBirds.reduce((sum, bird) => sum + bird.fitness, 0); // 处理总适应度为0的情况,随机返回一个个体 if (totalFitness === 0) { return savedBirds[Math.floor(Math.random() * savedBirds.length)]; } const rand = Math.random() * totalFitness; let fitnessSum = 0; for (let i = 0; i < savedBirds.length; i++) { fitnessSum += savedBirds[i].fitness; if (fitnessSum >= rand) return savedBirds[i]; } // 兜底:如果循环结束未返回,返回最后一个个体 return savedBirds[savedBirds.length - 1]; }
4. 神经网络输入与输出的合理性问题
- 输入范围异常:如果管道的
top值为负数(超出屏幕顶部),closest.top / height会得到负数,而神经网络通常期望输入在0-1范围内,可能导致模型无法有效学习。建议将输入调整为相对位置,比如小鸟到管道顶部的距离除以高度:inputs[1] = (this.y - closest.top) / height; // 小鸟与顶部管道的相对距离 inputs[2] = (closest.bottom - this.y) / height; // 小鸟与底部管道的相对距离 - 输出阈值与激活函数匹配:如果神经网络输出层使用sigmoid激活,0.5是中间值,但如果初始权重随机范围过大,可能导致输出持续偏向0或1。可以尝试调整变异幅度,或者将阈值改为动态值(比如基于种群平均输出)。
5. 种群保存逻辑的潜在漏洞
在draw函数中,当管道超出屏幕时执行pipes.splice(i, 1),但此时嵌套的鸟类碰撞判断循环可能未完成所有鸟的检测。建议将管道删除逻辑移到鸟类碰撞检测之外:
function draw() { if (counter % 150 == 0) { pipes.push(new Pipe()); } counter++; // 先处理管道更新和鸟类碰撞 for (var i = pipes.length - 1; i >= 0; i--) { pipes[i].update(); for (let j = birds.length - 1; j >= 0; j--) { if (pipes[i].hits(birds[j])) { savedBirds.push(birds.splice(j, 1)[0]); } } } // 单独处理管道删除,避免嵌套循环中的索引混乱 for (var i = pipes.length - 1; i >= 0; i--) { if (pipes[i].offscreen()) { pipes.splice(i, 1); } } // 剩下的逻辑不变 for (let bird of birds) { bird.think(pipes); bird.update(); } if (birds.length == 0 ) { counter = 0; nextGeneration(); pipes = []; } background(bgImg); for (let bird of birds) { bird.show(); } for (let pipe of pipes) { pipe.show(); } }
内容的提问来源于stack exchange,提问作者Nathace
相关产品推荐
相关产品推荐

