Evolving Neural Architectures with LLM Tools
Core principle: use the LLM as a tool for local, constrained decisions, never as an autonomous end-to-end generator. Wrap it inside an algorithmic scaffold that guarantees executability.
Minimal pipeline:
Python# 1. Mine reusable modules from existing source code (rule-based, no LLM) module_db = mine_modules_from_source(codebase_paths) # AST-based extraction # 2. Represent architecture as a hierarchical tree (not a data-flow graph) tree = build_tree_from_module(base_model, module_db) # 3. Evolve: coarse decision by algorithm, fine decision by LLM for step in range(num_iterations): coarse_action = diversity_guided_planner(tree, history_db) # e.g. "swap backbone.stage2" candidate_tree = llm_resolve_transformation(tree, coarse_action, module_db) # LLM fills in details if is_executable(candidate_tree): score = train_and_eval(candidate_tree) history_db.add(candidate_tree, score)
Progress:
- Step 1: Mine module database from source code via AST parsing
- Step 2: Convert candidate architectures into hierarchical tree representation
- Step 3: Define coarse-level evolutionary operators (structural/topological choices)
- Step 4: Delegate fine-grained parameter/module resolution to LLM
- Step 5: Enforce executability checks before evaluation
- Step 6: Update history database and Bayesian diversity model
- Step 7: Repeat with exploitation/exploration balance
Step 1: Module Mining (Rule-Based, Not LLM)
- Parse source into an Abstract Syntax Tree (AST); never use an LLM for extraction — it must be 100% correct, deterministic, and hallucination-free.
- Identify all classes inheriting the framework's base module type (e.g.,
torch.nn.Module). - For each module, record:
- Search space
s_i: every constructor (__init__) argument becomes a tunable hyperparameter. - Metadata
μ_i: default arg values, number of forward() inputs/outputs, and raw source snippet.
- Search space
- Store as
D_M = {(m_i, s_i, μ_i)}. This database is the sole source of legal building blocks — no free-form code synthesis.
Step 2: Deploy-Friendly Tree Representation
- Represent the full architecture as a tree, not a data-flow graph:
- Root = model; children = conceptual sub-modules (backbone, neck, head); leaves = scalar hyperparameters (channels, kernel size, bias).
- Intermediate nodes = modules (can be expanded further); leaf nodes = terminal hyperparameters.
- Support list-typed nodes for variable-length structures (e.g., stacking N repeated layers).
- This representation mirrors how humans structure code (nested modules), unlike data-flow graphs, making it directly compatible with reused source code and top-down refinement (start rough, add detail later).
Step 3–4: Coarse-to-Fine Evolution
- Coarse level (algorithmic): decide what kind of structural change to make (e.g., replace a subtree, insert a stage, change list length). Governed by a diversity-guided algorithm using Bayesian modeling over historical outcomes (
D_A = {(A_i, e_i)}) to balance exploration vs. exploitation. - Fine level (LLM): given the coarse directive and legal module choices from
D_M, the LLM resolves remaining degrees of freedom — which specific module, what hyperparameter values, how to connect it — producing a concrete tree transformation. - Critically: the LLM operates on a constrained decision template, not open-ended code generation. Its output space is limited to valid tree edits using cataloged modules.
Step 5–7: Execution and Feedback Loop
- Validate every candidate tree is executable (correct shapes, valid arg types) before spending compute on training.
- Convert tree → runnable model code deterministically (tree structure maps directly to nested module instantiation).
- Train/evaluate, then push
(A_i, e_i)back into the architecture database to refine future Bayesian-guided coarse planning.
Example 1:
Input: A codebase containing multiple custom PyTorch modules (ConvBlock, AttentionNeck, FPNHead).
Output: A module database with entries like {"ConvBlock": {search_space: {kernel_size, stride, channels}, defaults: {...}, forward_io: (1,1)}}. Architecture tree has model → backbone (list of ConvBlock) → neck (AttentionNeck) → head (FPNHead), each hyperparameter as a leaf.
Example 2:
Input: Coarse planner decides "insert an additional stage into backbone list."
Output: LLM selects a specific module from D_M compatible with adjacent stage's input/output shapes, sets its hyperparameters (e.g., channels doubled from previous stage), and returns the tree-edit patch — not raw code.
Example 3:
Input: Need to represent a complex, deeply nested model with variable number of repeated blocks.
Output: Tree with a list-node under backbone.stages, whose length is itself a tunable coarse-level decision (evolutionary algorithm adds/removes list elements) while LLM fills in the content of newly added elements.
- Never let the LLM generate free-form code for the full architecture — it biases toward training-corpus patterns and risks non-executable output. Confine LLM decisions to filling gaps in an already-valid tree structure.
- Keep module mining 100% rule-based (AST-based) to guarantee correctness and enable instant reanalysis of large codebases.
- Separate concerns by granularity: algorithm handles structural/topological breadth (diversity), LLM handles semantic plausibility of local choices (which module/values make sense together).
- Always validate executability before training — cheap static checks (shape/type compatibility) save expensive wasted training runs.
- Maintain a persistent history database of (architecture, score) pairs; use it for Bayesian-guided exploration so search doesn't repeat unproductive regions.
- Prefer tree representation over data-flow graphs when architectures come from real nested-module source code — trees map naturally to how PyTorch/TF models are actually written, avoiding intrusive rewriting.
- Support top-down refinement: allow coarse architecture skeletons to be proposed first, then progressively filled with details — this matches both human design workflows and LLM strengths (local reasoning) vs weaknesses (global consistency).
- Do not treat the LLM as an autonomous agent that writes the entire model end-to-end — this reduces controllability and reliability, and biases discovery toward memorized patterns.
- Do not use hand-crafted, narrow search spaces purely for safety — this defeats open-ended exploration; instead constrain via structure (tree) not via a small fixed operator set.
- Do not use an LLM for module extraction/mining — deterministic AST parsing is strictly better (faster, error-free, no hallucination).
- Do not conflate data-flow graph representations with deploy-ready code structure — data-flow graphs are hard to map back to nested module source code and complicate reuse of existing implementations.
- Do not let list-structured (repeated-block) architectures be flattened into fixed-size graphs — this loses the natural variable-depth expressiveness needed for open-ended search.
- Do not skip the executability check step — invalid trees wasted on full training runs are costly; always statically validate first.