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 that minimum generalizes to a different market regime, and nothing in the training loop pushes toward 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 one a genome encoding an architecture and hyperparameters; and you let natural selection do the sorting, while gradient descent keeps refining each individual within its own lifetime.

This runner went through four versions in a single intensive work session. Each one fixed a structural defect of the previous one.

v1: the naive version, and why it was not enough

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

The main defect: fitness was computed on the same data as training. An agent could literally memorize a market window and score excellently without having learned a generalizable strategy. Classic overfitting, but amplified by genetic selection; the GA actively selects the individuals that best exploit that 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 evaluation performance counts toward fitness.

That change alone dropped the population's average fitness; a sign that a good chunk of what looked like performance in v1 was pure memorization. Painful to watch, but exactly the signal you want: a lower but honest score beats an inflated, misleading one.

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

Optimizing a single fitness score (say returns) mechanically pushes toward agents that take extreme risks to maximize that one number. The solution was to move to NSGA-II (Non-dominated Sorting Genetic Algorithm II), which optimizes several objectives simultaneously without reducing them to an arbitrary weighted sum: returns, max drawdown, Sharpe ratio, cross-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 returns-vs-risk tradeoff through a pre-chosen weighting, you keep the whole tradeoff 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);
    }
  }
  // ... building later fronts by iterative removal
  return fronts;
}

Second addition in v3: a persistent Pareto archive. Without it, a good genome found at generation 12 can vanish by generation 15 if crossover luck fails to reproduce it; even if it stayed better than everything that replaced it. The archive keeps, across all generations, the set of ever-encountered non-dominated individuals, independent 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 with each new generation. A child born from crossing 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 reinjected into the genome after training, and passed on (with mutation) to offspring. An admitted biological heresy; Lamarck was wrong about living organisms, acquired traits are not inherited in biology; but nothing stops a numerical GA from cheating smartly: here, passing on what was learned radically speeds up convergence, since each generation restarts from an already-informed initialization instead of 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 (bull, bear, range). An agent that excels on one window and collapses on another can no longer dominate the Pareto front.
  • FLOPs complexity regularization: the network's compute cost (in FLOPs) becomes an objective of its own in NSGA-II. That stops evolution from converging on massive architectures simply because they have more raw capacity, without that being justified by the 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 in theory lets you plug in a different 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 moved to bounded async concurrency; a pool of N parallel evaluations instead of a sequential loop, with an explicit cap so available GPU/CPU resources never get saturated.

What v4 fixes over v3 in practice

v3 defect / v4 fix: Lost weights each generation / Lamarckian reinjection of trained weights. Overfitting to a single market window / Evaluation on multiple windows, varied regimes. Architectures growing without constraint / FLOPs as an explicit Pareto objective. GA coupled to DQN internals / Abstract RLBackend interface. Slow sequential evaluation / Bounded async concurrency.

v4 also fixed ten concrete "grounding" API bugs; cases where the GA code assumed an interface for TradingAgent that did not exactly match the real implementation. That kind of bug stays invisible until you confront the code with the agent's actual source: v4 was only validated after a line-by-line re-read against the real file.

Why mix evolution and gradient instead of picking one

You could ask why not just do pure RL, or pure evolution like NEAT. The answer fits in one sentence: gradient is excellent at refining locally (tuning continuous weights toward a nearby optimum), evolution is excellent at exploring globally (discovering architectures and hyperparameter combinations no gradient can reach, because the discrete search space is not differentiable). Using one without the other means giving up one of the two forms of exploration.

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

Next step

This single-level evolutionary architecture (a flat population of DQN genomes) hits its limits as the number of assets to cover grows. That is 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 is 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 智能体,有个说起来简单、解决起来难的问题:梯度只优化一个网络,朝一个局部最优,在一个市场窗口上。没有任何东西保证这个最优能泛化到别的市场状态,训练循环里也没有任何推动多样性的东西,两个从不同种子起跑的训练最后经常收敛到几乎一样的策略,盲区也一样。

我试的解法:把梯度下降换掉(或者说叠一层),换成遗传算法。不训一个智能体,进化一群智能体,每个是个编码架构和超参数的基因组,让自然选择筛,梯度在每个个体自己的一生里继续微调。

这个 runner 在一次高强度 session 里迭代了四个版本。每个版本修前一个的结构性缺陷。

v1:天真版本,以及为什么不够

第一版就是标准 GA 该有的样子:基因组种群,fitness 函数,选择,交叉,变异,下一代。每个基因组编码网络拓扑(层数、宽度)、DQN 超参数(学习率、epsilon 衰减、replay buffer 大小),再加几个架构选择(吃哪些数据源、embedding 多大)。

主要缺陷:fitness 和训练算在同一份数据上。智能体可以直接背下个市场窗口,拿高分,看似学会了策略,实则死记硬背。经典过拟合,但被遗传选择放大了,GA 会主动选出最会钻这个空子的个体。

v2:训练和评估分离

显然的修法:分阶段。每个基因组在一个市场窗口上训练,在另一个训练时没见过的窗口上评估。只有评估成绩算 fitness。

只这一改,种群平均 fitness 就掉了,一大块 v1 里的"成绩"是纯背诵。看着疼,但这正是我们要的信号:低但是诚实的分,胜过虚高作弊的分。

v3:上 NSGA-II,多目标 fitness

只优化一个 fitness 分数(比如收益率)会机械地把智能体推向为刷这一个数字冒极端风险。解法是上 NSGA-II(Non-dominated Sorting Genetic Algorithm II),同时优化多个目标,不做成武断的加权和:收益率、最大回撤、夏普比率、跨窗口稳定性。

NSGA-II 建 Pareto 前沿:找不到不损害别的目标准则还能改进某个目标的基因组集合。不预先用权重拍板收益和风险怎么换算,整条 trade-off 边界留着,最后再选。

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 作弊没人管:传获得性让收敛快得多,每代都从已被告知过的初始化点起步,而不是随机权重。

这个版本另外三个结构改动:

  • 环境多样性:每个基因组不再只在一个市场窗口评估,而是多个,抽不同状态(牛市、熊市、震荡)。只会在一种窗口上强的智能体再也统治不了 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 "grounding" bug,GA 以为 TradingAgent 接口长这样,实际文件长那样。不逐行对照真实源码根本看不见:v4 是对着真实文件逐行读完才算验证通过的。

为什么进化加梯度都要,而不是二选一

会问为什么不纯 RL,或者纯进化比如 NEAT。一句话:梯度擅长局部精调(连续权重往近处最优挪),进化擅长全局探索(发现梯度到不了的离散空间里的架构和超参数组合,因为不可微)。只用一个,等于主动扔掉一种探索。

代价是工程复杂度,四个版本不是奢侈,是 GA + RL 循环不再自己坑自己(过拟合、丢好个体、丢学到的权重)所需的迭代数。但结果是设计空间比简单 grid search 大得多,每个候选还保留梯度的局部效率。

下一步

这种单层进化架构(扁平的一群 DQN 基因组)资产一多就到头了。这才有了三层分级架构(Asset Analysts → Sector Managers → Portfolio Allocator),GA 在每一层独立跑……但那是另一篇文章的事了。

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

古典的な勾配降下訓練を遺伝的アルゴリズムNSGA-IIに置き換えて取引DQNエージェントを進化させた話。過学習から重みのラマルク進化まで4バージョン。

勾配降下だけの問題

取引アルゴリズム用DQNエージェントを古典的な勾配降下で訓練すると、言うのは簡単で解くのは難しい問題が出る。勾配は_1つ_のネットを_1つ_の極小へ、_1つ_の市場ウィンドウ上で最適化する。この極小が違う市場レジームに一般化する保証はなく、訓練ループ内のどこにも多様性への押しはない。違うシードから始めた2つのrunは、同じ死角を持つほぼ同一戦略に収束することが多い。

私が探った答え:勾配降下を遺伝的アルゴリズムに置き換える(というか重ねる)ことだ。エージェント1つを訓練する代わり、エージェント集団を進化させる。各個体はアーキテクチャとハイパーパラメータをエンコードしたゲノムを持ち、勾配は各個体の生涯内で磨きをかけ続ける間、自然選択に選別させる。

このrunnerは集中的な作業セッション1回で4バージョンを経た。各バージョンが前の構造的欠陥を直した。

v1:素朴版と、なぜ足りなかったか

初版は基本的GAの期待通りだった。ゲノム集団、適応度関数、選択、交叉、突然変異、次世代。各ゲノムはネットのトポロジー(層数、幅)、DQNハイパーパラメータ(学習率、epsilon減衰、リプレイバッファサイズ)、いくつかのアーキテクチャ選択(どのデータソースを食うか、埋め込みサイズ)をエンコードした。

主な欠陥:適応度が訓練と同じデータで計算されてた。エージェントは文字通り市場ウィンドウを暗記して、一般化戦略を何も学ばずに優秀なスコアを取れた。古典的過学習だが、遺伝選択で増幅される。GAはこの抜け穴をいちばん突く個体を積極的に選ぶからだ。

v2:訓練と評価を分ける

明らかな修正はフェーズ分離だった。各ゲノムは市場ウィンドウで訓練して、訓練で見たことのない違うウィンドウで評価する。適応度に数えるのは評価性能だけだ。

この変更だけで集団の平均適応度は下がった。v1で性能に見えたものの相当部分が純粋な暗記だった証拠だ。見るのは痛い。でもまさに欲しい信号だ。膨らんだ嘘スコアより低くて正直なスコアの方がいい。

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

適応度スコア1つ(たとえば収益)の最適化は、その1つの数字を最大化する極端リスクのエージェントへ機械的に向かう。解はNSGA-II(Non-dominated Sorting Genetic Algorithm 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には構造的死角があった。ゲノムはアーキテクチャを記述するのに、訓練で学んだ重みは新世代ごとに消えた。優秀な両親の交叉で生まれた子はアーキテクチャを受け継ぐのに、ゼロから学び直しだった。両親を優秀にした重みの痕跡はなかった。

v4はラマルク進化を入れる。訓練済み重みは訓練後にゲノムへ再注入されて、子孫へ(突然変異付きで)伝えられる。自覚的な生物学的異端だ。ラマルクは生物には間違ってた。獲得形質の遺伝は生物学に存在しない。でも数値GAが賢くごまかすのを止めるものは何もない。ここでは獲得物の伝達が収束を根本から速める。各世代がランダム重みじゃなく情報ある初期点から始めるからね。

この版の他3つの構造変更:

  • 環境多様性:各ゲノムは単一市場ウィンドウじゃなく複数で評価される。違うレジーム(上昇、下降、レンジ)から引く。1つのウィンドウで秀でて他で崩れるエージェントはパレートフロントを支配できなくなる。

  • FLOPs複雑性による正則化:ネット計算コスト(FLOPs)がNSGA-IIの独立目的になる。性能利得に見合わないのに brute capacity だけで巨大アーキテクチャに進化が収束するのを防ぐ。

  • 分離された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を明示パレート目的に DQN詳細と結合したGA 抽象RLBackendインターフェース 遅い逐次評価 有界async並行

v4は「グラウンディング」APIバグも10個直した。GAコードがTradingAgent向けインターフェースを仮定してて、実際の実装と完全一致しなかったケースだ。こういうバグは本物のエージェントソースコードと突き合わせないと見えない。v4は実ファイルと行ごとに向き合って初めて検証された。

進化と勾配を混ぜる理由、択一じゃない理由

純RLかNEAT式純進化でいいじゃないかと思うかもしれない。答えは一文だ。勾配は局所磨きが得意だ(近い最適へ連続重みを調整する)。進化は大域探索が得意だ(離散探索空間が微分不可能で勾配が届かないアーキテクチャやハイパーパラメータ組み合わせを発見する)。片方なしで使うのは2つの探索形の片方を捨てることだ。

代償は工学的複雑さだ。4バージョンは贅沢じゃなかった。GAとRLのループが自滅(過学習、良個体消失、獲得重み消失)をやめるのに必要な反復回数だった。でも結果は、単なるハイパーパラメータのグリッドサーチよりずっと広い設計空間を探るシステムになった。各評価候補には勾配の局所効率を残したままでね。

次の段階

この単層進化アーキテクチャ(DQNゲノムの平坦集団)は、カバー資産数が増えると限界に来る。3層階層アーキテクチャ(Asset AnalystsからSector ManagersからPortfolio Allocatorへ)への移行を動機付けた。各層でGAが独立動作する。でもそれは別の記事の話だ。

경사 하강법 대신 자연 선택으로 신경망을 진화시켜 봤어요

고전적인 경사 하강법 훈련을 유전 알고리즘 NSGA-II로 대체해서 트레이딩용 DQN 에이전트를 진화시킨 방법. 과적합에서 라마르크식 가중치 진화까지, 네 가지 버전의 이야기예요.

경사 하강법만 쓸 때의 문제

고전적인 경사 하강법으로 알고리즘 트레이딩용 DQN 에이전트를 훈련시키는 데는 말하기는 쉽고 풀기는 어려운 문제가 있어요. 경사는 하나의 시장 창에서 하나의 국소 최솟값 쪽으로 하나의 네트워크를 최적화해요. 그 최솟값이 다른 시장 국면에 일반화된다는 보장도 없고, 훈련 루프 안에 다양성을 밀어붙이는 것도 없어요. 다른 시드에서 출발한 두 번의 실행이 거의 같은 전략으로 수렴하고, 같은 사각지대를 공유하는 경우가 많아요.

제가 탐험한 답은 경사 하강법을 대체(정확히는 겹쳐 쓰기)하는 것이었어요. 유전 알고리즘으로요. 에이전트 하나를 훈련시키는 대신 에이전트 집단을 진화시키고, 각자는 아키텍처와 하이퍼파라미터를 인코딩한 게놈이고, 경사가 각 개체의 생애 안에서 계속 다듬는 동안 자연 선택이 골라내게 해요.

이 러너는 한 번의 집중 작업 세션에서 네 가지 버전을 거쳤어요. 각 버전은 이전 버전의 구조적 결함을 하나씩 고쳤어요.

v1: 순진한 버전, 그리고 부족했던 이유

첫 번째 버전은 기본 GA에게 기대하는 것을 했어요. 게놈 집단, 적합도 함수, 선택, 교차, 변이, 다음 세대. 각 게놈은 네트워크 구조(층 수, 너비)와 DQN 하이퍼파라미터(학습률, 엡실론 감쇠, 리플레이 버퍼 크기), 그리고 몇 가지 아키텍처 선택(어떤 데이터 소스를 쓸지, 임베딩 크기)을 인코딩했어요.

주된 결함은 적합도가 훈련과 같은 데이터에서 계산됐다는 거예요. 에이전트는 말 그대로 시장 창 하나를 외워서 일반화 가능한 전략을 배우지 않고도 훌륭한 점수를 얻을 수 있었어요. 전형적인 과적합이지만 유전적 선택으로 증폭돼요. GA는 이 허점을 가장 잘 악용하는 개체를 적극적으로 선택하니까요.

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는 라마르크식 진화를 도입해요. 훈련된 가중치를 훈련 뒤에 게놈에 다시 주입하고, 변이와 함께 자식에게 전달해요. 자인한 생물학적 이단이에요. 살아 있는 유기체에 대해 라마르크는 틀렸고, 획득 형질의 유전은 생물학에 존재하지 않아요. 하지만 숫자 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 DQN 세부 사항에 결합된 GA 추상 RLBackend 인터페이스 느린 순차 평가 제한된 비동기 동시성

v4는 열 가지 구체적인 "그라운딩" API 버그도 고쳤어요. GA 코드가 실제 구현과 정확히 맞지 않는 TradingAgent용 인터페이스를 가정하던 경우들이에요. 이런 버그는 코드를 에이전트의 진짜 소스 코드와 대면시키기 전에는 보이지 않아요. v4는 실제 파일을 한 줄씩 다시 읽은 뒤에야 검증됐어요.

진화와 경사를 고르지 않고 섞는 이유

순수 RL이나 NEAT 같은 순수 진화만 하지 왜 둘을 섞느냐고 물을 수 있어요. 답은 한 문장이에요. 경사는 국소 다듬기에 뛰어나고(가까운 최적값을 향해 연속 가중치를 조정), 진화는 전역 탐색에 뛰어나요(경사가 도달할 수 없는 아키텍처와 하이퍼파라미터 조합을 발견. 탐색 공간이 이산적이라 미분 가능하지 않으니까요). 둘 중 하나 없이 쓰면 두 가지 탐색 형태 중 하나를 포기하는 셈이에요.

대가는 엔지니어링 복잡성이에요. 네 가지 버전은 사치가 아니라 GA와 RL 루프가 스스로를 방해하지 않게(과적합, 좋은 개체 손실, 획득 가중치 손실) 하는 데 필요했던 반복 횟수였어요. 하지만 결과는 단순한 하이퍼파라미터 그리드 서치보다 훨씬 넓은 설계 공간을 탐험하면서도, 평가받는 각 후보에 대해 경사의 국소 효율을 유지하는 시스템이에요.

다음 단계

이 단일 수준 진화 아키텍처(평평한 DQN 게놈 집단)는 다뤄야 할 자산 수가 늘어나면 한계에 이르러요. 이것이 세 수준 계층 아키텍처(자산 분석가에서 섹터 관리자, 포트폴리오 배분자)로 넘어가게 된 동기고, GA는 각 수준에서 독립적으로 동작해요. 하지만 그건 다른 글의 주제예요.

Bir sinir ağını gradyan inişi yerine doğal seçilimle evrimleştirdim

Klasik gradyan inişli eğitimi, alım satım DQN ajanlarını evrimleştirmek için NSGA-II genetik algoritmasıyla nasıl değiştirdim: aşırı uyumdan ağırlıkların lamarkçı evrimine dört sürüm.

Yalnızca gradyan inişiyle ilgili sorun

Algoritmik alım satım için bir DQN ajanını klasik gradyan inişiyle eğitmek, söylemesi kolay çözmesi zor bir sorun koyar: gradyan bir ağı bir yerel minimuma, bir piyasa penceresinde optimize eder. Bu minimumun farklı bir piyasa rejimine genelleneceğinin garantisi yoktur ve eğitim döngüsündeki hiçbir şey çeşitliliği teşvik etmez; farklı tohumlardan başlayan iki run genelde neredeyse aynı stratejilere, aynı kör noktalarla yakınsar.

Keşfettiğim yanıt: gradyan inişini genetik bir algoritmayla değiştirmek (daha doğrusu üst üste koymak). Tek bir ajan eğitmek yerine bir ajan popülasyonu evrimleştirilir; her biri mimari ve hiper parametreleri kodlayan bir genomdur; her birey kendi ömrü içinde gradyanla ince ayarlanmaya devam ederken ayıklamayı doğal seçilim yapar.

Bu koşucu tek bir yoğun çalışma oturumunda dört sürümden geçti. Her biri öncekinin yapısal bir kusurunu düzeltti.

v1: naif sürüm ve neden yetmediği

İlk sürüm temel bir GAdan bekleneni yapıyordu: bir genom popülasyonu, bir fitness fonksiyonu, seçilim, çaprazlama, mutasyon, sonraki nesil. Her genom ağ topolojisini (katman sayısı, genişlik), DQN hiper parametrelerini (öğrenme hızı, epsilon azalması, tekrar tamponu boyutu) ve birkaç mimari seçimi (hangi veri kaynaklarını tüketmek, gömme boyutu) kodluyordu.

Ana kusur: fitness eğitimle aynı veriler üzerinde hesaplanıyordu. Bir ajan bir piyasa penceresini kelimenin tam anlamıyla ezberleyip genellenebilir bir strateji öğrenmeden mükemmel bir skor alabiliyordu. Klasik aşırı uyum ama genetik seçilimle güçlendirilmiş; GA aktif olarak bu açığı en iyi suistimal eden bireyleri seçer.

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

Bariz düzeltme fazları ayırmaktı: her genom bir piyasa penceresinde eğitilir, sonra eğitimde hiç görülmemiş farklı bir pencerede değerlendirilir. Fitness için yalnızca değerlendirme performansı sayılır.

Yalnızca bu değişiklik popülasyonun ortalama fitnessını düşürdü; v1de performans gibi görünen şeyin iyi bir kısmının saf ezber olduğunun işareti. Görmesi acı verici ama tam istediğimiz sinyal bu: daha düşük ama dürüst bir skor, şişirilmiş ve yanıltıcı bir skordan iyidir.

v3: NSGA-IIye ve çok amaçlı fitnessa geçiş

Tek bir fitness skorunu (örneğin getiriyi) optimize etmek mekanik olarak bu tek sayıyı maksimize etmek için aşırı risk alan ajanlara iter. Çözüm, keyfi ağırlıklı bir toplama indirgemeden birden çok hedefi aynı anda optimize eden NSGA-IIye (Non-dominated Sorting Genetic Algorithm II) geçmekti: getiri, maksimum drawdown, Sharpe oranı, pencereler arası kararlılık.

NSGA-II bir Pareto cephesi inşa eder: bir hedefte başka bir hedefi bozmadan iyileştirmenin mümkün olmadığı genomlar kümesi. Getiri ve risk arasında önceden seçilmiş bir ağırlıkla tek bir uzlaşma dayatmak yerine tüm uzlaşma sınırını tutar ve nihai seçimi açık bırakır.

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 çıkarımla inşası
  return fronts;
}

v3ün ikinci eklemesi: kalıcı bir Pareto arşivi. O olmadan 12. nesilde bulunan iyi bir genom, çaprazlama şansı onu yeniden üretmezse 15. nesilde kaybolabilir; yerini alan her şeyden hâlâ daha iyi olsa bile. Arşiv, mevcut popülasyondan bağımsız olarak tüm nesiller boyunca karşılaşılan baskın olmayan bireylerin kümesini korur.

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

v3ün yapısal bir kör noktası vardı: genom mimariyi tanımlıyordu ama eğitimde öğrenilen ağırlıklar her yeni nesilde kayboluyordu. İyi iki ebeveynin çaprazlamasından doğan bir yavru mimarilerini kalıtıyordu ama sıfırdan yeniden öğrenmek zorundaydı; ebeveynlerini performanslı kılan ağırlıklardan iz yoktu.

v4 lamarkçı evrimi getirir: eğitilmiş ağırlıklar eğitimden sonra genoma geri enjekte edilir ve yavruya (mutasyonla) aktarılır. Bu üstlenilmiş bir biyolojik sapkınlıktır; Lamarck canlı organizmalar için haksızdı, kazanılmış karakterlerin kalıtımı biyolojide yoktur; ama sayısal bir GAnın zekice hile yapmasına hiçbir şey engel değil: burada kazanılmışı aktarmak yakınsamayı kökten hızlandırır çünkü her nesil rastgele ağırlıklar yerine zaten bilgili bir başlatma noktasından başlar.

Bu sürümde üç yapısal değişiklik daha:

  • Çevresel çeşitlilik: her genom artık tek bir piyasa penceresinde değil, farklı rejimlerden çekilmiş birkaç pencerede değerlendirilir (yükselen, düşen, yatay). Bir pencerede parlayıp diğerinde çöken bir ajan Pareto cephesine artık hükmedemez.

  • FLOPs karmaşıklığıyla düzenlileştirme: ağın hesaplama maliyeti (FLOPs cinsinden) NSGA-II içinde başlı başına bir hedef olur. Bu, evrimin sırf ham kapasitesi daha fazla diye devasa mimarilere, performans kazancıyla haklı çıkarmadan yakınsamasını engeller.

  • Ayrıştırılmış RLBackend arayüzü: GA artık DQN detaylarını bilmez. Bir genomu yönetir ve soyut bir arayüz üzerinden train() / evaluate() çağırır, bu da teoride evrimsel motora dokunmadan başka bir RL algoritması takmaya izin verir.

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ı eşzamanlı eşzamanlılığa geçti; sıralı bir döngü yerine paralel N değerlendirmeden bir havuz ve mevcut GPU/CPU kaynaklarını doyurmamak için açık bir limit.

v4ün pratikte v3e göre düzelttiği şey

v3 kusuru v4 düzeltmesi Her nesilde kaybolan ağırlıklar Eğitilmiş ağırlıkların lamarkçı geri enjeksiyonu Tek piyasa penceresine aşırı uyum Çeşitli rejimlerde çok pencereli değerlendirme Kısıtsız büyüyen mimariler Açık Pareto hedefi olarak FLOPs DQN detaylarına bağlı GA Soyut RLBackend arayüzü Yavaş sıralı değerlendirme Sınırlı eşzamanlı eşzamanlılık

v4 ayrıca on somut "grounding" API hatasını düzeltti; GA kodunun TradingAgent için gerçek uygulamayla tam örtüşmeyen bir arayüz varsaydığı durumlar. Bu tür hata kod gerçek ajan kaynak koduyla yüzleşene kadar görünmezdir: v4 yalnızca gerçek dosya karşısında satır satır yeniden okuduktan sonra doğrulandı.

Evrim ve gradyanı birini seçmek yerine neden karıştırmalı

Neden saf RL ya da NEAT tarzı saf evrim yapmıyoruz diye sorulabilir. Yanıt tek cümlede: gradyan yerel ince ayar için mükemmeldir (sürekli ağırlıkları yakın bir optimuma ayarlamak), evrim küresel keşif için mükemmeldir (hiçbir gradyanın ulaşamayacağı mimarileri ve hiper parametre kombinasyonlarını keşfetmek, çünkü ayrık arama uzayı türevlenebilir değildir). Birini diğer olmadan kullanmak iki keşif biçiminden birinden mahrum kalmaktır.

Ödenecek bedel mühendislik karmaşıklığıdır; dört sürüm lüks değildi, GA + RL döngüsünün kendini sabote etmeyi bırakması için gereken yineleme sayısıydı (aşırı uyum, iyi bireylerin kaybı, kazanılmış ağırlıkların kaybı). Ama sonuç, basit bir hiper parametre grid searchten çok daha geniş bir tasarım uzayını keşfeden, değerlendirilen her aday için gradyanın yerel verimliliğini koruyan 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. Üç seviyeli hiyerarşik bir mimariye geçişi motive eden şey bu (Varlık Analistleri → Sektör Yöneticileri → Portföy Tahsisçisi), her seviyede bağımsız çalışan bir GA ile... ama o başka bir yazının konusu.

Ho fatto evolvere una rete neurale per selezione naturale invece che per discesa del gradiente

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

Il problema con la sola discesa del gradiente

Allenare un agente DQN per trading algoritmico con la classica discesa del gradiente pone un problema semplice da enunciare e difficile da risolvere: il gradiente ottimizza una rete verso un minimo locale, su una finestra di mercato. Niente garantisce che questo minimo generalizzi a un regime di mercato diverso, e niente nel ciclo di allenamento spinge verso la diversità; due run che partono da seed diversi convergono spesso verso strategie quasi identiche, con gli stessi angoli ciechi.

La risposta che ho esplorato: sostituire (o meglio sovrapporre) la discesa del gradiente con un algoritmo genetico. Invece di allenare un agente, facciamo evolvere una popolazione di agenti; ognuno un genoma che codifica un'architettura e iperparametri; e lasciamo che la selezione naturale faccia la cernita, mentre il gradiente continua a rifinire ogni individuo dentro la sua vita.

Questo runner ha attraversato quattro versioni in una sola sessione di lavoro intensiva. Ognuna ha corretto un difetto strutturale della precedente.

v1: la versione ingenua, e perché non bastava

La prima versione faceva ciò che ci si aspetta da un GA basico: una popolazione di genomi, una funzione di fitness, selezione, crossover, mutazione, generazione successiva. Ogni genoma codificava la topologia della rete (numero di strati, larghezza), gli iperparametri DQN (learning rate, epsilon decay, taglia del replay buffer), e qualche scelta architetturale (quali fonti di dati consumare, quale taglia di embedding).

Il difetto principale: la fitness era calcolata sugli stessi dati dell'allenamento. Un agente poteva letteralmente memorizzare una finestra di mercato e ottenere un punteggio ottimo senza aver imparato una strategia generalizzabile. Classico overfitting, ma amplificato dalla selezione genetica; il GA seleziona attivamente gli individui che sfruttano meglio questa falla.

v2: separare allenamento e valutazione

La correzione ovvia era separare le fasi: ogni genoma si allena su una finestra di mercato, poi è valutato su una finestra diversa, mai vista durante l'allenamento. Solo la performance in valutazione conta per la fitness.

Questo solo cambiamento ha fatto cadere la fitness media della popolazione; segno che una buona parte di ciò che sembrava performance in v1 era memorizzazione pura. Doloroso da vedere, ma è esattamente il segnale che vogliamo: un punteggio più basso ma onesto vale meglio di un punteggio gonfiato e ingannevole.

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

Ottimizzare un solo punteggio di fitness (diciamo il rendimento) spinge meccanicamente verso agenti che prendono rischi estremi per massimizzare quel solo numero. La soluzione è stata passare a NSGA-II (Non-dominated Sorting Genetic Algorithm II), che ottimizza insieme più obiettivi senza ridurli a una somma pesata arbitraria: rendimento, drawdown massimo, rapporto di Sharpe, stabilità inter-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 tra rendimento e rischio via una pesatura scelta in anticipo, teniamo tutta la frontiera di compromesso e lasciamo la scelta finale aperta.

// ordina la popolazione in fronti di non-dominanza
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 della v3: un archivio di Pareto persistente. Senza, un buon genoma trovato alla generazione 12 può sparire alla generazione 15 se il caso del crossover non lo riproduce; anche se restava migliore di tutto ciò che l'ha sostituito. L'archivio conserva, attraverso tutte le generazioni, l'insieme degli individui non dominati mai incontrati, indipendentemente dalla popolazione corrente.

v4: evoluzione lamarckiana e diversità ambientale

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

La v4 introduce l'evoluzione lamarckiana: i pesi allenati sono reiniettati nel genoma dopo l'allenamento, e trasmessi (con mutazione) alla discendenza. È un'eresia biologica assunta; Lamarck aveva torto per gli organismi viventi, l'ereditarietà dei caratteri acquisiti non esiste in biologia; ma niente impedisce a un GA numerico di barare in modo intelligente: qui, trasmettere l'acquisito accelera radicalmente la convergenza, dato che ogni generazione riparte da un punto di inizializzazione già informato invece che da pesi casuali.

Altri tre cambiamenti strutturali in questa versione:

  • Diversità ambientale: ogni genoma non è più valutato su una sola finestra di mercato ma su varie, tirate da regimi diversi (rialzista, ribassista, range). Un agente che eccelle su una finestra e crolla su un'altra non può più dominare il fronte di Pareto.

  • Regolarizzazione per complessità FLOPs: il costo di calcolo della rete (in FLOPs) diventa un obiettivo a sé in NSGA-II. Evita che l'evoluzione converga verso architetture enormi solo perché hanno più capacità grezza, senza che sia giustificato dal guadagno di performance.

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

// backend RL astratto per 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 in concorrenza asincrona limitata; un pool di N valutazioni in parallelo invece di un ciclo sequenziale, con un limite esplicito per non saturare le risorse GPU/CPU disponibili.

Cosa corregge la v4 rispetto alla v3 in pratica

Difetto v3 Correzione v4 Pesi persi a ogni generazione Reiniezione lamarckiana dei pesi allenati Overfitting a una sola finestra di mercato Valutazione su finestre multiple, regimi variati Architetture che crescono senza vincolo FLOPs come obiettivo Pareto esplicito GA accoppiato ai dettagli di DQN Interfaccia RLBackend astratta Valutazione sequenziale lenta Concorrenza async limitata

La v4 ha anche corretto dieci bug concreti di "grounding" API; casi dove il codice del GA supponeva un'interfaccia per TradingAgent che non corrispondeva esattamente all'implementazione reale. Questo tipo di bug è invisibile finché non confronti il codice al vero codice sorgente dell'agente: la v4 è stata validata solo dopo rilettura riga per riga contro il file reale.

Perché mescolare evoluzione e gradiente invece di sceglierne uno

Ci si potrebbe chiedere perché non fare solo RL puro, o evoluzione pura stile NEAT. La risposta sta in una frase: il gradiente è ottimo per rifinire localmente (aggiustare pesi continui verso un ottimo vicino), l'evoluzione è ottima per esplorare globalmente (scoprire architetture e combinazioni di iperparametri che nessun gradiente può raggiungere, perché lo spazio di ricerca discreto non è differenziabile). Usare l'uno senza l'altro, è privarsi di una delle due forme di esplorazione.

Il prezzo da pagare è la complessità di ingegneria; quattro versioni non erano un lusso, era 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 un semplice grid search di iperparametri, tenendo l'efficienza locale del gradiente per ogni candidato valutato.

Prossimo passo

Questa architettura evolutiva a un solo livello (una popolazione piatta di genomi DQN) raggiunge i suoi limiti quando il numero di asset da coprire sale. È ciò che ha motivato il passaggio a un'architettura gerarchica a tre livelli (Asset Analysts, poi Sector Managers, poi Portfolio Allocator), con un GA che opera indipendentemente a ogni livello... ma questa è materia di un altro articolo.

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

Wie ich klassisches Training per Gradientenabstieg durch einen genetischen NSGA-II-Algorithmus ersetzt habe, um DQN-Trading-Agenten zu evolvieren: vier Versionen, von Overfitting bis zur lamarckschen Evolution der Gewichte.

Das Problem mit Gradientenabstieg allein

Einen DQN-Agenten für algorithmisches Trading mit klassischem Gradientenabstieg zu trainieren stellt ein einfach zu formulierendes und schwer zu lösendes Problem: Der Gradient optimiert ein Netz hin zu einem lokalen Minimum, auf einem Marktfenster. Nichts garantiert, dass dieses Minimum auf ein anderes Marktregime generalisiert, und nichts in der Trainingsschleife drängt zu Diversität; zwei Runs, die von unterschiedlichen Seeds starten, konvergieren oft zu fast identischen Strategien mit denselben blinden Flecken.

Die Antwort, die ich erkundet habe: Den Gradientenabstieg durch einen genetischen Algorithmus ersetzen (oder eher überlagern). Statt einen Agenten zu trainieren, evolviert man eine Population von Agenten; jeder ein Genom, das eine Architektur und Hyperparameter kodiert; und man lässt die natürliche Selektion sortieren, während der Gradient jeden Einzelnen innerhalb seines eigenen Lebens weiter feinschleift.

Dieser Runner hat vier Versionen in einer einzigen intensiven Arbeitssession durchlaufen. Jede hat einen strukturellen Defekt der vorherigen korrigiert.

v1: die naive Version, und warum sie nicht reichte

Die erste Version tat, was man von einem Basis-GA erwartet: Eine Population von Genomen, eine Fitnessfunktion, Selektion, Crossover, Mutation, nächste Generation. Jedes Genom kodierte die Netztopologie (Schichtzahl, Breite), die DQN-Hyperparameter (Learning Rate, Epsilon Decay, Replay-Buffer-Größe) und einige Architekturentscheidungen (welche Datenquellen konsumieren, welche Embedding-Größe).

Der Hauptdefekt: Die Fitness wurde auf denselben Daten berechnet wie das Training. Ein Agent konnte buchstäblich ein Marktfenster auswendig lernen und einen exzellenten Score bekommen, ohne eine generalisierbare Strategie gelernt zu haben. Klassisches Overfitting, aber durch die genetische Selektion verstärkt; der GA selektiert aktiv die Individuen, die diese Lücke am besten ausnutzen.

v2: Training und Evaluierung trennen

Die offensichtliche Korrektur war, die Phasen zu trennen: 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 Änderung allein ließ die Durchschnittsfitness der Population fallen; ein Zeichen, dass ein guter Teil dessen, was in v1 wie Leistung aussah, reines Auswendiglernen war. Schmerzlich anzusehen, aber genau das Signal, das man will: Ein niedrigerer, aber ehrlicher Score ist besser als ein aufgeblähter und irreführender.

v3: Wechsel zu NSGA-II und Multi-Objective-Fitness

Einen einzigen Fitnessscore zu optimieren (sagen wir die Rendite) drängt mechanisch zu Agenten, die extreme Risiken eingehen, um diese eine Zahl zu maximieren. Die Lösung war der Wechsel zu NSGA-II (Non-dominated Sorting Genetic Algorithm II), der mehrere Ziele gleichzeitig optimiert, ohne sie auf eine arbiträre gewichtete Summe zu reduzieren: Rendite, maximaler Drawdown, Sharpe-Ratio, Stabilität über Fenster.

NSGA-II baut eine Pareto-Front: Die Menge der Genome, bei denen keine Verbesserung bei einem Ziel möglich ist, ohne ein anderes zu verschlechtern. Statt einen einzigen Kompromiss zwischen Rendite und Risiko über eine vorab gewählte Gewichtung zu erzwingen, behält man die gesamte Kompromissgrenze und lässt die finale 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);
    }
  }
  // ... Aufbau der folgenden Fronts durch iteratives Entfernen
  return fronts;
}

Zweite Neuerung der v3: Ein persistentes Pareto-Archiv. Ohne es kann ein gutes Genom, das in Generation 12 gefunden wurde, in Generation 15 verschwinden, wenn der Zufall des Crossovers es nicht reproduziert; selbst wenn es besser blieb als alles, was es ersetzt hat. Das Archiv bewahrt über alle Generationen die Menge der je angetroffenen nicht-dominierten Individuen, unabhängig von der aktuellen Population.

v4: lamarcksche Evolution und Umgebungsdiversität

Die v3 hatte einen strukturellen blinden Fleck: Das Genom beschrieb die Architektur, aber die während des Trainings gelernten Gewichte verschwanden mit jeder neuen Generation. Ein Nachkomme aus der Kreuzung zweier guter Eltern erbte ihre Architektur, musste aber von null neu lernen; keine Spur der Gewichte, die seine Eltern leistungsfähig gemacht hatten.

Die v4 führt die lamarcksche Evolution ein: Die trainierten Gewichte werden nach dem Training wieder in das Genom injiziert und (mit Mutation) an die Nachkommen weitergegeben. Es ist eine eingestandene biologische Häresie; Lamarck lag falsch für lebende Organismen, die Vererbung erworbener Merkmale existiert in der Biologie nicht; aber nichts hindert einen numerischen GA daran, intelligent zu schummeln: Hier beschleunigt das Weitergeben des Erworbenen die Konvergenz radikal, weil jede Generation von einem schon informierten Initialisierungspunkt startet statt von zufälligen Gewichten.

Drei weitere strukturelle Änderungen in dieser Version:

  • Umgebungsdiversität: Jedes Genom wird nicht mehr auf einem einzigen Marktfenster evaluiert, sondern auf mehreren, aus unterschiedlichen Regimen gezogen (bullisch, bärisch, Range). Ein Agent, der auf einem Fenster glänzt und auf einem anderen zusammenbricht, kann die Pareto-Front nicht mehr dominieren.
  • FLOPs-Komplexitätsregularisierung: Die Rechenkosten des Netzes (in FLOPs) werden ein eigenes Ziel in NSGA-II. Das verhindert, dass die Evolution zu massiven Architekturen konvergiert, einfach weil sie mehr rohe Kapazität haben, ohne dass es durch den Leistungsgewinn gerechtfertigt wäre.
  • Entkoppelte RLBackend-Schnittstelle: Der GA kennt die DQN-Details nicht mehr. Er handhabt ein Genom und ruft train() / evaluate() über eine abstrakte Schnittstelle auf, was es theoretisch erlaubt, einen anderen RL-Algorithmus anzustöpseln, ohne die Evolutionsengine anzufassen.
interface RLBackend {
  train(genome: Genome, window: MarketWindow): Promise<TrainedWeights>;
  evaluate(genome: Genome, weights: TrainedWeights, window: MarketWindow): Promise<FitnessVector>;
}

Letzter technischer Punkt: Die Evaluierung läuft jetzt in begrenzter asynchroner Konkurrenz; ein Pool von N parallelen Evaluierungen statt einer sequenziellen Schleife, mit explizitem Limit, um die verfügbaren GPU/CPU-Ressourcen nicht zu sättigen.

Was die v4 gegenüber der v3 in der Praxis korrigiert

Defekt v3 Korrektur v4 Verlorene Gewichte bei jeder Generation Lamarcksche Reinjektion der trainierten Gewichte Overfitting auf ein einziges Marktfenster Evaluierung auf mehreren Fenstern, variierte Regime Architekturen, die ohne Zwang wachsen FLOPs als explizites Pareto-Ziel GA an DQN-Details gekoppelt Abstrakte RLBackend-Schnittstelle Langsame sequenzielle Evaluierung Begrenzte Async-Konkurrenz

Die v4 hat auch zehn konkrete "Grounding"-Bugs der API korrigiert; Fälle, in denen der GA-Code eine Schnittstelle für TradingAgent annahm, die nicht genau der echten Implementierung entsprach. Diese Art Bug ist unsichtbar, solange man den Code nicht mit dem echten Quellcode des Agenten konfrontiert: Die v4 wurde erst nach Zeile-für-Zeile-Nachlese gegen die echte Datei validiert.

Warum Evolution und Gradient mischen statt eines von beiden

Man könnte fragen, warum nicht einfach pures RL oder pure Evolution nach Art von NEAT. Die Antwort passt in einen Satz: Der Gradient ist exzellent zum lokalen Feinschliff (kontinuierliche Gewichte hin zu einem nahen Optimum justieren), die Evolution ist exzellent zur globalen Erkundung (Architekturen und Hyperparameterkombinationen entdecken, die kein Gradient erreichen kann, weil der diskrete Suchraum nicht differenzierbar ist). Eines ohne das andere zu nutzen heißt, sich einer der beiden Erkundungsformen zu berauben.

Der Preis ist Engineering-Komplexität; vier Versionen waren kein Luxus, es war die Zahl nötiger Iterationen, damit die GA + RL-Schleife aufhört, sich selbst zu sabotieren (Overfitting, Verlust guter Individuen, Verlust erworbener Gewichte). Aber das Ergebnis ist ein System, das einen deutlich breiteren Designraum erkundet als ein simples Hyperparameter-Grid-Search und dabei die lokale Effizienz des Gradienten für jeden evaluierten Kandidaten behält.

Nächster Schritt

Diese einstufige evolutive Architektur (eine flache Population von DQN-Genomen) erreicht ihre Grenzen, wenn die Zahl abzudeckender Assets steigt. Das hat den Wechsel zu einer hierarchischen Drei-Ebenen-Architektur motiviert (Asset Analysts → Sector Managers → Portfolio Allocator), mit einem GA, der auf jeder Ebene unabhängig operiert... aber das ist das Thema eines anderen Artikels.

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

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

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

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

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

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

v1 : наивная версия, и почему её не хватало

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

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

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. Ответ укладывается в одну фразу : градиент отлично доводит локально (подстраивает непрерывные веса к близкому оптимуму), эволюция отлично исследует глобально (открывает архитектуры и комбинации гиперпараметров, которых никакой градиент не достигнет, потому что дискретное пространство поиска недифференцируемо). Использовать одно без другого значит лишить себя одной из двух форм исследования.

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

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

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

Hice evolucionar una red de neuronas por selección natural en vez de por descenso de gradiente

Cómo reemplacé el entrenamiento clásico por descenso de gradiente por un algoritmo genético NSGA-II para hacer evolucionar agentes DQN de trading: cuatro versiones, del sobreajuste a la evolución lamarckiana de los pesos.

El problema del descenso de gradiente por sí solo

Entrenar un agente DQN para trading algorítmico con el descenso de gradiente clásico plantea un problema simple de enunciar y difícil de resolver: el gradiente optimiza una red hacia un mínimo local, sobre una ventana de mercado. Nada garantiza que ese mínimo generalice a un régimen de mercado distinto, y nada en el bucle de entrenamiento empuja hacia la diversidad; dos ejecuciones que parten de semillas distintas suelen converger hacia estrategias casi idénticas, con los mismos puntos ciegos.

La respuesta que exploré: reemplazar (o más bien superponer) el descenso de gradiente con un algoritmo genético. En vez de entrenar un agente, haces evolucionar una población de agentes; cada uno un genoma que codifica una arquitectura y unos hiperparámetros; y dejas que la selección natural haga la criba, mientras el gradiente sigue puliendo cada individuo dentro de su propia vida.

Este runner pasó por cuatro versiones en una sola sesión de trabajo intensiva. Cada una corrigió un defecto estructural de la anterior.

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

La primera versión hacía lo que se espera de un GA básico: una población de genomas, una función de fitness, selección, cruce, mutación, generación siguiente. Cada genoma codificaba la topología de la red (número de capas, anchura), los hiperparámetros DQN (learning rate, epsilon decay, tamaño del replay buffer), y algunas decisiones arquitectónicas (qué fuentes de datos consumir, qué tamaño de embedding).

El defecto principal: el fitness se calculaba sobre los mismos datos que 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 GA selecciona activamente a los individuos que mejor explotan ese fallo.

v2: separar entrenamiento y evaluación

La corrección evidente era separar las fases: cada genoma se entrena sobre una ventana de mercado, y luego se evalúa sobre una ventana distinta, nunca vista durante el entrenamiento. Solo el rendimiento en evaluación cuenta para el fitness.

Solo ese cambio hizo caer el fitness medio de la población; señal de que buena parte de lo que parecía rendimiento en la v1 era memorización pura. Doloroso de ver, pero es exactamente la señal que buscas: una puntuación más baja pero honesta vale más que una puntuación inflada y engañosa.

v3: pasar a NSGA-II y un fitness multiobjetivo

Optimizar una sola puntuación de fitness (digamos el rendimiento) empuja mecánicamente hacia agentes que asumen riesgos extremos para maximizar esa única cifra. La solución fue pasar a NSGA-II (Non-dominated Sorting Genetic Algorithm II), que optimiza simultáneamente varios objetivos sin reducirlos a una suma ponderada arbitraria: rendimiento, drawdown máximo, ratio de Sharpe, estabilidad entre ventanas.

NSGA-II construye un frente de Pareto: el conjunto de genomas para los que ninguna mejora en un objetivo es posible sin degradar otro. En vez de forzar un único compromiso entre rendimiento y riesgo mediante una ponderación elegida de antemano, conservas toda la frontera de compromisos y dejas la elecció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 los frentes siguientes por retirada iterativa
  return fronts;
}

Segundo añadido de la v3: un archivo de Pareto persistente. Sin él, un buen genoma encontrado en la generación 12 puede desaparecer en la generación 15 si el azar del cruce no lo reproduce; aunque siga siendo mejor que todo lo que lo reemplazó. El archivo conserva, a través de todas las generaciones, el conjunto de individuos no dominados jamás encontrados, con independencia de la población actual.

v4: evolución lamarckiana y diversidad ambiental

La v3 tenía un punto ciego estructural: el genoma describía la arquitectura, pero los pesos aprendidos durante el entrenamiento desaparecían con cada nueva generación. Un descendiente nacido del cruce de dos buenos padres heredaba su arquitectura, pero tenía que reaprender desde cero; ningún rastro de los pesos que habían hecho rendir a sus padres.

La v4 introduce la evolución lamarckiana: los pesos entrenados se reinyectan en el genoma tras el entrenamiento, y se transmiten (con mutación) a la descendencia. Es una herejía biológica asumida; Lamarck se equivocaba con los organismos vivos, la herencia de los caracteres adquiridos no existe en biología; pero nada impide que un GA numérico haga trampas con inteligencia: aquí, transmitir lo adquirido acelera radicalmente la convergencia, porque cada generación parte de un punto de inicialización ya informado en vez de pesos aleatorios.

Otros tres cambios estructurales en esta versión:

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

  • Regularización por complejidad FLOPs: el coste de cálculo de la red (en FLOPs) se convierte en un objetivo de pleno derecho dentro de NSGA-II. Eso evita que la evolución converja hacia arquitecturas masivas simplemente porque tienen más capacidad bruta, sin que lo justifique la ganancia de rendimiento.

  • Interfaz RLBackend desacoplada: el GA ya no conoce los detalles de DQN. Manipula un genoma y llama a train() / evaluate() a través de una interfaz abstracta, lo que permite en teoría enchufar otro algoritmo 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 pasó a concurrencia asíncrona acotada; un pool de N evaluaciones en paralelo en vez de un bucle secuencial, con un límite explícito para no saturar los recursos GPU/CPU disponibles.

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

Defecto v3 Corrección v4 Pesos perdidos en cada generación Reinyección lamarckiana de los pesos entrenados Sobreajuste a una sola ventana de mercado Evaluación sobre ventanas múltiples, regímenes variados Arquitecturas que crecen sin restricción FLOPs como objetivo Pareto explícito GA acoplado a los detalles de DQN Interfaz RLBackend abstracta Evaluación secuencial lenta Concurrencia async acotada

La v4 también corrigió diez bugs concretos de "grounding" de API; casos donde el código del GA suponía una interfaz para TradingAgent que no correspondía exactamente con la implementación real. Ese tipo de bug es invisible hasta que confrontas el código con el verdadero código fuente del agente: la v4 solo se validó tras releer línea por línea frente al archivo real.

Por qué mezclar evolución y gradiente en vez de elegir uno u otro

Podrías preguntarte por qué no hacer simplemente RL puro, o evolución pura estilo NEAT. La respuesta cabe en una frase: el gradiente es excelente para afinar localmente (ajustar pesos continuos hacia un óptimo cercano), la evolución es excelente para explorar globalmente (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 es privarte de una de las dos formas de exploración.

El precio a pagar es la complejidad de ingeniería; cuatro versiones no eran un lujo, era el número de iteraciones necesarias para que el bucle GA + RL dejara de autosabotearse (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 un simple grid search de hiperparámetros, manteniendo la eficacia local del gradiente para cada candidato evaluado.

Próximo paso

Esta arquitectura evolutiva de un solo nivel (una población plana de genomas DQN) toca techo cuando el número de activos a cubrir sube. Eso motivó el paso a una arquitectura jerárquica de tres niveles (Asset Analysts → Sector Managers → Portfolio Allocator), con un GA operando de forma independiente en cada nivel... pero eso es tema de otro artículo.

Fiz uma rede neural evoluir por seleção natural em vez de descida de gradiente

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

O problema com a descida de gradiente sozinha

Treinar um agente DQN para trading algorítmico com a descida de gradiente clássica traz um problema simples de enunciar e difícil de resolver : o gradiente otimiza uma rede rumo 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 incentiva a diversidade ; dois runs partindo de sementes diferentes costumam convergir para estratégias quase idênticas, com os mesmos pontos cegos.

A resposta que explorei : substituir (ou melhor, sobrepor) a descida de gradiente com um algoritmo genético. Em vez de treinar um agente, fazemos uma população de agentes evoluir ; cada um um genoma codificando uma arquitetura e hiperparâmetros ; e deixamos a seleção natural fazer a triagem, enquanto o gradiente continua refinando cada indivíduo dentro da própria vida dele.

Esse runner passou por quatro versões em uma única sessão intensa de trabalho. Cada uma corrigiu um defeito estrutural da anterior.

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

A primeira versão fazia o que se espera de um GA 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 DQN (learning rate, decaimento de epsilon, tamanho do replay buffer), e algumas escolhas arquiteturais (quais fontes de dados consumir, qual tamanho de embedding).

O defeito principal : a fitness era calculada sobre os mesmos dados do 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 GA seleciona ativamente os indivíduos que melhor exploram essa brecha.

v2 : separar 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. Só o desempenho na avaliação conta para a fitness.

Só essa mudança fez a fitness média da população cair ; sinal de que boa parte do que parecia desempenho na v1 era memorização pura. Doloroso de ver, mas é exatamente o sinal que queremos : uma pontuação mais baixa mas honesta vale mais do que uma pontuação inflada e enganosa.

v3 : migrar para NSGA-II e uma fitness multiobjetivo

Otimizar uma única pontuação de fitness (digamos o retorno) empurra mecanicamente para agentes que assumem riscos extremos para maximizar esse único número. A solução foi migrar para NSGA-II (Non-dominated Sorting Genetic Algorithm II), que otimiza simultaneamente vários objetivos sem reduzi-los a uma soma ponderada arbitrária : retorno, drawdown máximo, índice de Sharpe, estabilidade entre janelas.

O NSGA-II constrói uma fronteira de Pareto : o conjunto dos genomas para os quais nenhuma melhoria em um objetivo é possível sem degradar outro. Em vez de forçar um único compromisso entre retorno e risco via uma ponderação escolhida de antemão, mantemos toda a fronteira de compromissos e deixamos a escolha final em aberto.

// Traduzido dos comentários : ordenação não dominada + construção dos fronts por remoção iterativa
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 dos fronts seguintes por remoção iterativa
  return fronts;
}

Segunda adição da v3 : um arquivo de Pareto persistente. Sem ele, um bom genoma encontrado na geração 12 pode desaparecer na geração 15 se o acaso do cruzamento não o reproduzir ; mesmo que continuasse melhor do que tudo que o substituiu. O arquivo conserva, ao longo de todas as gerações, o conjunto dos 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 descendente nascido do cruzamento de dois bons pais herdava a arquitetura deles, mas tinha que reaprender do zero ; nenhum traço dos pesos que tinham tornado os pais performáticos.

A v4 introduz a evolução lamarckiana : os pesos treinados são reinjetados no genoma após o treinamento, e transmitidos (com mutação) à descendência. É uma heresia biológica assumida ; Lamarck estava errado para organismos vivos, a hereditariedade dos caracteres adquiridos não existe em biologia ; mas nada impede um GA numérico de trapacear com inteligência : aqui, transmitir o adquirido acelera radicalmente a convergência, já que cada geração parte de um ponto de inicialização já informado 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, tiradas de regimes diferentes (alta, baixa, lateral). Um agente que brilha em uma janela e desaba em outra não pode mais dominar a fronteira de Pareto.

  • Regularização por complexidade FLOPs : o custo computacional da rede (em FLOPs) vira um objetivo por si só no NSGA-II. Isso evita que a evolução convirja para arquiteturas massivas simplesmente porque têm mais capacidade bruta, sem que isso se justifique pelo ganho de desempenho.

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

// Traduzido dos comentários : interface abstrata para desacoplar o GA do DQN
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 passou para concorrência assíncrona limitada ; um pool de N avaliações em paralelo em vez de um loop sequencial, com um limite explícito para não saturar os recursos de GPU/CPU disponíveis.

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

Defeito v3 Correção v4 Pesos perdidos a cada geração Reinjeção lamarckiana dos pesos treinados Overfitting a uma única janela de mercado Avaliação em múltiplas janelas, regimes variados Arquiteturas que crescem sem restrição FLOPs como objetivo Pareto explícito GA acoplado aos detalhes do DQN Interface RLBackend abstrata Avaliação sequencial lenta Concorrência async limitada

A v4 também corrigiu dez bugs concretos de "grounding" de API ; casos em que o código do GA assumia uma interface para TradingAgent que não correspondia exatamente à implementação real. Esse tipo de bug é invisível enquanto não confrontamos o código com o verdadeiro código-fonte do agente : a v4 só foi validada após releitura linha por linha diante do arquivo real.

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

Poderiam perguntar por que não fazer só RL puro, ou só evolução pura estilo NEAT. A resposta cabe em uma frase : o gradiente é excelente para refinar localmente (ajustar pesos contínuos rumo a um ótimo próximo), a evolução é excelente para explorar globalmente (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 é abrir mão de uma das duas formas de exploração.

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

Próxima etapa

Essa arquitetura evolutiva de um único nível (uma população plana de genomas DQN) atinge seus limites quando o número de ativos a cobrir aumenta. Foi isso que motivou a passagem para uma arquitetura hierárquica de três níveis (Asset Analysts → Sector Managers → Portfolio Allocator), com um GA operando de forma independente em cada nível... mas isso é assunto para outro artigo.

Saya mengembangkan jaringan saraf lewat seleksi alam alih-alih penurunan gradien

Bagaimana saya mengganti pelatihan klasik dengan penurunan gradien dengan algoritma genetika NSGA-II untuk mengembangkan agen DQN trading : empat versi, dari overfitting hingga evolusi lamarckian pada bobot.

Masalah dengan penurunan gradien saja

Melatih agen DQN untuk trading algoritmik dengan penurunan gradien klasik menimbulkan masalah yang sederhana diucapkan dan sulit dipecahkan : gradien mengoptimalkan satu jaringan menuju satu minimum lokal, pada satu jendela pasar. Tidak ada yang menjamin minimum itu menggeneralisasi ke rezim pasar yang berbeda, dan tidak ada dalam loop pelatihan yang mendorong keanekaragaman; dua run yang berangkat dari seed berbeda sering konvergen ke strategi yang hampir identik, dengan blind spot yang sama.

Jawaban yang saya jelajahi : mengganti (atau lebih tepatnya menumpuk) penurunan gradien dengan algoritma genetika. Alih-alih melatih satu agen, kita mengembangkan populasi agen; masing-masing genom mengodekan arsitektur dan hyperparameter; dan kita biarkan seleksi alam memilah, sementara gradien terus memoles tiap individu di dalam hidupnya sendiri.

Runner ini melewati empat versi dalam satu sesi kerja intensif. Masing-masing memperbaiki cacat struktural versi sebelumnya.

v1 : versi naif, dan kenapa tidak cukup

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

Cacat utama : fitness dihitung pada data yang sama dengan pelatihan. Agen bisa secara harfiah menghafal jendela pasar dan mendapat skor bagus tanpa mempelajari strategi yang bisa digeneralisasi. Overfitting klasik, tapi diamplifikasi oleh seleksi genetika; GA secara aktif menyeleksi individu yang paling mengeksploitasi celah ini.

v2 : memisahkan pelatihan dan evaluasi

Perbaikan yang jelas adalah memisahkan fase : setiap genom berlatih pada satu jendela pasar, lalu dievaluasi pada jendela berbeda, yang tidak pernah dilihat selama pelatihan. Hanya performa evaluasi yang dihitung untuk fitness.

Perubahan ini saja membuat fitness rata-rata populasi anjlok; tanda bahwa sebagian besar dari apa yang tampak sebagai performa di v1 adalah hafalan murni. Menyakitkan dilihat, tapi tepat sinyal yang kita inginkan : skor lebih rendah tapi jujur lebih baik daripada skor menggembung dan menipu.

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

Mengoptimalkan satu skor fitness (misalnya return) secara mekanis mendorong ke agen yang mengambil risiko ekstrem untuk memaksimalkan satu angka itu. Solusinya adalah beralih ke NSGA-II (Non-dominated Sorting Genetic Algorithm II), yang mengoptimalkan beberapa objektif sekaligus tanpa mereduksinya jadi jumlah berbobot yang sewenang-wenang : return, drawdown maksimal, rasio Sharpe, stabilitas antar-jendela.

NSGA-II membangun front Pareto : himpunan genom di mana tidak ada perbaikan pada satu objektif yang mungkin tanpa memperburuk yang lain. Alih-alih memaksakan satu kompromi antara return dan risiko lewat pembobotan yang dipilih di awal, kita 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);
    }
  }
  // ... pembangunan front berikutnya lewat pelepasan iteratif
  return fronts;
}

Tambahan kedua di v3 : arsip Pareto persisten. Tanpanya, genom bagus yang ditemukan di generasi 12 bisa hilang di generasi 15 kalau kebetulan crossover tidak mereproduksinya; meski ia tetap lebih baik dari semua yang menggantikannya. Arsip menyimpan, lintas semua generasi, himpunan individu tak terdominasi yang pernah ditemui, terlepas dari populasi saat ini.

v4 : evolusi lamarckian dan keanekaragaman lingkungan

V3 punya blind spot struktural : genom menggambarkan arsitektur, tapi bobot yang dipelajari selama pelatihan hilang di setiap generasi baru. Keturunan yang lahir dari crossover dua orang tua yang bagus mewarisi arsitektur mereka, tapi harus belajar ulang dari nol; tidak ada jejak bobot yang membuat orang tuanya performan.

V4 memperkenalkan evolusi lamarckian : bobot terlatih disuntikkan kembali ke genom setelah pelatihan, dan diwariskan (dengan mutasi) ke keturunan. Ini kesesatan biologis yang disadari; Lamarck salah untuk organisme hidup, hereditas sifat yang didapat tidak ada dalam biologi; tapi tidak ada yang melarang GA numerik curang dengan cerdas : di sini, mewariskan yang didapat mempercepat konvergensi secara radikal, karena setiap generasi berangkat dari titik inisialisasi yang sudah terinformasi alih-alih bobot acak.

Tiga perubahan struktural lain di versi ini :

  • Keanekaragaman lingkungan : setiap genom tidak lagi dievaluasi pada satu jendela pasar tapi pada beberapa, yang diambil dari rezim berbeda (bullish, bearish, range). Agen yang unggul pada satu jendela dan runtuh pada jendela lain tidak bisa lagi mendominasi front Pareto.

  • Regularisasi lewat kompleksitas FLOPs : biaya komputasi jaringan (dalam FLOPs) menjadi objektif tersendiri di NSGA-II. Ini mencegah evolusi konvergen ke arsitektur masif hanya karena kapasitas mentahnya lebih besar, tanpa dibenarkan oleh perolehan performa.

  • Antarmuka RLBackend yang terpisah : GA tidak lagi mengenal detail DQN. Ia memanipulasi genom dan memanggil train() / evaluate() lewat antarmuka abstrak, yang secara teori memungkinkan mencolokkan 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 berbatas; pool berisi N evaluasi paralel alih-alih loop sekuensial, dengan batas eksplisit supaya tidak menjenuhkan sumber daya GPU/CPU yang tersedia.

Apa yang diperbaiki v4 dibanding v3 dalam praktik

Cacat v3 Perbaikan v4 Bobot hilang di setiap generasi Penyuntikan lamarckian pada bobot terlatih Overfitting pada satu jendela pasar Evaluasi pada banyak jendela, rezim bervariasi Arsitektur yang membesar tanpa batas FLOPs sebagai objektif Pareto eksplisit GA terkopling pada detail DQN Antarmuka RLBackend abstrak Evaluasi sekuensial yang lambat Konkurensi async berbatas

V4 juga memperbaiki sepuluh bug konkret "grounding" API; kasus di mana kode GA mengasumsikan antarmuka untuk TradingAgent yang tidak persis cocok dengan implementasi nyata. Bug semacam ini tak terlihat selama kode tidak dihadapkan pada kode sumber nyata agen : v4 baru tervalidasi setelah dibaca ulang baris per baris berhadapan dengan berkas aslinya.

Kenapa mencampur evolusi dan gradien alih-alih memilih salah satu

Orang bisa bertanya kenapa tidak sekadar RL murni, atau evolusi murni ala NEAT. Jawabannya muat dalam satu kalimat : gradien unggul untuk memoles secara lokal (menyesuaikan bobot kontinu menuju optimum dekat), evolusi unggul untuk mengeksplorasi secara global (menemukan arsitektur dan kombinasi hyperparameter yang tidak bisa dicapai gradien mana pun, karena ruang pencarian diskret tidak terdiferensiasi). Memakai satu tanpa yang lain berarti kehilangan salah satu dari dua bentuk eksplorasi.

Harga yang dibayar adalah kompleksitas rekayasa; empat versi bukan kemewahan, itu jumlah iterasi yang diperlukan agar loop GA + RL berhenti menyabotase dirinya sendiri (overfitting, kehilangan individu bagus, kehilangan bobot yang didapat). Tapi hasilnya adalah sistem yang menjelajahi ruang desain jauh lebih luas daripada sekadar grid search hyperparameter, sambil menjaga efisiensi lokal gradien untuk setiap kandidat yang dievaluasi.

Langkah berikutnya

Arsitektur evolusioner satu tingkat ini (populasi datar genom DQN) mencapai batasnya ketika jumlah aset yang dicakup naik. Inilah yang memotivasi peralihan ke arsitektur hierarkis tiga tingkat (Asset Analysts → Sector Managers → Portfolio Allocator), dengan GA yang beroperasi independen di tiap tingkat... tapi itu topik artikel lain.

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

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

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

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

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

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

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

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

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

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

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

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

v3 : NSGA-II और बहु उद्देश्य फिटनेस पर जाना

एक ही फिटनेस स्कोर अनुकूलित करना (मान लो प्रतिफल) यंत्रवत उन एजेंटों की ओर धकेलता है जो उस एक अंक को अधिकतम करने के लिए चरम जोखिम लेते हैं। समाधान NSGA-II (Non-dominated Sorting Genetic Algorithm II) पर जाना था, जो कई उद्देश्यों को एक साथ अनुकूलित करता है बिना उन्हें मनमानी भारित राशि में घटाए : प्रतिफल, अधिकतम drawdown, 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 लामार्कवादी विकास लाता है : प्रशिक्षित भार प्रशिक्षण के बाद जीनोम में फिर से डाले जाते हैं, और वंश को (उत्परिवर्तन के साथ) प्रेषित किए जाते हैं। यह स्वीकृत जैविक पाखंड है; Lamarck जीवित जीवों के लिए गलत था, उपार्जित लक्षणों की वंशागति जीव विज्ञान में मौजूद नहीं है; लेकिन कुछ भी डिजिटल GA को चतुराई से धोखा देने से नहीं रोकता : यहाँ, उपार्जित को प्रेषित करना अभिसरण तेज करता है, क्योंकि हर पीढ़ी बेतरतीब भारों के बजाय पहले से सूचित आरंभीकरण बिंदु से शुरू होती है।

इस संस्करण में तीन अन्य संरचनात्मक बदलाव :

  • पर्यावरणीय विविधता : हर जीनोम अब एक ही बाजार विंडो पर मूल्यांकित नहीं होता बल्कि कई पर, अलग व्यवस्थाओं से निकाली गई (तेजी, मंदी, range)। एक एजेंट जो एक विंडो में चमकता है और दूसरी में ढहता है वह 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 संसाधन संतृप्त न करने के लिए स्पष्ट सीमा के साथ।

व्यवहार में v3 की तुलना में v4 क्या सुधारता है

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

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

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

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

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

अगला चरण

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

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

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

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

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

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

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

الإصدار 1 : النسخة الساذجة، ولماذا لم تكن كافية

الإصدار الأول فعل ما يُنتظر من GA أساسي : مجموعة جينومات، ودالة fitness، وانتقاء، وتزاوج، وطفرة، ثم جيل تالٍ. كل جينوم كان يرمّز طوبولوجيا الشبكة (عدد الطبقات، العرض)، والأفراط DQN (معدل التعلم، واضمحلال epsilon، وحجم replay buffer)، وبعض الخيارات البنيوية (أي مصادر بيانات يستهلك، وحجم الـembedding).

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

الإصدار 2 : فصل التدريب عن التقييم

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

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

الإصدار 3 : الانتقال إلى NSGA-II وfitness متعددة الأهداف

تحسين نتيجة fitness واحدة (لنقل العائد) يدفع آليا نحو وكلاء يأخذون مخاطر قصوى لتعظيم هذا الرقم الوحيد. وكان الحل هو الانتقال إلى NSGA-II (Non-dominated Sorting Genetic Algorithm 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);
    }
  }
  // ... construction des fronts suivants par retrait itératif
  return fronts;
}

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

الإصدار 4 : التطور اللاماركي والتنوع البيئي

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

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

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

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

  • التنظيم بتعقيد FLOPs : صار cost الحساب للشبكة (بـ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 المتاحة.

ما يصححه الإصدار 4 مقارنة بالإصدار 3 عمليا

عيب الإصدار 3 تصحيح الإصدار 4 أوزان ضائعة مع كل جيل إعادة حقن لاماركية للأوزان المدربة إفراط تخصيص لنافذة سوق واحدة تقييم على نوافذ متعددة، أنظمة متنوعة بنى تكبر دون قيد FLOPs كهدف Pareto صريح GA مقترن بتفاصيل DQN واجهة RLBackend مجردة تقييم تسلسلي بطيء تزامن async محدود

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

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

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

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

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

هذه البنية التطورية ذات المستوى الواحد (مجموعة مسطحة من جينومات DQN) تبلغ حدودها عندما يرتفع عدد الأصول الواجب تغطيتها. وهذا ما حفّز الانتقال إلى بنية هرمية بثلاثة مستويات (Asset Analysts ← Sector Managers ← Portfolio Allocator)، مع GA يعمل مستقلا في كل مستوى... لكن ذلك موضوع مقال آخر.

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

Cách tôi thay thế huấn luyện cổ điển bằng gradient descent bằng thuật toán di truyền NSGA-II để tiến hóa các tác nhân DQN giao dịch: bốn phiên bản, từ overfitting đến tiến hóa Lamarck 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 đặt ra một vấn đề dễ phát biểu nhưng khó giải: gradient tối ưu một mạng về một cực tiểu cục bộ, trên một cửa sổ thị trường. Không có gì đảm bảo cực tiểu đó khái quát hóa sang một chế độ thị trường khác, và không có gì trong vòng huấn luyện thúc đẩy sự đa dạng; hai lần chạy khởi đầu 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à chồng thêm) 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, ta tiến hóa một quần thể tác nhân; mỗi cá thể là một genome mã hóa một kiến trúc và các siêu tham số; và để chọn lọc tự nhiên làm việc sàng lọc, trong khi gradient tiếp tục tinh chỉnh từng cá thể trong chính đời sống của nó.

Runner này đã trải qua bốn phiên bản trong một phiên làm việc cường độ cao duy nhất. Mỗi phiên bản sửa một khiếm khuyết cấu trúc của phiên bản trước.

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

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

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

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

Cách sửa hiển nhiên là tách các pha: mỗi genome huấn luyện trên một cửa sổ thị trường, rồi được đánh giá trên một cửa sổ khác, chưa từng thấy trong lúc huấn luyện. Chỉ hiệu năng đánh giá mới tính vào fitness.

Riêng thay đổi này đã làm fitness trung bình của quần thể tụt xuống; dấu hiệu cho thấy phần lớn những gì trông như hiệu năng ở v1 chỉ là ghi nhớ thuần túy. Đau khi nhìn, nhưng đó chính xác là tín hiệu ta muốn: một điểm thấp hơn nhưng trung thực đáng giá hơn một điểm phồng và đánh lừa.

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

Tối ưu một điểm fitness duy nhất (ví dụ lợi nhuận) đẩy một cách máy móc về các tác nhân chấp nhận rủi ro cực đoan để tối đa hóa đúng con số đó. Giải pháp là chuyển sang NSGA-II (Non-dominated Sorting Genetic Algorithm II), tối ưu đồng thời nhiều mục tiêu mà không rút gọn chúng thành một tổng có trọng số tùy tiện: lợi nhuận, drawdown tối đa, tỷ lệ Sharpe, độ ổn định liên cửa sổ.

NSGA-II xây dựng một mặt Pareto: tập hợp các genome mà không thể cải thiện một mục tiêu nào mà không làm xấu mục tiêu khác. Thay vì ép một thỏa hiệp duy nhất giữa lợi nhuận và rủi ro qua một trọng số chọn trước, ta giữ toàn bộ biên 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 front tiếp theo bằng loại bỏ lặp
  return fronts;
}

Bổ sung thứ hai của v3: một archive Pareto bền vững. Không có nó, một genome tốt tìm thấy ở thế hệ 12 có thể biến mất ở thế hệ 15 nếu sự ngẫu nhiên của lai ghép không tái tạo nó; dù nó vẫn tốt hơn mọi thứ đã thay thế nó. Archive giữ lại, xuyên suốt mọi thế hệ, tập hợp các cá thể không bị chi phối từng gặp, độc lập với quần thể hiện tại.

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

v3 có một điểm mù cấu trúc: genome mô tả kiến trúc, nhưng các trọng số học được trong lúc huấn luyện biến mất ở mỗi thế hệ mới. Một hậu duệ sinh từ lai ghép của hai bố mẹ tốt thừa hưởng kiến trúc của chúng, nhưng phải học lại từ con số không; không còn dấu vết nào của các trọng số từng khiến bố mẹ nó hiệu quả.

v4 đưa vào tiến hóa Lamarck: các trọng số đã huấn luyện được bơm ngược vào genome sau huấn luyện, và truyền lại (kèm đột biến) cho đời sau. Đây là một dị giáo sinh học có chủ ý; Lamarck đã sai đối với sinh vật sống, di truyền tính trạng tập nhiễm không tồn tại trong sinh học; nhưng không có gì cấm một GA số gian lận một cách thông minh: ở đây, truyền lại cái đã học tăng tốc hội tụ triệt để, vì mỗi thế hệ khởi đầu từ một điểm đã 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 genome 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ổ, rút 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ổ mà sụp đổ trên cửa sổ khác không thể chi phối mặt Pareto nữa.

  • Chính quy hóa bằng độ 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 riêng trong NSGA-II. Việc này tránh để tiến hóa hội tụ về các kiến trúc khổng lồ chỉ vì chúng có dung lượng thô lớn hơn, mà hiệu năng tăng thêm không xứng đáng.

  • Interface RLBackend tách rời: GA không còn biết chi tiết của DQN. Nó thao tác genome và gọi train() / evaluate() qua một interface trừu tượng, về lý thuyết cho phép gắn một thuật toán RL khác mà không đụng tới độ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 đồng thời async có giới hạn; một pool N đánh giá song song thay vì vòng lặp tuần tự, với giới hạn rõ ràng để không bão hòa tài nguyên GPU/CPU hiện có.

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

Khiếm khuyết v3 Sửa chữa v4 Trọng số mất ở mỗi thế hệ Bơm ngược Lamarck các 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ình to không giới hạn FLOPs thành mục tiêu Pareto rõ ràng GA gắn chặt với chi tiết DQN Interface RLBackend trừu tượng Đánh giá tuần tự chậm Đồng thời async có giới hạn

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

Vì sao pha trộn tiến hóa và gradient thay vì chọn một trong hai

Người ta có thể hỏi sao không làm RL thuần, hay tiến hóa thuần kiểu NEAT. Câu trả lời gói trong một câu: gradient xuất sắc trong tinh chỉnh cục bộ (chỉnh các trọng số liên tục về một cực trị gần), tiến hóa xuất sắc trong 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 chạm tới, vì không gian tìm kiếm rời rạc không khả vi). Dùng một mà thiếu kia, là tự tước một trong hai dạng khám phá.

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

Bước tiếp theo

Kiến trúc tiến hóa một tầng này (một quần thể phẳng các genome DQN) chạm giới hạn khi số tài sản cần phủ tăng lên. Đó là điều thúc đẩy chuyển sang kiến trúc phân cấp ba tầng (Asset Analysts → Sector Managers → Portfolio Allocator), với một GA vận hành độc lập ở mỗi tầng... nhưng đó là chủ đề của một bài 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