File size: 6,620 Bytes
cf29823 f2487c7 cf29823 | 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 | """Pretrained vision-model embeddings for small-seal glyph retrieval."""
from __future__ import annotations
from pathlib import Path
from typing import Iterable
import numpy as np
import torch
import torch.nn as nn
from PIL import Image
from transformers import AutoImageProcessor, AutoModel
from torchvision.models import resnet50
DEFAULT_VISUAL_MODEL = "facebook/dinov2-small"
def load_visual_model(model_id: str, device: str = "cpu"):
"""Load a Hugging Face vision backbone in inference-only mode."""
processor = AutoImageProcessor.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id)
model.to(device)
model.eval()
return processor, model
def embed_images(
image_paths: Iterable[Path], processor, model, batch_size: int = 16, device: str = "cpu"
) -> np.ndarray:
"""Create L2-normalized CLS embeddings for a sequence of glyph images."""
paths = list(image_paths)
chunks: list[np.ndarray] = []
with torch.inference_mode():
for start in range(0, len(paths), batch_size):
batch_paths = paths[start : start + batch_size]
images = []
for path in batch_paths:
with Image.open(path) as image:
images.append(image.convert("RGB"))
inputs = processor(images=images, return_tensors="pt")
inputs = {key: value.to(device) for key, value in inputs.items()}
outputs = model(**inputs)
embedding = getattr(outputs, "pooler_output", None)
if embedding is None:
embedding = outputs.last_hidden_state[:, 0]
embedding = torch.nn.functional.normalize(embedding, dim=1)
chunks.append(embedding.cpu().numpy().astype(np.float32))
return np.concatenate(chunks, axis=0)
def load_local_resnet50_feature_model(
checkpoint_path: Path, num_classes: int, device: str = "cpu"
) -> nn.Module:
"""Load this project's classifier and expose its 2048-D pre-classifier features."""
model = resnet50(weights=None)
model.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(model.fc.in_features, num_classes))
try:
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=True)
except Exception:
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
if isinstance(checkpoint, nn.Module):
state_dict = checkpoint.state_dict()
elif isinstance(checkpoint, dict):
state_dict = checkpoint.get("state_dict", checkpoint.get("model_state", checkpoint))
else:
raise ValueError("不支持的模型文件格式")
model.load_state_dict(state_dict, strict=True)
feature_model = nn.Sequential(*list(model.children())[:-1]).to(device)
feature_model.eval()
return feature_model
def load_seal_retriever_feature_model(checkpoint_path: Path, device: str = "cpu") -> nn.Module:
"""Load a small-seal fine-tuned backbone and expose 2048-D features.
The training checkpoint intentionally stores no classification head, so it
can be used for retrieval across both supervised and single-sample chars.
"""
model = resnet50(weights=None)
try:
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=True)
except Exception:
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
state_dict = checkpoint.get("backbone_state_dict", checkpoint) if isinstance(checkpoint, dict) else checkpoint
if not isinstance(state_dict, dict):
raise ValueError("不支持的小篆检索训练权重格式")
incompatible = model.load_state_dict(state_dict, strict=False)
unexpected = [key for key in incompatible.unexpected_keys if not key.startswith("fc.")]
if unexpected:
raise ValueError(f"小篆检索权重包含未知参数:{unexpected[0]}")
feature_model = nn.Sequential(*list(model.children())[:-1]).to(device)
feature_model.eval()
return feature_model
def embed_images_local_resnet50(
image_paths: Iterable[Path],
model: nn.Module,
batch_size: int = 16,
device: str = "cpu",
image_size: int = 128,
) -> np.ndarray:
"""Create L2-normalized embeddings with the project's existing classifier backbone."""
paths = list(image_paths)
chunks: list[np.ndarray] = []
mean = torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32).view(3, 1, 1)
with torch.inference_mode():
for start in range(0, len(paths), batch_size):
images = []
for path in paths[start : start + batch_size]:
with Image.open(path) as image:
rgb = image.convert("RGB").resize(
(image_size, image_size), Image.Resampling.BILINEAR
)
pixels = np.asarray(rgb, dtype=np.float32) / 255.0
tensor = torch.from_numpy(pixels).permute(2, 0, 1)
images.append((tensor - mean) / std)
batch = torch.stack(images).to(device)
embedding = model(batch).flatten(1)
embedding = torch.nn.functional.normalize(embedding, dim=1)
chunks.append(embedding.cpu().numpy().astype(np.float32))
return np.concatenate(chunks, axis=0)
def embed_pil_images_local_resnet50(
images: Iterable[Image.Image],
model: nn.Module,
batch_size: int = 16,
device: str = "cpu",
image_size: int = 128,
) -> np.ndarray:
"""Embed in-memory images using the same preprocessing as the index builder."""
image_list = list(images)
chunks: list[np.ndarray] = []
mean = torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32).view(3, 1, 1)
with torch.inference_mode():
for start in range(0, len(image_list), batch_size):
tensors = []
for image in image_list[start : start + batch_size]:
rgb = image.convert("RGB").resize(
(image_size, image_size), Image.Resampling.BILINEAR
)
pixels = np.asarray(rgb, dtype=np.float32) / 255.0
tensor = torch.from_numpy(pixels).permute(2, 0, 1)
tensors.append((tensor - mean) / std)
batch = torch.stack(tensors).to(device)
embedding = torch.nn.functional.normalize(model(batch).flatten(1), dim=1)
chunks.append(embedding.cpu().numpy().astype(np.float32))
return np.concatenate(chunks, axis=0)
|