Tutorial Addestramento IA: esempio di fine-tuning di TinyLlama per NL→Bash
Un proof-of-concept per “demitizzare” la magia dietro il machine learning.
Con questo tutorial voglio accompagnarti passo-passo alla creazione di un assistente IA per la scrittura di comandi Bash, assumendo che tu sappia già muoverti in Linux ma non abbia mai messo mano a Hugging Face o al fine-tuning. L’obiettivo è demitizzare: vedere che dietro l’apparente magia dell'IA c’è una pipeline fatta di comandi Bash, qualche script Python e tanta pazienza.
Proof-of-concept – Alla fine otterremo un modello (“TinyBash”) che a volte sa proporre comandi ragionevoli. Non è pronto per la produzione: il dataset è piccolo e il training brevissimo. Ma noteremo comunque il salto da risposte totalmente casuali a output che cominciano ad assomigliare a Bash.
Per esser chiari fin da subito... funziona?
1. Dimensioni e disparità di scala
TinyBash parte da TinyLlama-1.1 B (≈1,1 mld di parametri), che con la quantizzazione a 4-bit occupa 0,4 GB e allena solo ~7 MB di delta-LoRA. Anche il più piccolo Code Llama 7 B è già 6 volte più grande; le varianti da 13 B, 34 B e 70 B arrivano a 12 volte, 31 volte e ≈64 volte i parametri di TinyBash. I modelli proprietari di casa OpenAI — ChatGPT Codex (codex-1) e ChatGPT o3 — non rivelano la taglia, ma le stime parlano di centinaia di miliardi → oltre 150-200 volte rispetto al nostro prototipo. Ne derivano reti con memoria di lungo contesto, ragionamento di livello superiore e, soprattutto, output di codice molto più affidabili.
2. Cosa cambia con più hardware
Su una GPU modesta (4 GB) usiamo batch 2, LoRA r=8 e 2 epoche: abbastanza per “vedere” il processo. Con 16 GB di VRAM puoi portare il batch a 4, raddoppiare r, togliere la quantizzazione o salire a 8-bit, aggiungere scheduler di LR-decay e spingere a 5-10 epoche. Su A100/H100, le big-tech ri-addestrano il modello intero in FP16/BF16, applicano RLHF o RLAIF e orchestrano tutto con pipeline MLOps automatizzate; il flusso concettuale è lo stesso, cambiano solo scala e controlli.3. Considerazioni sui dati
Noi usiamo il corpus di addestramento NL2Bash pubblicato dall’Università di Washington nella ricerca scientifica "NL2Bash: A Corpus and Semantic Parser for Natural Language Interface to the Linux Operating System" (vedi PDF). Esso conta 9 305 coppie NL → Bash (di cui 8090 nel training set), copre 102 utility Bash e 206 flag — materiale di qualità, ma pur sempre sei ordini di grandezza in meno (cioè milioni di volte più piccolo) rispetto ai miliardi di token di codice usati da Code Llama o Codex. Riguardo alla qualità del corpus di addestramento, misurata a campione, sta attorno all’85%, quindi gli errori presenti sono comunque pochi rispetto alle oltre novemila coppie totali.4. Aspettative realistiche
Nella ricerca su NL2Bash, l’Università di Washington ha usato un modello sequence-to-sequence con meccanismo di copia (ST-CopyNet): l’encoder legge la domanda in inglese, il decoder RNN genera il comando e può copiarne parti (file, pattern) direttamente dall’input; sui 606 esempi del test set – mai visti a training – questo modello ha azzeccato la struttura nel 49% dei casi e il comando completo nel 36%, pur disponendo di 9 305 coppie NL→Bash (8 090 di train). TinyBash, invece, parte da TinyLlama-1.1 B (1,1 mld di parametri) e ritocca solo 7 MB di delta-LoRA quantizzati a 4 bit: è un’architettura diversa, molto più grande ma anche compressa, quindi quel 36 % non è un tetto; l’unico raffronto corretto sarebbe far girare TinyBash sullo stesso test set, dove il risultato dipenderà da batch, learning-rate, epoche, rango LoRA e altri iper-parametri di fine-tuning. Nel nostro run di prova, su quattro prompt mostrati a fine tutorial TinyBash centra bene 1-2 risposte su 4 (ad es. “List all open TCP ports” è giusta, “Show total RAM” è sbagliata): numeri coerenti con la difficoltà del compito e adeguati a un prototipo didattico che serve a illustrare il processo, non a sostituire uno strumento di produzione.5. Quindi… funziona?
Come demo didattica, sì: dopo 2 epoche TinyBash passa da output casuali a comandi spesso plausibili, mostrando passo-passo la pipeline reale di un fine-tuning. Ma un 1,1 B a 4-bit resta fragile: errori di sintassi, flag inventati e “allucinazioni” sono frequenti. Per applicazioni reali in ambienti di produzione servono modelli grandi, molti più dati e rigorosi controlli. In altre parole, la magia è identica, scala e budget no — e questo tutorial ti fa toccare con mano il meccanismo prima di affrontare mostri da cento miliardi di parametri...
Per il testing, ho usato:
Component | Dettaglio |
---|---|
OS | Linux Mint 22 “Virginia” (kernel 6.8) |
GPU | NVIDIA GeForce GTX 1050 (4 GiB VRAM, arch 6.1 “Pascal”) |
RAM | 16 GB |
Driver | nvidia-driver 535 + CUDA 12.0.140 |
Python | 3.12.3 (in venv ) |
Prima di cominciare, ti propongo un breve glossario tecnico essenziale:
-
Modello (Model)
È l’insieme di parametri numerici (pesi + bias) e istruzioni matematiche che trasformano un input (per esempio una frase) in un output (per esempio un comando Bash). Un modello rappresenta matematicamente una rete neurale artificiale: i parametri sono i “numeri liberi” che il training può ottimizzare. Quando diciamo TinyLlama 1.1 B indichiamo proprio che il modello contiene circa 1,1 miliardi di parametri. In questo tutorial partiamo da tale modello di base e creiamo una variante fine-tuned chiamata TinyBash. -
Pesi (Weights)
I valori — solitamente nell’ordine dei miliardi — che il modello apprende durante l’addestramento. Determinano “che cosa ha imparato” e vengono salvati in file binari. Possiamo immaginarli come la “forza” delle sinapsi fra i neuroni artificiali: ogni peso stabilisce quanto l’attivazione di un neurone influenzi quello successivo nella rete. (I bias sono parametri speciali che spostano le attivazioni, ma rientrano anch’essi nel conteggio totale dei parametri.) -
Fine-tuning
Operazione con cui si ri-allena un modello pre-esistente su un nuovo insieme di dati per specializzarlo in un compito (nel nostro caso, NL → Bash). Il ri-addestramento avviene per un numero limitato di epoche: un’epoca corrisponde a un passaggio completo dell’intero dataset attraverso il modello (forward + back-propagation su ogni esempio). Più epoche significano più opportunità di apprendere, ma anche maggior rischio di overfitting. In questo tutorial useremo 2 epoche, sufficienti a dimostrare il processo senza richiedere tempi di calcolo eccessivi. -
LoRA (Low-Rank Adaptation)
Tecnica di Parameter-Efficient Fine-Tuning: invece di riscrivere tutti i pesi, aggiunge piccole matrici “delta” (rank basso) che si sommano al modello base. Riduce VRAM, tempo e storage — nel nostro script alleniamo appena ~7 MB di parametri. -
Quantizzazione (Quantize)
Conversione dei pesi da 16 / 32 bit a formati a bassa precisione (8-, 4-, o 2-bit) per risparmiare memoria e accelerare l’inferenza — cioè la fase in cui il modello, già addestrato, viene eseguito per generare una risposta a un nuovo input. In pratica, l’inferenza è l’uso del modello “in produzione”, distinto dal training. Con la quantizzazione a 4 bit che noi useremo, il footprint di TinyLlama scende da ~4 GB a ~0,4 GB, rendendo più veloce (e possibile su hardware modesto) il calcolo delle risposte. -
GGUF
Contenitore binario della famiglia GGML pensato per modelli quantizzati; include pesi, tokenizer e metadati. Ollama (e molte altre tool-chain) lo carica in un’unica syscall. -
Tokenizer
Modulo che segmenta il testo in unità (“token”) comprensibili al modello. Per TinyLlama è basato su SentencePiece; lo usiamo anche quando esportiamo in GGUF. -
Dataset
Collezione di esempi di addestramento. Qui utilizziamo NL2Bash e lo trasformiamo in stile Alpaca. -
Alpaca template
Formato di prompt a tre ruoli (<|system|>
,<|user|>
,<|assistant|>
) introdotto dall’esperimento Stanford Alpaca. Mantenerlo identico tra training e inferenza assicura coerenza nelle risposte.
Ci sarebbero molti altri termini da spiegare nel dettaglio per comprendere il codice che useremo, ma non ci addentreremo nelle spiegazioni teoriche. Il nostro focus è solo su del codice pronto per essere testato. Detto ciò, installiamo questi pacchetti:
sudo apt update sudo apt install build-essential git python3-venv cmake wget curl unzip pkg-config libcurl4-openssl-dev zlib1g-dev libopenblas-dev libomp-dev
Ollama è un runtime leggerissimo che esegue modelli quantizzati in formato GGUF:
curl -fsSL https://ollama.com/install.sh | sh
Prepariamo un virtual-env così non sporchiamo il sistema:
python3 -m venv ~/llm_env source ~/llm_env/bin/activate pip install --upgrade pip pip install unsloth bitsandbytes transformers peft datasets accelerate sentencepiece
Il primo script usa unsloth per caricare e quantizzare al volo il modello base.
Creiamo il file download.py
, rendiamolo eseguibile e lanciamolo:
#!/usr/bin/env python3 from unsloth import FastLanguageModel model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" model, tokenizer = FastLanguageModel.from_pretrained( model_name = model_name, device_map = "auto", load_in_4bit = True, )
Quando lo script termina, i pesi sono già a disposizione del training e non dobbiamo riscaricarli.
Useremo il dataset "NL2Bash: A Corpus and Semantic Parser for Natural Language Interface to the Linux Operating System", pubblicato nel 2018 da Xi Victoria Lin, Chenglong Wang, Luke Zettlemoyer, and Michael D. Ernst. Questi dati di addestramento sono composti di 8090 coppie di English NL (Natural Language) → Bash.
Il formato originale però non è adatto a un modello conversazionale, quindi lo riscriviamo in stile Alpaca: system / user / assistant.
Creiamo e lanciamo nl2bash_alpaca.py
:
#!/usr/bin/env python3 from datasets import load_dataset ds = load_dataset("jiacheng-ye/nl2bash", split="train") # uniforma in stile Alpaca def to_alpaca(example): return { "instruction": example["nl"], "output": example["bash"], "system": "Return ONLY valid Bash, no prose." } alpaca_ds = ds.map(to_alpaca).shuffle(seed=42) alpaca_ds.save_to_disk("nl2bash_alpaca")
Siamo arrivati alla parte più impegnativa. Creiamo e lanciamo train_tinybash.py per eseguire l'addestramento. Assicuriamoci che il nostro computer abbia una ventilazione efficiente, perché lavorerà al massimo per alcune ore. E' una buona idea anche monitorare la temperatura interna di CPU e GPU:
#!/usr/bin/env python import torch, json, os from unsloth import FastLanguageModel from unsloth.chat_templates import get_chat_template from trl import SFTTrainer from transformers import TrainingArguments, DataCollatorForSeq2Seq from datasets import load_from_disk # ── Parametri principali ────────────────────────────────────────────────────── MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" MAX_LEN = 2048 BATCH = 2 # con 4 GB VRAM GRAD_ACC = 8 # → eff. batch 16 LR = 5e-5 EPOCHS = 2 OUT_DIR = "tinybash_lora" GGUF_DIR = "tinybash_gguf" QUANT_METHOD = "q4_k_m" # migliore trade-off CPU # ── 1. Carica il modello base in 4-bit ──────────────────────────────────────── model, tok = FastLanguageModel.from_pretrained( model_name = MODEL_NAME, max_seq_length = MAX_LEN, load_in_4bit = True, device_map = "auto", ) # 1-bis. APPLICA IL TEMPLATE CON {SYSTEM} tok = get_chat_template( tok, chat_template = "alpaca", # oppure il tuo template custom map_eos_token = True) # allinea l’EOS # ── 1-ter. Funzione che combina system, instruction, output ──────────────── def format_unsloth(example): """ Converte *N* righe del batch in *N* prompt formattati. Se il dataset chiama la funzione su una sola riga (liste lunghe 1) funziona lo stesso. """ bos = tok.bos_token or "" eos = tok.eos_token or "" # example["instruction"] è una lista di lunghezza batch_size formatted_batch = [] for sys, inst, outp in zip(example["system"], example["instruction"], example["output"]): formatted_batch.append( f"{bos}<|system|>\n{sys}{eos}" f"<|user|>\n{inst}{eos}" f"<|assistant|>\n{outp}{eos}" ) return formatted_batch # 🔸 stessa lunghezza del batch # ── 2. Configura LoRA minimalista ───────────────────────────────────────────── model = FastLanguageModel.get_peft_model( model, r = 8, target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"], lora_alpha = 16, lora_dropout = 0, bias = "none", # opzionale use_gradient_checkpointing = "unsloth", random_state = 42, max_seq_length = MAX_LEN, ) # ── 3. Dataset già preparato (vedi Parte 1) ─────────────────────────────────── data = load_from_disk("nl2bash_alpaca") # ── 4. Iper-parametri Trainer ──────────────────────────────────────────────── args = TrainingArguments( output_dir = OUT_DIR, per_device_train_batch_size = BATCH, gradient_accumulation_steps = GRAD_ACC, learning_rate = LR, num_train_epochs = EPOCHS, logging_steps = 20, save_strategy = "epoch", bf16 = False, # NVIDIA GeForce GTX 1050 non supporta BF16 fp16 = True, # usa FP16, va bene su Pascal ) trainer = SFTTrainer( model = model, train_dataset = data, tokenizer = tok, data_collator = DataCollatorForSeq2Seq(tok), max_seq_length = MAX_LEN, args = args, formatting_func = format_unsloth, ) trainer.train()
Tempo di training – sulla GTX 1050 servono ~3 h per due epoche.
Dopo l’allenamento, i pesi LoRA sono in tinybash_lora/checkpoint-NNNN
. Dobbiamo usare il valore NNNN
più grande.
Questo è il file merge_and_export.py
, con il quale generiamo il modello addestrato in tinybash_gguf/unsloth.Q4_K_M.gguf
:
#!/usr/bin/env python3 from pathlib import Path from unsloth import FastLanguageModel from unsloth.chat_templates import get_chat_template from peft import PeftModel MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" OUT_DIR = Path(__file__).parent / "tinybash_lora/checkpoint-1010" # path locale GGUF_DIR = "tinybash_gguf" QUANT = "q4_k_m" print("🔹 Carico base 4-bit…") model, tok = FastLanguageModel.from_pretrained( model_name = MODEL_NAME, load_in_4bit = True, device_map = "auto", max_seq_length = 2048, ) tok = get_chat_template(tok, chat_template="alpaca", map_eos_token=True) print("🔹 Carico adapter LoRA…") model = PeftModel.from_pretrained( model, str(OUT_DIR), local_files_only=True ) print("🔹 Salvo GGUF fuso…") model.save_pretrained_gguf( GGUF_DIR, tok, quantization_method=QUANT ) print("GGUF creato in", Path(GGUF_DIR).resolve())
Creiamo la cartella model
per ollama:
mkdir model
cd model
cp ../tinybash_gguf/unsloth.Q4_K_M.gguf ./tinybash.q4_k_m.gguf
Questo è il file Modelfile
da mettere dentro la cartella model
:
# Modelfile
FROM ./tinybash.q4_k_m.gguf
# risposta secca e ripetibile
PARAMETER temperature 0.1
PARAMETER stop "</s>"
TEMPLATE """{{ if .System }}{{ .System }}
{{ end }}### Instruction:
{{ .Prompt }}
### Response:
{{ .Response }}</s>
"""
SYSTEM "You are TinyBash. Return ONLY valid Bash commands or short shell scripts, with no extra commentary."
Infine, registriamo il modello con ollama:
ollama rm tinybash # Opzionale, serve per rimuovere il modello se già precedentemente creato
ollama create tinybash -f Modelfile
Creiamo il file /usr/local/bin/tinybash:
che fa da wrapper:
#!/usr/bin/env bash
# tinybash: wrapper per ollama run tinybash
# Usage: tinybash <prompt…>
if [ $# -eq 0 ]; then
echo "Usage: tinybash <prompt…>"
exit 1
fi
prompt="$*"
echo "$prompt" | ollama run tinybash
Ora verifichiamo la differenza fra TinyLlama base e TinyBash addestrato:
$ PROMPT="List the 10 largest files in the current directory" $ echo "$PROMPT" | ollama run tinyllama Here is a list of the 10 largest files in the current directory as of this writing: 1. "C:\Users\USER\Desktop\file1.txt" - 37 MB (2022-08-24) 2. "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\mypackage\__init__.py" - 58 MB (2022-08-29) 3. "C:\Users\USER\Downloads\file2.txt" - 67 MB (2022-08-24) 4. "C:\Users\USER\Documents\folder1\file3.pdf" - 73 MB (2022-08-25) 5. "C:\Program Files\notepad++\plugins\pugjawoo\PugJawOO.dll" - 96 MB (2022-08-24) 6. "C:\Users\USER\Desktop\folder2\file4.docx" - 113 MB (2022-08-25) 7. "C:\Program Files\Notepad++\plugins\pugjawoo\PugJawOO.dll" - 119 MB (2022-08-24) 8. "C:\Users\USER\Downloads\folder3\file5.docx" - 147 MB (2022-08-26) 9. "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\mypackage\__init__.py" - 154 MB (2022-08-29) 10. "C:\Program Files\notepad++\plugins\pugjawoo\PugJawOO.dll" - 176 MB (2022-08-24) Note that the size of each file may vary depending on its content and other factors such as encoding or compression methods used. $ echo "$PROMPT" | ollama run tinybash ls | sort -nr | head -n 10
Altri esempi:
Prompt | TinyBash risponde |
---|---|
“List all open TCP ports” | netstat -tlnp |
“Show total RAM” | (bug) “Total amount of RAM is 1024 MB” |
“Add execute to permissions of all dirs in $HOME” | chmod 755 ~/* |
In questo modo, osserviamo che le risposte sono concettualmente più coerenti rispetto al modello base, ma la precisione è ancora bassa.
Il risultato non è production-ready, ma dimostra che la magia è in realtà un processo replicabile con gli strumenti giusti.
Per futuro riferimento, qui c'è una copia dei files che ho usato, comprensivi di tutto: TinyBash.zip. Questo zip contiene gli script Python e Bash del tutorial, il dataset NL2Bash già convertito in formato Alpaca, i checkpoint LoRA e il modello finale fuso in GGUF a 4-bit con il relativo Modelfile, la cartella “model” pronta per ollama create, l’intera tool-chain di llama.cpp nella versione testata, oltre a cache, tokenizer e configurazioni indispensabili per ricostruire l’esperimento anche offline. Ad ogni modo, se seguirai questo tutorial punto per punto, non avrai bisogno di scaricare alcun file dal mio blog.
Buon hacking,
22 maggio 2025