Spaces:
No application file
No application file
| import os | |
| import torch | |
| from PIL import Image | |
| from datasets import load_dataset | |
| from transformers import ( | |
| AutoProcessor, | |
| Qwen2VLForConditionalGeneration, | |
| TrainingArguments, | |
| Trainer, | |
| BitsAndBytesConfig | |
| ) | |
| from peft import LoraConfig, get_peft_model | |
| # ========================= | |
| # CONFIG | |
| # ========================= | |
| JSONL_PATH = "0508_clean.jsonl" | |
| MODEL_NAME = "Qwen/Qwen2-VL-2B-Instruct" | |
| OUTPUT_DIR = "./qlora-vlm" | |
| BATCH_SIZE = 1 | |
| GRAD_ACCUM = 4 | |
| LR = 2e-4 | |
| EPOCHS = 2 | |
| MAX_LENGTH = 1024 | |
| IMAGE_SIZE = 512 | |
| FRAC = 0.01 | |
| # ========================= | |
| # LOAD MODEL (4-bit) | |
| # ========================= | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_compute_dtype=torch.float16, | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_quant_type="nf4" | |
| ) | |
| processor = AutoProcessor.from_pretrained(MODEL_NAME) | |
| model = Qwen2VLForConditionalGeneration.from_pretrained( | |
| MODEL_NAME, | |
| quantization_config=bnb_config, | |
| device_map="auto", | |
| ) | |
| # ========================= | |
| # APPLY LORA | |
| # ========================= | |
| lora_config = LoraConfig( | |
| r=8, | |
| lora_alpha=16, | |
| target_modules=["q_proj", "v_proj"], | |
| lora_dropout=0.05, | |
| bias="none", | |
| task_type="CAUSAL_LM" | |
| ) | |
| model = get_peft_model(model, lora_config) | |
| # ========================= | |
| # DATASET | |
| # ========================= | |
| dataset = load_dataset( | |
| "json", | |
| data_files=JSONL_PATH | |
| ) | |
| dataset = dataset["train"].train_test_split(test_size=0.2) | |
| def preprocess(example): | |
| # Fix image path | |
| # image = Image.open(example["image"]).convert("RGB") | |
| # Clip markdown (important) | |
| prompt = "Convert this document image into structured Markdown." | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "image"}, | |
| {"type": "text", "text": prompt} | |
| ] | |
| }, | |
| {"role": "assistant", "content": example["markdown"]} | |
| ] | |
| text = processor.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=False | |
| ) | |
| return { | |
| "text": text, | |
| "image_path": example["image"] | |
| } | |
| # inputs = processor( | |
| # text=text, | |
| # images=image, | |
| # return_tensors="pt", | |
| # padding="max_length", | |
| # truncation=True, | |
| # max_length=MAX_LENGTH | |
| # ) | |
| # inputs["labels"] = inputs["input_ids"].clone() | |
| # return {k: v.squeeze(0) for k, v in inputs.items()} | |
| # ========================= | |
| # APPLY PREPROCESSING | |
| # ========================= | |
| train_ds = dataset["train"].select(range(int(FRAC * len(dataset["train"])))).map( | |
| preprocess, | |
| remove_columns=dataset["train"].column_names | |
| ) | |
| val_ds = dataset["test"].select(range(int(FRAC * len(dataset["test"])))).map( | |
| preprocess, | |
| remove_columns=dataset["test"].column_names | |
| ) | |
| # ========================= | |
| # COLLATE FUNCTION (KEY FIX) | |
| # ========================= | |
| def collate_fn(batch): | |
| images = [] | |
| texts = [] | |
| for x in batch: | |
| image = Image.open(x["image_path"]).convert("RGB") | |
| image = image.resize((IMAGE_SIZE, IMAGE_SIZE)) # 🔥 reduce memory | |
| images.append(image) | |
| texts.append(x["text"]) | |
| inputs = processor( | |
| text=texts, | |
| images=images, | |
| padding=True, # 🔥 dynamic padding (IMPORTANT) | |
| truncation=True, | |
| max_length=MAX_LENGTH, | |
| return_tensors="pt" | |
| ) | |
| labels = inputs["input_ids"].clone() | |
| labels[labels == processor.tokenizer.pad_token_id] = -100 | |
| inputs["labels"] = labels | |
| return inputs | |
| # ========================= | |
| # TRAINING | |
| # ========================= | |
| training_args = TrainingArguments( | |
| output_dir=OUTPUT_DIR, | |
| per_device_train_batch_size=BATCH_SIZE, | |
| per_device_eval_batch_size=1, | |
| gradient_accumulation_steps=GRAD_ACCUM, | |
| learning_rate=LR, | |
| num_train_epochs=EPOCHS, | |
| logging_steps=10, | |
| save_strategy="epoch", | |
| eval_strategy="epoch", | |
| fp16=True, | |
| report_to="none", | |
| remove_unused_columns=False | |
| ) | |
| trainer = Trainer( | |
| model=model, | |
| args=training_args, | |
| train_dataset=train_ds, | |
| eval_dataset=val_ds, | |
| data_collator=collate_fn | |
| ) | |
| trainer.train() | |
| # ========================= | |
| # SAVE MODEL | |
| # ========================= | |
| model.save_pretrained(OUTPUT_DIR) | |
| # ========================= | |
| # INFERENCE (TEST) | |
| # ========================= | |
| def generate_markdown(image_path): | |
| image = Image.open(image_path).convert("RGB") | |
| prompt = "Convert this document image into structured Markdown." | |
| messages = [ | |
| {"role": "user", "content": prompt} | |
| ] | |
| text = processor.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True | |
| ) | |
| inputs = processor( | |
| text=text, | |
| images=image, | |
| return_tensors="pt" | |
| ).to(model.device) | |
| with torch.no_grad(): | |
| output = model.generate(**inputs, max_new_tokens=512) | |
| result = processor.decode(output[0], skip_special_tokens=True) | |
| return result | |
| # Example usage | |
| # print(generate_markdown("sample.png")) |