ELMo vs BERT Mini Lab¶

Przewodnik przez wszystkie eksperymenty z pliku RUN.md. Notebook korzysta bezpośrednio ze skryptów w src/ i uruchamia je w tej samej kolejności co instrukcja terminalowa.

Pierwsze uruchomienie ELMo i BERT pobiera modele pretrained, dlatego wymaga dostępu do internetu. Kolejne uruchomienia korzystają z cache TensorFlow Hub.

Przygotowanie¶

Uruchom Jupyter w katalogu głównym repozytorium albo w notebooks/. Poniższy helper odnajduje repozytorium i wykonuje każdy eksperyment jako moduł Pythona.

In [2]:
import os
import runpy
import sys
from pathlib import Path

os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")

ROOT = Path.cwd().resolve()
if not (ROOT / "src").is_dir():
    ROOT = ROOT.parent

if not (ROOT / "src").is_dir():
    raise RuntimeError("Uruchom Jupyter w katalogu repozytorium lub w katalogu notebooks.")

if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

def run_experiment(module_name):
    print(f"\n>>> {module_name}\n")
    runpy.run_module(module_name, run_name="__main__")

print("Katalog repozytorium:", ROOT)
Katalog repozytorium: P:\coding\python\private\ELMoTest\ELMo-adoption

1. Sprawdzenie środowiska¶

Potwierdź wersje TensorFlow i NumPy, wykonaj prostą operację na tensorze oraz sprawdź, czy model ELMo daje się załadować. Ostrzeżenia CUDA można zignorować, jeżeli ten lab świadomie działa na CPU.

In [3]:
run_experiment("src.test_env")
>>> src.test_env

TF: 2.10.1
NumPy: 1.26.4
tf.Tensor(69.41580455077285, shape=(), dtype=float64)
ELMo OK

2. Inspekcja wyjść ELMo¶

Załaduj SavedModel ELMo i sprawdź jego sygnatury, nazwy wyjść, typy danych oraz kształty tensorów. To etap rozpoznania: najpierw ustalamy, co rzeczywiście udostępnia pobrany model, a dopiero potem wybieramy embedding.

In [4]:
run_experiment("src.elmo.inspect_elmo")
>>> src.elmo.inspect_elmo

Available signatures: ['tokens', 'default', 'word_emb']
Using signature: default
Output keys: ['elmo', 'default', 'word_emb', 'lstm_outputs2', 'sequence_len', 'lstm_outputs1']
-            elmo | dtype=<dtype: 'float32'> | shape=(2, 3, 1024)
-         default | dtype=<dtype: 'float32'> | shape=(2, 1024)
-        word_emb | dtype=<dtype: 'float32'> | shape=(2, 3, 512)
-   lstm_outputs2 | dtype=<dtype: 'float32'> | shape=(2, 3, 1024)
-    sequence_len | dtype=<dtype: 'int32'> | shape=(2,)
-   lstm_outputs1 | dtype=<dtype: 'float32'> | shape=(2, 3, 1024)

3. Wyjścia zdaniowe i tokenowe ELMo¶

Znajdź dwuwymiarowego kandydata pooled oraz trójwymiarowy tensor tokenowy. Porównaj ich kształty, przykładowe wartości i normy. Wektor pooled opisuje całe wejście, a tensor tokenowy zachowuje osobny wektor kontekstowy dla każdego tokenu.

In [5]:
run_experiment("src.elmo.token_vs_sentence")
>>> src.elmo.token_vs_sentence

All keys: ['elmo', 'default', 'lstm_outputs1', 'word_emb', 'lstm_outputs2', 'sequence_len']

Pooled candidate: default shape=(2, 1024) dtype=<dtype: 'float32'>
Example pooled vector[0][:8] = [-0.1487  0.283  -0.1837  0.0991  0.0502  0.288   0.0698  0.6621]
Norm pooled[0] = 8.875906944274902

Token-level candidate: elmo shape=(2, 5, 1024) dtype=<dtype: 'float32'>
Example token vector[0,0][:8] = [ 0.2276 -0.0571  0.1066  0.5878  0.1259 -0.0481  0.437   0.6652]
Norm token[0,0] = 13.297369003295898

4. Kontekstowe znaczenie słowa bank w ELMo¶

Porównaj bank w zdaniu finansowym i w zdaniu dotyczącym brzegu rzeki. Statyczna warstwa word_emb powinna zwrócić identyczny lub niemal identyczny wektor dla tego samego zapisu, natomiast warstwy LSTM i połączona reprezentacja ELMo powinny reagować na kontekst.

In [6]:
run_experiment("src.elmo.context_demo_bank")
>>> src.elmo.context_demo_bank

Token key used: lstm_outputs2
Tokens 1: ['The', 'central', 'bank', 'raised', 'interest', 'rates', 'yesterday.']
Tokens 2: ['A', 'fisherman', 'rested', 'on', 'the', 'river', 'bank', 'at', 'sunset.']
Index bank: 2 6
Cosine similarity(bank in finance vs bank in river) = 0.5187981128692627
---------------------
word_emb 1.0
lstm_outputs1 0.647405743598938
lstm_outputs2 0.5187981128692627
elmo 0.6329265236854553
---------------------
Cosine similarity of differences (context - full meaning) between bank in finance and bank in river:  0.4923973083496094
---------------------
--- Compare other differences of tokens to token bank ---
--- lstm_outputs1 ---
First sentence similarity: [0.21913307905197144, 0.37156981229782104, 1.0, 0.2970510721206665, 0.23443084955215454, 0.22554008662700653, 0.027519134804606438]
Second sentence similarity: [0.13589704036712646, 0.2786482572555542, 0.22113066911697388, 0.27685248851776123, 0.304684042930603, 0.48577630519866943, 1.0000001192092896, 0.33589863777160645, 0.12487739324569702]
Cosine similarity of differences between first sentence and bank: 0.3393205763506038
Cosine similarity of differences between second sentence and bank: 0.35152943929036456
--- lstm_outputs2 ---
First sentence similarity: [0.24313636124134064, 0.4706833064556122, 1.0, 0.3898720443248749, 0.35958269238471985, 0.31992655992507935, 0.2423626184463501]
Second sentence similarity: [0.06394350528717041, 0.4306705594062805, 0.3327454626560211, 0.3700907826423645, 0.28975558280944824, 0.6528207063674927, 1.0, 0.3110698163509369, 0.26548564434051514]
Cosine similarity of differences between first sentence and bank: 0.43222336896828245
Cosine similarity of differences between second sentence and bank: 0.41295356220669216

5. Inspekcja BERT i porównanie wektorów kontekstowych¶

Sprawdź tokeny WordPiece i wyjścia BERT, a następnie porównaj trzy miary:

  • pooled_output, przydatny diagnostycznie, ale niezoptymalizowany jako embedding semantyczny zdania;
  • dwa kontekstowe wektory tokenu bank;
  • mean pooling tokenów treści, z pominięciem [CLS], [SEP] i paddingu.

W tym eksperymencie sensowną interpretację mają przede wszystkim wyniki dla tokenu kontekstowego i mean poolingu.

In [7]:
run_experiment("src.bert.inspect_bert")
>>> src.bert.inspect_bert

Preprocessing keys: ['input_mask', 'input_word_ids', 'input_type_ids']
input_mask shape: (2, 128)
input_word_ids shape: (2, 128)
input_type_ids shape: (2, 128)
Output keys: ['default', 'sequence_output', 'pooled_output', 'encoder_outputs']

Pooled output shape: (2, 768)
Pooled vector for first sentence (first 8 dims): [-0.8554574  -0.47888643 -0.8189835   0.6334557   0.6097271  -0.16772108
  0.7454867   0.40493798]

Sequence output shape: (2, 128, 768)

Sentence 0: 'The central bank raised interest rates yesterday.'
WordPiece token vectors (first 8 dimensions):
  position  0        [CLS] vec[:8]: [ 0.05397778 -0.37047213 -0.35328773  0.1716542   0.00099186 -0.1486258
 -0.05441308  0.4789105 ]
  position  1          the vec[:8]: [ 0.27536267 -1.0990989  -0.12909013  0.6392275  -0.28300753 -0.02198591
 -0.480161    0.7330754 ]
  position  2      central vec[:8]: [ 0.10589901 -0.44337258 -0.15875721  0.03866633  0.46062434  0.2827007
 -0.7447245   0.5264697 ]
  position  3         bank vec[:8]: [ 0.19567712 -0.5792575  -0.37638965  0.14570451  0.8851406   0.26546443
 -0.8806373   0.6080461 ]
  position  4       raised vec[:8]: [-0.21969485 -0.5015561   0.2509767   0.32835215  0.30995718 -0.18658385
 -0.90809345  0.5719876 ]
  position  5     interest vec[:8]: [ 0.5806443  -0.86270666  0.39152336 -0.35048243  1.2660639  -0.2990956
 -1.9531304   1.3169113 ]
  position  6        rates vec[:8]: [ 0.6186595  -0.48993176  0.5264483   0.07381187  0.3686828  -0.11576612
 -1.2298725   0.5747908 ]
  position  7    yesterday vec[:8]: [-0.08196037 -0.7724959  -0.01066389  0.33832     0.401816    0.06407069
 -0.0740267   1.3621471 ]
  position  8            . vec[:8]: [ 0.5065632   0.02698314 -0.4655037   0.37739474 -0.07271276 -0.5830133
 -0.1283522  -0.09071816]
  position  9        [SEP] vec[:8]: [ 0.76614416  0.1485103  -0.32412663  0.6423916  -0.34358272 -0.6377625
  0.2428914  -0.26649314]
'bank' position in packed sequence: 3

Sentence 1: 'Fisherman rested on the river bank at sunset.'
WordPiece token vectors (first 8 dimensions):
  position  0        [CLS] vec[:8]: [ 0.12813582 -0.12687185 -0.20873152  0.11140953 -0.34956893 -0.19053493
  0.03896     0.8361999 ]
  position  1    fisherman vec[:8]: [ 0.90859425 -0.18009861 -0.579186   -0.00568197  0.17330232  0.34843546
 -0.22004323  0.31754175]
  position  2       rested vec[:8]: [ 0.9529585   0.03583702 -0.08778207  0.46728072 -0.22082405 -0.05925977
  0.1813859   1.2484426 ]
  position  3           on vec[:8]: [ 0.29311347 -0.20351684  0.0949386   0.23519458 -0.13326575  0.2802409
 -0.45601508  0.6076922 ]
  position  4          the vec[:8]: [ 0.3413011  -0.44612893 -0.14317907  0.2397534   0.11432406  0.12255086
 -0.03577439  0.962691  ]
  position  5        river vec[:8]: [ 0.5306748   0.02795732 -0.3424678   0.11826479  0.561594    0.4362433
  0.20837729  1.4342545 ]
  position  6         bank vec[:8]: [-0.08271334 -0.236519    0.07809599  0.02132444 -0.33831847  0.5903888
  0.08351099  1.3977606 ]
  position  7           at vec[:8]: [-0.31397918 -0.3726377   0.20148422  0.17135519 -0.25934803 -0.21453243
  0.11380459  1.3180579 ]
  position  8       sunset vec[:8]: [ 0.68765235 -0.23074177 -0.2042003  -0.29928878  0.73238915 -0.29645845
 -0.1831466   1.0140189 ]
  position  9            . vec[:8]: [ 0.04099102 -0.6158927  -0.18063396  0.30662158  0.04077045  0.08918443
 -0.35202256  0.5784356 ]
  position 10        [SEP] vec[:8]: [ 0.3977864   0.15893528 -0.09611361  0.525856   -0.14167729 -0.33648187
  0.46283796 -0.03933802]
'bank' position in packed sequence: 6

Cosine similarity of BERT pooled outputs: 0.9800999760627747
Cosine similarity of contextual 'bank' token vectors: 0.45333606004714966
Cosine similarity after mean pooling content tokens: 0.5147037506103516

6. Minimalny blok Transformera¶

Przepuść dwa zdania przez mały Transformer z embeddingami tokenów i pozycji, multi-head self-attention, połączeniami rezydualnymi, normalizacją warstwową oraz siecią feed-forward.

Model ma losowe wagi i nie jest wytrenowany. Wynik cosine pokazuje mechanikę oraz przepływ tensorów, a nie nauczone znaczenie językowe, dlatego może zmieniać się między uruchomieniami.

In [8]:
run_experiment("src.transformer.inspect_transformer")
>>> src.transformer.inspect_transformer

Tokenized: [['The', 'central', 'bank', 'raised', 'interest', 'rates', 'yesterday.'], ['A', 'fisherman', 'rested', 'on', 'the', 'river', 'bank', 'at', 'sunset.']]
Encoded input:
 [[ 2  5  4  9  7 10 15  0  0]
 [ 1  6 11  8 14 12  4  3 13]]
Output shape: (2, 9, 64)
Cosine similarity (mini-Transformer) of 'bank': 0.381972074508667

7. Maska paddingu i wagi attention¶

Zbuduj maskę paddingu i obejrzyj wagi attention z pierwszej głowy. Pozycje służące wyłącznie jako padding powinny otrzymywać zerową uwagę jako klucze. Wypisane wymiary tensora mają kolejność [batch, head, query_position, key_position].

In [9]:
run_experiment("src.transformer.padding_mask_and_weights")
>>> src.transformer.padding_mask_and_weights

Encoded with padding:
 [[ 3  1  9  7  6  4]
 [ 2  5  4  8 10  0]]
Pad mask:
 [[1 1 1 1 1 1]
 [1 1 1 1 1 0]]
Output shape: (2, 6, 64)
Attention scores shape: (2, 4, 6, 6)
Attention weights (first head, first example) [batch, head]:
tf.Tensor(
[[0.1666616  0.16666631 0.16666538 0.16666375 0.16667151 0.16667141]
 [0.16666645 0.16666062 0.16666807 0.16666964 0.1666692  0.16666605]
 [0.16666825 0.166668   0.1666671  0.16666979 0.16666605 0.16666077]
 [0.16666673 0.1666665  0.16666955 0.16666637 0.16666628 0.1666645 ]
 [0.16666278 0.1666528  0.16666439 0.16668333 0.16667756 0.16665916]
 [0.16666618 0.16666411 0.16666327 0.16665864 0.16667463 0.16667315]], shape=(6, 6), dtype=float32)
tf.Tensor(
[[0.2000011  0.19999053 0.20000985 0.20001853 0.19998002 0.        ]
 [0.19999789 0.19998094 0.20000403 0.20000893 0.20000817 0.        ]
 [0.20001902 0.19999412 0.20001097 0.19998789 0.19998805 0.        ]
 [0.20000605 0.20000596 0.19999135 0.19999126 0.20000541 0.        ]
 [0.19999458 0.19998644 0.20000859 0.20000161 0.20000885 0.        ]
 [0.20000386 0.20000723 0.20000531 0.19999678 0.19998679 0.        ]], shape=(6, 6), dtype=float32)

Wnioski¶

Eksperymenty prowadzą od inspekcji modeli do interpretacji reprezentacji: ELMo udostępnia kilka warstw kontekstowych, BERT wymaga indeksowania świadomego tokenizacji WordPiece, a self-attention potrzebuje jawnej maski dla dopełnionych sekwencji. Najważniejszy wniosek jest prosty: sam kształt tensora i nazwa wyjścia nie wystarczają; reprezentacja musi pasować do porównania, które wykonujemy.

In [ ]: