File size: 8,309 Bytes
c522400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
215
"""
finetune.py
===========
Fine-tuning con LoRA/QLoRA di un LLM su dataset.jsonl.
Usa TRL SFTTrainer β€” funziona su GPU consumer (4GB+) o su Google Colab gratuito.

Modello base consigliato (leggero, multilingue, italiano):
    Qwen/Qwen2.5-0.5B-Instruct   ← 0.5B param, ottimo su CPU/GPU piccola
    Qwen/Qwen2.5-1.5B-Instruct   ← 1.5B param, qualitΓ  migliore
    google/gemma-2-2b-it          ← 2B param, ottimo in italiano

Uso:
    # Training completo
    python finetune.py --dataset dataset.jsonl --hf-repo TUO_USERNAME/generai-model

    # Solo dry-run (verifica che funzioni)
    python finetune.py --dataset dataset.jsonl --dry-run
"""

import argparse
import json
import os
import sys
from pathlib import Path

# ── Dipendenze opzionali β€” errore chiaro se mancano ───────────────────────────
def _check_deps():
    missing = []
    for pkg in ["transformers", "trl", "peft", "datasets", "torch"]:
        try:
            __import__(pkg)
        except ImportError:
            missing.append(pkg)
    if missing:
        print("❌ Dipendenze mancanti per il training:")
        print(f"   pip install {' '.join(missing)}")
        print("   (oppure: pip install -r requirements-train.txt)")
        sys.exit(1)

_check_deps()

import torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer, SFTConfig


DEFAULT_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
OUTPUT_DIR    = "./generai-finetuned"


def load_dataset_jsonl(path: str) -> Dataset:
    rows = []
    with open(path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                rows.append(json.loads(line))
    if not rows:
        print(f"❌ Dataset vuoto: {path}")
        sys.exit(1)
    print(f"[dataset] {len(rows)} esempi caricati da {path}")
    return Dataset.from_list(rows)


def apply_chat_template(examples, tokenizer):
    """Applica il chat template del modello ai messaggi."""
    texts = []
    for msgs in examples["messages"]:
        text = tokenizer.apply_chat_template(
            msgs,
            tokenize=False,
            add_generation_prompt=False,
        )
        texts.append(text)
    return {"text": texts}


def build_model_and_tokenizer(model_name: str, use_4bit: bool):
    print(f"[model] Caricamento: {model_name}")

    tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    bnb_config = None
    if use_4bit:
        try:
            import bitsandbytes  # noqa
            bnb_config = BitsAndBytesConfig(
                load_in_4bit=True,
                bnb_4bit_quant_type="nf4",
                bnb_4bit_use_double_quant=True,
                bnb_4bit_compute_dtype=torch.bfloat16,
            )
            print("[model] Quantizzazione 4-bit attivata (QLoRA)")
        except ImportError:
            print("[model] bitsandbytes non disponibile β€” uso fp16 standard")
            use_4bit = False

    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        quantization_config=bnb_config,
        torch_dtype=torch.bfloat16 if not use_4bit else None,
        device_map="auto",
        trust_remote_code=True,
    )
    return model, tokenizer


def build_lora_config() -> LoraConfig:
    return LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=16,               # rank β€” piΓΉ alto = piΓΉ parametri addestrati
        lora_alpha=32,
        lora_dropout=0.05,
        bias="none",
        target_modules="all-linear",
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--dataset",   required=True,          help="Path al file dataset.jsonl")
    parser.add_argument("--model",     default=DEFAULT_MODEL,  help=f"Modello base HuggingFace (default: {DEFAULT_MODEL})")
    parser.add_argument("--hf-repo",   default="",             help="Username/nome-repo su HuggingFace per upload automatico")
    parser.add_argument("--epochs",    type=int,   default=3,  help="Numero di epoche (default: 3)")
    parser.add_argument("--batch",     type=int,   default=2,  help="Batch size (default: 2)")
    parser.add_argument("--lr",        type=float, default=2e-4, help="Learning rate (default: 2e-4)")
    parser.add_argument("--max-len",   type=int,   default=512, help="Lunghezza massima token (default: 512)")
    parser.add_argument("--4bit",      dest="use_4bit", action="store_true", help="Usa QLoRA 4-bit (serve bitsandbytes)")
    parser.add_argument("--dry-run",   action="store_true",    help="Verifica configurazione senza addestrare")
    args = parser.parse_args()

    if not Path(args.dataset).exists():
        print(f"❌ File non trovato: {args.dataset}")
        print("   Esegui prima: python export_dataset.py")
        sys.exit(1)

    # ── Carica dataset ─────────────────────────────────────────────────────────
    raw_ds = load_dataset_jsonl(args.dataset)

    # ── Carica modello ─────────────────────────────────────────────────────────
    model, tokenizer = build_model_and_tokenizer(args.model, args.use_4bit)

    # ── Applica chat template ──────────────────────────────────────────────────
    ds = raw_ds.map(
        lambda x: apply_chat_template(x, tokenizer),
        batched=True,
        remove_columns=raw_ds.column_names,
    )
    print(f"[dataset] Esempio tokenizzato:\n{ds[0]['text'][:300]}...\n")

    if args.dry_run:
        print("βœ… Dry-run completato. Configurazione corretta.")
        print(f"   Modello   : {args.model}")
        print(f"   Dataset   : {len(ds)} esempi")
        print(f"   Output    : {OUTPUT_DIR}")
        return

    # ── LoRA ───────────────────────────────────────────────────────────────────
    lora_cfg = build_lora_config()
    model = get_peft_model(model, lora_cfg)
    model.print_trainable_parameters()

    # ── Training config ────────────────────────────────────────────────────────
    sft_cfg = SFTConfig(
        output_dir=OUTPUT_DIR,
        num_train_epochs=args.epochs,
        per_device_train_batch_size=args.batch,
        gradient_accumulation_steps=4,
        learning_rate=args.lr,
        lr_scheduler_type="cosine",
        warmup_ratio=0.1,
        logging_steps=10,
        save_steps=50,
        save_total_limit=2,
        bf16=torch.cuda.is_available(),
        fp16=False,
        max_seq_length=args.max_len,
        dataset_text_field="text",
        report_to="none",
        push_to_hub=bool(args.hf_repo),
        hub_model_id=args.hf_repo or None,
    )

    trainer = SFTTrainer(
        model=model,
        train_dataset=ds,
        args=sft_cfg,
        tokenizer=tokenizer,
    )

    print("\nπŸš€ Avvio training...\n")
    trainer.train()

    # ── Salva ──────────────────────────────────────────────────────────────────
    trainer.save_model(OUTPUT_DIR)
    tokenizer.save_pretrained(OUTPUT_DIR)
    print(f"\nβœ… Modello salvato in: {OUTPUT_DIR}")

    if args.hf_repo:
        print(f"⬆️  Upload su HuggingFace: https://huggingface.co/{args.hf_repo}")
        trainer.push_to_hub()
    else:
        print("\nPer caricare su HuggingFace:")
        print(f"  huggingface-cli upload TUO_USERNAME/generai-model {OUTPUT_DIR}")
        print("\nPer usarlo in GenerAI:")
        print(f"  Imposta HF_MODEL={args.hf_repo or 'TUO_USERNAME/generai-model'} in .env")


if __name__ == "__main__":
    main()