Spaces:
Paused
Paused
File size: 7,792 Bytes
0127c21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | """
Single-image VQA specialist model.
Phase 5 (P0). Satisfies PS requirement:
- "Single-image visual question answering shall be mandatory."
- "At least one visual or vision-language component must be fine-tuned
or otherwise adapted using BigEarthNet.txt or any open source
training data." (this is that component)
MODEL CHOICE β Salesforce/blip-vqa-base
- Verified supports: general open-domain VQA (image + free-text
question -> free-text answer). It is NOT remote-sensing-native out of
the box β that's why the PS-mandated adaptation step in
backend/adaptation/ exists (see adaptation/README.md).
- Modality: input = 1 RGB image + 1 text question. Output = short text
answer (BLIP-VQA is trained as generative, not classification-head).
- Integration complexity: LOW. Pure `transformers`, no custom CUDA ops,
~385M params β runs on CPU (slow, seconds/query) or any small GPU.
This is why it was chosen over a 7B-parameter RS-VLM (e.g. GeoChat):
those need real GPU infra we cannot assume the team has in 4 days.
- License: BSD-3-Clause (Salesforce/BLIP). Free for hackathon/research
use. Verify before any commercial deployment.
- We are NOT claiming this is a validated remote-sensing benchmark
model. Confidence returned below is a decoding-confidence proxy from
the model's own generation, not a calibrated accuracy figure β see
VQAResult.confidence_note.
ADAPTATION
If a LoRA adapter trained by backend/adaptation/train_lora_vqa.py exists
at VQA_LORA_ADAPTER_PATH, it is loaded on top of the base model. If not,
the base (non-adapted) model is used and `adapted=False` is reported
honestly in every response β never silently claim adaptation happened.
"""
from __future__ import annotations
import os
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from preprocessing.raster_to_rgb import load_as_rgb
BASE_MODEL_ID = "Salesforce/blip-vqa-base"
DEFAULT_ADAPTER_DIR = Path(__file__).resolve().parent / "checkpoints" / "vqa_lora_bigearthnet"
ADAPTER_PATH = Path(os.environ.get("VQA_LORA_ADAPTER_PATH", DEFAULT_ADAPTER_DIR))
# Buckets mirror master brief Β§10: HIGH / MEDIUM / INSUFFICIENT confidence.
# Thresholds are a prototype policy, not a scientifically validated cutoff
# β state that explicitly wherever this is surfaced.
_HIGH_THRESHOLD = 0.75
_MEDIUM_THRESHOLD = 0.40
@dataclass
class VQAResult:
answer: str
confidence: float
confidence_level: str # "high" | "medium" | "insufficient"
confidence_note: str
model_id: str
adapted: bool
def _load_adapter_info(adapter_path: Path) -> Optional[dict]:
"""Phase 14: read the honest per-epoch/best-checkpoint metrics that
train_lora_vqa.py wrote alongside the adapter (adapter_info.json).
Missing/unreadable file is not an error β older adapters trained
before this existed simply won't have one."""
info_path = adapter_path / "adapter_info.json"
if not info_path.exists():
return None
try:
import json
return json.loads(info_path.read_text())
except Exception: # noqa: BLE001
return None
class _VQAModel:
"""Lazy-loaded singleton so the model loads once per process, not per request."""
def __init__(self) -> None:
self._lock = threading.Lock()
self._processor = None
self._model = None
self._adapted = False
self._adapter_info: Optional[dict] = None
self._device = "cpu"
def _ensure_loaded(self) -> None:
if self._model is not None:
return
with self._lock:
if self._model is not None: # re-check inside the lock
return
import torch
from transformers import BlipForQuestionAnswering, BlipProcessor
self._device = "cuda" if torch.cuda.is_available() else "cpu"
self._processor = BlipProcessor.from_pretrained(BASE_MODEL_ID)
model = BlipForQuestionAnswering.from_pretrained(BASE_MODEL_ID)
if ADAPTER_PATH.exists():
try:
from peft import PeftModel
model = PeftModel.from_pretrained(model, str(ADAPTER_PATH))
self._adapted = True
self._adapter_info = _load_adapter_info(ADAPTER_PATH)
except Exception as exc: # noqa: BLE001
# Fail open to the base model β a broken adapter should
# never take the whole VQA endpoint down mid-hackathon.
print(f"[vqa] Failed to load LoRA adapter at {ADAPTER_PATH}: {exc}. "
f"Falling back to base {BASE_MODEL_ID}.")
self._adapted = False
self._model = model.to(self._device).eval()
def answer(self, image_path: Path, question: str) -> VQAResult:
self._ensure_loaded()
import torch
image = load_as_rgb(image_path)
inputs = self._processor(image, question, return_tensors="pt").to(self._device)
with torch.no_grad():
out = self._model.generate(
**inputs,
max_new_tokens=30,
output_scores=True,
return_dict_in_generate=True,
)
answer_text = self._processor.decode(out.sequences[0], skip_special_tokens=True).strip()
confidence = _decode_confidence(out)
level, note = _bucket_confidence(confidence)
return VQAResult(
answer=answer_text or "(model returned an empty answer)",
confidence=confidence,
confidence_level=level,
confidence_note=note,
model_id=f"{BASE_MODEL_ID}" + (
f" + BigEarthNet LoRA adapter (best val eval_loss={self._adapter_info['best_eval_loss']:.4f}, "
f"epoch {self._adapter_info.get('best_epoch')})"
if self._adapted and self._adapter_info and self._adapter_info.get("best_eval_loss") is not None
else " + BigEarthNet LoRA adapter" if self._adapted else " (base, not yet adapted)"
),
adapted=self._adapted,
)
def _decode_confidence(generate_output) -> float:
"""Mean per-token generation probability, normalized to [0,1].
This is a decoding-confidence proxy (how sure the model was about the
tokens it emitted), NOT a calibrated task-accuracy estimate. Do not
present it as validated benchmark performance.
"""
import torch
scores = getattr(generate_output, "scores", None)
if not scores:
return 0.5 # no score info available β neutral default, not a real signal
token_probs = []
for step_logits in scores:
probs = torch.softmax(step_logits[0], dim=-1)
token_probs.append(probs.max().item())
if not token_probs:
return 0.5
return float(sum(token_probs) / len(token_probs))
def _bucket_confidence(confidence: float) -> tuple[str, str]:
if confidence >= _HIGH_THRESHOLD:
return "high", "Model was consistently confident across generated tokens."
if confidence >= _MEDIUM_THRESHOLD:
return "medium", "Model showed moderate token-level uncertainty β treat as indicative, not definitive."
return "insufficient", "Model showed high token-level uncertainty β reliable analysis cannot be assumed from this alone."
_singleton: Optional[_VQAModel] = None
_singleton_lock = threading.Lock()
def get_vqa_model() -> _VQAModel:
global _singleton
if _singleton is None:
with _singleton_lock:
if _singleton is None:
_singleton = _VQAModel()
return _singleton
def run_vqa(image_path: Path, question: str) -> VQAResult:
return get_vqa_model().answer(image_path, question)
|