2022-05-28 14:15:35 +02:00
|
|
|
{
|
|
|
|
"cells": [
|
2022-07-06 07:58:45 +02:00
|
|
|
{
|
|
|
|
"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> 12. <i>Model języka oparty na rekurencyjnej sieci neuronowej</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"
|
|
|
|
]
|
|
|
|
},
|
2022-05-28 14:15:35 +02:00
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"## Model języka oparty na rekurencyjnej sieci neuronowej\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Podejście rekurencyjne\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Na poprzednim wykładzie rozpatrywaliśmy różne funkcje\n",
|
|
|
|
"$A(w_1,\\dots,w_{i-1})$, dzięki którym możliwe było „skompresowanie” ciągu słów\n",
|
|
|
|
"(a właściwie ich zanurzeń) o dowolnej długości w wektor o stałej długości.\n",
|
|
|
|
"\n",
|
|
|
|
"Funkcję $A$ moglibyśmy zdefiniować w inny sposób, w sposób ****rekurencyjny****.\n",
|
|
|
|
"\n",
|
|
|
|
"Otóż moglibyśmy zdekomponować funkcję $A$ do\n",
|
|
|
|
"\n",
|
|
|
|
"- pewnego stanu początkowego $\\vec{s_0} \\in \\mathcal{R}^p$,\n",
|
|
|
|
"- pewnej funkcji rekurencyjnej $R : \\mathcal{R}^p \\times \\mathcal{R}^m \\rightarrow \\mathcal{R}^p$.\n",
|
|
|
|
"\n",
|
|
|
|
"Wówczas funkcję $A$ można będzie zdefiniować rekurencyjnie jako:\n",
|
|
|
|
"\n",
|
|
|
|
"$$A(w_1,\\dots,w_t) = R(A(w_1,\\dots,w_{t-1}), E(w_t)),$$\n",
|
|
|
|
"\n",
|
|
|
|
"przy czym dla ciągu pustego:\n",
|
|
|
|
"\n",
|
|
|
|
"$$A(\\epsilon) = \\vec{s_0}$$\n",
|
|
|
|
"\n",
|
|
|
|
"Przypomnijmy, że $m$ to rozmiar zanurzenia (embeddingu). Z kolei $p$ to rozmiar wektora stanu\n",
|
|
|
|
"(często $p=m$, ale nie jest to konieczne).\n",
|
|
|
|
"\n",
|
|
|
|
"Przy takim podejściu rekurencyjnym wprowadzamy niejako „strzałkę\n",
|
|
|
|
"czasu”, możemy mówić o przetwarzaniu krok po kroku.\n",
|
|
|
|
"\n",
|
|
|
|
"W wypadku modelowania języka możemy końcowy wektor stanu zrzutować do wektora o rozmiarze słownika\n",
|
|
|
|
"i zastosować softmax:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{y} = \\operatorname{softmax}(CA(w_1,\\dots,w_{i-1})),$$\n",
|
|
|
|
"\n",
|
|
|
|
"gdzie $C$ jest wyuczalną macierzą o rozmiarze $|V| \\times p$.\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Worek słów zdefiniowany rekurencyjnie\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Nietrudno zdefiniować model „worka słów” w taki rekurencyjny sposób:\n",
|
|
|
|
"\n",
|
|
|
|
"- $p=m$,\n",
|
|
|
|
"- $\\vec{s_0} = [0,\\dots,0]$,\n",
|
|
|
|
"- $R(\\vec{s}, \\vec{x}) = \\vec{s} + \\vec{x}.$\n",
|
|
|
|
"\n",
|
|
|
|
"Dodawanie (również wektorowe) jest operacją przemienną i łączną, więc\n",
|
|
|
|
"to rekurencyjne spojrzenie niewiele tu wnosi. Można jednak zastosować\n",
|
|
|
|
"inną funkcję $R$, która nie jest przemienna — w ten sposób wyjdziemy poza\n",
|
|
|
|
"nieuporządkowany worek słów.\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Związek z programowaniem funkcyjnym\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Zauważmy, że stosowane tutaj podejście jest tożsame z zastosowaniem funkcji typu `fold`\n",
|
|
|
|
"w językach funkcyjnych:\n",
|
|
|
|
"\n",
|
2022-07-06 07:58:45 +02:00
|
|
|
"![img](./12_Rekurencyjny_model_jezyka/fold.png \"Opis funkcji foldl w języku Haskell\")\n",
|
2022-05-28 14:15:35 +02:00
|
|
|
"\n",
|
|
|
|
"W Pythonie odpowiednik `fold` jest funkcja `reduce` z pakietu `functools`:\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "code",
|
|
|
|
"execution_count": 1,
|
|
|
|
"metadata": {},
|
|
|
|
"outputs": [
|
|
|
|
{
|
|
|
|
"name": "stdout",
|
|
|
|
"output_type": "stream",
|
|
|
|
"text": [
|
|
|
|
"18"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"source": [
|
|
|
|
"from functools import reduce\n",
|
|
|
|
"\n",
|
|
|
|
"def product(ns):\n",
|
|
|
|
" return reduce(lambda a, b: a * b, ns, 1)\n",
|
|
|
|
"\n",
|
|
|
|
"product([2, 3, 1, 3])"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Sieci rekurencyjne\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"W jaki sposób „złamać” przemienność i wprowadzić porządek? Jedną z\n",
|
|
|
|
"najprostszych operacji nieprzemiennych jest konkatenacja — możemy\n",
|
|
|
|
"dokonać konkatenacji wektora stanu i bieżącego stanu, a następnie\n",
|
|
|
|
"zastosować jakąś prostą operację (na wyjściu musimy mieć wektor o\n",
|
|
|
|
"rozmiarze $p$, nie $p + m$!), dobrze przy okazji „złamać” też\n",
|
|
|
|
"liniowość operacji. Możemy po prostu zastosować rzutowanie (mnożenie\n",
|
|
|
|
"przez macierz) i jakąś prostą funkcję aktywacji (na przykład sigmoidę):\n",
|
|
|
|
"\n",
|
|
|
|
"$$R(\\vec{s}, \\vec{e}) = \\sigma(W[\\vec{s},\\vec{e}] + \\vec{b}).$$\n",
|
|
|
|
"\n",
|
|
|
|
"Dodatkowo jeszcze wprowadziliśmy wektor obciążeń $\\vec{b}$, a zatem wyuczalne wagi obejmują:\n",
|
|
|
|
"\n",
|
|
|
|
"- macierz $W \\in \\mathcal{R}^p \\times \\mathcal{R}^{p+m}$,\n",
|
|
|
|
"- wektor obciążeń $b \\in \\mathcal{R}^p$.\n",
|
|
|
|
"\n",
|
|
|
|
"Olbrzymią zaletą sieci rekurencyjnych jest fakt, że liczba wag nie zależy od rozmiaru wejścia!\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Zwykła sieć rekurencyjna\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Wyżej zdefiniową sieć nazywamy „zwykłą” siecią rekurencyjną (*Vanilla RNN*).\n",
|
|
|
|
"\n",
|
|
|
|
"**Uwaga**: przez RNN czasami rozumie się taką „zwykłą” sieć\n",
|
|
|
|
"rekurencyjną, a czasami szerszą klasę sieci rekurencyjnych\n",
|
|
|
|
"obejmujących również sieci GRU czy LSTM (zob. poniżej).\n",
|
|
|
|
"\n",
|
2022-07-06 07:58:45 +02:00
|
|
|
"![img](./12_Rekurencyjny_model_jezyka/rnn.drawio.png \"Schemat prostego modelu języka opartego na zwykłej sieci rekurencyjnych\")\n",
|
2022-05-28 14:15:35 +02:00
|
|
|
"\n",
|
|
|
|
"**Uwaga**: powyższy schemat nie obejmuje już „całego” działania sieci,\n",
|
|
|
|
" tylko pojedynczy krok czasowy.\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Praktyczna niestosowalność prostych sieci RNN\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Niestety w praktyce proste sieci RNN sprawiają duże trudności jeśli\n",
|
|
|
|
"chodzi o propagację wsteczną — pojawia się zjawisko zanikającego\n",
|
|
|
|
"(rzadziej: eksplodującego) gradientu. Dlatego zaproponowano różne\n",
|
|
|
|
"modyfikacje sieci RNN. Zacznijmy od omówienia stosunkowo prostej sieci GRU.\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Sieć GRU\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"GRU (*Gated Recurrent Unit*) to sieć z dwiema ****bramkami**** (*gates*):\n",
|
|
|
|
"\n",
|
|
|
|
"- bramką resetu (*reset gate*) $\\Gamma_\\gamma \\in \\mathcal{R}^p$ — która określa, w jakim\n",
|
|
|
|
" stopniu sieć ma pamiętać albo zapominać stan z poprzedniego kroku,\n",
|
|
|
|
"- bramką aktualizacji (*update gate*) $\\Gamma_u \\in \\mathcal{R}^p$ — która określa wpływ\n",
|
|
|
|
" bieżącego wyrazu na zmianę stanu.\n",
|
|
|
|
"\n",
|
|
|
|
"Tak więc w skrajnym przypadku:\n",
|
|
|
|
"\n",
|
|
|
|
"- jeśli $\\Gamma_\\gamma = [0,\\dots,0]$, sieć całkowicie zapomina\n",
|
|
|
|
" informację płynącą z poprzednich wyrazów,\n",
|
|
|
|
"- jeśli $\\Gamma_u = [0,\\dots,0]$, sieć nie bierze pod uwagę\n",
|
|
|
|
" bieżącego wyrazu.\n",
|
|
|
|
"\n",
|
|
|
|
"Zauważmy, że bramki mogą selektywnie, na każdej pozycji wektora stanu,\n",
|
|
|
|
"sterować przepływem informacji. Na przykład $\\Gamma_\\gamma =\n",
|
|
|
|
"[0,1,\\dots,1]$ oznacza, że pierwsza pozycja wektora stanu jest\n",
|
|
|
|
"zapominana, a pozostałe — wnoszą wkład w całości.\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Wzory\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Najpierw zdefiniujmy pośredni stan $\\vec{\\xi} \\in \\mathcal{R}^p$:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\Gamma_\\gamma \\bullet \\vec{s_{t-1}}, E(w_t)] + b_{\\xi}),$$\n",
|
|
|
|
"\n",
|
|
|
|
"gdzie $\\bullet$ oznacza iloczyn Hadamarda (nie iloczyn skalarny!) dwóch wektorów:\n",
|
|
|
|
"\n",
|
|
|
|
"$$[x_1,\\dots,x_n] \\bullet [y_1,\\dots,y_n] = [x_1 y_1,\\dots,x_n y_n].$$\n",
|
|
|
|
"\n",
|
|
|
|
"Jak widać, obliczanie $\\vec{\\xi_t}$ bardzo przypomina zwykłą sieć rekurencyjną,\n",
|
|
|
|
"jedyna różnica polega na tym, że za pomocą bramki $\\Gamma_\\gamma$\n",
|
|
|
|
"modulujemy wpływ poprzedniego stanu.\n",
|
|
|
|
"\n",
|
|
|
|
"Ostateczna wartość stanu jest średnią ważoną poprzedniego stanu i bieżącego stanu pośredniego:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{s_t} = \\Gamma_u \\bullet \\vec{\\xi_t} + (1 - \\Gamma_u) \\bullet \\vec{s_{t-1}}.$$\n",
|
|
|
|
"\n",
|
2022-07-06 07:58:45 +02:00
|
|
|
"Skąd się biorą bramki $\\Gamma_\\gamma$ i $\\Gamma_u$? Również z poprzedniego stanu i z bieżącego wyrazu.\n",
|
2022-05-28 14:15:35 +02:00
|
|
|
"\n",
|
|
|
|
"$$\\Gamma_\\gamma = \\sigma(W_\\gamma[\\vec{s_{t-1}},E(w_t)] + \\vec{b_\\gamma}),$$\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\Gamma_u = \\sigma(W_u[\\vec{s_{t-1}},E(w_t)] + \\vec{b_u}),$$\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"### Sieć LSTM\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Architektura LSTM (*Long Short-Term Memory*), choć powstała wcześniej\n",
|
|
|
|
"niż GRU, jest od niej nieco bardziej skomplikowana.\n",
|
|
|
|
"\n",
|
|
|
|
"- zamiast dwóch bramek LSTM zawiera ****trzy bramki****: bramkę wejścia (*input gate*),\n",
|
|
|
|
" bramkę wyjścia (*output gate*) i bramkę zapominania (*forget gate*),\n",
|
|
|
|
"- oprócz ukrytego stanu $\\vec{s_t}$ sieć LSTM posiada również ****komórkę pamięci**** (*memory cell*),\n",
|
|
|
|
" $\\vec{c_t}$, komórka pamięci, w przeciwieństwie do stanu, zmienia się wolniej (intuicyjnie:\n",
|
|
|
|
" *jeśli nie zrobimy nic specjalnego, wartość komórki pamięci się nie zmieni*).\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Wzory\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Komórka pamięci modulowana jest za pomocą bramki zapominania ($\\Gamma_f$) i bramki\n",
|
2022-07-06 07:58:45 +02:00
|
|
|
"wejścia ($\\Gamma_i$), bramki te określają, na ile uwzględniamy, odpowiednio,\n",
|
2022-05-28 14:15:35 +02:00
|
|
|
"poprzednią wartość komórki pamięci $\\vec{c_{t-1}}$ i wejście, a\n",
|
|
|
|
"właściwie wejście w połączeniu z poprzednim stanem:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{c_t} = \\Gamma_f \\bullet \\vec{c_{t-1}} + \\Gamma_i \\bullet \\vec{\\xi_t},$$\n",
|
|
|
|
"\n",
|
|
|
|
"gdzie wektor pomocniczy $\\vec{\\xi_t}$ wyliczany jest w następujący sposób:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{\\xi_t} = \\operatorname{tanh}(W_{\\xi}[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_\\xi}.$$\n",
|
|
|
|
"\n",
|
|
|
|
"Nowa wartość stanu sieci nie zależy bezpośrednio od poprzedniej wartości stanu, lecz\n",
|
|
|
|
"jest równa komórce pamięci modulowanej bramką wyjścia:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{h_t} = \\Gamma_o \\bullet \\operatorname{tanh}(\\vec{c_t}).$$\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Obliczanie bramek\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Wartości wszystkie trzech bramek są liczone w identyczny sposób (wzory\n",
|
|
|
|
"różnią się tylko macierzami wag i wektorem obciążeń):\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\Gamma_f = \\sigma(W_f[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_f}),$$\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\Gamma_i = \\sigma(W_i[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_i}),$$\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\Gamma_o = \\sigma(W_o[\\vec{s_{t-1}}, E(w_t)] + \\vec{b_o}).$$\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Wartości początkowe\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Początkowe wartości stanu i komórki pamięci mogą być ustawione na zero:\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{s_0} = \\vec{0},$$\n",
|
|
|
|
"\n",
|
|
|
|
"$$\\vec{c_0} = \\vec{0}.$$\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"#### Podsumowanie\n",
|
|
|
|
"\n"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"cell_type": "markdown",
|
|
|
|
"metadata": {},
|
|
|
|
"source": [
|
|
|
|
"Sieci LSTM dominowały w zagadnieniach przetwarzania języka naturalnego\n",
|
|
|
|
"(ogólniej: przetwarzania sekwencji) do czasu pojawienia się\n",
|
|
|
|
"architektury Transformer w 2017 roku.\n",
|
|
|
|
"\n",
|
|
|
|
"Na sieci LSTM oparty był ELMo, jeden z pierwszych dużych\n",
|
|
|
|
"****pretrenowanych modeli języka****, dostrajanych później pod konkretne\n",
|
|
|
|
"zadania (na przykład klasyfikację tekstu), zob. artykuł [Deep\n",
|
|
|
|
"contextualized word\n",
|
|
|
|
"representations]([https://arxiv.org/pdf/1802.05365.pdf](https://arxiv.org/pdf/1802.05365.pdf)). Dokładniej\n",
|
|
|
|
"mówiąc, ELMo był siecią ****BiLSTM****, połączeniem dwóch sieci, jednej\n",
|
|
|
|
"działającej z lewej strony na prawą, drugiej — z prawej do lewej.\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",
|
2022-07-06 07:58:45 +02:00
|
|
|
"version": "3.10.5"
|
2022-05-28 14:15:35 +02:00
|
|
|
},
|
|
|
|
"org": null
|
|
|
|
},
|
|
|
|
"nbformat": 4,
|
|
|
|
"nbformat_minor": 1
|
|
|
|
}
|