Views
No views yet
1 (with gradient accumulation over each episode)1e-50.1bfloat161import torch
2from torch.utils.data import Dataset, DataLoader
3from datasets import Dataset as DT
4from transformers import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor
5from PIL import Image
6
7lass CustomDataset(Dataset):
8 def __init__(self, data):
9 self.text = data["text"]
10 self.panoramas = data["panoramas"]
11 self.candidates = data["candidates"]
12
13 def __len__(self):
14 return len(self.text)
15
16 def __getitem__(self, index):
17 return self.text[index], self.panoramas[index], self.candidates[index]
18
19# TODO: make the collatefunctor work with batches
20class CollateFunctor:
21 # No batch, therefore no max length
22 def __init__(self, processor, width, height):
23 self.processor = processor
24 self.width = width
25 self.height = height
26
27 def __call__(self, batch):
28 text, panoramas, candidates = batch[0]
29 label_start = processor.tokenizer("<|im_start|>assistant\nCandidate: ", return_tensors="pt").input_ids
30
31 images = [Image.open(img) for img in panoramas]
32 candidate_images = [Image.open(img) for img in candidates]
33 #candidate_images = [Image.open(img).resize((self.width, self.height), Image.Resampling.LANCZOS) for img in candidates]
34 images.extend(candidate_images)
35
36 processed = processor(text=text, images=[images], return_tensors="pt")
37
38 prompt_input_ids = processed["input_ids"]
39 input_ids = torch.cat([prompt_input_ids, label_start], dim=1)
40
41 attention_mask = torch.ones(1, input_ids.shape[1])
42 processed["input_ids"] = input_ids
43 processed["attention_mask"] = attention_mask
44
45 return processed
46
47
48def format_prompt(images_path, path_id, route_instruction, step_id, distance_traveled, candidates, processor, system_prompt):
49 # should be in the order: panorama_history, current_panorama, candidates views from left to right
50 images = os.listdir(images_path)
51 panoramas = [os.path.join(images_path, img) for img in images if img.startswith("pano")]
52 panoramas = sorted(panoramas, key=lambda x: int(x.split("_")[-1].split(".")[-2]))
53
54 # these are probably sorted by default, however you might need to check
55 candidate_images = [os.path.join(images_path, img) for img in images if img.startswith("pano") == False]
56 candidate_images = sorted(candidate_images, key=lambda x: int(x.split("_")[-1].split(".")[0]))
57
58 current_panorama = panoramas.pop(-1)
59
60 # route instruction, current step, cumulative distance
61 content = [
62 {
63 "type" : "text",
64 "text" : f"Route instruction: {route_instruction}\nCurrent step: {step_id}\nCumulative Distance Traveled: {distance_traveled} meters\n\nPanorama Images from Previous Steps:"
65 }
66 ]
67
68 # panorama from previous steps
69 for i, img in enumerate(panoramas):
70 content.append({
71 "type" : "text",
72 "text" : f"\n\tPanorama at step: {i}: "
73 })
74 content.append({
75 "type" : "image",
76 "image" : img
77 })
78
79 if len(panoramas) == 0:
80 content[0]["text"] += f"[]"
81
82 # current panorama
83 content.append({
84 "type" : "text",
85 "text" : f"\n\nCurrent Panorama Image:\n\t"
86 })
87
88 content.append({
89 "type" : "image",
90 "image" : current_panorama
91 })
92
93 # candidate directions
94 content.append({
95 "type" : "text",
96 "text" : "\n\nCandidate Directions:"
97 })
98
99 for i, candidate in enumerate(candidates):
100 relative_angle = round(candidate["relative_angle"], 0)
101 distance = round(candidate["distance"], 2)
102 direction = "Left" if relative_angle < 0 else "Right"
103
104 content.append({
105 "type" : "text",
106 "text" : f"\n\tCandidate: {i}:\n\t\tRelative angle: {abs(relative_angle)} degrees to the {direction}\n\t\tDistance: {distance} meters\n\t\tview: "
107 })
108
109 content.append({
110 "type" : "image",
111 "image" : candidate_images[i]
112 })
113
114
115 # adds candidate STOP and the select cnadidate view
116 content.append({
117 "type" : "text",
118 "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)"
119 })
120
121 messages = [
122 {"role" : "system", "content" : [{"type" : "text", "text" : system_prompt}]},
123 {"role" : "user", "content" : content},
124 ]
125
126 text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
127
128 panoramas.extend([current_panorama])
129
130 formatted_sample = {}
131 formatted_sample["text"] = text
132 formatted_sample["candidates"] = candidate_images
133 formatted_sample["panoramas"] = panoramas
134
135 formatted_data = [formatted_sample]
136 formatted_data = DT.from_list(formatted_data)
137 return formatted_data
138
139
140processor = AutoProcessor.from_pretrained("Vebbern/Qwen2.5-VL-3B-R2R-panoramic")
141model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
142 "Vebbern/Qwen2.5-VL-3B-R2R-panoramic",
143 torch_dtype=torch.bfloat16,
144 attn_implementation="flash_attention_2",
145 device_map="cuda"
146)
147
148# remember to set the correct image resolution (however a higher might still work as the vision encoder is not trained)
149collate_fn = CollateFunctor(processor, 320, 240)
150
151# Load mandatory system prompt
152with open("system_prompt.txt", "r") as f:
153 system_prompt = f.read()
154
155path_id = 4332 # id for the R2R path
156route_instruction = "Walk to the other end of the lobby and wait near the exit. "
157images_path = f"./images/{path_id}"
158step_id = 0
159cumulative_distance = 0
160candidates = {
161 "0" : {
162 "relative_angle" -60.62797609213225,
163 "relative_direction": "Left",
164 "distance": 2.3325929641723633
165 },
166 "1": {
167 "relative_angle": -0.00397697185949581,
168 "relative_direction": "Front",
169 "distance": 4.637096405029297
170 },
171 "2": {
172 "relative_angle": 25.24592108757226,
173 "relative_direction": "Front",
174 "distance": 3.3661904335021973
175 }
176}
177
178prompt = format_prompt(images_path, path_id, route_instruction, step_id, cumulative_distance, candidates, processor, system_prompt)
179
180dataset = CustomDataset(prompt)
181data_loader = DataLoader(
182 dataset,
183 batch_size=1,
184 collate_fn=collate_fn
185)
186
187# Run inference
188for batch in data_loader:
189 batch.to("cuda")
190
191 outputs = model(**batch)
192 argmax = torch.argmax(outputs.logits, dim=2)[0]
193 model_prediction = processor.decode(argmax[-1]) # is -1 because it does not predict one more
194 print(f"Predicted action: {model_prediction}")
195⚠️ 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.
| 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% |