selector.js 5.51 KB
/**
 * Selector — 精英保留 + 锦标赛选择
 *
 * 选择策略:
 *   1. 精英保留: 每代 top N 直接晋级 (不被淘汰)
 *   2. 锦标赛选择: 随机抽 k 个,选最优者存活
 *   3. 弱者淘汰: 不满足存活条件的标记 is_alive=false
 */
const evolutionQueries = require('../queries').evolutionQueries;

// ════════════════════════════════════════════════════════
// 选择策略
// ════════════════════════════════════════════════════════

/**
 * 精英保留 — 按适应度保留 top N
 *
 * @param {object[]} genomes - 基因组数组
 * @param {number} count - 保留数量
 * @returns {string[]} 精英 genome IDs
 */
function selectElites(genomes, count = 2) {
  const sorted = [...genomes]
    .filter(g => g.overall_fitness !== null && g.overall_fitness !== undefined)
    .sort((a, b) => (b.overall_fitness || 0) - (a.overall_fitness || 0));

  return sorted.slice(0, count).map(g => g.id);
}

/**
 * 锦标赛选择 — 每轮随机抽 k 个,取最优
 *
 * @param {object[]} genomes - 候选基因组
 * @param {number} survivors - 存活者数量
 * @param {number} tournamentSize - 每轮参赛者数量
 * @returns {string[]} 存活者 genome IDs
 */
function tournamentSelect(genomes, survivors, tournamentSize = 3) {
  const selected = new Set();
  const pool = [...genomes];

  while (selected.size < survivors && pool.length > 0) {
    // 随机抽 k 个
    const k = Math.min(tournamentSize, pool.length);
    const candidates = [];
    const indices = new Set();

    while (candidates.length < k) {
      const idx = Math.floor(Math.random() * pool.length);
      if (!indices.has(idx)) {
        indices.add(idx);
        candidates.push(pool[idx]);
      }
    }

    // 选最优
    candidates.sort((a, b) => (b.overall_fitness || 0) - (a.overall_fitness || 0));
    const winner = candidates[0];
    selected.add(winner.id);

    // 从池中移除赢家(避免重复选中)
    const winnerIdx = pool.findIndex(g => g.id === winner.id);
    if (winnerIdx >= 0) pool.splice(winnerIdx, 1);
  }

  return [...selected];
}

/**
 * 多样性保留 — 确保不同 Skill 至少各有一个存活
 *
 * @param {object[]} genomes - 候选基因组
 * @returns {string[]} 每个 Skill 的最佳 genome ID
 */
function diversityPreserve(genomes) {
  const bySkill = {};
  for (const g of genomes) {
    if (!bySkill[g.skill_name]) bySkill[g.skill_name] = [];
    bySkill[g.skill_name].push(g);
  }

  const preserved = [];
  for (const pool of Object.values(bySkill)) {
    pool.sort((a, b) => (b.overall_fitness || 0) - (a.overall_fitness || 0));
    preserved.push(pool[0].id);
  }

  return preserved;
}

// ════════════════════════════════════════════════════════
// 选择执行器
// ════════════════════════════════════════════════════════

/**
 * 对一个 run 的当代群体执行选择
 *
 * @param {string} runId
 * @param {object} options
 * @param {number} options.eliteCount - 精英保留数量 (默认 2)
 * @param {number} options.survivalRate - 存活率 0-1 (默认 0.5)
 * @param {number} options.tournamentSize - 锦标赛大小 (默认 3)
 */
async function selectGeneration(runId, options = {}) {
  const {
    eliteCount = 2,
    survivalRate = 0.5,
    tournamentSize = 3,
  } = options;

  const run = await evolutionQueries.getRun(runId);
  if (!run) throw new Error(`Run ${runId} not found`);

  const genomes = await evolutionQueries.getGenomesByRun(runId, {
    generation: run.current_generation,
    aliveOnly: true,
  });

  if (genomes.length <= eliteCount) {
    console.log(`  ⏭️  种群 ${genomes.length} ≤ 精英数 ${eliteCount},跳过选择`);
    return { survivors: genomes.length, killed: 0, elites: genomes.length };
  }

  const targetSurvivors = Math.max(
    eliteCount,
    Math.ceil(genomes.length * survivalRate)
  );

  // 1. 精英保留
  const eliteIds = selectElites(genomes, eliteCount);
  for (const id of eliteIds) {
    await evolutionQueries.markElite(id);
  }

  // 2. 多样性保留
  const diversityIds = diversityPreserve(genomes);

  // 3. 锦标赛选择(排除已选中的)
  const alreadySelected = new Set([...eliteIds, ...diversityIds]);
  const remaining = genomes.filter(g => !alreadySelected.has(g.id));
  const slotsLeft = Math.max(0, targetSurvivors - alreadySelected.size);
  const tournamentWinners = tournamentSelect(remaining, slotsLeft, tournamentSize);

  // 汇总存活者
  const survivorIds = new Set([...eliteIds, ...diversityIds, ...tournamentWinners]);

  // 4. 淘汰
  const killedIds = genomes
    .filter(g => !survivorIds.has(g.id))
    .map(g => g.id);

  if (killedIds.length > 0) {
    await evolutionQueries.killGenomes(killedIds);
  }

  const result = {
    survivors: survivorIds.size,
    killed: killedIds.length,
    elites: eliteIds.length,
    diversityPreserved: diversityIds.length,
    tournamentWinners: tournamentWinners.length,
  };

  console.log(`  🏆 精英: ${result.elites} | 多样性: ${result.diversityPreserved} | 锦标赛: ${result.tournamentWinners} | 淘汰: ${result.killed}`);

  return result;
}

module.exports = {
  selectElites,
  tournamentSelect,
  diversityPreserve,
  selectGeneration,
};