GitHub avatar

Fox's Blog

I evolved a neural network through natural selection instead of gradient descent

How I replaced classic gradient descent training with a NSGA-II genetic algorithm to evolve DQN trading agents: four versions, from overfitting to Lamarckian weight evolution.

The problem with gradient descent alone

Training a DQN agent for algorithmic trading with classic gradient descent has a simple problem to state and a hard one to solve: gradient descent optimizes one network toward one local minimum, on one market window. Nothing guarantees this minimum generalizes to a different market regime, and nothing in the training loop pushes for diversity; two runs starting from different seeds often converge to nearly identical strategies, with the same blind spots.

The answer I explored: replace (or rather overlay) gradient descent with a genetic algorithm. Instead of training one agent, you evolve a population of agents; each genome encodes an architecture and hyperparameters; and natural selection does the sorting, while gradient descent keeps fine-tuning each individual within its own lifetime.

This runner went through four versions in a single intensive session. Each version fixed a structural flaw in the previous one.

v1: the naive version, and why it wasn't enough

The first version did what you'd expect from a basic GA: a population of genomes, a fitness function, selection, crossover, mutation, next generation. Each genome encoded the network topology (number of layers, width), DQN hyperparameters (learning rate, epsilon decay, replay buffer size), and a few architectural choices (which data sources to consume, what embedding size).

The main flaw: fitness was computed on the same data used for training. An agent could literally memorize a market window and get an excellent score without having learned a generalizable strategy. Classic overfitting, but amplified by genetic selection; the GA actively selects individuals that best exploit this loophole.

v2: separating training and evaluation

The obvious fix was to separate the phases: each genome trains on one market window, then is evaluated on a different window, never seen during training. Only the evaluation performance counts toward fitness.

This change alone caused the average population fitness to drop; a sign that a large portion of what looked like performance in v1 was pure memorization. Painful to see, but it's exactly the signal you want: a lower but honest score is better than an inflated, misleading one.

v3: moving to NSGA-II and multi-objective fitness

Optimizing a single fitness score (say, returns) mechanically pushes agents toward taking extreme risks to maximize that single number. The solution was switching to NSGA-II (Non-dominated Sorting Genetic Algorithm II), which simultaneously optimizes several objectives without reducing them to an arbitrary weighted sum: returns, maximum drawdown, Sharpe ratio, inter-window stability.

NSGA-II builds a Pareto front: the set of genomes for which no improvement on one objective is possible without degrading another. Instead of forcing a single return-risk trade-off through a pre-chosen weighting, you keep the entire compromise frontier and leave the final choice open.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... subsequent front construction by iterative removal
  return fronts;
}

Second addition in v3: a persistent Pareto archive. Without it, a good genome found at generation 12 can disappear by generation 15 if crossover luck doesn't reproduce it; even if it remained better than everything that replaced it. The archive keeps, across all generations, the set of all non-dominated individuals ever encountered, regardless of the current population.

v4: Lamarckian evolution and environmental diversity

V3 had a structural blind spot: the genome described the architecture, but the weights learned during training disappeared at each new generation. A child born from crossover of two good parents inherited their architecture, but had to relearn from scratch; no trace of the weights that had made its parents performant.

V4 introduces Lamarckian evolution: trained weights are fed back into the genome after training, and transmitted (with mutation) to the offspring. This is deliberate biological heresy; Lamarck was wrong for living organisms -- inheritance of acquired characteristics doesn't exist in biology -- but nothing stops a digital GA from cheating intelligently: here, transmitting acquired knowledge radically accelerates convergence, since each generation restarts from an already-informed initialization rather than random weights.

Three other structural changes in this version:

  • Environmental diversity: each genome is no longer evaluated on a single market window but on several, drawn from different regimes (bullish, bearish, ranging). An agent that excels on one window and collapses on another can no longer dominate the Pareto front.

  • FLOPs complexity regularization: the network's computational cost (in FLOPs) becomes a full objective in NSGA-II. This prevents evolution from converging to massive architectures simply because they have more raw capacity, without a justified performance gain.

  • Decoupled RLBackend interface: the GA no longer knows DQN details. It manipulates a genome and calls train() / evaluate() through an abstract interface, which theoretically allows swapping in another RL algorithm without touching the evolutionary engine.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Last technical point: evaluation switched to bounded async concurrency; a pool of N parallel evaluations instead of a sequential loop, with an explicit limit to avoid saturating available GPU/CPU resources.

What v4 fixes versus v3 in practice

V3 flaw V4 fix Weights lost each generation Lamarckian re-injection of trained weights Overfitting to a single market window Evaluation on multiple windows, varied regimes Architectures growing unconstrained FLOPs as explicit Pareto objective GA coupled to DQN details Abstract RLBackend interface Slow sequential evaluation Bounded async concurrency

V4 also fixed ten concrete API "grounding" bugs; cases where the GA code assumed an interface for TradingAgent that didn't exactly match the real implementation. This kind of bug is invisible until you confront the code against the actual agent source: v4 was only validated after a line-by-line re-reading against the real file.

Why mix evolution and gradient rather than choose one

You might wonder why not just use pure RL, or pure evolution like NEAT. The answer is one sentence: gradient is excellent for local fine-tuning (adjusting continuous weights toward a nearby optimum), evolution is excellent for global exploration (discovering architectures and hyperparameter combinations no gradient can reach, because the discrete search space isn't differentiable). Using one without the other means depriving yourself of one of the two forms of exploration.

The price is engineering complexity; four versions weren't a luxury, they were the number of iterations needed for the GA + RL loop to stop sabotaging itself (overfitting, loss of good individuals, loss of acquired weights). But the result is a system that explores a much wider design space than a simple grid search of hyperparameters, while keeping the local efficiency of gradient descent for each evaluated candidate.

Next step

This single-level evolutionary architecture (a flat population of DQN genomes) reaches its limits when the number of assets to cover grows. That's what motivated the move to a three-level hierarchical architecture (Asset Analysts → Sector Managers → Portfolio Allocator), with a GA operating independently at each level... but that's the subject of another article.

J'ai fait évoluer un réseau de neurones par sélection naturelle plutôt que par descente de gradient

Comment j'ai remplacé l'entraînement classique par descente de gradient par un algorithme génétique NSGA-II pour faire évoluer des agents DQN de trading : quatre versions, du surapprentissage à l'évolution lamarckienne des poids.

Le problème avec la descente de gradient seule

Entraîner un agent DQN pour du trading algorithmique avec la descente de gradient classique pose un problème simple à énoncer et difficile à résoudre : le gradient optimise un réseau vers un minimum local, sur une fenêtre de marché. Rien ne garantit que ce minimum généralise à un régime de marché différent, et rien dans la boucle d'entraînement ne pousse vers la diversité; deux runs qui partent de graines différentes convergent souvent vers des stratégies presque identiques, avec les mêmes angles morts.

La réponse que j'ai explorée : remplacer (ou plutôt superposer) la descente de gradient avec un algorithme génétique. Au lieu d'entraîner un agent, on fait évoluer une population d'agents; chacun un génome encodant une architecture et des hyperparamètres; et on laisse la sélection naturelle faire le tri, pendant que le gradient continue de peaufiner chaque individu à l'intérieur de sa propre vie.

Ce runner a traversé quatre versions en une seule session de travail intensive. Chacune a corrigé un défaut structurel de la précédente.

v1 : la version naïve, et pourquoi elle ne suffisait pas

La première version faisait ce qu'on attend d'un GA basique : une population de genomes, une fonction de fitness, sélection, croisement, mutation, génération suivante. Chaque genome encodait la topologie du réseau (nombre de couches, largeur), les hyperparamètres DQN (learning rate, epsilon decay, taille du replay buffer), et quelques choix architecturaux (quelles sources de données consommer, quelle taille d'embedding).

Le défaut principal : la fitness était calculée sur les mêmes données que l'entraînement. Un agent pouvait littéralement mémoriser une fenêtre de marché et obtenir un score excellent sans avoir appris une stratégie généralisable. Classique surapprentissage, mais amplifié par la sélection génétique; le GA sélectionne activement les individus qui exploitent le mieux cette faille.

v2 : séparer entraînement et évaluation

La correction évidente était de séparer les phases : chaque genome s'entraîne sur une fenêtre de marché, puis est évalué sur une fenêtre différente, jamais vue pendant l'entraînement. Seule la performance en évaluation compte pour la fitness.

Ce changement seul a fait chuter la fitness moyenne de la population; signe qu'une bonne partie de ce qui semblait être de la performance en v1 était de la mémorisation pure. Douloureux à voir, mais c'est exactement le signal qu'on veut : un score plus bas mais honnête vaut mieux qu'un score gonflé et trompeur.

v3 : passer à NSGA-II et une fitness multi-objectif

Optimiser un seul score de fitness (disons le rendement) pousse mécaniquement vers des agents qui prennent des risques extrêmes pour maximiser ce seul chiffre. La solution a été de passer à NSGA-II (Non-dominated Sorting Genetic Algorithm II), qui optimise simultanément plusieurs objectifs sans les réduire à une somme pondérée arbitraire : rendement, drawdown maximal, ratio de Sharpe, stabilité inter-fenêtres.

NSGA-II construit un front de Pareto : l'ensemble des genomes pour lesquels aucune amélioration sur un objectif n'est possible sans dégrader un autre. Plutôt que de forcer un compromis unique entre rendement et risque via une pondération choisie à l'avance, on garde toute la frontière de compromis et on laisse le choix final ouvert.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... construction des fronts suivants par retrait itératif
  return fronts;
}

Deuxième ajout de la v3 : une archive de Pareto persistante. Sans elle, un bon genome trouvé à la génération 12 peut disparaître à la génération 15 si le hasard du croisement ne le reproduit pas; même s'il restait meilleur que tout ce qui l'a remplacé. L'archive conserve, à travers toutes les générations, l'ensemble des individus non dominés jamais rencontrés, indépendamment de la population courante.

v4 : évolution lamarckienne et diversité environnementale

La v3 avait un angle mort structurel : le génome décrivait l'architecture, mais les poids appris pendant l'entraînement disparaissaient à chaque nouvelle génération. Un descendant né du croisement de deux bons parents héritait de leur architecture, mais devait réapprendre depuis zéro; aucune trace des poids qui avaient rendu ses parents performants.

La v4 introduit l'évolution lamarckienne : les poids entraînés sont réinjectés dans le génome après l'entraînement, et transmis (avec mutation) à la descendance. C'est une hérésie biologique assumée; Lamarck avait tort pour les organismes vivants, l'hérédité des caractères acquis n'existe pas en biologie; mais rien n'empêche un GA numérique de tricher intelligemment : ici, transmettre l'acquis accélère radicalement la convergence, puisque chaque génération repart d'un point d'initialisation déjà informé plutôt que de poids aléatoires.

Trois autres changements structurels dans cette version :

  • Diversité environnementale : chaque genome n'est plus évalué sur une seule fenêtre de marché mais sur plusieurs, tirées de régimes différents (haussier, baissier, range). Un agent qui excelle sur une fenêtre et s'effondre sur une autre ne peut plus dominer le front de Pareto.

  • Régularisation par complexité FLOPs : le coût de calcul du réseau (en FLOPs) devient un objectif à part entière dans NSGA-II. Ça évite que l'évolution ne converge vers des architectures massives simplement parce qu'elles ont plus de capacité brute, sans que ce soit justifié par le gain de performance.

  • Interface RLBackend découplée : le GA ne connaît plus les détails de DQN. Il manipule un genome et appelle train() / evaluate() à travers une interface abstraite, ce qui permet en théorie de brancher un autre algorithme RL sans toucher au moteur évolutif.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Dernier point technique : l'évaluation est passée en concurrence asynchrone bornée; un pool de N évaluations en parallèle plutôt qu'une boucle séquentielle, avec une limite explicite pour ne pas saturer les ressources GPU/CPU disponibles.

Ce que corrige la v4 par rapport à la v3 en pratique

Défaut v3 Correction v4 Poids perdus à chaque génération Réinjection lamarckienne des poids entraînés Surapprentissage à une seule fenêtre de marché Évaluation sur fenêtres multiples, régimes variés Architectures qui grossissent sans contrainte FLOPs comme objectif Pareto explicite GA couplé aux détails de DQN Interface RLBackend abstraite Évaluation séquentielle lente Concurrence async bornée

La v4 a aussi corrigé dix bugs concrets de "grounding" API; des cas où le code du GA supposait une interface pour TradingAgent qui ne correspondait pas exactement à l'implémentation réelle. Ce genre de bug est invisible tant qu'on ne confronte pas le code au véritable code source de l'agent : la v4 n'a été validée qu'après relecture ligne à ligne face au fichier réel.

Pourquoi mélanger évolution et gradient plutôt que choisir l'un ou l'autre

On pourrait se demander pourquoi ne pas simplement faire du RL pur, ou de l'évolution pure façon NEAT. La réponse tient en une phrase : le gradient est excellent pour affiner localement (ajuster des poids continus vers un optimum proche), l'évolution est excellente pour explorer globalement (découvrir des architectures et des combinaisons d'hyperparamètres qu'aucun gradient ne peut atteindre, parce que l'espace de recherche discret n'est pas différentiable). Utiliser l'un sans l'autre, c'est se priver d'une des deux formes d'exploration.

Le prix à payer est la complexité d'ingénierie; quatre versions n'étaient pas un luxe, c'était le nombre d'itérations nécessaires pour que la boucle GA + RL cesse de se saboter elle-même (surapprentissage, perte de bons individus, perte de poids acquis). Mais le résultat est un système qui explore un espace de conception bien plus large qu'un simple grid search d'hyperparamètres, tout en gardant l'efficacité locale du gradient pour chaque candidat évalué.

Prochaine étape

Cette architecture évolutive à un seul niveau (une population plate de genomes DQN) atteint ses limites quand le nombre d'actifs à couvrir grimpe. C'est ce qui a motivé le passage à une architecture hiérarchique à trois niveaux (Asset Analysts → Sector Managers → Portfolio Allocator), avec un GA opérant indépendamment à chaque niveau... mais ça, c'est le sujet d'un autre article.

我通过自然选择进化了一个神经网络,而不是梯度下降

如何用NSGA-II遗传算法取代经典的梯度下降训练来进化DQN交易智能体:从过拟合到拉马克权重进化的四个版本

单独使用梯度下降的问题

使用经典梯度下降训练用于算法交易的DQN智能体有一个简单的问题陈述和一个难以求解的难题:梯度下降在一个市场窗口上将_一个_网络优化到_一个_局部最小值。没有任何东西能保证这个最小值能泛化到不同的市场制度,训练循环中也完全没有推动多样性的机制;从不同随机种子开始的两次运行通常会收敛到几乎相同的策略,带着相同的盲点。

我探索的答案是:用遗传算法取代(或者说覆盖)梯度下降。不是训练一个智能体,而是进化一群智能体;每个基因组编码一个架构和超参数;自然选择进行排序,而梯度下降则在每个个体的生命周期内持续微调。

这个项目在一次密集的会话中经历了四个版本。每个版本都修复了前一个版本的结构性缺陷。

v1:朴素版本,以及为什么它不够

第一个版本做的就是基本GA该做的事:一群基因组,一个适应度函数,选择、交叉、变异、下一代。每个基因组编码了网络拓扑结构(层数、宽度)、DQN超参数(学习率、epsilon衰减、回放缓冲区大小),以及一些架构选择(使用哪些数据源、嵌入大小)。

主要缺陷:适应度是在用于训练的同一份数据上计算的。一个智能体完全可以记住一个市场窗口,在没有学到可泛化策略的情况下获得优秀分数。典型的过拟合,但被遗传选择放大了;GA会主动选择最擅长利用这个漏洞的个体。

v2:分离训练和评估

明显的修复是分离各阶段:每个基因组在一个市场窗口上训练,然后在另一个从未在训练中见过的不同窗口上评估。只有评估性能计入适应度。

这一改变导致种群平均适应度下降;这意味着v1中看起来像性能的很大一部分实际上是纯粹的 memorize(记忆)。看着很痛苦,但这正是你想要的信号:一个较低但诚实的分数要好过一个膨胀的、误导性的分数。

v3:转向NSGA-II和多目标适应度

优化单一适应度分数(比如收益率)会机械地推动智能体承担极端风险来最大化这个单一数值。解决方案是切换到NSGA-II(非支配排序遗传算法II),它同时优化多个目标,而不将它们简化为任意加权和:收益率、最大回撤、夏普比率、跨窗口稳定性。

NSGA-II构建了一个帕累托前沿:一组基因组,其中任何一个目标的改进都会导致另一个目标的退化。不是通过预先选择的权重强制一个单一的收益-风险权衡,而是保留整个折中前沿,将最终选择留空。

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... 后续前沿通过迭代移除构建
  return fronts;
}

v3中的第二个新增:一个持久化帕累托存档。没有它,在第12代找到的优秀基因组可能会在第15代消失,如果交叉运气没有复制它;即使它仍然比取代它的所有个体都优秀。存档会保留所有世代中遇到过所有非支配个体,无论当前种群如何。

v4:拉马克进化和环境多样性

V3有一个结构性盲点:基因组描述了架构,但在训练中学到的权重在每个新世代都会消失。从两个优秀父母交叉产生的子代继承了他们的架构,但必须从头重新学习;没有留下任何曾使父母表现优异的权重的痕迹。

V4引入了拉马克进化:训练后的权重被反馈回基因组,并(带有变异地)传递给后代。这是故意的生物学异端;拉马克对生物体来说是错的----获得性特征的遗传在生物学中不存在----但没有什么能阻止数字GA智能地作弊:在这里,传递已获得的知识极大地加速了收敛,因为每一代都从一个已经初始化的状态开始,而不是随机权重。

这个版本还有另外三个结构性变化:

  • 环境多样性:每个基因组不再在单个市场窗口上评估,而是在多个取自不同制度(看涨、看跌、盘整)的窗口上评估。一个在一个窗口上表现出色而在另一个窗口上崩溃的智能体不再能主导帕累托前沿。

  • FLOPs复杂度正则化:网络的计算成本(以FLOPs计)成为NSGA-II中的一个完整目标。这防止了进化仅仅因为更大架构具有更多原始容量就收敛到庞大架构,而没有任何合理的性能提升。

  • 解耦的RLBackend接口:GA不再知道DQN的具体细节。它操作一个基因组并通过抽象接口调用train() / evaluate(),这理论上允许在不触及进化引擎的情况下替换为另一个RL算法。

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

最后的技术点:评估切换到了有界异步并发;一个包含N个并行评估的池子代替了顺序循环,并设有显式限制以避免饱和可用的GPU/CPU资源。

v4在实践中修复了v3的哪些问题

V3的缺陷 V4的修复 每代丢失权重 拉马克式重新注入训练权重 对单一市场窗口过拟合 在多个窗口、不同制度上评估 架构无约束增长 FLOPs作为显式帕累托目标 GA与DQN细节耦合 抽象RLBackend接口 缓慢的顺序评估 有界异步并发

V4还修复了十个具体的API"接地"错误;即GA代码假设的TradingAgent接口与实际实现不完全匹配的情况。这种错误在将代码与实际智能体源代码对照之前是不可见的:v4只有在逐行对照真实文件重新阅读后才得到验证。

为什么混合进化和梯度而不是选择一个

你可能会想为什么不只使用纯RL,或像NEAT这样的纯进化。答案是一句话:梯度擅长局部微调(将连续权重调整到附近的最优点),进化擅长全局探索(发现架构和超参数组合,梯度无法达到,因为离散搜索空间不可微)。只使用其中一个意味着剥夺自己两种探索形式中的一种。

代价是工程复杂性;四个版本不是奢侈,而是GA+RL循环停止自我破坏(过拟合、优秀个体丢失、已获得权重丢失)所需的迭代次数。但结果是一个探索比简单超参数网格搜索更广泛设计空间的系统,同时为每个评估候选保留梯度下降的局部效率。

下一步

这种单层进化架构(一个扁平的DQN基因组种群)在需要覆盖的资产数量增长时会达到极限。这促使了向三层分层架构(资产分析师→行业经理→投资组合分配器)的转变,每层独立运行一个GA……但这是另一篇文章的主题了。

勾配降下法ではなく自然選択でニューラルネットワークを進化させた

NSGA-II遺伝的アルゴリズムで古典的な勾配降下法トレーニングを置き換え、DQNトレーディングエージェントを進化させた方法:過学習からラマルク的重み進化までの4つのバージョン

勾配降下法だけの問題

アルゴリズム取引のためのDQNエージェントを古典的な勾配降下法で訓練するには、単純な問題の提示と難しい解決策があります。勾配降下法は、_1つの_市場ウィンドウ上で_1つの_ネットワークを_1つの_極小値に向けて最適化します。この極小値が異なる市場レジームに一般化される保証はなく、訓練ループ内に多様性を促進する仕組みもありません。異なるシードから開始した2回の実行は、多くの場合、ほぼ同一の戦略と同じブラインドスポットに収束します。

私が探求した答えは、勾配降下法を遺伝的アルゴリズムで置き換える(というよりオーバーレイする)ことです。1つのエージェントを訓練する代わりに、エージェントの集団を進化させます。各ゲノムがアーキテクチャとハイパーパラメータをエンコードし、自然選択が選別を行い、勾配降下法は各個体のライフタイム内で微調整を続けます。

このランナーは、1回の集中セッションで4つのバージョンを経ました。各バージョンは前のバージョンの構造的な欠陥を修正しました。

v1:ナイーブなバージョン、そしてなぜ不十分だったか

最初のバージョンは、基本的なGAに期待されることを行いました。ゲノムの集団、適応度関数、選択、交叉、突然変異、次世代。各ゲノムは、ネットワークトポロジー(層数、幅)、DQNハイパーパラメータ(学習率、イプシロン減衰、リプレイバッファサイズ)、およびいくつかのアーキテクチャ上の選択(使用するデータソース、埋め込みサイズ)をエンコードしていました。

主な欠陥は、適応度が訓練に使用されたものと同じデータで計算されたことです。エージェントは、市場ウィンドウを文字通り記憶し、一般化可能な戦略を学習することなく優れたスコアを得ることができました。古典的な過学習ですが、遺伝的选择によって増幅されます。GAはこの抜け穴を最もよく利用する個体を積極的に選択します。

v2:訓練と評価の分離

明らかな修正はフェーズを分離することでした。各ゲノムは1つの市場ウィンドウで訓練され、その後、訓練中に一度も見たことのない別のウィンドウで評価されます。評価パフォーマンスのみが適応度にカウントされます。

この変更だけでも、集団の平均適応度は低下しました。これは、v1でパフォーマンスに見えたものの大部分が純粋な記憶であったことを示しています。見るのはつらいですが、これはまさに欲しいシグナルです。低くても正直なスコアは、水増しされた誤解を招くスコアよりはるかに優れています。

v3:NSGA-IIと多目的適応度への移行

単一の適応度スコア(例えばリターン)を最適化すると、機械的にエージェントをその単一の数値を最大化するために極端なリスクを取る方向に押しやります。解決策は、複数の目的を恣意的な重み付き和に還元することなく同時に最適化するNSGA-II(非優越ソーティング遺伝的アルゴリズムII)に切り替えることでした。リターン、最大ドローダウン、シャープレシオ、ウィンドウ間安定性。

NSGA-IIはパレートフロントを構築します。1つの目的の改善が別の目的を劣化させることなく行えないゲノムの集合です。事前に選択された重み付けを通じて単一のリターン-リスクトレードオフを強制する代わりに、妥協のフロンティア全体を保持し、最終的な選択をオープンにします。

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... 反復削除による後続フロントの構築
  return fronts;
}

v3での2つ目の追加要素は永続パレートアーカイブです。これがないと、世代12で見つかった優れたゲノムが、交叉の運がそれを複製しなければ世代15までに消えてしまう可能性があります。たとえそれが取って代わったすべてのものよりも優れていたとしてもです。アーカイブは、現在の集団に関係なく、全世代にわたってこれまでに遭遇したすべての非優越個体の集合を保持します。

v4:ラマルク進化と環境多様性

V3には構造的なブラインドスポットがありました。ゲノムはアーキテクチャを記述していましたが、訓練中に学習された重みは新しい世代ごとに消えていました。2つの優れた親から交叉で生まれた子は、そのアーキテクチャを継承しましたが、ゼロから再学習する必要がありました。親を高性能にした重みの痕跡はありません。

V4はラマルク進化を導入します。訓練された重みは訓練後にゲノムにフィードバックされ、子孫に(突然変異とともに)伝達されます。これは意図的な生物学上の異端です。ラマルクは生物については間違っていました。獲得形質の遺伝は生物学には存在しません。しかし、デジタルGAが賢くズルをすることを妨げるものは何もありません。ここでは、獲得した知識を伝達することで収束が劇的に加速されます。なぜなら、各世代はランダムな重みではなく、すでに情報を得た初期化から再開するからです。

このバージョンには他に3つの構造的変更があります。

  • 環境多様性:各ゲノムはもはや単一の市場ウィンドウではなく、異なるレジーム(強気、弱気、レンジ相場)から抽出された複数のウィンドウで評価されます。あるウィンドウで優れ、別のウィンドウで崩壊するエージェントは、もはやパレートフロントを支配できません。

  • FLOPs複雑性正則化:ネットワークの計算コスト(FLOPs単位)がNSGA-IIの完全な目的になります。これにより、正当なパフォーマンス向上なしに、より大きなアーキテクチャが単により多くの生の容量を持つという理由だけで、進化が巨大なアーキテクチャに収束するのを防ぎます。

  • 分離されたRLBackendインターフェース:GAはDQNの詳細を知りません。ゲノムを操作し、抽象インターフェースを通じてtrain() / evaluate()を呼び出します。これにより、進化エンジンに触れることなく別のRLアルゴリズムを理論的に交換できます。

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

最後の技術的ポイント:評価は制限付き非同期並行処理に切り替えられました。逐次ループの代わりにN個の並列評価のプールを使用し、利用可能なGPU/CPUリソースの飽和を避けるために明示的な制限を設けています。

v4が実際にv3の何を修正したか

V3の欠陥 V4の修正 重みが世代ごとに失われる ラマルク的訓練済み重みの再注入 単一市場ウィンドウへの過学習 複数ウィンドウ、多様なレジームでの評価 アーキテクチャの無制限な成長 明示的なパレート目的としてのFLOPs GAがDQN詳細に結合 抽象RLBackendインターフェース 低速な逐次評価 制限付き非同期並行処理

V4はまた、10の具体的なAPI"グラウンディング"バグを修正しました。GAコードが実際の実装と正確には一致しないTradingAgentのインターフェースを想定していたケースです。この種のバグは、コードを実際のエージェントソースと突き合わせるまで見えません。v4は、実際のファイルに対する行ごとの再読後にのみ検証されました。

進化と勾配の両方を混ぜる理由、どちらかを選ばない理由

純粋なRLだけ、またはNEATのような純粋な進化だけを使わないのはなぜかと疑問に思うかもしれません。答えは一言です。勾配は局所的な微調整(連続的な重みを近くの最適値に調整する)に優れ、進化は大域的な探索(勾配が到達できないアーキテクチャやハイパーパラメータの組み合わせを発見する、なぜなら離散探索空間は微分可能ではないため)に優れています。一方だけを使うことは、2つの探索形態のうち1つを自分から奪うことを意味します。

代償はエンジニアリングの複雑さです。4つのバージョンはぜいたくではなく、GA+RLループが自己妨害(過学習、優れた個体の喪失、獲得した重みの喪失)をやめるために必要な反復回数でした。しかし結果は、評価された各候補に対して勾配降下法の局所的な効率を維持しながら、単純なハイパーパラメータのグリッドサーチよりもはるかに広い設計空間を探索するシステムです。

次のステップ

この単一レベルの進化アーキテクチャ(平坦なDQNゲノム集団)は、カバーする資産の数が増えると限界に達します。これが、3レベルの階層的アーキテクチャ(資産アナリスト→セクターマネージャー→ポートフォリオアロケーター)への移行の動機となり、各レベルでGAが独立して動作します…しかし、それは別の記事のテーマです。

경사 하강법 대신 자연 선택을 통해 신경망을 진화시켰습니다

NSGA-II 유전 알고리즘으로 기존 경사 하강법 훈련을 대체하여 DQN 트레이딩 에이전트를 진화시킨 방법: 과적합에서 라마르크적 가중치 진화까지 네 가지 버전

경사 하강법 단독의 문제

알고리즘 트레이딩을 위한 DQN 에이전트를 기존 경사 하강법으로 훈련하는 것은 간단한 문제 진술과 풀기 어려운 과제를 가지고 있습니다. 경사 하강법은 하나의 시장 윈도우에서 하나의 네트워크를 하나의 지역 최솟값으로 최적화합니다. 이 최솟값이 다른 시장 체제에 일반화된다는 보장은 없으며, 훈련 루프에는 다양성을 촉진하는 메커니즘도 없습니다. 서로 다른 시드에서 시작하는 두 번의 실행은 종종 거의 동일한 전략과 동일한 사각지대에 수렴합니다.

제가 탐구한 답변은 경사 하강법을 유전 알고리즘으로 대체(또는 오버레이)하는 것입니다. 하나의 에이전트를 훈련하는 대신 에이전트 개체군을 진화시킵니다. 각 게놈은 아키텍처와 하이퍼파라미터를 인코딩하고, 자연 선택이 정렬을 수행하며, 경사 하강법은 각 개체의 수명 내에서 미세 조정을 계속합니다.

이 러너는 단 한 번의 집중 세션에서 네 가지 버전을 거쳤습니다. 각 버전은 이전 버전의 구조적 결함을 수정했습니다.

v1: 순진한 버전과 그것이 왜 충분하지 않았는지

첫 번째 버전은 기본 GA에서 기대하는 대로 수행했습니다. 게놈 개체군, 적합도 함수, 선택, 교차, 돌연변이, 다음 세대. 각 게놈은 네트워크 토폴로지(층 수, 너비), DQN 하이퍼파라미터(학습률, 엡실론 감쇠, 리플레이 버퍼 크기), 그리고 몇 가지 아키텍처 선택(사용할 데이터 소스, 임베딩 크기)을 인코딩했습니다.

주요 결함: 적합도가 훈련에 사용된 것과 동일한 데이터로 계산되었습니다. 에이전트는 말 그대로 시장 윈도우를 기억하고 일반화 가능한 전략을 학습하지 않고도 우수한 점수를 얻을 수 있었습니다. 전형적인 과적합이지만 유전적 선택에 의해 증폭됩니다. GA는 이 허점을 가장 잘 활용하는 개체를 적극적으로 선택합니다.

v2: 훈련과 평가의 분리

명백한 수정은 단계를 분리하는 것이었습니다. 각 게놈은 하나의 시장 윈도우에서 훈련된 후, 훈련 중에 한 번도 본 적 없는 다른 윈도우에서 평가됩니다. 오직 평가 성과만이 적합도에 반영됩니다.

이 변경만으로도 평균 개체군 적합도가 감소했습니다. 이는 v1에서 성과처럼 보였던 것의 상당 부분이 순수한 암기였음을 나타냅니다. 보기에는 괴롭지만, 이것이 바로 당신이 원하는 신호입니다. 낮지만 정직한 점수가 부풀려진 오해의 소지가 있는 점수보다 훨씬 낫습니다.

v3: NSGA-II 및 다목적 적합도로의 전환

단일 적합도 점수(예: 수익률)를 최적화하면 기계적으로 에이전트가 극단적인 위험을 감수하여 그 단일 숫자를 최대화하도록 밀어붙입니다. 해결책은 여러 목표를 임의의 가중 합으로 축소하지 않고 동시에 최적화하는 NSGA-II(비지배 정렬 유전 알고리즘 II)로 전환하는 것이었습니다: 수익률, 최대 손실폭, 샤프 비율, 윈도우 간 안정성.

NSGA-II는 파레토 프론트를 구축합니다. 하나의 목표를 개선하면 다른 목표가 저하되는 게놈들의 집합입니다. 사전에 선택된 가중치를 통해 단일 수익-위험 트레이드오프를 강제하는 대신, 전체 절충 프론티어를 유지하고 최종 선택을 열어둡니다.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... 반복적 제거에 의한 후속 프론트 구축
  return fronts;
}

v3의 두 번째 추가 사항: 영구 파레토 아카이브. 이것이 없으면 12세대에서 발견된 좋은 게놈이 교차 운이 그것을 재현하지 못하면 15세대까지 사라질 수 있습니다. 그것을 대체한 모든 것보다 여전히 우수하더라도 말입니다. 아카이브는 현재 개체군에 관계없이 모든 세대에 걸쳐 지금까지 만난 모든 비지배 개체의 집합을 유지합니다.

v4: 라마르크 진화와 환경 다양성

V3에는 구조적 사각지대가 있었습니다. 게놈은 아키텍처를 설명했지만 훈련 중에 학습된 가중치는 각 새 세대마다 사라졌습니다. 두 우수한 부모의 교차에서 태어난 자식은 그들의 아키텍처를 물려받았지만 처음부터 다시 학습해야 했습니다. 부모를 성능 좋게 만든 가중치의 흔적은 전혀 없었습니다.

V4는 라마르크 진화를 도입합니다. 훈련된 가중치는 훈련 후 게놈에 피드백되고 (돌연변이와 함께) 자손에게 전달됩니다. 이것은 의도적인 생물학적 이단입니다. 라마르크는 생물에 대해서는 틀렸습니다. 후천적 형질의 유전은 생물학에 존재하지 않습니다. 그러나 디지털 GA가 똑똑하게 속임수를 쓰는 것을 막을 수는 없습니다. 여기서 획득한 지식을 전달하면 수렴이 극적으로 가속화됩니다. 각 세대가 무작위 가중치가 아닌 이미 정보를 얻은 초기화에서 다시 시작하기 때문입니다.

이 버전에는 세 가지 다른 구조적 변경 사항이 있습니다.

  • 환경 다양성: 각 게놈은 더 이상 단일 시장 윈도우가 아닌 서로 다른 체제(강세, 약세, 범위)에서 가져온 여러 윈도우에서 평가됩니다. 한 윈도우에서는 뛰어나고 다른 윈도우에서는 붕괴하는 에이전트는 더 이상 파레토 프론트를 지배할 수 없습니다.

  • FLOPs 복잡도 정규화: 네트워크의 계산 비용(FLOPs 단위)이 NSGA-II의 완전한 목표가 됩니다. 이는 정당한 성능 향상 없이 더 큰 아키텍처가 단순히 더 많은 원시 용량을 가졌다는 이유만으로 진화가 거대한 아키텍처로 수렴하는 것을 방지합니다.

  • 분리된 RLBackend 인터페이스: GA는 더 이상 DQN 세부 사항을 알지 못합니다. 게놈을 조작하고 추상 인터페이스를 통해 train() / evaluate()를 호출합니다. 이를 통해 이론적으로 진화 엔진을 건드리지 않고 다른 RL 알고리즘을 교체할 수 있습니다.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

마지막 기술적 포인트: 평가는 제한된 비동기 동시성으로 전환되었습니다. 순차 루프 대신 N개의 병렬 평가 풀을 사용하고, 사용 가능한 GPU/CPU 리소스 포화를 피하기 위해 명시적 제한을 설정했습니다.

v4가 실제로 v3의 무엇을 수정했는가

V3의 결함 V4의 수정 각 세대마다 가중치 손실 라마르크식 훈련된 가중치 재주입 단일 시장 윈도우에 대한 과적합 여러 윈도우, 다양한 체제에서 평가 아키텍처 무제한 성장 명시적 파레토 목표로서의 FLOPs GA가 DQN 세부 사항에 결합 추상 RLBackend 인터페이스 느린 순차 평가 제한된 비동기 동시성

V4는 또한 10개의 구체적인 API "그라운딩" 버그를 수정했습니다. GA 코드가 실제 구현과 정확히 일치하지 않는 TradingAgent 인터페이스를 가정했던 경우입니다. 이러한 종류의 버그는 코드를 실제 에이전트 소스와 대조하기 전까지는 보이지 않습니다. v4는 실제 파일에 대한 줄 단위 재독 후에야 검증되었습니다.

진화와 경사를 모두 혼합하는 이유, 하나를 선택하지 않는 이유

순수 RL만 사용하거나 NEAT와 같은 순수 진화만 사용하지 않는 이유가 궁금할 수 있습니다. 답은 한 문장입니다. 경사는 지역 미세 조정(연속 가중치를 가까운 최적점으로 조정)에 탁월하고, 진화는 전역 탐색(이산 탐색 공간이 미분 가능하지 않기 때문에 경사가 도달할 수 없는 아키텍처와 하이퍼파라미터 조합 발견)에 탁월합니다. 하나만 사용하면 두 가지 탐색 형태 중 하나를 스스로 박탈하는 것을 의미합니다.

대가는 엔지니어링 복잡성입니다. 네 가지 버전은 사치가 아니라 GA+RL 루프가 자체 방해(과적합, 우수 개체 손실, 획득한 가중치 손실)를 멈추는 데 필요한 반복 횟수였습니다. 그러나 결과는 평가된 각 후보에 대해 경사 하강법의 지역적 효율성을 유지하면서 단순한 하이퍼파라미터 그리드 검색보다 훨씬 더 넓은 설계 공간을 탐색하는 시스템입니다.

다음 단계

이 단일 레벨 진화 아키텍처(평평한 DQN 게놈 개체군)는 커버해야 할 자산 수가 증가함에 따라 한계에 도달합니다. 이것이 3레벨 계층적 아키텍처(자산 분석가 → 섹터 관리자 → 포트폴리오 할당자)로의 이동 동기가 되었으며, 각 레벨에서 GA가 독립적으로 작동합니다… 하지만 그것은 다른 기사의 주제입니다.

Gradyan inişi yerine doğal seçilimle bir sinir ağı evrimleştirdim

Klasik gradyan inişi eğitimini bir NSGA-II genetik algoritmasıyla değiştirerek DQN alım-satım ajanlarını nasıl evrimleştirdiğim: aşırı uyumdan Lamarckçı ağırlık evrimine dört sürüm

Sadece gradyan inişinin sorunu

Algoritmik alım-satım için bir DQN ajanını klasik gradyan inişiyle eğitmenin ifade etmesi basit, çözmesi zor bir sorunu vardır: gradyan inişi tek bir ağı tek bir yerel minimuma, tek bir piyasa penceresinde optimize eder. Bu minimumun farklı bir piyasa rejimine genellenebileceğini garanti eden hiçbir şey yoktur ve eğitim döngüsünde çeşitliliği iten hiçbir şey yoktur; farklı tohumlardan başlayan iki çalışma genellikle neredeyse aynı stratejilere ve aynı kör noktalara yakınsar.

Keşfettiğim yanıt: gradyan inişini bir genetik algoritmayla değiştirmek (veya üzerine yerleştirmek). Bir ajanı eğitmek yerine, bir ajan popülasyonunu evrimleştiriyorsunuz; her genom bir mimari ve hiperparametreler kodluyor; doğal seçilim sıralamayı yaparken, gradyan inişi her bireyi kendi yaşam süresi içinde ince ayarlamaya devam ediyor.

Bu koşucu, tek bir yoğun oturumda dört sürümden geçti. Her sürüm, bir öncekindeki yapısal bir kusuru düzeltti.

v1: saf sürüm ve neden yeterli olmadığı

İlk sürüm, temel bir GA'dan bekleyeceğiniz şeyi yaptı: bir genom popülasyonu, bir uygunluk fonksiyonu, seçilim, çaprazlama, mutasyon, sonraki nesil. Her genom, ağ topolojisini (katman sayısı, genişlik), DQN hiperparametrelerini (öğrenme oranı, epsilon azalması, tekrar oynatma arabelleği boyutu) ve birkaç mimari seçeneği (hangi veri kaynaklarının kullanılacağı, yerleştirme boyutu) kodluyordu.

Ana kusur: uygunluk, eğitim için kullanılan aynı veriler üzerinde hesaplanıyordu. Bir ajan, bir piyasa penceresini tam anlamıyla ezberleyebilir ve genellenebilir bir strateji öğrenmeden mükemmel bir puan alabilirdi. Klasik aşırı uyum, ancak genetik seçilimle güçlendirilmiş; GA bu açıktan en iyi şekilde yararlanan bireyleri aktif olarak seçer.

v2: eğitim ve değerlendirmeyi ayırma

Bariz düzeltme, aşamaları ayırmaktı: her genom bir piyasa penceresinde eğitilir, ardından eğitim sırasında hiç görülmemiş farklı bir pencerede değerlendirilir. Yalnızca değerlendirme performansı uygunluğa sayılır.

Bu tek değişiklik, ortalama popülasyon uygunluğunun düşmesine neden oldu; bu, v1'de performans gibi görünen şeyin büyük bir kısmının saf ezber olduğunun bir işareti. Görmek acı verici, ancak tam olarak istediğiniz sinyal bu: daha düşük ama dürüst bir puan, şişirilmiş, yanıltıcı olandan daha iyidir.

v3: NSGA-II ve çok amaçlı uygunluğa geçiş

Tek bir uygunluk puanını (örneğin getirileri) optimize etmek, ajanları mekanik olarak bu tek sayıyı maksimize etmek için aşırı riskler almaya iter. Çözüm, birden çok amacı keyfi bir ağırlıklı toplama indirgemeden eşzamanlı olarak optimize eden NSGA-II'ye (Baskın Olmayan Sıralama Genetik Algoritması II) geçmekti: getiriler, maksimum düşüş, Sharpe oranı, pencereler arası kararlılık.

NSGA-II bir Pareto cephesi oluşturur: bir amacın iyileştirilmesinin diğerini bozmasının mümkün olmadığı genom kümesi. Önceden seçilmiş bir ağırlıklandırma yoluyla tek bir getiri-risk ödünleşimini zorlamak yerine, tüm uzlaşma sınırını tutar ve nihai seçimi açık bırakırsınız.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... sonraki cephelerin yinelemeli kaldırma ile oluşturulması
  return fronts;
}

v3'teki ikinci ekleme: kalıcı bir Pareto arşivi. Onsuz, 12. nesilde bulunan iyi bir genom, çaprazlama şansı onu yeniden üretmezse 15. nesilde kaybolabilir; onun yerini alan her şeyden daha iyi kalsa bile. Arşiv, mevcut popülasyondan bağımsız olarak, tüm nesiller boyunca şimdiye kadar karşılaşılan tüm baskın olmayan bireylerin kümesini tutar.

v4: Lamarckçı evrim ve çevresel çeşitlilik

V3'ün yapısal bir kör noktası vardı: genom mimariyi tanımlıyordu, ancak eğitim sırasında öğrenilen ağırlıklar her yeni nesilde kayboluyordu. İki iyi ebeveynin çaprazlanmasından doğan bir çocuk, onların mimarisini miras aldı, ancak sıfırdan yeniden öğrenmek zorundaydı; ebeveynlerini performanslı yapan ağırlıkların hiçbir izi yoktu.

V4, Lamarckçı evrimi getiriyor: eğitilmiş ağırlıklar, eğitimden sonra genoma geri beslenir ve (mutasyonla birlikte) yavrulara iletilir. Bu, kasıtlı bir biyolojik sapkınlıktır; Lamarck canlı organizmalar için yanılmıştı -- edinilmiş karakteristiklerin kalıtımı biyolojide mevcut değildir -- ancak dijital bir GA'nın akıllıca hile yapmasını hiçbir şey engelleyemez: burada, edinilmiş bilgiyi iletmek, yakınsamayı radikal bir şekilde hızlandırır, çünkü her nesil rastgele ağırlıklar yerine zaten bilgilendirilmiş bir başlangıçtan yeniden başlar.

Bu sürümdeki diğer üç yapısal değişiklik:

  • Çevresel çeşitlilik: her genom artık tek bir piyasa penceresinde değil, farklı rejimlerden (yükseliş, düşüş, bant) alınan birden çok pencerede değerlendirilir. Bir pencerede mükemmel olan ve diğerinde çöken bir ajan, artık Pareto cephesine hakim olamaz.

  • FLOPs karmaşıklık düzenlileştirmesi: ağın hesaplama maliyeti (FLOPs cinsinden) NSGA-II'de tam bir amaç haline gelir. Bu, evrimin, haklı bir performans kazancı olmaksızın, yalnızca daha fazla ham kapasiteye sahip oldukları için devasa mimarilere yakınsamasını önler.

  • Ayrıştırılmış RLBackend arayüzü: GA artık DQN ayrıntılarını bilmez. Bir genomu manipüle eder ve soyut bir arayüz aracılığıyla train() / evaluate() işlevlerini çağırır; bu, teorik olarak, evrim motoruna dokunmadan başka bir RL algoritmasının değiştirilmesine olanak tanır.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Son teknik nokta: değerlendirme, sınırlı async eşzamanlılığa geçti; sıralı bir döngü yerine N paralel değerlendirmeden oluşan bir havuz ve mevcut GPU/CPU kaynaklarının doygunluğunu önlemek için açık bir sınır.

v4'ün pratikte v3'te hangi sorunları düzelttiği

V3 kusuru V4 düzeltmesi Her nesilde kaybedilen ağırlıklar Eğitilmiş ağırlıkların Lamarckçı yeniden enjekte edilmesi Tek bir piyasa penceresine aşırı uyum Birden çok pencerede, çeşitli rejimlerde değerlendirme Sınırsız büyüyen mimariler Açık Pareto hedefi olarak FLOPs GA'nın DQN ayrıntılarına bağlı olması Soyut RLBackend arayüzü Yavaş sıralı değerlendirme Sınırlı async eşzamanlılık

V4 ayrıca on somut API "temel" hatasını düzeltti; GA kodunun, gerçek uygulamayla tam olarak eşleşmeyen bir TradingAgent arayüzü varsaydığı durumlar. Bu tür hatalar, kodu gerçek ajan kaynağıyla karşılaştırana kadar görünmezdir: v4, yalnızca gerçek dosyaya karşı satır satır yeniden okuma yapıldıktan sonra doğrulandı.

Neden birini seçmek yerine hem evrimi hem de gradyanı karıştırmalı

Neden sadece saf RL veya NEAT gibi saf evrim kullanmadığınızı merak edebilirsiniz. Cevap tek bir cümle: gradyan, yerel ince ayar için mükemmeldir (sürekli ağırlıkları yakındaki bir optimuma ayarlamak), evrim ise küresel keşif için mükemmeldir (gradyanın ulaşamayacağı mimarileri ve hiperparametre kombinasyonlarını keşfetmek, çünkü ayrık arama uzayı türevlenebilir değildir). Birini diğeri olmadan kullanmak, iki keşif biçiminden birini kendinizden mahrum bırakmak anlamına gelir.

Bedeli mühendislik karmaşıklığıdır; dört sürüm bir lüks değil, GA+RL döngüsünün kendini sabote etmeyi (aşırı uyum, iyi bireylerin kaybı, edinilmiş ağırlıkların kaybı) durdurması için gereken yineleme sayısıydı. Ancak sonuç, değerlendirilen her aday için gradyan inişinin yerel verimliliğini korurken, basit bir hiperparametre ızgara aramasından çok daha geniş bir tasarım alanını keşfeden bir sistemdir.

Sonraki adım

Bu tek seviyeli evrimsel mimari (düz bir DQN genom popülasyonu), kapsanacak varlık sayısı arttığında sınırlarına ulaşır. Bu, üç seviyeli bir hiyerarşik mimariye (Varlık Analistleri → Sektör Yöneticileri → Portföy Tahsis Edici) geçişi motive etti ve her seviyede bağımsız olarak çalışan bir GA ile... ancak bu başka bir makalenin konusu.

Ho evoluto una rete neurale tramite selezione naturale invece della discesa del gradiente

Come ho sostituito il classico addestramento con discesa del gradiente con un algoritmo genetico NSGA-II per evolvere agenti di trading DQN: quattro versioni, dall'overfitting all'evoluzione lamarckiana dei pesi.

Il problema della sola discesa del gradiente

Addestrare un agente DQN per il trading algoritmico con la classica discesa del gradiente ha un problema semplice da enunciare e uno difficile da risolvere: la discesa del gradiente ottimizza una rete verso un minimo locale, su una finestra di mercato. Nulla garantisce che questo minimo generalizzi a un diverso regime di mercato, e nulla nel ciclo di addestramento spinge verso la diversità; due esecuzioni che partono da semi diversi spesso convergono a strategie quasi identiche, con gli stessi punti ciechi.

La risposta che ho esplorato: sostituire (o meglio sovrapporre) la discesa del gradiente con un algoritmo genetico. Invece di addestrare un agente, si evolve una popolazione di agenti; ogni genoma codifica un'architettura e iperparametri; e la selezione naturale fa il resto, mentre la discesa del gradiente continua a mettere a punto ogni individuo all'interno della propria vita.

Questo progetto è passato attraverso quattro versioni in una singola sessione intensiva. Ogni versione ha corretto un difetto strutturale della precedente.

v1: la versione ingenua, e perché non bastava

La prima versione faceva ciò che ci si aspetterebbe da un GA base: una popolazione di genomi, una funzione di fitness, selezione, crossover, mutazione, generazione successiva. Ogni genoma codificava la topologia della rete (numero di layer, larghezza), gli iperparametri DQN (tasso di apprendimento, decadimento epsilon, dimensione del buffer di replay), e alcune scelte architetturali (quali fonti di dati consumare, dimensione dell'embedding).

Il difetto principale: il fitness veniva calcolato sugli stessi dati usati per l'addestramento. Un agente poteva letteralmente memorizzare una finestra di mercato e ottenere un punteggio eccellente senza aver appreso una strategia generalizzabile. Classico overfitting, ma amplificato dalla selezione genetica; il GA seleziona attivamente gli individui che sfruttano meglio questa scappatoia.

v2: separare addestramento e valutazione

La soluzione ovvia era separare le fasi: ogni genoma si addestra su una finestra di mercato, poi viene valutato su una finestra diversa, mai vista durante l'addestramento. Solo la performance in valutazione conta per il fitness.

Questo singolo cambiamento ha causato un calo del fitness medio della popolazione; un segno che gran parte di ciò che sembrava performance in v1 era pura memorizzazione. È doloroso da vedere, ma è esattamente il segnale che si vuole: un punteggio più basso ma onesto è meglio di uno gonfiato e fuorviante.

v3: passaggio a NSGA-II e fitness multi-obiettivo

Ottimizzare un unico punteggio di fitness (ad esempio, i rendimenti) spinge meccanicamente gli agenti ad assumersi rischi estremi per massimizzare quel singolo numero. La soluzione è stata passare a NSGA-II (Non-dominated Sorting Genetic Algorithm II), che ottimizza simultaneamente diversi obiettivi senza ridurli a una somma pesata arbitraria: rendimenti, drawdown massimo, indice di Sharpe, stabilità tra finestre.

NSGA-II costruisce un fronte di Pareto: l'insieme dei genomi per cui nessun miglioramento su un obiettivo è possibile senza degradarne un altro. Invece di forzare un unico compromesso rendimento-rischio attraverso una ponderazione predefinita, si mantiene l'intera frontiera di compromesso e si lascia aperta la scelta finale.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... costruzione dei fronti successivi per rimozione iterativa
  return fronts;
}

Seconda aggiunta in v3: un archivio di Pareto persistente. Senza di esso, un buon genoma trovato alla generazione 12 può scomparire entro la generazione 15 se la fortuna del crossover non lo riproduce; anche se rimaneva migliore di tutto ciò che lo ha sostituito. L'archivio conserva, attraverso tutte le generazioni, l'insieme di tutti gli individui non dominati mai incontrati, indipendentemente dalla popolazione corrente.

v4: evoluzione lamarckiana e diversità ambientale

V3 aveva un punto cieco strutturale: il genoma descriveva l'architettura, ma i pesi appresi durante l'addestramento scomparivano a ogni nuova generazione. Un figlio nato dal crossover di due buoni genitori ereditava la loro architettura, ma doveva reimparare da zero; nessuna traccia dei pesi che avevano reso performanti i suoi genitori.

V4 introduce l'evoluzione lamarckiana: i pesi addestrati vengono reimmessi nel genoma dopo l'addestramento e trasmessi (con mutazione) alla prole. Questa è una deliberata eresia biologica; Lamarck aveva torto per gli organismi viventi -- l'ereditarietà dei caratteri acquisiti non esiste in biologia -- ma nulla impedisce a un GA digitale di barare intelligentemente: qui, trasmettere conoscenza acquisita accelera radicalmente la convergenza, poiché ogni generazione riparte da un'inizializzazione già informata anziché da pesi casuali.

Tre altri cambiamenti strutturali in questa versione:

  • Diversità ambientale: ogni genoma non viene più valutato su una singola finestra di mercato ma su diverse, tratte da regimi differenti (rialzista, ribassista, laterale). Un agente che eccelle su una finestra e crolla su un'altra non può più dominare il fronte di Pareto.

  • Regolarizzazione della complessità in FLOPs: il costo computazionale della rete (in FLOPs) diventa un obiettivo a pieno titolo in NSGA-II. Questo impedisce all'evoluzione di convergere verso architetture massicce solo perché hanno maggiore capacità grezza, senza un giustificato guadagno prestazionale.

  • Interfaccia RLBackend disaccoppiata: il GA non conosce più i dettagli di DQN. Manipola un genoma e chiama train() / evaluate() attraverso un'interfaccia astratta, che teoricamente permette di sostituire un altro algoritmo RL senza toccare il motore evolutivo.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Ultimo punto tecnico: la valutazione è passata a concorrenza asincrona limitata; un pool di N valutazioni parallele invece di un ciclo sequenziale, con un limite esplicito per evitare di saturare le risorse GPU/CPU disponibili.

Cosa risolve v4 rispetto a v3 nella pratica

Problema v3 Soluzione v4 Pesi persi ogni generazione Re-iniezione lamarckiana dei pesi addestrati Overfitting su una singola finestra di mercato Valutazione su più finestre, regimi variati Architetture che crescono senza vincoli FLOPs come obiettivo Pareto esplicito GA accoppiato ai dettagli DQN Interfaccia astratta RLBackend Valutazione sequenziale lenta Concorrenza asincrona limitata

V4 ha anche corretto dieci bug concreti di "grounding" dell'API; casi in cui il codice GA assumeva un'interfaccia per TradingAgent che non corrispondeva esattamente all'implementazione reale. Questo tipo di bug è invisibile finché non si confronta il codice con il codice sorgente effettivo dell'agente: v4 è stato validato solo dopo una rilettura riga per riga confrontata con il file reale.

Perché mescolare evoluzione e gradiente invece di sceglierne uno

Potresti chiederti perché non usare solo RL puro, o solo evoluzione come NEAT. La risposta è una frase: il gradiente è eccellente per la messa a punto locale (aggiustare pesi continui verso un ottimo vicino), l'evoluzione è eccellente per l'esplorazione globale (scoprire architetture e combinazioni di iperparametri che nessun gradiente può raggiungere, perché lo spazio di ricerca discreto non è differenziabile). Usare uno senza l'altro significa privarsi di una delle due forme di esplorazione.

Il prezzo è la complessità ingegneristica; quattro versioni non sono state un lusso, ma il numero di iterazioni necessarie perché il ciclo GA + RL smettesse di sabotarsi da solo (overfitting, perdita di buoni individui, perdita di pesi acquisiti). Ma il risultato è un sistema che esplora uno spazio di progettazione molto più ampio di una semplice ricerca a griglia di iperparametri, mantenendo al contempo l'efficienza locale della discesa del gradiente per ogni candidato valutato.

Prossimo passo

Questa architettura evolutiva a singolo livello (una popolazione piatta di genomi DQN) raggiunge i suoi limiti quando il numero di asset da coprire cresce. Questo è ciò che ha motivato il passaggio a un'architettura gerarchica a tre livelli (Analisti di Asset → Gestori di Settore → Allocatore di Portafoglio), con un GA che opera indipendentemente a ogni livello... ma questo è argomento di un altro articolo.

Ich habe ein neuronales Netz durch natürliche Selektion statt Gradientenabstieg evolviert

Wie ich das klassische Gradientenabstiegs-Training durch einen NSGA-II-Genetischen-Algorithmus ersetzt habe, um DQN-Trading-Agenten zu evolvieren: vier Versionen, von Overfitting bis zur lamarckschen Gewichtsentwicklung.

Das Problem des reinen Gradientenabstiegs

Das Training eines DQN-Agenten für algorithmischen Handel mit klassischem Gradientenabstieg hat ein einfach zu formulierendes und ein schwer zu lösendes Problem: Der Gradientenabstieg optimiert ein Netzwerk in Richtung eines lokalen Minimums, auf einem Marktfenster. Nichts garantiert, dass dieses Minimum auf ein anderes Marktregime generalisiert, und nichts im Trainingszyklus fördert Vielfalt; zwei Läufe mit unterschiedlichen Startwerten konvergieren oft zu nahezu identischen Strategien mit denselben blinden Flecken.

Der von mir untersuchte Ansatz: den Gradientenabstieg durch einen genetischen Algorithmus ersetzen (oder vielmehr überlagern). Statt eines Agenten wird eine Population von Agenten evolviert; jedes Genom codiert eine Architektur und Hyperparameter; und die natürliche Selektion übernimmt die Sortierung, während der Gradientenabstieg jeden Individuum innerhalb seiner eigenen Lebenszeit weiter verfeinert.

Dieses Projekt durchlief vier Versionen in einer einzigen intensiven Sitzung. Jede Version behob einen strukturellen Fehler der vorherigen.

v1: die naive Version und warum sie nicht ausreichte

Die erste Version tat, was man von einem einfachen GA erwartet: eine Population von Genomen, eine Fitnessfunktion, Selektion, Crossover, Mutation, nächste Generation. Jedes Genom codierte die Netzwerktopologie (Anzahl der Layer, Breite), DQN-Hyperparameter (Lernrate, Epsilon-Dekay, Replay-Buffer-Größe) und einige architektonische Entscheidungen (welche Datenquellen verwendet werden, welche Embedding-Größe).

Der Hauptfehler: Die Fitness wurde auf denselben Daten berechnet, die für das Training verwendet wurden. Ein Agent konnte buchstäblich ein Marktfenster auswendig lernen und eine hervorragende Bewertung erzielen, ohne eine generalisierbare Strategie gelernt zu haben. Klassisches Overfitting, aber verstärkt durch genetische Selektion; der GA selektiert aktiv die Individuen, die diese Gesetzeslücke am besten ausnutzen.

v2: Trennung von Training und Evaluierung

Die offensichtliche Lösung war die Trennung der Phasen: Jedes Genom trainiert auf einem Marktfenster und wird dann auf einem anderen, während des Trainings nie gesehenen Fenster evaluiert. Nur die Evaluierungsleistung zählt für die Fitness.

Diese einzelne Änderung führte zu einem Abfall der durchschnittlichen Populationsfitness; ein Zeichen dafür, dass ein großer Teil dessen, was in v1 wie Leistung aussah, reines Auswendiglernen war. Es ist schmerzhaft anzusehen, aber es ist genau das Signal, das man haben möchte: Eine niedrigere, aber ehrliche Bewertung ist besser als eine aufgeblähte, irreführende.

v3: Umstellung auf NSGA-II und multi-objective Fitness

Die Optimierung einer einzelnen Fitnesskennzahl (z. B. Rendite) treibt Agenten mechanisch dazu, extreme Risiken einzugehen, um diese einzelne Zahl zu maximieren. Die Lösung war die Umstellung auf NSGA-II (Non-dominated Sorting Genetic Algorithm II), der mehrere Ziele gleichzeitig optimiert, ohne sie auf eine willkürlich gewichtete Summe zu reduzieren: Rendite, maximaler Drawdown, Sharpe-Ratio, Stabilität zwischen Fenstern.

NSGA-II erstellt eine Pareto-Front: die Menge der Genome, bei denen keine Verbesserung eines Ziels möglich ist, ohne ein anderes zu verschlechtern. Anstatt einen einzigen Rendite-Risiko-Kompromiss durch eine vorab festgelegte Gewichtung zu erzwingen, behält man die gesamte Kompromissfront und lässt die endgültige Wahl offen.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... Konstruktion nachfolgender Fronten durch iterative Entfernung
  return fronts;
}

Zweite Ergänzung in v3: ein persistentes Pareto-Archiv. Ohne dieses kann ein gutes, in Generation 12 gefundenes Genom bis Generation 15 verschwinden, wenn das Crossover-Glück es nicht reproduziert; selbst wenn es besser blieb als alles, was es ersetzte. Das Archiv bewahrt über alle Generationen hinweg die Menge aller jemals gefundenen nicht-dominierten Individuen, unabhängig von der aktuellen Population.

v4: Lamarck'sche Evolution und Umweltvielfalt

V3 hatte einen strukturellen blinden Fleck: Das Genom beschrieb die Architektur, aber die während des Trainings gelernten Gewichte verschwanden bei jeder neuen Generation. Ein aus dem Crossover zweier guter Eltern geborenes Kind erbte deren Architektur, musste aber von Grund auf neu lernen; keine Spur der Gewichte, die seine Eltern leistungsfähig gemacht hatten.

V4 führt die Lamarck'sche Evolution ein: trainierte Gewichte werden nach dem Training zurück in das Genom eingespeist und (mit Mutation) an die Nachkommen weitergegeben. Dies ist eine bewusste biologische Häresie; Lamarck lag bei Lebewesen falsch -- die Vererbung erworbener Eigenschaften gibt es in der Biologie nicht -- aber nichts hindert einen digitalen GA daran, intelligent zu betrügen: Hier beschleunigt die Weitergabe erworbener Kenntnisse die Konvergenz radikal, da jede Generation von einer bereits informierten Initialisierung statt von zufälligen Gewichten ausgeht.

Drei weitere strukturelle Änderungen in dieser Version:

  • Umweltvielfalt: Jedes Genom wird nicht mehr auf einem einzigen Marktfenster evaluiert, sondern auf mehreren, die aus verschiedenen Regimen stammen (bullisch, bärisch, seitwärts). Ein Agent, der auf einem Fenster hervorragend abschneidet und auf einem anderen zusammenbricht, kann die Pareto-Front nicht mehr dominieren.

  • FLOPs-Komplexitätsregulierung: Die Rechenkosten des Netzwerks (in FLOPs) werden zu einem vollwertigen Ziel in NSGA-II. Dies verhindert, dass die Evolution zu massiven Architekturen konvergiert, nur weil sie mehr rohe Kapazität haben, ohne einen gerechtfertigten Leistungsgewinn.

  • Entkoppeltes RLBackend-Interface: Der GA kennt keine DQN-Details mehr. Er manipuliert ein Genom und ruft train() / evaluate() über ein abstraktes Interface auf, was theoretisch den Austausch gegen einen anderen RL-Algorithmus ermöglicht, ohne die Evolutions-Engine zu berühren.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Letzter technischer Punkt: Die Evaluierung wurde auf gebundene asynchrone Nebenläufigkeit umgestellt; ein Pool von N parallelen Evaluierungen statt einer sequenziellen Schleife, mit einer expliziten Grenze, um eine Sättigung der verfügbaren GPU-/CPU-Ressourcen zu vermeiden.

Was v4 gegenüber v3 in der Praxis behebt

V3-Fehler V4-Behebung Gewichte pro Generation verloren Lamarck'sche Wiedereinführung trainierter Gewichte Overfitting auf ein einzelnes Marktfenster Evaluierung auf mehreren Fenstern, verschiedenen Regimen Unbegrenzt wachsende Architekturen FLOPs als explizites Pareto-Ziel GA an DQN-Details gekoppelt Abstraktes RLBackend-Interface Langsame sequenzielle Evaluierung Gebundene asynchrone Nebenläufigkeit

V4 behob auch zehn konkrete API-"Grounding"-Fehler; Fälle, in denen der GA-Code ein Interface für TradingAgent annahm, das nicht exakt mit der tatsächlichen Implementierung übereinstimmte. Diese Art von Fehler ist unsichtbar, bis man den Code mit der tatsächlichen Agentenquelle abgleicht: v4 wurde erst nach einem zeilenweisen Neuabgleich mit der echten Datei validiert.

Warum Evolution und Gradient mischen statt sich für eines zu entscheiden

Du fragst dich vielleicht, warum nicht einfach reines RL oder reine Evolution wie NEAT verwendet wird. Die Antwort ist ein Satz: Der Gradient eignet sich hervorragend für die lokale Feinabstimmung (Anpassen kontinuierlicher Gewichte in Richtung eines nahen Optimums), die Evolution eignet sich hervorragend für die globale Erkundung (Entdecken von Architekturen und Hyperparameterkombinationen, die kein Gradient erreichen kann, da der diskrete Suchraum nicht differenzierbar ist). Nur eines zu verwenden bedeutet, sich einer der beiden Erkundungsformen zu berauben.

Der Preis ist die ingenieurtechnische Komplexität; vier Versionen waren kein Luxus, sondern die Anzahl der Iterationen, die nötig waren, damit die GA + RL-Schleife aufhörte, sich selbst zu sabotieren (Overfitting, Verlust guter Individuen, Verlust erworbener Gewichte). Aber das Ergebnis ist ein System, das einen viel größeren Entwurfsraum erkundet als eine einfache Gittersuche von Hyperparametern, während es die lokale Effizienz des Gradientenabstiegs für jeden evaluierten Kandidaten beibehält.

Nächster Schritt

Diese einstufige evolutionäre Architektur (eine flache Population von DQN-Genomen) stößt an ihre Grenzen, wenn die Anzahl der abzudeckenden Assets wächst. Das war die Motivation für den Umstieg auf eine dreistufige hierarchische Architektur (Asset-Analysten → Sektor-Manager → Portfolio-Allokator), mit einem GA, der auf jeder Ebene unabhängig operiert... aber das ist das Thema eines anderen Artikels.

Я эволюционировал нейронную сеть через естественный отбор вместо градиентного спуска

Как я заменил классическое обучение градиентным спуском на генетический алгоритм NSGA-II для эволюции DQN-агентов торговли: четыре версии, от переобучения до ламарковской эволюции весов.

Проблема одного лишь градиентного спуска

Обучение DQN-агента для алгоритмической торговли с помощью классического градиентного спуска имеет простую в формулировке и сложную в решении проблему: градиентный спуск оптимизирует одну сеть к одному локальному минимуму на одном рыночном окне. Ничто не гарантирует, что этот минимум обобщится на другой рыночный режим, и ничто в цикле обучения не способствует разнообразию; два запуска с разных начальных значений часто сходятся к почти идентичным стратегиям с одними и теми же слепыми зонами.

Ответ, который я исследовал: заменить (или, скорее, наложить) градиентный спуск на генетический алгоритм. Вместо обучения одного агента вы эволюционируете популяцию агентов; каждый геном кодирует архитектуру и гиперпараметры; а естественный отбор выполняет сортировку, в то время как градиентный спуск продолжает точно настраивать каждую особь в течение её собственной жизни.

Этот проект прошёл через четыре версии за одну интенсивную сессию. Каждая версия исправляла структурный недостаток предыдущей.

v1: наивная версия и почему её было недостаточно

Первая версия делала то, что можно ожидать от базового ГА: популяция геномов, функция приспособленности, отбор, скрещивание, мутация, следующее поколение. Каждый геном кодировал топологию сети (количество слоёв, ширину), гиперпараметры DQN (скорость обучения, затухание эпсилон, размер буфера воспроизведения) и некоторые архитектурные решения (какие источники данных использовать, какой размер эмбеддингов).

Главный недостаток: приспособленность вычислялась на тех же данных, что использовались для обучения. Агент мог буквально запомнить рыночное окно и получить отличный результат, не научившись обобщаемой стратегии. Классическое переобучение, но усиленное генетическим отбором; ГА активно отбирает особей, которые лучше всего используют эту лазейку.

v2: разделение обучения и оценки

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

Это единственное изменение вызвало падение средней приспособленности популяции -- признак того, что большая часть того, что выглядело как производительность в v1, была чистым запоминанием. Это больно видеть, но это именно тот сигнал, который нужен: более низкий, но честный результат лучше, чем завышенный и вводящий в заблуждение.

v3: переход на NSGA-II и многокритериальная приспособленность

Оптимизация единственного показателя приспособленности (скажем, доходности) механически подталкивает агентов к чрезмерному риску для максимизации этого единственного числа. Решением стал переход на NSGA-II (Non-dominated Sorting Genetic Algorithm II), который одновременно оптимизирует несколько целей, не сводя их к произвольной взвешенной сумме: доходность, максимальная просадка, коэффициент Шарпа, стабильность между окнами.

NSGA-II строит фронт Парето: множество геномов, для которых невозможно улучшение по одной цели без ухудшения другой. Вместо того чтобы навязывать единственный компромисс между доходностью и риском через предвзвешивание, вы сохраняете всю границу компромиссов и оставляете окончательный выбор открытым.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... построение последующих фронтов итеративным удалением
  return fronts;
}

Второе дополнение в v3: постоянный архив Парето. Без него хороший геном, найденный на поколении 12, может исчезнуть к поколению 15, если удача скрещивания не воспроизведёт его; даже если он оставался лучше всего, что его заменило. Архив сохраняет на протяжении всех поколений множество всех недоминируемых особей, когда-либо встреченных, независимо от текущей популяции.

v4: ламарковская эволюция и разнообразие среды

V3 имела структурную слепую зону: геном описывал архитектуру, но веса, изученные во время обучения, исчезали в каждом новом поколении. Ребёнок, рождённый от скрещивания двух хороших родителей, наследовал их архитектуру, но должен был учиться с нуля; никакого следа весов, которые делали его родителей производительными.

V4 вводит ламарковскую эволюцию: обученные веса возвращаются в геном после обучения и передаются (с мутацией) потомству. Это намеренная биологическая ересь; Ламарк ошибался в отношении живых организмов -- наследование приобретённых признаков не существует в биологии -- но ничто не мешает цифровому ГА умно жульничать: здесь передача приобретённых знаний радикально ускоряет сходимость, поскольку каждое поколение начинается с уже информированной инициализации, а не со случайных весов.

Три других структурных изменения в этой версии:

  • Разнообразие среды: каждый геном больше не оценивается на одном рыночном окне, а на нескольких, взятых из разных режимов (бычий, медвежий, боковой). Агент, который преуспевает на одном окне и терпит крах на другом, больше не может доминировать на фронте Парето.

  • Регуляризация сложности во FLOPs: вычислительная стоимость сети (во FLOPs) становится полноценной целью в NSGA-II. Это предотвращает сходимость эволюции к массивным архитектурам только потому, что у них больше сырой ёмкости, без оправданного прироста производительности.

  • Отделённый интерфейс RLBackend: ГА больше не знает деталей DQN. Он манипулирует геномом и вызывает train() / evaluate() через абстрактный интерфейс, что теоретически позволяет заменить другой RL-алгоритм, не затрагивая механизм эволюции.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Последний технический момент: оценка переключена на ограниченную асинхронную конкурентность; пул из N параллельных оценок вместо последовательного цикла, с явным лимитом во избежание насыщения доступных ресурсов GPU/CPU.

Что v4 исправляет по сравнению с v3 на практике

Проблема v3 Исправление v4 Веса терялись в каждом поколении Ламарковское возвращение обученных весов Переобучение на одно рыночное окно Оценка на нескольких окнах, разные режимы Архитектуры, растущие без ограничений FLOPs как явная цель Парето ГА, связанный с деталями DQN Абстрактный интерфейс RLBackend Медленная последовательная оценка Ограниченная асинхронная конкурентность

V4 также исправила десять конкретных ошибок "заземления" API; случаев, когда код ГА предполагал интерфейс для TradingAgent, который не совсем совпадал с реальной реализацией. Такой тип ошибок невидим, пока не сопоставишь код с фактическим исходным кодом агента: v4 была проверена только после построчного перечитывания в сопоставлении с реальным файлом.

Зачем смешивать эволюцию и градиент, а не выбирать одно

Вы можете задаться вопросом, почему бы не использовать только RL или только эволюцию вроде NEAT. Ответ состоит из одного предложения: градиент отлично подходит для локальной точной настройки (настройка непрерывных весов к ближайшему оптимуму), эволюция отлично подходит для глобального исследования (открытие архитектур и комбинаций гиперпараметров, которые не может достичь ни один градиент, потому что дискретное пространство поиска не дифференцируемо). Использовать одно без другого означает лишить себя одной из двух форм исследования.

Цена -- инженерная сложность; четыре версии были не роскошью, а количеством итераций, необходимых для того, чтобы цикл ГА + RL перестал сам себе вредить (переобучение, потеря хороших особей, потеря приобретённых весов). Но результат -- это система, которая исследует гораздо более широкое пространство проектирования, чем простой поиск по сетке гиперпараметров, сохраняя при этом локальную эффективность градиентного спуска для каждого оцениваемого кандидата.

Следующий шаг

Эта одноуровневая эволюционная архитектура (плоская популяция DQN-геномов) достигает своих пределов, когда количество охватываемых активов растёт. Именно это мотивировало переход к трёхуровневой иерархической архитектуре (Аналитики активов → Менеджеры секторов → Распределитель портфеля), с ГА, работающим независимо на каждом уровне... но это тема другой статьи.

Evolucioné una red neuronal mediante selección natural en lugar de gradiente descendente

Cómo reemplacé el entrenamiento clásico por gradiente descendente con un algoritmo genético NSGA-II para evolucionar agentes de trading DQN: cuatro versiones, desde sobreajuste hasta evolución Lamarckiana de pesos.

El problema del gradiente descendente por sí solo

Entrenar un agente DQN para trading algorítmico con gradiente descendente clásico tiene un problema simple de enunciar y uno difícil de resolver: el gradiente descendente optimiza una red hacia un mínimo local, en una ventana de mercado. Nada garantiza que este mínimo generalice a un régimen de mercado diferente, y nada en el bucle de entrenamiento impulsa la diversidad; dos ejecuciones partiendo de semillas diferentes a menudo convergen a estrategias casi idénticas, con los mismos puntos ciegos.

La respuesta que exploré: reemplazar (o más bien superponer) el gradiente descendente con un algoritmo genético. En lugar de entrenar un agente, se evoluciona una población de agentes; cada genoma codifica una arquitectura e hiperparámetros; y la selección natural hace la clasificación, mientras que el gradiente descendente sigue ajustando cada individuo dentro de su propia vida.

Este experimento pasó por cuatro versiones en una sola sesión intensiva. Cada versión corregía un defecto estructural de la anterior.

v1: la versión ingenua, y por qué no fue suficiente

La primera versión hacía lo que cabría esperar de un AG básico: una población de genomas, una función de fitness, selección, cruce, mutación, siguiente generación. Cada genoma codificaba la topología de la red (número de capas, anchura), los hiperparámetros de DQN (tasa de aprendizaje, decaimiento de epsilon, tamaño del búfer de repetición), y algunas elecciones arquitectónicas (qué fuentes de datos consumir, qué tamaño de embedding).

El defecto principal: el fitness se calculaba sobre los mismos datos usados para el entrenamiento. Un agente podía literalmente memorizar una ventana de mercado y obtener una puntuación excelente sin haber aprendido una estrategia generalizable. Sobreajuste clásico, pero amplificado por la selección genética; el AG selecciona activamente los individuos que mejor explotan esta laguna.

v2: separando entrenamiento y evaluación

La solución obvia fue separar las fases: cada genoma se entrena en una ventana de mercado, luego se evalúa en una ventana diferente, nunca vista durante el entrenamiento. Solo el rendimiento en evaluación cuenta para el fitness.

Este solo cambio hizo que el fitness promedio de la población disminuyera; una señal de que una gran parte de lo que parecía rendimiento en v1 era pura memorización. Doloroso de ver, pero es exactamente la señal que quieres: una puntuación más baja pero honesta es mejor que una inflada y engañosa.

v3: migrando a NSGA-II y fitness multiobjetivo

Optimizar una única puntuación de fitness (digamos, rendimientos) empuja mecánicamente a los agentes a tomar riesgos extremos para maximizar ese único número. La solución fue cambiAR a NSGA-II (Algoritmo Genético de Ordenamiento No-Dominado II), que optimiza simultáneamente varios objetivos sin reducirlos a una suma ponderada arbitraria: rendimientos, drawdown máximo, ratio de Sharpe, estabilidad entre ventanas.

NSGA-II construye un frente de Pareto: el conjunto de genomas para los cuales ninguna mejora en un objetivo es posible sin degradar otro. En lugar de forzar una única compensación entre riesgo y rendimiento mediante una ponderación preelegida, se mantiene toda la frontera de compromiso y se deja la decisión final abierta.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... construcción de frentes posteriores por eliminación iterativa
  return fronts;
}

Segunda adición en v3: un archivo Pareto persistente. Sin él, un buen genoma encontrado en la generación 12 puede desaparecer para la generación 15 si la suerte del cruce no lo reproduce; incluso si sigue siendo mejor que todo lo que lo reemplazó. El archivo conserva, a través de todas las generaciones, el conjunto de todos los individuos no dominados jamás encontrados, independientemente de la población actual.

v4: evolución Lamarckiana y diversidad ambiental

V3 tenía un punto ciego estructural: el genoma describía la arquitectura, pero los pesos aprendidos durante el entrenamiento desaparecían en cada nueva generación. Un hijo nacido del cruce de dos buenos padres heredaba su arquitectura, pero tenía que reaprender desde cero; sin rastro de los pesos que habían hecho eficientes a sus padres.

V4 introduce la evolución Lamarckiana: los pesos entrenados se retroalimentan al genoma después del entrenamiento y se transmiten (con mutación) a la descendencia. Esto es una herejía biológica deliberada; Lamarck se equivocaba para los organismos vivos -- la herencia de caracteres adquiridos no existe en biología -- pero nada impide que un AG digital haga trampa inteligentemente: aquí, transmitir conocimiento adquirido acelera radicalmente la convergencia, ya que cada generación reinicia desde una inicialización ya informada en lugar de pesos aleatorios.

Otros tres cambios estructurales en esta versión:

  • Diversidad ambiental: cada genoma ya no se evalúa en una sola ventana de mercado sino en varias, extraídas de diferentes regímenes (alcista, bajista, lateral). Un agente que sobresale en una ventana y colapsa en otra ya no puede dominar el frente de Pareto.

  • Regularización de complejidad en FLOPs: el costo computacional de la red (en FLOPs) se convierte en un objetivo completo en NSGA-II. Esto evita que la evolución converja a arquitecturas masivas simplemente porque tienen más capacidad bruta, sin una ganancia de rendimiento justificada.

  • Interfaz RLBackend desacoplada: el AG ya no conoce los detalles de DQN. Manipula un genoma y llama a train() / evaluate() a través de una interfaz abstracta, lo que teóricamente permite intercambiar otro algoritmo de RL sin tocar el motor evolutivo.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Último punto técnico: la evaluación cambió a concurrencia asíncrona acotada; un grupo de N evaluaciones paralelas en lugar de un bucle secuencial, con un límite explícito para evitar saturar los recursos de GPU/CPU disponibles.

Lo que v4 corrige frente a v3 en la práctica

Defecto de v3 Corrección de v4
Pesos perdidos cada generación Reinyección Lamarckiana de pesos entrenados
Sobreajuste a una única ventana de mercado Evaluación en múltiples ventanas, regímenes variados
Arquitecturas creciendo sin control FLOPs como objetivo explícito de Pareto
AG acoplado a detalles de DQN Interfaz abstracta RLBackend
Evaluación secuencial lenta Concurrencia asíncrona acotada

V4 también corrigió diez errores concretos de "conexión a tierra" de la API; casos donde el código del AG asumía una interfaz para TradingAgent que no coincidía exactamente con la implementación real. Este tipo de error es invisible hasta que confrontas el código contra la fuente real del agente: v4 solo se validó después de una relectura línea por línea contra el archivo real.

Por qué mezclar evolución y gradiente en lugar de elegir uno

Podrías preguntarte por qué no usar solo RL puro, o solo evolución como NEAT. La respuesta es una frase: el gradiente es excelente para el ajuste local (ajustar pesos continuos hacia un óptimo cercano), la evolución es excelente para la exploración global (descubrir arquitecturas y combinaciones de hiperparámetros que ningún gradiente puede alcanzar, porque el espacio de búsqueda discreto no es diferenciable). Usar uno sin el otro significa privarse de una de las dos formas de exploración.

El precio es la complejidad técnica; cuatro versiones no fueron un lujo, fueron el número de iteraciones necesarias para que el bucle AG + RL dejara de sabotearse a sí mismo (sobreajuste, pérdida de buenos individuos, pérdida de pesos adquiridos). Pero el resultado es un sistema que explora un espacio de diseño mucho más amplio que una simple búsqueda en cuadrícula de hiperparámetros, manteniendo al mismo tiempo la eficiencia local del gradiente descendente para cada candidato evaluado.

Próximo paso

Esta arquitectura evolutiva de un solo nivel (una población plana de genomas DQN) alcanza sus límites cuando crece el número de activos a cubrir. Eso es lo que motivó el paso a una arquitectura jerárquica de tres niveles (Analistas de Activos → Gestores de Sector → Asignador de Cartera), con un AG operando independientemente en cada nivel... pero ese es el tema de otro artículo.

Evoluí uma rede neural por seleção natural em vez de gradiente descendente

Como substituí o treinamento clássico por gradiente descendente por um algoritmo genético NSGA-II para evoluir agentes de trading DQN: quatro versões, de overfitting à evolução Lamarckiana de pesos.

O problema do gradiente descendente sozinho

Treinar um agente DQN para trading algorítmico com gradiente descendente clássico tem um problema simples de enunciar e um difícil de resolver: o gradiente descendente otimiza uma rede em direção a um mínimo local, em uma janela de mercado. Nada garante que esse mínimo generalize para um regime de mercado diferente, e nada no loop de treinamento estimula a diversidade; duas execuções partindo de sementes diferentes frequentemente convergem para estratégias quase idênticas, com os mesmos pontos cegos.

A resposta que explorei: substituir (ou melhor, sobrepor) o gradiente descendente por um algoritmo genético. Em vez de treinar um agente, você evolui uma população de agentes; cada genoma codifica uma arquitetura e hiperparâmetros; e a seleção natural faz a classificação, enquanto o gradiente descendente continua ajustando cada indivíduo dentro de sua própria vida.

Este experimento passou por quatro versões em uma única sessão intensiva. Cada versão corrigiu uma falha estrutural da anterior.

v1: a versão ingênua, e por que não foi suficiente

A primeira versão fazia o que você esperaria de um AG básico: uma população de genomas, uma função de fitness, seleção, cruzamento, mutação, próxima geração. Cada genoma codificava a topologia da rede (número de camadas, largura), os hiperparâmetros do DQN (taxa de aprendizado, decaimento do épsilon, tamanho do buffer de replay), e algumas escolhas arquiteturais (quais fontes de dados consumir, qual tamanho de embedding).

A principal falha: o fitness era calculado nos mesmos dados usados para o treinamento. Um agente podia literalmente memorizar uma janela de mercado e obter uma pontuação excelente sem ter aprendido uma estratégia generalizável. Overfitting clássico, mas amplificado pela seleção genética; o AG seleciona ativamente os indivíduos que melhor exploram essa brecha.

v2: separando treinamento e avaliação

A correção óbvia foi separar as fases: cada genoma treina em uma janela de mercado, depois é avaliado em uma janela diferente, nunca vista durante o treinamento. Apenas o desempenho na avaliação conta para o fitness.

Essa mudança sozinha fez o fitness médio da população cair; um sinal de que grande parte do que parecia desempenho na v1 era pura memorização. Doloroso de ver, mas é exatamente o sinal que você quer: uma pontuação mais baixa mas honesta é melhor do que uma inflada e enganosa.

v3: migrando para NSGA-II e fitness multiobjetivo

Otimizar uma única pontuação de fitness (digamos, retornos) empurra mecanicamente os agentes a assumir riscos extremos para maximizar aquele único número. A solução foi migrar para o NSGA-II (Algoritmo Genético de Ordenação Não-Dominada II), que otimiza simultaneamente vários objetivos sem reduzi-los a uma soma ponderada arbitrária: retornos, drawdown máximo, índice de Sharpe, estabilidade entre janelas.

O NSGA-II constrói uma frente de Pareto: o conjunto de genomas para os quais nenhuma melhoria em um objetivo é possível sem degradar outro. Em vez de forçar uma única compensação risco-retorno através de uma ponderação pré-escolhida, você mantém toda a fronteira de compromisso e deixa a decisão final em aberto.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... construção de frentes subsequentes por remoção iterativa
  return fronts;
}

Segunda adição na v3: um arquivo Pareto persistente. Sem ele, um bom genoma encontrado na geração 12 pode desaparecer na geração 15 se a sorte do cruzamento não o reproduzir; mesmo que ele continue sendo melhor que tudo que o substituiu. O arquivo mantém, através de todas as gerações, o conjunto de todos os indivíduos não dominados já encontrados, independentemente da população atual.

v4: evolução Lamarckiana e diversidade ambiental

A v3 tinha um ponto cego estrutural: o genoma descrevia a arquitetura, mas os pesos aprendidos durante o treinamento desapareciam a cada nova geração. Um filho nascido do cruzamento de dois bons pais herdava sua arquitetura, mas tinha que reaprender do zero; nenhum traço dos pesos que tornaram seus pais eficientes.

A v4 introduz a evolução Lamarckiana: os pesos treinados são realimentados no genoma após o treinamento e transmitidos (com mutação) para a prole. Isso é uma heresia biológica deliberada; Lamarck estava errado para organismos vivos -- a herança de características adquiridas não existe na biologia -- mas nada impede um AG digital de trapacear inteligentemente: aqui, transmitir conhecimento adquirido acelera radicalmente a convergência, já que cada geração reinicia a partir de uma inicialização já informada em vez de pesos aleatórios.

Outras três mudanças estruturais nesta versão:

  • Diversidade ambiental: cada genoma não é mais avaliado em uma única janela de mercado, mas em várias, extraídas de diferentes regimes (altista, baixista, lateral). Um agente que se destaca em uma janela e colapsa em outra não pode mais dominar a frente de Pareto.

  • Regularização de complexidade em FLOPs: o custo computacional da rede (em FLOPs) torna-se um objetivo completo no NSGA-II. Isso impede que a evolução convirja para arquiteturas massivas simplesmente porque têm mais capacidade bruta, sem um ganho de desempenho justificado.

  • Interface RLBackend desacoplada: o AG não conhece mais os detalhes do DQN. Ele manipula um genoma e chama train() / evaluate() através de uma interface abstrata, o que teoricamente permite trocar outro algoritmo de RL sem tocar no motor evolutivo.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Último ponto técnico: a avaliação mudou para concorrência assíncrona limitada; um pool de N avaliações paralelas em vez de um loop sequencial, com um limite explícito para evitar saturar os recursos de GPU/CPU disponíveis.

O que a v4 corrige em relação à v3 na prática

Defeito da v3 Correção da v4
Pesos perdidos a cada geração Reinjeção Lamarckiana de pesos treinados
Overfitting a uma única janela de mercado Avaliação em múltiplas janelas, regimes variados
Arquiteturas crescendo sem controle FLOPs como objetivo explícito de Pareto
AG acoplado a detalhes do DQN Interface abstrata RLBackend
Avaliação sequencial lenta Concorrência assíncrona limitada

A v4 também corrigiu dez erros concretos de "aterramento" da API; casos onde o código do AG assumia uma interface para TradingAgent que não correspondia exatamente à implementação real. Esse tipo de erro é invisível até que você confronte o código com a fonte real do agente: a v4 só foi validada após uma releitura linha por linha comparada com o arquivo real.

Por que misturar evolução e gradiente em vez de escolher um

Você pode se perguntar por que não usar apenas RL puro, ou apenas evolução como NEAT. A resposta é uma frase: o gradiente é excelente para ajuste local (ajustar pesos contínuos em direção a um ótimo próximo), a evolução é excelente para exploração global (descobrir arquiteturas e combinações de hiperparâmetros que nenhum gradiente pode alcançar, porque o espaço de busca discreto não é diferenciável). Usar um sem o outro significa privar-se de uma das duas formas de exploração.

O preço é a complexidade de engenharia; quatro versões não foram um luxo, foram o número de iterações necessárias para que o loop AG + RL parasse de sabotar a si mesmo (overfitting, perda de bons indivíduos, perda de pesos adquiridos). Mas o resultado é um sistema que explora um espaço de design muito mais amplo do que uma simples busca em grade de hiperparâmetros, mantendo ao mesmo tempo a eficiência local do gradiente descendente para cada candidato avaliado.

Próximo passo

Esta arquitetura evolutiva de nível único (uma população plana de genomas DQN) atinge seus limites quando o número de ativos a cobrir cresce. Foi isso que motivou a mudança para uma arquitetura hierárquica de três níveis (Analistas de Ativos → Gestores de Setor → Alocador de Portfólio), com um AG operando independentemente em cada nível... mas esse é o assunto de outro artigo.

Saya mengevolusi jaringan saraf melalui seleksi alam alih-alih gradient descent

Bagaimana saya mengganti pelatihan gradient descent klasik dengan algoritma genetik NSGA-II untuk mengevolusi agen trading DQN: empat versi, dari overfitting hingga evolusi bobot Lamarckian.

Masalah gradient descent saja

Melatih agen DQN untuk trading algoritmik dengan gradient descent klasik memiliki masalah yang sederhana untuk dinyatakan dan sulit untuk dipecahkan: gradient descent mengoptimalkan satu jaringan menuju satu minimum lokal, pada satu jendela pasar. Tidak ada yang menjamin minimum ini dapat digeneralisasi ke rezim pasar yang berbeda, dan tidak ada dalam loop pelatihan yang mendorong keberagaman; dua kali eksekusi dari seed yang berbeda sering kali konvergen ke strategi yang hampir identik, dengan titik buta yang sama.

Jawaban yang saya eksplorasi: mengganti (atau lebih tepatnya melapisi) gradient descent dengan algoritma genetik. Alih-alih melatih satu agen, Anda mengevolusi populasi agen; setiap genom mengkodekan arsitektur dan hiperparameter; dan seleksi alam melakukan pengurutan, sementara gradient descent terus menyempurnakan setiap individu dalam masa hidupnya sendiri.

Percobaan ini melalui empat versi dalam satu sesi intensif. Setiap versi memperbaiki cacat struktural dari versi sebelumnya.

v1: versi naif, dan mengapa itu tidak cukup

Versi pertama melakukan apa yang Anda harapkan dari GA dasar: populasi genom, fungsi fitness, seleksi, crossover, mutasi, generasi berikutnya. Setiap genom mengkodekan topologi jaringan (jumlah lapisan, lebar), hiperparameter DQN (learning rate, epsilon decay, ukuran buffer replay), dan beberapa pilihan arsitektural (sumber data mana yang digunakan, ukuran embedding apa).

Cacat utamanya: fitness dihitung pada data yang sama yang digunakan untuk pelatihan. Seorang agen benar-benar bisa menghafal jendela pasar dan mendapatkan skor sangat baik tanpa mempelajari strategi yang dapat digeneralisasi. Overfitting klasik, tetapi diperkuat oleh seleksi genetik; GA secara aktif memilih individu yang paling mengeksploitasi celah ini.

v2: memisahkan pelatihan dan evaluasi

Perbaikan yang jelas adalah memisahkan fase-fasenya: setiap genom berlatih pada satu jendela pasar, kemudian dievaluasi pada jendela yang berbeda, yang tidak pernah terlihat selama pelatihan. Hanya kinerja evaluasi yang diperhitungkan dalam fitness.

Perubahan ini saja menyebabkan rata-rata fitness populasi turun; sebuah tanda bahwa sebagian besar dari apa yang tampak seperti kinerja di v1 adalah hafalan murni. Menyakitkan untuk dilihat, tetapi itulah sinyal yang Anda inginkan: skor yang lebih rendah tetapi jujur lebih baik daripada skor yang meningkat dan menyesatkan.

v3: beralih ke NSGA-II dan fitness multi-objektif

Mengoptimalkan satu skor fitness (misalnya, imbal hasil) secara mekanis mendorong agen untuk mengambil risiko ekstrem demi memaksimalkan satu angka tersebut. Solusinya adalah beralih ke NSGA-II (Non-dominated Sorting Genetic Algorithm II), yang secara simultan mengoptimalkan beberapa tujuan tanpa mereduksinya menjadi jumlah terbobot yang arbitrer: imbal hasil, drawdown maksimum, rasio Sharpe, stabilitas antar-jendela.

NSGA-II membangun front Pareto: kumpulan genom di mana tidak ada peningkatan pada satu tujuan yang mungkin tanpa menurunkan tujuan lain. Alih-alih memaksakan satu trade-off imbal hasil-risiko melalui pembobotan yang telah dipilih sebelumnya, Anda menyimpan seluruh perbatasan kompromi dan membiarkan pilihan akhir tetap terbuka.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... konstruksi front berikutnya dengan penghapusan iteratif
  return fronts;
}

Penambahan kedua di v3: arsip Pareto persisten. Tanpanya, genom bagus yang ditemukan di generasi 12 bisa menghilang di generasi 15 jika keberuntungan crossover tidak mereproduksinya; bahkan jika genom itu tetap lebih baik dari semua yang menggantikannya. Arsip ini menyimpan, di semua generasi, kumpulan semua individu yang tidak terdominasi yang pernah ditemui, terlepas dari populasi saat ini.

v4: evolusi Lamarckian dan keragaman lingkungan

V3 memiliki titik buta struktural: genom menggambarkan arsitektur, tetapi bobot yang dipelajari selama pelatihan menghilang di setiap generasi baru. Seorang anak yang lahir dari crossover dua orang tua yang baik mewarisi arsitektur mereka, tetapi harus belajar dari awal lagi; tidak ada jejak bobot yang membuat orang tuanya berkinerja baik.

V4 memperkenalkan evolusi Lamarckian: bobot yang telah dilatih dimasukkan kembali ke dalam genom setelah pelatihan, dan ditransmisikan (dengan mutasi) kepada keturunannya. Ini adalah bid'ah biologis yang disengaja; Lamarck salah untuk organisme hidup -- pewarisan karakteristik yang diperoleh tidak ada dalam biologi -- tetapi tidak ada yang menghentikan GA digital untuk curang secara cerdas: di sini, mentransmisikan pengetahuan yang diperoleh secara radikal mempercepat konvergensi, karena setiap generasi memulai kembali dari inisialisasi yang sudah diinformasikan daripada bobot acak.

Tiga perubahan struktural lain dalam versi ini:

  • Keragaman lingkungan: setiap genom tidak lagi dievaluasi pada satu jendela pasar tunggal tetapi pada beberapa jendela, yang diambil dari rezim yang berbeda (bullish, bearish, ranging). Seorang agen yang unggul di satu jendela dan runtuh di jendela lain tidak lagi dapat mendominasi front Pareto.

  • Regularisasi kompleksitas FLOPs: biaya komputasi jaringan (dalam FLOPs) menjadi tujuan penuh dalam NSGA-II. Ini mencegah evolusi konvergen ke arsitektur masif hanya karena memiliki kapasitas mentah yang lebih besar, tanpa peningkatan kinerja yang dapat dibenarkan.

  • Antarmuka RLBackend yang terdekopling: GA tidak lagi mengetahui detail DQN. Ia memanipulasi genom dan memanggil train() / evaluate() melalui antarmuka abstrak, yang secara teoretis memungkinkan menukar algoritma RL lain tanpa menyentuh mesin evolusioner.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Poin teknis terakhir: evaluasi beralih ke konkurensi asinkron terbatas; kumpulan N evaluasi paralel alih-alih loop sekuensial, dengan batas eksplisit untuk menghindari kejenuhan sumber daya GPU/CPU yang tersedia.

Apa yang v4 perbaiki versus v3 dalam praktik

Cacat v3 Perbaikan v4
Bobot hilang setiap generasi Injeksi ulang Lamarckian dari bobot terlatih
Overfitting ke satu jendela pasar Evaluasi pada beberapa jendela, rezim bervariasi
Arsitektur tumbuh tanpa kendali FLOPs sebagai tujuan Pareto eksplisit
GA terikat ke detail DQN Antarmuka abstrak RLBackend
Evaluasi sekuensial lambat Konkurensi asinkron terbatas

V4 juga memperbaiki sepuluh bug "pembumian" API yang konkret; kasus di mana kode GA mengasumsikan antarmuka untuk TradingAgent yang tidak persis cocok dengan implementasi aslinya. Jenis bug ini tidak terlihat sampai Anda membandingkan kode dengan sumber agen yang sebenarnya: v4 hanya divalidasi setelah pembacaan ulang baris demi baris terhadap file asli.

Mengapa mencampur evolusi dan gradien daripada memilih salah satu

Anda mungkin bertanya-tanya mengapa tidak menggunakan RL murni saja, atau evolusi murni seperti NEAT. Jawabannya adalah satu kalimat: gradien sangat baik untuk penyetelan lokal (menyesuaikan bobot kontinu menuju titik optimal terdekat), evolusi sangat baik untuk eksplorasi global (menemukan arsitektur dan kombinasi hiperparameter yang tidak dapat dicapai gradien, karena ruang pencarian diskrit tidak dapat didiferensiasikan). Menggunakan salah satu tanpa yang lain berarti menghilangkan salah satu dari dua bentuk eksplorasi.

Harganya adalah kompleksitas rekayasa; empat versi bukanlah kemewahan, itu adalah jumlah iterasi yang diperlukan agar loop GA + RL berhenti menyabotase dirinya sendiri (overfitting, kehilangan individu yang baik, kehilangan bobot yang diperoleh). Tetapi hasilnya adalah sistem yang mengeksplorasi ruang desain yang jauh lebih luas daripada pencarian kisi sederhana dari hiperparameter, sambil mempertahankan efisiensi lokal dari gradient descent untuk setiap kandidat yang dievaluasi.

Langkah selanjutnya

Arsitektur evolusioner satu tingkat ini (populasi datar genom DQN) mencapai batasnya ketika jumlah aset yang akan dicakup bertambah. Itulah yang mendorong perpindahan ke arsitektur hierarkis tiga tingkat (Analis Aset → Manajer Sektor → Alokator Portofolio), dengan GA beroperasi secara independen di setiap tingkat... tapi itu adalah topik untuk artikel lain.

मैंने ग्रेडिएंट डिसेंट के बजाय प्राकृतिक चयन के माध्यम से एक तंत्रिका नेटवर्क विकसित किया

कैसे मैंने DQN ट्रेडिंग एजेंटों को विकसित करने के लिए क्लासिक ग्रेडिएंट डिसेंट प्रशिक्षण को NSGA-II आनुवंशिक एल्गोरिथम से बदला: चार संस्करण, ओवरफिटिंग से लेकर लैमार्कियन भार विकास तक।

अकेले ग्रेडिएंट डिसेंट की समस्या

क्लासिक ग्रेडिएंट डिसेंट के साथ एल्गोरिदमिक ट्रेडिंग के लिए DQN एजेंट को प्रशिक्षित करने में एक सरल समस्या है जिसे बताना आसान है और एक कठिन समस्या है जिसे हल करना मुश्किल है: ग्रेडिएंट डिसेंट एक नेटवर्क को एक स्थानीय न्यूनतम की ओर, एक बाज़ार विंडो पर अनुकूलित करता है। कोई गारंटी नहीं है कि यह न्यूनतम किसी भिन्न बाज़ार व्यवस्था के लिए सामान्यीकृत होगा, और प्रशिक्षण लूप में विविधता को बढ़ावा देने वाला कुछ भी नहीं है; अलग-अलग सीड से शुरू होने वाले दो रन अक्सर समान अंधे धब्बों के साथ लगभग समान रणनीतियों में अभिसरित होते हैं।

जिस उत्तर का मैंने पता लगाया: ग्रेडिएंट डिसेंट को एक आनुवंशिक एल्गोरिथम से बदलना (या बल्कि ओवरले करना)। एक एजेंट को प्रशिक्षित करने के बजाय, आप एजेंटों की एक आबादी विकसित करते हैं; प्रत्येक जीनोम एक आर्किटेक्चर और हाइपरपैरामीटर को एन्कोड करता है; और प्राकृतिक चयन छँटाई करता है, जबकि ग्रेडिएंट डिसेंट प्रत्येक व्यक्ति को उसके अपने जीवनकाल में ठीक करता रहता है।

यह प्रयोग एक गहन सत्र में चार संस्करणों से गुज़रा। प्रत्येक संस्करण ने पिछले वाले में एक संरचनात्मक दोष को ठीक किया।

v1: भोला संस्करण, और यह पर्याप्त क्यों नहीं था

पहले संस्करण ने वही किया जो आप एक बुनियादी GA से उम्मीद करेंगे: जीनोम की आबादी, एक फिटनेस फ़ंक्शन, चयन, क्रॉसओवर, उत्परिवर्तन, अगली पीढ़ी। प्रत्येक जीनोम ने नेटवर्क टोपोलॉजी (परतों की संख्या, चौड़ाई), DQN हाइपरपैरामीटर (सीखने की दर, एप्सिलॉन क्षय, रीप्ले बफ़र आकार), और कुछ आर्किटेक्चरल विकल्प (कौन से डेटा स्रोतों का उपभोग करना है, किस एम्बेडिंग आकार) को एन्कोड किया।

मुख्य दोष: फिटनेस की गणना उसी डेटा पर की गई जिसका उपयोग प्रशिक्षण के लिए किया गया था। एक एजेंट सचमुच एक बाज़ार विंडो को याद कर सकता है और सामान्यीकरण योग्य रणनीति सीखे बिना उत्कृष्ट स्कोर प्राप्त कर सकता है। क्लासिक ओवरफिटिंग, लेकिन आनुवंशिक चयन द्वारा प्रवर्धित; GA सक्रिय रूप से उन व्यक्तियों का चयन करता है जो इस खामी का सबसे अच्छा शोषण करते हैं।

v2: प्रशिक्षण और मूल्यांकन को अलग करना

स्पष्ट समाधान चरणों को अलग करना था: प्रत्येक जीनोम एक बाज़ार विंडो पर प्रशिक्षित होता है, फिर एक अलग विंडो पर मूल्यांकन किया जाता है, जो प्रशिक्षण के दौरान कभी नहीं देखा गया था। केवल मूल्यांकन प्रदर्शन फिटनेस में गिना जाता है।

इस अकेले बदलाव के कारण औसत जनसंख्या फिटनेस गिर गई; एक संकेत कि v1 में प्रदर्शन जैसा दिखने वाला एक बड़ा हिस्सा शुद्ध स्मरण था। देखना दर्दनाक है, लेकिन यह बिल्कुल वही संकेत है जो आप चाहते हैं: एक निचला लेकिन ईमानदार स्कोर एक बढ़े हुए, भ्रामक स्कोर से बेहतर है।

v3: NSGA-II और बहु-उद्देश्यीय फिटनेस में संक्रमण

एक एकल फिटनेस स्कोर (जैसे, रिटर्न) को अनुकूलित करना यांत्रिक रूप से एजेंटों को उस एक संख्या को अधिकतम करने के लिए अत्यधिक जोखिम लेने की ओर धकेलता है। समाधान NSGA-II (नॉन-डॉमिनेटेड सॉर्टिंग जेनेटिक एल्गोरिथम II) में स्विच करना था, जो एक साथ कई उद्देश्यों को अनुकूलित करता है बिना उन्हें एक मनमाना भारित योग में घटाए: रिटर्न, अधिकतम ड्रॉडाउन, शार्प अनुपात, अंतर-विंडो स्थिरता।

NSGA-II एक पेरेटो फ्रंट बनाता है: जीनोम का वह सेट जिसके लिए एक उद्देश्य पर कोई सुधार दूसरे को ख़राब किए बिना संभव नहीं है। पूर्व-चयनित भारांक के माध्यम से एक एकल रिटर्न-जोखिम समझौता थोपने के बजाय, आप पूरी समझौता सीमा रखते हैं और अंतिम विकल्प को खुला छोड़ देते हैं।

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... पुनरावृत्त हटाने द्वारा बाद के फ्रंट निर्माण
  return fronts;
}

v3 में दूसरा जोड़: एक स्थायी पेरेटो संग्रह। इसके बिना, पीढ़ी 12 में पाया गया एक अच्छा जीनोम पीढ़ी 15 तक गायब हो सकता है यदि क्रॉसओवर भाग्य इसे पुन: उत्पन्न नहीं करता है; भले ही यह उन सभी से बेहतर बना रहे जिन्होंने इसे बदला। संग्रह, सभी पीढ़ियों में, वर्तमान जनसंख्या की परवाह किए बिना, कभी भी सामने आए सभी गैर-प्रभुत्व वाले व्यक्तियों के सेट को रखता है।

v4: लैमार्कियन विकास और पर्यावरणीय विविधता

V3 में एक संरचनात्मक अंधा स्थान था: जीनोम ने आर्किटेक्चर का वर्णन किया, लेकिन प्रशिक्षण के दौरान सीखे गए भार प्रत्येक नई पीढ़ी में गायब हो गए। दो अच्छे माता-पिता के क्रॉसओवर से पैदा हुआ एक बच्चा उनकी आर्किटेक्चर विरासत में लेता था, लेकिन उसे शुरू से फिर से सीखना पड़ता था; उन भारों का कोई निशान नहीं जिन्होंने उसके माता-पिता को प्रभावी बनाया था।

V4 लैमार्कियन विकास का परिचय देता है: प्रशिक्षित भार प्रशिक्षण के बाद जीनोम में वापस फीड किए जाते हैं, और संतानों को (उत्परिवर्तन के साथ) प्रेषित किए जाते हैं। यह जानबूझकर जैविक विधर्म है; लैमार्क जीवित जीवों के लिए गलत था -- जीव विज्ञान में अर्जित विशेषताओं की विरासत मौजूद नहीं है -- लेकिन एक डिजिटल GA को बुद्धिमानी से धोखा देने से कोई नहीं रोकता है: यहाँ, अर्जित ज्ञान को प्रसारित करना अभिसरण को मौलिक रूप से तेज करता है, क्योंकि प्रत्येक पीढ़ी यादृच्छिक भार के बजाय पहले से सूचित प्रारंभिकीकरण से पुनः शुरू होती है।

इस संस्करण में तीन अन्य संरचनात्मक परिवर्तन:

  • पर्यावरणीय विविधता: प्रत्येक जीनोम का अब एक एकल बाज़ार विंडो पर मूल्यांकन नहीं किया जाता बल्कि कई पर किया जाता है, जो विभिन्न व्यवस्थाओं (तेजी, मंदी, रेंजिंग) से लिए गए हैं। एक एजेंट जो एक विंडो पर उत्कृष्ट प्रदर्शन करता है और दूसरी पर विफल हो जाता है, वह अब पेरेटो फ्रंट पर हावी नहीं हो सकता।

  • FLOPs जटिलता नियमीकरण: नेटवर्क की कम्प्यूटेशनल लागत (FLOPs में) NSGA-II में एक पूर्ण उद्देश्य बन जाती है। यह विकास को केवल इसलिए विशाल आर्किटेक्चर में अभिसरित होने से रोकता है क्योंकि उनके पास उचित प्रदर्शन लाभ के बिना अधिक कच्ची क्षमता है।

  • डिकपल्ड RLBackend इंटरफ़ेस: GA अब DQN विवरण नहीं जानता है। यह एक जीनोम में हेरफेर करता है और एक अमूर्त इंटरफ़ेस के माध्यम से train() / evaluate() को कॉल करता है, जो सैद्धांतिक रूप से विकासवादी इंजन को छुए बिना किसी अन्य RL एल्गोरिथम को स्वैप करने की अनुमति देता है।

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

अंतिम तकनीकी बिंदु: मूल्यांकन बाउंडेड एसिंक कंकरेंसी में बदल गया; अनुक्रमिक लूप के बजाय N समानांतर मूल्यांकनों का एक पूल, उपलब्ध GPU/CPU संसाधनों की संतृप्ति से बचने के लिए एक स्पष्ट सीमा के साथ।

व्यवहार में v4 बनाम v3 क्या ठीक करता है

v3 दोष v4 सुधार
भार हर पीढ़ी खो जाते हैं प्रशिक्षित भारों का लैमार्कियन पुनः इंजेक्शन
एक एकल बाज़ार विंडो पर ओवरफिटिंग कई विंडो, विविध व्यवस्थाओं पर मूल्यांकन
आर्किटेक्चर बिना किसी बाधा के बढ़ रहे हैं FLOPs स्पष्ट पेरेटो उद्देश्य के रूप में
GA DQN विवरणों से जुड़ा हुआ है सार RLBackend इंटरफ़ेस
धीमी अनुक्रमिक मूल्यांकन बाउंडेड एसिंक कंकरेंसी

V4 ने दस ठोस API "ग्राउंडिंग" बग भी ठीक किए; ऐसे मामले जहां GA कोड ने TradingAgent के लिए एक इंटरफ़ेस मान लिया जो वास्तविक कार्यान्वयन से बिल्कुल मेल नहीं खाता था। इस प्रकार का बग तब तक अदृश्य रहता है जब तक आप कोड को वास्तविक एजेंट स्रोत के सामने नहीं रखते: v4 को वास्तविक फ़ाइल के विरुद्ध पंक्ति-दर-पंक्ति पुन: पढ़ने के बाद ही मान्य किया गया था।

एक को चुनने के बजाय विकास और ग्रेडिएंट को क्यों मिलाएं

आप सोच सकते हैं कि केवल शुद्ध RL, या NEAT जैसे शुद्ध विकास का उपयोग क्यों नहीं करते। उत्तर एक वाक्य है: ग्रेडिएंट स्थानीय फ़ाइन-ट्यूनिंग (एक निकटतम इष्टतम की ओर निरंतर भार को समायोजित करना) के लिए उत्कृष्ट है, विकास वैश्विक अन्वेषण (आर्किटेक्चर और हाइपरपैरामीटर संयोजनों की खोज करना जो कोई ग्रेडिएंट नहीं पहुंच सकता, क्योंकि असतत खोज स्थान अवकलनीय नहीं है) के लिए उत्कृष्ट है। एक के बिना दूसरे का उपयोग करने का मतलब है दो रूपों में से एक अन्वेषण से खुद को वंचित करना।

कीमत इंजीनियरिंग जटिलता है; चार संस्करण विलासिता नहीं थे, वे उन पुनरावृत्तियों की संख्या थे जिनकी GA + RL लूप को स्वयं को तोड़फोड़ करने से रोकने के लिए आवश्यकता थी (ओवरफिटिंग, अच्छे व्यक्तियों की हानि, अर्जित भार की हानि)। लेकिन परिणाम एक ऐसी प्रणाली है जो हाइपरपैरामीटर की एक साधारण ग्रिड खोज की तुलना में बहुत व्यापक डिज़ाइन स्थान की खोज करती है, साथ ही मूल्यांकन किए गए प्रत्येक उम्मीदवार के लिए ग्रेडिएंट डिसेंट की स्थानीय दक्षता बनाए रखती है।

अगला कदम

यह एकल-स्तरीय विकासवादी आर्किटेक्चर (DQN जीनोम की एक सपाट आबादी) अपनी सीमाओं तक पहुँच जाता है जब कवर करने के लिए परिसंपत्तियों की संख्या बढ़ती है। यही वह चीज़ है जिसने तीन-स्तरीय पदानुक्रमित आर्किटेक्चर (एसेट एनालिस्ट → सेक्टर मैनेजर → पोर्टफोलियो अलोकेटर) में जाने के लिए प्रेरित किया, जिसमें प्रत्येक स्तर पर स्वतंत्र रूप से काम करने वाला GA है... लेकिन यह एक अन्य लेख का विषय है।

طورت شبكة عصبية عبر الانتقاء الطبيعي بدلاً من الانحدار المتدرج

كيف استبدلت تدريب الانحدار المتدرج التقليدي بخوارزمية جينية NSGA-II لتطوير وكلاء تداول DQN: أربعة إصدارات، من الإفراط في التخصيص إلى تطور لامارك للأوزان.

المشكلة مع الانحدار المتدرج وحده

تدريب وكيل DQN للتداول الخوارزمي باستخدام الانحدار المتدرج التقليدي له مشكلة بسيطة في الصياغة وأخرى صعبة في الحل: الانحدار المتدرج يحسِّن شبكة واحدة نحو حد أدنى محلي واحد، على نافذة سوق واحدة. لا شيء يضمن أن هذا الحد الأدنى يعمم على نظام سوق مختلف، ولا يوجد في حلقة التدريب ما يدفع نحو التنوع؛ تشغيلتان تبدأان من بذور مختلفة غالباً ما تتقاربان إلى استراتيجيات متطابقة تقريباً، مع نفس النقاط العمياء.

الإجابة التي استكشفتها: استبدال (أو بالأحرى تراكب) الانحدار المتدرج بخوارزمية جينية. بدلاً من تدريب وكيل واحد، تقوم بتطوير مجموعة من الوكلاء؛ كل جينوم يرمز بنية معماريّة ومعلمات تشعبية؛ والانتقاء الطبيعي يقوم بالفرز، بينما يستمر الانحدار المتدرج في ضبط كل فرد ضمن عمره الخاص.

مر هذا المشروع بأربعة إصدارات في جلسة واحدة مكثفة. كل إصدار أصلح خللاً بنيوياً في الإصدار السابق.

v1: الإصدار الساذج، ولماذا لم يكن كافياً

الإصدار الأول فعل ما تتوقعه من خوارزمية جينية أساسية: مجموعة من الجينومات، دالة لياقة، انتقاء، تقاطع، طفرة، جيل تالٍ. كل جينوم يرمز طوبولوجيا الشبكة (عدد الطبقات، العرض)، معلمات DQN التشعبية (معدل التعلم، اضمحلال إبسيلون، حجم مخزن الاستدعاء)، وبعض الاختيارات المعمارية (أي مصادر البيانات يستهلكها، ما حجم التضمين).

الخلل الرئيسي: تم حساب اللياقة على نفس البيانات المستخدمة في التدريب. يمكن للوكيل حرفياً حفظ نافذة سوق والحصول على درجة ممتازة دون أن يتعلم استراتيجية قابلة للتعميم. إفراط تقليدي في التخصيص، لكنه مضخم بالانتقاء الجيني؛ الخوارزمية الجينية تختار بنشاط الأفراد الذين يستغلون هذه الثغرة بشكل أفضل.

v2: فصل التدريب والتقييم

الإصلاح الواضح كان فصل المراحل: كل جينوم يتدرب على نافذة سوق واحدة، ثم يُقيَّم على نافذة مختلفة، لم يرها أثناء التدريب. أداء التقييم فقط هو الذي يُحتسب في اللياقة.

هذا التغيير وحده تسبب في انخفاض متوسط لياقة السكان؛ علامة على أن جزءاً كبيراً مما بدا وكأنه أداء في v1 كان حفظاً محضاً. مؤلم رؤيته، لكنه بالضبط الإشارة التي تريدها: درجة أقل لكنها صادقة أفضل من درجة منتفخة ومضللة.

v3: الانتقال إلى NSGA-II واللياقة متعددة الأهداف

تحسين درجة لياقة واحدة (مثل العوائد) يدفع الوكلاء ميكانيكياً نحو المخاطرة الشديدة لتعظيم هذا الرقم الواحد. كان الحل هو الانتقال إلى NSGA-II (خوارزمية الفرز غير المسيطر عليها الجينية II)، التي تعمل على تحسين عدة أهداف في وقت واحد دون اختزالها إلى مجموع مرجح تعسفي: العوائد، الحد الأقصى للانخفاض، نسبة شارب، الاستقرار عبر النوافذ.

NSGA-II تبني جبهة باريتو: مجموعة الجينومات التي لا يمكن تحسين أي هدف فيها دون تدهور آخر. بدلاً من فرض مفاضلة واحدة بين العائد والمخاطرة من خلال ترجيح محدد مسبقاً، تحتفظ بحدود التسوية بأكملها وتترك الخيار النهائي مفتوحاً.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... بناء الجبهات اللاحقة عن طريق الإزالة التكرارية
  return fronts;
}

الإضافة الثانية في v3: أرشيف باريتو المستمر. بدونه، يمكن أن يختفي جينوم جيد وُجد في الجيل 12 بحلول الجيل 15 إذا لم يحالفه حظ التقاطع في إعادة إنتاجه؛ حتى لو بقي أفضل من كل ما حل محله. يحتفظ الأرشيف، عبر جميع الأجيال، بمجموعة جميع الأفراد غير المسيطر عليهم الذين تم العثور عليهم على الإطلاق، بغض النظر عن السكان الحاليين.

v4: التطور اللاماركي والتنوع البيئي

كان لدى v3 نقطة عمياء هيكلية: الجينوم يصف البنية المعمارية، لكن الأوزان التي تم تعلمها أثناء التدريب تختفي عند كل جيل جديد. الطفل المولود من تقاطع والدين جيدين يرث بنيتهما المعمارية، لكن عليه إعادة التعلم من الصفر؛ لا أثر للأوزان التي جعلت والديه أدائيين.

v4 تقدم التطور اللاماركي: يتم إعادة الأوزان المدربة إلى الجينوم بعد التدريب، ونقلها (مع الطفرة) إلى النسل. هذا بدعة بيولوجية متعمدة؛ لامارك كان مخطئاً بالنسبة للكائنات الحية ـ وراثة الصفات المكتسبة غير موجودة في علم الأحياء ـ لكن لا شيء يمنع الخوارزمية الجينية الرقمية من الغش بذكاء: هنا، نقل المعرفة المكتسبة يسرّع التقارب بشكل جذري، لأن كل جيل يبدأ من تهيئة مسبقة مطلعة بدلاً من أوزان عشوائية.

ثلاثة تغييرات هيكلية أخرى في هذا الإصدار:

  • التنوع البيئي: لم يعد كل جينوم يُقيَّم على نافذة سوق واحدة بل على عدة نوافذ، مسحوبة من أنظمة مختلفة (صاعد، هابط، متذبذب). لم يعد بإمكان وكيل يتفوق في نافذة وينهار في أخرى أن يهيمن على جبهة باريتو.

  • تنظيم تعقيد FLOPs: تصبح التكلفة الحسابية للشبكة (بـ FLOPs) هدفاً كاملاً في NSGA-II. هذا يمنع التطور من التقارب إلى بنى معمارية ضخمة لمجرد أنها تملك سعة خام أكبر، دون مكسب أداء مبرر.

  • واجهة RLBackend المفصولة: الخوارزمية الجينية لم تعد تعرف تفاصيل DQN. إنها تتعامل مع جينوم وتستدعي train() / evaluate() عبر واجهة مجردة، مما يسمح نظرياً باستبدال خوارزمية تعزيز أخرى دون المساس بمحرك التطور.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

آخر نقطة تقنية: التقييم تحول إلى ترابط غير متزامن محدود؛ مجموعة من N تقييمات متوازية بدلاً من حلقة تسلسلية، مع حد صريح لتجنب إشباع موارد GPU/CPU المتاحة.

ما يصلحه v4 مقابل v3 عملياً

خلل v3 إصلاح v4
الأوزان تُفقد كل جيل إعادة الحقن اللاماركي للأوزان المدربة
الإفراط في التخصيص لنافذة سوق واحدة تقييم على نوافذ متعددة، وأنظمة متنوعة
البنى المعمارية تنمو بدون قيود FLOPs كهدف صريح في باريتو
GA مقترنة بتفاصيل DQN واجهة RLBackend مجردة
تقييم تسلسلي بطيء ترابط غير متزامن محدود

v4 أيضاً أصلح عشرة أخطاء ملموسة في "توافق" API؛ حالات افترض فيها كود GA واجهة لـ TradingAgent لم تطابق التنفيذ الفعلي بالضبط. هذا النوع من الأخطاء غير مرئي حتى تواجه الكود مع مصدر الوكيل الفعلي: تم التحقق من صحة v4 فقط بعد إعادة قراءة سطر بسطر مقابل الملف الحقيقي.

لماذا مزج التطور والانحدار بدلاً من اختيار واحد

قد تتساءل لماذا لا تستخدم RL الخالص، أو التطور الخالص مثل NEAT. الإجابة في جملة واحدة: الانحدار ممتاز للضبط المحلي (تعديل الأوزان المستمرة نحو أمثل قريب)، والتطور ممتاز للاستكشاف العالمي (اكتشاف بنى معمارية وتوليفات معلمات تشعبية لا يمكن لأي انحدار الوصول إليها، لأن فضاء البحث المتقطع غير قابل للاشتقاق). استخدام أحدهما دون الآخر يعني حرمان نفسك من أحد شكلي الاستكشاف.

الثمن هو التعقيد الهندسي؛ أربعة إصدارات لم تكن ترفاً، بل كانت عدد التكرارات اللازمة لكي تتوقف حلقة GA + RL عن تخريب نفسها (الإفراط في التخصيص، فقدان الأفراد الجيدين، فقدان الأوزان المكتسبة). لكن النتيجة هي نظام يستكشف فضاء تصميم أوسع بكثير من بحث شبكي بسيط للمعلمات التشعبية، مع الحفاظ على الكفاءة المحلية للانحدار المتدرج لكل مرشح يتم تقييمه.

الخطوة التالية

هذه البنية التطورية أحادية المستوى (مجموعة مسطحة من جينومات DQN) تصل إلى حدودها عندما ينمو عدد الأصول المراد تغطيتها. هذا ما دفع إلى الانتقال إلى بنية هرمية ثلاثية المستويات (محللو الأصول ← مديرو القطاعات ← موزع المحفظة)، مع خوارزمية جينية تعمل بشكل مستقل في كل مستوى... لكن هذا موضوع مقال آخر.

Tôi đã tiến hóa mạng nơ-ron qua chọn lọc tự nhiên thay vì gradient descent

Cách tôi thay thế huấn luyện gradient descent cổ điển bằng thuật toán di truyền NSGA-II để tiến hóa các tác nhân giao dịch DQN: bốn phiên bản, từ overfitting đến tiến hóa Lamarckian trọng số.

Vấn đề với chỉ gradient descent

Huấn luyện một tác nhân DQN cho giao dịch thuật toán bằng gradient descent cổ điển có một vấn đề đơn giản để phát biểu và một vấn đề khó để giải quyết: gradient descent tối ưu hóa một mạng về một cực tiểu địa phương, trên một cửa sổ thị trường. Không gì đảm bảo cực tiểu này tổng quát hóa được sang một chế độ thị trường khác, và không có gì trong vòng lặp huấn luyện thúc đẩy sự đa dạng; hai lần chạy từ các seed khác nhau thường hội tụ về các chiến lược gần như giống hệt nhau, với cùng những điểm mù.

Câu trả lời tôi khám phá: thay thế (hay đúng hơn là phủ lên) gradient descent bằng một thuật toán di truyền. Thay vì huấn luyện một tác nhân, bạn tiến hóa một quần thể các tác nhân; mỗi bộ gen mã hóa một kiến trúc và các siêu tham số; và chọn lọc tự nhiên thực hiện việc phân loại, trong khi gradient descent tiếp tục tinh chỉnh từng cá thể trong vòng đời của nó.

Dự án này đã trải qua bốn phiên bản trong một phiên làm việc chuyên sâu duy nhất. Mỗi phiên bản sửa một lỗ hổng cấu trúc của phiên bản trước.

v1: phiên bản ngây thơ, và tại sao nó chưa đủ

Phiên bản đầu tiên làm những gì bạn mong đợi từ một GA cơ bản: một quần thể bộ gen, một hàm fitness, chọn lọc, lai ghép, đột biến, thế hệ tiếp theo. Mỗi bộ gen mã hóa cấu trúc liên kết mạng (số lớp, độ rộng), các siêu tham số DQN (tốc độ học, suy giảm epsilon, kích thước bộ đệm replay), và một vài lựa chọn kiến trúc (nguồn dữ liệu nào để tiêu thụ, kích thước embedding).

Lỗ hổng chính: fitness được tính trên cùng dữ liệu dùng cho huấn luyện. Một tác nhân có thể ghi nhớ nguyên một cửa sổ thị trường và đạt điểm xuất sắc mà không học được chiến lược tổng quát hóa. Overfitting kinh điển, nhưng được khuếch đại bởi chọn lọc di truyền; GA chủ động chọn lọc các cá thể khai thác lỗ hổng này tốt nhất.

v2: tách biệt huấn luyện và đánh giá

Sửa lỗi hiển nhiên là tách biệt các giai đoạn: mỗi bộ gen huấn luyện trên một cửa sổ thị trường, sau đó được đánh giá trên một cửa sổ khác, chưa từng thấy trong huấn luyện. Chỉ hiệu suất đánh giá mới được tính vào fitness.

Riêng thay đổi này đã khiến fitness trung bình của quần thể giảm; một dấu hiệu cho thấy một phần lớn những gì trông như hiệu suất trong v1 chỉ là ghi nhớ thuần túy. Thật khó nhìn nhận, nhưng đó chính xác là tín hiệu bạn muốn: một điểm số thấp hơn nhưng trung thực tốt hơn một điểm số thổi phồng, gây hiểu lầm.

v3: chuyển sang NSGA-II và fitness đa mục tiêu

Tối ưu hóa một điểm fitness duy nhất (ví dụ lợi nhuận) đẩy các tác nhân về mặt cơ học đến việc chấp nhận rủi ro cực đoan để tối đa hóa con số đơn lẻ đó. Giải pháp là chuyển sang NSGA-II (Thuật toán di truyền sắp xếp không bị trội II), đồng thời tối ưu hóa nhiều mục tiêu mà không quy chúng thành một tổng trọng số tùy ý: lợi nhuận, mức sụt giảm tối đa, tỷ lệ Sharpe, độ ổn định giữa các cửa sổ.

NSGA-II xây dựng một mặt Pareto: tập hợp các bộ gen mà không thể cải thiện mục tiêu này mà không làm suy giảm mục tiêu khác. Thay vì ép buộc một sự đánh đổi lợi nhuận-rủi ro duy nhất thông qua trọng số được chọn trước, bạn giữ toàn bộ ranh giới thỏa hiệp và để ngỏ lựa chọn cuối cùng.

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... xây dựng các mặt tiếp theo bằng cách loại bỏ lặp
  return fronts;
}

Bổ sung thứ hai trong v3: một lưu trữ Pareto bền vững. Nếu không có nó, một bộ gen tốt được tìm thấy ở thế hệ 12 có thể biến mất ở thế hệ 15 nếu may mắn lai ghép không tái tạo được nó; ngay cả khi nó vẫn tốt hơn mọi thứ thay thế nó. Lưu trữ này giữ lại, qua tất cả các thế hệ, tập hợp tất cả các cá thể không bị trội từng gặp, bất kể quần thể hiện tại.

v4: tiến hóa Lamarckian và đa dạng môi trường

V3 có một điểm mù cấu trúc: bộ gen mô tả kiến trúc, nhưng các trọng số đã học trong quá trình huấn luyện biến mất ở mỗi thế hệ mới. Một đứa con sinh ra từ lai ghép của hai cha mẹ tốt thừa hưởng kiến trúc của họ, nhưng phải học lại từ đầu; không dấu vết nào của các trọng số đã làm cho cha mẹ nó hoạt động tốt.

V4 giới thiệu tiến hóa Lamarckian: các trọng số đã huấn luyện được đưa trở lại vào bộ gen sau huấn luyện, và truyền lại (có đột biến) cho con cháu. Đây là dị giáo sinh học có chủ đích; Lamarck đã sai đối với sinh vật sống -- di truyền các đặc tính thu được không tồn tại trong sinh học -- nhưng không gì ngăn một GA kỹ thuật số gian lận một cách thông minh: ở đây, truyền lại kiến thức thu được giúp tăng tốc hội tụ triệt để, vì mỗi thế hệ khởi động lại từ một khởi tạo đã có thông tin thay vì trọng số ngẫu nhiên.

Ba thay đổi cấu trúc khác trong phiên bản này:

  • Đa dạng môi trường: mỗi bộ gen không còn được đánh giá trên một cửa sổ thị trường duy nhất mà trên nhiều cửa sổ, được lấy từ các chế độ khác nhau (tăng, giảm, đi ngang). Một tác nhân xuất sắc trên một cửa sổ và sụp đổ trên cửa sổ khác không còn có thể thống trị mặt Pareto.

  • Chính quy hóa độ phức tạp FLOPs: chi phí tính toán của mạng (tính bằng FLOPs) trở thành một mục tiêu đầy đủ trong NSGA-II. Điều này ngăn tiến hóa hội tụ về các kiến trúc khổng lồ chỉ vì chúng có nhiều dung lượng thô hơn, mà không có sự cải thiện hiệu suất chính đáng.

  • Giao diện RLBackend tách rời: GA không còn biết chi tiết DQN. Nó thao tác một bộ gen và gọi train() / evaluate() thông qua một giao diện trừu tượng, về mặt lý thuyết cho phép hoán đổi một thuật toán RL khác mà không chạm vào động cơ tiến hóa.

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Điểm kỹ thuật cuối: đánh giá chuyển sang tương tranh bất đồng bộ có giới hạn; một nhóm N đánh giá song song thay vì vòng lặp tuần tự, với một giới hạn rõ ràng để tránh bão hòa tài nguyên GPU/CPU khả dụng.

v4 sửa gì so với v3 trong thực tế

Lỗi v3 Sửa v4
Trọng số mất mỗi thế hệ Tái tiêm Lamarckian trọng số đã huấn luyện
Overfitting vào một cửa sổ thị trường duy nhất Đánh giá trên nhiều cửa sổ, chế độ đa dạng
Kiến trúc phát triển không kiểm soát FLOPs như mục tiêu Pareto rõ ràng
GA gắn chặt với chi tiết DQN Giao diện RLBackend trừu tượng
Đánh giá tuần tự chậm Tương tranh bất đồng bộ có giới hạn

V4 cũng sửa mười lỗi "đối sánh" API cụ thể; các trường hợp mã GA giả định một giao diện cho TradingAgent không khớp chính xác với triển khai thực tế. Loại lỗi này vô hình cho đến khi bạn đối chiếu mã với mã nguồn tác nhân thực: v4 chỉ được xác thực sau khi đọc lại từng dòng so sánh với tệp thực.

Tại sao kết hợp tiến hóa và gradient thay vì chọn một

Bạn có thể tự hỏi tại sao không chỉ dùng RL thuần túy, hoặc tiến hóa thuần túy như NEAT. Câu trả lời trong một câu: gradient xuất sắc cho tinh chỉnh cục bộ (điều chỉnh các trọng số liên tục về một tối ưu lân cận), tiến hóa xuất sắc cho khám phá toàn cục (khám phá các kiến trúc và tổ hợp siêu tham số mà không gradient nào có thể đạt tới, vì không gian tìm kiếm rời rạc không khả vi). Sử dụng cái này mà không có cái kia có nghĩa là tự tước đi một trong hai hình thức khám phá.

Cái giá là độ phức tạp kỹ thuật; bốn phiên bản không phải là xa xỉ, chúng là số lần lặp cần thiết để vòng lặp GA + RL ngừng tự phá hoại (overfitting, mất cá thể tốt, mất trọng số thu được). Nhưng kết quả là một hệ thống khám phá một không gian thiết kế rộng hơn nhiều so với tìm kiếm lưới đơn giản các siêu tham số, trong khi vẫn giữ được hiệu quả cục bộ của gradient descent cho mỗi ứng viên được đánh giá.

Bước tiếp theo

Kiến trúc tiến hóa đơn cấp này (một quần thể phẳng các bộ gen DQN) đạt đến giới hạn khi số lượng tài sản cần bao phủ tăng lên. Đó là động lực cho việc chuyển sang kiến trúc phân cấp ba cấp (Chuyên viên phân tích tài sản → Quản lý ngành → Phân bổ danh mục đầu tư), với một GA hoạt động độc lập ở mỗi cấp... nhưng đó là chủ đề của một bài viết khác.

ฉันวิวัฒนาการโครงข่ายประสาทเทียมผ่านการคัดเลือกโดยธรรมชาติแทนที่จะใช้เกรเดียนต์ดีเซนต์

วิธีที่ฉันแทนที่การฝึกแบบเกรเดียนต์ดีเซนต์ดั้งเดิมด้วยอัลกอริทึมพันธุกรรม NSGA-II เพื่อวิวัฒนาการเอเยนต์เทรดดิ้ง DQN: สี่เวอร์ชัน จากการโอเวอร์ฟิตติ้งสู่การวิวัฒนาการแบบลามาร์กของน้ำหนัก

ปัญหาของเกรเดียนต์ดีเซนต์เพียงอย่างเดียว

การฝึกเอเยนต์ DQN สำหรับการเทรดดิ้งเชิงอัลกอริทึมด้วยเกรเดียนต์ดีเซนต์ดั้งเดิมมีปัญหาที่พูดง่ายแต่แก้ยาก: เกรเดียนต์ดีเซนต์ปรับให้เหมาะสมกับโครงข่าย หนึ่ง สู่จุดต่ำสุดเฉพาะที่ หนึ่ง บนหน้าต่างตลาด หนึ่ง ไม่มีอะไรรับประกันว่าจุดต่ำสุดนี้จะสามารถสรุป generalize ไปยังสภาวะตลาดอื่นได้ และไม่มีอะไรในลูปการฝึกที่ผลักดันให้เกิดความหลากหลาย การรันสองครั้งจาก seed ต่างกันมักจะลู่เข้าสู่กลยุทธ์ที่เกือบเหมือนกัน โดยมีจุดบอดเดียวกัน

คำตอบที่ฉันสำรวจ: แทนที่ (หรือวางซ้อน) เกรเดียนต์ดีเซนต์ด้วยอัลกอริทึมพันธุกรรม แทนที่จะฝึกเอเยนต์ตัวเดียว คุณวิวัฒนาการประชากรของเอเยนต์; จีโนมแต่ละตัวเข้ารหัสสถาปัตยกรรมและไฮเปอร์พารามิเตอร์; และการคัดเลือกโดยธรรมชาติทำการจัดเรียง ในขณะที่เกรเดียนต์ดีเซนต์ยังคงปรับแต่งรายบุคคลภายในช่วงชีวิตของมัน

โปรเจกต์นี้ผ่านสี่เวอร์ชันในเซสชันเข้มข้นครั้งเดียว แต่ละเวอร์ชันแก้ไขจุดบกพร่องเชิงโครงสร้างของเวอร์ชันก่อนหน้า

v1: เวอร์ชันพื้นฐาน และเหตุผลที่มันยังไม่พอ

เวอร์ชันแรกทำสิ่งที่คุณคาดหวังจาก GA พื้นฐาน: ประชากรของจีโนม, ฟังก์ชันฟิตเนส, การคัดเลือก, การไขว้เปลี่ยน, การกลายพันธุ์, รุ่นถัดไป จีโนมแต่ละตัวเข้ารหัสโทโพโลยีของโครงข่าย (จำนวนเลเยอร์, ความกว้าง), ไฮเปอร์พารามิเตอร์ DQN (อัตราการเรียนรู้, การลดค่าเอปซิลอน, ขนาดบัฟเฟอร์รีเพลย์), และตัวเลือกทางสถาปัตยกรรมบางอย่าง (แหล่งข้อมูลใดที่จะใช้, ขนาด embedding)

ข้อบกพร่องหลัก: ฟิตเนสถูกคำนวณบนข้อมูลเดียวกับที่ใช้ในการฝึก เอเยนต์สามารถจดจำหน้าต่างตลาดทั้งหน้าต่างและได้คะแนนยอดเยี่ยมโดยไม่ได้เรียนรู้กลยุทธ์ที่สามารถ generalize ได้ โอเวอร์ฟิตติ้งแบบคลาสสิก แต่ถูกขยายโดยการคัดเลือกทางพันธุกรรม; GA คัดเลือกบุคคลที่ใช้ช่องโหว่นี้ได้ดีที่สุดอย่างจริงจัง

v2: แยกการฝึกและการประเมินผล

การแก้ไขที่ชัดเจนคือการแยกเฟส: จีโนมแต่ละตัวฝึกบนหน้าต่างตลาดหนึ่ง จากนั้นถูกประเมินบนหน้าต่างที่แตกต่างกัน ซึ่งไม่เคยเห็นระหว่างการฝึก เฉพาะประสิทธิภาพการประเมินเท่านั้นที่นับเป็นฟิตเนส

การเปลี่ยนแปลงนี้เพียงอย่างเดียวทำให้ฟิตเนสเฉลี่ยของประชากรลดลง; สัญญาณว่าส่วนใหญ่ของสิ่งที่ดูเหมือนประสิทธิภาพใน v1 เป็นการจดจำล้วนๆ มันเจ็บปวดที่ต้องเห็น แต่มันเป็นสัญญาณที่คุณต้องการ: คะแนนที่ต่ำกว่าแต่ซื่อสัตย์ดีกว่าคะแนนที่สูงเกินจริงและทำให้เข้าใจผิด

v3: ย้ายไปใช้ NSGA-II และฟิตเนสแบบหลายวัตถุประสงค์

การปรับคะแนนฟิตเนสเดียว (เช่น ผลตอบแทน) ผลักดันเอเยนต์โดยกลไกให้รับความเสี่ยงสูงสุดเพื่อเพิ่มตัวเลขเดียวนั้นให้สูงสุด ทางออกคือการเปลี่ยนไปใช้ NSGA-II (อัลกอริทึมพันธุกรรมการจัดเรียงแบบไม่โดนครอบงำ II) ซึ่งปรับหลายวัตถุประสงค์พร้อมกันโดยไม่ลดทอนให้เป็นผลรวมถ่วงน้ำหนักตามอำเภอใจ: ผลตอบแทน, การลดลงสูงสุด, อัตราส่วน Sharpe, ความเสถียรข้ามหน้าต่าง

NSGA-II สร้างแนวหน้า Pareto: เซตของจีโนมซึ่งไม่สามารถปรับปรุงวัตถุประสงค์หนึ่งได้โดยไม่ทำให้อีกวัตถุประสงค์แย่ลง แทนที่จะบังคับการแลกเปลี่ยนผลตอบแทน-ความเสี่ยงเดียวผ่านการถ่วงน้ำหนักที่เลือกไว้ล่วงหน้า คุณเก็บเส้นแบ่งประนีประนอมทั้งหมดไว้และปล่อยให้ตัวเลือกสุดท้ายเปิดกว้าง

function nonDominatedSort(population: Genome[]): Genome[][] {
  const fronts: Genome[][] = [[]];
  for (const p of population) {
    p.dominationCount = 0;
    p.dominatedSet = [];
    for (const q of population) {
      if (dominates(p, q)) p.dominatedSet.push(q);
      else if (dominates(q, p)) p.dominationCount++;
    }
    if (p.dominationCount === 0) {
      p.rank = 0;
      fronts[0].push(p);
    }
  }
  // ... การสร้างแนวหน้าถัดไปโดยการเอาออกซ้ำ
  return fronts;
}

การเพิ่มเติมที่สองใน v3: คลังเก็บ Pareto แบบถาวร หากไม่มีมัน จีโนมที่ดีที่พบในรุ่นที่ 12 สามารถหายไปในรุ่นที่ 15 ได้หากโชคในการไขว้เปลี่ยนไม่สืบพันธุ์มัน; แม้ว่ามันจะยังดีกว่าทุกสิ่งที่มาแทนที่มัน คลังเก็บจะเก็บรักษา, ข้ามทุกรุ่น, เซตของบุคคลที่ไม่โดนครอบงำทั้งหมดที่เคยพบ, โดยไม่คำนึงถึงประชากรปัจจุบัน

v4: การวิวัฒนาการแบบลามาร์กและความหลากหลายของสภาพแวดล้อม

V3 มีจุดบอดเชิงโครงสร้าง: จีโนมอธิบายสถาปัตยกรรม แต่น้ำหนักที่เรียนรู้ระหว่างการฝึกหายไปในทุกรุ่นใหม่ ลูกที่เกิดจากการไขว้เปลี่ยนของพ่อแม่ที่ดีสองตัวสืบทอดสถาปัตยกรรมของพวกเขา แต่ต้องเรียนรู้ใหม่ตั้งแต่ต้น; ไม่มีร่องรอยของน้ำหนักที่ทำให้พ่อแม่ของมันมีประสิทธิภาพ

V4 นำเสนอ การวิวัฒนาการแบบลามาร์ก: น้ำหนักที่ฝึกแล้วถูกป้อนกลับเข้าไปในจีโนมหลังการฝึก และส่งต่อ (พร้อมการกลายพันธุ์) ไปยังลูกหลาน นี่เป็นการนอกศาสนาทางชีววิทยาโดยเจตนา; ลามาร์กผิดสำหรับสิ่งมีชีวิต -- การถ่ายทอดลักษณะที่ได้มาไม่มีอยู่ในชีววิทยา -- แต่ไม่มีอะไรหยุด GA ดิจิทัลจากการโกงอย่างชาญฉลาด: ที่นี่ การส่งต่อความรู้ที่ได้มาช่วยเร่งการลู่เข้าอย่างรุนแรง เนื่องจากแต่ละรุ่นเริ่มต้นจากการเริ่มต้นที่มีข้อมูลแล้วแทนที่จะเป็นน้ำหนักสุ่ม

การเปลี่ยนแปลงเชิงโครงสร้างอีกสามอย่างในเวอร์ชันนี้:

  • ความหลากหลายของสภาพแวดล้อม: จีโนมแต่ละตัวไม่ถูกประเมินอีกต่อไปบนหน้าต่างตลาดเดียว แต่บนหลายหน้าต่าง ที่ดึงจากสภาวะต่างๆ (ขาขึ้น, ขาลง, sideways) เอเยนต์ที่เก่งในหน้าต่างหนึ่งและพังในอีกหน้าต่างไม่สามารถครอบงำแนวหน้า Pareto ได้อีกต่อไป

  • การทำให้เป็นปกติของความซับซ้อน FLOPs: ต้นทุนการคำนวณของโครงข่าย (ในหน่วย FLOPs) กลายเป็นวัตถุประสงค์เต็มรูปแบบใน NSGA-II สิ่งนี้ป้องกันการวิวัฒนาการจากการลู่เข้าสู่สถาปัตยกรรมขนาดใหญ่เพียงเพราะมันมีขีดความสามารถดิบมากกว่า โดยไม่มีการเพิ่มประสิทธิภาพที่สมเหตุสมผล

  • อินเทอร์เฟซ RLBackend ที่แยกออกจากกัน: GA ไม่รู้รายละเอียด DQN อีกต่อไป มันจัดการจีโนมและเรียก train() / evaluate() ผ่านอินเทอร์เฟซนามธรรม ซึ่งในทางทฤษฎีอนุญาตให้สลับอัลกอริทึม RL อื่นได้โดยไม่ต้องแตะเครื่องมือวิวัฒนาการ

interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

ประเด็นทางเทคนิคสุดท้าย: การประเมินเปลี่ยนเป็น การทำงานพร้อมกันแบบไม่ตรงกันแบบมีขอบเขต; กลุ่มของการประเมิน N รายการแบบขนานแทนลูปตามลำดับ โดยมีขีดจำกัดชัดเจนเพื่อหลีกเลี่ยงการทำให้ทรัพยากร GPU/CPU ที่มีอยู่เต็มอิ่ม

สิ่งที่ v4 แก้ไขเทียบกับ v3 ในทางปฏิบัติ

จุดบกพร่อง v3 การแก้ไข v4
น้ำหนักหายไปทุกรุ่น การฉีดกลับแบบลามาร์กของน้ำหนักที่ฝึกแล้ว
โอเวอร์ฟิตติ้งกับหน้าต่างตลาดเดียว ประเมินบนหลายหน้าต่าง, หลากหลายสภาวะ
สถาปัตยกรรมเติบโตอย่างไร้ขีดจำกัด FLOPs เป็นวัตถุประสงค์ Pareto ที่ชัดเจน
GA ผูกติดกับรายละเอียด DQN อินเทอร์เฟซ RLBackend นามธรรม
การประเมินตามลำดับช้า การทำงานพร้อมกันแบบไม่ตรงกันแบบมีขอบเขต

V4 ยังแก้ไขจุดบกพร่อง "การเชื่อมต่อ" API ที่เป็นรูปธรรมสิบจุด; กรณีที่โค้ด GA สมมติอินเทอร์เฟซสำหรับ TradingAgent ที่ไม่ตรงกับการใช้งานจริงทุกประการ จุดบกพร่องประเภทนี้มองไม่เห็นจนกว่าคุณจะ confront โค้ดกับซอร์สเอเยนต์จริง: v4 ได้รับการตรวจสอบหลังจากอ่านทีละบรรทัดเทียบกับไฟล์จริงเท่านั้น

ทำไมต้องผสมวิวัฒนาการและเกรเดียนต์แทนที่จะเลือกอย่างใดอย่างหนึ่ง

คุณอาจสงสัยว่าทำไมไม่ใช้ RL บริสุทธิ์ หรือวิวัฒนาการบริสุทธิ์อย่าง NEAT คำตอบในหนึ่งประโยค: เกรเดียนต์ยอดเยี่ยมสำหรับการปรับแต่งเฉพาะที่ (ปรับน้ำหนักต่อเนื่องไปสู่จุดที่เหมาะสมใกล้เคียง), วิวัฒนาการยอดเยี่ยมสำหรับการสำรวจทั่วโลก (ค้นพบสถาปัตยกรรมและการรวมกันของไฮเปอร์พารามิเตอร์ที่ไม่มีเกรเดียนต์ใดเข้าถึงได้ เนื่องจากพื้นที่ค้นหาแบบไม่ต่อเนื่องนั้นไม่สามารถหาอนุพันธ์ได้) การใช้อย่างใดอย่างหนึ่งโดยไม่มีอีกอย่างหมายถึงการกีดกันตัวเองจากการสำรวจหนึ่งในสองรูปแบบ

ราคาคือความซับซ้อนทางวิศวกรรม; สี่เวอร์ชันไม่ใช่ความฟุ่มเฟือย แต่เป็นจำนวน iteration ที่จำเป็นสำหรับลูป GA + RL ที่จะหยุดทำลายตัวเอง (โอเวอร์ฟิตติ้ง, สูญเสียบุคคลที่ดี, สูญเสียน้ำหนักที่ได้มา) แต่ผลลัพธ์คือระบบที่สำรวจพื้นที่การออกแบบที่กว้างกว่าการค้นหาแบบกริดอย่างง่ายของไฮเปอร์พารามิเตอร์มาก ในขณะที่ยังคงประสิทธิภาพเฉพาะที่ของเกรเดียนต์ดีเซนต์สำหรับผู้สมัครแต่ละรายที่ถูกประเมิน

ขั้นตอนถัดไป

สถาปัตยกรรมวิวัฒนาการระดับเดียวนี้ (ประชากรราบของจีโนม DQN) ถึงขีดจำกัดเมื่อจำนวนสินทรัพย์ที่ต้องครอบคลุมเพิ่มขึ้น นั่นคือแรงจูงใจในการย้ายไปสู่สถาปัตยกรรมแบบลำดับชั้นสามระดับ (นักวิเคราะห์สินทรัพย์ → ผู้จัดการภาค → ผู้จัดสรรพอร์ต) โดยมี GA ทำงานอย่างอิสระในแต่ละระดับ... แต่นั่นเป็นหัวข้อของบทความอื่น

Related Articles