| # Implementation Plan: Knowledge Editing Baselines |
|
|
| ## Baseline Lineup |
|
|
| | Method | Category | Role | |
| |--------|----------|------| |
| | **LoRA v5** | Your method | Optimization-based, handles distributional visual subjects + conditional suppression | |
| | **OVERTONE (LoRA v3)** | Text KME baseline (adapted) | Token-level adaptive targets, shows text-only KME on VLM | |
| | **MEMIT** | Classic locate-then-edit | Shows point-subject methods struggle with distributional setting | |
| | **DualEdit** | VLM-specific editor (COLM 2025) | Best existing VLM editor, gating mechanism provides visual conditioning | |
|
|
| --- |
|
|
| ## Part 1: MEMIT Baseline |
|
|
| **Effort:** Small (1-2 days) |
| **Status:** EasyEdit has MEMIT at `EasyEdit/easyeditor/models/memit/`. Just needs wiring up. |
|
|
| ### Files to Create |
|
|
| #### 1. `experiment/knowledge_editing/hparams/memit.yaml` |
| |
| ```yaml |
| alg_name: "MEMIT" |
| model_name: "llava-hf/llava-1.5-7b-hf" |
| device: 0 |
| stats_dir: "./data/stats" |
|
|
| # Target mid-range layers for LLaMA backbone |
| layers: [4, 5, 6, 7, 8] |
| layer_selection: "all" |
| fact_token: "subject_last" |
| |
| v_num_grad_steps: 25 |
| v_lr: 5e-1 |
| v_loss_layer: 31 |
| v_weight_decay: 1e-3 |
| clamp_norm_factor: 4 |
| kl_factor: 0.0625 |
| mom2_adjustment: true |
| mom2_update_weight: 15000 |
| |
| # Module templates for LLaVA-1.5's LLaMA backbone |
| # MEMIT operates on model.language_model (a LlamaForCausalLM) |
| # These paths are relative to the language_model, not the full LlavaForConditionalGeneration |
| rewrite_module_tmp: "model.layers.{}.mlp.down_proj" |
| layer_module_tmp: "model.layers.{}" |
| mlp_module_tmp: "model.layers.{}.mlp" |
| attn_module_tmp: "model.layers.{}.self_attn" |
| ln_f_module: "model.norm" |
| lm_head_module: "lm_head" |
|
|
| mom2_dataset: "wikipedia" |
| mom2_n_samples: 100000 |
| mom2_dtype: "float32" |
| model_parallel: false |
| ``` |
| |
| #### 2. `experiment/knowledge_editing/memit_wrapper.py` |
| |
| Adapts text-only MEMIT for the multimodal pipeline: |
| |
| - **Extract `model.language_model`** from `LlavaForConditionalGeneration` — MEMIT operates on CausalLM directly |
| - **Convert edit requests** from image-based format to MEMIT's text-only format: |
| ```python |
| # Our format (from build_requests): |
| {"prompt": "Describe this image.", "target": "A bathroom with a sink and mirror.", "image": <PIL>} |
|
|
| # MEMIT format: |
| {"prompt": "In the bathroom, the {} is", "subject": "toilet", "target_new": {"str": "not visible"}} |
| ``` |
| - **Call `apply_memit_to_model()`** from EasyEdit on the language model |
| - **Return the full LlavaForConditionalGeneration** with edited language model |
| |
| Function signature must match `run_baselines.py` pattern: |
| ```python |
| def apply_memit_to_multimodal_model(model, processor, requests, hparams, |
| copy=False, return_orig_weights=True, |
| keep_original_weight=False): |
| ``` |
|
|
| **Key limitation:** MEMIT is text-only, cannot condition on images. It does an unconditional edit |
| suppressing the bathroom→toilet association globally. Expected to hurt locality (suppress toilet |
| even when present). This is the point — demonstrates why vision-conditioned methods are needed. |
|
|
| **Pitfalls:** |
| - Module path mismatch: paths must be relative to `model.language_model`, not full model |
| - Covariance computation (`mom2`): runs model on Wikipedia text, needs CausalLM not full VLM |
| - Tokenizer: use `processor.tokenizer` or `processor._processor.tokenizer` |
| - `target_new` vs `target`: wrapper must translate field names |
|
|
| ### Files to Modify |
|
|
| #### 3. `experiment/knowledge_editing/run_baselines.py` |
|
|
| Add `memit` to three places: |
|
|
| **`load_hparams()` (~line 181):** |
| ```python |
| elif method == "memit": |
| from easyeditor.models.memit import MEMITHyperParams |
| return MEMITHyperParams.from_hparams(yaml_path) |
| ``` |
| |
| **`get_apply_algo()` (~line 197):** |
| ```python |
| elif method == "memit": |
| from experiment.knowledge_editing.memit_wrapper import apply_memit_to_multimodal_model |
| return apply_memit_to_multimodal_model |
| ``` |
| |
| **CLI args (~line 459):** Add `"memit"` to choices list. |
| |
| #### 4. `experiment/evaluation/validate.py` |
| |
| MEMIT edits weights directly (like LoRA merged), so treat `--model_type memit` same as `merged`. |
| Minimal change — just document it or add alias. |
| |
| --- |
| |
| ## Part 2: DualEdit Baseline |
| |
| **Effort:** Medium (3-5 days) |
| **Paper:** "DualEdit: Dual Editing for Knowledge Updating in Vision-Language Models" (COLM 2025) |
| **Code:** https://github.com/zhiyiscs/DualEdit |
|
|
| ### How DualEdit Works |
|
|
| 1. Inserts **two learnable cross-attention adapters** at modality-specific key layers: |
| - Layer 16: text adapter (modifies textual representations) |
| - Layer 19: vision adapter (modifies visual representations) |
| 2. **Gating mechanism**: cosine similarity of last-token representations decides whether to apply edit |
| - `Sim = cos(h^e, h^i)` — edit sample vs input sample |
| - If `Sim > τ`: route through adapter (apply edit) |
| - Else: use original model (preserve behavior) |
| 3. **Loss** = `L_rel + L_gen + L_loc` (reliability + generality + locality) |
|
|
| ### Why It Fits Our Problem |
|
|
| - Gating = natural implementation of our `g(image)` visual grounding function |
| - Dual-modality editing matches our insight (prior in LLM, triggered by visual input) |
| - Near-perfect locality (99.89% M-Loc in paper) via gating |
|
|
| ### Key Adaptation: Per-Sample → Distributional |
|
|
| DualEdit is designed for per-sample edits. We adapt to **batched training**: |
| - Train ONE pair of adapters across ALL bathroom-no-toilet images |
| - Store mean last-token representation as gate prototype |
| - At inference, gate fires for inputs similar to prototype |
|
|
| ### Files to Create |
|
|
| ``` |
| experiment/knowledge_editing/dualedit/ |
| __init__.py |
| adapter.py |
| gating.py |
| dualedit_main.py |
| dualedit_hparams.py |
| ``` |
|
|
| #### 1. `dualedit/adapter.py` — Dual Adapter Modules |
|
|
| Cross-attention adapter (from paper Eq. 5, Section 3.2): |
|
|
| ```python |
| class DualEditAdapter(nn.Module): |
| """Learnable adapter using cross-attention. |
| |
| Inserted at a specific layer. Uses the edit signal (h_e from the edit sample) |
| as Key/Value, and the current hidden state as Query. |
| |
| Separate weight matrices W_1, W_2, W_3 for text vs vision modalities. |
| """ |
| def __init__(self, hidden_size, d_a=64): |
| # W_1^{t/v}: Query projection (hidden_size -> d_a) |
| # W_2^{t/v}: Key projection (hidden_size -> d_a) |
| # W_3^{t/v}: Value projection (hidden_size -> hidden_size) |
| # Text and vision get separate weight matrices |
| ... |
| |
| def forward(self, h_k, h_e_k): |
| # h_k: current layer's hidden state for text or vision tokens |
| # h_e_k: edit signal from edit sample at this layer |
| # Returns: edited hidden state |
| # h_hat = Softmax(h_k @ W_1 . (h_e_k @ W_2)^T) . h_e_k @ W_3 |
| ... |
| ``` |
|
|
| Two instances needed: one at layer 16 (text), one at layer 19 (vision). |
|
|
| #### 2. `dualedit/gating.py` — Gating Mechanism |
|
|
| ```python |
| class DualEditGate: |
| """Cosine similarity gate on last-token representations. |
| |
| At training time: stores edit sample representations. |
| At inference time: compares input repr to stored prototypes. |
| If similarity > threshold: activate adapters. |
| Otherwise: bypass (use original model). |
| """ |
| def __init__(self, threshold=0.6): |
| self.threshold = threshold |
| self.edit_prototypes = [] # mean last-token reprs from edit samples |
| |
| def register_edit_repr(self, h_last_token): |
| """Store representation during training.""" |
| ... |
| |
| def compute_prototype(self): |
| """Average all stored reprs into a single gate key.""" |
| ... |
| |
| def should_edit(self, h_input_last_token) -> bool: |
| """Check if input should be routed through adapters.""" |
| sim = F.cosine_similarity(h_input_last_token, self.prototype, dim=-1) |
| return sim > self.threshold |
| ``` |
|
|
| Threshold: τ=0.6 for LLaVA-1.5 (from paper Appendix D). |
|
|
| #### 3. `dualedit/dualedit_main.py` — Main Apply Function + Training |
| |
| ```python |
| def apply_dualedit_to_multimodal_model(model, processor, requests, hparams, **kwargs): |
| """ |
| 1. Insert adapter modules at layers 16 and 19 |
| 2. Freeze all params except adapters |
| 3. Train adapters on edit requests: |
| - L_rel: -log P(target | edit_image, prompt) on bathroom_no_toilet |
| - L_gen: same on rephrased prompts + different bathroom_no_toilet images |
| - L_loc: KL preservation on bathroom_with_toilet + unrelated |
| 4. Compute and store gate prototype (mean last-token repr) |
| 5. Return edited model with adapters + gating state |
| """ |
| ``` |
| |
| Training details (from paper Appendix D): |
| - Learning rate: 1e-4 |
| - Batch size: 4 |
| - Max iterations: 50,000 (but we can use fewer for our distributional task) |
| - Checkpoint every 1000 iterations, select best by loss |
| |
| #### 4. `dualedit/dualedit_hparams.py` — Hyperparameters |
| |
| ```python |
| @dataclass |
| class DualEditHyperParams: |
| model_name: str = "llava-hf/llava-1.5-7b-hf" |
| text_adapter_layer: int = 16 |
| vision_adapter_layer: int = 19 |
| adapter_dim: int = 64 |
| gating_threshold: float = 0.6 |
| edit_lr: float = 1e-4 |
| n_iterations: int = 5000 |
| batch_size: int = 4 |
| reliability_weight: float = 1.0 |
| generality_weight: float = 0.5 |
| locality_weight: float = 1.0 |
| checkpoint_every: int = 500 |
| ``` |
| |
| #### 5. `experiment/knowledge_editing/hparams/dualedit.yaml` |
| |
| YAML version of the above hparams. |
| |
| ### Files to Modify |
| |
| #### 6. `experiment/knowledge_editing/run_baselines.py` |
| |
| Add `dualedit` to `load_hparams()`, `get_apply_algo()`, and CLI args. |
| |
| #### 7. `experiment/evaluation/validate.py` |
| |
| Add `model_type="dualedit"` support: |
| 1. Load base LlavaForConditionalGeneration |
| 2. Insert adapter modules at layers 16 and 19 |
| 3. Load adapter weights + gating keys from `.pt` file |
| 4. Register forward hooks for gating |
| |
| Follow existing GRACE/WISE save/restore pattern in `_extract_adapter_states()`. |
| |
| ### Pitfalls |
| |
| 1. **Layer numbering**: Verify 0-indexed layers 16 and 19 match paper's intent for LLaVA-1.5 |
| 2. **Gating prototype noise**: Mean of diverse bathroom images may be noisy — may need multiple prototypes or tuned threshold |
| 3. **Memory**: Training requires backprop through frozen model. ~16-20GB VRAM with batch_size=4 and fp16. May need gradient checkpointing. |
| 4. **Multimodal loss**: Locality loss must handle both text-only (unrelated) and image+text (bathroom-with-toilet) inputs |
| |
| --- |
| |
| ## Implementation Order |
| |
| ``` |
| Week 1: |
| Day 1-2: MEMIT |
| ├── Create memit.yaml hparams |
| ├── Create memit_wrapper.py |
| ├── Wire into run_baselines.py |
| └── Test: run MEMIT on small edit set, verify weights change, run eval |
| |
| Day 3-4: DualEdit (architecture) |
| ├── Clone DualEdit repo, study source code |
| ├── Create dualedit/ directory structure |
| ├── Implement adapter.py (cross-attention modules) |
| └── Implement gating.py (cosine similarity gate) |
| |
| Day 5: DualEdit (training + integration) |
| ├── Implement dualedit_main.py (training loop) |
| ├── Create hparams, wire into run_baselines.py |
| └── Add save/restore + validate.py support |
| |
| Week 2: |
| Day 1: Testing & debugging |
| ├── Run DualEdit training on bathroom dataset |
| ├── Debug gating behavior (check threshold sensitivity) |
| └── Verify locality on bathroom-with-toilet images |
| |
| Day 2: Joint evaluation |
| ├── Run all methods through validate.py |
| ├── Compare: LoRA v5, OVERTONE, MEMIT, DualEdit |
| └── Generate comparison table |
| ``` |
| |
| --- |
| |
| ## Reference: Existing Infrastructure |
| |
| | Component | Location | Notes | |
| |-----------|----------|-------| |
| | LoRA v5 (your method) | `experiment/training/finetune_lora_v5.py` | Already implemented | |
| | OVERTONE (LoRA v3) | `experiment/training/finetune_lora_v3.py` | Already implemented | |
| | EasyEdit baselines | `experiment/knowledge_editing/run_baselines.py` | WISE, GRACE, LoRA, IKE | |
| | EasyEdit MEMIT | `EasyEdit/easyeditor/models/memit/memit_main.py` | Needs wrapper | |
| | Evaluation | `experiment/evaluation/validate.py` | Supports lora, merged, delta_w, grace, wise | |
| | Edit set builder | `experiment/knowledge_editing/build_edit_set.py` | Generates edit_set.json | |
| | Dataset | `experiment/data/datasets.py` | CC3M bathroom categories | |
| | LLaVA compat | `experiment/knowledge_editing/llava15_compat.py` | Processor wrapper | |
| | Config | `experiment/config/train_config.py` | Comprehensive config system | |
| | DualEdit paper code | https://github.com/zhiyiscs/DualEdit | To be cloned | |
| |
| ## Reference: Key Papers |
| |
| - **MEMIT**: Meng et al. (2023). "Mass-Editing Memory in a Transformer." ICLR. |
| - **DualEdit**: Shi et al. (2025). "DualEdit: Dual Editing for Knowledge Updating in Vision-Language Models." COLM. |
| - **VisEdit**: Chen et al. (2024). "Attribution Analysis Meets Model Editing." AAAI. (Not implementing, but VisEdit/ code available for reference) |
| - **OVERTONE**: Liu et al. (2025). "Mitigating Heterogeneous Token Overfitting in LLM Knowledge Editing." ICML. |
| - **AlphaEdit**: Fang et al. (2025). "AlphaEdit: Null-Space Constrained Knowledge Editing." ICLR. (Optional post-hoc enhancement) |
| |