--- license: apache-2.0 language: - en base_model: - Qwen/Qwen2.5-VL-3B-Instruct tags: - R2R - VLN - Room-to-Room - LVLM --- # Qwen2.5-VL-3B-R2R-panoramic **Qwen2.5-VL-3B-R2R-panoramic** is a Vision-and-Language Navigation (VLN) model fine-tuned from [Qwen2.5-VL-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct) on the [Room-to-Room (R2R)](https://bringmeaspoon.org/) dataset using the Matterport3D (MP3D) simulator. The model is trained using a panoramic action space, where the model recives a preprocessed panoramic image and a set of candidate views which each point towards a node in a Matterport3D simualtor environment. Only the LLM component is fine-tuned โ€” the vision encoder and cross-modal projector are kept frozen. ## ๐Ÿง  Model Summary - **Base Model**: Qwen2.5-VL-3B-Instruct - **Dataset**: Room-to-Room (R2R) via the Matterport3D simulator. - **Image Resolution**: 320x240 for candidate images and 960ร—240 for panoramic images. - **Action Space**: Panoramic. ## ๐Ÿงช Training Setup - **Frozen Modules**: Vision encoder and cross-modal projector - **Fine-Tuned Module**: LLM decoder (Qwen2.5) - **Optimizer**: AdamW - **Batch Size**: `1` (with gradient accumulation over each episode) - **Learning Rate**: `1e-5` - **Weight Decay**: `0.1` - **Precision**: `bfloat16` - **LR Scheduler**: Linear scheduler with warmup (first 10% of steps) - **Hardware**: Trained on a single NVIDIA A100 80GB GPU Training was done using supervised learning for next-action prediction. The model was conditioned at each step with a system prompt, panoramic RGB image observations (960ร—240) of current view, as well as variable amount of candidate RGB iamges (320x240), and cumulative episode history including previosu panoramas. The model was trained offline (not in the MP3D simulator) using teacher-forcing on a preprocessed R2R dataset. ## ๐Ÿ“ฆ Usage ```python import torch from torch.utils.data import Dataset, DataLoader from datasets import Dataset as DT from transformers import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor from PIL import Image lass CustomDataset(Dataset): def __init__(self, data): self.text = data["text"] self.panoramas = data["panoramas"] self.candidates = data["candidates"] def __len__(self): return len(self.text) def __getitem__(self, index): return self.text[index], self.panoramas[index], self.candidates[index] # TODO: make the collatefunctor work with batches class CollateFunctor: # No batch, therefore no max length def __init__(self, processor, width, height): self.processor = processor self.width = width self.height = height def __call__(self, batch): text, panoramas, candidates = batch[0] label_start = processor.tokenizer("<|im_start|>assistant\nCandidate: ", return_tensors="pt").input_ids images = [Image.open(img) for img in panoramas] candidate_images = [Image.open(img) for img in candidates] #candidate_images = [Image.open(img).resize((self.width, self.height), Image.Resampling.LANCZOS) for img in candidates] images.extend(candidate_images) processed = processor(text=text, images=[images], return_tensors="pt") prompt_input_ids = processed["input_ids"] input_ids = torch.cat([prompt_input_ids, label_start], dim=1) attention_mask = torch.ones(1, input_ids.shape[1]) processed["input_ids"] = input_ids processed["attention_mask"] = attention_mask return processed def format_prompt(images_path, path_id, route_instruction, step_id, distance_traveled, candidates, processor, system_prompt): # should be in the order: panorama_history, current_panorama, candidates views from left to right images = os.listdir(images_path) panoramas = [os.path.join(images_path, img) for img in images if img.startswith("pano")] panoramas = sorted(panoramas, key=lambda x: int(x.split("_")[-1].split(".")[-2])) # these are probably sorted by default, however you might need to check candidate_images = [os.path.join(images_path, img) for img in images if img.startswith("pano") == False] candidate_images = sorted(candidate_images, key=lambda x: int(x.split("_")[-1].split(".")[0])) current_panorama = panoramas.pop(-1) # route instruction, current step, cumulative distance content = [ { "type" : "text", "text" : f"Route instruction: {route_instruction}\nCurrent step: {step_id}\nCumulative Distance Traveled: {distance_traveled} meters\n\nPanorama Images from Previous Steps:" } ] # panorama from previous steps for i, img in enumerate(panoramas): content.append({ "type" : "text", "text" : f"\n\tPanorama at step: {i}: " }) content.append({ "type" : "image", "image" : img }) if len(panoramas) == 0: content[0]["text"] += f"[]" # current panorama content.append({ "type" : "text", "text" : f"\n\nCurrent Panorama Image:\n\t" }) content.append({ "type" : "image", "image" : current_panorama }) # candidate directions content.append({ "type" : "text", "text" : "\n\nCandidate Directions:" }) for i, candidate in enumerate(candidates): relative_angle = round(candidate["relative_angle"], 0) distance = round(candidate["distance"], 2) direction = "Left" if relative_angle < 0 else "Right" content.append({ "type" : "text", "text" : f"\n\tCandidate: {i}:\n\t\tRelative angle: {abs(relative_angle)} degrees to the {direction}\n\t\tDistance: {distance} meters\n\t\tview: " }) content.append({ "type" : "image", "image" : candidate_images[i] }) # adds candidate STOP and the select cnadidate view content.append({ "type" : "text", "text" : "\n\tCandidate: Stop\n\nNow, analyze the route instruction, your current position, and the available candidate directions. Select the candidate that best matches the instruction and helps you continue along the correct path. Answer on the format: Candidate: (and then the number)" }) messages = [ {"role" : "system", "content" : [{"type" : "text", "text" : system_prompt}]}, {"role" : "user", "content" : content}, ] text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=False) panoramas.extend([current_panorama]) formatted_sample = {} formatted_sample["text"] = text formatted_sample["candidates"] = candidate_images formatted_sample["panoramas"] = panoramas formatted_data = [formatted_sample] formatted_data = DT.from_list(formatted_data) return formatted_data processor = AutoProcessor.from_pretrained("Vebbern/Qwen2.5-VL-3B-R2R-panoramic") model = Qwen2_5_VLForConditionalGeneration.from_pretrained( "Vebbern/Qwen2.5-VL-3B-R2R-panoramic", torch_dtype=torch.bfloat16, attn_implementation="flash_attention_2", device_map="cuda" ) # remember to set the correct image resolution (however a higher might still work as the vision encoder is not trained) collate_fn = CollateFunctor(processor, 320, 240) # Load mandatory system prompt with open("system_prompt.txt", "r") as f: system_prompt = f.read() path_id = 4332 # id for the R2R path route_instruction = "Walk to the other end of the lobby and wait near the exit. " images_path = f"./images/{path_id}" step_id = 0 cumulative_distance = 0 candidates = { "0" : { "relative_angle" -60.62797609213225, "relative_direction": "Left", "distance": 2.3325929641723633 }, "1": { "relative_angle": -0.00397697185949581, "relative_direction": "Front", "distance": 4.637096405029297 }, "2": { "relative_angle": 25.24592108757226, "relative_direction": "Front", "distance": 3.3661904335021973 } } prompt = format_prompt(images_path, path_id, route_instruction, step_id, cumulative_distance, candidates, processor, system_prompt) dataset = CustomDataset(prompt) data_loader = DataLoader( dataset, batch_size=1, collate_fn=collate_fn ) # Run inference for batch in data_loader: batch.to("cuda") outputs = model(**batch) argmax = torch.argmax(outputs.logits, dim=2)[0] model_prediction = processor.decode(argmax[-1]) # is -1 because it does not predict one more print(f"Predicted action: {model_prediction}") ``` > โš ๏ธ Sorry for the rough code โ€” the goal here is to show how the system prompt and inputs should be structured for inference. The system prompt is included in the repo. ## ๐Ÿ“Š Evaluation Results The model was evaluated on the standard Room-to-Room (R2R) validation sets using the Matterport3D simulator. Performance is measured using the standard VLN (Vision-and-Language Navigation) metrics. | Metric | Val Seen | Val Unseen | Test | |-------------------------|----------|------------|-------| | Path Length (โ†“) | 9.98 | 9.83 | 9.96 | | Navigation Error (โ†“) | 5.69 | 6.65 | 6.53 | | Oracle Success Rate (โ†‘) | 56% | 46% | 50% | | Success Rate (โ†‘) | 50% | 38% | 41% | | SPL (โ†‘) | 47% | 35% | 38% | ### ๐Ÿงพ Metric Definitions - **Navigation Error**: Mean distance from the goal when the agent stops. - **Success Rate**: Percentage of episodes where the agent ends within 3 meters of the goal. - **SPL (Success weighted by Path Length)**: Penalizes long or inefficient paths. - **Oracle Success**: If the agent had stopped at its closest point to the goal. ### ๐Ÿ“ Remarks This model performs far behind R2R State-of-the-art models, likely due to a combination of factors such as underlying model archtiecture, training strategy, and panoramic representation. ## ๐Ÿ” Related Models There also exists a low-level action space eqivalent of this model. - **Low-Level Action Space Version**: [Qwen2.5-VL-3B-R2R-low-level](https://huggingface.co/Vebbern/Qwen2.5-VL-3B-R2R-low-level) ## ๐Ÿชช License This model is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).