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.
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.
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.
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.
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.
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.
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.
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].
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.