Sitemap

Il transformer illustrato — IT

18 min readJun 25, 2023

Traduzione italiana di The illustrated Transformer by Jay Alammar
Non sono un traduttore professionista.
La proprietà intellettuale dell’articolo è di Jay Alammar

Italian translation of The illustrated Transformer by Jay Alammar
I’m not a professional translator.
The intellectual property of the article is owned by Jay Alammar

Nel post precedente, abbiamo esaminato l’Attention — un metodo onnipresente nei moderni modelli di deep learning. L’attention è uno strumento che ha contribuito a migliorare le prestazioni delle applicazioni di traduzione automatica che utilizzano modelli neurali. In questo post, esamineremo Il Transformer, un modello che utilizza l’attention per aumentare la velocità con cui queste reti possono essere addestrate. Il Trasformer ha perfino supera il modello di traduzione automatica neurale di Google in attività specifiche. Il più grande vantaggio, tuttavia, deriva dal modo in cui il Transformer si presta alla parallelizzazione. È infatti raccomandazione di Google Cloud sfruttare il Transformer come modello di riferimento per utilizzare la loro proposte di Cloud TPU. Proviamo a scomporre il modello e vediamo come funziona.

Il Transformer è stato proposto nell’articolo Attention is All You Need. Una sua implementazione TensorFlow è disponibile come parte del pacchetto Tensor2Tensor. Il gruppo NLP di Harvard ha creato una guida che spiega l’articolo con implementazioni in PyTorch. In questo post, cercheremo di semplificare un po’ le cose e di introdurre i concetti uno per uno, sperando che sia più facile da capire per le persone senza una conoscenza approfondita dell’argomento.

Aggiornamento 2020: Ho creato il video “Transformer narrati” che è un approccio più soft all’argomento:

Uno sguardo generale

Iniziamo osservando il modello come fosse una scatola nera. In un’applicazione di traduzione automatica, prenderebbe una frase in una lingua e restituirebbe la sua traduzione in un’altra.

Press enter or click to view image in full size

Aprendo quella meraviglia di Optimus Prime, vediamo un componente di codifica, un componente di decodifica e le connessioni tra di essi.

Press enter or click to view image in full size

Il componente di codifica è uno stack di encoders (il paper ne concatena sei uno dietro l’altro — non c’è nulla di magico nel numero sei, si possono sicuramente fare esperimenti con altre configurazioni). Il componente di decodifica è una concatenazione di altri sei decoders.

Press enter or click to view image in full size

Gli encoders sono tutti identici nella struttura (ma non condividono i pesi). Ognuno è suddiviso in due sottolivelli:

Press enter or click to view image in full size

Gli input dell’encoder passano prima attraverso uno strato di self-attention, uno strato che permette all’encoder di analizzare altre parole nella frase di input mentre codifica una parola specifica. Analizzeremo più da vicino la self-attention in seguito nel post.

Le uscite dello strato di self-attention vengono inviate a una rete neurale feed-forward. La stessa rete feed-forward viene applicata indipendentemente a ogni parola.

Il decoder ha anche esso entrambi questi livelli, ma tra di essi c’è uno strato di attention che aiuta il decoder a focalizzarsi sulle parti rilevanti della frase di input (simile a ciò che fa l’attention nei modelli seq2seq).

Press enter or click to view image in full size

Introduzione dei tensori al meccanismo

Ora che abbiamo visto i principali componenti del modello, iniziamo a esaminare i vari vettori/tensori e come vengono elaborati tra questi componenti per trasformare l’input in output.

Come generalmente avviene nelle applicazioni di NLP, iniziamo trasformando ogni parola di input in un vettore utilizzando un algoritmo di embedding.

Press enter or click to view image in full size

Ogni parola viene convertita in un vettore di dimensione 512. Rappresenteremo questi vettori con semplici riquadri.

L’embedding della frase in input avviene solo prima di entrare nel primo encoder. La caratteristica comune a tutti gli endoders è che ricevono una lista di vettori, ognuno di dimensione 512 — Nel primo encoder l’input sarebbe direttamente l’output dell’algoritmo di embedding, mentre negli altri encoders sarebbe l’output dell’encoder sottostante. La dimensione di questa lista è un iperparametro che possiamo impostare — idealmente corrisponderà alla lunghezza della frase più lunga nel nostro set di dati di addestramento.

Dopo l’embedding delle parole della sequenza iniziale, ciascuna di esse viene processata dai due strati dell’encoder.

Press enter or click to view image in full size

Qui iniziamo a vedere una caratteristica chiave del Transformer, ovvero che l’encoder processa singolarmente ogni parola. Verrebbe da pensare che non ci siano correlazioni tra le parole ma non è cosi infatti le dipendenze tra i termini sono introdotte nella self-attention. Lo strato feed-forward, tuttavia, non ha tali dipendenze, e quindi i vari percorsi possono essere parallelizzati.

Successivamente, cambieremo l’esempio con una frase più breve e osserveremo cosa accade in ciascun sottolivello dell’encoder.

Stiamo codificando!

Come abbiamo già accennato, l’encoder riceve una lista di vettori in input che elabora attraverso uno strato di ‘self-attention’, per poi processarli utilizzando una rete neurale feed-forward la quale genera l’output per l’encoder successivo.

Press enter or click to view image in full size

Ogni parola passa attraverso un processo di self-attention. Per poi attraversare una rete neurale feed-forward — la stessa rete in cui ogni vettore viene processato separatamente.

Self-Attention in generale

Non lasciarti ingannare dal fatto che io utilizzi la parola “self-attention” come se fosse un concetto che tutti dovrebbero conoscere. Personalmente, non ero mai venuto a conoscenza di questo concetto prima di leggere l’articolo “Attention is All You Need”. Vediamo come funziona.

Supponiamo che la seguente frase sia una frase di input che vogliamo tradurre:

The animal didn't cross the street because it was too tired "

A cosa si riferisce “it” in questa frase? Si riferisce alla strada o all’animale? È una domanda semplice per un essere umano, ma non altrettanto semplice per un algoritmo.

Quando il modello elabora la parola “it”, la self-attention gli consente di associare “it” a “animal”.

Man mano che il modello elabora ogni parola (ogni posizione nella sequenza di input), la self-attention gli consente di guardare altre posizioni nella sequenza di input per trovare indizi che possano aiutarlo a ottenere una migliore codifica della parola.

Se hai familiarità con le reti neurali ricorrenti (RNN), pensa a come mantenere uno stato nascosto consenta a un’RNN di confrontare la rappresentazione delle parole/vettori precedenti che ha elaborato con quella corrente che sta elaborando. La self-attention è il metodo che il Transformer utilizza per confrontare la “comprensione” di parole precedenti con quella che stà elaborando attualmente.

Mentre stiamo codificando la parola “it” nell’encoder n. 5 (l’encoder superiore nello stack), parte del meccanismo di attention si stava concentrando su “The animal” e ha incorporato una parte della sua rappresentazione nella codifica di “it”.

Di un’occhiata al notebook Tensor2Tensor in cui puoi caricare un modello Transformer e esaminarlo utilizzando questa visualizzazione interattiva.

Self-Attention in Dettaglio

Iniziamo guardando come calcolare la self-attention utilizzando vettori e successivamente vedremo come viene effettivamente implementata — utilizzando le matrici.

Il primo passo per calcolare la self-attention è quello di creare tre vettori da ciascun vettore di input dell’encoder (in questo caso, l’embedding di ogni parola). Quindi, per ogni parola, creiamo un vettore di Query, un vettore di Key e un vettore di Value. Questi vettori vengono creati moltiplicando l’embedding per tre matrici che abbiamo addestrato durante il processo di training.

Nota che questi nuovi vettori sono di dimensioni più piccole rispetto al vettore di embedding. La loro dimensionalità è di 64, mentre l’embedding e i vettori di input/output dell’encoder hanno una dimensionalità di 512. Non è obbligatorio che siano più piccoli, è una scelta architetturale per rendere il calcolo della multiheaded attention (in gran parte) costante.

Press enter or click to view image in full size

Moltiplicando x1 per la matrice dei pesi WQ otteniamo q1, il vettore “query” associato a quella parola. Allo stesso modo creiamo, “key” e “value” (con matrici differenti), per ogni parola nella frase di input.

Cosa sono i vettori “query”, “key” e “value”?

Sono astrazioni utili per calcolare e comprendere l’attention. Una volta conclusa la lettura che segue su come viene calcolata l’attention, saprai praticamente tutto sul ruolo che ciascuno di questi vettori svolge.

Il secondo passo della self-attention è calcolare uno score. Immaginiamo di applicare la self-attention alla prima parola in questo esempio, “Thinking”. Dobbiamo valutare ogni parola della frase di input rispetto a questa parola. Lo score determinerà quanto focalizzare le altre parti della frase di input durante la codifica della parola considerata.

Lo score viene calcolato facendo il prodotto scalare tra il vettore di query della parola che stiamo considerando, con il vettore di key della parola con cui la vogliamo confrontare. Quindi, se stiamo elaborando la self-attention per la parola in posizione #1, il primo score sarebbe il prodotto scalare tra q1 e k1. Il secondo score sarebbe il prodotto scalare tra q1 e k2.

Press enter or click to view image in full size

Il terzo e quarto passaggio consistono nel dividere i punteggi per 8 (la radice quadrata della dimensione dei vettori key utilizzati nell’articolo, ossia 64). Questo per ottenere gradienti più stabili. Potremmo utilizzare altri valori ma questo è quello predefinito. Successivamente viene applicata la softmax al risultato, quest’ultima normalizza i punteggi in modo che siano tutti positivi e la loro somma sia uguale a 1.

Press enter or click to view image in full size

Il punteggio della softmax determina quanto ogni parola sarà correlata a questa posizione. Chiaramente, la parola in questa posizione avrà il punteggio softmax più alto.

Il quinto passaggio consiste nel moltiplicare ciascun vettore di value per il risultato della softmax (per poi sommarli). L’intuizione qui è quella di mantenere più alti i valori della/e parola/e su cui vogliamo concentrarci mentre rendere molto bassi quelli per le parole irrilevanti (andandoli a moltiplicare per numeri piccoli).

Il sesto passaggio serve a sommare i vettori di value precedentemente pesati con i valori della softmax. Questo produce l’output del layer di self-attention per questa posizione (ovvero per la prima parola).

Press enter or click to view image in full size

Questo conclude il calcolo della self-attention. Il vettore risultante è quello che possiamo inviare al layer di feed-forward. Nell’implementazione effettiva, tuttavia, questo calcolo viene effettuato in forma matriciale per una elaborazione più veloce. Vediamo l’implementazione effettiva ora che abbiamo compreso l’intuizione del calcolo a livello di singola parola.

Calcolo Matriciale per la Self-Attention

Il primo passo consiste nel calcolare le matrici di Query, Key e Value. Lo facciamo raggruppando le parole in una matrice X e moltiplicandola separatamente per le matrici di pesi che abbiamo allenato (WQ, WK, WV).

Ogni riga nella matrice X corrisponde a una parola nella frase di input. Possiamo notare la differenza di dimensione tra il vettore di embedding (512, o 4 caselle nella figura) e i vettori q/k/v (64, o 3 caselle nella figura).

Infine, dato che stiamo lavorando con matrici, possiamo condensare i passaggi dal due al sei in una formula per calcolare gli output del layer di self-attention.

Press enter or click to view image in full size

Calcolo della self-attention in forma matriciale.

La bestia con molte teste

Il paper ha ulteriormente migliorato il layer di self-attention aggiungendo un meccanismo chiamato “multi-headed” attention. Questo incrementa le prestazioni del layer in due modi:

  1. Aumenta la capacità del modello di concentrarsi su diverse posizioni. Nell’esempio precedente, z1 contiene un po’ di ogni altra codifica, ma potrebbe essere dominato dalla parola stessa. Se stiamo traducendo una frase come “The animal didn’t cross the street because it was too tired”, sarebbe utile sapere a quale parola si riferisce “it”.
  2. Fornisce al layer di attention più “sottospazi di rappresentazione”. Come vedremo in seguito, con la multi-headed attention non abbiamo solo un set di matrici di pesi Query/Key/Value (il Transformer utilizza otto attention distinte di modo da otteniamo otto set per ogni encoder/decoder). Ogni set di matrici viene inizializzato casualmente, per poi (dopo l’addestramento) essere utilizzato per proiettare gli embedding di input (o i vettori provenienti dagli encoder/decoder inferiori) in un diverso sottospazio di rappresentazione.
Press enter or click to view image in full size

Con la multi-headed attention, manteniamo matrici di pesi Q/K/V separati per ogni attention, ottenendo di conseguenza matrici Q/K/V differenti per ogni testa. Come abbiamo fatto prima, moltiplichiamo X per le matrici WQ/WK/WV per ottenere le matrici Q/K/V.

Se eseguiamo il calcolo di self-attention descritto sopra, con otto diverse matrici di pesi, otteniamo otto diverse matrici Z.

Press enter or click to view image in full size

Questo ci pone di fronte a un problema, il layer di feed-forward non si aspetta otto matrici, ma una sola (un vettore per ogni parola). Dobbiamo quindi condensarle in una singola matrice.

Come facciamo? Concateniamo le matrici e le moltiplichiamo per una matrice di pesi aggiuntiva WO.

Press enter or click to view image in full size

Praticamente è tutto quello che c’è da sapere sulla multi-headed self-attention. Mi rendo conto che ci sono parecchie matrici. Cercherò di metterle tutte in un’unica immagine in modo da poterle visualizzare tutte insieme.

Press enter or click to view image in full size

Ora che abbiamo parlato delle attention heads, torniamo all’esempio precedente per vedere su cosa si stanno concentrando le diverse attention mentre codifichiamo la parola “it” nella nostra frase di esempio:

Mentre codifichiamo la parola “it”, una delle attention si concentra principalmente su “the animal”, mentre un’altra si concentra su “tired” — in un certo senso, la rappresentazione della parola “it” nel modello include un po’ della rappresentazione sia di “animal” che di “tired”.

Tuttavia, se aggiungiamo tutte le attention all’immagine, può essere più difficile interpretarle:

Rappresentare l’Ordine della Sequenza Utilizzando l’Encoding Posizionale

Una cosa che manca nel modello, come lo abbiamo descritto finora, è un modo per tener conto dell’ordine delle parole nella sequenza iniziale.

Per affrontare questo problema, il Transformer aggiunge un vettore a ciascun embedding di input. Questi vettori seguono un pattern specifico che il modello impara, il quale aiuta a determinare la posizione di ogni parola, o la distanza tra diverse parole nella sequenza. L’intuizione qui è che aggiungere questi valori agli embeddings fornisce informazioni significative una volta che sono proiettati nei vettori Q/K/V e durante la dot-product attention.

Press enter or click to view image in full size

Per dare al modello il senso dell’ordine delle parole, aggiungiamo vettori di encoding posizionale, i cui valori seguono un pattern specifico.

Se assumiamo che l’embedding abbia una dimensionalità di 4, gli encoding posizionali effettivi sarebbero simili a questo:

Press enter or click to view image in full size

Un esempio reale di encoding posizionale con una dimensione di embedding esemplificativa di 4.

A che cosa potrebbe assomigliare questo pattern?

Nella figura seguente, ogni riga corrisponde a un encoding posizionale di un vettore. Quindi, la prima riga sarebbe il vettore che aggiungeremmo all’embedding della prima parola in una sequenza di input. Ogni riga contiene 512 valori, ognuno con un valore compreso tra 1 e -1. Li abbiamo colorati per rendere visibile il pattern.

Press enter or click to view image in full size

Un esempio reale di encoding posizionale per 20 parole (righe) con una dimensione di embedding di 512 (colonne). Come puoi vedere sembra essere diviso a metà lungo il centro. Questo perché i valori della metà sinistra sono generati da una funzione (che utilizza il seno), mentre la metà destra è generata da un’altra funzione (che utilizza il coseno). Vengono infine concatenati per formare ciascuno dei vettori di encoding posizionale.

La formula per la codifica posizionale è descritta nell’articolo (sezione 3.5). È possibile vedere il codice per generare le codifiche posizionali in get_timing_signal_1d(). Questo non è l'unico metodo possibile per la codifica posizionale. Tuttavia, offre il vantaggio di poter scalare a lunghezze di sequenze non viste in precedenza (ad esempio, se al nostro modello addestrato viene chiesto di tradurre una frase più lunga rispetto a quelle presenti nel nostro set di addestramento).

Aggiornamento luglio 2020: La codifica posizionale mostrata sopra proviene dall’implementazione Tranformer2Transformer del Transformer. Il metodo mostrato nell’articolo è leggermente diverso in quanto non concatena direttamente, ma intreccia i due segnali. La figura seguente mostra come appare. Qui il codice che lo genera::

Press enter or click to view image in full size

I Residuals

Un dettaglio dell’architettura dell’encoder che dobbiamo menzionare prima di procedere è che ogni sottolivello (self-attention, ffnn) di ogni encoder ha una residual connection e una layer-normalization.

Se dovessimo visualizzare i vettori e l’operazione di normalizzazione del layer associata alla self-attention, sarebbe così:

Press enter or click to view image in full size

Questo vale anche per i sottolivelli del decoder. Se pensassimo a un Transformer con 2 encoder e decoder che si susseguono, sarebbe qualcosa del genere:

Press enter or click to view image in full size

La parte del Decoder

Ora che abbiamo coperto la maggior parte dei concetti relativi al lato encoder, conosciamo anche il funzionamento dei componenti del decoder. Ma diamo un’occhiata a come lavorano insieme.

L’encoder inizia elaborando la sequenza di input. L’output dell’encoder superiore viene quindi trasformato in un insieme di vettori di attention K e V. Questi vettori verranno utilizzati da ciascun decoder nel suo strato di “encoder-decoder attention”, che aiuterà il decoder a concentrarsi sui punti appropriati nella sequenza di input:

Press enter or click to view image in full size

Dopo aver completato la fase di codifica, iniziamo la fase di decodifica. Ogni passaggio nella fase di decodifica produce un elemento dalla sequenza di output (la frase tradotta in inglese in questo caso).

I passaggi successivi ripetono il processo fino a quando non viene raggiunto un simbolo speciale di fine frase () che indica che il decoder del Transformer ha completato la sua generazione. L’output di ogni passaggio viene inserito al decoder inferiore nel passaggio successivo. E proprio come abbiamo fatto con gli input dell’encoder, incorporiamo e aggiungiamo una codifica di posizione a questi input del decoder per indicare la posizione di ciascuna parola.

Press enter or click to view image in full size

Gli strati di self-attention nel decoder operano in modo leggermente diverso rispetto a quelli nell’encoder:

Nel decoder, lo strato di self-attention tiene conto solo delle posizioni precedenti della sequenza di output. Ciò viene fatto mascherando le posizioni future (impostandole a -inf) prima del passaggio alla softmax nel calcolo della self-attention.

Lo strato di “Encoder-Decoder Attention” funziona proprio come la multiheaded self-attention, ad eccezione che crea la matrice delle Queries dallo strato sottostante e prende le matrici delle Keys e delle Values dall’output dell’encoder.

Lineare finale e Softmax.

La catena di decoder produce un vettore di numeri decimali. Come lo convertiamo in una parola? Questo è il compito del livello lineare finale e della Softmax.

Il livello lineare è un semplice fully connected network che proietta il vettore prodotto dai decoder in un vettore molto più ampio chiamato vettore dei logits.

Supponiamo che il nostro modello conosca 10.000 parole inglesi uniche (il “vocabolario di output”) apprese dal set di dati di addestramento. In questo caso avremmo un vettore di logits di lunghezza 10.000, ognuna corrispondente al punteggio di una parola unica. Questa è l’interpretazione dei valori ottenuti dopo il layer lineare.

È a questo punto che la Softmax trasforma questi valori in probabilità (tutte positive, che sommano a 1,0). Selezioniamo la cella con la probabilità più alta e la parola associata ad essa viene prodotta come output per questo passo della generazione.

Press enter or click to view image in full size

Si parte dal basso con il vettore prodotto come output dai decoder, il quale viene convertito in una parola in output.

Ora che abbiamo coperto l’intero processo di input output di un Transformer addestrato, sarebbe utile dare un’occhiata all’idea che si trova alla base del training del modello.

Durante l’addestramento, un modello non trainato passerebbe attraverso lo stesso processo appena illustrato, ma poiché lo stiamo addestrando su un set di dati etichettato, possiamo confrontare il suo output con l’output effettivamente corretto.

Per visualizzare questo, supponiamo che il nostro vocabolario di output contenga solo sei parole (“a”, “am”, “i”, “thanks”, “student”, and “<eos>” (abbreviato per ‘end of sentence’)).

Press enter or click to view image in full size

Il vocabolario di output del nostro modello viene creato nella fase di preelaborazione, prima di iniziare l’addestramento.

Una volta definito il vocabolario di output, possiamo utilizzare un vettore della stessa lunghezza per indicare ogni parola (codifica one-hot). Ad esempio, possiamo indicare la parola “am” utilizzando il seguente vettore:

Press enter or click to view image in full size

Esempio: codifica one-hot del vocabolario di output.

Dopo questo riepilogo, discutiamo la loss function del modello — ovvero la metrica che ottimizzeremo durante la fase di training.

Loss Function

Supponiamo di essere nel primo passo della fase di training del modello e che lo stiamo addestrando su un semplice esempio — tradurre “merci” in “thanks”.

L’output desiderato dovrebbe essere una distribuzione di probabilità piccata sulla parola “grazie”, ma poiché il modello non è ancora addestrato, ciò sarà improbabile che accada alla prima iterazione.

Press enter or click to view image in full size

Poiché i parametri (pesi) del modello sono inizializzati casualmente, il modello (non addestrato) produce una distribuzione di probabilità con valori arbitrari per ogni cella/parola. Possiamo confrontarla con l’output effettivo, quindi regolare tutti i pesi del modello utilizzando la backpropagation per avvicinare l’output del Transformer all’output desiderato.

Come confrontiamo due distribuzioni di probabilità? Semplicemente sottraendo una dall’altra. Per ulteriori dettagli, guarda cross-entropy e Kullback-Leibler divergence.

Questa è una prova semplificata. Per un esempio più realistico consideriamo — l’input: “je suis étudiant” e l’output previsto: “i am a student”. In questo caso vogliamo che il nostro modello produca successivamente distribuzioni di probabilità in cui:

  • Ogni distribuzione di probabilità è rappresentata da un vettore di larghezza vocab_size (6 nel nostro esempio (realisticamente un numero come 30.000 o 50.000))
  • La prima distribuzione di probabilità ha la probabilità più alta nella cella associata alla parola “i”
  • La seconda distribuzione di probabilità ha la probabilità più alta nella cella associata alla parola “am”
  • E così via, fino a quando la quinta distribuzione di output indica il simbolo ‘<end of sentence>' anch'esso associato a uno dei 10.000 elementi del vocabolario.
Press enter or click to view image in full size

Le distribuzioni di probabilità su cui traineremo il nostro modello per l’esempio campione.

Dopo aver addestrato il modello per un tempo sufficiente su un dataset abbastanza ampio, ci aspettiamo che le distribuzioni di probabilità prodotte assomiglino a queste:

Press enter or click to view image in full size

Dopo l’addestramento ci aspettiamo che il modello produca la traduzione. Naturalmente, questo non è un vero indicatore in quanto questa frase faceva parte del dataset di addestramento (vedi: cross validation). Notate che ogni posizione ottiene un po’ di probabilità anche se è improbabile che sia l’output prodotto: questa è una proprietà molto utile della softmax che permette il processo di addestramento.

Poiché il modello genera un output alla volta, possiamo assumere che selezioni la parola con la probabilità più alta e scarti il resto. Questo è uno dei possibili metodi chiamato decodifica greedy. Un altro modo sarebbe quello di mantenere, ad esempio, le prime due parole (‘I’ e ‘a’), per poi nel passaggio successivo, eseguire il modello due volte: una volta assumendo che la prima posizione di output sia la parola ‘I’ e un’altra che sia ‘un’. La versione che produce la loss function minore viene conservata. Ripetiamo questo per le posizioni #2 e #3…ecc. Questo metodo si chiama “beam search”, dove nel nostro esempio, la beam_size è due (due ipotesi parziali (traduzioni incomplete) vengono conservate in memoria), e top_beams è anche due (restituiremo due traduzioni). Questi sono entrambi iperparametri con cui si può sperimentare.

Avanti e trasforma

Spero che tu abbia trovato questo un punto di partenza utile per rompere il ghiaccio con i concetti principali del Transformer. Se vuoi approfondire, ti suggerisco i seguenti approfondimenti:

Lavori correlati:

Riconoscimenti

Grazie a Illia Polosukhin, Jakob Uszkoreit, Llion Jones , Lukasz Kaiser, Niki Parmar, e Noam Shazeer per aver fornito feedback sulle versioni precedenti di questo post.

Per favore contatta Jay Alammar su Twitter per qualsiasi correzione o feedback sull’articolo originario.

Altrimenti per correzione o feedback su questa traduzione contattate Valerio Mannucci su Twitter.

Originally published at https://valeman100.github.io on June 25, 2023.

--

--

Valerio Mannucci
Valerio Mannucci

Written by Valerio Mannucci

Ai Developer @ Joinrs.com - Foundation model entusiast

No responses yet