Vai al contenuto principale

Tutorial TRIBE v2: simulare l’attività cerebrale umana da video, audio e testo

Impara a eseguire il modello TRIBE V2 di Meta su Google Colab, a prevedere l’attività corticale da stimoli naturalistici e a visualizzare i risultati come mappe di calore 3D interattive.
Aggiornato 29 set 2026  · 15 min leggi

Scopri con l'IA

ChatGPTClaudePerplexity

Esperimenti fMRI reali costano da $1.000 a $3.000 all’ora di tempo allo scanner, richiedono mesi di pianificazione e producono comunque registrazioni rumorose, distorte da battiti cardiaci e artefatti di movimento. E se potessi eseguire un esperimento di neuroscienze in pochi minuti?

Il modello foundation trimodale TRIBE v2 di Meta AI lo rende possibile prevedendo l’attività fMRI dell’intero cervello a partire da input video, audio e testo. È addestrato su oltre 1.100 ore di registrazioni fMRI di 720 soggetti ed è open-source con licenza CC-BY-NC.

In questo tutorial, noi:

  • Capiremo cos’è TRIBE v2 e come funziona la sua architettura
  • Eseguiremo l’inferenza su input di testo, audio e video
  • Visualizzeremo l’attività corticale prevista come mappe di calore 3D interattive usando nilearn
  • Eseguiremo un esperimento comparativo in silico tra contenuti linguistici e contenuti visivi/spaziali
  • Avvieremo una demo con Gradio 

Cos’è TRIBE v2?

TRIBE v2 (TRImodal Brain Encoder) è un modello di deep learning che mappa stimoli naturalistici su risposte cerebrali fMRI previste. Dato un clip video, un file audio o un blocco di testo, il modello restituisce un segnale BOLD previsto per ciascuno dei 20.484 vertici sulla superficie corticale fsaverage5 a 1 Hz, cioè una previsione al secondo.

Le previsioni sono per il soggetto medio (non il cervello di una persona specifica), ovvero la risposta canonica media di gruppo che TRIBE v2 ha appreso da 720 partecipanti su quattro dataset naturalistici. Le previsioni zero-shot del modello superano le registrazioni fMRI su singolo soggetto del dataset Human Connectome Project 7T, che ha la qualità di segnale più alta nel set di training.

Proprietà chiave

Proprietà

Dettaglio

Spazio di output

20.484 vertici corticali sulla superficie fsaverage5 e previsioni a tutto cervello su circa 70.000 voxel (corteccia + sottocorteccia)

Risoluzione temporale

1 Hz (corrisponde alla frequenza TR dell’fMRI)

Modalità di input

Video (V-JEPA2-Giant), Audio (Wav2Vec-BERT 2.0), Testo (LLaMA 3.2-3B)

Parametri dell’encoder

~1B parametri apprendibili nel layer di integrazione transformer

Dati di training

1.115 ore di fMRI su 720 soggetti e 4 dataset

Generalizzazione

Zero-shot su nuovi soggetti, compiti e lingue

Licenza

CC-BY-NC 4.0 (uso di ricerca, non commerciale)

Il modello è adattato dall’articolo A foundation model of vision, audition, and language for in-silico neuroscience, che dimostra come TRIBE v2 recuperi la fusiform face area per i volti, la parahippocampal place area per le scene, l’area di Broca per la sintassi complessa e la rete linguistica lateralizzata a sinistra per il parlato, senza alcun dato fMRI in fase di inferenza. 

Panoramica dell’architettura di TRIBE v2

TRIBE v2 ha tre fasi che vengono eseguite in sequenza a ogni chiamata di inferenza:

Modello di previsione dell’attività cerebrale TRIBE v2

Figura: modello di previsione dell’attività cerebrale TRIBE v2 (Generata con IA)

Fase 1: Estrazione delle feature (congelata)

Per prima cosa, tre encoder preaddestrati separati elaborano in modo indipendente ciascuna modalità di input in embedding densi allineati nel tempo. Nessuno di questi encoder viene aggiornato durante il training (congelati), quindi TRIBE v2 eredita le loro rappresentazioni così come sono. Ecco alcune metriche per l’estrazione delle feature per ciascuna modalità:

  • Testo: LLaMA 3.2-3B converte il testo di input in embedding densi (D = 2048)

  • Audio: Wav2Vec-BERT 2.0 codifica i segnali audio a ~2 Hz (D = 1024)

  • Video: V-JEPA2-Giant elabora i frame visivi in feature temporali (D = 1280)

Fase 2: Integrazione universale (apprendibile)

I tre flussi di embedding vengono fusi in una singola rappresentazione condivisa ed elaborati da un Transformer che effettua attenzione lungo il tempo. Qui risiedono i pesi appresi di TRIBE v2 e qui vengono catturate le interazioni cross-modali come segue:

  • Rappresentazione condivisa: Tutti gli embedding delle modalità sono proiettati in uno spazio unificato (D_model = 1152)

  • Fusione Transformer: Un Transformer a 8 layer e 8 head integra i segnali su una finestra di contesto lunga (~100 s)

  • Flessibilità di modalità: Il dropout di modalità (p = 0.3) abilita l’inferenza con qualsiasi sottoinsieme (testo/audio/video)

Fase 3: Mappatura cerebrale (apprendibile)

La rappresentazione latente fusa è proiettata sulla superficie corticale per produrre la previsione fMRI finale. Questa fase converte le feature astratte del modello in stime di attività cerebrale risolte nello spazio e nel tempo. 

  • Allineamento temporale: Gli output sono allineati e campionati a 1 Hz per corrispondere alla tempistica fMRI
  • Proiezione corticale: Un layer lineare condizionato sul soggetto mappa le feature ai vertici della superficie cerebrale
  • Output finale: Una matrice (T, 20484) rappresenta l’attività cerebrale prevista nel tempo

Poiché tre estrattori di feature sono congelati durante il training, TRIBE v2 apprende solo i layer di proiezione e i pesi del transformer che integrano i loro output. Questa scelta progettuale è importante perché rende il modello robusto a stimoli fuori distribuzione, dato che eredita la capacità di generalizzazione di tre modelli preaddestrati su larga scala invece di essere addestrato end-to-end solo su dati fMRI.

Nota: un trucco chiave di training è il dropout di modalità. Durante il training, ogni modalità viene azzerata indipendentemente con probabilità 0,3. Questo costringe il modello a fare previsioni significative da qualsiasi sottoinsieme di modalità. Quindi, in inferenza, puoi passare solo audio o solo testo e ottenere comunque una previsione corticale utile.

Demo TRIBE v2: prevedere le risposte cerebrali

In questa sezione, costruiremo un flusso di lavoro passo-passo che esegue l’inferenza TRIBE v2 su input di testo, audio o video e visualizza l’attività corticale prevista come mappa di calore 3D interattiva. Eseguiremo anche un esperimento di confronto che replica il paradigma in silico dell’articolo originale. Infine, svilupperemo un’app Gradio che chiunque può usare per esplorare la demo in tempo reale.

Passo 1: Prerequisiti e hardware

Prima di iniziare, configura il runtime di Colab. Nota che puoi anche usare qualsiasi altro servizio con una GPU A100 stabile e RAM elevata.

  • Apri Runtime e seleziona cambia tipo di runtime
  • Seleziona A100 GPU e abilita High RAM
  • Clicca su Salva

TRIBE v2 carica simultaneamente tre encoder congelati, tra cui LLaMA 3.2-3B (~7 GB), V-JEPA2-Giant (~14 GB) e Wav2Vec-BERT 2.0 (~1 GB), insieme ai pesi del transformer di TRIBE. L’occupazione totale di VRAM è 28–32 GB. 

Nota: una T4 (16 GB) esaurirà la memoria quando model.predict() carica LLaMA. Usa l’A100 (40 GB) o A100 con High RAM (80 GB) per prestazioni migliori.

Verifica la tua GPU prima di installare qualsiasi cosa eseguendo quanto segue:

import subprocess, sys
result = subprocess.run(
    ['nvidia-smi', '--query-gpu=name,memory.total',
     '--format=csv,noheader,nounits'],
    capture_output=True, text=True)
print(result.stdout.strip())
import torch
assert torch.cuda.is_available(), "No GPU detected"
props = torch.cuda.get_device_properties(0)
assert props.total_memory > 30e9, (
    f"Need ≥40 GB VRAM. Got {props.total_memory/1e9:.0f} GB. Switch to A100.")
print(f"GPU: {props.name} — {props.total_memory/1e9:.0f} GB")

La chiamata subprocess.run() invoca nvidia-smi con il flag --query-gpu per estrarre il nome della GPU e la VRAM totale. Le due assert fungono da uscite anticipate; la prima conferma che CUDA è disponibile, la seconda verifica che la VRAM totale superi i 30 GB. Fallire rumorosamente qui è meglio che fallire silenziosamente dentro model.predict() 10 minuti dopo con un criptico errore di out-of-memory di CUDA.

Passo 2: Correggi il conflitto di versione di NumPy

Salta questo passo se non stai eseguendo su Google Colab. Questo è il primo bug che incontrerai perché Colab include NumPy 2.x di default. Diverse dipendenze interne di TRIBE v2, in particolare neuralset, sono state compilate contro NumPy <2.1, che ha rimosso il simbolo _center da numpy._core.umath. Il risultato è questo errore quando provi a import tribev2:

ImportError
cannot import name '_center' from 'numpy._core.umath'
(/usr/local/lib/python3.12/dist-packages/numpy/_core/umath.py)

La correzione consiste nel fissare NumPy a <2.1 prima che vengano installati tribev2 o qualsiasi sua dipendenza, quindi riavviare il runtime. Esegui semplicemente la seguente cella, che disinstalla la versione corrente di NumPy e la sostituisce con una inferiore a 2.1. 

import subprocess, sys
print("Pinning NumPy to <2.1 (required for neuralset compatibility)...")
subprocess.run([sys.executable, '-m', 'pip', 'uninstall', '-y', 'numpy'])
subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',
                'numpy>=1.26.4,<2.1.0'])

Una volta finalizzati ambiente e dipendenze, possiamo procedere con l’installazione di TRIBE v2.

Passo 3: Installa TRIBE V2 

Con il kernel appena riavviato e NumPy fissato, ora possiamo installare in sicurezza il pacchetto tribev2 da GitHub insieme alle librerie di visualizzazione e UI. 

import numpy as np
from packaging.version import Version
assert Version(np.__version__) < Version('2.1.0'), (
    f"NumPy is {np.__version__}. Run Step 2a and restart first.")
print(f"NumPy {np.__version__} Checked")
# Install tribev2 from GitHub 
!pip install -q 'tribev2[plotting] @ git+https://github.com/facebookresearch/tribev2.git'
!pip install -q 'gradio>=4.19.0' 'nilearn>=0.10.3' 'plotly>=5.18.0'

L’extra tribev2[plotting] installa pyvista, una libreria Python per la visualizzazione 3D, e nilearn, una libreria Python per la neuroimmagine, insieme al pacchetto core. L’installazione direttamente dall’URL GitHub assicura di ottenere l’ultima commit senza dover clonare il repository localmente. 

I pacchetti nilearn e gradio vengono installati separatamente perché i loro vincoli di versione sono più flessibili e beneficiano di una risoluzione indipendente dal grafo di dipendenze di tribev2.

Passo 4: Autenticazione su HuggingFace

L’encoder di testo usa LLaMA 3.2-3B, che è un modello con accesso limitato su HuggingFace. Devi accettare esplicitamente la licenza di Meta prima che i pesi possano essere scaricati. Fallo una volta:

  • Visita HuggingFace e clicca su Accept license
  • Crea un token di sola lettura in Settings/Access Tokens 
  • In Colab, clicca l’icona della chiave nella barra laterale a sinistra e seleziona Add secret. Infine, assegna al tuo token il nome “HF_TOKEN” e imposta “value: your token”.

Una volta impostato l’HF token, esegui il seguente codice per accedere al tuo account:

import os
# Load token from Colab Secrets
try:
    from google.colab import userdata
    os.environ['HF_TOKEN'] = userdata.get('HF_TOKEN')
    print("HF_TOKEN loaded from Colab Secrets")
except Exception:
    from huggingface_hub import login
    login()

Il percorso preferito usa google.colab.userdata.get(), che legge dall’archivio Secrets crittografato di Colab, non esponibile accidentalmente in un link del notebook condiviso.

Il fallback chiama huggingface_hub.login(), che richiede in modo interattivo il token e lo maschera mentre digiti. Entrambi i percorsi scrivono il token in os.environ['HF_TOKEN'], da cui la libreria HuggingFace Hub lo rileverà automaticamente quando scarica i pesi dei modelli con accesso limitato.

Passo 5: Carica il modello preaddestrato

Con NumPy fissato, l’autenticazione configurata e LLaMA in cache, ora possiamo caricare il checkpoint dell’encoder TRIBE v2 da HuggingFace. Questo scarica circa 1 GB alla prima esecuzione e impiega pochi secondi dalle cache nelle esecuzioni successive.

from pathlib import Path
from tribev2.demo_utils import TribeModel
import torch
CACHE_DIR = Path('/content/tribe_cache')
CACHE_DIR.mkdir(exist_ok=True)
print('Loading TRIBE v2 (first run downloads ~1 GB)...')
model = TribeModel.from_pretrained(
    'facebook/tribev2',
    cache_folder=str(CACHE_DIR)
)
print('Model loaded')
if torch.cuda.is_available():
    used  = torch.cuda.memory_allocated() / 1e9
    total = torch.cuda.get_device_properties(0).total_memory / 1e9
    print(f'VRAM after load: {used:.1f} / {total:.1f} GB')

TribeModel.from_pretrained() scarica il checkpoint dell’encoder TRIBE da facebook/tribev2 su HuggingFace e lo salva in cache_folder. Questo checkpoint contiene i pesi di integrazione del transformer e il blocco del soggetto, ma non i tre estrattori di feature. Questi vengono scaricati separatamente quando model.predict() usa per la prima volta ciascuna modalità.

Dopo aver caricato solo l’encoder TRIBE, vengono allocati circa 2–4 GB di VRAM, mentre i restanti 24–28 GB verranno consumati quando model.predict() carica V-JEPA2-Giant e LLaMA 3.2-3B al primo utilizzo.

Passo 6: Correggi il timeout di download

Dopo il caricamento del modello TRIBE, chiamare model.predict() su input testuali per la prima volta innesca un download lazy dei pesi di LLaMA 3.2-3B (~6 GB). Il timeout predefinito di HuggingFace Hub è 10 secondi, causando questo errore a metà inferenza:

ReadTimeout
The read operation timed out
Computing word embeddings:  0%|  | 0/9 [00:10<?, ?it/s]

Per risolvere, aumenta le variabili d’ambiente di timeout, poi pre-scarica LLaMA esplicitamente usando snapshot_download così da ottenere un avanzamento visibile e il resume automatico in caso di interruzione, invece di un fallimento silenzioso annidato dentro predict().

import os
os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = '300'   
os.environ['HF_HUB_HTTP_TIMEOUT']     = '300' 
from huggingface_hub import snapshot_download
print("Pre-downloading LLaMA 3.2-3B (~6 GB)...")
print("Runs once — subsequent calls load from cache.\n")
snapshot_download(
    repo_id        = "meta-llama/Llama-3.2-3B",
    cache_dir      = "/content/tribe_cache/llama",
    ignore_patterns= ["*.bin"], 
)
print("\n LLaMA 3.2-3B cached")

snapshot_download() scarica un intero repository nella cache locale usando il protocollo di range-request di HuggingFace, il che significa che riprende automaticamente se la connessione cade a metà file. L’argomento ignore_patterns=["*.bin"] salta il vecchio formato binario di PyTorch e scarica solo i file safetensors, riducendo la dimensione totale del download di circa il 40%.

Passo 7: Helper per la visualizzazione cerebrale

Prima di eseguire l’inferenza reale, impostiamo il livello di visualizzazione. Queste funzioni helper convertono l’array grezzo (T, 20484) di previsione in mappe di calore 3D interattive del cervello usando nilearn. 

TRIBE v2 restituisce previsioni come array NumPy di forma (T, 20484), dove T è il numero di secondi di input. I primi 10.242 vertici sono nell’emisfero sinistro e i restanti 10.242 in quello destro. 

Usiamo nilearn.plotting.view_surf per renderizzare ciascun emisfero come superficie WebGL interattiva. La mesh “inflated” espone la geometria dei solchi che altrimenti rimarrebbe nascosta nelle pieghe, e la mappa della profondità dei solchi fornisce un riferimento anatomico sotto la mappa di calore.

Passo 7.1: Scarica la mesh fsaverage5

La mesh fsaverage5 è il template corticale standard di FreeSurfer che TRIBE v2 usa come spazio di output. La scarichiamo una volta qui così che tutte le chiamate di visualizzazione successive possano farvi riferimento senza doverla riscaricare dalla rete.

import numpy as np
from nilearn import datasets as nl_datasets
from nilearn.plotting import view_surf
from IPython.display import display, HTML
N_PER_HEMI = 10242   # fsaverage5: 10242 vertices per hemisphere
print('Fetching fsaverage5 mesh...')
fsavg = nl_datasets.fetch_surf_fsaverage(mesh='fsaverage5')
print('Mesh ready')
print('Keys:', [k for k in fsavg.keys() if k != 'description'])

La funzione fetch_surf_fsaverage(mesh='fsaverage5') scarica il template FreeSurfer fsaverage5 dal CDN di nilearn e lo mette in cache. Restituisce anche un oggetto Bunch (dizionario) con chiavi come infl_left, infl_right, sulc_left e sulc_right. 

Passo 7.2: Dividi gli emisferi e renderizza

Questo sotto-passo definisce le tre funzioni core da cui dipende tutta la visualizzazione in questo tutorial. La funzione split_hemis() partiziona l’array dei vertici, render_hemi() costruisce la superficie WebGL interattiva per un emisfero e show_brain() assembla entrambe nella disposizione affiancata.

def split_hemis(v):
    n = v.shape[0]
    if n == 2 * N_PER_HEMI:
        return v[:N_PER_HEMI], v[N_PER_HEMI:]
    return v[:n//2], v[n//2:]  
def render_hemi(pred_vec, hemi='left', title=''):
    lh, rh = split_hemis(pred_vec)
    data = lh if hemi == 'left' else rh
    vmax = max(float(np.percentile(np.abs(data), 99)), 1e-6)
    return view_surf(
        surf_mesh = fsavg[f'infl_{hemi}'],   
        surf_map  = data,
        bg_map    = fsavg[f'sulc_{hemi}'],   
        hemi      = hemi,
        threshold = '20%',  
        cmap      = 'hot',    
        black_bg  = True,
        vmax      = vmax,
        bg_on_data= True,     
        colorbar  = True,
        title     = title,
    )
def show_brain(pred_vec, title='', t=None):
    sfx = f' — t={t}s' if t is not None else ''
    lv  = render_hemi(pred_vec, 'left',  f'{title} [Left]{sfx}')
    rv  = render_hemi(pred_vec, 'right', f'{title} [Right]{sfx}')
    html = (
        '<div style="display:flex;gap:10px;background:#000;'
        'border-radius:10px;">'
        f'<div style="flex:1">{lv.get_iframe(width="100%",height="460px")}</div>'
        f'<div style="flex:1">{rv.get_iframe(width="100%",height="460px")}</div>'
        '</div>'
    )
    display(HTML(html))

Capiremo nel dettaglio la funzione di ciascun helper:

  • La funzione split_hemis() taglia il vettore di previsione all’indice 10.242, che è il punto di divisione standard per la mesh fsaverage5 secondo la convenzione di FreeSurfer. L’emisfero sinistro occupa gli indici 0–10241 e quello destro 10242–20483. Il branch di fallback in basso gestisce i casi limite in cui il modello restituisce un conteggio di vertici non standard.

  • Dentro la funzione render_hemi(), vmax è calcolato come il 99° percentile dei valori di attivazione assoluti invece del massimo reale. Questo impedisce a un singolo vertice estremo di comprimere l’intera mappa colori in un intervallo stretto, rendendo visibile il pattern spaziale. 

  • La funzione view_surf() restituisce un oggetto SurfaceView contenente 2,4 MB di HTML WebGL auto-contenuto. La chiamata get_iframe() lo incapsula in un tag <iframe> dimensionato secondo i valori forniti. Quindi, quando chiamiamo display(HTML(...)) con due iframe affiancati, otteniamo il layout sinistra/destra.

Con il modello caricato e gli helper di visualizzazione pronti, possiamo eseguire la prima vera inferenza. 

Passo 8: Esegui l’inferenza

L’inferenza di TRIBE v2 avviene in due passaggi. Prima, model.get_events_dataframe() estrae eventi allineati nel tempo dall’input, insieme ai timing delle parole dal testo, agli embedding Wav2Vec a 2 Hz dall’audio o agli embedding V-JEPA2 a 2 Hz dai frame video. 

Il DataFrame degli eventi risultante viene poi passato a model.predict(), che esegue il transformer e il blocco del soggetto per produrre le previsioni corticali finali.

import tempfile, os
SAMPLE_TEXT = '''
The brain processes language through a distributed network in the left hemisphere.
Broca's area coordinates syntactic structure, while Wernicke's area handles semantics.
Together they form the language circuit activated when reading or hearing speech.
'''
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w')
try:
    tmp.write(SAMPLE_TEXT.strip())
    tmp.flush()
    os.fsync(tmp.fileno())   
    tmp.close()
    events = model.get_events_dataframe(text_path=tmp.name)
finally:
    if os.path.exists(tmp.name):
        os.unlink(tmp.name) 
print(f'Events: {events.shape}')
print(events[['type', 'start', 'duration']].head(8))
print('\nRunning model.predict()...')
preds, segments = model.predict(events=events)
preds = np.asarray(preds)
print(f'Prediction shape: {preds.shape}')
print(f'  T = {preds.shape[0]}s   (1 Hz fMRI frequency)')
print(f'  V = {preds.shape[1]} vertices  (fsaverage5 cortical surface)')

La sequenza di scrittura tmp.write(), tmp.flush(), os.fsync(tmp.fileno()), tmp.close() è la correzione critica per un bug sottile. Se chiami get_events_dataframe() dentro un blocco with prima che il file sia chiuso, il buffer di scrittura interno di Python potrebbe non essere ancora stato sincronizzato con l’OS e tribev2 leggerà un file vuoto generando ValueError. La chiamata os.fsync() garantisce che la page cache dell’OS sia svuotata su disco prima che tribev2 apra il percorso.

La funzione model.predict() restituisce una tupla (preds, segments). L’array preds ha forma (T, 20484), una previsione corticale al secondo di input per tutti i 20.484 vertici fsaverage5. Incapsularlo in np.asarray() garantisce che sia un array NumPy semplice a prescindere dal tipo interno restituito dal modello. Una volta ottenuto preds, puoi visualizzare la risposta corticale in qualsiasi timestep:

T = preds.shape[0]
print(f'Timesteps: 0 to {T-1}')
T_SHOW = min(5, T - 1)
show_brain(preds[T_SHOW], title='Language stimulus', t=T_SHOW)

Per impostazione predefinita usiamo t=5 perché il segnale BOLD (Blood-Oxygen-Level-Dependent) ha un ritardo emodinamico, e la risposta vascolare all’attività neurale raggiunge il picco circa 5–6 secondi dopo l’inizio dello stimolo. Visualizzare a t=0 mostra un’attivazione quasi nulla a prescindere dal contenuto dello stimolo, poiché la risposta vascolare cerebrale non si è ancora sviluppata. La guardia min(5, T-1) previene un errore di indice quando l’input produce meno di 6 timestep.

Output TRIBE v2 per singolo testo

Passo 9: Esperimento di confronto

Una singola mappa di attivazione ti dice quali aree sono attive, ma non cosa rende uno stimolo diverso da un altro. Questo passo fa passare due input attraverso il modello e calcola una mappa di contrasto (A − B) per isolare le differenze specifiche di regione tra contenuti linguistici e contenuti visivi/spaziali.

Passo 9.1: Definisci un helper riutilizzabile per l’inferenza

Invece di ripetere lo schema scrivi -> flush -> close -> infer per ogni condizione, lo incapsuliamo in un’unica funzione text_to_preds(). Questo assicura che i passaggi critici di flush del file non vengano mai omessi per nessuna condizione.

TEXT_A = '''
She spoke slowly and clearly, her voice filling the quiet room.
Every sentence carried meaning, and each word was chosen with care.
Language connects us, the professor said, bridging minds across time.
'''
TEXT_B = '''
The canyon walls rose steeply, layers of red and orange sandstone.
A hawk circled overhead, its wings barely moving in the thermal current.
Shadows shifted as the sun tracked its arc across the open desert sky.
'''
def text_to_preds(text):
    tmp = tempfile.NamedTemporaryFile(
        delete=False, suffix='.txt', mode='w', encoding='utf-8')
    try:
        tmp.write(text.strip())
        tmp.flush()
        os.fsync(tmp.fileno())
        tmp.close()
        evts = model.get_events_dataframe(text_path=tmp.name)
        p, _ = model.predict(events=evts)
        return np.asarray(p)
    finally:
        if os.path.exists(tmp.name):
            os.unlink(tmp.name)
print('Condition A: language content...')
preds_a = text_to_preds(TEXT_A)
print('Condition B: visual/spatial content...')
preds_b = text_to_preds(TEXT_B)

Usiamo due passaggi di testo con contenuto semantico distinto, con l’aspettativa che il contenuto linguistico attivi maggiormente la corteccia temporale dell’emisfero sinistro, mentre il contenuto visivo/spaziale recluti maggiormente la corteccia occipitale e parietale posteriore.

La funzione text_to_preds() incapsula l’intera pipeline in una funzione riutilizzabile, applicando lo stesso schema sicuro del Passo 8 così che il file temporaneo sia sempre completamente svuotato prima che tribev2 lo legga. L’argomento encoding='utf-8' è esplicito per evitare problemi di codifica dipendenti dalla piattaforma.

Passo 9.2: Renderizza attivazioni grezze e mappa di contrasto

Ora che entrambe le condizioni sono previste, visualizziamo ciascuna individualmente e poi le sottraiamo vertice per vertice per produrre la mappa di contrasto. 

T_shared = min(preds_a.shape[0], preds_b.shape[0])
t_show   = min(5, T_shared - 1)
print('\n[A] Language content:')
show_brain(preds_a[t_show], title='Condition A: Language', t=t_show)
print('\n[B] Visual/spatial content:')
show_brain(preds_b[t_show], title='Condition B: Visual', t=t_show)
print('\n[A − B] Contrast: Language > Visual')
show_brain(preds_a[t_show] - preds_b[t_show], title='Contrast A − B', t=t_show)

La mappa di contrasto preds_a[t_show] - preds_b[t_show] è una sottrazione diretta vertice per vertice in cui valori positivi indicano regioni in cui la condizione A attiva di più e valori negativi indicano regioni in cui la condizione B attiva di più. 

Poiché entrambe le condizioni condividono lo stesso percorso di elaborazione del testo, le mappe grezze appariranno ampiamente simili. Questa mappa di contrasto mette in evidenza le differenze specifiche di dominio tra contenuti linguistici e visivi.

Passo 9.3: Traccia la differenza temporale

Le mappe di calore cerebrali mostrano pattern spaziali in un singolo istante. Questo passo aggiunge una prospettiva temporale: per esempio, come si confronta l’attivazione complessiva tra le condizioni su tutti i timestep e quando le due condizioni divergono più fortemente? 

import matplotlib.pyplot as plt
diff_norms = [
    np.linalg.norm(preds_a[i] - preds_b[i])
    for i in range(T_shared)
]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 3.5))
ax1.plot(np.abs(preds_a).mean(axis=1)[:T_shared],
         color='#e74c3c', linewidth=2, label='A: Language')
ax1.plot(np.abs(preds_b).mean(axis=1)[:T_shared],
         color='#3498db', linewidth=2, label='B: Visual')
ax1.set_title('Mean cortical activation over time')
ax1.set_xlabel('Time (s)'); ax1.legend(); ax1.grid(True, alpha=0.3)
ax2.plot(diff_norms, color='#f39c12', linewidth=2)
ax2.fill_between(range(T_shared), diff_norms, alpha=0.2, color='#f39c12')
ax2.set_title('||A − B|| difference over time')
ax2.set_xlabel('Time (s)'); ax2.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()

TRIBE v2 Confronto tra due input testuali

Il grafico a sinistra traccia np.abs(preds).mean(axis=1), cioè il valore assoluto medio dell’attivazione collassato su tutti i 20.484 vertici a ogni secondo. Questo mostra quanto fortemente ciascuna condizione coinvolge la corteccia e quando la risposta raggiunge il picco. Prendere il valore assoluto è importante perché i valori BOLD previsti possono essere negativi (disattivazione), e vogliamo la magnitudine, non la media con segno.

Il grafico a destra traccia la norma L2 del vettore differenza a ogni timestep, np.linalg.norm(preds_a[i] - preds_b[i]). Un picco in questa curva attorno a t=5–7 s è coerente con il ritardo emodinamico: entrambe le condizioni hanno bisogno di tempo perché la risposta BOLD si costruisca prima di divergere. L’ombreggiatura con fill_between() rende chiari visivamente l’insorgenza e il picco della divergenza.

Passo 10: Avvia la demo Gradio

Questo ultimo passo incapsula la logica di inferenza e visualizzazione in un’app Gradio con un’interfaccia pulita, uno slider del timestep e una tab di confronto A/B. 

import gradio as gr
_pred_cache = {}   
def _infer(mod, vid, aud, txt):
    """Run inference and cache the result. Subsequent calls return cached array."""
    key = (mod, vid, aud, hash(txt or ''))
    if key not in _pred_cache:
        if mod == 'video':
            evts = model.get_events_dataframe(video_path=vid)
        elif mod == 'audio':
            evts = model.get_events_dataframe(audio_path=aud)
        else:
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w')
            tmp.write((txt or '').strip()); tmp.flush()
            os.fsync(tmp.fileno()); tmp.close()
            evts = model.get_events_dataframe(text_path=tmp.name)
            os.unlink(tmp.name)
        p, _ = model.predict(events=evts)
        _pred_cache[key] = np.asarray(p)
    return _pred_cache[key]
demo.launch(
    share      = True,    
    debug      = False,
    server_name= "0.0.0.0",
)

Ecco come si combinano l’UI di Gradio e la pipeline di inferenza:

  • La funzione _infer() funge da livello centrale di inferenza, gestendo tutte e tre le modalità (video, audio e testo) preparando gli input, chiamando model.predict() e restituendo l’attività cerebrale prevista.

  • Viene usata una cache delle previsioni per memorizzare i risultati in base a una chiave composta da modalità, percorsi di input e hash del testo. Questo assicura che input identici non attivino ripetutamente l’inferenza del modello.

  • Il meccanismo di caching è critico perché componenti UI come gli slider attivano callback frequentemente. Senza caching, ogni interazione ri-eseguirebbe l’inferenza (fino a ~60 secondi), mentre con il caching i risultati vengono restituiti istantaneamente dopo la prima esecuzione.

  • L’interfaccia fornisce due tab: una con modalità a input singolo con slider del timestep per esplorare l’attività cerebrale nel tempo e una modalità di confronto che esegue due input e visualizza la loro differenza come mappa di calore di contrasto.

Infine, demo.launch() è configurato con share=True per generare un URL pubblico e server_name="0.0.0.0" per consentire l’accesso esterno, rendendo l’app facilmente distribuibile.

Osservazioni e insight pratici su TRIBE v2

Dopo aver eseguito la demo con input diversi (video, audio e testo), emergono alcuni pattern consistenti che aiutano a interpretare gli output di TRIBE v2. Alcuni insight dalla demo sono:

  • Dinamiche temporali: Man mano che l’input procede, l’attività cerebrale cambia nel tempo invece di restare statica. Noterai che l’attivazione cresce e si sposta tra le regioni, specialmente nei primi secondi. Questo riflette la natura ritardata del segnale sottostante e conferma che il modello cattura risposte dipendenti dal tempo.
  • Effetto degli input visivi sulle regioni posteriori: Negli esempi basati su video, le attivazioni più forti appaiono nella parte posteriore del cervello. Questo è in linea con le regioni di elaborazione visiva, indicando che il modello risponde correttamente agli stimoli visivi.
  • Mappe di contrasto: Quando si confrontano due input, la mappa di differenza è spesso più informativa delle singole mappe. Invece di un’attivazione diffusa ovunque, il contrasto evidenzia dove il cervello risponde in modo diverso a ciascuno stimolo, facilitando l’interpretazione dell’effetto delle diverse modalità.

Trappole comuni

Il modello non pretende di essere accurato al 100% e presenta alcune insidie:

  • Mappe rumorose: Si osserva che input molto brevi (pochi secondi) producono spesso attivazioni diffuse e a bassa intensità, difficili da interpretare. Gli input devono avere una certa durata (15–30 secondi) per fornire abbastanza contesto al modello per generare pattern significativi.
  • Modalità mancanti: Se esegui audio o testo senza video, potresti vedere avvisi su alcuni estrattori rimossi. È previsto: il modello disabilita semplicemente i rami non utilizzati e continua con gli input disponibili.
  • Caching: Senza caching, ogni interazione dell’UI (come muovere lo slider) attiverebbe un’esecuzione completa del modello, rendendo la demo inutilizzabile. Con il caching attivato, le previsioni vengono calcolate una volta e riutilizzate, consentendo un’esplorazione fluida e in tempo reale.
  • Inconsistenze dell’ambiente: Qualsiasi cambiamento nelle dipendenze (soprattutto nelle versioni di NumPy) o una gestione impropria dei file (come file di testo non flushati) può portare a errori silenziosi.

Limitazioni

TRIBE v2 è un potente strumento di ricerca, ma ha importanti vincoli che influenzano come interpretare i suoi output. Comprendere questi limiti è essenziale prima di trarre conclusioni scientifiche o cliniche dalle previsioni.

  • Soggetto medio: Le previsioni rappresentano la media della popolazione. I cervelli individuali differiscono per anatomia corticale, organizzazione funzionale e profilo di rumore. Il fine-tuning su ~1 ora di dati fMRI individuali è supportato dal modello, ma è oltre lo scopo di questo tutorial.
  • Risoluzione fMRI: Il segnale BOLD ha ~1 Hz di risoluzione temporale e ~4 mm di risoluzione spaziale. TRIBE v2 eredita entrambi i limiti e non può catturare dinamiche neurali a millisecondi o dettagli spaziali sotto-girali.
  • Osservatore passivo: Il modello prevede risposte a stimoli presentati a un osservatore passivo. Non ha una rappresentazione di attenzione, output motorio, interazione sociale o altri stati cognitivi attivi.
  • Ambito delle modalità: Solo visione, udito e linguaggio sono modellati. Modalità come olfatto, tatto, propriocezione e dolore sono assenti.
  • Non è uno strumento clinico: Le previsioni non dovrebbero essere usate per diagnosi, pianificazione del trattamento o applicazioni cliniche.

Conclusione

In questo tutorial, abbiamo costruito una pipeline TRIBE v2 funzionante su Google Colab A100: dalla risoluzione di due bug concreti (il conflitto di versione NumPy 2.x e il timeout di download su HuggingFace) all’esecuzione di vere previsioni corticali, fino a visualizzarle come mappe di calore 3D interattive e condurre un esperimento di confronto che replica il paradigma in silico dell’articolo.

Le quattro lezioni di engineering più importanti da questo tutorial sono: 

  1. Fissa NumPy a <2.1 e riavvia il runtime prima di installare tribev2

  2. Imposta HF_HUB_DOWNLOAD_TIMEOUT=300 e pre-scarica LLaMA con snapshot_download prima di chiamare model.predict()

  3. Scrivi sempre → flush() → fsync() → close() i file temporanei prima di passare il loro percorso al modello

  4. Metti in cache le previsioni in un dizionario, così le interazioni con lo slider dell’UI non ri-eseguono l’inferenza.

Da qui, spiccano due estensioni naturali. La prima sono stimoli più ricchi: veri clip di film o segmenti di podcast di 30–60 secondi producono dinamiche temporali e pattern spaziali molto più chiari rispetto ai brevi passaggi di testo. 

La seconda è il fine-tuning individuale: con ~1 ora di dati fMRI di un soggetto specifico, il blocco soggetto di TRIBE v2 può essere fine-tunato in un’epoca per produrre previsioni personalizzate che superano il modello medio di gruppo di 2–4x secondo i risultati dell’articolo.

Il notebook completo è disponibile sul repository GitHub di TRIBE v2. Vale la pena leggere l’articolo per intero, in particolare la Sezione 2.5 (esperimenti di visione in silico) e la Sezione 2.8 (insight sull’integrazione multimodale), che mostrano cosa rende possibile questo tipo di strumenti per la ricerca in neuroscienze.

FAQ del tutorial TRIBE v2

Di quale GPU ho davvero bisogno per eseguire TRIBE v2?

Ti servono almeno 40 GB di VRAM per l’intera pipeline trimodale. L’A100 40 GB su Colab Pro è l’opzione minima praticabile. Se usi solo input audio e salti testo e video, potresti rientrare in una L4 (24 GB), ma è da testare.

Posso saltare il passaggio di autenticazione su HuggingFace?

Sì, se eviti completamente l’input testuale perché LLaMA 3.2-3B viene scaricato solo quando model.predict() è chiamato con eventi di testo. Se usi solo input audio o video, l’estrattore di testo non viene mai inizializzato e non è richiesto alcun token HuggingFace. I pesi dell’encoder TRIBE su facebook/tribev2 non sono soggetti a restrizioni.

Perché il cervello non mostra alcun pattern di attivazione, solo un colore uniforme e basso?

Le tre cause più comuni sono:

  • L’input potrebbe essere troppo breve, quindi usa almeno 15–30 secondi di input.

  • La soglia potrebbe sopprimere segnali reali. Prova a ridurre la soglia da '20%' a '5%' in render_hemi()

  • Se il file text temp era vuoto a causa del bug di flush/close, allora aggiungi os.fsync() e tmp.close() prima di chiamare get_events_dataframe().

Come si confronta questo con la demo interattiva ufficiale di Meta?

Il modello sottostante e i pesi sono identici. La demo di Meta usa un renderer WebGL personalizzato con una silhouette della testa e controlli di riproduzione video sincronizzati con l’animazione cerebrale. La nostra demo Gradio usa invece nilearn.plotting.view_surf, che renderizza la stessa mesh “inflated” fsaverage5 con la stessa mappa colori “hot” tramite il motore WebGL di Plotly.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.

Argomenti
Deep Learning
Large Language Models
AI generativa

Corsi di Deep Learning

Programma

Apprendimento profondo in Python

18 ore
Continua il tuo viaggio nell'apprendimento automatico e nell'apprendimento profondo. Usa la libreria PyTorch per creare reti neurali per modellare diversi tipi di dati.
Vedi i dettagliRight Arrow
Inizia Il Corso
Mostra altroRight Arrow
Correlato

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

I 15 migliori server MCP remoti che ogni AI builder dovrebbe conoscere nel 2026

Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Abid Ali Awan's photo

Abid Ali Awan

15 min

blog

Che cos'è Snowflake? Guida per principianti alla piattaforma dati cloud

Esplora le basi di Snowflake, la piattaforma dati cloud. Scopri la sua architettura, le sue funzionalità e come integrarla nelle tue pipeline di dati.
Tim Lu's photo

Tim Lu

12 min

Mostra AltroMostra Altro