Filosofia di TensorFlow di Hugging Face

'Hugging Face's TensorFlow Philosophy'.

Introduzione

Nonostante la crescente concorrenza di PyTorch e JAX, TensorFlow rimane il framework di deep learning più utilizzato. Differisce anche in alcuni modi molto importanti rispetto alle altre due librerie. In particolare, è strettamente integrato con il suo API di alto livello Keras e la sua libreria di caricamento dei dati tf.data.

C’è una tendenza tra gli ingegneri PyTorch (immaginami che guardo cupamente l’ufficio open-plan qui) a considerare questo come un problema da superare; il loro obiettivo è capire come fare in modo che TensorFlow si metta da parte in modo da poter utilizzare il codice di addestramento a basso livello e di caricamento dati a cui sono abituati. Questo è completamente il modo sbagliato di affrontare TensorFlow! Keras è un ottimo API di alto livello. Se lo metti da parte in qualsiasi progetto più grande di un paio di moduli, finirai per riprodurre gran parte della sua funzionalità da solo quando ti rendi conto di averne bisogno.

Come ingegneri raffinati, rispettati e altamente attraenti di TensorFlow, vogliamo utilizzare la potenza e la flessibilità incredibili dei modelli all’avanguardia, ma vogliamo gestirli con gli strumenti e l’API con cui siamo familiari. Questo post del blog parlerà delle scelte che facciamo presso Hugging Face per consentirlo e di cosa aspettarsi dal framework come programmatore TensorFlow.

Intermezzo: 30 secondi a 🤗

Gli utenti esperti possono tranquillamente scorrere o saltare questa sezione, ma se è il tuo primo incontro con Hugging Face e transformers, dovrei iniziare dando un’overview dell’idea principale della libreria: basta richiedere un modello preaddestrato per nome e lo ottieni in una sola riga di codice. Il modo più semplice è semplicemente utilizzare la classe TFAutoModel:

from transformers import TFAutoModel

model = TFAutoModel.from_pretrained("bert-base-cased")

Questa riga istanzierà l’architettura del modello e caricherà i pesi, fornendoti una replica esatta del famoso modello BERT originale. Questo modello non farà molto da solo, però, manca di una testa di output o di una funzione di perdita. In effetti, è il “tronco” di una rete neurale che si ferma subito dopo l’ultimo strato nascosto. Quindi, come si mette una testa di output? Semplice, basta usare una diversa classe AutoModel. Qui carichiamo il modello Vision Transformer (ViT) e aggiungiamo una testa di classificazione delle immagini:

from transformers import TFAutoModelForImageClassification

model_name = "google/vit-base-patch16-224"
model = TFAutoModelForImageClassification.from_pretrained(model_name)

Ora il nostro model ha una testa di output e, facoltativamente, una funzione di perdita appropriata per il nuovo compito. Se la nuova testa di output differisce dal modello originale, allora i suoi pesi verranno inizializzati casualmente. Tutti gli altri pesi verranno caricati dal modello originale. Ma perché facciamo questo? Perché dovremmo usare il tronco di un modello esistente anziché creare il modello di cui abbiamo bisogno da zero?

Risulta che i modelli di grandi dimensioni preaddestrati su molti dati sono punti di partenza molto migliori per quasi qualsiasi problema di apprendimento automatico rispetto al metodo standard di inizializzare casualmente i pesi. Questo è chiamato transfer learning, e se ci pensi, ha senso: risolvere bene un compito testuale richiede una certa conoscenza del linguaggio, e risolvere bene un compito visivo richiede una certa conoscenza delle immagini e dello spazio. La ragione per cui l’apprendimento automatico richiede così tanti dati senza il transfer learning è semplicemente che questa conoscenza di base del dominio deve essere riacquisita da zero per ogni problema, il che richiede un enorme volume di esempi di addestramento. Tuttavia, utilizzando il transfer learning, un problema può essere risolto con mille esempi di addestramento che potrebbero averne richiesti un milione senza di esso, e spesso con una maggiore accuratezza finale. Per saperne di più su questo argomento, consulta le sezioni pertinenti del Corso Hugging Face!

Tuttavia, quando si utilizza il transfer learning, è molto importante elaborare gli input del modello allo stesso modo in cui sono stati elaborati durante l’addestramento. Ciò garantisce che il modello debba riconoscere il meno possibile quando trasferiamo la sua conoscenza a un nuovo problema. In transformers, questa pre-elaborazione è spesso gestita con i tokenizer. I tokenizer possono essere caricati allo stesso modo dei modelli, utilizzando la classe AutoTokenizer. Assicurati di caricare il tokenizer corrispondente al modello che desideri utilizzare!

from transformers import TFAutoModel, AutoTokenizer

# Assicurati sempre di caricare un tokenizer e un modello corrispondenti!
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
model = TFAutoModel.from_pretrained("bert-base-cased")

# Carichiamo alcuni dati e li tokenizziamo
test_strings = ["Questa è una frase!", "Questa è un'altra!"]
tokenized_inputs = tokenizer(test_strings, return_tensors="np", padding=True)

# Ora i nostri dati sono tokenizzati, possiamo passarli al nostro modello o usarli in fit()!
outputs = model(tokenized_inputs)

Questo è solo un assaggio della libreria, naturalmente – se vuoi di più, puoi dare un’occhiata ai nostri notebook o ai nostri esempi di codice. Ci sono anche diversi altri esempi della libreria in azione su keras.io!

A questo punto, hai compreso alcuni dei concetti di base e delle classi in transformers. Tutto ciò che ho scritto finora è indipendente dal framework (ad eccezione di “TF” in TFAutoModel), ma quando vuoi effettivamente addestrare e servire il tuo modello, è lì che le cose inizieranno a divergere tra i framework. E questo ci porta al focus principale di questo articolo: come ingegnere TensorFlow, cosa dovresti aspettarti da transformers?

Filosofia n. 1: Tutti i modelli TensorFlow dovrebbero essere oggetti di tipo Keras Model e tutti i layer TensorFlow dovrebbero essere oggetti di tipo Keras Layer.

Questo è quasi scontato per una libreria TensorFlow, ma vale comunque la pena sottolinearlo. Dal punto di vista dell’utente, l’effetto più importante di questa scelta è che puoi chiamare direttamente i metodi Keras come fit(), compile() e predict() sui nostri modelli.

Ad esempio, assumendo che i tuoi dati siano già preparati e tokenizzati, ottenere previsioni da un modello di classificazione di sequenze con TensorFlow è semplice come:

model = TFAutoModelForSequenceClassification.from_pretrained(my_model)
model.predict(my_data)

E se vuoi invece addestrare quel modello, è semplicemente:

model.fit(my_data, my_labels)

Tuttavia, questa comodità non significa che sei limitato alle attività che supportiamo di default. I modelli Keras possono essere composti come layer in altri modelli, quindi se hai un’idea geniale che coinvolge la fusione di cinque modelli diversi, non c’è nulla che ti fermi, tranne forse la memoria limitata della GPU. Forse vuoi unire un modello di linguaggio preaddestrato con un vision transformer preaddestrato per creare un ibrido, come il recente Flamingo di Deepmind, o vuoi creare la prossima sensazione virale di testo-immagine come Dall-E Mini Craiyon? Ecco un esempio di un modello ibrido che utilizza il subclassing di Keras:

class HybridVisionLanguageModel(tf.keras.Model):
  def __init__(self):
    super().__init__()
    self.language = TFAutoModel.from_pretrained("gpt2")
    self.vision = TFAutoModel.from_pretrained("google/vit-base-patch16-224")

  def call(self, inputs):
    # Ho un'idea veramente meravigliosa per questo
    # che in questo riquadro di codice è troppo breve per contenere

Filosofia n. 2: Le funzioni di perdita sono fornite per impostazione predefinita, ma possono essere facilmente modificate.

In Keras, il modo standard per addestrare un modello è crearlo, quindi compilarlo con un ottimizzatore e una funzione di perdita, e infine addestrarlo con fit(). È molto facile caricare un modello con transformers, ma impostare la funzione di perdita può essere complicato – anche per l’addestramento di modelli di linguaggio standard, la funzione di perdita può essere sorprendentemente non ovvia, e alcuni modelli ibridi hanno perdite estremamente complesse.

La nostra soluzione a questo è semplice: se compili senza un argomento di perdita, ti forniremo quella che probabilmente desideri. In particolare, ti forniremo una che sia compatibile sia con il tuo modello di base che con il tipo di output: se compili un modello di linguaggio mascherato basato su BERT senza una perdita, ti forniremo una perdita di modellazione del linguaggio mascherato che gestisce correttamente il padding e la mascheratura, e calcolerà le perdite solo sui token corrotti, corrispondendo esattamente al processo di addestramento BERT originale. Se per qualche motivo davvero non vuoi che il tuo modello venga compilato con alcuna perdita, specifica semplicemente loss=None durante la compilazione.

model = TFAutoModelForQuestionAnswering.from_pretrained("bert-base-cased")
model.compile(optimizer="adam")  # Nessun argomento di perdita!
model.fit(my_data, my_labels)

Ma anche, e molto importantemente, vogliamo lasciarti libero di fare qualcosa di più complesso non appena lo desideri. Se specifici un argomento di perdita a compile(), allora il modello utilizzerà quello al posto della perdita predefinita. E, naturalmente, se crei il tuo modello subclassificato come l’HybridVisionLanguageModel sopra, hai il controllo completo su ogni aspetto della funzionalità del modello tramite i metodi call() e train_step() che scrivi.

Filosofia Dettaglio di Implementazione #3: Le etichette sono flessibili

Una fonte di confusione in passato era dove esattamente le etichette dovessero essere passate al modello. Il modo standard per passare le etichette a un modello Keras è come un argomento separato, o come parte di una tupla (input, etichette):

model.fit(input, etichette)

In passato, invece, chiedevamo agli utenti di passare le etichette nel dizionario di input quando si utilizzava la perdita predefinita. Il motivo di ciò era che il codice per il calcolo della perdita per quel particolare modello era contenuto nel metodo di passaggio in avanti call(). Questo funzionava, ma era sicuramente non standard per i modelli Keras e causava diversi problemi, tra cui incompatibilità con le metriche standard di Keras, senza parlare della confusione degli utenti. Fortunatamente, ciò non è più necessario. Ora raccomandiamo che le etichette vengano passate nel modo normale di Keras, anche se il vecchio metodo funziona ancora per motivi di compatibilità all’indietro. In generale, molte cose che erano complicate ora dovrebbero “funzionare semplicemente” per i nostri modelli TensorFlow: provateli!

Filosofia #4: Non dovresti dover scrivere la tua pipeline di dati, specialmente per compiti comuni

Oltre ai transformer, un enorme repository aperto di modelli pre-addestrati, c’è anche 🤗 datasets, un enorme repository aperto di dataset – testo, visione, audio e altro ancora. Questi dataset si convertono facilmente in Tensor TensorFlow e array Numpy, rendendoli facili da usare come dati di addestramento. Ecco un esempio rapido che ci mostra come tokenizzare un dataset e convertirlo in Numpy. Come sempre, assicurati che il tuo tokenizer corrisponda al modello con cui desideri allenarti, altrimenti le cose diventeranno molto strane!

from datasets import load_dataset
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
from tensorflow.keras.optimizers import Adam

dataset = load_dataset("glue", "cola")  # Semplice dataset di classificazione testuale
dataset = dataset["train"]  # Prendiamo solo la divisione di addestramento per ora

# Carica il nostro tokenizer e tokenizza i nostri dati
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenized_data = tokenizer(dataset["text"], return_tensors="np", padding=True)
labels = np.array(dataset["label"]) # L'etichetta è già un array di 0 e 1

# Carica e compila il nostro modello
model = TFAutoModelForSequenceClassification.from_pretrained("bert-base-cased")
# I tassi di apprendimento più bassi sono spesso migliori per il fine-tuning dei transformer
model.compile(optimizer=Adam(3e-5))

model.fit(tokenized_data, labels)

Questo approccio è ottimo quando funziona, ma per dataset più grandi potresti trovarlo iniziare a diventare un problema. Perché? Perché l’array tokenizzato e le etichette dovrebbero essere completamente caricati in memoria e perché Numpy non gestisce array “irregolari”, quindi ogni campione tokenizzato dovrebbe essere riempito alla lunghezza del campione più lungo dell’intero dataset. Questo renderà il tuo array ancora più grande e tutti quei token di riempimento rallenteranno anche l’addestramento!

Come ingegnere TensorFlow, normalmente è qui che si ricorre a tf.data per creare una pipeline che trasmetterà i dati dallo storage anziché caricarli tutti in memoria. Tuttavia, è una seccatura, quindi abbiamo pensato a te. Prima di tutto, utilizziamo il metodo map() per aggiungere le colonne del tokenizer al dataset. Ricorda che i nostri dataset sono di default supportati su disco, quindi non verranno caricati in memoria fino a quando non li converti in array!

def tokenize_dataset(data):
    # Le chiavi del dizionario restituito verranno aggiunte al dataset come colonne
    return tokenizer(data["text"])

dataset = dataset.map(tokenize_dataset)

Ora il nostro dataset ha le colonne che vogliamo, ma come lo addestriamo? Semplice: avvolgilo con un tf.data.Dataset e tutti i nostri problemi saranno risolti: i dati vengono caricati al volo e il padding viene applicato solo ai batch anziché all’intero dataset, il che significa che abbiamo bisogno di molti meno token di riempimento:

tf_dataset = model.prepare_tf_dataset(
    dataset,
    batch_size=16,
    shuffle=True
)

model.fit(tf_dataset)

Perché prepare_tf_dataset() è un metodo sul tuo modello? Semplice: perché il tuo modello sa quali colonne sono valide come input e filtra automaticamente le colonne nel dataset che non sono nomi di input validi! Se preferisci avere un controllo più preciso sul tf.data.Dataset che viene creato, puoi utilizzare il metodo di livello inferiore Dataset.to_tf_dataset() invece.

Filosofia #5: XLA è fantastico!

XLA è il compilatore just-in-time condiviso da TensorFlow e JAX. Converte il codice di algebra lineare in versioni ottimizzate che si eseguono più velocemente e utilizzano meno memoria. È davvero fantastico e cerchiamo di supportarlo il più possibile. È estremamente importante per consentire l’esecuzione dei modelli su TPU, ma offre un aumento di velocità anche per GPU e persino per CPU! Per utilizzarlo, basta compilare il tuo modello con l’argomento jit_compile=True (questo funziona per tutti i modelli Keras, non solo quelli di Hugging Face):

model.compile(optimizer="adam", jit_compile=True)

Abbiamo apportato di recente una serie di miglioramenti significativi in questa area. In particolare, abbiamo aggiornato il nostro codice generate() per utilizzare XLA: questa è una funzione che genera iterativamente l’output di testo dai modelli di linguaggio. Questo ha comportato un enorme miglioramento delle prestazioni: il nostro codice TF legacy era molto più lento rispetto a PyTorch, ma il nuovo codice è molto più veloce di esso e simile a JAX in velocità! Per ulteriori informazioni, consulta il nostro articolo sulle generazioni XLA.

Tuttavia, XLA è utile anche per altre cose oltre alla generazione! Abbiamo anche apportato una serie di correzioni per consentirti di addestrare i tuoi modelli con XLA, e di conseguenza i nostri modelli TF hanno raggiunto velocità simili a quelle di JAX per attività come l’addestramento di modelli di linguaggio.

È importante essere chiari sulla principale limitazione di XLA: XLA si aspetta che le dimensioni di input siano statiche. Ciò significa che se il tuo compito coinvolge lunghezze di sequenza variabili, dovrai eseguire una nuova compilazione XLA per ogni diversa forma di input che passi al tuo modello, il che può annullare i vantaggi delle prestazioni! Puoi vedere alcuni esempi su come affrontiamo questo nei nostri notebook TensorFlow e nell’articolo sulle generazioni XLA sopra citato.

Filosofia #6: Il deployment è altrettanto importante dell’addestramento

TensorFlow ha un ecosistema ricco, soprattutto per quanto riguarda il deployment dei modelli, che manca agli altri framework più orientati alla ricerca. Stiamo lavorando attivamente per consentirti di utilizzare questi strumenti per deployare l’intero modello per l’elaborazione. Siamo particolarmente interessati a supportare TF Serving e TFX. Se ti interessa, dai un’occhiata al nostro articolo sul deployment dei modelli con TF Serving!

Un ostacolo importante nel deployment dei modelli NLP, tuttavia, è che gli input dovranno comunque essere tokenizzati, il che significa che non è sufficiente solo deployare il modello. Una dipendenza da tokenizers può essere fastidiosa in molti scenari di deployment, quindi stiamo lavorando per rendere possibile incorporare la tokenizzazione nel tuo stesso modello, consentendoti di deployare un singolo artefatto di modello per gestire l’intero flusso di lavoro, dalle stringhe di input alle previsioni di output. Al momento, supportiamo solo i modelli più comuni come BERT, ma è un’area di lavoro attiva! Se vuoi provarlo, puoi utilizzare uno snippet di codice come questo:

# This is a new feature, so make sure to update to the latest version of transformers!
# You will also need to pip install tensorflow_text

import tensorflow as tf
from transformers import TFAutoModel, TFBertTokenizer


class EndToEndModel(tf.keras.Model):
    def __init__(self, checkpoint):
        super().__init__()
        self.tokenizer = TFBertTokenizer.from_pretrained(checkpoint)
        self.model = TFAutoModel.from_pretrained(checkpoint)

    def call(self, inputs):
        tokenized = self.tokenizer(inputs)
        return self.model(**tokenized)

model = EndToEndModel(checkpoint="bert-base-cased")

test_inputs = [
    "This is a test sentence!",
    "This is another one!",
]
model.predict(test_inputs)  # Pass strings straight to model!

Conclusion: Siamo un progetto open-source e ciò significa che la comunità è tutto

Hai creato un modello interessante? Condividilo! Una volta creato un account e impostato le tue credenziali, è semplice come:

model_name = "google/vit-base-patch16-224"
model = TFAutoModelForImageClassification.from_pretrained(model_name)

model.fit(my_data, my_labels)

model.push_to_hub("my-new-model")

Puoi anche utilizzare il PushToHubCallback per caricare regolarmente i checkpoint durante una sessione di addestramento più lunga! In entrambi i casi, otterrai una pagina del modello e una scheda del modello generata automaticamente, e, cosa più importante, chiunque altro potrà utilizzare il tuo modello per ottenere previsioni o come punto di partenza per ulteriori addestramenti, utilizzando la stessa API utilizzata per caricare qualsiasi modello esistente.

model_name = "il-tuo-nome-utente/il-mio-nuovo-modello"
model = TFAutoModelForImageClassification.from_pretrained(model_name)

Credo che il fatto che non ci sia una distinzione tra grandi modelli di fondazione famosi e modelli addestrati da un singolo utente esemplifichi la convinzione centrale di Hugging Face: il potere degli utenti di costruire grandi cose. L’apprendimento automatico non è mai stato concepito come un flusso di risultati da modelli chiusi detenuti solo da alcune aziende selezionate; dovrebbe essere una collezione di strumenti aperti, artefatti, pratiche e conoscenze che vengono costantemente ampliati, testati, criticati e sviluppati – un bazar, non una cattedrale. Se hai una nuova idea, un nuovo metodo o addestri un nuovo modello con ottimi risultati, fai sapere a tutti!

E, in una vena simile, ci sono cose che ti mancano? Bug? Fastidi? Cose che dovrebbero essere intuitive ma non lo sono? Facci sapere! Se sei disposto a prendere una pala (metaforica) e iniziare a sistemarla, tanto meglio, ma non essere timido nel farlo anche se non hai tempo o competenze per migliorare il codice tu stesso. Spesso, i responsabili principali possono non notare i problemi perché gli utenti non li segnalano, quindi non assumere che noi debbano essere consapevoli di qualcosa! Se ti sta infastidendo, chiedi nei forum, oppure se sei abbastanza sicuro che si tratti di un bug o di una funzionalità importante mancante, segnalalo.

Molte di queste cose sono dettagli minori, certo, ma per usare una frase (piuttosto goffa), un grande software è fatto da migliaia di piccoli commit. È attraverso il costante sforzo collettivo di utenti e responsabili che il software open source migliora. L’apprendimento automatico sarà una questione sociale importante negli anni 2020, e la forza del software e delle comunità open source determinerà se diventerà una forza aperta e democratica aperta a critiche e rivalutazioni, oppure se sarà dominata da giganteschi modelli black-box i cui proprietari non permetteranno a estranei, anche a coloro di cui i modelli prendono decisioni, di vedere i loro preziosi pesi proprietari. Quindi non essere timido – se c’è qualcosa che non va, se hai un’idea su come potrebbe essere fatto meglio, se vuoi contribuire ma non sai dove, allora dicci!

(E se riesci a creare un meme per prendere in giro il team di PyTorch dopo che la tua nuova fantastica funzionalità viene integrata, tanto meglio.)