496 lines
17 KiB
Plaintext
496 lines
17 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n",
|
|
"<div class=\"alert alert-block alert-info\">\n",
|
|
"<h1> Modelowanie języka</h1>\n",
|
|
"<h2> 11. <i>Transformer</i> [wykład]</h2> \n",
|
|
"<h3> Filip Graliński (2022)</h3>\n",
|
|
"</div>\n",
|
|
"\n",
|
|
"![Logo 2](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech2.jpg)\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Transformer\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Atencja jako „miękka” baza danych\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"O atencji można myśleć metaforycznie jako o odpytywaniu „miękkiej”, wektorowej\n",
|
|
"bazy danych. Możemy sobie wyobrazić, że słowa $w_1,\\dots,w_{j-1}$ są\n",
|
|
"naszą bazą danych, a słowo $w_j$ (z którego kierujemy „snop” uwagi)\n",
|
|
"jest **zapytaniem** (*query*). To zapytanie dopasowujemy do **kluczy**\n",
|
|
"(*keys*), w najprostszym ujęciu po prostu słów $w_1,\\dots,w_{j-1}$ (a\n",
|
|
"właściwie ich zanurzeń). Jeśli klucz pasuje do zapytania, odpowiednia\n",
|
|
"wartość (*value*) jest wydobywana z bazy. Nasza baza jest jednak\n",
|
|
"„miękka”, nie — zerojedynkowa, zapytanie pasuje do klucza w pewnym\n",
|
|
"stopniu, mniej lub bardziej.\n",
|
|
"\n",
|
|
"W najprostszym ujęciu wartości są tym samym co klucze, czyli z naszej\n",
|
|
"bazy wydobywamy te same zanurzenia słów, których używamy jako kluczy.\n",
|
|
"Można jednak skomplikować schemat, rozróżniając klucze i wartości —\n",
|
|
"mogą one powstawać przez rzutowanie podstawowych zanurzeń różnymi\n",
|
|
"macierzami:\n",
|
|
"\n",
|
|
"$$\\vec{k_i} = W_k E(w_i),$$\n",
|
|
"\n",
|
|
"$$\\vec{v_i} = W_v E(w_i).$$\n",
|
|
"\n",
|
|
"Również samo zapytanie może powstać przez rzutowanie:\n",
|
|
"\n",
|
|
"$$\\vec{q_i} = W_q E(w_i).$$\n",
|
|
"\n",
|
|
"Jeśli zanurzenie $E(w_i)$ o rozmiarze $m$ przedstawimy w postaci\n",
|
|
"kolumnowej, wówczas macierze będą $W_k$ i $W_q$ będą miały rozmiar\n",
|
|
"$d_k \\times m$, gdzie $d_k$ jest rozmiarem kluczy i zapytań (dlaczego\n",
|
|
"wektory kluczy i zapytań powinny mieć raczej ten sam rozmiar?), zaś macierz\n",
|
|
"$W_v$ — $d_v \\times m$, gdzie $d_v$ to rozmiar zanurzenia wektora wartości.\n",
|
|
"Zazwyczaj $d_k = d_v = m$, ale nie jest to obligatoryjne.\n",
|
|
"\n",
|
|
"Teraz nieznormalizowane wagi atencji przyjmą postać:\n",
|
|
"\n",
|
|
"$$\\hat{\\alpha}_{i,j} = \\vec{q_i}^T\\vec{k_j} = (W_q E(w_i))(W_k E(k_j)).$$\n",
|
|
"\n",
|
|
"Zauważmy, że ciąg $\\hat{\\alpha}_{1,j},\\dots,\\hat{\\alpha}_{j-1,j}$ można potraktować jako wektor\n",
|
|
"$\\hat{\\vec{\\alpha}_{*,j}}$ i wyliczać w postaci zwartej:\n",
|
|
"\n",
|
|
"$$\\hat{\\vec{\\alpha}_{*,j}} = \\vec{q_j}^T K$$\n",
|
|
"\n",
|
|
"gdzie $K$ to macierz kluczy złożona z wektorów\n",
|
|
"$\\vec{k_1},\\dots,\\vec{k_{j-1}}$, tj. macierz o rozmiarze $d_k \\times (j-1)$.\n",
|
|
"\n",
|
|
"Wektor znormalizowanych wag atencji będzie miał wówczas postać:\n",
|
|
"\n",
|
|
"$$\\vec{\\alpha}_{*,j} = \\operatorname{softmax}(\\vec{q_j}^T K).$$\n",
|
|
"\n",
|
|
"Dokonajmy teraz agregacji wartości — obliczamy średnią wektorów\n",
|
|
"wartości ($\\vec{v_i}$) ważoną atencją:\n",
|
|
"\n",
|
|
"$$A(w_1,\\dots,j-1) = \\alpha_{1,j} \\vec{v_1} + \\dots + \\alpha_{j-1,j} \\vec{v_{j-1}} = \\sum_{i=1}^{j-1} \\alpha_{i,j} v_i.$$\n",
|
|
"\n",
|
|
"Jeśli $j-1$ wektorów wartości ułożymy w macierz $V$ (o rozmiarze\n",
|
|
"$(j-1) \\times d_v$), powyższy wzór będziemy mogli zapisać jako iloczyn wektora wag atencji i macierzy $V$:\n",
|
|
"\n",
|
|
"$$A(w_1,\\dots,j-1) = \\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V.$$\n",
|
|
"\n",
|
|
"Sposób patrzenia na atencję przez pryzmat trójki\n",
|
|
"zapytania-klucze-wartości okaże się niezwykle ważny w wypadku modelu Transformer.\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Model Transformer — historia\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Architekturę Transformer opracowano, pierwotnie, na potrzeby\n",
|
|
"tłumaczenia automatycznego (rok 2017, artykuł [Attention Is All You\n",
|
|
"Need]([https://arxiv.org/abs/1706.03762](https://arxiv.org/abs/1706.03762))). Szybko okazało się, że\n",
|
|
"podobnie jak w wypadku modelu ELMo dla sieci LSTM, można **pretrenować**\n",
|
|
"duże modele Transformer (po prostu na dużych korpusach tekstowych, w\n",
|
|
"sposób nienadzorowany), a potem dostrajać pod konkretne zadanie\n",
|
|
"przetwarzania języka naturalnego. Jednym z pierwszych przykładów\n",
|
|
"takiego podejścia był model BERT (rok 2018, artykuł [BERT:\n",
|
|
"Pre-training of Deep Bidirectional Transformers for Language\n",
|
|
"Understanding]([https://arxiv.org/abs/1810.04805](https://arxiv.org/abs/1810.04805))). To podejście było\n",
|
|
"później rozwinięte w postaci różnych modeli Transformer, również dla innych\n",
|
|
"języków niż angielski (RoBERTa, XLM, Polish RoBERTa itd.).\n",
|
|
"\n",
|
|
"Na tym wykładzie my skupimy się na innej odnodze modeli Transformer —\n",
|
|
"modelach generatywnych, takich jak na przykład GPT-2 czy GPT-3. To\n",
|
|
"podejście jest bliższe duchowi czystego modelowania języka — model\n",
|
|
"języka jest używany wprost jako generator.\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### GPT-2 — przykład działania\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Dokonajmy najpierw tokenizacji:\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"{'input_ids': tensor([[ 464, 2159, 1810, 6711, 481, 2221, 287, 1160, 2078, 287]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"from transformers import AutoTokenizer\n",
|
|
"tokenizer = AutoTokenizer.from_pretrained(\"gpt2\")\n",
|
|
"text = \"The World War III will begin in 2028 in\"\n",
|
|
"encoded_input = tokenizer(text, return_tensors='pt')\n",
|
|
"encoded_input"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"['The', ' World', ' War', ' III', ' will', ' begin', ' in', ' 20', '28', ' in']"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"[tokenizer.decode(i) for i in encoded_input.input_ids[0]]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Zwróćmy uwagę, że w GPT-2 tokeny obejmują spacje!\n",
|
|
"\n",
|
|
"Teraz uruchommy zasadniczy model:\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from transformers import AutoModelForCausalLM\n",
|
|
"model = AutoModelForCausalLM.from_pretrained(\"gpt2\")\n",
|
|
"outputs = model(**encoded_input)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"softmax(outputs[0][0][-1])"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Z modelu GPT-2 otrzymamy rozkład prawdopodobieństwa kolejnego wyrazu, najpierw w postaci\n",
|
|
"nieznormalizowanych **logitów**:\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"tensor([-130.2947, -129.5677, -136.4030, ..., -138.3791, -138.8967,\n",
|
|
" -131.6319], grad_fn=<SelectBackward0>)"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"logits = outputs[0][0][-1]\n",
|
|
"logits"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"[[' earnest', 0.07378227263689041], [' the', 0.06698606163263321], [' 1945', 0.043497972190380096], [' September', 0.024068640545010567], [' March', 0.0228887926787138], [' October', 0.02232857048511505], [' Europe', 0.02032744698226452], [' 2020', 0.018564637750387192], [' Japan', 0.018423961475491524], [' December', 0.016560807824134827], [' January', 0.015074416995048523], [' July', 0.014139187522232533], [' April', 0.013183596543967724], [' November', 0.012901309877634048], [' 20', 0.012770282104611397], [' Afghanistan', 0.012765118852257729], [' 1944', 0.01266297698020935], [' June', 0.012072316370904446], [' 1914', 0.011765970848500729], [' May', 0.011659453622996807]]"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"from torch import softmax, topk\n",
|
|
"\n",
|
|
"k = 20\n",
|
|
"\n",
|
|
"t = topk(softmax(logits, -1), k)\n",
|
|
"\n",
|
|
"tb = [[tokenizer.decode(t.indices[ix]), t.values[ix].item()] for ix in range(k)]\n",
|
|
"tb"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Generowanie tekstu za pomocą GPT-2\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from transformers import pipeline\n",
|
|
"generator = pipeline('text-generation', model='gpt2')\n",
|
|
"generator('Hello, I\\'m a language model,', max_length=30, num_return_sequences=1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Model Transformer — podstawowa idea\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Model Transformer sprowadza się właściwie do atencji; nie posiada\n",
|
|
"żadnego komponentu rekurencyjnego, ani nawet nie stosujemy czegoś w\n",
|
|
"rodzaju połączenia modelu worka słów i modelu n-gramowego.\n",
|
|
"\n",
|
|
"W pierwszym przybliżeniu przy obliczaniu rozkładu prawdopodobieństwa\n",
|
|
"dla kolejnego wyrazu, to jest:\n",
|
|
"\n",
|
|
"$$P(w_j|w_1\\dots w_{j-1})$$\n",
|
|
"\n",
|
|
"na $j$-tym miejscu (w miejscu przewidywanego wyrazu) doklejamy\n",
|
|
"specjalny token, powiedzmy `<mask>`. Token ten będzie „atendował” do\n",
|
|
"innych wszystkich wcześniejszych tokenów w zdaniu:\n",
|
|
"\n",
|
|
"$$\\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V.$$\n",
|
|
"\n",
|
|
"Samo to byłoby oczywiście zbyt proste:\n",
|
|
"\n",
|
|
"1. Otrzymalibyśmy model (ważonego) worka słów, w dodatku każde słowo\n",
|
|
" miałoby zawsze taką samą wagę! — token $w_j$, który atenduje jest\n",
|
|
" zawsze ten sam (`<mask>`). Musimy wzbogacić reprezentację wektorową\n",
|
|
" słów i specjalnego tokenu (`<mask>`).\n",
|
|
"\n",
|
|
"2. Model Transformer w swojej podstawowej postaci w ogóle nie jest\n",
|
|
" wyposażony w pojęcie sekwencji — w przeciwieństwie do sieci\n",
|
|
" rekurencyjnych, które w sposób inherentny operują krok po kroku, w\n",
|
|
" sekwencji (w czasie). Musimy pozycję tokenów wprowadzić do sieci\n",
|
|
" Transformer nie przez modyfikację jej architektury, lecz przez dołączenie\n",
|
|
" informacji pozycyjnej do początkowych zanurzeń.\n",
|
|
"\n",
|
|
"3. Model Transformer nie powinien mieć żadnych tokenów OOV/UNK. Musimy\n",
|
|
" wrócić do kwestii tokenizacji tekstu i wprowadzić podział rzadszych\n",
|
|
" tokenów na mniejsze, **podwyrazowe** jednostki.\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Atencja wsobna\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Jeśli chodzi problem (1), rozwiążemy go przez wprowadzenie\n",
|
|
"****skontekstualizowanych reprezentacji**** tokenów.\n",
|
|
"\n",
|
|
"Na przykład słowo *mysz* ma jedno wejściowe (*statyczne*) zanurzenie\n",
|
|
"(embedding) — bez względu na to, czy chodzi o zwierzę czy urządzenie\n",
|
|
"peryferyjne, tymczasem dość łatwo ustalić na podstawie kontekstu, o\n",
|
|
"które znaczenie chodzi.\n",
|
|
"\n",
|
|
"Rozwiązanie polega na tym, że wszystkim tokenom będziemy przypisywać kolejne\n",
|
|
"zanurzenia skontekstualizowane — zależne od innych tokenów w zdaniu. W\n",
|
|
"tym celu zastosujemy atencję wsobną (samo-atencję, *self-attention*).\n",
|
|
"Każdy token będzie atendował potencjalnie do każdego innego tokenu,\n",
|
|
"również do samego siebie (!).\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Wzory\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Rozpatrywać zatem będziemy nie tylko pojedynczy wektor znormalizowanych atencji\n",
|
|
"\n",
|
|
"$$\\vec{\\alpha}_{*,j}^T V = \\operatorname{softmax}(\\vec{q_j}^T K)^T V,$$\n",
|
|
"\n",
|
|
"lecz całą serię wektorów:\n",
|
|
"\n",
|
|
"$$\\vec{\\alpha}_{*,1},\\dots,\\vec{\\alpha}_{*,i},\\dots,\\vec{\\alpha}_{*,j},$$\n",
|
|
"\n",
|
|
"gdzie:\n",
|
|
"\n",
|
|
"$$\\vec{\\alpha}_{*,i} = \\operatorname{softmax}(\\vec{q_i}^T K)$$\n",
|
|
"\n",
|
|
"i $K$ jest macierzą kluczy o rozmiarze $d_k \\times j$ (tym razem obejmuje również sam $i$-ty token).\n",
|
|
"\n",
|
|
"Nowa, skontekstualizowana reprezentacja $i$-tego tokenu będzie po prostu średnią wszystkich\n",
|
|
"wektorów ważoną atencją:\n",
|
|
"\n",
|
|
"$$E_1(w_i) = \\operatorname{softmax}(\\vec{q_i}^T K)^T V,$$\n",
|
|
"\n",
|
|
"gdzie:\n",
|
|
"\n",
|
|
"- $E_1(w_i)$ — skontekstualizowane zanurzenie $i$-tego tokenu; używając indeksu $_1$\n",
|
|
" zaznaczamy, że to jest pierwszy skonstekstualizowany embedding, rekurencyjnie będziemy budowali\n",
|
|
" kolejne $E_2(w_i)$, $E_3(w_i)$ itd. (zaś wejściowy statyczny embedding możemy oznaczyć przez $E_0(w_i)$);\n",
|
|
"- $V$ — macierz wartości o rozmiarze $j \\times d_v$.\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"##### Zwarta postać macierzowa atencji wsobnej\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Z praktycznych powodów (szybkość obliczeń na kartach graficznych) dużą\n",
|
|
"zaletą atencji wsobnej jest to, że wyliczenie skonstekstualizowanych zanurzeń dla wszystkich tokenów\n",
|
|
"w tekście da się zapisać w postaci zwartego wzoru macierzowego:\n",
|
|
"\n",
|
|
"$$E_1 = \\operatorname{Attention}(Q, K, V) = \\operatorname{softmax}(QK)^T V,$$\n",
|
|
"\n",
|
|
"gdzie $Q$ to macierz wszystkich zapytań o rozmiarze $j \\times d_k$ (wektory ułożone poziomo).\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"##### Skalowanie atencji\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Twórcy modelu Transformer odkryli, że lepsze wyniki daje skalowanie atencji\n",
|
|
"przez stałą zależną od rozmiaru wektora klucza/zapytania $d_k$:\n",
|
|
"\n",
|
|
"$$\\operatorname{Attention}(Q, K, V) = \\operatorname{softmax}(\\frac{QK}{d_k})^T V,$$\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Wielogłowicowa atencja\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Od samego początku w Transformerze zamiast jednej atencji zaproponowano wiele ****głowic atencji****\n",
|
|
"$(\\operatorname{head}_1,\\dots,\\operatorname{head}_h)$, każda głowica atencji działa w następujący sposób:\n",
|
|
"\n",
|
|
"$$\\operatorname{head_i} = \\operatorname{Attention}(QW_i^Q, KW_i^K,VW_i^V),$$\n",
|
|
"\n",
|
|
"to znaczy każda głowica atencji działa tak samo, tylko przed jej zastosowaniem mnożymy\n",
|
|
"wektory zapytań, kluczy i wartości przez różne wyuczalne macierze, odpowiednio,\n",
|
|
"$W_i^Q$, $W_i^K$, $W_i^V$. Otrzymamy w ten sposób $h$ wektorów, konkatenujemy je po prostu i mnożymy\n",
|
|
"przez dodatkową wyuczalną macierz $W^O$:\n",
|
|
"\n",
|
|
"$$\\operatorname{MultiHead}(Q, K, V) = [\\operatorname{head}_1,...,\\operatorname{head}_n]W^O.$$\n",
|
|
"\n",
|
|
"Przyjmujemy, że $d_k = d_v = m/h$, wtedy rozmiary macierzy $W_i^Q$ i $W_i^K$ będą wynosiły\n",
|
|
"$m \\times d_k$, macierzy $W_i^V$ — $m \\times d_v$, $W^O$ — $hd_v \\times m$.\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.10.4"
|
|
},
|
|
"org": null
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 1
|
|
}
|