Lippershey-Base / README.md
imbue2025's picture
Update README.md
2d489db verified
|
Raw
History Blame Contribute Delete
12.2 kB
metadata
license: openrail
tags:
  - TSP
  - VRP
  - 3D
  - 3D_Packing
  - MILP
  - base_model

Lippershey-Base is a pretrained, self-supervised foundation model designed to extract highly generalizable topological and geometric representations from Mixed-Integer Linear Programs (MILPs) and discrete planning structures.

By representing arbitrary optimization problems as variable-constraint bipartite graphs, Lippershey-Base learns the underlying mathematical language of optimization, providing a powerful pretrained backbone for downstream reinforcement learning (PPO), exact solver acceleration, and primal heuristics.


1. Mathematical Background & Problem Formulation

Traditional deep learning models for combinatorial optimization (CO) are usually constrained to single, specific problems (e.g., TSP-only or Knapsack-only architectures). Lippershey-Base achieves generality by operating on the standard mathematical formulation of Mixed-Integer Linear Programming (MILP), which can represent almost all discrete planning and combinatorial optimization problems.

Bipartite Graph Representation

Any MILP can be mathematically written as: min⁑cTx\min \quad c^T x s.t.Ax≀b\text{s.t.} \quad A x \le b li≀xi≀ui,βˆ€i∈{1,…,n}l_i \le x_i \le u_i, \quad \forall i \in \{1, \dots, n\} xi∈Zβˆ€i∈I,xi∈Rβˆ€i∈Cx_i \in \mathbb{Z} \quad \forall i \in \mathcal{I}, \quad x_i \in \mathbb{R} \quad \forall i \in \mathcal{C}

We map this system into a sparse bipartite graph $\mathcal{G} = (\mathcal{V}_v, \mathcal{V}_c, \mathcal{E})$:

  • Variable Nodes ($\mathcal{V}_v$): Representing the decision variables $x_i$. Node features $X_v \in \mathbb{R}^{n \times 5}$ encode: $$[c_i, l_i, u_i, \text{is_integer}_i, \text{is_masked}_i]$$
  • Constraint Nodes ($\mathcal{V}_c$): Representing the constraints $j$. Node features $X_c \in \mathbb{R}^{m \times 5}$ encode: $$[b_j, 0, 0, 0, 0] \quad (\text{padded to match dimension})$$
  • Directed Bipartite Edges ($\mathcal{E}$): Connects constraint node $j$ to variable node $i$ if $A_{ji} \neq 0$. The edge attribute $e_{ji} \in \mathbb{R}^{1}$ is the matrix coefficient $A_{ji}$.

2. Model Architecture: Bipartite Graph Transformer

Lippershey-Base utilizes an alternating message-passing Graph Transformer block to model the algebraic and primal-dual structures of MILPs.

Alternating Bipartite Attention (ABA)

Unlike standard homogeneous GNNs, each layer in Lippershey-Base consists of two asymmetrical attention steps:

  1. Constraint-to-Variable (C2V) Update: Variables aggregate information from the constraints they participate in. This allows variables to learn their resource bottlenecks. $$h_{v_i}^{(l+1)} = \text{MultiHeadAttn}\left( \mathbf{Q}=v_i^{(l)}, \mathbf{K}=c_j^{(l)}, \mathbf{V}=c_j^{(l)}, \text{edge_attr}=e_{ji} \right)$$ $$v_i^{(l+1)} = \text{LayerNorm}\left( v_i^{(l)} + \text{GELU}(h_{v_i}^{(l+1)}) \right)$$ $$v_i^{(l+1)} = v_i^{(l+1)} + \text{FFN}(v_i^{(l+1)})$$

  2. Variable-to-Constraint (V2C) Update: Constraints aggregate information from their constituent variables to update constraint embeddings. $$h_{c_j}^{(l+1)} = \text{MultiHeadAttn}\left( \mathbf{Q}=c_j^{(l)}, \mathbf{K}=v_i^{(l+1)}, \mathbf{V}=v_i^{(l+1)}, \text{edge_attr}=e_{ji} \right)$$ $$c_j^{(l+1)} = \text{LayerNorm}\left( c_j^{(l)} + \text{GELU}(h_{c_j}^{(l+1)}) \right)$$ $$c_j^{(l+1)} = c_j^{(l+1)} + \text{FFN}(c_j^{(l+1)})$$


3. The Self-Supervised Pre-Training Paradigm

Lippershey-Base is pre-trained using Masked Objective Reconstruction (MOR) on structurally valid MILP spaces.

Why MOR forces Geometric Understanding

In a MILP, the objective vector $c$ defines the direction of optimization, while $Ax \le b$ defines the feasible polytope. By masking $c_i$ (setting it to $0$ and marking is_masked=1), the model cannot see the optimization direction of variable $i$. To reconstruct $c_i$, the model must analyze how variable $i$ is coupled with other variables through the constraint boundaries. It must implicitly understand the concept of slack, feasibility boundaries, and variable trade-offs, which are the fundamental pillars of mathematical optimization.

Guaranteed Feasibility via Constraint Planting

Pre-training on random infeasible linear systems leads to chaotic representations. Lippershey-Base's training data is synthesized using Constraint Planting:

  1. Generate a target binary solution $x^* \in {0, 1}^n$.
  2. Generate a sparse constraint matrix $A \in \mathbb{R}^{m \times n}$.
  3. Set the constraint bounds $b = Ax^* + s$, where $s \in \mathbb{R}^m_{\ge 0}$ is a positive slack vector. This guarantees that $x^*$ is always a feasible solution.
  4. Generate $c$ correlated with $x^*$ to ensure the planted solution behaves like an optimal or near-optimal point.

4. Downstream Fine-Tuning Scenarios (Extensive Code Guides)

Once pre-trained, the encoder parameters in LippersheyBase can be transferred to various downstream tasks. Below are three detailed, production-grade integration guides.

Scenario A: Deep Reinforcement Learning (PPO) for Sequential Planning

In routing (TSP/VRP) or scheduling (Job-Shop), decisions are made sequentially. Here, we fine-tune Lippershey-Base as the policy network backbone.

import torch
import torch.nn as nn
from torch_geometric.data import Batch
from modeling_lippershey import LippersheyBase

class LippersheyPPOAgent(nn.Module):
    """
    Production-grade PPO Actor-Critic using Lippershey-Base as a pre-trained backbone.
    Enforces hard constraints natively via Action Masking.
    """
    def __init__(self, pretrained_safetensors_path, hidden_dim=256):
        super().__init__()
        # Load the pre-trained mathematical encoder
        self.encoder = LippersheyBase.from_pretrained_safetensors(
            pretrained_safetensors_path,
            node_in_dim=5,
            hidden_dim=hidden_dim
        )
        
        # Freeze backbone parameters early in fine-tuning to preserve features
        # can be unfrozen later for end-to-end tuning
        for param in self.encoder.parameters():
            param.requires_grad = False
            
        # Actor Head (Policy): Outputs categorical logit for each variable decision
        self.actor = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.GELU(),
            nn.Linear(hidden_dim // 2, 1)
        )
        
        # Critic Head (Value): Predicts expected future objective value
        self.critic_pooling = nn.Linear(hidden_dim, 1)
        self.critic = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.GELU(),
            nn.Linear(hidden_dim // 2, 1)
        )

    def forward(self, batch: Batch, action_mask=None):
        """
        Args:
            batch: PyG Batch containing bipartite representations of the current step.
            action_mask: Boolean tensor [num_variables_in_batch] marking legal decisions.
        """
        # 1. Extract latents from pre-trained backbone
        v = self.encoder.var_proj(batch.x_var)
        c = self.encoder.con_proj(batch.x_con)
        
        for layer in self.encoder.layers:
            v, c = layer(v, c, batch.edge_index_c2v, batch.edge_attr)
            
        # v shape: [total_num_variables_in_batch, hidden_dim]
        
        # 2. Compute Policy Logits (Actor)
        logits = self.actor(v).squeeze(-1) # Shape: [total_num_variables]
        
        # Apply strict constraint masks (set invalid variable choices to -inf)
        if action_mask is not None:
            logits = logits.masked_fill(~action_mask, float('-inf'))
            
        # 3. Compute State Value (Critic)
        # Pool variable embeddings to represent the entire graph state
        # batch.batch is PyG's indicator tensor mapping nodes to their graph index
        graph_repr = torch.zeros(batch.num_graphs, v.size(-1), device=v.device)
        graph_repr.index_add_(0, batch.x_var_batch, v) # Sum pooling
        
        state_value = self.critic(graph_repr) # Shape: [num_graphs, 1]
        
        return logits, state_value

    def get_action_and_value(self, batch, action_mask=None, action=None):
        logits, state_value = self.forward(batch, action_mask)
        
        # Create a categorical distribution over variables
        dist = torch.distributions.Categorical(logits=logits)
        if action is None:
            action = dist.sample()
            
        return action, dist.log_prob(action), dist.entropy(), state_value

Scenario B: Accelerated Branch-and-Bound (Variable Selection)

In exact MILP solvers (such as SCIP), the most computationally expensive step is Variable Branching. We can fine-tune Lippershey-Base to imitate "Strong Branching" (expert demonstrations), turning variable selection into a fast GPU-accelerated inference task.

class LippersheyBranchingModel(nn.Module):
    """
    Finetuned classifier to select fractional variables for branching in B&B trees.
    """
    def __init__(self, pretrained_safetensors_path, hidden_dim=256):
        super().__init__()
        self.encoder = LippersheyBase.from_pretrained_safetensors(
            pretrained_safetensors_path, node_in_dim=5, hidden_dim=hidden_dim
        )
        self.branch_head = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.GELU(),
            nn.Linear(hidden_dim // 2, 1) # Probability score of being the branching variable
        )

    def forward(self, batch, fractional_mask):
        """
        fractional_mask: Boolean mask indicating which variables are currently fractional (LP relaxation).
        """
        v = self.encoder.var_proj(batch.x_var)
        c = self.encoder.con_proj(batch.x_con)
        for layer in self.encoder.layers:
            v, c = layer(v, c, batch.edge_index_c2v, batch.edge_attr)
            
        scores = self.branch_head(v).squeeze(-1)
        
        # Mask out variables that are already integer
        scores = scores.masked_fill(~fractional_mask, float('-inf'))
        return scores # Apply Softmax to select the branching target

Scenario C: Primal Heuristic Warm-Starting (Sub-MILP Generation)

For massive industrial scheduling/packing problems, solvers like Gurobi can take hours to find the first feasible solution. We can use Lippershey-Base to predict the probability of binary variables being 1 in the optimal solution, freeze high-confidence variables, and let the solver solve the remaining sub-problem in seconds.

@torch.no_grad()
def generate_warm_start_bounds(pretrained_model, batch, threshold=0.95):
    """
    Predicts optimal variable values and returns indices to freeze for Gurobi.
    """
    pretrained_model.eval()
    preds = pretrained_model(
        batch.x_var, batch.x_con, batch.edge_index_c2v, batch.edge_attr
    )
    # Convert logits/reconstructions to probability space via sigmoid
    probabilities = torch.sigmoid(preds).cpu().numpy()
    
    freeze_to_one = np.where(probabilities > threshold)[0]
    freeze_to_zero = np.where(probabilities < (1 - threshold))[0]
    
    return freeze_to_one, freeze_to_zero

5. Performance Metrics & Pre-Training Log Analysis

When pre-training Lippershey-Base, use the following guidelines to evaluate the quality of the learned representation:

  • Validation Loss (MSE): Measures how accurately the model reconstructs the continuous coefficients. A healthy convergence on structured MILPs should see val/loss drop below 0.15 (L1 average absolute error $\approx 0.38$ on $[-1, 1]$ scale).
  • Grad Norm (L2): Should stabilize between 0.3 and 1.0. Constant clipping to 1.0 indicates learning rate is too high; an decaying gradient norm towards 0 without loss convergence indicates gradient vanishing.
  • Prediction Mean Convergence: Check train/pred_mean against train/true_mean. They should overlap near the $0$ axis, indicating the model's global prediction scale matches the mathematical distribution of your planning domain.