Vai al contenuto principale

Come fare fine-tuning di FunctionGemma: guida passo passo

Impara a fare il fine-tuning di FunctionGemma in meno di 10 minuti usando le GPU gratuite di Kaggle: dalla preparazione del dataset e valutazione baseline all’addestramento e alla validazione post fine-tuning.
· 13 min leggi

Scopri con l'IA

In questo tutorial esploreremo FunctionGemma, un modello linguistico leggero di Google DeepMind per il function calling, e ti spiegherò perché il fine-tuning è essenziale per ottenere un uso degli strumenti affidabile e conforme agli schemi. 

Inizieremo configurando un ambiente Kaggle con GPU, quindi caricheremo e prepareremo sia il dataset sia il modello base FunctionGemma.

Successivamente, eseguiremo valutazioni pre fine-tuning per stabilire una baseline in termini di selezione degli strumenti e accuratezza delle chiamate di funzione. Faremo poi il fine-tuning di FunctionGemma con training supervisionato e valuteremo le prestazioni dopo l’addestramento per verificare che il fine-tuning sia stato applicato correttamente.

Se cerchi esercizi pratici per imparare il fine-tuning, ti consiglio di dare un’occhiata al corso Fine-Tuning with Llama 3. 

Che cos’è FunctionGemma? 

FunctionGemma è una versione specializzata del modello open Gemma 3 270M di Google, progettata specificamente per il function calling e l’uso di tool, piuttosto che per la conversazione generale.

Utilizza la stessa architettura di Gemma 3 ma include un formato dedicato e un focus di training che le consente di generare output strutturati che rappresentano chiamate di funzione.

FunctionGemma viene rilasciata come modello base che gli sviluppatori possono sottoporre a fine-tuning per casi d’uso specifici. Le sue dimensioni e il suo design la rendono leggera, efficiente e distribuibile su dispositivi con risorse limitate, come laptop e hardware edge.

Perché fare il fine-tuning di FunctionGemma?

Sebbene FunctionGemma sia addestrata per il function calling, modelli di queste dimensioni rendono al meglio quando vengono specializzati tramite fine-tuning su dati specifici per il task. 

Il fine-tuning aiuta il modello a imparare pattern stabili per selezionare la funzione corretta da un set di tool e per formattare correttamente gli argomenti corrispondenti in casi d’uso reali. 

Questo processo porta a output strutturati più coerenti e prevedibili, migliorando l’affidabilità nei flussi di lavoro pratici.

Fine-tuning di FunctionGemma: guida passo passo

Vediamo i passaggi necessari per fare il fine-tuning di FunctionGemma. 

1. Configurare un ambiente Kaggle

Inizia andando su Kaggle e crea un nuovo notebook. Una volta aperto, individua il pannello Session options sulla destra e imposta l’Accelerator su GPU (T4 ×2). 

Questo abilita l’accelerazione GPU per la sessione e ti consente di fare il fine-tuning del modello usando le risorse di calcolo gratuite di Kaggle.

Selezionare la GPU per la sessione Kaggle.

Dopo aver abilitato la GPU, crea una nuova cella di codice ed esegui il seguente comando per installare tutte le dipendenze Python richieste. Il kernel potrebbe impiegare un po’ ad avviarsi; in seguito i pacchetti verranno installati automaticamente.

%pip -q install -U datasets accelerate trl kagglehub sentencepiece huggingface_hub tqdm evaluate jiwer

Successivamente, aggiungi in modo sicuro il tuo access token di Hugging Face. 

Dal menu in alto, apri Add-ons → Secrets, quindi clicca su Add Secret. Imposta il nome del secret su HUGGINGFACE_TOKEN e incolla la tua API key di Hugging Face come valore. 

Usare Kaggle Secrets è più sicuro che inserire hard-coded variabili d’ambiente o token direttamente nel notebook.

Impostare il secret di Hugging Face in Kaggle.

Una volta salvato il secret, Kaggle fornirà uno snippet per accedervi in modo programmatico. 

Usa il seguente codice per autenticarti con l’Hugging Face Hub:

from kaggle_secrets import UserSecretsClient
from huggingface_hub import login

hf_token = UserSecretsClient().get_secret("HUGGINGFACE_TOKEN")
if not hf_token:
    raise ValueError("HUGGINGFACE_TOKEN not found in Kaggle Secrets.")
login(token=hf_token)

Dopo l’autenticazione, potrai accedere a modelli e dataset con accesso controllato, nonché pubblicare il tuo modello FunctionGemma con fine-tuning sull’Hugging Face Hub per la condivisione e il riutilizzo.

2. Caricare il dataset Hermes Reasoning Tool-Use

Il dataset Hermes Reasoning Tool-Use è un dataset aperto in inglese in un formato di ragionamento per l’uso di tool, con esempi strutturati progettati per addestrare e valutare i modelli sulla selezione di strumenti e sui task di function calling in stile JSON. 

Contiene decine di migliaia di esempi di ragionamento sull’uso di tool con prompt in linguaggio naturale e schemi di tool in un formato adatto al fine-tuning supervisionato.

In questo tutorial useremo un sottoinsieme del dataset per sperimentare più velocemente. 

Per prima cosa, definisci un seed casuale e il numero totale di esempi da usare. Poi suddividi i dati in split di training e valutazione:

from datasets import load_dataset

SEED = 40
N_TOTAL = 3000
N_EVAL  = 300
N_TRAIN = N_TOTAL - N_EVAL

Quindi, carica il dataset, mescolalo e crea i sottoinsiemi di train ed eval:

raw = load_dataset("interstellarninja/hermes_reasoning_tool_use", split="train")
raw = raw.shuffle(seed=SEED).select(range(N_TOTAL))
train_ds = raw.select(range(N_TRAIN))
eval_ds  = raw.select(range(N_TRAIN, N_TOTAL))

print("train:", len(train_ds), "eval:", len(eval_ds))

Download del dataset da Hugging Face

Questo ti dà 2.700 campioni di training e 300 di valutazione, sufficienti per dimostrare il fine-tuning mantenendo gestibili i requisiti di calcolo.

train: 2700 eval: 300

3. Caricare il modello e il processor di FunctionGemma

In questo tutorial usiamo KaggleHub per caricare il modello FunctionGemma direttamente nel notebook Kaggle. 

KaggleHub risolve automaticamente la posizione del modello e lo rende disponibile nell’ambiente del notebook, eliminando la necessità di download manuali da Hugging Face.

import kagglehub
from transformers import AutoProcessor, AutoModelForCausalLM

model_path = kagglehub.model_download("google/functiongemma/transformers/functiongemma-270m-it")

Dopo aver eseguito la cella qui sopra, i file del modello appariranno nella sezione Input del notebook Kaggle, a conferma che il modello è stato caricato correttamente ed è disponibile in locale.

Uso della libreria Kaggle Hub per accedere al modello functiongemma.

Una volta disponibile il modello, carichiamo sia il processor sia il modello nella memoria della GPU. 

Il processor gestisce i template di chat e la formattazione degli schemi dei tool, mentre il modello genera chiamate di funzione strutturate. 

Impostare device_map="auto" garantisce che il modello sia allocato sulla GPU disponibile, e dtype="auto" seleziona una precisione efficiente supportata dall’hardware.

processor = AutoProcessor.from_pretrained(model_path, device_map="auto")
model = AutoModelForCausalLM.from_pretrained(model_path, dtype="auto", device_map="auto")
tokenizer = processor.tokenizer if hasattr(processor, "tokenizer") else processor

print("dtype:", model.dtype, "| device:", model.device)

Questo conferma che FunctionGemma è caricato correttamente, posizionato sulla GPU e pronto per valutazione e fine-tuning.

dtype: torch.bfloat16 | device: cuda:1

4. Normalizzare le definizioni dei tool ed estrarre le gold function call

Prima del fine-tuning di FunctionGemma, dobbiamo standardizzare come sono rappresentati i tool ed estrarre in modo affidabile le chiamate di funzione corrette (gold) dal dataset. 

Il dataset Hermes Reasoning Tool-Use può archiviare le definizioni dei tool in più formati, quindi questo passaggio converte tutto in una struttura coerente che possiamo usare per il fine-tuning supervisionato.

Normalizzare il campo tools

Iniziamo normalizzando il campo tools. Nel dataset, tools può comparire come lista Python, come stringa JSON o potrebbe mancare del tutto. 

La funzione di supporto seguente converte tutte le rappresentazioni valide in una lista Python pulita e ignora in modo sicuro le voci malformate o vuote.

import re, json

def normalize_tools_field(tools):
    if tools is None:
        return []
    if isinstance(tools, list):
        return tools
    if isinstance(tools, str):
        s = tools.strip()
        if not s:
            return []
        try:
            parsed = json.loads(s)
            return parsed if isinstance(parsed, list) else []
        except Exception:
            return []
    return []

Normalizzare i tipi dei parametri

Poi normalizziamo i tipi dei parametri. Le definizioni dei tool in Hermes spesso usano annotazioni informali come str,int o tipi contenitore come List[str]. La funzione seguente converte questi in tipi JSON Schema validi e compatibili con il function calling di Hugging Face.

def _parse_hermes_type(t) -> dict:
    if t is None:
        return {"type": "string"}
    if isinstance(t, dict):
        return t if "type" in t else {"type": "object"}
    if isinstance(t, list):
        return {"type": "array"}
    if not isinstance(t, str):
        return {"type": "string"}

    t = t.strip()
    prim = {
        "str": "string", "string": "string",
        "int": "integer", "integer": "integer",
        "float": "number", "number": "number",
        "bool": "boolean", "boolean": "boolean",
        "dict": "object", "object": "object",
        "list": "array", "array": "array",
    }
    if t.lower() in prim:
        return {"type": prim[t.lower()]}

    m = re.match(r"List\[(.+)\]$", t)
    if m:
        return {"type": "array", "items": _parse_hermes_type(m.group(1).strip())}

    m = re.match(r"Dict\[(.+),\s*(.+)\]$", t)
    if m:
        return {"type": "object", "additionalProperties": _parse_hermes_type(m.group(2).strip())}

    return {"type": "string"}

Convertire i tool in stile Hermes in schemi di funzione Hugging Face

Usando queste utility, convertiamo le definizioni dei tool in stile Hermes in schemi Hugging Face {"type": "function", "function": {...}}. Questa funzione supporta due formati:

  • Stile Hermes per-argomento (in cui ogni parametro ha una piccola specifica propria)
  • Tool che già somigliano a uno schema JSON

Per il formato per-argomento, questa implementazione considera i parametri elencati come obbligatori se non viene fornita esplicitamente una lista di required.

def hermes_tools_to_hf_schema(tools_field):
    """
    Handles both:
    - Hermes per-arg style: {"parameters": {"x":{"type":"str"}, ...}}
    - Already-JSON-schema style: {"parameters":{"type":"object","properties":...,"required":[...]}}
    """
    hermes_tools = normalize_tools_field(tools_field)
    out = []

    for tool in hermes_tools:
        if not isinstance(tool, dict):
            continue

        name = tool.get("name")
        desc = tool.get("description", "")
        params = tool.get("parameters", {}) or {}

        # If params already look like JSON schema (best case)
        if isinstance(params, dict) and "type" in params and "properties" in params:
            json_schema_params = params
            if "required" not in json_schema_params:
                json_schema_params["required"] = []
        else:
            props, req = {}, []
            if isinstance(params, dict):
                for p_name, p_spec in params.items():
                    p_spec = p_spec or {}
                    if isinstance(p_spec, dict):
                        p_desc = p_spec.get("description", "")
                        p_type = p_spec.get("type", "str")
                    else:
                        p_desc, p_type = "", "str"

                    frag = dict(_parse_hermes_type(p_type))
                    if p_desc:
                        frag["description"] = p_desc
                    props[p_name] = frag
                    req.append(p_name)

            json_schema_params = {"type": "object", "properties": props, "required": req}

        out.append({
            "type": "function",
            "function": {
                "name": name,
                "description": desc,
                "parameters": json_schema_params,
            }
        })

    return out

Funzione di supporto

Definiamo un piccolo helper per recuperare gli schemi dei tool convertiti per ciascun esempio del dataset:

def get_tools_hf(ex):
    return hermes_tools_to_hf_schema(ex.get("tools"))

Estrarre la gold function call

Quindi estraiamo la chiamata al tool gold dalla conversazione. Le conversazioni Hermes possono contenere più turni, quindi analizziamo i blocchi <tool_call>{...}</tool_call> ed estraiamo il primo oggetto di tool call valido (nome + argomenti). Questo mantiene la supervisione semplice e coerente.

TOOL_CALL_RE = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL)

def extract_first_tool_call_obj(text: str):
    if not text:
        return None
    m = TOOL_CALL_RE.search(text)
    if not m:
        return None
    try:
        obj = json.loads(m.group(1))
        if isinstance(obj, dict) and "name" in obj and "arguments" in obj:
            return obj
    except Exception:
        return None
    return None

Allineare la tool call con il task dell’utente

Per allineare la tool call con il task corretto dell’utente, analizziamo la cronologia della conversazione, troviamo il messaggio utente che corrisponde al task del dataset e poi prendiamo la risposta dell’assistant immediatamente successiva. Se la risposta dell’assistant contiene una tool call, la restituiamo come etichetta gold.

def _role(turn):
    return (turn.get("from") or "").lower().strip()

def get_gold_tool_call_task_aligned(ex):
    task = (ex.get("task") or "").strip()
    conv = ex.get("conversations") or []
    if not conv:
        return None

    idx = None
    if task:
        for i, t in enumerate(conv):
            if _role(t) in ["human", "user"]:
                val = (t.get("value") or "").strip()
                if val == task or task in val or val in task:
                    idx = i
                    break
    if idx is None:
        for i in range(len(conv)-1, -1, -1):
            if _role(conv[i]) in ["human", "user"]:
                idx = i
                break
    if idx is None:
        return None

    for j in range(idx+1, len(conv)):
        if _role(conv[j]) in ["gpt", "assistant", "model"]:
            gold = extract_first_tool_call_obj(conv[j].get("value", ""))
            if gold:
                return gold
        if _role(conv[j]) in ["human", "user"]:
            break
    return None

Al termine di questo passaggio, ogni esempio utilizzabile del dataset ha:

  • Un set normalizzato di schemi dei tool (formato Hugging Face)
  • Una gold function call pulita e allineata al task
  • Una struttura coerente pronta per costruire campioni di fine-tuning supervisionato.

6. Creare campioni per il fine-tuning supervisionato del function calling

Ora dobbiamo convertirli in campioni di training per Supervised Fine-Tuning (SFT) nel formato esatto atteso da FunctionGemma, usando le funzioni di supporto di cui sopra. 

L’obiettivo è creare esempi in cui l’input contenga:

  • Un messaggio dello sviluppatore che attiva il function calling
  • Il task dell’utente
  • Le dichiarazioni dei tool (gli schemi)

E il target è una singola chiamata FunctionGemma strutturata in questo formato:

<start_function_call>call:TOOL_NAME{args:<escape>{...}<escape>}<end_function_call>

Step 1: Costruire righe pulite e utilizzabili

Iniziamo iterando sul dataset e filtrando tutti gli esempi che non possono essere usati per il training.

Questa funzione mantiene solo esempi in cui:

  • Esiste una gold tool call
  • Esiste una lista di tool valida
  • Il nome del tool gold compare nella lista dei tool
  • Gli argomenti sono un dizionario valido
  • Se il tool non ha campi obbligatori, gli argomenti vengono forzati a {} per evitare target rumorosi o incoerenti
from datasets import Dataset

def build_simple_rows(ds, max_rows=None):
    rows = []
    for ex in ds:
        # 1) task-aligned gold tool call
        gold = get_gold_tool_call_task_aligned(ex)
        if not gold:
            continue

        # 2) tools -> HF schema
        hf_tools = get_tools_hf(ex)
        if not hf_tools:
            continue

        # 3) build required_map: tool_name -> required fields
        required_map = {}
        for t in hf_tools:
            if t.get("type") == "function":
                fn = t.get("function", {})
                name = fn.get("name")
                req = (fn.get("parameters", {}) or {}).get("required", []) or []
                if name:
                    required_map[name] = req

        # 4) guard: gold tool must be in tool list
        tool_names = set(required_map.keys())
        if gold["name"] not in tool_names:
            continue

        # 5) force {} when no required params
        gold_args = gold.get("arguments", {})
        if not isinstance(gold_args, dict):
            gold_args = {}

        req = required_map.get(gold["name"], [])
        if len(req) == 0:
            gold_args = {}  # key fix

        rows.append({
            "user_content": ex.get("task", ""),
            "tool_name": gold["name"],
            "tool_arguments": json.dumps(gold_args, ensure_ascii=False),
            "hf_tools": hf_tools,
        })

        if max_rows and len(rows) >= max_rows:
            break

    return rows

Ora generiamo le righe di training e valutazione:

simple_train = build_simple_rows(train_ds, max_rows=N_TRAIN)
simple_eval  = build_simple_rows(eval_ds,  max_rows=N_EVAL)
print("usable train:", len(simple_train), "usable eval:", len(simple_eval))

Nel nostro caso, otteniamo 961 campioni di training e 109 campioni di valutazione sui 3.000 iniziali. Questo calo è atteso perché il filtro è severo. Rimuove esempi con tool call mancanti, definizioni di tool non valide, nomi di tool non corrispondenti o argomenti malformati.

usable train: 961 usable eval: 109

Questo step di pulizia è molto importante. Prima di applicarlo, il dataset conteneva campioni rumorosi e disallineati, che portavano a un comportamento instabile del function calling e a risultati scarsi anche dopo il fine-tuning. Dopo aver filtrato solo esempi di alta qualità e allineati al task, FunctionGemma diventa molto più coerente e i risultati del fine-tuning migliorano sensibilmente.

Step 2: Convertire ogni riga in un singolo campione di training testuale

Successivamente formattiamo ogni riga nella struttura prompt+target esatta richiesta da FunctionGemma. Usiamo apply_chat_template del processor per iniettare correttamente le dichiarazioni dei tool, quindi aggiungiamo la gold function call come target.

def format_row_as_text(row):
    messages = [
        {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
        {"role": "user", "content": row["user_content"]},
    ]
    prompt = processor.apply_chat_template(
        messages,
        tools=row["hf_tools"],
        add_generation_prompt=True,
        tokenize=False,
    )
    target = (
        f"<start_function_call>call:{row['tool_name']}"
        f"{{args:<escape>{row['tool_arguments']}<escape>}}"
        f"<end_function_call>"
    )
    return prompt + target

Ora convertiamo le righe in oggetti Dataset di Hugging Face:

train_text_ds = Dataset.from_list([{"text": format_row_as_text(r)} for r in simple_train])
eval_text_ds  = Dataset.from_list([{"text": format_row_as_text(r)} for r in simple_eval])

Step 3: Garantire che ogni campione sia una stringa valida

Alcune versioni di TRL richiedono che formatting_func restituisca una stringa, e il training può interrompersi se un campione diventa una lista o un valore non stringa. Questo helper forza text a essere sempre una stringa pulita.

# ensure text is always a string
def force_text_string(ds):
    def fix(ex):
        t = ex.get("text", "")
        if isinstance(t, list):
            t = "\n".join(map(str, t))
        return {"text": str(t)}
    return ds.map(fix)

train_text_ds = force_text_string(train_text_ds)
eval_text_ds  = force_text_string(eval_text_ds)

Infine, stampa un campione per confermare la formattazione:

print(train_text_ds[0]["text"])

Dovresti vedere un prompt completamente renderizzato che contiene le dichiarazioni dei tool, seguito da una gold function call simile a:

<bos><start_of_turn>developer
You are a model that can do function calling with the following functions<start_function_declaration>declaration:get_pollution_levels{description:<escape>Retrieve pollution levels information<escape>,parameters:{properties:{location:{description:<escape>The location for which you want to get the pollution levels (e.g., Beijing, London, New York)<escape>,type:<escape>STRING<escape>},pollutant:{description:<escape>Specify a pollutant for pollution levels (e.g., PM2.5, PM10, ozone)<escape>,type:<escape>STRING<escape>}},required:[<escape>location<escape>],type:<escape>DICT<escape>}}<end_function_declaration><start_function_declaration>declaration:get_water_quality{description:<escape>Retrieve water quality information<escape>,parameters:{properties:{location:{description:<escape>The location for which you want to get the water quality (e.g., river, lake, beach)<escape>,type:<escape>STRING<escape>}},required:[<escape>location<escape>],type:<escape>DICT<escape>}}<end_function_declaration><end_of_turn>
<start_of_turn>user
I'm planning a beach cleanup at Zuma Beach this weekend and need to ensure safety. Can you provide the current water quality and if it's poor, check the pollution level of PM2.5 there?<end_of_turn>
<start_of_turn>model
<start_function_call>call:get_water_quality{args:<escape>{"location": "Zuma Beach"}<escape>}<end_function_call>

7. Valutare FunctionGemma prima del fine-tuning

Prima del training, dovresti misurare quanto bene il modello base FunctionGemma esegue il function calling. Questa baseline ti aiuta a confermare che il fine-tuning migliori effettivamente la selezione degli strumenti e la formattazione delle chiamate di funzione.

In questa sezione valuteremo due aspetti:

  • Accuratezza del nome del tool: se il modello seleziona il nome della funzione corretto
  • TC-CER (Tool-Call Character Error Rate): quanto il blocco di function call generato corrisponde al blocco gold, dove valori più bassi sono migliori

Usiamo due espressioni regolari per estrarre il nome del tool predetto e l’intero blocco di function call dall’output del modello, quindi li confrontiamo con le etichette gold.

import re
import evaluate
import torch
from tqdm.auto import tqdm

cer_metric = evaluate.load("cer")

# Gemma / FunctionGemma-style only
FG_BLOCK_RE = re.compile(r"<start_function_call>.*?<end_function_call>", re.DOTALL)
FG_NAME_RE  = re.compile(r"call:([a-zA-Z0-9_]+)\{", re.DOTALL)

def extract_tool_name(gen: str):
    gen = gen or ""
    m = FG_NAME_RE.search(gen)
    return m.group(1) if m else None

def extract_call_block(gen: str):
    gen = gen or ""
    m = FG_BLOCK_RE.search(gen)
    return m.group(0) if m else ""

def gold_call_block(r):
    return (
        f"<start_function_call>call:{r['tool_name']}"
        f"{{args:<escape>{r['tool_arguments']}<escape>}}"
        f"<end_function_call>"
    )

Ciclo di valutazione baseline

La funzione qui sotto esegue l’inference su un sottoinsieme di esempi di valutazione e calcola entrambe le metriche. Manteniamo il sampling disabilitato (do_sample=False) per rendere i risultati deterministici e più facili da confrontare prima e dopo il fine-tuning.

@torch.inference_mode()
def eval_tool_and_cer(proc, mdl, rows, n=50, max_new_tokens=128):
    mdl.eval()
    n = min(n, len(rows))
    tool_ok = 0
    preds, refs = [], []

    for i in tqdm(range(n)):
        r = rows[i]
        messages = [
            {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
            {"role": "user", "content": r["user_content"]},
        ]

        inputs = proc.apply_chat_template(
            messages,
            tools=r["hf_tools"],
            add_generation_prompt=True,
            return_dict=True,
            return_tensors="pt",
        )
        inputs = {k: v.to(mdl.device) for k, v in inputs.items()}

        out = mdl.generate(
            **inputs,
            do_sample=False,
            max_new_tokens=max_new_tokens,
            pad_token_id=proc.eos_token_id,
        )

        gen = proc.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True).strip()

        if extract_tool_name(gen) == r["tool_name"]:
            tool_ok += 1

        preds.append(extract_call_block(gen))
        refs.append(gold_call_block(r))

    return {
        "n_eval": n,
        "tool_accuracy": tool_ok / n,
        "TC-CER (lower is better)": cer_metric.compute(predictions=preds, references=refs),
    }

Esegui la valutazione baseline:

pre_metrics = eval_tool_and_cer(processor, model, simple_eval, n=50, max_new_tokens=128)
print("PRE metrics:", pre_metrics)

Anche prima del fine-tuning, FunctionGemma mostra prestazioni di base solide su questo task. 

Su un sottoinsieme di 50 esempi di valutazione, il modello raggiunge l’88% di accuratezza sul nome del tool, il che significa che seleziona la funzione corretta nella maggior parte dei casi. 

Inoltre, la Tool-Call Character Error Rate (TC-CER) è di circa il 33% per l’intero blocco di function call, indicando che, sebbene spesso venga scelto il tool corretto, gli argomenti generati e la formattazione deviano ancora dal target gold.

PRE metrics: {'n_eval': 50, 'tool_accuracy': 0.88, 'TC-CER (lower is better)': 0.33399307273626916}

Questi risultati confermano che il modello base FunctionGemma comprende già la meccanica del function calling. 

Tuttavia, il tasso di errore sui caratteri relativamente alto evidenzia la necessità di fine-tuning per migliorare completezza degli argomenti, coerenza di formattazione e correttezza strutturale complessiva delle chiamate di funzione generate.

Ispezionare una singola predizione

Le metriche sono utili, ma è importante anche guardare gli output qualitativi. L’helper seguente esegue l’inference per un esempio e stampa la function call predetta insieme alla gold reference.

import torch

@torch.inference_mode()
def infer_one(proc, mdl, rows, idx=0, max_new_tokens=128):
    r = rows[idx]

    messages = [
        {"role": "developer", "content": "You are a model that can do function calling with the following functions"},
        {"role": "user", "content": r["user_content"]},
    ]

    inputs = proc.apply_chat_template(
        messages,
        tools=r["hf_tools"],
        add_generation_prompt=True,
        return_dict=True,
        return_tensors="pt",
    )
    inputs = {k: v.to(mdl.device) for k, v in inputs.items()}

    out = mdl.generate(
        **inputs,
        do_sample=False,
        max_new_tokens=max_new_tokens,
        pad_token_id=proc.eos_token_id,
    )

    gen = proc.decode(
        out[0][inputs["input_ids"].shape[-1]:],
        skip_special_tokens=True
    ).strip()

    pred_tool = extract_tool_name(gen)

    return {
        "task": r["user_content"],
        "tool_match": (pred_tool == r["tool_name"]),
        "predicted": extract_call_block(gen),
        "gold": gold_call_block(r),
    }

Eseguilo su un esempio:

pre = infer_one(processor, model, simple_eval, idx=15)


print(f"""
TASK:
{pre['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{pre['predicted']}

--- GOLD ---
{pre['gold']}
""")

Output:

TASK:
I'm reviewing the schedule in the surgical unit. Can you fetch the surgical nursing details for patient ID 12345 undergoing a 'Cardiac Bypass' today? If the procedure is handled by Nurse Ratched, let's record a post-operative care task for this evening.

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:get_surgical_nursing_information{nurse_id:<escape>Ratched<escape>,patient_id:<escape>12345<escape>,procedure_type:<escape>Cardiac Bypass<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass"}<escape>}<end_function_call>

In questo esempio, il modello seleziona correttamente la funzione appropriata, motivo per cui il tool match è True. 

Tuttavia, la chiamata generata include argomenti extra e non segue l’esatta struttura target usata in training. 

Sebbene questi argomenti siano sensati dal punto di vista semantico, non corrispondono al formato della gold function call, contribuendo a un tasso di errore sui caratteri più alto. Questo illustra perché è necessario il fine-tuning per migliorare la coerenza strutturale e la conformità agli schemi, non solo la selezione degli strumenti.

8. Configurare il trainer del modello per il fine-tuning 

Ora che abbiamo dataset SFT puliti, il passo successivo è configurare il trainer che effettuerà il fine-tuning di FunctionGemma. Usiamo SFTTrainer di TRL, un modo leggero e affidabile per fare fine-tuning di modelli in stile chat su esempi testuali.

Impostiamo anche una cartella di output così che Kaggle salvi il checkpoint con fine-tuning in una posizione persistente.

from trl import SFTConfig, SFTTrainer

OUT_DIR = "/kaggle/working/functiongemma-hermes-ft"

Per ridurre l’uso di VRAM durante il training, abilitiamo il gradient checkpointing e disabilitiamo la KV cache.

# VRAM savings
model.gradient_checkpointing_enable()
model.config.use_cache = False

Configurazione del trainer

Definiamo la configurazione di training usando SFTConfig. Queste impostazioni bilanciano stabilità ed efficienza sulle GPU di Kaggle, mantenendo breve il tempo di training.

cfg = SFTConfig(
    output_dir=OUT_DIR,
    max_length=512,
    packing=False,
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=5e-5,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    logging_steps=10,
    eval_strategy="steps",
    eval_steps=10,
    report_to="none",
    fp16=(model.dtype == torch.float16),
    bf16=(model.dtype == torch.bfloat16),
    optim="adamw_torch_fused",
)

La funzione di formattazione assicura che ogni esempio di training venga restituito come singola stringa, come richiesto dall’attuale versione di TRL.

def formatting_func(example):
    # MUST return a STRING (not list) for your TRL version
    t = example.get("text", "")
    if isinstance(t, list):
        t = "\n".join(map(str, t))
    return str(t)

Inizializzare il trainer

Infine, inizializziamo l’SFTTrainer con modello, configurazione, dataset e tokenizer.

trainer = SFTTrainer(
    model=model,
    args=cfg,
    train_dataset=train_text_ds,
    eval_dataset=eval_text_ds,
    processing_class=tokenizer,
    formatting_func=formatting_func,
)

9. Fine-tuning di FunctionGemma

Con il trainer configurato, possiamo ora avviare il fine-tuning del modello. Questo passaggio esegue il training supervisionato sul dataset preparato e valuta periodicamente il modello durante l’addestramento.

trainer.train()
trainer.save_model(OUT_DIR)
processor.save_pretrained(OUT_DIR)
print("Saved to:", OUT_DIR)

Al termine del training, salviamo sia il modello con fine-tuning sia il processor nella cartella di output, in modo che possano essere riutilizzati per l’inference o caricati sull’Hugging Face Hub.

Training del modello e validation loss

Durante il training, sia la training loss sia la validation loss diminuiscono in modo costante. Anche l’accuratezza media dei token migliora nel tempo, indicando che il modello ha imparato a produrre function call più accurate e coerenti.

Pubblicare il modello FunctionGemma con fine-tuning su Hugging Face

Una volta completato il fine-tuning, possiamo pubblicare il modello per riuso, condivisione o deployment. Caricare il modello sull’Hugging Face Hub consente ad altri di caricarlo direttamente usando le API standard di Transformers.

HF_REPO_ID = "kingabzpro/functiongemma-hermes-3k-ft"
model.push_to_hub(HF_REPO_ID)
processor.push_to_hub(HF_REPO_ID)
print("Pushed to:", HF_REPO_ID)

Dopo il completamento dell’upload, il modello con fine-tuning è disponibile pubblicamente su Hugging Face:

Pushed to: kingabzpro/functiongemma-hermes-3k-ft

Modello con fine-tuning pubblicato su Hugging Face Hub.

Fonte: kingabzpro/functiongemma-hermes-3k-ft · Hugging Face

Valutazione post fine-tuning e analisi degli errori

Dopo il fine-tuning, ricarichiamo da disco il modello e il processor salvati ed eseguiamo la stessa pipeline di valutazione usata prima del training. In questo modo il confronto tra le prestazioni pre e post fine-tuning è equo e coerente.

from transformers import AutoProcessor, AutoModelForCausalLM

ft_processor = AutoProcessor.from_pretrained(OUT_DIR, device_map="auto")
ft_model = AutoModelForCausalLM.from_pretrained(OUT_DIR, dtype="auto", device_map="auto")

Valutiamo quindi il modello con fine-tuning sul set di valutazione usando accuratezza del nome del tool e TC-CER.

post_metrics = eval_tool_and_cer(ft_processor, ft_model, simple_eval, n=50, max_new_tokens=64)
print("POST metrics:", post_metrics)

I risultati mostrano un chiaro miglioramento rispetto alla baseline:

POST metrics: {'n_eval': 50, 'tool_accuracy': 0.98, 'TC-CER (lower is better)': 0.1454725383473528}

L’accuratezza del nome del tool migliora dall’88 al 98 percento, e il tasso di errore sui caratteri per l’intero blocco di function call si riduce di oltre la metà. Ciò indica che il modello non solo seleziona più affidabilmente il tool corretto, ma produce anche output che si avvicinano maggiormente allo schema target.

Per comprendere meglio i miglioramenti, ispezioniamo predizioni individuali. Nell’esempio qui sotto, il modello seleziona la funzione corretta e produce una chiamata ben strutturata con argomenti più completi.

post = infer_one(ft_processor, ft_model, simple_eval, idx=15)

print(f"""
TASK:
{post['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{post['predicted']}

--- GOLD ---
{post['gold']}
""")

Sebbene la chiamata predetta possa includere argomenti aggiuntivi oltre il target gold, la struttura complessiva e la formattazione degli argomenti sono molto più coerenti rispetto a prima del fine-tuning. Ciò riflette una migliore consapevolezza dello schema e un allineamento più forte tra l’intento dell’utente e le function call generate.

TASK:
I'm reviewing the schedule in the surgical unit. Can you fetch the surgical nursing details for patient ID 12345 undergoing a 'Cardiac Bypass' today? If the procedure is handled by Nurse Ratched, let's record a post-operative care task for this evening.

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass", "nurse_id": "Ratched", "task": "Post-operative care"}<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:get_surgical_nursing_information{args:<escape>{"patient_id": "12345", "procedure_type": "Cardiac Bypass"}<escape>}<end_function_call>

Esploriamo un altro sample.

post = infer_one(ft_processor, ft_model, simple_eval, idx=25)

print(f"""
TASK:
{post['task']}

TOOL MATCH: {pre['tool_match']}

--- PREDICTED ---
{post['predicted']}

--- GOLD ---
{post['gold']}
""")

Un secondo esempio mostra una function call pulita e corretta per un task di estrazione URL, dimostrando che il modello con fine-tuning generalizza bene tra tool diversi.

TASK:
I would like to extract details from a LinkedIn company page. Could you assist me in fetching the information from this URL: https://www.linkedin.com/company/abc-corporation?

TOOL MATCH: True

--- PREDICTED ---
<start_function_call>call:extract{args:<escape>{"url": "https://www.linkedin.com/company/abc-corporation"}<escape>}<end_function_call>

--- GOLD ---
<start_function_call>call:extract{args:<escape>{"url": "https://www.linkedin.com/company/abc-corporation", "html": true}<escape>}<end_function_call>

Nel complesso, questi risultati confermano che il fine-tuning migliora con successo sia l’accuratezza sia la correttezza strutturale per i task di function calling.

Il notebook Kaggle completo per questo tutorial è disponibile qui:https://www.kaggle.com/code/kingabzpro/finetuning-functiongemma

Se riscontri problemi, puoi clonare il notebook, aggiungere il tuo secret di Hugging Face ed eseguirlo end-to-end.

Considerazioni finali

Se stai provando ad addestrare FunctionGemma su dataset di generazione testuale generici, è meglio fermarsi prima. FunctionGemma non è progettata per comportarsi come un modello linguistico generalista. 

Per la generazione di testo open-ended, la famiglia di modelli Gemma 3 è una scelta migliore. 

FunctionGemma è costruita appositamente per il function calling, dove l’obiettivo è selezionare il tool corretto, produrre argomenti conformi allo schema e interagire in modo affidabile con sistemi esterni come API, database e servizi.

La piccola dimensione di FunctionGemma è una scelta progettuale deliberata. Con 270 milioni di parametri, è ottimizzata per deployment a bassa latenza e con risorse ridotte, inclusi computer locali, dispositivi edge e infrastrutture private. Questo la rende adatta a function calling in tempo reale e workflow agentici, dove correttezza e struttura contano più della prosa fluente.

In questo tutorial abbiamo visto come fare il fine-tuning di FunctionGemma sul dataset Hermes Reasoning Tool-Use. Un punto chiave è che preparazione dei dati e valutazione contano più del tempo di training in sé. 

Pulendo accuratamente il dataset, allineando i task con le gold tool call e imponendo una rigorosa coerenza di schema, siamo riusciti a migliorare notevolmente il comportamento del modello con un numero relativamente piccolo di campioni di alta qualità.

I risultati mostrano chiaramente l’impatto del fine-tuning. L’accuratezza del nome del tool è passata dall’88 al 98 percento e il tasso di errore sui caratteri per i blocchi di function call completi si è ridotto di oltre la metà. Ancora più importante, il modello è diventato molto più coerente nel produrre function call ben strutturate e prevedibili, allineate all’uso nel mondo reale.

FunctionGemma FAQs

Che cos&rsquo;&egrave; FunctionGemma e in cosa &egrave; diversa da Gemma 3?

FunctionGemma è una versione specializzata del modello open Gemma 3 270M. Mentre Gemma 3 standard è progettato per la generazione di testo, FunctionGemma è esplicitamente addestrato a tradurre il linguaggio naturale in chiamate di funzione strutturate (richieste API), diventando un "router" dedicato per workflow agentici piuttosto che un chatbot conversazionale.

hat hardware do I need to run FunctionGemma?

Essendo un modello da 270M di parametri, FunctionGemma è estremamente leggera (~300MB quantizzata). Può essere eseguita in modo efficiente su dispositivi edge come laptop, telefoni e hardware embedded (ad esempio NVIDIA Jetson Nano) senza richiedere una GPU cloud dedicata.

Perch&eacute; devo fare il fine-tuning di FunctionGemma se &egrave; gi&agrave; specializzata?

Sebbene il modello base comprenda il concetto di uso dei tool, è necessario il fine-tuning per insegnargli i tuoi schemi API e la tua logica di business specifici. I benchmark di Google mostrano che il fine-tuning può aumentare l'accuratezza del function calling da ~58% (base) a oltre 85% in task specifici di dominio.

FunctionGemma pu&ograve; gestire conversazioni multi-turn?

FunctionGemma è ottimizzata principalmente per l'esecuzione di comandi single-turn (Istruzione utente → Tool Call). Per ragionamento complesso multi-turn o per mantenere una lunga cronologia di conversazione, è meglio usarla come "traffic controller" che delega le richieste difficili a un modello più grande (come Gemma 3 27B) mentre gestisce in locale le azioni semplici.

Abid Ali Awan

Autore

Abid Ali Awan

In quanto data scientist certificato, sono appassionato di sfruttare tecnologie all’avanguardia per creare applicazioni di machine learning innovative. Con una solida esperienza in riconoscimento vocale, analisi e reportistica dei dati, MLOps, AI conversazionale e NLP, ho affinato le mie competenze nello sviluppo di sistemi intelligenti in grado di avere un impatto concreto. Oltre alla mia expertise tecnica, sono anche un comunicatore efficace, con il talento di rendere chiari e sintetici concetti complessi. Di conseguenza, sono diventato un blogger molto seguito in ambito data science, condividendo idee ed esperienze con una community in crescita di professionisti dei dati. Attualmente mi concentro sulla creazione e sull’editing di contenuti, lavorando con large language model per sviluppare contenuti potenti e coinvolgenti che possano aiutare aziende e singoli a valorizzare al meglio i propri dati.

Argomenti

Correlato