ARC-AGI Program Synthesis System

repository·main·Indexed 41 days ago

https://github.com/epang080516/arc_agi

A DreamCoder-inspired, LLM-assisted program synthesis system designed to solve Abstract and Reasoning Corpus (ARC-AGI) tasks. It features a Latent Program Network (LPN) implemented with JAX and Flax, utilizing a hybrid approach of LLM-driven Python program generation and a growing library of learned programs to solve complex reasoning tasks.

Tokens
11.7K
Snippets
33
Records
39
Agent score
51%

What's inside ARC-AGI

  1. LPN Repository Structure

    main

    The repository is organized as follows:

    • src/configs/: Configuration files (managed by Hydra).
    • src/datasets/: Data processing utilities (specifically for re-arc).
    • src/models/: Neural network architectures (the LPN implementation).
    • src/train.py: The main entry point for training models.
  2. Configure Program Selection Heuristics

    main

    When selecting which programs from the library to include in the LLM prompt, the system supports different selection strategies:

    • Lexicographical Sorting (Default): Sorts library programs first by primary accuracy score, then uses the secondary accuracy score as a tie-breaker. The best program is then included in the prompt.
    • Score-weighted Sampling (Exploration): Instead of picking the absolute best, the system sums the primary and secondary scores, converts them into probabilities using a softmax function, and samples programs. This encourages exploration of the program space.

    For the final submission implementation, the system uses two selected programs from the library in the prompt to provide better context.

  3. Understand the Evolutionary Program Synthesis Architecture

    main

    The system uses a hybrid approach combining LLM-driven program generation with a growing library of successful programs to solve ARC-AGI tasks.

    Core Workflow:

    1. Initialization: Starts with an empty library of Python programs.
    2. Prompting: For each task, the LLM is prompted to generate Python programs that solve the training examples. The prompt includes:
      • Input/output grids in multiple formats (dimensions, Base64, ASCII, and Python nested lists).
      • Chain-of-Thought (CoT) reasoning instructions.
      • The current best program(s) from the existing library.
    3. Scoring: Every program in the library is evaluated using two metrics:
      • Primary Accuracy Score: Number of correct training examples.
      • Secondary Accuracy Score: Average cell-level accuracy on training examples.
    4. Library Expansion: The LLM-generated program with the best primary and secondary scores is added to the library.
    5. Iteration: The system loops through tasks, expanding the library, for a user-defined number of rounds.

    This approach aims to bridge the gap between the scalability of LLMs (generating diverse programs) and the knowledge transfer of systems like DreamCoder (using a library of programs).

  4. Install the Latent Program Network (LPN)

    main

    To install LPN, first ensure you have JAX installed following the official JAX instructions. Then, clone the repository, add the current directory to your PYTHONPATH, and install the dependencies via pip.

    Additionally, you must set your HuggingFace token and WandB API key as environment variables for the project to function correctly.

    git clone https://github.com/clement-bonnet/lpn
    cd lpn
    export PYTHONPATH=${PYTHONPATH}:${PWD}
    pip install -U -r requirements.txt
    
    # Set required environment variables
    export HF_TOKEN=...
    export WANDB_API_KEY=...
  5. Optimize LLM Prompts for ARC-AGI Tasks

    main

    Based on experimental results, the following prompt engineering techniques significantly improve performance:

    • Multi-format Grid Representation: Represent input/output grids using:
      • Grid dimensions
      • Base64 image encoding
      • ASCII
      • Python nested lists
    • Chain-of-Thought (CoT): Require the LLM to perform reasoning steps.
    • Error Feedback: Include failed training examples and the specific difference between the expected output and the actual output in the prompt. Providing this context is critical for performance.
  6. Install and set up the ARC-AGI repository

    main

    To set up the environment for running the ARC-AGI program synthesis system, follow these steps:

    1. Clone and enter the directory:
      git clone <repository_url>
      cd arc_agi
    2. Configure Environment Variables: Create a .env file in the root directory and add your API credentials. You only need to provide the ones you intend to use:
      • OPENAI_API_KEY
      • ANTHROPIC_API_KEY
      • XAI_API_KEY
      • LOGFIRE_TOKEN
    3. Create a Python 3.11 Virtual Environment:
      python3.11 -m venv venv
      source venv/bin/activate
    4. Install Dependencies:
      pip install -U -r requirements_py311.txt
    python3.11 -m venv venv
    source venv/bin/activate
    pip install -U -r requirements_py311.txt
  7. Perform Gradient Ascent on Latents

    main

    Instead of random search, you can optimize latents directly using gradient ascent. Define a loss function based on lpn._compute_log_probs (the log probability of the ground truth output given the context). Use jax.value_and_grad to compute gradients with respect to the latents and update them using an optimizer like optax.adam.

    def log_probs_fn(latents, input_seq, output_seq):
        # Repeat latents for all pairs in the task
        latents = latents[..., None, :].repeat(output_seq.shape[-2], axis=-2)
        row_logits, col_logits, grid_logits = lpn.decoder.apply(
            {"params": loaded_state.params["decoder"]},
            input_seq, output_seq, latents, dropout_eval=True
        )
        return lpn.apply({"params": loaded_state.params}, row_logits, col_logits, grid_logits, output_seq, method=lpn._compute_log_probs)
    
    # Optimization loop
    log_probs, grads = jax.value_and_grad(log_probs_fn)(search_latents, input_seq, output_seq)
    search_latents = search_latents + lr * grads
  8. Perform Gradient Ascent in Latent Space

    main

    To find optimal latent contexts that maximize the log-probability of a specific task, you can perform gradient ascent on the log_prob_other_pairs function. This is useful for exploring the model's learned task manifold.

    Workflow:

    1. Define a function that computes log_prob for a given context using the decoder.
    2. Use jax.value_and_grad to get the gradient of the log-probability with respect to the context.
    3. Apply an optimizer (like optax.adam) to update the current_context in the direction of the gradient.
    4. Map the resulting contexts back to CDF space (using norm.cdf) for visualization.
    # 1. Define log_prob function
    @jax.jit
    def log_prob_other_pairs(context):
        input_seq, output_seq = lpn._flatten_input_output_for_decoding(pairs[1:], grid_shapes[1:])
        row_logits, col_logits, grid_logits = lpn.decoder.apply(
            {"params": loaded_state.params["decoder"]},
            input_seq=input_seq,
            output_seq=output_seq,
            context=context[..., None, :].repeat(output_seq.shape[-2], axis=-2),
            dropout_eval=True,
        )
        return lpn._compute_log_probs(row_logits, col_logits, grid_logits, output_seq)
    
    # 2. Gradient Ascent Loop
    optimizer = optax.adam(learning_rate=0.01)
    current_context = start_context
    opt_state = optimizer.init(current_context)
    
    for _ in range(26):
        log_prob, grad = jax.value_and_grad(log_prob_other_pairs)(current_context)
        updates, opt_state = optimizer.update(-grad, opt_state, current_context)
        current_context = optax.apply_updates(current_context, updates)
  9. ARC-AGI Transformation Rule Requirements

    main

    When using the CoT reasoning template, the model is instructed to follow a specific contract for the transformation rule:

    • Input/Output Format: Grids are rectangular matrices of integers (0-9).
    • Color Mapping:
      • 0: black, 1: blue, 2: red, 3: green, 4: yellow, 5: grey, 6: pink, 7: orange, 8: purple, 9: brown.
    • Reasoning Protocol: The model must use <reasoning></reasoning> tags to resolve uncertainties and ambiguities before writing code.
    • Code Interface: The output must contain a Python function with the following signature:
      def transform(input_grid: list[list[int]]) -> list[list[int]]:
          # implementation
          return output_grid
    • Constraints: Do not include tests in the generated code; only output the transform function.
  10. Perform Random Search in Latent Space

    main

    To find optimal latents for a task, you can perform a random search by sampling from a normal distribution around the mean latent vector. The search is controlled by a scale parameter. For each sampled context, use lpn.apply with the method=lpn._generate_output_from_context to generate predicted grids and shapes, then evaluate them using lpn._compute_log_probs to find the best candidate.

    # Sampling contexts
    scale = 1.0
    num_samples = 1000
    contexts = latents.mean(axis=-2) + scale * jax.random.normal(jax.random.PRNGKey(0), (num_samples, 32))
    
    # Generate outputs and evaluate
    output_grids, output_shapes = lpn.apply(
        {"params": loaded_state.params},
        context=contexts,
        input=input,
        input_grid_shape=input_grid_shape,
        dropout_eval=True,
        method=lpn._generate_output_from_context
    )
  11. Run inference and generate ARC-AGI submissions

    main

    To generate predictions for the ARC-AGI challenge, use the Evaluator class.

    1. Initialize Evaluator with your LPN model instance.
    2. Configure inference_mode (e.g., "gradient_ascent") and provide inference_mode_kwargs to control the optimization process during inference.
    3. Load the challenge JSON file.
    4. Call evaluator.json_submission(challenges, loaded_state.params) to produce the generations.
    5. Save the resulting dictionary to a JSON file.
    from src.evaluator import Evaluator
    import json
    
    # Configure Evaluator
    evaluator = Evaluator(
        lpn,
        inference_mode="gradient_ascent",
        inference_mode_kwargs={
            "num_steps": 200,
            "lr": 1.0,
            "lr_schedule": True,
            "optimizer": "adam",
            "optimizer_kwargs": {"b2": 0.9},
            "accumulate_gradients_decoder_pairs": True,
        },
    )
    
    # Run submission
    with open("/path/to/arc-agi_test_challenges.json", "r") as f:
        challenges = json.load(f)
    
    generations = evaluator.json_submission(challenges, loaded_state.params, progress_bar=True)
    
    with open("submission.json", "w") as f:
        json.dump(generations, f, indent=4)
  12. Load LPN Model Checkpoints from WandB

    main

    To use a pre-trained Latent Program Network (LPN), you can download artifacts from Weights & Biases (WandB). The process involves:

    1. Initializing a WandB run.
    2. Downloading the model artifact.
    3. Extracting the configuration from the artifact metadata.
    4. Instantiating the EncoderTransformer, DecoderTransformer, and the LPN model.
    5. Loading the trained parameters from the state.msgpack file into a Flax TrainState.

    Note: This requires omegaconf, hydra, jax, flax, and wandb to be installed.

    import os
    import jax
    import jax.numpy as jnp
    import wandb
    import omegaconf
    import hydra
    from flax.training.train_state import TrainState
    from flax.serialization import from_bytes
    from lpn.src.models.lpn import LPN
    from lpn.src.models.transformer import EncoderTransformer, DecoderTransformer
    
    # 1. Download artifact
    artifact_path = "TheThinker/ARC/quiet-thunder-789--checkpoint:v0"
    run = wandb.init()
    artifact = run.use_artifact(artifact_path, type='model')
    run.finish()
    cfg = omegaconf.OmegaConf.create(artifact.metadata)
    artifact_dir = artifact.download()
    
    # 2. Instantiate model components
    encoder = EncoderTransformer(hydra.utils.instantiate(cfg.encoder_transformer))
    decoder = DecoderTransformer(hydra.utils.instantiate(cfg.decoder_transformer))
    lpn = LPN(encoder=encoder, decoder=decoder)
    
    # 3. Initialize state for loading (requires dummy inputs matching model config)
    key = jax.random.PRNGKey(0)
    grids = jax.random.randint(key, (1, 3, decoder.config.max_rows, decoder.config.max_cols, 2), minval=0, maxval=decoder.config.vocab_size)
    shapes = jax.random.randint(key, (1, 3, 2, 2), minval=1, maxval=min(decoder.config.max_rows, decoder.config.max_cols) + 1)
    variables = lpn.init(key, grids, shapes, dropout_eval=False, prior_kl_coeff=0.0, pairwise_kl_coeff=0.0, mode="mean")
    
    # 4. Setup optimizer and TrainState
    optimizer = optax.adamw(0.0)
    train_state = TrainState.create(apply_fn=lpn.apply, tx=optimizer, params=variables["params"])
    
    # 5. Load weights
    with open(os.path.join(artifact_dir, "state.msgpack"), "rb") as data_file:
        byte_data = data_file.read()
    loaded_state = from_bytes(train_state, byte_data)