VisME-Qwen25VL-3B / visme.py
HugC's picture
Upload folder using huggingface_hub
2e124cb verified
Raw
History Blame Contribute Delete
16.9 kB
import logging
import os
import torch
import torch.nn as nn
import numpy as np
from PIL import Image
from urllib.parse import urlparse
from typing import List, Optional, Union
from transformers import AutoConfig
from transformers import Qwen2_5_VLProcessor
from transformers import Qwen2_5_VLForConditionalGeneration
from qwen_vl_utils import process_vision_info
MAX_LENGTH = 2048
IMAGE_BASE_FACTOR = 14
IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_PIXELS = 1280 * IMAGE_FACTOR * IMAGE_FACTOR
FPS = 1
MAX_FRAMES = 64
FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
EOS_TOKEN = "<|endoftext|>"
logger = logging.getLogger(__name__)
class VisME(nn.Module):
def __init__(
self,
model_name: str,
pooling: str = 'last',
normalize: bool = True,
max_length: int = MAX_LENGTH,
min_pixels: int = MIN_PIXELS,
max_pixels: int = MAX_PIXELS,
total_pixels: int = MAX_TOTAL_PIXELS,
fps: float = FPS,
max_frames: int = MAX_FRAMES,
processor=None,
default_instruction: str = "You are a helpful assistant.",
attn_implementation: Optional[str] = "flash_attention_2",
**kwargs,
):
"""
Initialize the Qwen2_5_VL embedding model.
Args:
model_name: Path to the base model (HuggingFace model ID or local path)
device: Device to use
max_length: Maximum sequence length
attn_implementation: Attention implementation method
pooling: Pooling strategy ('last' or 'eos')
normalize: Whether to normalize embeddings
checkpoint_path: Path to a trained model checkpoint (if provided, will be loaded with higher priority)
use_custom_model: Whether to use the custom Qwen2_5_VLForConditionalGeneration
system_prompt: System prompt string
output_layer: Which layer to extract embeddings from:
- -1 or None: Last hidden layer (default)
- int (0 to num_layers-1): Specific hidden layer index
- tuple (layer_idx, component): e.g., (-1, 'attn') or (-2, 'mlp')
output_component: Which component of the layer to use (when output_layer is int):
- 'full': Complete layer output (default, after attn + mlp)
- 'attn': Self-attention output only
- 'mlp': MLP output only
processor: Optional custom processor instance. If None, will load default processor from model_name
**kwargs: Additional parameters
"""
super().__init__()
self.pooling = pooling
self.normalize = normalize
self.default_instruction = default_instruction
self.max_length = max_length
self.min_pixels = min_pixels
self.max_pixels = max_pixels
self.total_pixels = total_pixels
self.fps = fps
self.max_frames = max_frames
self.eos_token = kwargs.get("eos_token", EOS_TOKEN)
config = AutoConfig.from_pretrained(model_name, trust_remote_code=True)
config._attn_implementation = attn_implementation
config.use_cache = False
base_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
model_name,
config=config,
dtype=torch.bfloat16,
low_cpu_mem_usage=True,
)
self.model = base_model.model
if processor is not None:
self.processor = processor
else:
self.processor = Qwen2_5_VLProcessor.from_pretrained(
model_name,
padding_side='left',
trust_remote_code=True
)
@property
def device(self):
return next(self.model.parameters()).device
@staticmethod
def is_image_path(path: str) -> bool:
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
if path.startswith(('http://', 'https://')):
parsed_url = urlparse(path)
clean_path = parsed_url.path
else:
clean_path = path
_, ext = os.path.splitext(clean_path.lower())
return ext in image_extensions
@staticmethod
def is_video_input(video) -> bool:
if isinstance(video, str):
return True
if isinstance(video, list) and len(video) > 0:
first_elem = video[0]
if isinstance(first_elem, Image.Image):
return True
if isinstance(first_elem, str):
return VisME.is_image_path(first_elem)
return False
def _load_images(self, images):
if images is None:
return None
if not (isinstance(images, list) or isinstance(images, tuple)):
images = [images]
if len(images) == 0:
return images
if images[0] is None:
return images
if isinstance(images[0], (Image.Image, np.ndarray, torch.Tensor)):
return images
if isinstance(images[0], str):
loaded_images = []
for image in images:
if image is None:
loaded_images.append(None)
continue
if image.startswith(('http://', 'https://', 'file://')):
loaded_images.append(image)
else:
loaded_images.append('file://' + image)
return loaded_images
raise ValueError("Unsupported image type")
def _batch_to_device(self, batch):
_batch = {}
for key, value in batch.items():
if isinstance(value, torch.Tensor):
_batch[key] = value.to(self.device)
else:
_batch[key] = value
return _batch
def _load_input(self, input):
"""
Args:
input: dict, {'text': list of text, 'image': list of image file path or PIL images, 'instruction': list of instruction}
Returns:
texts: list of text
images: list of image file path or PIL images
instructions: list of instruction
"""
images = input.get('image', [])
instructions = input.get('instruction', [])
texts = input.get('text', [])
if not isinstance(images, list):
images = [images]
if not isinstance(instructions, list):
if instructions is None:
instructions = ""
instructions = [instructions]
if not isinstance(texts, list):
if texts is None:
texts = ""
texts = [texts]
assert len(texts) > 0 or len(images) > 0, "At least one of text or image must be provided!"
if len(images) == 0:
images = [None] * len(texts)
if len(texts) == 0:
texts = [""] * len(images)
if len(instructions) == 0:
instructions = [""] * len(images)
if len(instructions) == 1:
instructions = instructions * len(images)
return images, instructions, texts
def format_input(
self,
text: list[str] | str | None = None,
image: list[str | Image.Image] | str | Image.Image | None = None,
video: list[str | list[str | Image.Image]] | list[str | Image.Image] | None = None,
instruction: str | None = None,
fps: float | None = None,
max_frames: int | None = None,
) -> list[dict]:
content = []
conversation = [
{"role": "system", "content": [{"type": "text", "text": instruction or self.default_instruction}]},
{"role": "user", "content": content}
]
# Normalize text input to list
if text is None:
texts = []
elif isinstance(text, str):
texts = [text]
else:
texts = text
# Normalize image input to list
if image is None:
images = []
elif not isinstance(image, list):
images = [image]
else:
images = image
# Normalize video input to list
if video is None:
videos = []
elif self.is_video_input(video):
videos = [video]
else:
# Assume it's a list of videos
videos = video
# Add text, image, or video content to conversation
if not texts and not images and not videos:
logger.warning("No text, image, or video content found in input")
content.append({'type': 'text', 'text': "NULL"})
return conversation
# Process each video
for vid in videos:
video_content = None
video_kwargs = {'total_pixels': self.total_pixels}
if isinstance(vid, list):
# Video as frame sequence
video_content = vid
if self.max_frames is not None:
video_content = _sample_frames(video_content, self.max_frames)
video_content = [
('file://' + ele if isinstance(ele, str) else ele)
for ele in video_content
]
elif isinstance(vid, str):
# Video as file path
video_content = vid if vid.startswith(('http://', 'https://')) else 'file://' + vid
video_kwargs = {'fps': fps or self.fps, 'max_frames': max_frames or self.max_frames}
else:
raise TypeError(f"Unrecognized video type: {type(vid)}")
# Add video input to content
if video_content:
content.append({
'type': 'video',
'video': video_content,
**video_kwargs
})
# Process each image
for img in images:
image_content = None
if isinstance(img, Image.Image):
image_content = img
elif isinstance(img, str):
image_content = img if img.startswith(('http://', 'https://')) else 'file://' + img
else:
raise TypeError(f"Unrecognized image type: {type(img)}")
# Add image input to content
if image_content:
content.append({
'type': 'image',
'image': image_content,
"min_pixels": self.min_pixels,
"max_pixels": self.max_pixels
})
# Process each text
for txt in texts:
content.append({'type': 'text', 'text': txt})
return conversation
def preprocess_input(
self,
inputs: Union[dict, List[dict]],
**kwargs
):
if isinstance(inputs, dict):
inputs = [inputs]
fps = kwargs.get('fps', self.fps)
max_frames = kwargs.get('max_frames', self.max_frames)
conversations = [self.format_input(
text=ele.get('text'),
image=ele.get('image'),
video=ele.get('video'),
instruction=ele.get('instruction'),
fps=fps,
max_frames=max_frames
) for ele in inputs]
text = self.processor.apply_chat_template(conversations, tokenize=False, add_generation_prompt=True)
text = [t+self.eos_token for t in text]
try:
images, video_inputs, video_kwargs = process_vision_info(
conversations, image_patch_size=IMAGE_BASE_FACTOR,
return_video_metadata=True, return_video_kwargs=True
)
except Exception as e:
logger.error(f"Error in processing vision info: {e}")
images = None
video_inputs = None
video_kwargs = {'do_sample_frames': False}
text = self.processor.apply_chat_template(
[{'role': 'user', 'content': [{'type': 'text', 'text': 'NULL'}]}],
add_generation_prompt=True, tokenize=False
)
if video_inputs is not None:
videos, video_metadata = zip(*video_inputs)
videos = list(videos)
video_metadata = list(video_metadata)
else:
videos, video_metadata = None, None
inputs = self.processor(
text=text, images=images, videos=videos, video_metadata=video_metadata, truncation=True,
max_length=self.max_length, padding=True, return_tensors='pt',
**video_kwargs
)
return inputs
def _pooling(self, embeddings_source: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
"""Pool sequence hidden states using ``self.pooling`` (last / eos / mean)."""
pooling = getattr(self, "pooling", "last")
if pooling in ("last", "eos"):
left_padding = attention_mask[:, -1].sum() == attention_mask.shape[0]
batch_size = embeddings_source.shape[0]
if left_padding:
reps = embeddings_source[torch.arange(batch_size), -1, :]
else:
eos_indices = attention_mask.sum(dim=1) - 1
reps = embeddings_source[
torch.arange(batch_size, device=embeddings_source.device), eos_indices
]
elif pooling == "mean":
mask_expanded = attention_mask.unsqueeze(-1).expand(embeddings_source.size()).float()
sum_embeddings = torch.sum(embeddings_source * mask_expanded, dim=1)
sum_mask = torch.clamp(mask_expanded.sum(dim=1), min=1e-9)
reps = sum_embeddings / sum_mask
else:
raise NotImplementedError(f"Pooling method '{pooling}' not implemented")
return reps
def _encode(self, input):
"""
Encode inputs and extract embeddings from specified layer.
Args:
input: Processed inputs (input_ids, attention_mask, etc.)
Returns:
Tensor of shape (batch_size, hidden_dim)
"""
outputs = self.model(
**input,
output_hidden_states=False,
return_dict=True,
use_cache=False,
)
embeddings_source = outputs.last_hidden_state
reps = self._pooling(embeddings_source, input['attention_mask'])
if self.normalize:
reps = torch.nn.functional.normalize(reps, p=2, dim=-1)
return reps
def encode_text(
self,
texts: List[str],
instruction: Optional[Union[str, List[str]]] = None,
**kwargs,
) -> torch.Tensor:
"""Convenience method for text-only embeddings"""
inputs = {"text": texts, "instruction": instruction}
return self.encode_input(inputs, instruction=instruction, **kwargs)
def encode_image(
self,
images,
instruction: Optional[Union[str, List[str]]] = None,
**kwargs,
) -> torch.Tensor:
"""Convenience method for image-only embeddings.
Args:
images: Can be:
- List[Image.Image]: Single image per input
- List[List[Image.Image]]: Multiple images per input
"""
inputs = {"image": images, "instruction": instruction}
return self.encode_input(inputs, instruction=instruction, **kwargs)
def encode_input(
self,
inputs: dict,
**kwargs,
) -> torch.Tensor:
"""Batch processing for large collections of texts/images.
Args:
texts: List of text inputs (optional)
images: Can be:
- List[Image.Image]: Single image per input
- List[List[Image.Image]]: Multiple images per input
instruction: Instruction(s) for the model
batch_size: Number of items to process at once
show_progress: Whether to display progress bar
"""
if 'input_ids' in inputs:
assert isinstance(inputs['input_ids'], torch.Tensor), "input_ids must be a tensor"
assert isinstance(inputs['attention_mask'], torch.Tensor), "attention_mask must be a tensor"
inputs = self._batch_to_device(inputs)
else:
inputs = self.preprocess_input(inputs, **kwargs)
inputs = self._batch_to_device(inputs)
embeddings = self._encode(inputs)
return embeddings
def _sample_frames(frames: list[str | Image.Image], max_segments: int) -> list[str | Image.Image]:
duration = len(frames)
if duration <= max_segments:
return frames
frame_id_array = np.linspace(0, duration - 1, max_segments, dtype=int)
frame_id_list = frame_id_array.tolist()
sampled_frames = [ frames[frame_idx] for frame_idx in frame_id_list ]
return sampled_frames