File size: 7,515 Bytes
26f4812
fa677ac
26f4812
fa677ac
 
6fa9b85
fa677ac
 
 
 
 
6fa9b85
fa677ac
6fa9b85
 
 
 
 
26f4812
 
6fa9b85
26f4812
6fa9b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26f4812
fa677ac
6fa9b85
 
fa677ac
6fa9b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa677ac
6fa9b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
---
base_model: Qwen/Qwen3.5-4B-Base
library_name: transformers
language:
- id
license: apache-2.0
pipeline_tag: image-text-to-text
tags:
- image-captioning
- qwen3.5
- bahasa-indonesia
- lora
- lora-merged
- vlm
- multimodal
- json-output
datasets:
- custom
---

# Qwen3.5-4B ImCap โ€” Indonesian Image Captioning (LoRA merged)

Model ini merupakan hasil fine-tuning (LoRA, sudah di-**merge** ke base weights) dari
[`Qwen/Qwen3.5-4B-Base`](https://huggingface.co/Qwen/Qwen3.5-4B-Base) untuk tugas **image captioning Bahasa Indonesia**.

---

## Deskripsi Singkat

| Atribut | Nilai |
|---|---|
| Base model | `Qwen/Qwen3.5-4B-Base` |
| Metode fine-tune | LoRA (rank/alpha sesuai config SFT) |
| Status adapter | **Merged** ke base weights |
| Bahasa output | Bahasa Indonesia ๐Ÿ‡ฎ๐Ÿ‡ฉ |
| Format output | JSON โ€” `{"caption": "..."}` |
| `enable_thinking` saat training | `False` |
| EOS token | `<\|im_end\|>` (+ `<\|endoftext\|>`) |
| Precision | bfloat16 |

---

## Cara Pakai (Inference)

### Minimal (bfloat16, GPU)

```python
import torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForImageTextToText

REPO = "Adicandra/Qwen3.5-4B-ImageCaptioning-LoRA"

processor = AutoProcessor.from_pretrained(REPO)
model = AutoModelForImageTextToText.from_pretrained(
    REPO,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
model.eval()
```

### System Prompt (WAJIB sama dengan training)

```python
SYSTEM_PROMPT = (
    "Annotator dataset image captioning. Tulis caption Bahasa Indonesia yang deskriptif.\n\n"
    "Aturan:\n"
    "- Deskripsikan subjek utama, detail visual (warna, posisi, atribut), dan latar belakang.\n"
    "- Jika gambar mengandung teks penting (meme, infografis, berita, poster), sertakan isi teksnya dalam caption.\n"
    "- KHUSUS UNTUK GAMBAR MEME: Analisis dan jelaskan makna sarkasme, ironi, atau humor yang terkandung di dalamnya jika ada.\n"
    "- Panjang caption fleksibel: 2-3 kalimat untuk gambar biasa, lebih panjang jika ada teks/informasi penting atau sarkasme.\n"
    "- Hanya deskripsikan yang terlihat (serta konteks humor/sarkasme jika itu meme). Jangan tebak identitas/nama. Jangan awali dengan \"gambar ini menunjukkan\".\n"
    '- Output: Harus berupa JSON valid dengan format: {"caption": "isi caption disini"}'
)
```

### Render Chat + Generate

```python
import json, re

USER_PROMPT = "Buatkan caption deskriptif untuk gambar ini."
MAX_IMAGE_SIZE = (560, 560)

# โ”€โ”€ Load & resize gambar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
img = Image.open("path/to/image.jpg").convert("RGB")
img.thumbnail(MAX_IMAGE_SIZE, Image.Resampling.LANCZOS)

# โ”€โ”€ Susun messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user",   "content": [
        {"type": "image"},
        {"type": "text", "text": USER_PROMPT},
    ]},
]

# โ”€โ”€ Render template (enable_thinking=False wajib) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
text = processor.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
    enable_thinking=False,
)

# โ”€โ”€ Tokenize โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
inputs = processor(text=[text], images=[[img]], return_tensors="pt").to(model.device)
input_len = inputs.input_ids.shape[1]

# โ”€โ”€ EOS tokens โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
im_end_id = processor.tokenizer.convert_tokens_to_ids("<|im_end|>")
eot_id    = processor.tokenizer.convert_tokens_to_ids("<|endoftext|>")
eos_ids   = list({im_end_id, eot_id} - {-1})

# โ”€โ”€ Generate โ€” greedy (deterministik) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with torch.no_grad():
    out = model.generate(
        **inputs,
        max_new_tokens=256,
        do_sample=False,
        use_cache=True,
        eos_token_id=eos_ids,
        pad_token_id=processor.tokenizer.pad_token_id,
    )

raw = processor.tokenizer.decode(out[0, input_len:], skip_special_tokens=True)

# โ”€โ”€ Parse JSON output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def extract_caption(raw: str):
    cleaned = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
    try:
        obj = json.loads(cleaned)
        if isinstance(obj, dict) and "caption" in obj:
            return obj["caption"], "valid"
    except Exception:
        pass
    m = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
    if m:
        try:
            obj = json.loads(m.group(0))
            if isinstance(obj, dict) and "caption" in obj:
                return obj["caption"], "recovered"
        except Exception:
            pass
    return cleaned, "invalid"

caption, status = extract_caption(raw)
print(f"[{status}] {caption}")
```

### Generate dengan Sampling

```python
# Tambahkan argumen berikut ke model.generate() untuk sampling:
with torch.no_grad():
    out = model.generate(
        **inputs,
        max_new_tokens=256,
        do_sample=True,
        temperature=0.7,
        top_p=0.8,
        top_k=20,
        min_p=0.0,
        repetition_penalty=1.2,
        use_cache=True,
        eos_token_id=eos_ids,
        pad_token_id=processor.tokenizer.pad_token_id,
    )
```

---

## Format Output

Model selalu menghasilkan JSON valid:

```json
{"caption": "Seekor kucing oranye sedang duduk di atas meja kayu berwarna cokelat, menatap ke arah kanan frame dengan mata setengah terpejam. Latar belakang berupa dinding putih yang sedikit buram."}
```

Jika gambar mengandung teks (meme, infografis, poster), teks tersebut akan disertakan dalam caption beserta konteks humor/sarkasme bila relevan.

---

## Diagnostik: Cek Apakah `<think>` Muncul

Model di-train dengan `enable_thinking=False`. Pastikan argumen tersebut selalu diteruskan ke `apply_chat_template`. Jika token `<think>` masih muncul di output:

1. Pastikan `enable_thinking=False` di `apply_chat_template`.
2. Gunakan `suppress_tokens=[think_id]` saat `generate()` sebagai fallback.
3. Fungsi `extract_caption()` di atas sudah mem-strip blok `<think>...</think>` secara otomatis.

---

## Keterbatasan & Catatan

- Model hanya menghasilkan caption **Bahasa Indonesia**; tidak dirancang untuk bahasa lain.
- Untuk input resolusi tinggi (> 560ร—560), model tetap berfungsi tetapi performa optimal pada thumbnail 560ร—560.
- **Jangan tebak identitas/nama orang** dari gambar โ€” sesuai aturan system prompt.
- Evaluasi formal (CIDEr, BLEU, METEOR) belum tersedia; performa diukur secara kualitatif.

---

## Lisensi

Mengikuti lisensi base model: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).

---

## Citation

Jika menggunakan model ini dalam penelitian, silakan sitasi base model Qwen3.5 dan sebutkan repo ini sebagai fine-tune untuk image captioning Bahasa Indonesia.