memo: A Probabilistic Programming Language for Reasoning about Reasoning

repository·main·Indexed 41 days ago

https://github.com/kach/memo

A domain-specific probabilistic programming language (PPL) designed for computational cognitive models and recursive reasoning (theory of mind). Built on JAX, memo provides high-performance, differentiable, array-based inference for models like Rational Speech Act (RSA), POMDPs, and inverse planning. It simulates all possible traces simultaneously to provide full posterior distributions via JAX-compiled array programs.

Tokens
27K
Snippets
78
Records
90
Agent score
50%

What's inside memo

  1. What is memo?

    main

    memo is a probabilistic programming language designed for expressing computational cognitive models that involve recursive reasoning about reasoning.

    It is built to improve upon the WebPPL-based Bayesian modeling tradition (such as probmods, agentmodels, and problang) by utilizing modern programming techniques and hardware acceleration (including GPUs).

    Key benefits include:

    • Simplicity: Models are significantly easier to express; idiomatic code can be up to 3x smaller than traditional implementations (e.g., a POMDP solver in 15 lines).
    • Performance: Execution and data fitting are dramatically faster, with reported speedups of 3,000x or more compared to non-optimized implementations.
  2. Explore memo demo implementations

    main

    The demo/ directory contains a collection of implementations demonstrating how memo can be used to model various cognitive, economic, and linguistic phenomena. These demos range from simple Python scripts to Jupyter notebooks, covering several domains:

    • Language: Variations on Rational Speech Act (RSA) models, scalar implicature, and interfacing with deep learning/computer vision (e.g., 7-segment displays).
    • Economic Games: Modeling strategic interactions like Schelling games, Ultimatum games, Bayesian Persuasion, and I-POMDPs.
    • Planning and Inverse Planning: Implementing MDPs, POMDPs, and using memo to perform inverse planning (e.g., the 'Takeaway' demo to infer computational costs).
    • Developmental and Comparative Psychology: Modeling false belief tests (Sally-Anne), diverse desires, and belief revision.
    • Information Theory: Calculating Expected Information Gain (EIG) and Empowerment.
    • Puzzles: Solving classic logic and probability puzzles like Monty Hall, Newcomb's problem, and Cheryl's birthday.
  3. Create hypothetical worlds with `imagine`

    main

    The imagine[...] expression allows you to set up a hypothetical scenario. The statements inside the brackets modify the world (e.g., defining new agent behaviors or observations), and the final line is the expression evaluated within that hypothetical context.

    imagine[
        bob: chooses(y in Y, wpp=1),
        alice: observes [bob.y] is bob.y,
        alice[Pr[bob.x == 7]]
    ]
  4. When to use memo vs Gen or WebPPL

    main

    memo is specialized for fast tabular/enumerative inference on models with recursive reasoning (e.g., RSA, POMDP planning, inverse planning). It is ideal for making nested queries.

    Use Gen or WebPPL if:

    1. You need to model sophisticated inference schemes like MCMC, particle filters, or variational inference.
    2. You are performing inference over an unbounded domain of hypotheses (e.g., programs from a grammar). memo requires finite support (though you can approximate unbounded domains by truncating them or using fine discretization).

    Key Difference: Unlike sampling-based PPLs that generate traces, memo simulates all possible traces simultaneously, providing access to the full posterior distribution via JAX-compiled array programs.

  5. Reference agent choices in expressions

    main

    When an agent makes a choice using chooses, that choice becomes a variable available within the agent's scope. You can reference these choices to build subsequent expressions.

    • Internal Scope: Refer to an agent's own choices as simple variables.
    • External Scope: Use dot notation (agent.choice) to reference another agent's choices.
    • Bracket Notation: You can also use agent[choice] or agent[expression] as an alternative to dot notation.
    alice: chooses(x in X, wpp=1)
    alice: chooses(y in Y, wpp=f(x, y))
    # Reference another agent
    bob.x + alice.y
    # Equivalent bracket notation
    alice[x] + alice[y]
  6. Query an agent's mental model using square brackets

    main

    You can evaluate an expression from a specific agent's perspective by wrapping the expression in square brackets following the agent's name. This effectively queries their internal mental model.

    Var[alice[abs(x) * 2]]
    alice[bob.y == 7]
  7. Anatomy of a memo model

    main

    A memo model is a decorated Python function that defines agents, their choices, and beliefs. The structure follows this pattern:

    @memo
    def model[x: X, y: Y](a, b, c=3.14):
        ''' Optional docstring '''
        alice: ... # Agent statements
        bob: ...
        return ... # Expression to compute

    Components:

    • @memo: The decorator that turns the function into a memo model.
    • [x: X, y: Y]: Axes of the array to compute. Each axis variable iterates over a domain.
    • (a, b, c=3.14): Scalar free parameters (with optional defaults). You can also pass arrays using the ellipsis annotation: a: ....
    • Agent Statements: A sequence of statements (like chooses, thinks, observes) that define agent behavior.
    • return ...: An expression computed for each cell in the returned array. You can use multiple return statements for multiple return values.
    @memo
    def model[x: X, y: Y](a, b, c=3.14):
        ''' This is a model of ____! '''
        alice: 
        bob: 
        return
  8. Understand output array dimension reduction

    main

    memo minimizes redundant computation by setting axes to size 1 if the model output does not depend on that specific input axis. This allows for efficient broadcasting.

    Examples:

    • If f(a, b) returns a, shape is (len(a), 1).
    • If f(a, b) returns b, shape is (1, len(b)).
    • If f(a, b) returns a + b, shape is (len(a), len(b)).
    • If f(a, b) returns a constant, shape is (1, 1).
    X = np.arange(10)
    
    @memo
    def f[a: X, b: X]():
        return a
    f().shape  # (10, 1)
    
    @memo
    def f[a: X, b: X]():
        return b
    f().shape  # (1, 10)
    
    @memo
    def f[a: X, b: X]():
        return a + b
    f().shape  # (10, 10)
    
    @memo
    def f[a: X, b: X]():
        return 999
    f().shape  # (1, 1)
  9. Perform automatic differentiation on memo models

    main

    Since memo models are differentiable via JAX, you can use jax.value_and_grad to compute gradients with respect to the model's parameters. This is useful for fitting models to data using gradient descent.

    @memo
    def f[…](a, b): …
    
    # Returns (output_value, gradient_wrt_a_and_b)
    jax.value_and_grad(f)(a, b)
  10. Install memo

    main

    To install memo, follow these steps in order:

    1. Ensure Python 3.12 or higher is installed. Verify this by running python --version.
    2. Create and activate a virtual environment. This prevents dependency conflicts.
      python -m venv venv
      . venv/bin/activate
    3. Install JAX. memo uses JAX for fast, differentiable, GPU-enabled code.
      • For CPU: pip install jax
      • For GPU: Consult the JAX website for specific instructions.
    4. Install memo-lang.
      pip install memo-lang

    CRITICAL: Install memo-lang, not memo. The memo package on PyPI is an unrelated project.

    To verify the installation, run the following in a Python shell:

    from memo import memo
    python -m venv venv
    . venv/bin/activate
    pip install jax
    pip install memo-lang
  11. Disable editor type-checking for memo models

    main

    Because memo uses syntax that regular Python type-checkers (like those in VS Code or Ruff) do not recognize, you may see red underlines. To suppress these errors, use @no_type_check from typing and the # ruff: noqa comment.

    # ruff: noqa
    from typing import no_type_check
    
    @no_type_check
    @memo
    def model[u: U, r: R]():
       # ...
  12. Call one memo from another

    main

    Memos can reference other memos. When calling a memo, ensure all required parameters are passed.

    • Forwarding parameters: Use ... as a shorthand to pass all parameters from the caller to the called memo.
    • Accessing specific return values: If a memo returns multiple values, use index notation g[index] to select a specific one before passing arguments.
    @memo
    def f[x: X](a, b, c): …
    
    @memo
    def g(a, b, c):
        # Using shorthand to forward (a, b, c)
        alice: chooses(x in X, wpp=f[x](...))
    
    # Accessing index 0 of a multi-return memo
    g[0][x, y](3.14)