BERT a explicat: Arhitectură, preformare și reglare fină
2018 a marcat un punct de cotitură în istoria procesării limbajului natural. Odată cu publicarea lui BERT (Reprezentări codificatoare bidirecționale de la transformatoare), echipa Google AI are a introdus un model care a redefinit stadiul tehnicii pe 11 benchmark-uri NLP simultan. Pentru Pentru prima dată, un singur model pre-antrenat ar putea fi adaptat la sarcini foarte diferite (clasificare, răspuns la întrebări, NER) obținând rezultate superioare tuturor sistemelor specializate precedente.
Dar ce face BERT atât de revoluționar? Răspunsul constă în trei inovații fundamentale: cel bidirecţionalitate profundă, The preformare la scară largă iar cel simplitatea de reglaj fin pentru sarcinile din aval. În acest articol vom analiza fiecare aspect al arhitecturii BERT, de la mecanismele de atenție internă până la implementare exersați cu HuggingFace, trecând prin variațiile care sunt derivate din acesta.
Acesta este al doilea articol din serie NLP modern: de la BERT la LLM. Dacă nu ați citit încă primul articol despre elementele fundamentale (tokenizare, încorporare și conducte NLP), Vă sfătuiesc să faceți acest lucru înainte de a continua: multe dintre conceptele abordate aici sunt construite pe acele baze.
Ce vei învăța
- de ce BERT a reprezentat o revoluție în NLP și limitările modelelor anterioare
- Arhitectura Transformer numai pentru codificatorul care stă la baza BERT
- Cum funcționează auto-atenția cu mai multe capete și formule matematice
- Reprezentare de intrare: înglobare de token, segment și poziție
- Cele două strategii de pre-antrenament: Masked Language Model (MLM) și Next Sentence Prediction (NSP)
- Tokenizare WordPiece și jetoane speciale [CLS], [SEP], [MASK]
- Cum să faci reglaj fin pentru clasificare, NER, răspunsuri la întrebări
- Implementare practică completă cu HuggingFace Transformers
- Variante BERT: Roberta, ALBERT, DistilBERT, DeBERTa, ELECTRA
- BERT pentru limba italiană: modele disponibile și comparație
- Limitele BERT și modul în care modelele ulterioare le-au depășit
Prezentare generală a seriei
| # | Articol | Concentrează-te |
|---|---|---|
| 1 | Fundamentele NLP | Tokenizare, încorporare, conducte |
| 2 | Sunteți aici - BERT și Transformer | Atentie Arhitectura, Pre-training |
| 3 | Analiza sentimentelor | Clasificarea textului cu BERT |
| 4 | Recunoașterea entității numite | Extragerea de entități din text |
| 5 | HuggingFace Transformers | Bibliotecă și modele pre-instruite |
| 6 | Reglajul fin al modelelor | Adaptați BERT la domeniul dvs |
| 7 | NLP pentru italiană | Șabloane și resurse pentru limba italiană |
| 8 | De la BERT la LLM | GPT, LLaMA și generare de text |
1. de ce BERT a revoluționat NLP
Pentru a înțelege impactul BERT, trebuie să ne întoarcem la peisajul NLP de dinainte de 2018 și să înțelegem ce probleme fundamentale nu au reușit să rezolve modelele anterioare.
1.1 Limitările Word2Vec și GloVe
După cum am văzut în primul articol al seriei, Word2Old (2013) e mănușă (2014) au fost un punct de cotitură important: pentru prima dată, cuvintele au fost reprezentați ca vectori denși într-un spațiu continuu, în care relații semantice ca „rege – bărbat + femeie = regină” a apărut în mod natural din geometria spațiului.
Aceste modele suferă însă de o limitare fundamentală: produc reprezentări statice. Fiecare cuvânt are un vector unic, indiferent de contextul în care apare. Să luăm în considerare cuvântul „bancă”:
Problema reprezentărilor statice
- „Am depus banii în bancă" (instituție bancară)
- „Cel bancă era prea tânăr la școală” (mobil)
- "O bancă ceață a acoperit valea" (masă de ceață)
- „Cel bancă de pești a fost enorm" (grup de pești)
Cu Word2Vec, cuvântul „bancă” are un unic vector care este o medie neclară a tuturor acestor sensuri. Modelul nu are nicio modalitate de a distinge contextul.
1.2 ELMo: Primul pas către context
În 2018, înainte de BERT, ELMo (Incorporare din modele de limbaj) de AllenAI încercase deja să rezolve problema contextelor. ELMo a folosit o rețea biLSTM (Memorie bidirecțională pe termen scurt pe termen lung) pre-antrenat pentru o sarcină a modelării limbajului pentru a produce înglobări contextuale.
Problema ELMo a fost dublă: în primul rând, era bidirecțională superficial, în sensul că două LSTM-uri (unul înainte, unul înapoi) au fost concatenate, dar nu interacționat în timpul procesării. În al doilea rând, LSTM-urile suferă de a informaţii blocaj: dependențele pe termen lung sunt progresiv „uitate” pe măsură ce succesiunea devine mai lungă.
1.3 Punctul de cotitură: Bidirecționalitate profundă
BERT rezolvă ambele probleme datorită mecanismului de autoatenție a Transformatorului. Fiecare jetoane dintr-o secvență poate „viziona” toate celelalte jetoane simultan, atât cei din stânga cât și cei din dreapta. Această bidirecționalitate nu este concatenarea a două modele separate (ca în ELMo), dar o interacțiune profundă care se întâmplă la fiecare strat al arhitecturii.
Comparație: Abordări ale bidirecționalității
| Model | Tip | Context | Prescripţie |
|---|---|---|---|
| Word2Vec/GloVe | Static | Fără context | Un vector pe cuvânt |
| GPT-1 | Unidirecțional (stânga -> dreapta) | Numai contextul anterior | El nu vede viitorul |
| Cască | Bidirectional superficial | Context legat | Fără interacțiune între direcții |
| BERT | Bidirecțional profund | Context complet la fiecare strat | Numai codificator (fără generație) |
Această bidirecționalitate profundă este motivul pentru care BERT a putut să stabilească noi înregistrări pe 11 repere NLP la momentul publicării sale, inclusiv GLUE, SQuAD 1.1, SQuAD 2.0 și MultiNLI.
2. Arhitectura BERT: transformatorul codificatorului
BERT este un model numai codificator, adică folosește doar partea de codificator a arhitecturii originale Transformer descrisă în lucrarea „Attention Is All You Need” (2017). Să vedem fiecare componentă în detaliu.
2.1 Privire de ansamblu asupra arhitecturii
Arhitectura BERT poate fi vizualizată ca o serie de blocuri transformatoare stivuite vertical. Fiecare bloc conține un strat de auto-atenție cu mai multe capete urmată de o rețea feed-forward, cu strat de normalizare și conexiuni reziduale.
Input: [CLS] Il gatto si siede sul tappeto [SEP]
| | | | | | |
+-----+-------+-----+----+-----+----+-------+-----+
| Token Embeddings |
| + Segment Embeddings |
| + Position Embeddings |
+-----+-------+-----+----+-----+----+-------+-----+
| | | | | | |
+---------------------------------------------------------+
| Transformer Encoder Block 1 |
| +---------------------------------------------------+ |
| | Multi-Head Self-Attention | |
| | Q = XWq K = XWk V = XWv | |
| | Attention(Q,K,V) = softmax(QK^T/sqrt(dk))V | |
| +---------------------------------------------------+ |
| | Add & Layer Norm | |
| +---------------------------------------------------+ |
| | Feed-Forward Network | |
| | FFN(x) = max(0, xW1 + b1)W2 + b2 | |
| +---------------------------------------------------+ |
| | Add & Layer Norm | |
+---------------------------------------------------------+
| | | | | | |
... ... ... ... ... ... ...
| | | | | | |
+---------------------------------------------------------+
| Transformer Encoder Block L (12 o 24) |
+---------------------------------------------------------+
| | | | | | |
[CLS]out T1 T2 T3 T4 T5 T6 [SEP]out
|
Pooling --> Classificazione / Output per task
2.2 BERT-bază vs BERT-mare
Lucrarea originală propune două configurații:
Configurații BERT
| Parametru | BERT-de bază | BERT-mare |
|---|---|---|
| Straturi transformatoare (L) | 12 | 24 |
| Dimensiune ascunsă (H) | 768 | 1024 |
| Capete de atenție (A) | 12 | 16 |
| Parametri totali | 110M | 340M |
| Dim. pentru Cap | 768/12 = 64 | 1024/16 = 64 |
| Feed-Forward Dim. | 3072 (4 x 768) | 4096 (4 x 1024) |
| Secv. max. Lungime | 512 | 512 |
| Dimensiunea vocabularului | 30.522 | 30.522 |
2.3 Reprezentarea intrărilor: cele trei înglobări
Una dintre inovațiile BERT este a ta reprezentarea intrării, compus din suma a trei înglobări distincte:
Input: [CLS] Il gatto dorme [SEP] Il cane corre [SEP]
| | | | | | | | |
Token Emb: E[CLS] E_il E_gatto E_dorme E[SEP] E_il E_cane E_corre E[SEP]
+ + + + + + + + +
Segment Emb: EA EA EA EA EA EB EB EB EB
+ + + + + + + + +
Position Emb: E0 E1 E2 E3 E4 E5 E6 E7 E8
= = = = = = = = =
Input finale: I0 I1 I2 I3 I4 I5 I6 I7 I8
Încorporare de jetoane: Fiecare jeton din secvență este mapat la un vector dens de dimensiune H (768 pentru BERT-bază) printr-o matrice de încorporare învățată în timpul antrenament. Vocabularul WordPiece conține 30.522 de jetoane.
Înglobări de segmente: BERT poate primi una sau două propoziții separate ca intrare token [SEP]. Înglobările de segmente indică cărei propoziții îi aparține fiecare jeton: toate simbolurile a primei propoziţii primesc încorporarea E_A, cei din a doua E_B. Acest lucru este fundamental pentru sarcini precum Inferența limbajului natural în care BERT trebuie să raționeze asupra relaţiei dintre două propoziţii.
Poziții încorporate: Spre deosebire de Transformerul original care folosește funcții sinusoidal, BERT folosește înglobare de poziție Am învățat. Fiecare poziție (0 la 511) are un vector învățat în timpul antrenamentului, ceea ce înseamnă că BERT se poate descurca secvențe de până la maximum 512 jetoane.
from transformers import BertTokenizer, BertModel
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
# Tokenizzare un input con due frasi
text_a = "Il gatto dorme"
text_b = "Il cane corre"
encoded = tokenizer(text_a, text_b, return_tensors='pt')
print("Token IDs:", encoded['input_ids'])
print("Token Type IDs (Segments):", encoded['token_type_ids'])
print("Attention Mask:", encoded['attention_mask'])
# Decodifica per vedere i token
tokens = tokenizer.convert_ids_to_tokens(encoded['input_ids'][0])
print("Tokens:", tokens)
# Output: ['[CLS]', 'il', 'gatto', 'dor', '##me', '[SEP]', 'il', 'cane', 'cor', '##re', '[SEP]']
# Accesso agli embedding layers
token_embeddings = model.embeddings.word_embeddings
position_embeddings = model.embeddings.position_embeddings
segment_embeddings = model.embeddings.token_type_embeddings
print(f"Token embedding matrix: {token_embeddings.weight.shape}")
# Output: Token embedding matrix: torch.Size([30522, 768])
print(f"Position embedding matrix: {position_embeddings.weight.shape}")
# Output: Position embedding matrix: torch.Size([512, 768])
print(f"Segment embedding matrix: {segment_embeddings.weight.shape}")
# Output: Segment embedding matrix: torch.Size([2, 768])
2.4 Auto-atenție cu mai multe capete
Inima lui BERT (și fiecare Transformer) și mecanismul lui autoatenție. Intuitiv, atenția personală permite fiecărui jeton să „vizioneze” toate celelalte jetonuri în succesiune și decideți cât de relevant este fiecare dintre ele pentru reprezentarea dvs.
Atenție la scară de produs punct
Pentru fiecare simbol, atenția calculează trei vectori prin proiecții liniare învățate:
- Interogări (Q): "Ce caut?"
- cheie (K): "Ce ofer?"
- Valoare (V): „Ce informații aduc?”
Formula atenției este:
Atenție(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)VUnde d_k și dimensiunea cheilor (64 pentru BERT-base). Factorul \frac{1}{\sqrt{d_k}} servește la stabilizarea gradienți: fără această scalare, produsul punctual produce valori foarte mari pt dimensionalitate ridicată, iar softmax-ul devine „saturat” producând distribuții aproape de o singură dată.
Atenție cu mai multe capete
În loc să calculeze o singură funcție de atenție, BERT folosește multi-capete atentie: Calculul este replicat h ori (12 pentru baza BERT, 16 pentru BERT-large), fiecare cu matrice de proiecție diferite. Acest lucru permite modelului să surprinde simultan diferite tipuri de relații: un șef s-ar putea specializa în relaţiile sintactice, alta în coreferenţe, alta în dependenţe semantice.
MultiHead(Q, K, V) = Concat(head_1, ..., head_h)W^O head_i = Atenție(QW_i^Q, KW_i^K, VW_i^V)Unde W_i^Q \in \mathbb{R}^{d_{model} \times d_k}, W_i^K \in \mathbb{R}^{d_{model} \times d_k}, W_i^V \in \mathbb{R}^{d_{model} \times d_v} e W^O \in \mathbb{R}^{hd_v \times d_{model}}.
import torch
import torch.nn.functional as F
import math
def scaled_dot_product_attention(Q, K, V, mask=None):
"""Calcolo dell'attention scalata."""
d_k = Q.size(-1)
# Calcolo dei punteggi di attention
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# Applicazione della maschera (opzionale)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax per ottenere i pesi
attention_weights = F.softmax(scores, dim=-1)
# Output pesato
output = torch.matmul(attention_weights, V)
return output, attention_weights
class MultiHeadAttention(torch.nn.Module):
"""Multi-Head Attention come in BERT."""
def __init__(self, d_model=768, num_heads=12):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads # 64 per BERT-base
self.W_q = torch.nn.Linear(d_model, d_model)
self.W_k = torch.nn.Linear(d_model, d_model)
self.W_v = torch.nn.Linear(d_model, d_model)
self.W_o = torch.nn.Linear(d_model, d_model)
def forward(self, x, mask=None):
batch_size, seq_len, _ = x.size()
# Proiezioni lineari
Q = self.W_q(x) # (batch, seq_len, d_model)
K = self.W_k(x)
V = self.W_v(x)
# Reshape per multi-head: (batch, heads, seq_len, d_k)
Q = Q.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2)
# Attention per ogni head
attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)
# Concatenazione delle head
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch_size, seq_len, self.d_model)
# Proiezione finale
output = self.W_o(attn_output)
return output, attn_weights
# Test
mha = MultiHeadAttention(d_model=768, num_heads=12)
x = torch.randn(1, 10, 768) # batch=1, seq_len=10, dim=768
output, weights = mha(x)
print(f"Output shape: {output.shape}") # torch.Size([1, 10, 768])
print(f"Weights shape: {weights.shape}") # torch.Size([1, 12, 10, 10])
2.5 Rețea de tip Feed-Forward
După fiecare strat de atenție, BERT aplică o rețea de feed-forward în funcție de poziție, de exemplu aceeași rețea este aplicată independent fiecărei poziții a secvenței:
FFN(x) = \text{GELU}(xW_1 + b_1)W_2 + b_2Mărimea intermediară este de obicei de 4 ori dimensiunea ascunsă (3072 pentru BERT-bază, 4096 pentru BERT-mare). BERT folosește funcția de activare GELU (Gauss Error Linear Unit) în loc de ReLU tradițională, care oferă o tranziție mai lină și o performanță mai bună în practică.
2.6 Normalizarea stratului și conexiunile reziduale
Fiecare substrat (atenție și feed-forward) din BERT este învelit de a conexiune reziduală urmată de normalizarea stratului:
\text{ieșire} = \text{LayerNorm}(x + \text{Sublayer}(x))Conexiunile reziduale permit gradienților să curgă direct prin rețea în timpul propagării inverse, prevenind problema gradientului de dispariție în foarte rețele adânc. Normalizarea stratului stabilizează antrenamentul prin normalizarea activărilor de-a lungul dimensiunii caracteristicii.
import torch
import torch.nn as nn
class TransformerEncoderBlock(nn.Module):
"""Un singolo blocco encoder come in BERT."""
def __init__(self, d_model=768, num_heads=12, d_ff=3072, dropout=0.1):
super().__init__()
# Multi-Head Attention
self.attention = nn.MultiheadAttention(
embed_dim=d_model,
num_heads=num_heads,
dropout=dropout,
batch_first=True
)
# Feed-Forward Network
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout)
)
# Layer Normalization
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# Sub-layer 1: Multi-Head Attention + Residual + LayerNorm
attn_output, _ = self.attention(x, x, x, key_padding_mask=mask)
x = self.norm1(x + self.dropout(attn_output))
# Sub-layer 2: FFN + Residual + LayerNorm
ffn_output = self.ffn(x)
x = self.norm2(x + ffn_output)
return x
class BERTEncoder(nn.Module):
"""Stack di encoder blocks come in BERT-base."""
def __init__(self, num_layers=12, d_model=768, num_heads=12, d_ff=3072):
super().__init__()
self.layers = nn.ModuleList([
TransformerEncoderBlock(d_model, num_heads, d_ff)
for _ in range(num_layers)
])
def forward(self, x, mask=None):
for layer in self.layers:
x = layer(x, mask)
return x
# Test: simula BERT-base
encoder = BERTEncoder(num_layers=12, d_model=768, num_heads=12, d_ff=3072)
x = torch.randn(2, 128, 768) # batch=2, seq_len=128
output = encoder(x)
print(f"Output: {output.shape}") # torch.Size([2, 128, 768])
# Conta parametri
total_params = sum(p.numel() for p in encoder.parameters())
print(f"Parametri encoder: {total_params:,}")
# ~85M (solo encoder, senza embeddings)
3. Pre-training: Cum BERT învață limba
BERT este antrenat în prealabil pe o cantitate imensă de text fără etichetă folosind două obiective de formare complementare: cel Model de limbaj mascat (MLM) iar cel Predicția următoarei propoziții (NSP). Ideea fundamentală este că modelul înveți reprezentări bogate ale limbajului din aceste sarcini auto-supravegheate și asta aceste cunoștințe pot fi apoi transferate la sarcini specifice prin reglaj fin.
Date pre-antrenament
- BooksCorpus: ~800 de milioane de cuvinte (11.000 de cărți nepublicate)
- Wikipedia în engleză: ~2.500 de milioane de cuvinte (numai text, fără tabele/liste)
- Total: ~3,3 miliarde de cuvinte
- Antrenamentul: 4 Pod-uri TPU (64 cipuri TPU), 4 zile pentru baza BERT
3.1 Model de limbaj mascat (MLM)
Modelul de limbaj mascat este principala inovație a BERT. Problema cu limbajul modelarea tradițională (predicția cuvântului următor) și asta este intrinsec unidirecțional: Modelul poate privi doar la stânga. A face modelul bidirecțional, BERT introduce un truc elegant: mascarea unei porțiuni jetoane de intrare aleatoare și cereți modelului să le prezică.
Strategia 80/10/10
Pentru fiecare secvență de intrare, 15% de jetoane este selectat pentru predicție. Dintre aceste jetoane selectate:
- 80% este înlocuit cu simbolul special [MASK]
- 10% este înlocuit cu un jeton aleator din dicționar
- 10% este lăsat neschimbat
Această strategie rezolvă o problemă subtilă: în timpul reglajului fin, jetonul [MASK] eșuează nu apare niciodată în intrare. Dacă modelul ar vedea [MASK] numai în timpul pre-antrenamentului, ar fi acolo a nepotrivire între pre-antrenament și reglaj fin. Înlocuirea a 10% cu jetoane aleatoriu și lăsând 10% neschimbat, modelul învață să producă reprezentări bune pentru toată lumea jetoane, nu doar pentru cei deghizat.
import random
import torch
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
def apply_mlm_masking(tokens, tokenizer, mask_prob=0.15):
"""Applica la strategia di masking 80/10/10 di BERT."""
masked_tokens = list(tokens)
labels = [-100] * len(tokens) # -100 = ignora nella loss
for i in range(len(tokens)):
# Non mascherare token speciali
if tokens[i] in [tokenizer.cls_token_id, tokenizer.sep_token_id,
tokenizer.pad_token_id]:
continue
if random.random() < mask_prob:
labels[i] = tokens[i] # Salva il token originale come label
rand = random.random()
if rand < 0.8:
# 80%: sostituisci con [MASK]
masked_tokens[i] = tokenizer.mask_token_id
elif rand < 0.9:
# 10%: sostituisci con token casuale
masked_tokens[i] = random.randint(0, tokenizer.vocab_size - 1)
# else: 10%: lascia invariato
return masked_tokens, labels
# Esempio
text = "Il gatto si siede sul tappeto rosso"
encoded = tokenizer.encode(text)
print("Originale:", tokenizer.decode(encoded))
masked, labels = apply_mlm_masking(encoded, tokenizer)
print("Mascherato:", tokenizer.decode(masked))
print("Labels (posizioni da predire):",
[(i, tokenizer.decode([labels[i]])) for i in range(len(labels)) if labels[i] != -100])
3.2 Predicția următoarei propoziții (NSP)
Al doilea obiectiv pre-antrenament este Predicția următoarei propoziții. Pentru fiecare pereche de propoziții (A, B) în intrare, modelul trebuie să prezică dacă B este propoziția care urmează de fapt A în textul original (Este Următorul) sau dacă este o frază aleatorie (Nu Următorul). Setul de date este construit astfel încât 50% dintre perechi să fie pozitive și 50% negativ.
Input = [CLS] Il gatto dorme [SEP] E' stanco dopo aver giocato [SEP]
Label = IsNext (frase B segue A nel testo originale)
Input = [CLS] Il gatto dorme [SEP] Roma e' la capitale d'Italia [SEP]
Label = NotNext (frase B e' casuale, non correlata ad A)
Obiectivul NSP ajută BERT să capteze relațiile dintre propoziții, utile pentru sarcini precum Răspunsuri la întrebări și inferență în limbaj natural. Cu toate acestea, studiile ulterioare (în special Roberta) au arătat că NSP poate să nu fie necesar și poate chiar deteriora performanță, așa cum vom vedea în secțiunea despre variante.
3.3 Pierderea combinată
Pierderea totală a BERT în timpul pre-antrenamentului este suma celor două pierderi:
\mathcal{L}_{total} = \mathcal{L}_{MLM} + \mathcal{L}_{NSP}Unde \mathcal{L}_{MLM} și pierderea de entropie încrucișată pe jetoane mascat și \mathcal{L}_{NSP} și pierderea de entropie încrucișată binar pe clasificarea IsNext/NotNext.
4. Tokenizare WordPiece
BERT folosește tokenizarea WordPiece, un algoritm de tokenizare subcuvânt care echilibrează eficiența vocabularului și acoperirea lingvistică. WordPiece și stare dezvoltat inițial pentru sistemul de traducere automată Google.
4.1 Cum funcționează WordPiece
Ideea din spatele WordPiece este simplă, dar puternică: pornind de la un vocabular de indivizi caractere și îmbină iterativ perechile de jetoane care maximizează verosimilitate a corpusului de instruire. Procesul continuă până când vocabularul ajunge din urmă o dimensiune țintă (30.522 pentru BERT).
Rezultatul este un vocabular care conține:
- Cuvinte comune întregi (de exemplu, „the”, „of”, „and”)
- Prefixe și rădăcini comune (de exemplu, „un”, „re”, „pre”)
- Sufixele și terminațiile marcate cu
##(de exemplu, „##ing”, „##tion”, „##ed”) - Caractere unice pentru a gestiona orice cuvânt necunoscut
4.2 Jetoane speciale
În plus față de jetoanele de vocabular WordPiece, BERT folosește mai multe jetoane speciale:
Jetoane speciale BERT
| Jetoane | ID | Domeniul de aplicare |
|---|---|---|
| [PAD] | 0 | Umplutură pentru a standardiza lungimea secvențelor dintr-un lot |
| [UNK] | 100 | Jeton necunoscut (nu în dicționar) |
| [CLS] | 101 | Începutul secvenței, reprezentarea sa finală este folosită pentru clasificare |
| [SEPT.] | 102 | Separator între cele două propoziții de intrare |
| [MASCA] | 103 | Mască pentru modelul de limbaj mascat în timpul pre-antrenamentului |
from transformers import BertTokenizer
# Carica il tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# Tokenizzazione di parole comuni e rare
examples = [
"The cat sat on the mat",
"Electroencephalography is fascinating",
"L'intelligenza artificiale rivoluzionera il mondo",
"Tokenizzazione subword con WordPiece"
]
for text in examples:
tokens = tokenizer.tokenize(text)
ids = tokenizer.encode(text)
print(f"Testo: {text}")
print(f" Tokens: {tokens}")
print(f" IDs: {ids}")
print(f" Num tokens: {len(tokens)}")
print()
# Output per "Electroencephalography is fascinating":
# Tokens: ['electro', '##ence', '##pha', '##log', '##raphy', 'is', 'fascinating']
# La parola lunga viene spezzata in subword, ma "is" e "fascinating" restano intere
# Decodifica
encoded = tokenizer("Hello world!", return_tensors="pt")
decoded = tokenizer.decode(encoded['input_ids'][0])
print(f"Encoded -> Decoded: {decoded}")
# Output: [CLS] hello world ! [SEP]
# Vocabolario
print(f"Dimensione vocabolario: {tokenizer.vocab_size}")
# Output: 30522
5. Reglarea fină a BERT pentru sarcinile din aval
Paradigma introdusă de BERT este „pre-antrenează, apoi ajustează”. Faza de pre-antrenament produce un model cu o înțelegere profundă a limbajului. Faza de reglare fină adaptați acest model la o sarcină specifică prin adăugarea unui strat de ieșire și antrenament întregul model (sau o parte din acesta) pe datele etichetate ale sarcinii.
Frumusețea reglajului fin este că necesită puține date etichetate e timp redus de antrenament comparativ cu antrenarea unui model de la zero. De obicei, 2-4 epoci cu o rată scăzută de învățare (2e-5 până la 5e-5) sunt suficiente.
5.1 Clasificarea textului
Pentru clasificarea textului (analiza sentimentelor, clasificarea subiectelor, detectarea spamului), este utilizată reprezentarea jetonului [CLS] ca intrare la un clasificator liniar:
Input: [CLS] Questo film e' fantastico [SEP]
|
BERT: 12 layers di Transformer Encoder
|
[CLS] output (768-dim) --> Linear(768, num_classes) --> Softmax --> Predizione
|
"Positivo" (p=0.95)
from transformers import BertForSequenceClassification, BertTokenizer
from transformers import Trainer, TrainingArguments
from datasets import load_dataset
import torch
# 1. Carica modello pre-addestrato con classification head
model = BertForSequenceClassification.from_pretrained(
'bert-base-uncased',
num_labels=2 # positivo/negativo
)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 2. Prepara il dataset (esempio: IMDB reviews)
dataset = load_dataset('imdb')
def tokenize_function(examples):
return tokenizer(
examples['text'],
padding='max_length',
truncation=True,
max_length=256
)
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 3. Configura il training
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=64,
warmup_steps=500,
weight_decay=0.01,
learning_rate=2e-5,
logging_dir='./logs',
evaluation_strategy='epoch',
save_strategy='epoch',
load_best_model_at_end=True,
)
# 4. Crea il Trainer e avvia il fine-tuning
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets['train'],
eval_dataset=tokenized_datasets['test'],
)
trainer.train()
# 5. Valutazione
results = trainer.evaluate()
print(f"Accuracy: {results['eval_loss']:.4f}")
5.2 Recunoașterea entității denumite (NER)
Pentru NER, în loc să folosiți doar simbolul [CLS], este folosită ieșirea lui fiecare jeton pentru a clasifica fiecare într-o categorie de entitate (persoană, loc, organizație etc.):
Input: [CLS] Mario Rossi vive a Roma [SEP]
| | | | | | |
BERT: 12 layers di Transformer Encoder
| | | | | | |
Output: O B-PER I-PER O O B-LOC O
Ogni token --> Linear(768, num_entity_types) --> Softmax --> Tipo entità
from transformers import BertForTokenClassification, BertTokenizer
from transformers import pipeline
# Usa un modello già fine-tuned per NER
ner_pipeline = pipeline(
"ner",
model="dbmdz/bert-large-cased-finetuned-conll03-english",
aggregation_strategy="simple"
)
text = "Mario Rossi works at Google in Mountain View, California"
entities = ner_pipeline(text)
for entity in entities:
print(f" {entity['word']}: {entity['entity_group']} "
f"(score: {entity['score']:.3f})")
# Output:
# Mario Rossi: PER (score: 0.998)
# Google: ORG (score: 0.997)
# Mountain View: LOC (score: 0.995)
# California: LOC (score: 0.999)
5.3 Răspunsuri la întrebări
Pentru răspunsul la întrebări extractive, BERT primește o întrebare și un context ca intrare separate prin [SEP]. Modelul trebuie să prezică pozițiile lui început e Sfârşit a răspunsului în context:
Input: [CLS] Quando e' nato Einstein? [SEP] Albert Einstein e' nato il 14 marzo 1879 [SEP]
| | | | | | | | | | | | | |
BERT: 12 layers di Transformer Encoder
| | | | | | | | | | | | | |
Start: 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.02 0.8 0.02 0.01 0.01 0.01
End: 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.85 0.01
Risposta estratta: "14 marzo 1879"
from transformers import pipeline
# Pipeline di Question Answering
qa_pipeline = pipeline(
"question-answering",
model="deepset/bert-base-cased-squad2"
)
context = """
BERT (Bidirectional Encoder Representations from Transformers)
e' un modello di linguaggio sviluppato da Google AI nel 2018.
E' stato addestrato su Wikipedia e BookCorpus, per un totale
di circa 3.3 miliardi di parole. BERT-base ha 110 milioni di
parametri e 12 layer di Transformer encoder.
"""
questions = [
"Chi ha sviluppato BERT?",
"Quanti parametri ha BERT-base?",
"Su quali dati e' stato addestrato BERT?",
"In che anno e' stato pubblicato BERT?"
]
for question in questions:
result = qa_pipeline(question=question, context=context)
print(f"D: {question}")
print(f"R: {result['answer']} (score: {result['score']:.3f})")
print()
5.4 Clasificarea perechilor de propoziții
Pentru sarcini precum Inferența limbajului natural (NLI) sau detectarea parafrazelor, BERT primește două propoziții și trebuie să clasifice relația lor (implicație, contradicție, neutră):
from transformers import pipeline
# Natural Language Inference
nli_pipeline = pipeline(
"text-classification",
model="cross-encoder/nli-deberta-v3-small"
)
pairs = [
("Il gatto dorme sul divano", "Un animale sta riposando"),
("Il gatto dorme sul divano", "Il cane corre nel parco"),
("Tutti gli studenti hanno superato l'esame", "Nessuno studente ha fallito"),
]
for premise, hypothesis in pairs:
result = nli_pipeline(f"{premise} [SEP] {hypothesis}")
print(f"Premessa: {premise}")
print(f"Ipotesi: {hypothesis}")
print(f"Risultato: {result[0]['label']} ({result[0]['score']:.3f})")
print()
6. Extrageți încorporații din BERT
Pe lângă reglarea fină pentru sarcini specifice, BERT este extrem de util ca extractor de caracteristici. Reprezentările produse de straturile interne captează informații lingvistice la diferite niveluri de abstractizare.
from transformers import BertTokenizer, BertModel
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased', output_hidden_states=True)
model.eval()
text = "Il Natural Language Processing e' affascinante"
inputs = tokenizer(text, return_tensors='pt')
with torch.no_grad():
outputs = model(**inputs)
# outputs contiene:
# - last_hidden_state: output dell'ultimo layer (batch, seq_len, 768)
# - pooler_output: [CLS] passato attraverso un linear + tanh (batch, 768)
# - hidden_states: tuple di 13 tensori (embedding + 12 layers)
last_hidden = outputs.last_hidden_state
pooler = outputs.pooler_output
all_layers = outputs.hidden_states
print(f"Last hidden state: {last_hidden.shape}")
# torch.Size([1, N, 768])
print(f"Pooler output: {pooler.shape}")
# torch.Size([1, 768])
print(f"Numero di layer: {len(all_layers)}")
# 13 (embedding layer + 12 transformer layers)
# Strategie per ottenere sentence embeddings:
# 1. Usa il [CLS] token dell'ultimo layer
cls_embedding = last_hidden[:, 0, :]
print(f"[CLS] embedding: {cls_embedding.shape}")
# 2. Mean pooling sull'ultimo layer (spesso migliore)
attention_mask = inputs['attention_mask'].unsqueeze(-1)
mean_embedding = (last_hidden * attention_mask).sum(1) / attention_mask.sum(1)
print(f"Mean pooling: {mean_embedding.shape}")
# 3. Concatena gli ultimi 4 layer (cattura più informazione)
last_4_layers = torch.cat(
[all_layers[i] for i in [-1, -2, -3, -4]],
dim=-1
)
print(f"Last 4 layers concatenated: {last_4_layers.shape}")
# torch.Size([1, N, 3072])
Ce strat să folosiți?
Cercetările au arătat că diferite straturi captează diferite tipuri de informații:
- Straturi 1-4 (bas): Informaţii morfologice şi sintactice de bază
- Straturi 5-8 (medii): Informații sintactice complexe, dependențe
- Straturi 9-12 (înalte): Informații semantice de nivel înalt
- Ultimul strat: Cel mai bun pentru sarcini semantice (NLI, similaritate)
- Straturi medii: Cel mai bun pentru sarcini sintactice (etichetare POS, analizare)
7. Variante de BERT: Evoluția familiei
De la publicarea BERT, numeroase grupuri de cercetare au propus variante ale acestuia îmbunătățirea mai multor aspecte. Iată o prezentare generală a celor mai importante.
Tabel de comparație a variantelor BERT
| Model | An | Conducerea inovației | Parametrii |
|---|---|---|---|
| BERT-de bază | 2018 | MLM + NSP, bidirecțional profund | 110M |
| Roberta | 2019 | Fără NSP, mascare dinamică, mai multe date, antrenament mai lung | 125M |
| ALBERT | 2019 | Partajarea parametrilor, încorporarea factorizată | 12M-235M |
| DistilBERT | 2019 | Distilarea cunoștințelor, cu 60% mai rapidă | 66M |
| ELECTRA | 2020 | Detectare token înlocuită (mai eficientă decât MLM) | 110M |
| DeBERTa | 2020 | Atenție dezlegată (conținut separat + poziție) | 134M-390M |
| XLNet | 2019 | Modelarea limbajului de permutare | 340M |
| SpanBERT | 2019 | Mascarea traveilor învecinate + SBO | 110M |
| ERNIE | 2019 | Mascarea entităților îmbunătățite prin cunoștințe | 110M |
7.1 Roberta: Optimizarea instruirii BERT
ROBERTa (abordare BERT optimizată robust), adus de Facebook AI (acum Meta), a dovedit că BERT a fost subantrenat. Schimbările principalele sunt:
- Îndepărtarea NSP: Ținta NSP nu ajută și poate chiar degrada performanța
- Mascare dinamică: În loc să mascați o singură dată în timpul preprocesării, modelele de mascare sunt generate dinamic la fiecare epocă
- Mai multe date: 160 GB de text (față de 16 GB de BERT), inclusiv CC-News, OpenWebText, Stories
- Antrenament mai lung: pas de 500K (față de 100K de BERT), dimensiune mai mare a lotului
- Secvențe mai lungi: încă 512 jetoane (BERT a început cu 128)
7.2 ALBERT: Compresie inteligentă
ALBERT (A Lite BERT) de Google abordează problema creșterii parametrii cu două tehnici elegante:
- Parametrizare factorizată încorporare: Dimensiunea vocabularului separat din dimensiunea ascunsă. În loc de o matrice V x H, utilizați V x E și E x H (cu E mult mai mic decât H)
- Partajarea parametrilor pe mai multe straturi: Toate straturile Transformer partajează aceiași parametri. Acest lucru reduce drastic numărul de parametri fără a pierde multa calitate
ALBERT-xxlarge atinge performanțe mai mari decât BERT-large cu doar 235M de parametri (vs 340M), deși timpul de inferență nu scade pe măsură ce rămâne numărul de straturi neschimbat.
7.3 DistilBERT: Distilarea cunoştinţelor
DistilBERT de Hugging Face folosește distilare a cunoștințelor pentru a crea un model mai mic, mai rapid. Se antrenează un model „elev” (6 straturi). pentru a imita rezultatele unui model „profesor” (bază BERT, 12 straturi).
- 40% mai mic: 66M parametri vs 110M
- 60% mai rapid: 6 straturi vs 12
- 97% din performanță: Menține aproape toată calitatea bazei BERT
7.4 ELECTRA: O abordare complet diferită
ELECTRA înlocuiește Masked Language Model cu o abordare inspirată la GAN-uri (Generative Adversarial Networks). În loc să mascheze jetoanele și să le prezică, ELECTRA:
- Un mic generator (un mic BERT) produce jetoane de înlocuire plauzibile
- Discriminatorul (modelul principal) trebuie să identifice care jeton au fost înlocuite (detecția jetonului înlocuit)
Beneficiul cheie: discriminatorul învață de la toată lumea semnele de secvența, nu doar 15% mascat ca în BERT. Acest lucru face ca ELECTRA să fie mult mai mult eficient din punct de vedere al datelor și al calculului.
7.5 DeBERTa: Atenție decuplată
DeBERTa (BERT îmbunătățit pentru decodare cu atenție dezlegată) de către Microsoft introduce două inovații:
- Atenție dezlegată: Separă conținutul și informațiile despre locație în doi vectori distincti, permițând modelului să raționeze despre „ce” și „unde” indiferent
- Decodor de mască îmbunătățit: Adaugă informații despre poziție absolută în decodor pentru a îmbunătăți predicțiile MLM
DeBERTa v3 este în prezent unul dintre cele mai performante modele de codificator la multe benchmark-uri, depășind adesea modele mult mai mari.
8. BERT pentru limba italiană
Originalul BERT a fost instruit pe text în limba engleză. Pentru italiană, avem mai multe opțiuni, fiecare cu avantaje și dezavantaje.
Modele BERT pentru italiană
| Model | De bază | Date de antrenament | Vocabular |
|---|---|---|---|
| mBERT | BERT-de bază | Wikipedia în 104 limbi | 110.000 de jetoane multilingve |
| dbmdz/bert-base-italian-xxl-cased | BERT-de bază | OPUS, Wikipedia IT (~13 GB) | 30.000 de jetoane italiene |
| Alberto | BERT-de bază | Tweet în italiană | 30.000 de jetoane italiene |
| UmBERTo | Roberta | OSCAR corpus italian | 32K jetoane SentencePiece |
| XLM-RoBERTa | Roberta | CC-100 în 100 de limbi | 250.000 de jetoane multilingve |
from transformers import pipeline, AutoTokenizer, AutoModelForMaskedLM
# 1. BERT Italiano (dbmdz)
fill_mask_it = pipeline(
"fill-mask",
model="dbmdz/bert-base-italian-xxl-cased"
)
results = fill_mask_it("Roma e' la [MASK] d'Italia.")
for r in results[:3]:
print(f" {r['token_str']}: {r['score']:.4f}")
# Output: capitale: 0.8234, citta: 0.0521, ...
# 2. UmBERTo (basato su RoBERTa)
fill_mask_umberto = pipeline(
"fill-mask",
model="Musixmatch/umberto-commoncrawl-cased-v1"
)
results = fill_mask_umberto("L'intelligenza artificiale <mask> il futuro.")
for r in results[:3]:
print(f" {r['token_str']}: {r['score']:.4f}")
# 3. Multilingual BERT
fill_mask_mbert = pipeline(
"fill-mask",
model="bert-base-multilingual-cased"
)
results = fill_mask_mbert("Roma e' la [MASK] d'Italia.")
for r in results[:3]:
print(f" {r['token_str']}: {r['score']:.4f}")
# mBERT funziona, ma il modello italiano dedicato e' molto più' preciso
Ce model ar trebui să alegi pentru italiană?
- Sarcini generale (NER, clasificare, QA):
dbmdz/bert-base-italian-xxl-casedsau UmBERTo - Analiza rețelelor sociale: Alberto (antrenat pe tweet)
- Sarcini multilingve: XLM-RoBERTa (excelent pentru zero-shot interlingvistic)
- Calitate maxima: Ajustarea XLM-RoBERTa-large pe datele italiene
9. Limitele BERT
În ciuda impactului său revoluționar, BERT are câteva limitări structurale care afectează aplicabilitatea acestuia:
9.1 512 Token Limit
BERT poate procesa secvențe maxime 512 jetoane. Pentru documente lungi (articole, contracte legale, lucrări științifice), aceasta este o limită severă. Strategii de atenuare includ trunchiere, ea fereastra culisanta iar cel punerea în comun ierarhică, dar niciuna nu este optimă.
9.2 Complexitatea pătratică a atenției
Autoatenția are complexitate computațională O(n^2) respect la lungimea secvenţei n. Aceasta înseamnă lungimea dublă a intrării de patru ori costul de calcul și consumul de memorie. Modele ca Longformer, BigBird e Linformer ei propun mecanisme rare de atenție pentru a ocoli această problemă.
9.3 Numai codificator: Fără capacitate generativă
BERT este un model numai codificator. Poate produce reprezentări bogate a textului, dar nu poate genera text nou. Pentru generație aveți nevoie de un decodor (ca în GPT) sau o arhitectură codificator-decodor (ca în T5 sau BART).
9.4 Nepotrivire pre-antrenament/ajustare fină
Indicatorul [MASK] apare în timpul pre-antrenamentului, dar niciodată în timpul reglajului fin sau al inferenței. Deși strategia 80/10/10 atenuează parțial problema, această nepotrivire rămâne o slăbiciune teoretică. ELECTRA rezolvă complet problema fără a folosi niciodată [MASK].
9.5 Independența jetoanelor mascate
Când BERT maschează mai multe jetoane, le prezice în consecință independent. Nu modelează dependențele dintre jetoanele mascate. De exemplu, dacă expresia este „New [MASK] [MASK]” iar răspunsul este „New York City”, BERT prezice „York” și „City” independent, fără influențează predicția unuia asupra celuilalt. XLNet abordează această problemă cu modelarea limbajului de permutare.
Rezumat Limitări și soluții
| Limită | Impact | Soluţie |
|---|---|---|
| Maxim 512 jetoane | Fara documente lungi | Longformer, BigBird, fereastră glisantă |
| Atenție O(n^2) | Costuri de calcul ridicate | Linformer, Performer, Flash Atenție |
| Numai codificatoare | Fără generare de text | T5, BART, GPT |
| [MASK] nepotrivire | Discrepanță de antrenare/inferență | ELECTRA, XLNet |
| Previziuni independente | Fără dependențe între jetoanele mascate | XLNet, modele autoregresive |
10. De la BERT la modelele moderne
BERT a inaugurat era „pre-antrenamentului, apoi reglajului fin” în NLP, influențând profund toată arhitectura ulterioară. Să vedem cum a evoluat peisajul.
10.1 Arborele genealogic model
Transformer (2017)
|
+------ Encoder-Only ------+------ Decoder-Only ------+--- Encoder-Decoder ---+
| | | |
BERT (2018) GPT-1 (2018) T5 (2019) BART (2019)
| | |
+-- RoBERTa (2019) GPT-2 (2019) mT5 (2020)
+-- ALBERT (2019) |
+-- DistilBERT (2019) GPT-3 (2020)
+-- XLNet (2019) |
+-- ELECTRA (2020) GPT-4 (2023)
+-- DeBERTa (2020) |
+-- DeBERTa v3 (2021) LLaMA (2023)
|
LLaMA 2 (2023)
|
Mistral (2023)
|
LLaMA 3 (2024)
10.2 Lecția cheie a BERT
Cea mai importantă moștenire a BERT nu este o arhitectură specifică, ci o paradigmă: Antrenați un model pe cantități uriașe de text neetichetat folosind obiective auto-supravegheate și apoi adaptați-l la sarcini specifice cu puține date etichetat. Această paradigmă stă la baza tuturor modelelor moderne, de la GPT la LLaMA.
10.3 Acolo unde BERT este încă relevant
În ciuda apariției unor modele generative mult mai mari, BERT și descendenții săi numai codificatorul rămâne cea mai bună alegere pentru multe scenarii:
- Clasificarea textelor: Sentiment, subiect, detectare spam
- Recunoașterea entității numite: Extragerea entităților structurate
- Căutare semantică: Căutare bazată pe sens (cu transformatori de propoziții)
- Recuperarea informațiilor: Clasificarea documentelor relevante
- Generare de încorporare: Reprezentări dense pentru text
Modelele de codificatoare precum BERT sunt mai multe eficient, Mai mult rapid e mai putin costisitoare comparativ cu modelele mari generative. Pentru multe sarcini înțelegerea limbajului, un DeBERTa-v3 reglat depășește modelele cu de 100 de ori mai mult parametrii.
11. Concluzii și când să utilizați BERT
În acest articol am analizat în profunzime BERT: din arhitectura doar cu codificator a Transformerului, la mecanismul de autoatenție cu mai multe capete, la strategiile de pre-antrenament (MLM și NSP), până la implementări practice cu HuggingFace și variantele sale sunt derivate.
Cadrul decizional: Când să folosiți BERT
| Scenariu | Model recomandat | Motivația |
|---|---|---|
| Clasificarea textului (producție) | DeBERTa-v3 sau Roberta | Precizie maximă, cost rezonabil |
| NER (italiană) | dbmdz BERT IT sau UmBERTo | Vocabular nativ italian |
| Resurse limitate / margine | DistilBERT sau TinyBERT | Rapid, compact, calitate 97%. |
| Eficiența antrenamentului | ELECTRA | Învață din toate jetoanele, nu doar 15% |
| Generarea textului | GPT / T5 / LLaMA | BERT nu poate genera |
| Documente lungi | Longformer / BigBird | BERT limitat la 512 jetoane |
| Multilingv / zero-shot | XLM-RoBERTa | Excelent transfer interlingvistic |
BERT rămâne un pilon al NLP-ului modern. Deşi Marile Modele de Limbă Modelele generative domină titlurile, modelele de codificatori precum BERT sunt coloana vertebrală a nenumărate aplicații în producție: motoare de căutare, sisteme de recomandare, filtre de spam, asistenți legali, analiză de sentiment și multe altele.
În următorul articol din serie, vom pune aceste cunoștințe în practică prin construirea unui sistem de Analiza sentimentelor complet pentru textul italian, din pregătirea setului de date pentru implementarea modelului.
Resurse pentru a afla mai multe
- Lucrare originală: „BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding” (Devlin și colab., 2018)
- Lucrarea Roberta: „O abordare de preformare BERT optimizată robust” (Liu et al., 2019)
- Lucrare ELECTRA: „ELECTRA: Pre-training Text Encoders as Discriminators Rather Than Generators” (Clark et al., 2020)
- Lucrare DeBERTa: „DeBERTa: Decoding-enhanced BERT with Disentangled Attention” (He et al., 2020)
- Documentația HuggingFace BERT: huggingface.co/docs/transformers/model_doc/bert
- BERT ilustrat: jalammar.github.io/illustrated-bert/







