{
  "version": "https://jsonfeed.org/version/1",
  "title": "gruszka.dev",
  "home_page_url": "https://gruszka.dev",
  "feed_url": "https://gruszka.dev/tag-markow.json",
  "description": "Things I would like to share",
  "items": [
    {
      "id": "https://gruszka.dev/jak-komputer-czyta-tekst.html",
      "url": "https://gruszka.dev/jak-komputer-czyta-tekst.html",
      "title": "Jak komputer czyta tekst - od liczenia słów do wektorów",
      "content_html": "<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Chcesz uruchomić ten kod samemu?</strong> Wszystkie przykłady z tego posta znajdziesz w gotowym notatniku Jupyter: <a href=\"./jak-komputer-czyta-tekst.ipynb\">jak-komputer-czyta-tekst.ipynb</a></p>\n</div>\n<p>W dwóch poprzednich wpisach zbudowaliśmy sobie solidny fundament. W <a href=\"cechy-jezykowe-a-llm.html\">pierwszym</a> rozłożyliśmy język na czynniki pierwsze - fonetyka, morfologia, składnia, semantyka, pragmatyka. W <a href=\"semiotyka-a-llm.html\">drugim</a> zapytaliśmy: &quot;OK, ale czy LLM w ogóle rozumie to, co generuje?&quot; i doszliśmy do wniosku, że LLM jest maszyną znaków, nie umysłem.</p>\n<p>Ale zostało mi jedno fundamentalne pytanie, na które nie odpowiedziałem: <strong>jak komputer w ogóle &quot;bierze&quot; tekst do środka?</strong></p>\n<p>Bo przecież komputer nie widzi liter. Nie widzi słów. Komputer widzi <strong>liczby</strong>. Więc jak to się dzieje, że wpisujesz &quot;Kot siedzi na macie&quot; i ChatGPT jakoś to przetwarza? Jaka jest droga od tekstu do liczby, od liczby do &quot;rozumienia&quot;?</p>\n<p>Ta droga to fascynująca historia ewolucji - od najprostszego liczenia słów, przez coraz sprytniejsze triki matematyczne, aż po wektory, które potrafią uchwycić podobieństwa znaczeniowe. Każdy krok na tej drodze był odpowiedzią na ograniczenia poprzedniego.</p>\n<p>To jest <strong>trzeci wpis z serii &quot;zrozumiec LLM&quot;</strong>. Dzisiaj zmieniamy perspektywę z lingwistycznej i filozoficznej na <strong>techniczną</strong>. Ale bez obaw - dalej będzie dużo przykładów, dużo &quot;aha!&quot; i zero wzorów, których nie da się zrozumieć ;-)</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    T[&quot;✂️ Tokenizacja&lt;br/&gt;&lt;i&gt;tniemy tekst&lt;/i&gt;&quot;] --&gt; B[&quot;🔢 BoW / TF-IDF&lt;br/&gt;&lt;i&gt;liczymy słowa&lt;/i&gt;&quot;]\n    B --&gt; N[&quot;🔗 N-gramy / Markow&lt;br/&gt;&lt;i&gt;dodajemy kontekst&lt;/i&gt;&quot;]\n    N --&gt; NB[&quot;📊 Bayes&lt;br/&gt;&lt;i&gt;klasyfikujemy&lt;/i&gt;&quot;]\n    NB --&gt; W[&quot;📐 Word2Vec&lt;br/&gt;&lt;i&gt;wektory znaczeń&lt;/i&gt;&quot;]\n    \n    style T fill:#ff9999,color:#000\n    style B fill:#ffcc99,color:#000\n    style N fill:#ffff99,color:#000\n    style NB fill:#ccff99,color:#000\n    style W fill:#99ccff,color:#000\n</code></pre>\n<p>Narracja tego wpisu: <strong>&quot;Najpierw tniesz tekst na kawałki, potem liczysz, potem rozumiesz.&quot;</strong> Proste? Zobaczymy ;-) Zaczynamy od cięcia.</p>\n<hr />\n<h2><a href=\"#tokenizacja---jak-tekst-jest-dzielony-na-kawałki\" aria-hidden=\"true\" class=\"anchor\" id=\"tokenizacja---jak-tekst-jest-dzielony-na-kawałki\"></a>Tokenizacja - jak tekst jest dzielony na kawałki</h2>\n<h3><a href=\"#problem-komputer-nie-widzi-słów\" aria-hidden=\"true\" class=\"anchor\" id=\"problem-komputer-nie-widzi-słów\"></a>Problem: komputer nie widzi słów</h3>\n<p>Wyobraźcie sobie, że jesteście komputerem. Ktoś wam pokazuje tekst:</p>\n<blockquote>\n<p>&quot;Ala ma kota.&quot;</p>\n</blockquote>\n<p>Co widzicie? Literki? Słowa? Nie. Widzicie ciąg bajtów: <code>65 6c 61 20 6d 61 20 6b 6f 74 61 2e</code>. Zero pojęcia, gdzie zaczyna się jedno słowo, a kończy drugie. Zero pojęcia, że &quot;kot&quot; to zwierzak, a nie trzy losowe znaki.</p>\n<p>Żeby komputer cokolwiek mógł zrobić z tekstem, musi go najpierw <strong>pociąć na kawałki</strong>. I ten proces nazywa się <strong>tokenizacją</strong>.</p>\n<p>Tokenizacja to <strong>krok zerowy</strong>. Bez niej nie ma TF-IDF, nie ma n-gramów, nie ma embeddingów, nie ma LLM. Wszystko zaczyna się od cięcia.</p>\n<h3><a href=\"#naiwne-podejście-tnij-po-spacjach\" aria-hidden=\"true\" class=\"anchor\" id=\"naiwne-podejście-tnij-po-spacjach\"></a>Naiwne podejście: tnij po spacjach</h3>\n<p>Najprostszy pomysł: dzielimy tekst po spacjach.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">&quot;Ala ma kota.&quot; → [&quot;Ala&quot;, &quot;ma&quot;, &quot;kota.&quot;]\n</code></pre>\n<p>Zauważyliście? &quot;kota.&quot; ma kropkę przyklejoną. To nie jest słowo &quot;kot&quot; w dopełniaczu - to jest słowo &quot;kota&quot; z interpunkcją. A co z takimi tekstami:</p>\n<ul>\n<li>&quot;nie-przy-stoj-ny&quot; → jedno słowo czy pięć?</li>\n<li>&quot;Cześć!&quot; → słowo + interpunkcja?</li>\n<li>&quot;New York&quot; → jedno słowo czy dwa?</li>\n<li>&quot;:) &quot; → słowo? znak? emocja?</li>\n</ul>\n<p>Krótko: <strong>cięcie po spacjach nie działa</strong>. Świat jest za skomplikowany na tak proste reguły.</p>\n<h3><a href=\"#token--słowo--morfem\" aria-hidden=\"true\" class=\"anchor\" id=\"token--słowo--morfem\"></a>Token ≠ słowo ≠ morfem</h3>\n<p>W <a href=\"cechy-jezykowe-a-llm.html\">pierwszym poście</a> poznaliśmy morfemy - najmniejsze jednostki znaczące. &quot;Nieszczęśliwy&quot; to trzy morfemy: &quot;nie-szczęśliw-y&quot;. A teraz uwaga: <strong>token to nie to samo co morfem.</strong></p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    subgraph &quot;Lingwista widzi:&quot;\n        A[&quot;nieszczęśliwy&quot;] --&gt; A1[&quot;nie + szczęśliw + y&lt;br/&gt;&lt;i&gt;morfemy&lt;/i&gt;&quot;]\n    end\n    subgraph &quot;GPT-4o widzi (tokeny BPE):&quot;\n        B[&quot;nieszczęśliwy&quot;] --&gt; B1[&quot;nie + -s + zcz + ę + śli + wy&quot;]\n    end\n    \n    style A1 fill:#99ff99,color:#000\n    style B1 fill:#ff9999,color:#000\n</code></pre>\n<p>Token to po prostu <strong>kawałek tekstu</strong> wyznaczony przez tokenizator. Może być całym słowem, może być częścią słowa, może być pojedynczym znakiem. Zależy od algorytmu.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Trzy poziomy cięcia tekstu:</strong></p>\n<ul>\n<li><strong>Słowo</strong> - jednostka, którą intuicyjnie &quot;czujesz&quot; (oddzielona spacjami)</li>\n<li><strong>Morfem</strong> - najmniejsza jednostka ZNACZĄCA w języku (nie-, szczęśliw-, -y)</li>\n<li><strong>Token</strong> - kawałek tekstu wyznaczony przez ALGORYTM komputerowy (nie, -s, zcz, ę, śli, wy)</li>\n</ul>\n</div>\n<h3><a href=\"#bpe---byte-pair-encoding\" aria-hidden=\"true\" class=\"anchor\" id=\"bpe---byte-pair-encoding\"></a>BPE - Byte Pair Encoding</h3>\n<p>I tu wchodzi <strong>BPE (Byte Pair Encoding)</strong> - najpopularniejszy algorytm tokenizacji, używany przez GPT-2, GPT-3, GPT-4, Llama, i wiele innych modeli.</p>\n<p>Ideę BPE można sprowadzić do jednego zdania: <strong>szukamy najczęstszej pary znaków i łączymy ją w jeden token. I powtarzamy.</strong></p>\n<p>Brzmi prosto? Bo jest proste ;-) Zobaczmy to na przykładzie.</p>\n<h4><a href=\"#bpe-krok-po-kroku\" aria-hidden=\"true\" class=\"anchor\" id=\"bpe-krok-po-kroku\"></a>BPE krok po kroku</h4>\n<p>Mamy mały korpus z pięcioma polskimi słowami i ich częstotliwościami:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">&quot;las&quot;  ×10     &quot;lak&quot;  ×5      &quot;lat&quot;  ×12\n&quot;bat&quot;  ×4      &quot;lasy&quot; ×5\n</code></pre>\n<p>(&quot;las&quot; = las, &quot;lak&quot; = lak do pieczęci, &quot;lat&quot; = lat (od &quot;rok&quot;), &quot;bat&quot; = bicz, &quot;lasy&quot; = po prostu lasy)</p>\n<p><strong>Krok 0:</strong> Dzielimy wszystko na pojedyncze znaki:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">l a s    ×10     l a k    ×5      l a t    ×12\nb a t    ×4      l a s y  ×5\n</code></pre>\n<p>Nasz początkowy słownik to: <code>[&quot;a&quot;, &quot;b&quot;, &quot;k&quot;, &quot;l&quot;, &quot;s&quot;, &quot;t&quot;, &quot;y&quot;]</code> - po prostu wszystkie unikalne znaki.</p>\n<p><strong>Krok 1:</strong> Znajdujemy najczęstszą parę sąsiadujących znaków:</p>\n<ul>\n<li>(l, a) występuje w &quot;las&quot; (10), &quot;lak&quot; (5), &quot;lat&quot; (12) i &quot;lasy&quot; (5) = <strong>32 razy</strong> ← zwycięzca!</li>\n<li>(a, t) występuje w &quot;lat&quot; (12) i &quot;bat&quot; (4) = 16 razy</li>\n<li>(a, s) występuje w &quot;las&quot; (10) i &quot;lasy&quot; (5) = 15 razy</li>\n</ul>\n<p>Łączymy (l, a) → <strong>&quot;la&quot;</strong>. Nasz słownik rośnie o jeden token:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Słownik: [&quot;a&quot;, &quot;b&quot;, &quot;k&quot;, &quot;l&quot;, &quot;s&quot;, &quot;t&quot;, &quot;y&quot;, &quot;la&quot;]\n\nla s     ×10     la k     ×5      la t     ×12\nb a t    ×4      la s y   ×5\n</code></pre>\n<p><strong>Krok 2:</strong> Znowu szukamy najczęstszej pary:</p>\n<ul>\n<li>(la, s) występuje w &quot;las&quot; (10) i &quot;lasy&quot; (5) = <strong>15 razy</strong> ← zwycięzca!</li>\n<li>(la, t) występuje w &quot;lat&quot; (12) = 12 razy</li>\n<li>(la, k) występuje w &quot;lak&quot; (5) = 5 razy</li>\n</ul>\n<p>Łączymy (la, s) → <strong>&quot;las&quot;</strong>:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Słownik: [&quot;a&quot;, &quot;b&quot;, &quot;k&quot;, &quot;l&quot;, &quot;s&quot;, &quot;t&quot;, &quot;y&quot;, &quot;la&quot;, &quot;las&quot;]\n\nlas      ×10     la k     ×5      la t     ×12\nb a t    ×4      las y    ×5\n</code></pre>\n<p><strong>Krok 3:</strong> Najczęstsza para to teraz (la, t) = 12 razy. Łączymy → <strong>&quot;lat&quot;</strong>:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Słownik: [..., &quot;la&quot;, &quot;las&quot;, &quot;lat&quot;]\n\nlas      ×10     la k     ×5      lat      ×12\nb a t    ×4      las y    ×5\n</code></pre>\n<p><strong>Krok 4:</strong> Najczęstsza para to (la, k) = 5 razy. Łączymy → <strong>&quot;lak&quot;</strong>:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Słownik: [..., &quot;la&quot;, &quot;las&quot;, &quot;lat&quot;, &quot;lak&quot;]\n\nlas      ×10     lak      ×5      lat      ×12\nb a t    ×4      las y    ×5\n</code></pre>\n<p><strong>Krok 5:</strong> Najczęstsza para to (las, y) = 5 razy. Łączymy → <strong>&quot;lasy&quot;</strong>:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Słownik: [..., &quot;la&quot;, &quot;las&quot;, &quot;lat&quot;, &quot;lak&quot;, &quot;lasy&quot;]\n\nlas      ×10     lak      ×5      lat      ×12\nb a t    ×4      lasy     ×5\n</code></pre>\n<p>I tak dalej, aż osiągniemy docelowy rozmiar słownika. W prawdziwych modelach:</p>\n<ul>\n<li><strong>GPT-2</strong> ma słownik <strong>50 257</strong> tokenów (256 bajtów + 50 000 scalenia + 1 specjalny)</li>\n<li><strong>GPT-4/o</strong> ma słownik ~<strong>100 000</strong> tokenów</li>\n<li><strong>Llama 3</strong> też używa około ~<strong>100 000</strong> tokenów</li>\n</ul>\n<h3><a href=\"#kiedy-tokenizer-jest-używany\" aria-hidden=\"true\" class=\"anchor\" id=\"kiedy-tokenizer-jest-używany\"></a>Kiedy tokenizer jest używany?</h3>\n<p>To ważne pytanie, bo odpowiedź brzmi: <strong>zawsze. Na obu etapach.</strong></p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    subgraph &quot;TRENING&quot;\n        TC[&quot;📚 Korpus treningowy&quot;] --&gt; TT[&quot;✂️ Tokenizer&quot;] --&gt; TI[&quot;🔢 Token IDs&quot;]\n        TI --&gt; TM[&quot;🧠 Trenujesz model&quot;]\n    end\n    subgraph &quot;PROMPTOWANIE (inferencja)&quot;\n        PC[&quot;💬 Twój prompt&quot;] --&gt; PT[&quot;✂️ Tokenizer&quot;] --&gt; PI[&quot;🔢 Token IDs&quot;]\n        PI --&gt; PM[&quot;🤖 Model generuje&quot;] --&gt; PO[&quot;🔢 Nowe token IDs&quot;]\n        PO --&gt; PD[&quot;📤 Decoder&quot;] --&gt; PF[&quot;💬 Odpowiedź&quot;]\n    end\n    \n    TT -.-&gt;|&quot;ten sam!&quot;| PT\n    style TT fill:#ff9999,color:#000\n    style PT fill:#ff9999,color:#000\n</code></pre>\n<ol>\n<li>\n<p><strong>Trening:</strong> najpierw trenujesz tokenizer na ogromnym korpusie (uczy się, które pary scalać). Potem tokenizujesz CAŁY korpus treningowy na ID-ki. Model uczy się na tych ID-kach.</p>\n</li>\n<li>\n<p><strong>Promptowanie:</strong> kiedy piszesz do ChatGPT, twój tekst przechodzi przez ten <strong>SAM tokenizer</strong> → ID-ki → model generuje nowe ID-ki → decoder zamienia z powrotem na tekst.</p>\n</li>\n</ol>\n<p>Kluczowe: <strong>tokenizer jest trenowany osobno, przed modelem.</strong> Potem jest &quot;zamrożony&quot; - nie zmienia się już nigdy. GPT-2 ma swój tokenizer (słownik 50K), GPT-4 ma swój (słownik 100K). I to jest powód, dla którego polski jest cięty gorzej w GPT-2 - bo <strong>tokenizer</strong> GPT-2 był trenowany głównie na angielskim, więc mało polskich ciągów znaków zostało scalonych. Model sam mógłby &quot;rozumieć&quot; polski lepiej, ale tokenizer już mu pociął tekst na drobne kawałki.</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Eksperyment dla was:</strong> Wejdźcie na <a href=\"https://tiktokenizer.vercel.app/\">tiktokenizer.vercel.app</a>, wpiszcie jakiś tekst po polsku i zobaczcie, jak model GPT-2 tnie go na tokeny. Zobaczycie, że polskie znaki (ą, ę, ł, ś, ć) często zajmują po 2-3 tokeny! Bo GPT-2 był trenowany głównie na angielskim, więc polski jest dla niego &quot;egzotyczny&quot;.</p>\n</div>\n<h3><a href=\"#różne-modele---różne-cięcie\" aria-hidden=\"true\" class=\"anchor\" id=\"różne-modele---różne-cięcie\"></a>Różne modele - różne cięcie</h3>\n<p>Kluczowa rzecz: <strong>każdy model ma swój własny tokenizator</strong>. Ten sam tekst może być pocięty zupełnie inaczej:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>import</a-k> <a-v>tiktoken</a-v>\n\n<a-v>text</a-v> <a-o>=</a-o> <a-s>&quot;Nieprawdopodobnie szczęśliwy&quot;</a-s>\n\n<a-v>gpt2</a-v> <a-o>=</a-o> <a-v>tiktoken</a-v>.<a-pr>get_encoding</a-pr>(<a-s>&quot;gpt2&quot;</a-s>)\n<a-v>gpt4</a-v> <a-o>=</a-o> <a-v>tiktoken</a-v>.<a-pr>get_encoding</a-pr>(<a-s>&quot;cl100k_base&quot;</a-s>)\n\n<a-f>print</a-f>(<a-s>&quot;GPT-2:&quot;</a-s>, <a-v>gpt2</a-v>.<a-pr>encode</a-pr>(<a-v>text</a-v>))\n<a-f>print</a-f>(<a-s>&quot;GPT-4:&quot;</a-s>, <a-v>gpt4</a-v>.<a-pr>encode</a-pr>(<a-v>text</a-v>))\n\n<a-f>print</a-f>(<a-s>&quot;GPT-2:&quot;</a-s>, [<a-v>gpt2</a-v>.<a-pr>decode</a-pr>([<a-v>t</a-v>]) <a-k>for</a-k> <a-v>t</a-v> <a-o>in</a-o> <a-v>gpt2</a-v>.<a-pr>encode</a-pr>(<a-v>text</a-v>)])\n<a-f>print</a-f>(<a-s>&quot;GPT-4:&quot;</a-s>, [<a-v>gpt4</a-v>.<a-pr>decode</a-pr>([<a-v>t</a-v>]) <a-k>for</a-k> <a-v>t</a-v> <a-o>in</a-o> <a-v>gpt4</a-v>.<a-pr>encode</a-pr>(<a-v>text</a-v>)])</code></pre>\n<p>GPT-2 prawdopodobnie potnie polski tekst na mnóstwo małych fragmentów (bo nie &quot;zna&quot; polskiego dobrze), a GPT-4 zrobi to o wiele efektywniej (bo widział więcej polskiego tekstu).</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    subgraph &quot;GPT-2 (angielski BPE):&quot;\n        G2[&quot;Nieprawdopodobnie szczęśliwy&quot;] --&gt; G2T[&quot;N + ie + p + raw + d + op + od + ob + nie + s + z + cz + ⍰ + ⍰ + ⍰ + ⍰ + li + wy&lt;br/&gt;&lt;i&gt;18 tokenów!&lt;/i&gt;&quot;]\n    end\n    subgraph &quot;GPT-4o (multilingual BPE):&quot;\n        G4[&quot;Nieprawdopodobnie szczęśliwy&quot;] --&gt; G4T[&quot;Nie + p + rawd + op + odob + nie + szcz + ę + śli + wy&lt;br/&gt;&lt;i&gt;10 tokenów&lt;/i&gt;&quot;]\n    end\n    subgraph &quot;Polish RoBERTa (polski SentencePiece):&quot;\n        PR[&quot;Nieprawdopodobnie szczęśliwy&quot;] --&gt; PRT[&quot;Nie + prawdopodobnie + szczęśliwy&lt;br/&gt;&lt;i&gt;3 tokeny!&lt;/i&gt;&quot;]\n    end\n    \n    style G2T fill:#ff9999,color:#000\n    style G4T fill:#ffcc99,color:#000\n    style PRT fill:#99ff99,color:#000\n</code></pre>\n<h3><a href=\"#a-co-z-polskimi-tokenizatorami\" aria-hidden=\"true\" class=\"anchor\" id=\"a-co-z-polskimi-tokenizatorami\"></a>A co z polskimi tokenizatorami?</h3>\n<p>Istnieją modele trenowane specjalnie na polskim tekście - i ich tokenizatory tną polski <strong>znacznie</strong> lepiej:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>transformers</a-v> <a-k>import</a-k> <a-cr>AutoTokenizer</a-cr>\n\n<a-v>text</a-v> <a-o>=</a-o> <a-s>&quot;Nieprawdopodobnie szczęśliwy&quot;</a-s>\n\n<a-v>herbert</a-v> <a-o>=</a-o> <a-cr>AutoTokenizer</a-cr>.<a-pr>from_pretrained</a-pr>(<a-s>&quot;allegro/herbert-base-cased&quot;</a-s>)\n<a-f>print</a-f>(<a-s>&quot;HerBERT (Allegro, polski WordPiece):&quot;</a-s>, <a-v>herbert</a-v>.<a-pr>tokenize</a-pr>(<a-v>text</a-v>))\n\n<a-v>roberta</a-v> <a-o>=</a-o> <a-cr>AutoTokenizer</a-cr>.<a-pr>from_pretrained</a-pr>(<a-s>&quot;sdadas/polish-roberta-base-v2&quot;</a-s>)\n<a-f>print</a-f>(<a-s>&quot;Polish RoBERTa (polski SentencePiece):&quot;</a-s>, <a-v>roberta</a-v>.<a-pr>tokenize</a-pr>(<a-v>text</a-v>))</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">HerBERT (Allegro, polski WordPiece):  ['Nie', 'prawdopodobnie&lt;/w&gt;', 'szczęśliwy&lt;/w&gt;']  → 3 tokeny\nPolish RoBERTa (polski SentencePiece): ['▁Nie', 'prawdopodobnie', '▁szczęśliwy']        → 3 tokeny\n</code></pre>\n<p>Trzy tokeny! &quot;prawdopodobnie&quot; i &quot;szczęśliwy&quot; to dla polskiego tokenizera pojedyncze tokeny. Bo ten tokenizer &quot;widział&quot; te słowa na polskim korpusie tyle razy, że scalił je w całe jednostki.</p>\n<p>Dwa polskie tokenizery, które warto znać:</p>\n<table>\n<thead>\n<tr>\n<th>Tokenizer</th>\n<th>Twórca</th>\n<th>Algorytm</th>\n<th>Zaimplementowany w</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>HerBERT</strong></td>\n<td>Allegro</td>\n<td>WordPiece</td>\n<td>polski BERT na KGR10 korpusie</td>\n</tr>\n<tr>\n<td><strong>Polish RoBERTa</strong></td>\n<td>sdadas</td>\n<td>SentencePiece (Unigram)</td>\n<td>polski RoBERTa na dużym korpusie</td>\n</tr>\n</tbody>\n</table>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Dlaczego polskie modele tną lepiej?</strong> Bo ich tokenizery były trenowane <strong>na polskim tekście</strong>. &quot;prawdopodobnie&quot; występowało w polskim korpusie tysiące razy, więc BPE/SentencePiece scalił je w jeden token. GPT-2 widział głównie angielski, więc &quot;prawdopodobnie&quot; mu się nie scaliło - i tnie je na kawałki. To jest też powód, dla którego GPT-4o (trenowany na dużo bardziej wielojęzycznym korpusie) tnie polski lepiej niż GPT-2 - ale nadal gorzej niż typowo polskie modele.</p>\n</div>\n<h3><a href=\"#stwórz-własny-polski-tokenizer\" aria-hidden=\"true\" class=\"anchor\" id=\"stwórz-własny-polski-tokenizer\"></a>Stwórz własny polski tokenizer!</h3>\n<p>Skoro już rozumiemy, jak BPE działa, spróbujmy wytrenować <strong>własny tokenizer</strong> na polskim tekście. Użyjemy biblioteki <code>tokenizers</code> od HuggingFace (nie mylić z <code>tiktoken</code>):</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>tokenizers</a-v> <a-k>import</a-k> <a-cr>Tokenizer</a-cr>\n<a-k>from</a-k> <a-v>tokenizers</a-v>.<a-v>models</a-v> <a-k>import</a-k> <a-co>BPE</a-co>\n<a-k>from</a-k> <a-v>tokenizers</a-v>.<a-v>trainers</a-v> <a-k>import</a-k> <a-cr>BpeTrainer</a-cr>\n<a-k>from</a-k> <a-v>tokenizers</a-v>.<a-v>pre_tokenizers</a-v> <a-k>import</a-k> <a-cr>Whitespace</a-cr>\n\n<a-v>tokenizer</a-v> <a-o>=</a-o> <a-f>Tokenizer</a-f>(<a-f>BPE</a-f>(<a-v>unk_token</a-v><a-o>=</a-o><a-s>&quot;[UNK]&quot;</a-s>))\n<a-v>tokenizer</a-v>.<a-pr>pre_tokenizer</a-pr> <a-o>=</a-o> <a-f>Whitespace</a-f>()\n\n<a-v>trainer</a-v> <a-o>=</a-o> <a-f>BpeTrainer</a-f>(<a-v>vocab_size</a-v><a-o>=</a-o><a-n>300</a-n>, <a-v>special_tokens</a-v><a-o>=</a-o>[<a-s>&quot;[UNK]&quot;</a-s>])\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Ala ma kota i psa&quot;</a-s>,\n    <a-s>&quot;Kot siedzi na macie&quot;</a-s>,\n    <a-s>&quot;Pies biega po parku&quot;</a-s>,\n    <a-s>&quot;Ala ma kota i kota i jeszcze raz kota&quot;</a-s>,\n    <a-s>&quot;Kot lubi mleko i kot lubi spać&quot;</a-s>,\n    <a-s>&quot;Pies lubi biegać po parku i gonić kota&quot;</a-s>,\n    <a-s>&quot;Szczęśliwy kot to kot który ma dużo mleka&quot;</a-s>,\n    <a-s>&quot;Nieprawdopodobnie szczęśliwy pies biega po parku&quot;</a-s>,\n    <a-s>&quot;Szczęśliwy Ala ma kota i psa i szczęśliwy dom&quot;</a-s>,\n    <a-s>&quot;Dom to miejsce gdzie mieszka szczęśliwa rodzina&quot;</a-s>,\n]\n\n<a-v>tokenizer</a-v>.<a-pr>train_from_iterator</a-pr>(<a-v>corpus</a-v>, <a-v>trainer</a-v>)\n<a-f>print</a-f>(<a-s>f&quot;Rozmiar słownika: </a-s><a-p>{</a-p><a-v>tokenizer</a-v><a-eb>.</a-eb><a-pr>get_vocab_size</a-pr><a-eb>()</a-eb><a-p>}</a-p><a-s>&quot;</a-s>)\n\n<a-v>test</a-v> <a-o>=</a-o> <a-s>&quot;Nieprawdopodobnie szczęśliwy&quot;</a-s>\n<a-v>encoding</a-v> <a-o>=</a-o> <a-v>tokenizer</a-v>.<a-pr>encode</a-pr>(<a-v>test</a-v>)\n<a-f>print</a-f>(<a-s>f&#39;&quot;</a-s><a-p>{</a-p><a-v>test</a-v><a-p>}</a-p><a-s>&quot; → </a-s><a-p>{</a-p><a-v>encoding</a-v><a-eb>.</a-eb><a-pr>tokens</a-pr><a-p>}</a-p><a-s>&#39;</a-s>)</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Rozmiar słownika: 130\n&quot;Nieprawdopodobnie szczęśliwy&quot; → ['Nieprawdopodobnie', 'szczęśliwy']\n</code></pre>\n<p><strong>Dwa tokeny!</strong> Bo nasz mały korpus jest po polsku - i &quot;Nieprawdopodobnie&quot; oraz &quot;szczęśliwy&quot; wystąpiły wystarczająco często, żeby BPE scalił je w pojedyncze tokeny.</p>\n<p>Ale chwila - co właściwie znaczą te parametry w kodzie?</p>\n<table>\n<thead>\n<tr>\n<th>Parametr</th>\n<th>Co robi</th>\n<th>Przykład</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong><code>vocab_size</code></strong></td>\n<td>Maksymalna liczba tokenów w słowniku. BPE będzie scalał pary, aż słownik osiągnie ten rozmiar. Im większy, tym dłuższe tokeny (całe słowa). Im mniejszy, tym mniejsze kawałki (pojedyncze litery).</td>\n<td>GPT-2: 50 257, nasz: 300</td>\n</tr>\n<tr>\n<td><strong><code>special_tokens</code></strong></td>\n<td>Tokeny o specjalnym znaczeniu dla modelu. Zawsze są w słowniku, niezależnie od treningu.</td>\n<td><code>[UNK]</code>, <code>&lt;PAD&gt;</code>, <code>&lt;S&gt;</code> (start), <code>&lt;/S&gt;</code> (koniec)</td>\n</tr>\n<tr>\n<td><strong><code>unk_token</code></strong></td>\n<td>Token &quot;nieznany&quot; - zastępuje każdy znak, którego tokenizer nie potrafi rozpoznać. Np. jeśli w tekście pojawi się emoji, a tokenizer nie ma go w słowniku, wstawi <code>[UNK]</code>.</td>\n<td><code>[UNK]</code> = &quot;nie wiem co to jest&quot;</td>\n</tr>\n</tbody>\n</table>\n<p>Prosta analogia: <code>vocab_size</code> to grubość waszego słownika - ile haseł się w nim zmieści. <code>unk_token</code> to hasło oznaczające &quot;nie ma takiego słowa&quot;. A <code>special_tokens</code> to &quot;strony zarezerwowane&quot; - zawsze są w słowniku, niezależnie od tego, czego nauczysz tokenizer.</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Eksperyment:</strong> Skopiujcie ten kod, zmieńcie <code>vocab_size</code> na np. 50 i zobaczcie, co się stanie. Zobaczycie, że przy mniejszym słowniku tokenizer tnie słowa na mniejsze kawałki - bo ma mniej &quot;miejsca&quot; na scalenia. To jest dokładnie to, o czym mówiliśmy: rozmiar słownika to hiperparametr, i to od niego zależy, jak drobno tekst jest cięty.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\">Warning</p>\n<p><strong>Dlaczego to ma znaczenie?</strong> Bo LLM ma limit tokenów w oknie kontekstowym. GPT-3 ma 4K tokenów, GPT-4 Turbo ma 128K. Ale &quot;token&quot; to nie &quot;słowo&quot;! W angielskim ~1 token ≈ 0.75 słowa. W polskim, zwłaszcza z GPT-2, to może być ~1 token ≈ 0.4 słowa. Czyli polski tekst &quot;zjada&quot; więcej tokenów i szybciej wyczerpuje limit.</p>\n</div>\n<h3><a href=\"#inne-algorytmy-tokenizacji\" aria-hidden=\"true\" class=\"anchor\" id=\"inne-algorytmy-tokenizacji\"></a>Inne algorytmy tokenizacji</h3>\n<p>BPE to nie jedyny gracz w mieście. Oto krótkie zestawienie:</p>\n<table>\n<thead>\n<tr>\n<th>Algorytm</th>\n<th>Kto używa</th>\n<th>Jak działa</th>\n<th>Kluczowa różnica</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>BPE</strong></td>\n<td>GPT, Llama, many others</td>\n<td>Scala najczęstszą parę</td>\n<td>Prosty, od dołu</td>\n</tr>\n<tr>\n<td><strong>WordPiece</strong></td>\n<td>BERT, DistilBERT, Electra</td>\n<td>Scala parę z najwyższym &quot;score&quot;</td>\n<td>Score = freq(pary) / (freq(a) × freq(b))</td>\n</tr>\n<tr>\n<td><strong>Unigram</strong></td>\n<td>T5, Pegasus, ALBERT</td>\n<td>Zaczyna od dużego słownika, usuwa najmniej przydatne</td>\n<td>Probabilistyczny, może dawać różne tokenizacje</td>\n</tr>\n<tr>\n<td><strong>SentencePiece</strong></td>\n<td>Wielojęzyczne modele</td>\n<td>BPE lub Unigram na surowym tekście (nawet bez spacji!)</td>\n<td>Działa z językami bez spacji (chiński, japoński)</td>\n</tr>\n</tbody>\n</table>\n<p>Różnica między BPE a WordPiece jest subtelna: BPE scala po prostu <strong>najczęstszą</strong> parę. WordPiece scala parę, która jest <strong>najbardziej zaskakująca</strong> - czyli występuje razem częściej niż by wynikało z częstotliwości poszczególnych elementów. To trochę jak związek, który jest &quot;bardziej niż suma części&quot; ;-)</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Nawiązanie do posta 1:</strong> Pamiętacie, jak mówiliśmy, że polska morfologia to koszmar dla LLM? 7 przypadków, fleksja, mnóstwo końcówek... No to teraz widzicie dlaczego. Tokenizator nie &quot;wie&quot;, że &quot;domu&quot;, &quot;domowi&quot;, &quot;domem&quot; to odmiany tego samego słowa. Dla niego to po prostu różne sekwencje znaków. To &quot;zrozumienie&quot; relacji między formami - tego model musi się nauczyć sam, w trakcie treningu.</p>\n</div>\n<h3><a href=\"#quiz-jak-bpe-to-potnie\" aria-hidden=\"true\" class=\"anchor\" id=\"quiz-jak-bpe-to-potnie\"></a>Quiz: jak BPE to potnie?<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup></h3>\n<p>Mamy nasze scalenia z powyższego przykładu: l+a→la, la+s→las, la+t→lat, la+k→lak, las+y→lasy. Jak BPE podzieli te nowe słowa?</p>\n<ol>\n<li>&quot;klasa&quot; (zakładając, że nie było w korpusie)</li>\n<li>&quot;lata&quot; (zakładając, że nie było w korpusie)</li>\n<li>&quot;laska&quot; (popularne polskie słowo!)</li>\n</ol>\n<hr />\n<h2><a href=\"#korpusy-bag-of-words-i-tf-idf\" aria-hidden=\"true\" class=\"anchor\" id=\"korpusy-bag-of-words-i-tf-idf\"></a>Korpusy, Bag-of-Words i TF-IDF</h2>\n<h3><a href=\"#co-to-jest-korpus\" aria-hidden=\"true\" class=\"anchor\" id=\"co-to-jest-korpus\"></a>Co to jest korpus?</h3>\n<p>OK, mamy tokeny. Ale skąd tokenizator wie, które pary znaków scalać? Z <strong>korpusu</strong>! BPE uczy się najczęstszych połączeń z tekstu. I żeby zrobić cokolwiek z tokenami - policzyć je, znaleźć wzorce, wytrenować model - też potrzebujemy korpusu.</p>\n<p><strong>Korpus</strong> to po prostu zbiór tekstów. Może to być:</p>\n<ul>\n<li>zbiór wszystkich artykułów z Wikipedii</li>\n<li>zbiór recenzji produktów z Allegro</li>\n<li>zbiór maili w Twojej skrzynce</li>\n<li>zbiór wszystkich wpisów na Wykopie (odpukać)</li>\n</ul>\n<p>Każdy pojedynczy tekst w korpusie nazywamy <strong>dokumentem</strong>. Proste?</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    C[&quot;📚 KORPUS&lt;br/&gt;&lt;i&gt;zbiór dokumentów&lt;/i&gt;&quot;] --&gt; D1[&quot;📄 Dokument 1&lt;br/&gt;Ten film jest świetny&quot;]\n    C --&gt; D2[&quot;📄 Dokument 2&lt;br/&gt;Ten film jest beznadziejny&quot;]\n    C --&gt; D3[&quot;📄 Dokument 3&lt;br/&gt;Świetna książka, polecam&quot;]\n    \n    style C fill:#9999ff,color:#fff\n    style D1 fill:#ffcc99,color:#000\n    style D2 fill:#ffcc99,color:#000\n    style D3 fill:#ffcc99,color:#000\n</code></pre>\n<p>I to są właśnie zbiory danych, na których trenuje się prawdziwe modele - i tokenizery, i same modele:</p>\n<table>\n<thead>\n<tr>\n<th>Korpus</th>\n<th>Co zawiera</th>\n<th>Rozmiar</th>\n<th>Ciekawostka</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Wikitext</strong></td>\n<td>Artykuły z Wikipedii (angielskiej)</td>\n<td>~500 MB</td>\n<td>Standardowy benchmark do ewaluacji modeli językowych</td>\n</tr>\n<tr>\n<td><strong>Wiki-40B</strong></td>\n<td>Wikipedia w 59 językach (w tym polskim!)</td>\n<td>~40 GB</td>\n<td>Pierwszy krok wielu modeli wielojęzycznych</td>\n</tr>\n<tr>\n<td><strong>EuroParl</strong></td>\n<td>Transkrypcje z parlamentu UE (21 języków)</td>\n<td>~2 GB</td>\n<td>Wysokiej jakości teksty oficjalne, świetny do tłumaczeń</td>\n</tr>\n<tr>\n<td><strong>Common Crawl</strong></td>\n<td>Zrzuty treści ze stron internetowych</td>\n<td><strong>petabajty</strong> (PB!)</td>\n<td>Największy publicznie dostępny korpus webowy. Większość LLM-ów z niego korzysta</td>\n</tr>\n<tr>\n<td><strong>OpenWebText</strong></td>\n<td>Kopia linków z Reddita z &gt;3 punktami oceny</td>\n<td>~38 GB</td>\n<td>Z niego trenowano GPT-2. Filtrowana &quot;jakość&quot; z Reddita</td>\n</tr>\n<tr>\n<td><strong>The Pile</strong></td>\n<td>Mix 22 źródeł (arXiv, GitHub, Wikipedia, książki...)</td>\n<td>~825 GB</td>\n<td>Stworzony przez EleutherAI jako zbiór &quot;do wszystkiego&quot;</td>\n</tr>\n<tr>\n<td><strong>RedPajama</strong></td>\n<td>Otwarta replika danych treningowych LLaMA</td>\n<td>~1.2 TB</td>\n<td>To dlatego LLaMA jest tak dobra - trenowana na ogromnym, różnorodnym zbiorze</td>\n</tr>\n<tr>\n<td><strong>OSCAR</strong></td>\n<td>Zrzuty z internetu, filtrowane po językach</td>\n<td>~6.5 TB</td>\n<td>Ma osobne podzbiory dla każdego języka, w tym polski OSCAR</td>\n</tr>\n</tbody>\n</table>\n<p>A polskie korpusy? Oto najważniejsze:</p>\n<ul>\n<li><strong>NKJP</strong> (Narodowy Korpus Języka Polskiego) - miliony polskich tekstów, zrównoważony zbiór z różnych dziedzin</li>\n<li><strong>Polish-ROBERTa corpus</strong> - ~20 GB polskiego tekstu z internetu, na nim trenowano Polish RoBERTa</li>\n<li><strong>OSCAR (polska część)</strong> - polskie strony z Common Crawl, kilkaset GB</li>\n<li><strong>Wikipedia po polsku</strong> - ~2 GB artykułów, często punkt wyjścia do polskich modeli</li>\n</ul>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    subgraph &quot;Skąd biorą się dane treningowe LLM&quot;\n        CC[&quot;🌐 Common Crawl&lt;br/&gt;&lt;i&gt;petabajty z internetu&lt;/i&gt;&quot;] --&gt; MIX\n        WP[&quot;📚 Wikipedia&lt;br/&gt;&lt;i&gt;49 GB, 59 języków&lt;/i&gt;&quot;] --&gt; MIX\n        GH[&quot;💻 GitHub&lt;br/&gt;&lt;i&gt;kod źródłowy&lt;/i&gt;&quot;] --&gt; MIX\n        BK[&quot;📖 Książki&lt;br/&gt;&lt;i&gt;Project Gutenberg etc.&lt;/i&gt;&quot;] --&gt; MIX\n        AX[&quot;📄 ArXiv&lt;br/&gt;&lt;i&gt;publikacje naukowe&lt;/i&gt;&quot;] --&gt; MIX\n        \n        MIX[&quot;🧹 Filtrowanie&lt;br/&gt;&lt;i&gt;usuwanie spamu, duplikatów&lt;/i&gt;&quot;] --&gt; CORPUS[&quot;📊 Czysty korpus&lt;br/&gt;&lt;i&gt;setki GB - TB&lt;/i&gt;&quot;]\n        CORPUS --&gt; TOK[&quot;✂️ Trening tokenizera&quot;]\n        CORPUS --&gt; LLM[&quot;🧠 Trening modelu&quot;]\n    end\n    \n    style CC fill:#ff9999,color:#000\n    style MIX fill:#ffff99,color:#000\n    style CORPUS fill:#99ff99,color:#000\n    style TOK fill:#ffcc99,color:#000\n    style LLM fill:#9999ff,color:#fff\n</code></pre>\n<p>Kluczowa obserwacja: <strong>ten sam korpus służy do trenowania tokenizera i modelu.</strong> Najpierw trenujesz tokenizer na korpusie (uczy się, które pary znaków scalać), potem tokenizujesz cały korpus za pomocą tego tokenizera, i na tych tokenach trenujesz model.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Dlaczego to jest ważne?</strong> Jeśli twój korpus ma mało polskiego tekstu (np. GPT-2 trenowany głównie na angielskim), to tokenizer nie scali polskich słów, a model nie nauczy się polskiego dobrze. Dlatego polskie modele (HerBERT, Polish RoBERTa) używają korpusów z dużą ilością polskiego tekstu - np. NKJP + polski OSCAR + polską Wikipedię.</p>\n</div>\n<h3><a href=\"#bag-of-words---torba-pełna-słów\" aria-hidden=\"true\" class=\"anchor\" id=\"bag-of-words---torba-pełna-słów\"></a>Bag-of-Words - torba pełna słów</h3>\n<p>Mamy korpus. Teraz chcemy zamienić każdy dokument na <strong>liczby</strong>, żeby komputer mógł z tym coś zrobić.</p>\n<p>Najprostszy sposób: <strong>Bag-of-Words (BoW)</strong> - torba słów. Liczymy, ile razy każde słowo występuje w dokumencie.</p>\n<p>Przykład. Mamy trzy krótkie recenzje filmu:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Dokument 1: &quot;Ten film jest świetny&quot;\nDokument 2: &quot;Ten film jest beznadziejny&quot;\nDokument 3: &quot;Świetny film polecam&quot;\n</code></pre>\n<p>Budujemy słownik wszystkich unikalnych słów:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">{ten, film, jest, świetny, beznadziejny, polecam}\n</code></pre>\n<p>I teraz każdy dokument zamieniamy na wektor zliczeń:</p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>ten</th>\n<th>film</th>\n<th>jest</th>\n<th>świetny</th>\n<th>beznadziejny</th>\n<th>polecam</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Dok 1</strong></td>\n<td>1</td>\n<td>1</td>\n<td>1</td>\n<td>1</td>\n<td>0</td>\n<td>0</td>\n</tr>\n<tr>\n<td><strong>Dok 2</strong></td>\n<td>1</td>\n<td>1</td>\n<td>1</td>\n<td>0</td>\n<td>1</td>\n<td>0</td>\n</tr>\n<tr>\n<td><strong>Dok 3</strong></td>\n<td>0</td>\n<td>1</td>\n<td>0</td>\n<td>1</td>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>\n<p>Każdy dokument to teraz po prostu ciąg liczb. Komputer jest happy.</p>\n<p>Ale my nie do końca ;-) Bo zauważcie problem:</p>\n<blockquote>\n<p>Dokument 1: &quot;Ten film jest świetny&quot; → [1, 1, 1, 1, 0, 0]<br />\nDokument 2: &quot;Ten film jest beznadziejny&quot; → [1, 1, 1, 0, 1, 0]</p>\n</blockquote>\n<p>Te wektory są prawie identyczne! Różnią się na jednej pozycji. A przecież jeden mówi &quot;świetny&quot;, a drugi &quot;beznadziejny&quot; - zupełnie inne znaczenie.</p>\n<p>I jest drugi problem: &quot;Dog bites man&quot; i &quot;Man bites dog&quot; dadzą <strong>dokładnie ten sam</strong> wektor BoW. BoW kompletnie <strong>ignoruje kolejność słów</strong>.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Dlaczego po polsku to nie działa tak samo?</strong> Bo polski ma przypadki! &quot;Pies gryzie człowieka&quot; i &quot;Człowiek gryzie psa&quot; to dla BoW <strong>różne</strong> zdania - bo &quot;pies&quot; ≠ &quot;psa&quot; i &quot;człowieka&quot; ≠ &quot;człowiek&quot;. To są inne słowa. Angielski nie ma przypadków, więc &quot;dog&quot; to zawsze &quot;dog&quot; niezależnie od roli w zdaniu - i dlatego BoW nie odróżnia &quot;dog bites man&quot; od &quot;man bites dog&quot;. To jest właśnie ta polska morfologia z <a href=\"cechy-jezykowe-a-llm.html\">pierwszego posta</a>, która czasem nam pomaga, a czasem przeszkadza ;-)</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\">Warning</p>\n<p><strong>Bag-of-Words w pigułce:</strong></p>\n<ul>\n<li>Plus: banalnie prosty, działa szybko</li>\n<li>Minus: ignoruje kolejność, ignoruje znaczenie, ignoruje kontekst</li>\n<li>Metafora: wrzucasz wszystkie słowa do torby, potrząsasz, i patrzysz co wypadnie. Torba nie wie, co było pierwsze, a co ostatnie.</li>\n</ul>\n</div>\n<h3><a href=\"#bag-of-words-w-kodzie\" aria-hidden=\"true\" class=\"anchor\" id=\"bag-of-words-w-kodzie\"></a>Bag-of-Words w kodzie</h3>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>feature_extraction</a-v>.<a-v>text</a-v> <a-k>import</a-k> <a-cr>CountVectorizer</a-cr>\n<a-k>import</a-k> <a-v>pandas</a-v> <a-k>as</a-k> <a-v>pd</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Ten film jest świetny&quot;</a-s>,\n    <a-s>&quot;Ten film jest beznadziejny&quot;</a-s>,\n    <a-s>&quot;Świetny film polecam&quot;</a-s>,\n]\n\n<a-v>vectorizer</a-v> <a-o>=</a-o> <a-f>CountVectorizer</a-f>()\n<a-co>X</a-co> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>fit_transform</a-pr>(<a-v>corpus</a-v>)\n\n<a-v>df</a-v> <a-o>=</a-o> <a-v>pd</a-v>.<a-pr>DataFrame</a-pr>(\n    <a-co>X</a-co>.<a-pr>toarray</a-pr>(),\n    <a-v>columns</a-v><a-o>=</a-o><a-v>vectorizer</a-v>.<a-pr>get_feature_names_out</a-pr>(),\n    <a-v>index</a-v><a-o>=</a-o>[<a-s>&quot;Dok 1&quot;</a-s>, <a-s>&quot;Dok 2&quot;</a-s>, <a-s>&quot;Dok 3&quot;</a-s>]\n)\n<a-f>print</a-f>(<a-v>df</a-v>)</code></pre>\n<p>Wynik:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">        beznadziejny  film  jest  polecam  świetny  ten\nDok 1              0     1     1        0        1    1\nDok 2              1     1     1        0        0    1\nDok 3              0     1     0        1        1    0\n</code></pre>\n<p>Dokładnie ta sama tabela, co wyżej - tylko teraz wygenerowana przez kod. Zauważcie: &quot;film&quot; jest wszędzie (wartość 1 w każdym wierszu), a &quot;beznadziejny&quot; tylko w Dokumencie 2. To jest cała magia BoW - proste zliczanie.</p>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p><strong>Czy BoW potrafi generować nowe teksty?</strong> Nie. BoW to tylko sposób <strong>reprezentacji</strong> tekstu (tekst → liczby). Służy do analizy, porównywania i klasyfikacji dokumentów, ale nie generuje nic nowego. Nie przewiduje też, jakie słowo powinno pojść po &quot;Kot siedzi na...&quot;. Żeby generować tekst, potrzebujemy czegoś, co rozumie <strong>kolejność</strong> słów i potrafi przewidywać następne. I to jest dokładnie to, do czego dochodzimy za chwilę przy n-gramach i łańcuchach Markowa ;-)</p>\n</div>\n<h3><a href=\"#tf-idf---a-co-jeśli-nie-wszystkie-słowa-są-równe\" aria-hidden=\"true\" class=\"anchor\" id=\"tf-idf---a-co-jeśli-nie-wszystkie-słowa-są-równe\"></a>TF-IDF - a co jeśli nie wszystkie słowa są równe?</h3>\n<p>BoW traktuje każde słowo tak samo. Ale przecież <strong>nie każde słowo jest tak samo ważne</strong>!</p>\n<p>Słowo &quot;jest&quot; występuje w niemal każdym polskim zdaniu. Słowo &quot;semiotyka&quot; - raczej rzadko. Które jest bardziej informatywne? Oczywiście &quot;semiotyka&quot; - bo jeśli widzisz to słowo w dokumencie, to dużo więcej mówi ci o jego treści niż &quot;jest&quot;.</p>\n<p>TF-IDF (Term Frequency - Inverse Document Frequency) to sposób, żeby to uchwycić.</p>\n<p>Przypomnijmy nasz korpus - trzy krótkie recenzje filmu:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Dokument 1: &quot;Ten film jest świetny&quot;         (4 słowa)\nDokument 2: &quot;Ten film jest beznadziejny&quot;     (4 słowa)\nDokument 3: &quot;Świetny film polecam&quot;            (3 słowa)\n</code></pre>\n<p>Policzmy TF-IDF dla słowa <strong>&quot;świetny&quot;</strong> w Dokumencie 1:</p>\n<p><strong>TF (Term Frequency)</strong> - jak często słowo występuje w jednym dokumencie:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">TF(&quot;świetny&quot;, Dokument 1) = 1 / 4 = 0.25\n</code></pre>\n<p>(1 wystąpienie w 4-słowowym dokumencie)</p>\n<p><strong>IDF (Inverse Document Frequency)</strong> - jak &quot;rzadkie&quot; jest słowo w całym korpusie:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">IDF(&quot;świetny&quot;) = log(3 / 2) = 0.176\n</code></pre>\n<p>(3 dokumenty w korpusie, 2 z nich zawierają &quot;świetny&quot;)</p>\n<p><strong>TF-IDF</strong> = TF × IDF:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">TF-IDF(&quot;świetny&quot;, Dokument 1) = 0.25 × 0.176 = 0.044\n</code></pre>\n<p>A słowo &quot;jest&quot;, które jest WSZĘDZIE?</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">IDF(&quot;jest&quot;) = log(3 / 2) = 0.176  (też w 2 z 3 dokumentów)\nIDF(&quot;film&quot;) = log(3 / 3) = 0      (we WSZYSTKICH dokumentach!)\n</code></pre>\n<p>&quot;Film&quot; dostaje <strong>zero</strong> w IDF! Bo jeśli słowo jest w każdym dokumencie, to nie wnosi żadnej informacji, która pomogłaby odróżnić jeden dokument od drugiego.</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Intuicja za TF-IDF w jednym zdaniu:</strong> Słowo jest ważne dla danego dokumentu, jeśli często tam występuje (wysokie TF), ale rzadko w innych dokumentach (wysokie IDF). Czyli: &quot;Czy jesteś wyjątkowy?&quot;</p>\n</div>\n<h3><a href=\"#tf-idf-w-kodzie\" aria-hidden=\"true\" class=\"anchor\" id=\"tf-idf-w-kodzie\"></a>TF-IDF w kodzie</h3>\n<p>Spróbujcie sami. Oto kompletny przykład w Pythonie:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>feature_extraction</a-v>.<a-v>text</a-v> <a-k>import</a-k> <a-cr>TfidfVectorizer</a-cr>\n<a-k>import</a-k> <a-v>pandas</a-v> <a-k>as</a-k> <a-v>pd</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Ten film jest świetny&quot;</a-s>,\n    <a-s>&quot;Ten film jest beznadziejny&quot;</a-s>,\n    <a-s>&quot;Świetny film polecam&quot;</a-s>,\n]\n\n<a-v>vectorizer</a-v> <a-o>=</a-o> <a-f>TfidfVectorizer</a-f>()\n<a-v>tfidf_matrix</a-v> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>fit_transform</a-pr>(<a-v>corpus</a-v>)\n\n<a-v>df</a-v> <a-o>=</a-o> <a-v>pd</a-v>.<a-pr>DataFrame</a-pr>(\n    <a-v>tfidf_matrix</a-v>.<a-pr>toarray</a-pr>(),\n    <a-v>columns</a-v><a-o>=</a-o><a-v>vectorizer</a-v>.<a-pr>get_feature_names_out</a-pr>(),\n    <a-v>index</a-v><a-o>=</a-o>[<a-s>&quot;Dok 1&quot;</a-s>, <a-s>&quot;Dok 2&quot;</a-s>, <a-s>&quot;Dok 3&quot;</a-s>]\n)\n<a-f>print</a-f>(<a-v>df</a-v>.<a-pr>round</a-pr>(<a-n>2</a-n>))</code></pre>\n<p>Wynik będzie wyglądał mniej więcej tak:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">       beznadziejny  film   jest   polecam  świetny   ten\nDok 1         0.00  0.37  0.49     0.00     0.37   0.49\nDok 2         0.58  0.35  0.46     0.00     0.00   0.46\nDok 3         0.00  0.41  0.00     0.54     0.41   0.00\n</code></pre>\n<p>Zauważcie: &quot;film&quot; ma niskie wartości wszędzie (bo jest wszędzie). &quot;beznadziejny&quot; ma wysoką wartość tylko w Dokumencie 2 (bo tylko tam występuje). TF-IDF działa!</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Eksperyment dla was:</strong> Pomyślcie o waszych notatkach ze studiów (albo z pracy). Jakie słowa miałyby wysokie TF-IDF? Prawdopodobnie terminy fachowe - &quot;rekurencja&quot;, &quot;entropy&quot;, &quot;backpropagation&quot;. A jakie miałyby niskie? &quot;I&quot;, &quot;jest&quot;, &quot;na&quot;, &quot;że&quot; - bo są wszędzie.</p>\n</div>\n<h3><a href=\"#tf-idf-jako-wyszukiwarka\" aria-hidden=\"true\" class=\"anchor\" id=\"tf-idf-jako-wyszukiwarka\"></a>TF-IDF jako wyszukiwarka</h3>\n<p>TF-IDF ma jedno super praktyczne zastosowanie: <strong>przeszukiwanie tekstu</strong>. To jest właściwie to, jak działały pierwsze wyszukiwarki internetowe.</p>\n<p>Idea jest prosta: zamieniasz zapytanie użytkownika na wektor TF-IDF, i szukasz dokumentu, którego wektor jest do niego <strong>najbardziej podobny</strong> (tzw. podobieństwo kosinusowe).</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>feature_extraction</a-v>.<a-v>text</a-v> <a-k>import</a-k> <a-cr>TfidfVectorizer</a-cr>\n<a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>metrics</a-v>.<a-v>pairwise</a-v> <a-k>import</a-k> <a-v>cosine_similarity</a-v>\n<a-k>import</a-k> <a-v>numpy</a-v> <a-k>as</a-k> <a-v>np</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Ten film jest świetny&quot;</a-s>,\n    <a-s>&quot;Ten film jest beznadziejny&quot;</a-s>,\n    <a-s>&quot;Świetny film polecam&quot;</a-s>,\n]\n\n<a-v>vectorizer</a-v> <a-o>=</a-o> <a-f>TfidfVectorizer</a-f>()\n<a-v>tfidf_matrix</a-v> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>fit_transform</a-pr>(<a-v>corpus</a-v>)\n\n<a-v>query</a-v> <a-o>=</a-o> <a-s>&quot;świetny film&quot;</a-s>\n<a-v>query_vec</a-v> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>transform</a-pr>([<a-v>query</a-v>])\n\n<a-v>similarities</a-v> <a-o>=</a-o> <a-f>cosine_similarity</a-f>(<a-v>query_vec</a-v>, <a-v>tfidf_matrix</a-v>)[<a-n>0</a-n>]\n<a-v>ranked</a-v> <a-o>=</a-o> <a-f>sorted</a-f>(<a-f>zip</a-f>(<a-v>corpus</a-v>, <a-v>similarities</a-v>), <a-v>key</a-v><a-o>=</a-o><a-k>lambda</a-k> <a-v>x</a-v>: <a-o>-</a-o><a-v>x</a-v>[<a-n>1</a-n>])\n<a-k>for</a-k> <a-v>i</a-v>, (<a-v>doc</a-v>, <a-v>sim</a-v>) <a-o>in</a-o> <a-f>enumerate</a-f>(<a-v>ranked</a-v>, <a-n>1</a-n>):\n    <a-f>print</a-f>(<a-s>f&#39;</a-s><a-p>{</a-p><a-v>i</a-v><a-p>}</a-p><a-s>. &quot;</a-s><a-p>{</a-p><a-v>doc</a-v><a-p>}</a-p><a-s>&quot; → </a-s><a-p>{</a-p><a-v>sim</a-v><a-eb>:.3f</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Zapytanie: &quot;świetny film&quot;\n\n1. &quot;Świetny film polecam&quot;         → 0.694  ← najlepszy wynik!\n2. &quot;Ten film jest świetny&quot;       → 0.667\n3. &quot;Ten film jest beznadziejny&quot;   → 0.229\n</code></pre>\n<p>Dokument 3 wygrywa - bo &quot;świetny&quot; i &quot;film&quot; to dla niego słowa-klucze o wysokim TF-IDF. Dokument 2 ma &quot;film&quot;, ale nie ma &quot;świetny&quot; - więc jego podobieństwo jest niskie.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Podobieństwo kosinusowe</strong> mierzy kąt między dwoma wektorami. Jeśli wektory wskazują w tym samym kierunku (podobne proporcje słów) - podobieństwo jest bliskie 1. Jeśli w przeciwnych - bliskie 0. Nie przejmujcie się matematyką - intuicja jest prosta: &quot;jak bardzo te dwa wektory są do siebie podobne?&quot;</p>\n</div>\n<hr />\n<h2><a href=\"#n-gramy---czyli-dodajemy-kontekst\" aria-hidden=\"true\" class=\"anchor\" id=\"n-gramy---czyli-dodajemy-kontekst\"></a>N-gramy - czyli dodajemy kontekst</h2>\n<h3><a href=\"#problem-słowa-nie-żyją-w-próżni\" aria-hidden=\"true\" class=\"anchor\" id=\"problem-słowa-nie-żyją-w-próżni\"></a>Problem: słowa nie żyją w próżni</h3>\n<p>TF-IDF jest lepszy niż czysty BoW, bo przynajmniej waży słowa według ważności. Ale nadal traktuje każde słowo <strong>osobno</strong>. Nie wie, że &quot;nie&quot; + &quot;lubię&quot; = coś zupełnie innego niż &quot;lubię&quot; samo.</p>\n<p>Albo przykład z angielskiego, który świetnie pokazuje problem:</p>\n<ul>\n<li>&quot;big red <strong>machine</strong> and carpet&quot; - mówimy o maszynie</li>\n<li>&quot;big red <strong>carpet</strong> and machine&quot; - mówimy o dywanie</li>\n</ul>\n<p>Same słowa są te same! Ale kolejność zmienia wszystko. BoW i TF-IDF tego nie widzą.</p>\n<h3><a href=\"#czym-są-n-gramy\" aria-hidden=\"true\" class=\"anchor\" id=\"czym-są-n-gramy\"></a>Czym są n-gramy?</h3>\n<p><strong>N-gram</strong> to po prostu ciąg N kolejnych elementów (zwykle słów albo znaków).</p>\n<p>Przykład ze zdaniem &quot;Ala ma kota&quot;:</p>\n<table>\n<thead>\n<tr>\n<th>Typ</th>\n<th>N</th>\n<th>Wynik</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Unigramy</td>\n<td>1</td>\n<td>Ala, ma, kota</td>\n</tr>\n<tr>\n<td>Bigramy</td>\n<td>2</td>\n<td>Ala ma, ma kota</td>\n</tr>\n<tr>\n<td>Trigramy</td>\n<td>3</td>\n<td>Ala ma kota</td>\n</tr>\n</tbody>\n</table>\n<p>I teraz magia: zamiast liczyć pojedyncze słowa, liczymy <strong>pary słów</strong> (bigramy). I nagle:</p>\n<ul>\n<li>&quot;nie lubię&quot; staje się jednym bytem (negacja + czasownik = negatywny sens)</li>\n<li>&quot;big red&quot; i &quot;red carpet&quot; to różne rzeczy</li>\n<li>&quot;New York&quot; to jedna jednostka, nie dwa słowa</li>\n</ul>\n<p>Można też łączyć n-gramy z TF-IDF! W sklearn wystarczy zmienić jeden parametr:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>feature_extraction</a-v>.<a-v>text</a-v> <a-k>import</a-k> <a-cr>TfidfVectorizer</a-cr>\n<a-k>import</a-k> <a-v>pandas</a-v> <a-k>as</a-k> <a-v>pd</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Ten film jest świetny&quot;</a-s>,\n    <a-s>&quot;Ten film jest beznadziejny&quot;</a-s>,\n    <a-s>&quot;Świetny film polecam&quot;</a-s>,\n]\n\n<a-v>vectorizer</a-v> <a-o>=</a-o> <a-f>TfidfVectorizer</a-f>(<a-v>ngram_range</a-v><a-o>=</a-o>(<a-n>1</a-n>, <a-n>2</a-n>))\n<a-v>tfidf_matrix</a-v> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>fit_transform</a-pr>(<a-v>corpus</a-v>)\n\n<a-v>df</a-v> <a-o>=</a-o> <a-v>pd</a-v>.<a-pr>DataFrame</a-pr>(\n    <a-v>tfidf_matrix</a-v>.<a-pr>toarray</a-pr>(),\n    <a-v>columns</a-v><a-o>=</a-o><a-v>vectorizer</a-v>.<a-pr>get_feature_names_out</a-pr>(),\n    <a-v>index</a-v><a-o>=</a-o>[<a-s>&quot;Dok 1&quot;</a-s>, <a-s>&quot;Dok 2&quot;</a-s>, <a-s>&quot;Dok 3&quot;</a-s>]\n)\n<a-f>print</a-f>(<a-v>df</a-v>.<a-pr>round</a-pr>(<a-n>2</a-n>))</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">       beznadziejny  film  film jest  film polecam  jest  jest beznadziejny  jest świetny  polecam   ten  ten film  świetny  świetny film\nDok 1          0.00  0.29       0.37           0.0  0.37               0.00          0.49      0.0  0.37      0.37     0.37           0.0\nDok 2          0.46  0.27       0.35           0.0  0.35               0.46          0.00      0.0  0.35      0.35     0.00           0.0\nDok 3          0.00  0.30       0.00           0.5  0.00               0.00          0.00      0.5  0.00      0.00     0.38           0.5\n</code></pre>\n<p>Zobaczcie, co się stało - słownik cech urósł! Oprócz pojedynczych słów (&quot;film&quot;, &quot;jest&quot;, &quot;świetny&quot;) mamy teraz <strong>pary</strong>: &quot;film jest&quot;, &quot;jest świetny&quot;, &quot;jest beznadziejny&quot;, &quot;świetny film&quot;, &quot;ten film&quot;, &quot;film polecam&quot;. Każda para to osobna kolumna z własnym TF-IDF.</p>\n<p>Dzięki temu model widzi, że &quot;jest świetny&quot; (Dok 1) i &quot;jest beznadziejny&quot; (Dok 2) to <strong>różne rzeczy</strong> - bo to różne bigramy z różnymi wartościami TF-IDF.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>N-gramy to kompromis.</strong> Im większe N, tym więcej kontekstu łapiesz, ale tym więcej danych potrzebujesz. Z trigramami masz 3× więcej kombinacji niż z unigramami. Z 4-gramami - jeszcze więcej. I szybko dochodzisz do momentu, gdzie większość n-gramów występuje w korpusie tylko raz, co nie jest przydatne.</p>\n</div>\n<h3><a href=\"#łańcuchy-markowa---gdy-n-gramy-zaczynają-przewidywać\" aria-hidden=\"true\" class=\"anchor\" id=\"łańcuchy-markowa---gdy-n-gramy-zaczynają-przewidywać\"></a>Łańcuchy Markowa - gdy n-gramy zaczynają &quot;przewidywać&quot;</h3>\n<p>Same n-gramy to tylko zliczenia - tak jak BoW, nie generują tekstu. Ale jeśli dodamy do nich <strong>prawdopodobieństwa przejść</strong>, nagle zyskujemy coś, co <strong>generuje nowy tekst</strong>.</p>\n<p>Pomysł jest prosty: dla każdego słowa patrzymy, jakie słowa najczęściej po nim następują. I wybieramy następne słowo probabilistycznie.</p>\n<p>Weźmy mały korpus:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">&quot;ala ma kota ala ma psa kot lubi mleko ala lubi kota&quot;\n</code></pre>\n<p>Liczymy bigramy i prawdopodobieństwa przejść:</p>\n<table>\n<thead>\n<tr>\n<th>Słowo</th>\n<th>Następne słowo</th>\n<th>Prawdopodobieństwo</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>ala</strong></td>\n<td>ma</td>\n<td>2/3 = <strong>67%</strong></td>\n</tr>\n<tr>\n<td><strong>ala</strong></td>\n<td>lubi</td>\n<td>1/3 = 33%</td>\n</tr>\n<tr>\n<td><strong>ma</strong></td>\n<td>kota</td>\n<td>1/2 = 50%</td>\n</tr>\n<tr>\n<td><strong>ma</strong></td>\n<td>psa</td>\n<td>1/2 = 50%</td>\n</tr>\n<tr>\n<td><strong>kot</strong></td>\n<td>lubi</td>\n<td>1/1 = 100%</td>\n</tr>\n<tr>\n<td><strong>lubi</strong></td>\n<td>mleko</td>\n<td>1/2 = 50%</td>\n</tr>\n<tr>\n<td><strong>lubi</strong></td>\n<td>kota</td>\n<td>1/2 = 50%</td>\n</tr>\n</tbody>\n</table>\n<p>To jest właśnie <strong>łańcuch Markowa</strong> - model, w którym prawdopodobieństwo następnego stanu zależy <strong>tylko od obecnego stanu</strong> (albo kilku ostatnich).</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    A[&quot;ala&quot;] --&gt;|&quot;67%&quot;| B[&quot;ma&quot;]\n    A --&gt;|&quot;33%&quot;| L[&quot;lubi&quot;]\n    B --&gt;|&quot;50%&quot;| E[&quot;kota&quot;]\n    B --&gt;|&quot;50%&quot;| F[&quot;psa&quot;]\n    L --&gt;|&quot;50%&quot;| M[&quot;mleko&quot;]\n    L --&gt;|&quot;50%&quot;| E\n    \n    style A fill:#9999ff,color:#fff\n    style B fill:#ffcc99,color:#000\n    style E fill:#99ff99,color:#000\n</code></pre>\n<p>&quot;ala&quot; → najczęściej &quot;ma&quot; (67%) → &quot;kota&quot; albo &quot;psa&quot; (po 50%). Czyli generujemy np.: &quot;ala ma kota&quot;. Albo &quot;ala ma psa&quot;. Albo &quot;ala lubi kota&quot;. Wszystkie te zdania są &quot;nowe&quot; - nie wystąpiły jako całości w korpusie - ale model skleił je z prawdopodobieństw przejść.</p>\n<h3><a href=\"#jak-to-działa-pod-spodem---krok-po-kroku\" aria-hidden=\"true\" class=\"anchor\" id=\"jak-to-działa-pod-spodem---krok-po-kroku\"></a>Jak to działa pod spodem - krok po kroku</h3>\n<p>Zanim przejdziemy do kodu, prześledźmy cały proces na palcach. Mamy ten sam korpus:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">&quot;ala ma kota ala ma psa kot lubi mleko ala lubi kota&quot;\n</code></pre>\n<p><strong>Krok 1: Budujemy słownik trigramów.</strong> Przesuwamy &quot;okienko&quot; o 3 słowa i patrzymy, co jest za nim:</p>\n<table>\n<thead>\n<tr>\n<th>Okno (trigram)</th>\n<th>Następne słowo</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>ala ma <strong>kota</strong></td>\n<td>ala</td>\n</tr>\n<tr>\n<td>ma kota <strong>ala</strong></td>\n<td>ma</td>\n</tr>\n<tr>\n<td>kota ala <strong>ma</strong></td>\n<td>psa</td>\n</tr>\n<tr>\n<td>ala ma <strong>psa</strong></td>\n<td>kot</td>\n</tr>\n<tr>\n<td>ma psa <strong>kot</strong></td>\n<td>lubi</td>\n</tr>\n<tr>\n<td>psa kot <strong>lubi</strong></td>\n<td>mleko</td>\n</tr>\n<tr>\n<td>kot lubi <strong>mleko</strong></td>\n<td>ala</td>\n</tr>\n<tr>\n<td>lubi mleko <strong>ala</strong></td>\n<td>lubi</td>\n</tr>\n<tr>\n<td>mleko ala <strong>lubi</strong></td>\n<td>kota</td>\n</tr>\n</tbody>\n</table>\n<p>Każdy trigram ma dokładnie jedno możliwe następne słowo (bo nasz korpus jest malutki).</p>\n<p><strong>Krok 2: Generujemy.</strong> Zaczynamy od trigramu startowego <code>(&quot;ala&quot;, &quot;ma&quot;, &quot;kota&quot;)</code>:</p>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p><strong>Ten trigram startowy to nasz &quot;prompt&quot;!</strong> Tak samo jak w ChatGPT wpisujesz tekst i model kontynuuje - tu wpisujemy &quot;ala ma kota&quot; i generator dobiera kolejne słowa. Jedyna różnica to skala: ChatGPT ma kontekst na tysiące tokenów, a my na 3 słowa.</p>\n</div>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Start:    ala ma kota\nKrok 1:   ala ma kota [ala]     ← po (ala,ma,kota) następuje &quot;ala&quot;\nKrok 2:   ala ma kota ala [ma]  ← po (ma,kota,ala) następuje &quot;ma&quot;\nKrok 3:   ala ma kota ala ma [psa]   ← po (kota,ala,ma) następuje &quot;psa&quot;\nKrok 4:   ... ma psa [kot]      ← po (ala,ma,psa) następuje &quot;kot&quot;\nKrok 5:   ... psa kot [lubi]    ← po (ma,psa,kot) następuje &quot;lubi&quot;\nKrok 6:   ... kot lubi [mleko]  ← po (psa,kot,lubi) następuje &quot;mleko&quot;\nKrok 7:   ... lubi mleko [ala]  ← po (kot,lubi,mleko) następuje &quot;ala&quot;\nKrok 8:   ... mleko ala [lubi]  ← po (lubi,mleko,ala) następuje &quot;lubi&quot;\nKrok 9:   ... ala lubi [kota]   ← po (mleko,ala,lubi) następuje &quot;kota&quot;\nKrok 10:  STOP ← trigram (ala,lubi,kota) nie jest w słowniku\n</code></pre>\n<p>Wynik: <code>&quot;ala ma kota ala ma psa kot lubi mleko ala lubi kota&quot;</code> - dokładnie nasz korpus, tylko &quot;przeklejony&quot; od środka. Na większym korpusie wynik byłby inny za każdym razem.</p>\n<h3><a href=\"#prosty-generator-tekstu-z-n-gramów\" aria-hidden=\"true\" class=\"anchor\" id=\"prosty-generator-tekstu-z-n-gramów\"></a>Prosty generator tekstu z n-gramów</h3>\n<p>Oto kompletny (i naprawdę krótki!) generator tekstu w Pythonie:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>import</a-k> <a-v>random</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> <a-s>&quot;ala ma kota ala ma psa kot lubi mleko ala lubi kota&quot;</a-s>\n<a-v>tokens</a-v> <a-o>=</a-o> <a-v>corpus</a-v>.<a-pr>split</a-pr>()\n\n<a-v>trigrams</a-v> <a-o>=</a-o> {}\n<a-k>for</a-k> <a-v>i</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-f>len</a-f>(<a-v>tokens</a-v>) <a-o>-</a-o> <a-n>3</a-n>):\n    <a-v>key</a-v> <a-o>=</a-o> (<a-v>tokens</a-v>[<a-v>i</a-v>], <a-v>tokens</a-v>[<a-v>i</a-v> <a-o>+</a-o> <a-n>1</a-n>], <a-v>tokens</a-v>[<a-v>i</a-v> <a-o>+</a-o> <a-n>2</a-n>])\n    <a-v>next_word</a-v> <a-o>=</a-o> <a-v>tokens</a-v>[<a-v>i</a-v> <a-o>+</a-o> <a-n>3</a-n>]\n    <a-k>if</a-k> <a-v>key</a-v> <a-o>not in</a-o> <a-v>trigrams</a-v>:\n        <a-v>trigrams</a-v>[<a-v>key</a-v>] <a-o>=</a-o> []\n    <a-v>trigrams</a-v>[<a-v>key</a-v>].<a-pr>append</a-pr>(<a-v>next_word</a-v>)\n\n<a-v>current</a-v> <a-o>=</a-o> (<a-s>&quot;ala&quot;</a-s>, <a-s>&quot;ma&quot;</a-s>, <a-s>&quot;kota&quot;</a-s>)  <a-c># ← to jest nasz &quot;prompt&quot;!</a-c>\n<a-v>output</a-v> <a-o>=</a-o> <a-f>list</a-f>(<a-v>current</a-v>)\n\n<a-k>for</a-k> <a-v>_</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-n>10</a-n>):\n    <a-k>if</a-k> <a-f>tuple</a-f>(<a-v>output</a-v>[<a-o>-</a-o><a-n>3</a-n>:]) <a-o>not in</a-o> <a-v>trigrams</a-v>:\n        <a-k>break</a-k>\n    <a-v>possibilities</a-v> <a-o>=</a-o> <a-v>trigrams</a-v>[<a-f>tuple</a-f>(<a-v>output</a-v>[<a-o>-</a-o><a-n>3</a-n>:])]\n    <a-v>output</a-v>.<a-pr>append</a-pr>(<a-v>random</a-v>.<a-pr>choice</a-pr>(<a-v>possibilities</a-v>))\n\n<a-f>print</a-f>(<a-s>&quot; &quot;</a-s>.<a-pr>join</a-pr>(<a-v>output</a-v>))</code></pre>\n<p>Możliwy wynik: <code>&quot;ala ma kota ala ma psa kot lubi mleko ala ma kota ala ma psa&quot;</code></p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>Dlaczego za każdym razem wychodzi to samo?</strong> Bo nasz korpus jest tak mały, że większość trigramów ma <strong>tylko jedno</strong> możliwe następne słowo. <code>random.choice</code> nie ma z czego wybierać! Na większym korpusie (np. całej polskiej Wikipedii) ten sam kod generowałby <strong>za każdym razem inny tekst</strong> - bo prawie każdy trigram miałby kilka możliwych kontynuacji o różnych prawdopodobieństwach. I to jest właśnie moment, kiedy generowanie staje się interesujące.</p>\n</div>\n<p>Nic wielkiego, prawda? Ale to dlatego, że nasz korpus jest mikroskopijny. Na prawdziwym korpusie (np. całej Wikipedii) generator trigramowy potrafi produkować zdania, które brzmią sensownie, choć nie wystąpiły nigdy wcześniej.</p>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p><strong>Łańcuchy Markowa to pierwsze &quot;modele językowe&quot;!</strong> Przewidywanie następnego słowa na podstawie kontekstu - to jest DOKŁADNIE to, co robi ChatGPT. Oczywiście GPT używa o wiele bardziej zaawansowanej metody (Transformer + attention na tysiącach tokenów kontekstu), ale <strong>fundamentalna idea jest ta sama</strong>: prawdopodobieństwo następnego tokenu. LLM to potomek łańcuchów Markowa. Na sterydach ;-)</p>\n</div>\n<hr />\n<h2><a href=\"#techniki-bayesowskie---czyli-klasyfikujemy-tekst\" aria-hidden=\"true\" class=\"anchor\" id=\"techniki-bayesowskie---czyli-klasyfikujemy-tekst\"></a>Techniki bayesowskie - czyli klasyfikujemy tekst</h2>\n<h3><a href=\"#a-gdybyśmy-chcieli-nie-generować-ale-klasyfikować\" aria-hidden=\"true\" class=\"anchor\" id=\"a-gdybyśmy-chcieli-nie-generować-ale-klasyfikować\"></a>A gdybyśmy chcieli nie generować, ale KLASYFIKOWAĆ?</h3>\n<p>Łańcuchy Markowa są super do generowania tekstu. Ale w praktyce często chcemy coś innego: <strong>przypisać tekst do kategorii</strong>.</p>\n<ul>\n<li>Czy ten mail to <strong>spam</strong> czy <strong>nie-spam</strong>?</li>\n<li>Czy ta recenzja jest <strong>pozytywna</strong> czy <strong>negatywna</strong>?</li>\n<li>Czy ten artykuł jest o <strong>sporcie</strong>, <strong>polityce</strong> czy <strong>technologii</strong>?</li>\n</ul>\n<p>I tu wchodzi <strong>Naive Bayes</strong> - jeden z najprostszych, a zarazem najużyteczniejszych algorytmów klasyfikacji tekstu.</p>\n<h3><a href=\"#twierdzenie-bayesa---intuicja\" aria-hidden=\"true\" class=\"anchor\" id=\"twierdzenie-bayesa---intuicja\"></a>Twierdzenie Bayesa - intuicja</h3>\n<p>Bez wzorów, samym przykładem:</p>\n<p>Wyobraźcie sobie, że idzie do was kolega i mówi: &quot;Kaszlę.&quot; Jakie jest prawdopodobieństwo, że ma przeziębienie?</p>\n<p>Zależy! Jeśli jest listopad i wszyscy w biurze chorują - wysokie. Jeśli jest lipiec i kaszle raz - niskie.</p>\n<p>Twierdzenie Bayesa to formalny sposób myślenia o takich sytuacjach: <strong>aktualizujemy naszą wiedzę na podstawie nowych dowodów.</strong></p>\n<p>A teraz przełóżmy to na tekst. Pytanie brzmi:</p>\n<blockquote>\n<p>&quot;Jeśli dokument zawiera słowo 'viagra', jakie jest prawdopodobieństwo, że to spam?&quot;</p>\n</blockquote>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    D[&quot;📧 Nowy mail:&lt;br/&gt;'Kup viagra tanio!'&quot;] --&gt; Q[&quot;P(spam | 'kup viagra tanio') = ?&quot;]\n    Q --&gt; S[&quot;📊 P(spam) × P('kup'|spam) × P('viagra'|spam) × P('tanio'|spam)&quot;]\n    Q --&gt; H[&quot;📊 P(nie-spam) × P('kup'|nie-spam) × P('viagra'|nie-spam) × P('tanio'|nie-spam)&quot;]\n    S --&gt; C{&quot;Które&lt;br/&gt;prawdopodobieństwo&lt;br/&gt;jest większe?&quot;}\n    H --&gt; C\n    C --&gt;|Spam!| R[&quot;🗑️ SPAM&quot;]\n    \n    style D fill:#ffcc99,color:#000\n    style C fill:#9999ff,color:#fff\n    style R fill:#ff9999,color:#000\n</code></pre>\n<h3><a href=\"#naive-bayes-w-akcji\" aria-hidden=\"true\" class=\"anchor\" id=\"naive-bayes-w-akcji\"></a>Naive Bayes w akcji</h3>\n<p>Mamy mały zbiór maili:</p>\n<table>\n<thead>\n<tr>\n<th>Mail</th>\n<th>Treść</th>\n<th>Etykieta</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>1</td>\n<td>&quot;Kup viagra tanio teraz&quot;</td>\n<td>Spam</td>\n</tr>\n<tr>\n<td>2</td>\n<td>&quot;Viagra darmowa oferta&quot;</td>\n<td>Spam</td>\n</tr>\n<tr>\n<td>3</td>\n<td>&quot;Spotkanie jutro o 10&quot;</td>\n<td>Nie-spam</td>\n</tr>\n<tr>\n<td>4</td>\n<td>&quot;Prześlij mi raport jutro&quot;</td>\n<td>Nie-spam</td>\n</tr>\n</tbody>\n</table>\n<p>Przychodzi nowy mail: <strong>&quot;Viagra jutro spotkanie&quot;</strong>. Spam czy nie?</p>\n<p>Naive Bayes liczy:</p>\n<ol>\n<li>P(Spam) = 2/4 = 0.5, P(Nie-spam) = 2/4 = 0.5</li>\n<li>P(&quot;viagra&quot; | Spam) = 2/2 = 1.0 (oba spamy mają &quot;viagra&quot;)</li>\n<li>P(&quot;viagra&quot; | Nie-spam) = 0/2 = 0.0 (żaden nie-spam nie ma &quot;viagra&quot;)</li>\n<li>P(&quot;jutro&quot; | Spam) = 0/2 = 0.0</li>\n<li>P(&quot;jutro&quot; | Nie-spam) = 2/2 = 1.0</li>\n</ol>\n<p>P(Spam | &quot;viagra jutro spotkanie&quot;) ∝ 0.5 × 1.0 × 0.0 × ... = <strong>0</strong>!</p>\n<p>P(Nie-spam | &quot;viagra jutro spotkanie&quot;) ∝ 0.5 × 0.0 × 1.0 × ... = <strong>0</strong>!</p>\n<p>Ups. Obecność &quot;jutro&quot; (słowo z nie-spamu) wyzerowało spam, a obecność &quot;viagra&quot; (słowo spamowe) wyzerowało nie-spam. Ten konkretny mail jest trudny ;-)</p>\n<p>W praktyce stosuje się tzw. <strong>Laplace smoothing</strong> (dodajemy 1 do każdego licznika), żeby uniknąć zerowania:</p>\n<ul>\n<li>P(&quot;viagra&quot; | Spam) = (2 + 1) / (7 + 14) = 0.214 (dodajemy 1, dzielimy przez wszystkie słowa w spamie + unikalne słowa)</li>\n<li>P(&quot;viagra&quot; | Nie-spam) = (0 + 1) / (7 + 14) = 0.048</li>\n</ul>\n<p>Teraz: P(Spam | mail) ∝ 0.5 × 0.214 × 0.048 × 0.048 ≈ <strong>0.000246</strong>\nP(Nie-spam | mail) ∝ 0.5 × 0.048 × 0.143 × 0.143 ≈ <strong>0.000490</strong></p>\n<p>Nie-spam wygrywa! Bo &quot;jutro&quot; i &quot;spotkanie&quot; silnie wskazują na normalny mail.</p>\n<h3><a href=\"#dlaczego-naive\" aria-hidden=\"true\" class=\"anchor\" id=\"dlaczego-naive\"></a>Dlaczego &quot;Naive&quot;?</h3>\n<p>Bo zakładamy, że słowa są <strong>niezależne</strong>. Czyli: prawdopodobieństwo wystąpienia &quot;viagra&quot; nie zależy od tego, czy &quot;tanie&quot; też występuje. Oczywiście to nie jest prawda! &quot;Viagra&quot; i &quot;tanie&quot; występują razem częściej niż przypadkiem. Ale model i tak działa zaskakująco dobrze...</p>\n<p>To jest trochę jak założenie, że pogoda w Warszawie nie zależy od pogody w Krakowie. Oczywiście trochę zależy! Ale jeśli chcesz szybko oszacować, czy potrzebujesz parasola, to założenie niezależności daje ci w miarę dobrą odpowiedź ;-)</p>\n<h3><a href=\"#klasyfikator-spamu-w-pythonie\" aria-hidden=\"true\" class=\"anchor\" id=\"klasyfikator-spamu-w-pythonie\"></a>Klasyfikator spamu w Pythonie</h3>\n<p>Skoro już rozumiemy matematykę, zróbmy to w kilku linijkach kodu. Biblioteka <code>scikit-learn</code> ma gotowy klasyfikator Naive Bayes:</p>\n<ul>\n<li><strong><code>CountVectorizer</code></strong> - zamienia tekst na wektor zliczeń słów (czyli nasz BoW z poprzedniej sekcji)</li>\n<li><strong><code>MultinomialNB</code></strong> - to jest właśnie Naive Bayes, tzw. &quot;wielomianowy&quot; (bo liczy prawdopodobieństwa słów), z <code>alpha=1.0</code> czyli Laplace smoothing</li>\n</ul>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>naive_bayes</a-v> <a-k>import</a-k> <a-cr>MultinomialNB</a-cr>\n<a-k>from</a-k> <a-v>sklearn</a-v>.<a-v>feature_extraction</a-v>.<a-v>text</a-v> <a-k>import</a-k> <a-cr>CountVectorizer</a-cr>\n\n<a-v>emails</a-v> <a-o>=</a-o> [\n    <a-s>&quot;Kup viagra tanio teraz&quot;</a-s>,\n    <a-s>&quot;Viagra darmowa oferta&quot;</a-s>,\n    <a-s>&quot;Spotkanie jutro o 10&quot;</a-s>,\n    <a-s>&quot;Prześlij mi raport jutro&quot;</a-s>,\n    <a-s>&quot;Tanie leki online kup teraz&quot;</a-s>,\n    <a-s>&quot;Raport z wczoraj prześlij&quot;</a-s>,\n]\n<a-v>labels</a-v> <a-o>=</a-o> [<a-n>1</a-n>, <a-n>1</a-n>, <a-n>0</a-n>, <a-n>0</a-n>, <a-n>1</a-n>, <a-n>0</a-n>]  <a-c># 1=spam, 0=nie-spam</a-c>\n\n<a-v>vectorizer</a-v> <a-o>=</a-o> <a-f>CountVectorizer</a-f>()\n<a-co>X</a-co> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>fit_transform</a-pr>(<a-v>emails</a-v>)\n\n<a-v>classifier</a-v> <a-o>=</a-o> <a-f>MultinomialNB</a-f>(<a-v>alpha</a-v><a-o>=</a-o><a-n>1.0</a-n>)\n<a-v>classifier</a-v>.<a-pr>fit</a-pr>(<a-co>X</a-co>, <a-v>labels</a-v>)\n\n<a-v>new_email</a-v> <a-o>=</a-o> <a-v>vectorizer</a-v>.<a-pr>transform</a-pr>([<a-s>&quot;Viagra jutro spotkanie&quot;</a-s>])\n<a-v>prediction</a-v> <a-o>=</a-o> <a-v>classifier</a-v>.<a-pr>predict</a-pr>(<a-v>new_email</a-v>)\n<a-f>print</a-f>(<a-s>&quot;Spam!&quot;</a-s> <a-k>if</a-k> <a-v>prediction</a-v>[<a-n>0</a-n>] <a-o>==</a-o> <a-n>1</a-n> <a-k>else</a-k> <a-s>&quot;Nie-spam.&quot;</a-s>)</code></pre>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Eksperyment dla was:</strong> Stwórzcie 5 pozytywnych zdań o filmie (&quot;Ten film był niesamowity!&quot;, &quot;Wspaniała akcja!&quot;, ...) i 5 negatywnych (&quot;Nudny jak flaki z olejem&quot;, &quot;Strata czasu&quot;, ...). Wypiszcie słowa, które występują TYLKO w pozytywnych i TYLKO w negatywnych. To jest intuicja Naive Bayes - każde słowo &quot;głosuje&quot; na jedną z kategorii.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\">Warning</p>\n<p><strong>Ograniczenie:</strong> Naive Bayes widzi słowa, ale nie kontekst. &quot;Film jest NIESAMOWITY... żesz beznadziejny&quot; - model zobaczy &quot;niesamowity&quot; i powie &quot;pozytywny&quot;. Nie ogarnia ironii ani złożonych konstrukcji. Potrzebujemy czegoś, co rozumie <strong>relacje między słowami</strong>. I tu wchodzimy w świat wektorów...</p>\n</div>\n<hr />\n<h2><a href=\"#word2vec---słowa-stają-się-geometrią\" aria-hidden=\"true\" class=\"anchor\" id=\"word2vec---słowa-stają-się-geometrią\"></a>Word2Vec - słowa stają się geometrią</h2>\n<h3><a href=\"#moment-eureka\" aria-hidden=\"true\" class=\"anchor\" id=\"moment-eureka\"></a>Moment &quot;eureka&quot;</h3>\n<p>Wszystkie metody, które dotąd poznaliśmy, mają jeden wspólny problem: <strong>traktują słowa jako dyskretne, niezależne byty</strong>. W BoW &quot;kot&quot; jest na pozycji 47 w wektorze, &quot;pies&quot; na pozycji 138. Nie ma między nimi żadnej relacji.</p>\n<p>A przecież MY wiemy, że &quot;kot&quot; i &quot;pies&quot; są do siebie podobne - oba to zwierzęta domowe. &quot;Kot&quot; i &quot;samochód&quot; - zupełnie różne. Jak sprawić, żeby komputer też to &quot;wiedział&quot;?</p>\n<p>Odpowiedź z 2013 roku, od zespołu Tomáša Mikolova w Google: <strong>zamieńmy słowa w punkty w przestrzeni wielowymiarowej</strong>. Słowa podobne znaczeniowo będą blisko siebie. Słowa różne - daleko.</p>\n<p>To jest <strong>Word2Vec</strong>. I to jest przełom.</p>\n<h3><a href=\"#one-hot-encoding-punkt-wyjścia\" aria-hidden=\"true\" class=\"anchor\" id=\"one-hot-encoding-punkt-wyjścia\"></a>One-hot encoding: punkt wyjścia</h3>\n<p>Zanim Word2Vec, standardem było tzw. <strong>one-hot encoding</strong> - każde słowo to wektor z jedynką na swojej pozycji i zerami wszędzie indziej:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">&quot;kot&quot;  → [0, 0, 0, 1, 0, 0, ...]  (1 na pozycji 3)\n&quot;pies&quot; → [0, 0, 1, 0, 0, 0, ...]  (1 na pozycji 2)\n&quot;samochód&quot; → [1, 0, 0, 0, 0, 0, ...] (1 na pozycji 0)\n</code></pre>\n<p>Problem? <strong>Każde słowo jest w takiej samej odległości od każdego innego.</strong> Odległość między &quot;kot&quot; a &quot;pies&quot; jest taka sama jak między &quot;kot&quot; a &quot;samochód&quot;. Zero informacji o znaczeniu.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    subgraph &quot;One-hot: wszystko jest tak samo daleko&quot;\n        O1[&quot;kot&lt;br/&gt;[0,0,1,0]&quot;] ---|&quot;=&quot;| O2[&quot;pies&lt;br/&gt;[0,1,0,0]&quot;]\n        O1 ---|&quot;=&quot;| O3[&quot;samochód&lt;br/&gt;[1,0,0,0]&quot;]\n    end\n    \n    style O1 fill:#9999ff,color:#fff\n    style O2 fill:#ffcc99,color:#000\n    style O3 fill:#ff9999,color:#000\n</code></pre>\n<h3><a href=\"#word2vec-pokaż-mi-twoich-sąsiadów-a-powiem-ci-kim-jesteś\" aria-hidden=\"true\" class=\"anchor\" id=\"word2vec-pokaż-mi-twoich-sąsiadów-a-powiem-ci-kim-jesteś\"></a>Word2Vec: &quot;Pokaż mi twoich sąsiadów, a powiem ci, kim jesteś&quot;</h3>\n<p>Word2Vec opiera się na genialnej intuicji lingwistycznej: <strong>słowa, które występują w podobnym kontekście, mają podobne znaczenie.</strong></p>\n<p>Jeśli widzisz: &quot;___ biega po parku i goni gołębie&quot;, to co tam wpadnie? Pies? Kot? Raczej nie &quot;samochód&quot; ani &quot;demokracja&quot;. Kontekst definiuje słowo.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    subgraph &quot;Kontekst słowa 'pies'&quot;\n        P1[&quot;___ biega po parku&quot;]\n        P2[&quot;Mój ___ szczeka na listonosza&quot;]\n        P3[&quot;Karmię ___ karmą&quot;]\n    end\n    subgraph &quot;Kontekst słowa 'kot'&quot;\n        K1[&quot;___ śpi na kanapie&quot;]\n        K2[&quot;Mój ___ łapie myszy&quot;]\n        K3[&quot;Karmię ___ karmą&quot;]\n    end\n    \n    P3 -.-&gt;|&quot;wspólny kontekst!&quot;| K3\n    \n    style P3 fill:#ffff99,color:#000\n    style K3 fill:#ffff99,color:#000\n</code></pre>\n<p>&quot;Karmię ___ karmą&quot; - zarówno pies, jak i kot pasują. To znaczy, że kontekst tych słów jest częściowo wspólny. I to jest właśnie to, co Word2Vec wykorzystuje.</p>\n<p>Pamiętacie <strong>Saussure'a</strong> z <a href=\"semiotyka-a-llm.html\">drugiego posta</a>? Znaczenie jest relacyjne. &quot;Kot&quot; znaczy to, co znaczy, bo NIE jest &quot;psem&quot;, NIE jest &quot;domem&quot;. Word2Vec to matematyczna implementacja tej idei!</p>\n<h3><a href=\"#cbow-i-skip-gram---dwie-strony-jednej-monety\" aria-hidden=\"true\" class=\"anchor\" id=\"cbow-i-skip-gram---dwie-strony-jednej-monety\"></a>CBOW i Skip-gram - dwie strony jednej monety</h3>\n<p>Word2Vec ma dwa warianty. Zobaczmy oba:</p>\n<p><strong>CBOW (Continuous Bag of Words):</strong> z kontekstu zgadujemy słowo środkowe.</p>\n<p><code>&quot;Kot ___ na macie&quot; → siedzi? leży? śpi?</code></p>\n<p><strong>Skip-gram:</strong> ze słowa środkowego zgadujemy kontekst.</p>\n<p><code>&quot;siedzi&quot; → kot? na? macie?</code></p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    subgraph &quot;CBOW: kontekst → słowo&quot;\n        C1[&quot;Kot&quot;] --&gt; H1[&quot;🧠 Ukryta warstwa&lt;br/&gt;(uśrednione wektory)&quot;]\n        C2[&quot;na&quot;] --&gt; H1\n        C3[&quot;macie&quot;] --&gt; H1\n        H1 --&gt; O1[&quot;siedzi ✅&quot;]\n    end\n    \n    subgraph &quot;Skip-gram: słowo → kontekst&quot;\n        S1[&quot;siedzi&quot;] --&gt; H2[&quot;🧠 Ukryta warstwa&quot;]\n        H2 --&gt; O2[&quot;Kot?&quot;]\n        H2 --&gt; O3[&quot;na?&quot;]\n        H2 --&gt; O4[&quot;macie?&quot;]\n    end\n    \n    style H1 fill:#9999ff,color:#fff\n    style H2 fill:#ff9999,color:#000\n</code></pre>\n<p>Jak to działa wewnątrz? Sieć neuronowa z jedną ukrytą warstwą:</p>\n<ol>\n<li>Każde słowo zaczyna jako one-hot wektor (np. [0, 0, 1, 0, ...])</li>\n<li>Mnożymy przez macierz wag <strong>W</strong> (rozmiar: słownik × wymiar embeddingu, np. 50 000 × 300)</li>\n<li>Wynik to wektor o rozmiarze <strong>E</strong> (np. 300 liczb) - to jest nasz embedding!</li>\n<li>W CBOW: uśredniamy wektory kontekstu i przewidujemy środek. W Skip-gram: bierzemy środek i przewidujemy sąsiadów.</li>\n<li>Sieć trenuje się na milionach par (słowo, kontekst), aktualizując macierz <strong>W</strong></li>\n<li>Po treningu, <strong>wiersz macierzy W odpowiadający danemu słowu to jego embedding</strong></li>\n</ol>\n<p>Brzmi skomplikowanie? Zróbmy to w kodzie - od zera, bez żadnych bibliotek ML:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>import</a-k> <a-v>numpy</a-v> <a-k>as</a-k> <a-v>np</a-v>\n\n<a-v>np</a-v>.<a-pr>random</a-pr>.<a-pr>seed</a-pr>(<a-n>42</a-n>)\n\n<a-c># --- 1. Słownik i one-hot encoding ---</a-c>\n<a-v>corpus</a-v> <a-o>=</a-o> <a-s>&quot;kot siedzi na macie i śpi pies siedzi na dywanie i śpi kot biega po pokoju pies biega po parku&quot;</a-s>.<a-pr>split</a-pr>()\n<a-v>vocab</a-v> <a-o>=</a-o> <a-f>list</a-f>(<a-f>set</a-f>(<a-v>corpus</a-v>))\n<a-v>word2idx</a-v> <a-o>=</a-o> {<a-v>w</a-v>: <a-v>i</a-v> <a-k>for</a-k> <a-v>i</a-v>, <a-v>w</a-v> <a-o>in</a-o> <a-f>enumerate</a-f>(<a-v>vocab</a-v>)}\n<a-v>vocab_size</a-v> <a-o>=</a-o> <a-f>len</a-f>(<a-v>vocab</a-v>)\n\n<a-k>def</a-k> <a-f>one_hot</a-f>(<a-v>word</a-v>):\n    <a-v>vec</a-v> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>zeros</a-pr>(<a-v>vocab_size</a-v>)\n    <a-v>vec</a-v>[<a-v>word2idx</a-v>[<a-v>word</a-v>]] <a-o>=</a-o> <a-n>1</a-n>\n    <a-k>return</a-k> <a-v>vec</a-v>\n\n<a-f>print</a-f>(<a-s>f&#39;One-hot &quot;kot&quot;: </a-s><a-p>{</a-p><a-f>one_hot</a-f><a-eb>(</a-eb><a-s>&quot;kot&quot;</a-s><a-eb>)</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)\n<a-c># [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]  - tylko jedna jedynka!</a-c>\n\n<a-c># --- 2. Macierz wag (to będą nasze embeddingi) ---</a-c>\n<a-v>embed_dim</a-v> <a-o>=</a-o> <a-n>5</a-n>  <a-c># w prawdziwym Word2Vec to 100-300</a-c>\n<a-cr>W1</a-cr> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>random</a-pr>.<a-pr>randn</a-pr>(<a-v>vocab_size</a-v>, <a-v>embed_dim</a-v>) <a-o>*</a-o> <a-n>0.01</a-n>  <a-c># słownik → embedding</a-c>\n<a-cr>W2</a-cr> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>random</a-pr>.<a-pr>randn</a-pr>(<a-v>embed_dim</a-v>, <a-v>vocab_size</a-v>) <a-o>*</a-o> <a-n>0.01</a-n>  <a-c># embedding → słownik</a-c>\n\n<a-c># --- 3. Pary treningowe (CBOW: kontekst → środek) ---</a-c>\n<a-v>window</a-v> <a-o>=</a-o> <a-n>2</a-n>\n<a-v>pairs</a-v> <a-o>=</a-o> []\n<a-k>for</a-k> <a-v>i</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-v>window</a-v>, <a-f>len</a-f>(<a-v>corpus</a-v>) <a-o>-</a-o> <a-v>window</a-v>):\n    <a-v>context</a-v> <a-o>=</a-o> <a-v>corpus</a-v>[<a-v>i</a-v> <a-o>-</a-o> <a-v>window</a-v>:<a-v>i</a-v>] <a-o>+</a-o> <a-v>corpus</a-v>[<a-v>i</a-v> <a-o>+</a-o> <a-n>1</a-n>:<a-v>i</a-v> <a-o>+</a-o> <a-v>window</a-v> <a-o>+</a-o> <a-n>1</a-n>]\n    <a-v>target</a-v> <a-o>=</a-o> <a-v>corpus</a-v>[<a-v>i</a-v>]\n    <a-v>pairs</a-v>.<a-pr>append</a-pr>((<a-v>context</a-v>, <a-v>target</a-v>))\n\n<a-f>print</a-f>(<a-s>f&#39;Kontekst: </a-s><a-p>{</a-p><a-v>pairs</a-v><a-eb>[</a-eb><a-n>0</a-n><a-eb>][</a-eb><a-n>0</a-n><a-eb>]</a-eb><a-p>}</a-p><a-s> → Cel: </a-s><a-p>{</a-p><a-v>pairs</a-v><a-eb>[</a-eb><a-n>0</a-n><a-eb>][</a-eb><a-n>1</a-n><a-eb>]</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)\n<a-c># Kontekst: [&#39;kot&#39;, &#39;siedzi&#39;, &#39;macie&#39;, &#39;i&#39;] → Cel: na</a-c>\n\n<a-c># --- 4. Trening (gradient descent) ---</a-c>\n<a-k>def</a-k> <a-f>softmax</a-f>(<a-v>x</a-v>):\n    <a-v>e</a-v> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>exp</a-pr>(<a-v>x</a-v> <a-o>-</a-o> <a-v>np</a-v>.<a-pr>max</a-pr>(<a-v>x</a-v>))\n    <a-k>return</a-k> <a-v>e</a-v> <a-o>/</a-o> <a-v>e</a-v>.<a-pr>sum</a-pr>()\n\n<a-k>for</a-k> <a-v>epoch</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-n>500</a-n>):\n    <a-v>loss</a-v> <a-o>=</a-o> <a-n>0</a-n>\n    <a-k>for</a-k> <a-v>context_words</a-v>, <a-v>target_word</a-v> <a-o>in</a-o> <a-v>pairs</a-v>:\n        <a-v>context_idx</a-v> <a-o>=</a-o> [<a-v>word2idx</a-v>[<a-v>w</a-v>] <a-k>for</a-k> <a-v>w</a-v> <a-o>in</a-o> <a-v>context_words</a-v>]\n        <a-v>target_idx</a-v> <a-o>=</a-o> <a-v>word2idx</a-v>[<a-v>target_word</a-v>]\n\n        <a-c># forward: uśrednione embeddingi kontekstu → przewidujemy środek</a-c>\n        <a-v>hidden</a-v> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>mean</a-pr>(<a-cr>W1</a-cr>[<a-v>context_idx</a-v>], <a-v>axis</a-v><a-o>=</a-o><a-n>0</a-n>)\n        <a-v>output</a-v> <a-o>=</a-o> <a-f>softmax</a-f>(<a-v>hidden</a-v> @ <a-cr>W2</a-cr>)\n        <a-v>loss</a-v> <a-o>-=</a-o> <a-v>np</a-v>.<a-pr>log</a-pr>(<a-v>output</a-v>[<a-v>target_idx</a-v>] <a-o>+</a-o> <a-n>1e-8</a-n>)\n\n        <a-c># backward: aktualizujemy wagi</a-c>\n        <a-v>grad</a-v> <a-o>=</a-o> <a-v>output</a-v>.<a-pr>copy</a-pr>()\n        <a-v>grad</a-v>[<a-v>target_idx</a-v>] <a-o>-=</a-o> <a-n>1</a-n>\n        <a-cr>W2</a-cr> <a-o>-=</a-o> <a-n>0.05</a-n> <a-o>*</a-o> <a-v>np</a-v>.<a-pr>outer</a-pr>(<a-v>hidden</a-v>, <a-v>grad</a-v>)\n        <a-v>grad_hidden</a-v> <a-o>=</a-o> <a-v>grad</a-v> @ <a-cr>W2</a-cr>.<a-pr>T</a-pr>\n        <a-k>for</a-k> <a-v>idx</a-v> <a-o>in</a-o> <a-v>context_idx</a-v>:\n            <a-cr>W1</a-cr>[<a-v>idx</a-v>] <a-o>-=</a-o> <a-n>0.05</a-n> <a-o>*</a-o> <a-v>grad_hidden</a-v> <a-o>/</a-o> <a-f>len</a-f>(<a-v>context_idx</a-v>)\n\n<a-c># --- 5. Wynik: embeddingi! ---</a-c>\n<a-k>for</a-k> <a-v>word</a-v> <a-o>in</a-o> [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;pies&quot;</a-s>, <a-s>&quot;siedzi&quot;</a-s>, <a-s>&quot;biega&quot;</a-s>]:\n    <a-f>print</a-f>(<a-s>f&quot;</a-s><a-p>{</a-p><a-v>word</a-v><a-p>}</a-p><a-s>: </a-s><a-p>{</a-p><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-v>word</a-v><a-eb>]].</a-eb><a-pr>round</a-pr><a-eb>(</a-eb><a-n>3</a-n><a-eb>)</a-eb><a-p>}</a-p><a-s>&quot;</a-s>)\n\n<a-c># --- 6. Podobieństwo kosinusowe ---</a-c>\n<a-k>def</a-k> <a-f>cosine</a-f>(<a-v>a</a-v>, <a-v>b</a-v>):\n    <a-k>return</a-k> <a-v>np</a-v>.<a-pr>dot</a-pr>(<a-v>a</a-v>, <a-v>b</a-v>) <a-o>/</a-o> (<a-v>np</a-v>.<a-pr>linalg</a-pr>.<a-pr>norm</a-pr>(<a-v>a</a-v>) <a-o>*</a-o> <a-v>np</a-v>.<a-pr>linalg</a-pr>.<a-pr>norm</a-pr>(<a-v>b</a-v>) <a-o>+</a-o> <a-n>1e-8</a-n>)\n\n<a-f>print</a-f>(<a-s>f&#39;kot ↔ pies: </a-s><a-p>{</a-p><a-f>cosine</a-f><a-eb>(</a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;kot&quot;</a-s><a-eb>]], </a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;pies&quot;</a-s><a-eb>]]):.3f</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)\n<a-f>print</a-f>(<a-s>f&#39;kot ↔ siedzi: </a-s><a-p>{</a-p><a-f>cosine</a-f><a-eb>(</a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;kot&quot;</a-s><a-eb>]], </a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;siedzi&quot;</a-s><a-eb>]]):.3f</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)\n<a-f>print</a-f>(<a-s>f&#39;kot ↔ biega: </a-s><a-p>{</a-p><a-f>cosine</a-f><a-eb>(</a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;kot&quot;</a-s><a-eb>]], </a-eb><a-cr>W1</a-cr><a-eb>[</a-eb><a-v>word2idx</a-v><a-eb>[</a-eb><a-s>&quot;biega&quot;</a-s><a-eb>]]):.3f</a-eb><a-p>}</a-p><a-s>&#39;</a-s>)</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">One-hot &quot;kot&quot;: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]\nKontekst: ['kot', 'siedzi', 'macie', 'i'] → Cel: na\nkot: [-2.866 -0.264 -0.225 -1.822  2.783]\npies: [-1.133  0.678 -1.537  1.989  2.241]\nsiedzi: [ 1.524 -0.023  1.804 -2.355 -0.788]\nbiega: [ 2.     4.098 -0.315  2.971  0.815]\nkot ↔ pies: 0.378\nkot ↔ siedzi: -0.177\nkot ↔ biega: -0.407\n</code></pre>\n<p>Zauważcie: <strong>kot i pies</strong> (0.378) są bardziej podobne niż <strong>kot i biega</strong> (-0.407). Sieć sama odkryła, że &quot;kot&quot; i &quot;pies&quot; to zwierzęta, bo występują w podobnym kontekście. Zero etykiet, zero nadzoru - tylko tekst i matematyka.</p>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>To jest <strong>cały Word2Vec w ~30 linijkach</strong>. Prawdziwy Word2Vec dodaje jeszcze negative sampling (bo softmax na 50 000 słów jest wolny) i optymalizacje, ale zasada jest dokładnie ta sama.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p><strong>&quot;Fake task&quot;</strong>: Interesuje nas nie to, co sieć przewiduje, ale <strong>wagi ukrytej warstwy</strong>. Sieć trenujemy na &quot;sztucznym&quot; zadaniu przewidywania kontekstu, ale to, co chcemy wyciągnąć, to macierz W - nasze embeddingi. To trochę jak trenowanie kogoś do rozwiązywania krzyżówek nie po to, żeby dobrze rozwiązywał krzyżówki, ale po to, żeby poszerzył słownik ;-)</p>\n</div>\n<h3><a href=\"#cbow-vs-skip-gram\" aria-hidden=\"true\" class=\"anchor\" id=\"cbow-vs-skip-gram\"></a>CBOW vs Skip-gram</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>CBOW</th>\n<th>Skip-gram</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Kierunek</strong></td>\n<td>Kontekst → Słowo środkowe</td>\n<td>Słowo środkowe → Kontekst</td>\n</tr>\n<tr>\n<td><strong>Szybkość</strong></td>\n<td>Szybszy</td>\n<td>Wolniejszy</td>\n</tr>\n<tr>\n<td><strong>Rzadkie słowa</strong></td>\n<td>Radzi sobie gorzej</td>\n<td>Radzi sobie lepiej</td>\n</tr>\n<tr>\n<td><strong>Częste słowa</strong></td>\n<td>Radzi sobie lepiej</td>\n<td>Radzi sobie gorzej</td>\n</tr>\n<tr>\n<td><strong>Kiedy użyć</strong></td>\n<td>Duży korpus, częste słowa</td>\n<td>Mały korpus, rzadkie słowa</td>\n</tr>\n</tbody>\n</table>\n<p>Według oryginalnej pracy Mikolova et al.: <strong>Skip-gram jest lepszy dla rzadkich słów i małych zbiorów danych. CBOW jest szybszy i lepszy dla częstych słów.</strong></p>\n<h3><a href=\"#magiczna-przestrzeń-wektorowa\" aria-hidden=\"true\" class=\"anchor\" id=\"magiczna-przestrzeń-wektorowa\"></a>Magiczna przestrzeń wektorowa</h3>\n<p>Po wytrenowaniu Word2Vec, każde słowo to wektor (np. 300 liczb). I ta przestrzeń ma niezwykłe właściwości:</p>\n<p><strong>Słowa podobne są blisko:</strong></p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">odległość(&quot;kot&quot;, &quot;pies&quot;) &lt; odległość(&quot;kot&quot;, &quot;samochód&quot;)\nodległość(&quot;Polska&quot;, &quot;Niemcy&quot;) &lt; odległość(&quot;Polska&quot;, &quot;banan&quot;)\n</code></pre>\n<p><strong>Analogie działają jak dodawanie i odejmowanie wektorów:</strong></p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">wektor(&quot;król&quot;) - wektor(&quot;mężczyzna&quot;) + wektor(&quot;kobieta&quot;) ≈ wektor(&quot;królowa&quot;)\n</code></pre>\n<p>Dlaczego? Bo wektor &quot;króla&quot; koduje wiele wymiarów znaczeniowych - w jednym z nich jest informacja o &quot;rodzaju&quot; (mężczyzna/kobieta), w innym o &quot;władzy&quot; (monarchia). Odejmując &quot;mężczyznę&quot; i dodając &quot;kobietę&quot;, zmieniamy wymiar rodzaju, zachowując pozostałe.</p>\n<p>Inne przykłady analogii:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">Polska - Warszawa + Berlin ≈ Niemcy\nmały - mniejszy + duży ≈ większy\nłódź - woda + powietrze ≈ samolot\n</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    K[&quot;👑 król&lt;br/&gt;[0.50, 0.68, ...]&quot;] --&gt;|&quot;odjąć 'mężczyzna'&quot;| KM[&quot;[0.35, 0.55, ...]&lt;br/&gt;&lt;i&gt;monarcha, ale bez rodzaju&lt;/i&gt;&quot;]\n    M[&quot;👨 mężczyzna&lt;br/&gt;[0.15, 0.13, ...]&quot;]\n    W[&quot;👩 kobieta&lt;br/&gt;[0.12, 0.20, ...]&quot;]\n    KM --&gt;|&quot;dodać 'kobieta'&quot;| Q[&quot;👸 królowa&lt;br/&gt;[0.38, 0.64, ...]&quot;]\n    \n    K -.- M\n    Q -.- W\n    \n    style K fill:#ff9999,color:#000\n    style M fill:#ffcc99,color:#000\n    style W fill:#ccff99,color:#000\n    style Q fill:#99ccff,color:#000\n</code></pre>\n<p>To jest to, o czym mówiliśmy w <a href=\"cechy-jezykowe-a-llm.html\">pierwszym poście</a> przy okazji semantyki. Teraz widzicie, <strong>jak to działa pod spodem</strong>.</p>\n<h3><a href=\"#word2vec-w-kodzie\" aria-hidden=\"true\" class=\"anchor\" id=\"word2vec-w-kodzie\"></a>Word2Vec w kodzie</h3>\n<p>Najpierw wytrenujmy własny model na małym korpusie, żeby zobaczyć jak to działa od zera:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>gensim</a-v>.<a-v>models</a-v> <a-k>import</a-k> <a-cr>Word2Vec</a-cr>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;siedzi&quot;</a-s>, <a-s>&quot;na&quot;</a-s>, <a-s>&quot;macie&quot;</a-s>, <a-s>&quot;i&quot;</a-s>, <a-s>&quot;śpi&quot;</a-s>],\n    [<a-s>&quot;pies&quot;</a-s>, <a-s>&quot;siedzi&quot;</a-s>, <a-s>&quot;na&quot;</a-s>, <a-s>&quot;dywanie&quot;</a-s>, <a-s>&quot;i&quot;</a-s>, <a-s>&quot;śpi&quot;</a-s>],\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;biega&quot;</a-s>, <a-s>&quot;po&quot;</a-s>, <a-s>&quot;pokoju&quot;</a-s>],\n    [<a-s>&quot;pies&quot;</a-s>, <a-s>&quot;biega&quot;</a-s>, <a-s>&quot;po&quot;</a-s>, <a-s>&quot;parku&quot;</a-s>],\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;i&quot;</a-s>, <a-s>&quot;pies&quot;</a-s>, <a-s>&quot;bawią&quot;</a-s>, <a-s>&quot;się&quot;</a-s>, <a-s>&quot;w&quot;</a-s>, <a-s>&quot;ogrodzie&quot;</a-s>],\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;je&quot;</a-s>, <a-s>&quot;karmę&quot;</a-s>, <a-s>&quot;z&quot;</a-s>, <a-s>&quot;miski&quot;</a-s>],\n    [<a-s>&quot;pies&quot;</a-s>, <a-s>&quot;je&quot;</a-s>, <a-s>&quot;karmę&quot;</a-s>, <a-s>&quot;z&quot;</a-s>, <a-s>&quot;miski&quot;</a-s>],\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;łapie&quot;</a-s>, <a-s>&quot;mysz&quot;</a-s>, <a-s>&quot;w&quot;</a-s>, <a-s>&quot;domu&quot;</a-s>],\n    [<a-s>&quot;pies&quot;</a-s>, <a-s>&quot;goni&quot;</a-s>, <a-s>&quot;kota&quot;</a-s>, <a-s>&quot;po&quot;</a-s>, <a-s>&quot;ogrodzie&quot;</a-s>],\n    [<a-s>&quot;ptak&quot;</a-s>, <a-s>&quot;siedzi&quot;</a-s>, <a-s>&quot;na&quot;</a-s>, <a-s>&quot;gałęzi&quot;</a-s>, <a-s>&quot;drzewa&quot;</a-s>],\n    [<a-s>&quot;ryba&quot;</a-s>, <a-s>&quot;pływa&quot;</a-s>, <a-s>&quot;w&quot;</a-s>, <a-s>&quot;akwarium&quot;</a-s>],\n    [<a-s>&quot;samochód&quot;</a-s>, <a-s>&quot;jeździ&quot;</a-s>, <a-s>&quot;po&quot;</a-s>, <a-s>&quot;drodze&quot;</a-s>],\n    [<a-s>&quot;rower&quot;</a-s>, <a-s>&quot;jeździ&quot;</a-s>, <a-s>&quot;po&quot;</a-s>, <a-s>&quot;ścieżce&quot;</a-s>],\n    [<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;mruczy&quot;</a-s>, <a-s>&quot;gdy&quot;</a-s>, <a-s>&quot;głaszczesz&quot;</a-s>, <a-s>&quot;go&quot;</a-s>],\n    [<a-s>&quot;pies&quot;</a-s>, <a-s>&quot;merda&quot;</a-s>, <a-s>&quot;ogonem&quot;</a-s>, <a-s>&quot;gdy&quot;</a-s>, <a-s>&quot;widzi&quot;</a-s>, <a-s>&quot;pana&quot;</a-s>],\n]\n\n<a-v>model</a-v> <a-o>=</a-o> <a-f>Word2Vec</a-f>(\n    <a-v>sentences</a-v><a-o>=</a-o><a-v>corpus</a-v>,\n    <a-v>vector_size</a-v><a-o>=</a-o><a-n>10</a-n>,   <a-c># wymiar wektora (mały = edukacyjny)</a-c>\n    <a-v>window</a-v><a-o>=</a-o><a-n>3</a-n>,         <a-c># rozmiar okna kontekstowego</a-c>\n    <a-v>min_count</a-v><a-o>=</a-o><a-n>1</a-n>,      <a-c># ignoruj słowa rzadsze niż N</a-c>\n    <a-v>sg</a-v><a-o>=</a-o><a-n>0</a-n>,             <a-c># 0 = CBOW, 1 = Skip-gram</a-c>\n    <a-v>epochs</a-v><a-o>=</a-o><a-n>200</a-n>,       <a-c># ile przejść po danych</a-c>\n)\n\n<a-f>print</a-f>(<a-v>model</a-v>.<a-pr>wv</a-pr>.<a-pr>most_similar</a-pr>(<a-s>&quot;kot&quot;</a-s>, <a-v>topn</a-v><a-o>=</a-o><a-n>3</a-n>))\n<a-c># [(&#39;pies&#39;, 0.96), (&#39;śpi&#39;, 0.95), (&#39;na&#39;, 0.95)]</a-c>\n\n<a-f>print</a-f>(<a-v>model</a-v>.<a-pr>wv</a-pr>.<a-pr>similarity</a-pr>(<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;pies&quot;</a-s>))       <a-c># ~0.96</a-c>\n<a-f>print</a-f>(<a-v>model</a-v>.<a-pr>wv</a-pr>.<a-pr>similarity</a-pr>(<a-s>&quot;kot&quot;</a-s>, <a-s>&quot;samochód&quot;</a-s>))   <a-c># ~0.83</a-c></code></pre>\n<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>Zauważcie: nasz korpus jest MALUTKI (15 zdań), więc wyniki są dalekie od idealnych — &quot;kot&quot; i &quot;samochód&quot; mają aż 0.83 podobieństwa, co jest bez sensu. Ale model poprawnie stawia &quot;pies&quot; najbliżej &quot;kot&quot;! Na prawdziwych korpusach (miliardy zdań) te wektory stają się bardzo dokładne.</p>\n</div>\n<p>A tak korzystamy z <strong>gotowego modelu</strong> wytrenowanego na miliardach słów:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>gensim</a-v>.<a-v>downloader</a-v> <a-k>import</a-k> <a-v>load</a-v>\n\n<a-v>model</a-v> <a-o>=</a-o> <a-f>load</a-f>(<a-s>&quot;glove-wiki-gigaword-50&quot;</a-s>)\n\n<a-v>result</a-v> <a-o>=</a-o> <a-v>model</a-v>.<a-pr>most_similar</a-pr>(\n    <a-v>positive</a-v><a-o>=</a-o>[<a-s>&quot;king&quot;</a-s>, <a-s>&quot;woman&quot;</a-s>],\n    <a-v>negative</a-v><a-o>=</a-o>[<a-s>&quot;man&quot;</a-s>],\n    <a-v>topn</a-v><a-o>=</a-o><a-n>5</a-n>\n)\n\n<a-k>for</a-k> <a-v>word</a-v>, <a-v>score</a-v> <a-o>in</a-o> <a-v>result</a-v>:\n    <a-f>print</a-f>(<a-s>f&quot;</a-s><a-p>{</a-p><a-v>word</a-v><a-p>}</a-p><a-s>: </a-s><a-p>{</a-p><a-v>score</a-v><a-eb>:.3f</a-eb><a-p>}</a-p><a-s>&quot;</a-s>)\n\n<a-c># queen: 0.852</a-c>\n<a-c># throne: 0.737</a-c>\n<a-c># ...</a-c></code></pre>\n<p>Możecie też sprawdzić podobieństwo między słowami:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-f>print</a-f>(<a-v>model</a-v>.<a-pr>similarity</a-pr>(<a-s>&quot;cat&quot;</a-s>, <a-s>&quot;dog&quot;</a-s>))    <a-c># ~0.92</a-c>\n<a-f>print</a-f>(<a-v>model</a-v>.<a-pr>similarity</a-pr>(<a-s>&quot;cat&quot;</a-s>, <a-s>&quot;car&quot;</a-s>))    <a-c># ~0.15</a-c></code></pre>\n<p>&quot;Cat&quot; i &quot;dog&quot; - podobieństwo 0.92. &quot;Cat&quot; i &quot;car&quot; - 0.15. Model &quot;wie&quot;, że kot jest bliżej psa niż samochodu.</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">Tip</p>\n<p><strong>Eksperyment:</strong> Wejdźcie na <a href=\"https://projector.tensorflow.org/\">TensorFlow Embedding Projector</a> - to interaktywna wizualizacja przestrzeni wektorowej. Możecie wpisywać słowa i widzieć, co jest blisko. To jest JEDNA z najpiękniejszych wizualizacji w całym ML. Serio, sprawdźcie!</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\">Warning</p>\n<p><strong>Uwaga na uprzedzenia:</strong> Word2Vec uczy się z danych. Jeśli w danych treningowych &quot;programista&quot; częściej występuje blisko &quot;mężczyzna&quot; niż &quot;kobieta&quot; - to model to wyłapie. Słynny przykład: <code>wektor(&quot;programista&quot;) - wektor(&quot;mężczyzna&quot;) + wektor(&quot;kobieta&quot;) ≈ &quot;gospodyni domowa&quot;</code>. To jest uprzedzenie ukryte w danych, które model bezrefleksyjnie reprodukuje. Pamiętacie semiosferę z <a href=\"semiotyka-a-llm.html\">drugiego posta</a>? Semiosfera nie jest neutralna - i dane treningowe też nie są.</p>\n</div>\n<h3><a href=\"#od-rzadkich-wektorów-do-gęstych\" aria-hidden=\"true\" class=\"anchor\" id=\"od-rzadkich-wektorów-do-gęstych\"></a>Od rzadkich wektorów do gęstych</h3>\n<p>Zróbmy jeszcze raz porównanie, żeby to sobie utrwalić:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph LR\n    subgraph &quot;BoW / TF-IDF: rzadki wektor&quot;\n        R[&quot;'Kot siedzi na macie'&quot;] --&gt; RV[&quot;[0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, ...]&lt;br/&gt;Długość = rozmiar słownika&lt;br/&gt;Większość to ZERA&quot;]\n    end\n    subgraph &quot;Word2Vec: gęsty wektor&quot;\n        D[&quot;'kot'&quot;] --&gt; DV[&quot;[0.23, -0.45, 0.67, 0.12, -0.89, ...]&lt;br/&gt;Długość = 300&lt;br/&gt;Wszystkie wartości NIEZEROWE&quot;]\n    end\n    \n    style RV fill:#ff9999,color:#000\n    style DV fill:#99ff99,color:#000\n</code></pre>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>BoW / TF-IDF</th>\n<th>Word2Vec</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Typ</strong></td>\n<td>Rzadki (sparse)</td>\n<td>Gęsty (dense)</td>\n</tr>\n<tr>\n<td><strong>Długość wektora</strong></td>\n<td>= rozmiar słownika (może być 100 000+)</td>\n<td>= wymiar embeddingu (zwykle 100-300)</td>\n</tr>\n<tr>\n<td><strong>Wartości</strong></td>\n<td>Głównie zera</td>\n<td>Wszystkie niezerowe</td>\n</tr>\n<tr>\n<td><strong>Podobieństwo</strong></td>\n<td>Trudno uchwycić</td>\n<td>Kosinusowe podobieństwo działa super</td>\n</tr>\n<tr>\n<td><strong>Kontekst</strong></td>\n<td>Brak</td>\n<td>Uchwycony przez okno kontekstowe</td>\n</tr>\n<tr>\n<td><strong>Analogie</strong></td>\n<td>Nie ma</td>\n<td>Działają (król - mężczyzna + kobieta ≈ królowa)</td>\n</tr>\n</tbody>\n</table>\n<h3><a href=\"#czy-word2vec-potrafi-generować-tekst\" aria-hidden=\"true\" class=\"anchor\" id=\"czy-word2vec-potrafi-generować-tekst\"></a>Czy Word2Vec potrafi generować tekst?</h3>\n<p>Nie. Word2Vec tworzy <strong>mapę znaczeń</strong> — mówi ci, że &quot;kot&quot; jest blisko &quot;pies&quot;, ale nie potrafi ułożyć z tego zdania. To jak słownik synonimów: wiesz co jest podobne, ale nie napiszesz wiersza.</p>\n<p>Ale... co jeśli połączymy Word2Vec z łańcuchami Markowa? Łańcuch Markowa wybiera następne słowo na podstawie częstotliwości. A jeśli zamiast losować równie, sprawimy, że słowa bardziej podobne do kontekstu będą miały większą szansę?</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-python\"><a-k>from</a-k> <a-v>gensim</a-v>.<a-v>models</a-v> <a-k>import</a-k> <a-cr>Word2Vec</a-cr>\n<a-k>import</a-k> <a-v>numpy</a-v> <a-k>as</a-k> <a-v>np</a-v>\n<a-k>from</a-k> <a-v>collections</a-v> <a-k>import</a-k> <a-v>defaultdict</a-v>\n<a-k>import</a-k> <a-v>random</a-v>\n\n<a-v>corpus</a-v> <a-o>=</a-o> [\n    <a-s>&quot;kot siedzi na macie i śpi&quot;</a-s>,\n    <a-s>&quot;pies siedzi na dywanie i śpi&quot;</a-s>,\n    <a-s>&quot;kot biega po pokoju&quot;</a-s>,\n    <a-s>&quot;pies biega po parku&quot;</a-s>,\n    <a-s>&quot;kot i pies bawią się w ogrodzie&quot;</a-s>,\n    <a-s>&quot;kot je karmę z miski&quot;</a-s>,\n    <a-s>&quot;pies je karmę z miski&quot;</a-s>,\n    <a-s>&quot;kot łapie mysz w domu&quot;</a-s>,\n    <a-s>&quot;pies goni kota po ogrodzie&quot;</a-s>,\n    <a-s>&quot;ptak siedzi na gałęzi drzewa&quot;</a-s>,\n    <a-s>&quot;kot patrzy przez okno i mruczy&quot;</a-s>,\n    <a-s>&quot;pies szczeka na listonosza&quot;</a-s>,\n    <a-s>&quot;kot śpi cały dzień na kanapie&quot;</a-s>,\n]\n\n<a-v>tokenized</a-v> <a-o>=</a-o> [<a-v>sentence</a-v>.<a-pr>split</a-pr>() <a-k>for</a-k> <a-v>sentence</a-v> <a-o>in</a-o> <a-v>corpus</a-v>]\n\n<a-v>w2v_model</a-v> <a-o>=</a-o> <a-f>Word2Vec</a-f>(<a-v>sentences</a-v><a-o>=</a-o><a-v>tokenized</a-v>, <a-v>vector_size</a-v><a-o>=</a-o><a-n>10</a-n>, <a-v>window</a-v><a-o>=</a-o><a-n>3</a-n>, <a-v>min_count</a-v><a-o>=</a-o><a-n>1</a-n>, <a-v>epochs</a-v><a-o>=</a-o><a-n>200</a-n>)\n\n<a-c># budujemy macierz przejść (jak w łańcuchach Markowa)</a-c>\n<a-v>transitions</a-v> <a-o>=</a-o> <a-f>defaultdict</a-f>(<a-v>list</a-v>)\n<a-k>for</a-k> <a-v>sentence</a-v> <a-o>in</a-o> <a-v>tokenized</a-v>:\n    <a-k>for</a-k> <a-v>i</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-f>len</a-f>(<a-v>sentence</a-v>) <a-o>-</a-o> <a-n>1</a-n>):\n        <a-v>transitions</a-v>[<a-v>sentence</a-v>[<a-v>i</a-v>]].<a-pr>append</a-pr>(<a-v>sentence</a-v>[<a-v>i</a-v> <a-o>+</a-o> <a-n>1</a-n>])\n\n<a-k>def</a-k> <a-f>generate</a-f>(<a-v>start</a-v>, <a-v>length</a-v><a-o>=</a-o><a-n>6</a-n>):\n    <a-v>words</a-v> <a-o>=</a-o> [<a-v>start</a-v>]\n    <a-k>for</a-k> <a-v>_</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-v>length</a-v>):\n        <a-v>current</a-v> <a-o>=</a-o> <a-v>words</a-v>[<a-o>-</a-o><a-n>1</a-n>]\n        <a-k>if</a-k> <a-v>current</a-v> <a-o>not in</a-o> <a-v>transitions</a-v>:\n            <a-k>break</a-k>\n\n        <a-v>candidates</a-v> <a-o>=</a-o> <a-v>transitions</a-v>[<a-v>current</a-v>]\n\n        <a-c># uśredniony wektor ostatnich słów = kontekst</a-c>\n        <a-v>context_vector</a-v> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>mean</a-pr>(\n            [<a-v>w2v_model</a-v>.<a-pr>wv</a-pr>[<a-v>w</a-v>] <a-k>for</a-k> <a-v>w</a-v> <a-o>in</a-o> <a-v>words</a-v>[<a-o>-</a-o><a-n>3</a-n>:] <a-k>if</a-k> <a-v>w</a-v> <a-o>in</a-o> <a-v>w2v_model</a-v>.<a-pr>wv</a-pr>],\n            <a-v>axis</a-v><a-o>=</a-o><a-n>0</a-n>,\n        )\n\n        <a-c># punktujemy kandydatów za podobieństwo do kontekstu</a-c>\n        <a-v>scored</a-v> <a-o>=</a-o> []\n        <a-k>for</a-k> <a-v>candidate</a-v> <a-o>in</a-o> <a-v>candidates</a-v>:\n            <a-k>if</a-k> <a-v>candidate</a-v> <a-o>in</a-o> <a-v>w2v_model</a-v>.<a-pr>wv</a-pr>:\n                <a-v>similarity</a-v> <a-o>=</a-o> <a-v>np</a-v>.<a-pr>dot</a-pr>(<a-v>context_vector</a-v>, <a-v>w2v_model</a-v>.<a-pr>wv</a-pr>[<a-v>candidate</a-v>]) <a-o>/</a-o> (\n                    <a-v>np</a-v>.<a-pr>linalg</a-pr>.<a-pr>norm</a-pr>(<a-v>context_vector</a-v>) <a-o>*</a-o> <a-v>np</a-v>.<a-pr>linalg</a-pr>.<a-pr>norm</a-pr>(<a-v>w2v_model</a-v>.<a-pr>wv</a-pr>[<a-v>candidate</a-v>]) <a-o>+</a-o> <a-n>1e-8</a-n>\n                )\n                <a-v>scored</a-v>.<a-pr>append</a-pr>((<a-v>candidate</a-v>, <a-v>similarity</a-v>))\n\n        <a-k>if</a-k> <a-v>scored</a-v>:\n            <a-c># losujemy, ale z wagami — podobne słowa mają większą szansę</a-c>\n            <a-v>weights</a-v> <a-o>=</a-o> [<a-v>score</a-v> <a-o>+</a-o> <a-n>1</a-n> <a-k>for</a-k> <a-v>_</a-v>, <a-v>score</a-v> <a-o>in</a-o> <a-v>scored</a-v>]\n            <a-v>chosen</a-v> <a-o>=</a-o> <a-v>random</a-v>.<a-pr>choices</a-pr>([<a-v>w</a-v> <a-k>for</a-k> <a-v>w</a-v>, <a-v>_</a-v> <a-o>in</a-o> <a-v>scored</a-v>], <a-v>weights</a-v><a-o>=</a-o><a-v>weights</a-v>, <a-v>k</a-v><a-o>=</a-o><a-n>1</a-n>)[<a-n>0</a-n>]\n            <a-v>words</a-v>.<a-pr>append</a-pr>(<a-v>chosen</a-v>)\n        <a-k>else</a-k>:\n            <a-v>words</a-v>.<a-pr>append</a-pr>(<a-v>random</a-v>.<a-pr>choice</a-pr>(<a-v>candidates</a-v>))\n    <a-k>return</a-k> <a-s>&quot; &quot;</a-s>.<a-pr>join</a-pr>(<a-v>words</a-v>)\n\n<a-k>for</a-k> <a-v>_</a-v> <a-o>in</a-o> <a-f>range</a-f>(<a-n>5</a-n>):\n    <a-f>print</a-f>(<a-f>generate</a-f>(<a-s>&quot;kot&quot;</a-s>))</code></pre>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">kot siedzi na macie i śpi\nkot patrzy przez okno i mruczy\nkot je karmę z miski\nkot śpi cały dzień na kanapie\nkot łapie mysz w domu\n</code></pre>\n<p>Nie jest to Szekspir, ale teksty są bardziej spójne niż czysty losowy Markow. Idea jest prosta:</p>\n<ol>\n<li>Łańcuch Markowa daje <strong>kandydatów</strong> na następne słowo</li>\n<li>Word2Vec <strong>punktuje</strong> kandydatów na podstawie podobieństwa do kontekstu</li>\n<li>Wybieramy losowo, ale z <strong>obciążeniem</strong> — słowa bardziej pasujące mają większą szansę</li>\n</ol>\n<p>To jest malutki krok w kierunku tego, co robią dzisiejsze LLM-y. One też przewidują następne słowo, ale zamiast prostej macierzy przejścia mają wielowarstwowe sieci Transformer z mechanizmem uwagi. Tam &quot;punktowanie kandydatów&quot; jest o wiele, wiele bardziej zaawansowane.</p>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p><strong>Kluczowa różnica:</strong> Word2Vec daje każdemu słowu JEDEN wektor. Ale &quot;zamek&quot; w &quot;zamek w drzwiach&quot; i &quot;zamek na wzgórzu&quot; to zupełnie inne znaczenia! Word2Vec nie rozróżnia — dlatego w kolejnych wpisach wejdziemy w Transformer-y, gdzie <strong>kontekst zmienia znaczenie</strong> każdego słowa.</p>\n</div>\n<hr />\n<h2><a href=\"#podsumowanie---cała-droga-w-jednym-miejscu\" aria-hidden=\"true\" class=\"anchor\" id=\"podsumowanie---cała-droga-w-jednym-miejscu\"></a>Podsumowanie - cała droga w jednym miejscu</h2>\n<p>Oto nasza mapa drogowa, od cięcia tekstu do geometrii znaczeń:</p>\n<table>\n<thead>\n<tr>\n<th>Metoda</th>\n<th>Co robi</th>\n<th>Kontekst?</th>\n<th>Czego nie umie</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Tokenizacja (BPE)</strong></td>\n<td>Tnie tekst na kawałki</td>\n<td>Nie</td>\n<td>Nie rozumie, co tnie</td>\n</tr>\n<tr>\n<td><strong>BoW / TF-IDF</strong></td>\n<td>Liczy słowa, waży rzadkością</td>\n<td>Nie</td>\n<td>Ignoruje kolejność</td>\n</tr>\n<tr>\n<td><strong>N-gramy</strong></td>\n<td>Patrzy na sekwencje słów</td>\n<td>Lokalny (2-3 słowa)</td>\n<td>Krótki kontekst, szybko rosnący słownik</td>\n</tr>\n<tr>\n<td><strong>Naive Bayes</strong></td>\n<td>Klasyfikuje na podstawie prawdopodobieństwa</td>\n<td>Nie (słowa &quot;niezależne&quot;)</td>\n<td>Nie łapie zależności między słowami</td>\n</tr>\n<tr>\n<td><strong>Word2Vec</strong></td>\n<td>Zamienia słowa w wektory znaczeniowe</td>\n<td>Tak (okno kontekstowe)</td>\n<td>Jedno słowo = jeden wektor (ignoruje wieloznaczność)</td>\n</tr>\n</tbody>\n</table>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-mermaid\">graph TD\n    subgraph &quot;Ewolucja: od liczenia do rozumienia&quot;\n        T[&quot;✂️ Tokenizacja&lt;br/&gt;&lt;i&gt;tekst → kawałki&lt;/i&gt;&quot;] --&gt;|&quot;jak policzyć?&quot;| B[&quot;🔢 BoW / TF-IDF&lt;br/&gt;&lt;i&gt;kawałki → liczby&lt;/i&gt;&quot;]\n        B --&gt;|&quot;brak kontekstu!&quot;| N[&quot;🔗 N-gramy / Markow&lt;br/&gt;&lt;i&gt;dodajemy kolejność&lt;/i&gt;&quot;]\n        N --&gt;|&quot;chcemy klasyfikować!&quot;| NB[&quot;📊 Naive Bayes&lt;br/&gt;&lt;i&gt;prawdopodobieństwo klas&lt;/i&gt;&quot;]\n        NB --&gt;|&quot;słowa to nie liczby!&quot;| W[&quot;📐 Word2Vec&lt;br/&gt;&lt;i&gt;geometria znaczeń&lt;/i&gt;&quot;]\n        W --&gt;|&quot;co dalej?&quot;| LLM[&quot;🤖 Transformers / LLM&lt;br/&gt;&lt;i&gt;kontekst + attention + skala&lt;/i&gt;&quot;]\n    end\n    \n    style T fill:#ff9999,color:#000\n    style B fill:#ffcc99,color:#000\n    style N fill:#ffff99,color:#000\n    style NB fill:#ccff99,color:#000\n    style W fill:#99ccff,color:#000\n    style LLM fill:#9999ff,color:#fff\n</code></pre>\n<p>I kluczowa perspektywa: <strong>z każdej z tych metod LLM wziął coś dla siebie.</strong></p>\n<ul>\n<li><strong>Tokenizacja (BPE)</strong> to pierwszy krok pipeline'u każdego LLM. ChatGPT tnie wasz tekst na tokeny ZANIM cokolwiek z nim zrobi.</li>\n<li><strong>TF-IDF i BoW</strong> to fundamenty myślenia o tekście jako o liczbach. Bez tego pomysłu, że słowa można &quot;policzyć&quot;, nie byłoby uczenia reprezentacji (representation learning).</li>\n<li><strong>N-gramy i łańcuchy Markowa</strong> to pierwowzór przewidywania następnego tokena. To jest dokładnie to, co robi LLM - tylko że LLM ma kontekst na tysiące tokenów, nie na 2-3.</li>\n<li><strong>Naive Bayes</strong> pokazał, że probabilistyczne podejście do tekstu działa zaskakująco dobrze. LLM też jest modelem probabilistycznym - przewiduje prawdopodobieństwo następnego tokena.</li>\n<li><strong>Word2Vec</strong> to przodek tego, co dziś nazywamy &quot;embedding layer&quot; w Transformerach. GPT nie używa już Word2Vec jako osobnego kroku, ale jego warstwa embeddingowa realizuje tę samą ideę: token → wektor.</li>\n</ul>\n<p><strong>LLM nie spadł z nieba. Stoi na barkach gigantów.</strong></p>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\">Important</p>\n<p><strong>Co w następnym wpisie?</strong> Wejdziemy w to, co dzieje się, gdy te wszystkie pomysły połączymy z mechanizmem <strong>uwagi (attention)</strong> i sieciami <strong>Transformer</strong>. Bo od Word2Vec (2013) do &quot;Attention Is All You Need&quot; (2017) do ChatGPT (2022) jest &quot;tylko&quot; kilkanaście lat i kilka przełomowych pomysłów ;-)</p>\n</div>\n<hr />\n<p>Wiem, że tego posta jest dużo, ale chciałem, żeby ta droga od &quot;tnięcia tekstu&quot; do &quot;wektorów znaczeń&quot; była pełna i zrozumiała.</p>\n<p>Mam nadzieję, że chociaż kilka razy powiecie &quot;aha!&quot;. Jeśli coś jest niejasne - <strong>napiszcie w komentarzach</strong>, postaram się wyjaśnić. A jeśli macie lepsze przykłady analogii Word2Vec - tym bardziej dajcie znać.</p>\n<p>Która metoda was najbardziej zaskoczyła? Czy wiedzieliście, że ChatGPT &quot;myśli&quot; w tokenach, nie w słowach? I że jego &quot;myślenie&quot; to tak naprawdę potomek łańcuchów Markowa?</p>\n<p>Do następnego!</p>\n<hr />\n<p><strong>Źródła i ciekawe linki:</strong></p>\n<p>Jeśli chcecie wejść głębiej, oto materiały, z których korzystałem:</p>\n<ul>\n<li><a href=\"https://huggingface.co/learn/llm-course/chapter6/5\">Byte-Pair Encoding tokenization — Hugging Face</a> - świetne, krokowe wprowadzenie do BPE z kodem</li>\n<li><a href=\"https://huggingface.co/docs/transformers/en/tokenizer_summary\">Tokenization algorithms — Hugging Face</a> - przegląd BPE, WordPiece, Unigram, SentencePiece</li>\n<li><a href=\"https://sebastianraschka.com/blog/2025/bpe-from-scratch.html\">BPE Tokenizer From Scratch — Sebastian Raschka</a> - implementacja BPE od zera w Python, bardzo edukacyjna</li>\n<li><a href=\"https://codesignal.com/learn/courses/foundations-of-nlp-data-processing-2/lessons/introduction-to-tf-idf-vectorization-in-nlp\">Introduction to TF-IDF Vectorization in NLP — CodeSignal</a> - czytelne wprowadzenie do TF-IDF z przykładami</li>\n<li><a href=\"https://stackabuse.com/python-for-nlp-developing-an-automatic-text-filler-using-n-grams/\">Python for NLP: Developing an Automatic Text Filler using N-grams — StackAbuse</a> - super artykuł o n-gramach z generatorem tekstu</li>\n<li><a href=\"https://medium.com/in-pursuit-of-artificial-intelligence/brief-introduction-to-n-gram-and-tf-idf-tokenization-e58d22555bab\">Brief Introduction to N-gram and TF-IDF — Medium</a> - porównanie TF-IDF vs n-gramy z kodem</li>\n<li><a href=\"https://www.baeldung.com/cs/word-embeddings-cbow-vs-skip-gram\">Word Embeddings: CBOW vs Skip-Gram — Baeldung</a> - klarowne porównanie obu architektur Word2Vec</li>\n<li><a href=\"https://medium.com/data-science/nlp-101-word2vec-skip-gram-and-cbow-93512ee24314\">NLP 101: Word2Vec — Skip-gram and CBOW — Medium</a> - dobre obrazki i intuicja dla Word2Vec</li>\n<li><a href=\"https://aclanthology.org/2020.aespen-1.6/\">TF-IDF Character N-grams versus Word Embedding-based Models — ACL Anthology</a> - kontrast między klasycznymi cechami a embeddingami</li>\n<li><a href=\"https://projector.tensorflow.org/\">TensorFlow Embedding Projector</a> - interaktywna wizualizacja przestrzeni wektorowej (obowiązkowe!)</li>\n</ul>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>Odpowiedzi do quizu tokenizacyjnego: 1) &quot;klasa&quot; → [&quot;k&quot;, &quot;las&quot;, &quot;a&quot;] (l+a→la, la+s→las, ale &quot;k&quot; nie pasuje do żadnego scalenia) 2) &quot;lata&quot; → [&quot;lat&quot;, &quot;a&quot;] (l+a→la, la+t→lat) 3) &quot;laska&quot; → [&quot;las&quot;, &quot;k&quot;, &quot;a&quot;] (l+a→la, la+s→las, ale &quot;las&quot;+&quot;k&quot; nie ma w scaleniach, więc zostaje podzielone). <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n</ol>\n</section>\n",
      "summary": "",
      "date_published": "2026-06-09T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Błażej Gruszka",
          "url": "https://www.linkedin.com/in/blazejgruszka/",
          "avatar": "https://github.com/bgruszka.png"
        }
      ],
      "tags": [
        "llm",
        "ai",
        "nlp",
        "tokenizacja",
        "word2vec",
        "embeddings",
        "tf-idf",
        "markow",
        "bayes",
        "language-models"
      ],
      "language": "pl"
    }
  ]
}