---
title: "Невронните мрежи с прости думи"
url: "https://romankryvolapov.com/bg/neural-networks-basics/"
description: "Как е устроен LLM: токенизация, embedding, attention в трансформъра, квантуване и семплери, които определят избора на следващия токен."
language: bg
published: 2025-05-07
updated: 2026-11-13
---
<span id="0787"></span>

**Здравейте!**

<span id="c7e3"></span>

В интернет могат да се намерят различни обяснения как работят невронните мрежи, но тези, които са ми попадали, бяха или прекалено специфични и насочени към специалисти, или прекалено опростени.

<span id="2497"></span>

Опитах се да напиша свои обяснения, които да не са прекалено опростени, но при това да остават по възможност разбираеми.

<span id="8d9e"></span>

Статията е на 10 процента компилирана от други статии, на 30 процента – от множество диалози с различни LLM, и на 60 процента „написана на ръка“ въз основа на статии и отговори.

## Входни данни

На входа невронната мрежа получава входни данни във вид на заявка на потребителя. Към заявката на потребителя се добавя и обща информация, която позволява да се получи по-точен отговор. Ако невронната мрежа поддържа RAG (Retrieval-Augmented Generation), към заявката се добавят също:

- Данни от векторната база, ако в нея предварително е добавено онова, което е нужно за генериране на по-актуален отговор

- Данни, извлечени от интернет страница или документ – за да се намали размерът им, често се използва специализирана embedding невронна мрежа: тя намира всички подходящи късове текст и ги добавя към контекста на заявката

Пример за настройки на embedding невронна мрежа за Chrome разширението Page Assist

![](/images/2019/02/image-1-1024x649.png)

## Как работи RAG

Системата се състои от два основни компонента

**Retrieval (извличане):**\
Търси релевантна информация във външна база знания (най-често – векторна база, например FAISS, Qdrant, Weaviate). Тази база обикновено е построена предварително от текстови документи (pdf, markdown, html и др.), преобразувани в ембединги с помощта на модел (например BERT, Instructor или SentenceTransformer).

**Generation (генериране):**\
LLM (GPT, LLaMA, Mistral и др.) получава изходната заявка + намерените документи и генерира крайния отговор.

Не бъркайте двата различни „embedding“ в тази статия: тук става дума за отделен embedding модел, който превръща цял фрагмент от текст в един вектор, за да се търсят сходни фрагменти; по-нататък, в стъпка 2, – за token embedding, векторите на отделните токени вътре в самия LLM. Механиката е сходна, но това са различни модели и различни вектори.

**За RAG и function calling имам отделна статия:**

[/bg/rag-and-function-calling/](/bg/rag-and-function-calling/)

**И отделна – за векторните бази:**

[/bg/vector-databases/](/bg/vector-databases/)

В някои невронни мрежи за отделяне на заявката на потребителя, контекста, получен с помощта на RAG, и общото съобщение се използва маркиране.

**Пример:** 

**context:**\
това са данни, получени от интернет с помощта на embedding невронна мрежа или от векторно хранилище и подредени по съвпадение на score в низходящ ред

```json
{
  "context": [
    "Пеницилинът е първият открит антибиотик, произведен от плесенна гъба от рода Penicillium. Използва се за лечение на бактериални инфекции като ангина, сифилис и пневмония. Пеницилинът разрушава клетъчната стена на бактериите, което води до тяхната гибел.",
    "Някои хора имат алергия към пеницилин. Това може да предизвика сериозни реакции, включително анафилаксия, затова преди предписването на препарата е важно да се уточни дали пациентът има алергия."
  ],
  "instructions": "Отговори с прости думи, като използваш само информацията от контекста. Ако отговор няма – напиши 'Информацията не е намерена'.",
  "question": "Какво е пеницилин и за какво се прилага?"
}
```

## От какво се състои моделът

### Трансформър (Transformer)

<span id="bd99"></span>

Архитектура на невронна мрежа, която е станала основа на съвременните езикови модели като ChatGPT, BERT, LLaMA, Gemma и много други.\
Тя е описана за първи път в научна статия от 2017 г. със заглавие „Attention is All You Need“.\
Казано по-просто, трансформърът е „умна машина“, която умее да чете и разбира текст, като обработва всички думи едновременно, а не по една, както са правили предишните модели (например RNN, LSTM).

### Слоеве на трансформъра (Transformer Layers)

<span id="1b47"></span>

това са повтарящи се блокове, през които минава всеки токен вътре в модела.

<span id="e0b5"></span>

**Един слой (или „трансформър блок“) включва:**

<span id="b861"></span>

**Self-Attention:**\
токенът „гледа“ към другите токени и решава кои са важни.

<span id="aad9"></span>

**Feed-Forward Network:**\
уточняване и преобразуване на всеки токен.

<span id="d353"></span>

**Layer Normalization или LayerNorm:**\
стабилизиране на изчисленията.

<span id="1648"></span>

**Residual Connections:**\
за да не „забрави“ моделът началната информация.

<span id="2156"></span>

LayerNorm може да е преди или след Residual (Pre-LN vs Post-LN). Почти всички съвременни LLM (Llama, Qwen, Gemma, DeepSeek) използват Pre-LN, а вместо класическия LayerNorm – неговия опростен вариант RMSNorm. Стъпките по-нататък в статията са описани по класическата схема Post-LN, така е по-нагледно.

<span id="b165"></span>

и други етапи.

<span id="5f62"></span>

Всеки слой на трансформъра (Transformer Block) е отделен набор от параметри, които:\
се обучават независимо,\
се прилагат независимо към входните данни,\
дават все по-„дълбоко“ разбиране на смисъла и контекста.

<span id="085b"></span>

**Какво означава „32 слоя“:**\
всеки токен минава през 32 такива операции, последователно една след друга; след всеки слой токенът става все по-информиран, т.е. представянето му (векторът) отразява все по-дълбоко контекста

<span id="61ef"></span>

**Брой слоеве на популярните отворени модели (transformer layers, по официалните конфигурации, средата на 2026 г.):**

```text
| Модел                     | Брой слоеве      |
|---------------------------|------------------|
| Llama 3.1 (8B)            | 32               |
| Llama 3.1 (70B)           | 80               |
| Llama 4 Scout (109B)      | 48               |
| Gemma 3 (12B)             | 48               |
| Gemma 3 (27B)             | 62               |
| Gemma 4 (31B)             | 60               |
| Qwen 3 (8B)               | 36               |
| Qwen 3 (32B)              | 64               |
| Qwen 3 (235B-A22B)        | 94               |
| Qwen 3.5 (9B)             | 32               |
| DeepSeek V3 / R1 / V4 Pro | 61               |
| DeepSeek V4 Flash         | 43               |
| GPT-OSS (20B)             | 24               |
| GPT-OSS (120B)            | 36               |
```

При MoE-моделите (Mixture of Experts – Llama 4, Qwen 3 235B-A22B, DeepSeek V3/V4, GPT-OSS) във всеки слой вместо един FFN блок има набор от „експерти“, от които за всеки токен се активира само малка част: при Qwen 3 235B-A22B от 235 милиарда параметъра за всеки токен работят само 22 милиарда.

### Активация (Activation)

<span id="83d1"></span>

това са междинните изходи на невронната мрежа след прилагането на функции и слоеве.\
Може да се каже, че активациите са данни, които „живеят вътре в мрежата“ на всеки етап от преминаването на входа през модела.

### Тензор (Tensor)

<span id="a64d"></span>

в невронната мрежа е многомерен масив от числа, с който работят слоевете на модела.\
**Входни данни:**\
Текст → токени → ембединги → тензор batch_size × seq_len × embedding_dim.\
**Тегла на модела:**\
Матриците с тегла в слоевете също са тензори.\
**Междинни представяния (активации):**\
Изходът на всеки слой (например LayerNorm, Attention) е тензор.\
**Градиенти:**\
При обучението моделът изчислява градиенти (тензори) за обновяване на теглата.

## Стъпки в работата на невронната мрежа

## Стъпка 1: Токенизация (от текст / изображение към токени)

### Токенизация на текст

<span id="c30b"></span>

LLM не работи директно с думи – работи с токени (части от думи или символи), превърнати в числа.

<span id="6d3e"></span>

**Пример:**

```text
Текст на потребителя:
"Здравей, свят"

ID на токена в речника на модела за думата:
"Здравей" = 1123
"," = 15
"свят" = 345

Получаваме масив от ID на токени:
["Здравей", ",", "свят"] = [1123, 15, 345]
```

<span id="98bc"></span>

**Как се постига това:**\
Използва се алгоритъм като Byte Pair Encoding (BPE), Unigram, WordPiece или SentencePiece.\
BPE токенизаторът намира най-честите двойки символи.\
WordPiece строи токени въз основа на вероятността на йерархични разбивания.\
Често токените не са отделни думи, а части от думи.

В речника има специални маркери за границите на думите: в BPE речниците интервалът се кодира в началото на токена, а в WordPiece с маркера „##“ се означават токените-продължения на думата. Затова нито един субтокен не пресича границата между две думи.\
Самият разбор на низа зависи от алгоритъма: WordPiece върви „алчно“ – взема възможно най-дългия токен, който съвпада с началото на оставащия низ, BPE последователно прилага научените правила за сливане на двойки, а Unigram избира най-вероятното разбиване изцяло.

<span id="3e58"></span>

**Пример:**

```text
Текст на потребителя:
"невероятно"

ID на токена в речника на модела за части от думи:
"не" = 24
"вероят" = 126
"но" = 36
Получаваме масив от ID на токени:
["не", "вероят", "но"] = [24, 126, 36]

В WordPiece речник същата дума би могла да се раздели като:
("не", "##вероятно")
където "##" означава продължение на думата
```

**Различни по смисъл думи могат да имат общи токени, например „приказка“ и „пример“ могат да имат общ токен „при“:**\
Макар че „при“ се повтаря и в двете думи, моделът не гледа само този къс, а и съседните токени, и цялата фраза (за това – по-нататък в статията).\
Първо „при“ се превръща във вектор – просто набор от числа, които описват тази част от думата.\
След това трансформърът (многослойна мрежа) смесва този вектор с векторите на съседните токени („казка“ или „мер“) и добавя информация за позицията в изречението.\
В резултат още в първия слой „при“ в „приказка“ вече се различава от „при“ в „пример“, защото там е „примесен“ различен контекст.\
Тоест общото късче „при“ само по себе си е неутрално, а смисълът се оформя по-нататък през слоевете въз основа на обкръжението.

<span id="84d2"></span>

**Защо токени, а не думи:**\
По-малък речник = икономия на памет.\
По-добре се обработват редките и съставните думи.\
Позволява на модела да „се учи“ да разбира структурата на думите.

<span id="be78"></span>

Размерът на речника, или „vocab_size“, определя колко уникални токена може да обработва моделът.\
По-голям речник = по-малко разбивания на думи на части, но по-голям обем на embedding слоя.

<span id="b327"></span>

Думата „програмиране“ може да бъде изцяло един токен в модел с голям речник,\
а може да се раздели на части („пр“, „ограм“, „иране“) в модел с по-малък речник.

Практичен нюанс за читателите на този блог: кирилицата се токенизира „по-скъпо“ от латиницата. Токенизаторите са обучени основно върху английски, затова българската, руската или украинската дума по-често се разбива на по-голям брой токени, отколкото английската със същата дължина. Един и същ текст на български заема значително повече токени: контекстът свършва по-бързо, а в платените API това буквално е по-скъпо.

<span id="8788"></span>

**Размери на речника на популярните отворени модели (по официалните конфигурации, средата на 2026 г.):**

```text
Модел                     | Размер на речника |
--------------------------|----------------|
Llama 2 (2023)            | 32 000         |
Llama 3.1                 | 128 256        |
Llama 4                   | 202 048        |
Gemma 2                   | 256 128        |
Gemma 3 / Gemma 4         | 262 144        |
Qwen 3                    | 151 936        |
Qwen 3.5                  | 248 320        |
DeepSeek V3 / R1 / V4     | 129 280        |
GPT-OSS (20B / 120B)      | 201 088        |
```

Добре се вижда как речникът расте от поколение на поколение. При затворените модели (GPT, Claude, Gemini) речникът и останалите детайли на архитектурата не се публикуват.

### Специални токени и chat-шаблон

Освен токените на обикновения текст в речника има служебни токени: начало и край на последователността, граници на репликите, при новите модели – маркери за извикване на инструменти и блокове с разсъждения. Моделът ги е виждал при дообучаването и се опира на тях, за да разбира къде чия реплика е и кога е време да спре.

Затова диалогът преди токенизацията се обвива в chat-шаблон – възприетата за конкретния модел маркировка с роли „система“, „потребител“ и „асистент“. Опростено изглежда така:

```text
<|system|>Ти си полезен асистент.<|end|>
<|user|>Какво е пеницилин?<|end|>
<|assistant|>
```

Моделът просто продължава текста след маркера на асистента, а завършва генерирането със свой служебен токен за край на репликата.

Всяко семейство модели има свой шаблон и объркването му е най-честият начин да „счупите“ локално стартиране: моделът започва да отговаря вместо потребителя, не спира навреме или сипе служебни токени направо в текста. Готовите бекенди като llama.cpp и Ollama обикновено вземат правилния шаблон автоматично от метаданните на GGUF файла.

### Токенизация на изображения

За разлика от текстовата токенизация (където токените са думи, субдуми, символи), при изображенията токените са фрагменти от изображението или представяния на признаци. По-долу са разгледани основните подходи.

За изображенията вместо токени се получава веднага матрица (или тензор) от пиксели.

В класическите конволюционни мрежи (CNN) малки участъци от картинката (например 3×3 или 5×5 пиксела) се плъзгат по изображението и за всеки участък конволюцията с набор от филтри дава вектор от признаци. Тези вектори се събират в карти на признаците и се предават нататък.

В съвременните трансформъри за изображения (Vision Transformer) картинката се разбива на „патчове“ (квадрати, да речем, 16×16 пиксела), всеки патч се изравнява във вектор и също се проектира през матрица на ембедингите във вектор-представяне, както токен в NLP.

**Основни начини за токенизация на изображения:**

**Patch Embedding (разбиване на патчове) – класическият подход на ViT:**\
Изображението се разделя на мрежа от квадратни патчове, например 16×16 пиксела.\
Всеки патч се разгъва във вектор (flatten), после линейно се проектира в ембединг с фиксирана размерност (например 768).\
В резултат се получава последователност от токени: един токен на патч.

**Пример:**\
224×224 RGB изображение с патчове 16×16 → 14×14 = 196 токена + \[CLS\] токен.\
Всеки токен: вектор с размер 768.

**CNN Feature Maps като токени:**\
Използват се конволюционни мрежи (ResNet, ConvNext) за извличане на признаци.\
Пространствените признаци (feature map) на изхода могат да се интерпретират като токени, където всеки елемент на мрежата е вектор.\
Прилага се във ResNet-вариантите на CLIP и други хибридни модели; непрекъснатите признаци на визуалния енкодер използват и мултимодални LLM като LLaVA.

**VQ-VAE / VQ-GAN токенизация (дискретна):**\
Кодиращият модул (енкодерът) преобразува изображението в карта на признаците и след това я квантува в дискретни токени (индекси от речник).\
Всеки токен е индекс в речник от визуални патчове.\
Използва се в първата версия на DALL·E, Parti, Chameleon и други генеративни модели.\
Плюсове: моделът работи с „думи“ на визуалния език.\
Минуси: загуба на точност, нестабилна генерация.

**Segment/Region-based токени (DETR, Region Attention):**\
Изображението се разбива на смислови региони (сегментация, обекти).\
Всеки регион се преобразува в токен чрез агрегиране на признаци.\
Използва се в задачи за детекция на обекти и визуални отговори на въпроси (VQA).

**Patch + Positional Encoding:**\
Както в NLP, към всеки патч се добавя позиционна информация (абсолютна, относителна или learnable), за да се запази пространствената структура на изображението.

### Максимална дължина на контекста

Контекстът е оперативната памет на модела, а размерът на контекста е максималният брой токени (думи, символи или части от тях), които езиковият модел може да обработи за една заявка.\
Това е „обемът памет“, който моделът може да вижда едновременно, за да формира отговор. Всичко, което излиза извън този прозорец, моделът забравя или не вижда директно.

Размерът на контекста влияе пряко върху способността на модела да помни предишните съобщения в диалога.

Моделът няма вградена дългосрочна памет – той не „помни“ вас като човек. Той просто обработва целия предишен диалог като входен текст (токени), който се предава при всяка заявка. Това се нарича контекст.

**Максимална дължина на контекста на популярните отворени модели (по официалните конфигурации, средата на 2026 г.):**

```text
| Модел                   | Максимален контекст          |
|-------------------------|------------------------------|
| Llama 3.1               | 131 072                      |
| Llama 4 Scout           | 10 000 000                   |
| Gemma 3                 | 131 072                      |
| Gemma 4                 | 262 144                      |
| Qwen 3                  | 40 960 (131 072 с YaRN)      |
| Qwen 3.5                | 262 144                      |
| DeepSeek V3.1 / R1      | 131 072                      |
| DeepSeek V4             | 1 048 576                    |
| GPT-OSS                 | 131 072                      |
```

При затворените модели контекстът е от същия порядък: например Gemini 2.5 Pro държи около милион токена.

### Защо дългият контекст е скъп

Цената на контекста е двойна. Първо, вниманието сравнява всеки токен с всеки: при удвояване на дължината на последователността обемът на изчисленията расте приблизително четири пъти. Второ, за всеки обработен токен моделът съхранява неговите K- и V-вектори във всички слоеве (KV-кеш, за него – в стъпката за итерацията на генерирането) – при дълги контексти този кеш заема гигабайти и може да изяжда повече памет от самите тегла.

Затова около дългия контекст има толкова инженерни трикове: Gemma и GPT-OSS редуват слоеве с пълно внимание със слоеве с „плъзгащ се прозорец“, които гледат само най-близката хилядарка-две токени, а DeepSeek свива ключовете и стойностите в компактен латентен вектор (MLA). Именно такива похвати правят практически възможен контекст от стотици хиляди и милиони токени.

## Стъпка 2: Token embedding (от токен към вектор)

<span id="c9d7"></span>

В LLM модела за всеки ID на токен се съхранява Token Embedding, или масив от числа. Тези числа описват какво означава този токен, сякаш превеждате думата в математическа форма.

<span id="c825"></span>

Първоначално token embedding има фиксирана стойност за всеки токен, но после се уточнява, докато минава през слоевете.

<span id="e149"></span>

Когато Token Embedding премине през няколко слоя на трансформъра, той става контекстуализиран: отчита значението на цялата фраза. На изхода получаваме вектор, който съдържа „смисъла“ на думата в контекста.

Връзките между векторите се задават от матриците с тегла между слоевете.

Първоначално тези тегла се инициализират случайно или по специално правило (например Xavier или He), а после по време на обучението за всяко тегло се изчислява градиентът на загубата (доколко грешката расте или намалява при промяната му).

Алгоритъмът за обратно разпространение („backpropagation“) изчислява как промяната на всяко тегло ще повлияе на крайната грешка, а след това чрез градиентно спускане (или негови вариации) коригира тези тегла.

Така постепенно се усилват връзките между онези компоненти на векторите, които заедно дават полезна информация, и отслабват всички ненужни – получава се набор от взаимосвързани вектори, оптимални за решаване на задачата.

### Размерност на вектора (Embedding Size, d_model) на всеки слой на трансформъра

<span id="6700"></span>

Броят измерения на token embedding е фиксиран за целия модел и зависи от неговата архитектура.

<span id="6c08"></span>

**Пример:**

```text
Текст на потребителя:
"Здравей"

ID на токена в речника на модела за думата:
"Здравей" = 1123

Token embedding за ID на токен 1123 = масив от стойности във формат число с плаваща запетая от d_model= 4096 елемента:
[0.034, 0.120, 0.905, ..., 0.028]
```

**Размер на embedding size на популярните отворени модели (по официалните конфигурации, средата на 2026 г.):**

```text
Модел                     | d_model |
--------------------------|---------|
Llama 3.1 (8B)            | 4096    |
Llama 3.1 (70B)           | 8192    |
Llama 4 Scout (109B)      | 5120    |
Gemma 3 (12B)             | 3840    |
Gemma 3 (27B)             | 5376    |
Gemma 4 (31B)             | 5376    |
Qwen 3 (8B)               | 4096    |
Qwen 3 (32B)              | 5120    |
Qwen 3 (235B-A22B)        | 4096    |
Qwen 3.5 (9B)             | 4096    |
DeepSeek V3 / R1 / V4 Pro | 7168    |
DeepSeek V4 Flash         | 4096    |
GPT-OSS (20B / 120B)      | 2880    |
```

**Броят елементи на вектора на смисъла влияе върху:**\
По-голяма размерност = повече „място“ за съхранение на семантика, синтаксис, контекст.\
Това позволява да се различават по-фини смисли между токените.\
Цената е в паметта: квадратично с растежа на d_model растат вътрешните матрици на проекциите (размерът им е d_model × d_model), а слоят на ембедингите и активациите растат линейно.

```text
матрицата на проекцията при d_model = 4096
заема 4 пъти повече памет, отколкото
матрицата на проекцията при d_model = 2048
```

Числата, от които се състоят тези вектори и тегла, могат да се съхраняват с различна точност – за форматите на числата и квантуването вж. раздела в края на статията.

## Стъпка 3: Positional Encoding / Embeddings

<span id="13ee"></span>

Без допълнителна информация фразите:\
„Котката яде рибата“\
„Рибата яде котката“\
биха могли да се възприемат еднакво, тъй като наборът от думи е един и същ.

<span id="3f4b"></span>

За да се даде на модела усещане за ред, към всеки токен (дума или част от дума) се добавя позиционен вектор – набор от числа, който съобщава на модела на кои позиции се намират думите.

```text
Token embedding + Positional Encoding / Embeddings = Сумарен вектор
```

Така са правели класическите трансформъри и по тази схема е най-нагледно да се обяснява. При съвременните модели позицията се внася иначе – вътре в механизма на вниманието, чрез завъртане на векторите (RoPE, за него – по-долу в тази стъпка); сумарният вектор при тях просто е равен на ембединга на токена.

<span id="8692"></span>

**Positional Encoding / Embedding е:**\
Вектор със същата размерност като на токена (d)\
Представя позицията в последователността\
Може да се зададе с формула (sin/cos) или да се обучава\
Обединява се с вектора на токена на входа на модела

<span id="f52f"></span>

**Какъв тип се използва в моделите:**

```text
| Метод                       | Модел/семейство             | Описание                                      |
|-----------------------------|-----------------------------|-----------------------------------------------|
| Sinusoidal Encoding         | Transformer                 | Не се обучават, основани на синуси и          |
|                             | (Vaswani et al., 2017)      | косинуси с различна честота                   |
|-----------------------------|-----------------------------|-----------------------------------------------|
| Learnable Embeddings        | BERT, GPT-2/GPT-3,          | Обучаема таблица с позиции, подобна на        |
|                             | OPT, ELECTRA                | ембедингите на думите                         |
|-----------------------------|-----------------------------|-----------------------------------------------|
| Rotary Positional Embedding | почти всички съвременни LLM:| Завъртане на векторите – запазва относителните|
| (RoPE)                      | Llama 2/3/4, Qwen, Gemma,   | позиции между токените                        |
|                             | Mistral, DeepSeek, GPT-OSS  |                                               |
|-----------------------------|-----------------------------|-----------------------------------------------|
| ALiBi                       | BLOOM, MPT                  | Линеен bias, добавя се към attention score,   |
|                             |                             | не изисква съхранение на позиции              |
|-----------------------------|-----------------------------|-----------------------------------------------|
| Relative Position Bias      | T5, DeBERTa, Transformer-XL,| Използва отмествания между токените вместо    |
|                             | Pegasus, LongT5             | абсолютни позиции                             |
```

**Пример:**

```text
Текст на потребителя:
"Здравей, свят"

ID на токена в речника на модела за думата:
"Здравей" = 1123
"," = 15
"свят" = 345
Получаваме масив от ID на токени:
["Здравей", ",", "свят"] = [1123, 15, 345]

Token embedding от базата на модела за:
ID на токен 1123 = [0.034, 0.120, 0.905, ..., 0.028]
ID на токен 15 = [0.022, -0.010, -0.313, ..., 0.117]
ID на токен 345 = [-0.102, 0.241, 0.543, ..., 0.055]

Позиция на ID на токена на думата в текста на потребителя:
[1123, 15, 345] = [позиция 0, позиция 1, позиция 2]

Изчисляваме или вземаме от базата на модела вектора на позицията:
Вектор на позиция 0 = [0.001, 0.087, -0.432, ..., 0.019]
Вектор на позиция 1 = [0.005, -0.013, 0.021, ..., -0.012]
Вектор на позиция 2 = [-0.003, 0.099, -0.082, ..., 0.003]

Сумарен вектор за ID 1123:
Token embedding [0.034, 0.120, 0.905, ..., 0.028] +
Вектор на позиция 0 [0.001, 0.087, -0.432, ..., 0.019] =
[0.035, 0.207, 0.473, ..., 0.047]
аналогично за останалите токени
```

### Изчисляване на Sinusoidal Position Encoding

<span id="c60a"></span>

Синусоидалното позиционно кодиране се използва в Transformer-моделите, за да се предостави информация за позицията на токените във входната последователност. За разлика от рекурентните мрежи, Transformer няма вграден механизъм за отчитане на реда на токените. Затова е необходимо информацията за позицията на всеки токен да се кодира изрично.

<span id="0f01"></span>

**От гледна точка на математиката:**

```text
pos - позиция на токена в последователността (започвайки от 0)
i - индекс на измерението на вектора на позиционното кодиране (започвайки от 0)
d_model - размерност на вектора на позиционното кодиране (размерност на ембединга)
PE(pos, i) - i-ият елемент на вектора на позиционното кодиране за позиция pos.
10000 - това е хиперпараметър. Използва се за мащабиране на позицията и на честотата на синусоидите. Изборът на тази стойност позволява на модела лесно да екстраполира към последователности, по-дълги от тези, върху които е обучен.

Тогава i-ият елемент на вектора на позиционното кодиране за позиция pos:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
```

Формулата използва различни честоти (дължини на вълните) на синусоидалните функции за различните измерения на вектора на позиционното кодиране.\
Четните измерения използват синус, а нечетните – косинус. Това позволява на модела да различава позициите по различни фази и амплитуди.\
Делението на pos на 10000^(2i/d_model) намалява честотата на синусоидата с нарастването на индекса на измерението i.\
Това създава по-бавни колебания за по-високите измерения, което позволява на модела да различава позициите в различни мащаби.

**Пример:**

```text
pos = 0 (първи токен)
d_model = 4 (размерност на вектора на позиционното кодиране)

Тогава векторът на позиционното кодиране PE(0) ще има размерност 4.
Ще изчислим всеки елемент:

i = 0:
PE(0, 0) = sin(0 / 10000^(2*0/4)) = sin(0) = 0
PE(0, 1) = cos(0 / 10000^(2*0/4)) = cos(0) = 1

i = 1:
PE(0, 2) = sin(0 / 10000^(2*1/4)) = sin(0) = 0
PE(0, 3) = cos(0 / 10000^(2*1/4)) = cos(0) = 1

PE(0) = [0, 1, 0, 1]

Сега ще изчислим вектора на позиционното кодиране за pos = 1:

i = 0:
PE(1, 0) = sin(1 / 10000^(2*0/4)) = sin(1) ≈ 0.8415
PE(1, 1) = cos(1 / 10000^(2*0/4)) = cos(1) ≈ 0.5403

i = 1:
PE(1, 2) = sin(1 / 10000^(2*1/4)) = sin(0.01) ≈ 0.01
PE(1, 3) = cos(1 / 10000^(2*1/4)) = cos(0.01) ≈ 1

PE(1) = [0.8415, 0.5403, 0.01, 1]
```

<span id="be2c"></span>

**Важно:**\
В реалните Transformer-модели позиционното кодиране обикновено се изчислява предварително за всички възможни позиции в последователността и се запазва като таблица.\
След това, при подаване на входната последователност, съответните вектори на позиционното кодиране се добавят към ембедингите на токените.\
Изборът на хиперпараметъра 10000 е емпиричен и може да се настройва според конкретната задача.

<span id="9e4a"></span>

**Относителни позиционни отмествания:**\
Вместо да се кодира абсолютната позиция на всеки токен, някои варианти на трансформъри въвеждат **относителни** позиционни отмествания. Това позволява на модела директно да отчита разстоянието (и посоката) между двойки токени, а не тяхната „глобална“ позиция в последователността.

<span id="34db"></span>

**Защо са нужни относителните отмествания:**\
При генериране или обработка на дълги текстове е важно колко далеч една от друга се намират думите, а не само техните абсолютни индекси.\
Абсолютните ембединги се обобщават зле към последователности, по-дълги от тези, върху които моделът е обучаван.\
Относителните отмествания дават по-голяма гъвкавост: моделът се учи например, че „на 3 позиции надясно може да има обект на действието“, независимо къде в текста стои изречението.

<span id="22c5"></span>

Transformer-XL, относителни позиции чрез отместване на ключовете и заявките:

```text
В класическия self-attention изчисляваме:
score_{i,j} = (Q_i · K_j) / sqrt(d_k)

В Transformer-XL се въвеждат два допълнителни набора ембединги:
E^R[r] – ембединг за относителното отместване r = j - i
U, V – два вектора на отместване

Крайна формула:

Q_i·E^R[j-i] - зависима от съдържанието част на вниманието, отчитаща относителното отместване
V·E^R[j-i] - независима от съдържанието част, задаваща базовия bias за дадено отместване

score_{i,j} = (1/√d_k) * (Q_i·K_j + Q_i·E^R[j-i] + U·K_j + V·E^R[j-i])
```

### Как работи RoPE

Практически всички съвременни LLM използват Rotary Positional Embedding (RoPE). Идеята: вместо да се прибавя вектор на позицията към ембединга на входа, координатите на векторите Q и K се разбиват на двойки и всяка двойка се завърта на ъгъл, пропорционален на позицията на токена. Всяка двойка има своя „честота“ на въртене – от бърза към бавна, приблизително като честотите на синусоидите по-горе.

Какво дава това:\
Позицията се внася вътре във всеки слой на вниманието, а не веднъж на входа.\
Скаларното произведение на два завъртени вектора зависи само от разликата в техните позиции – моделът автоматично получава относителни позиции, без отделни таблици с отмествания.\
Векторите V и самите ембединги не се променят: позицията влияе точно там, където се сравняват токените.

С RoPE са свързани и техниките за разтягане на контекста. Ако моделът е обучен на 40 хиляди токена, а трябват 130 – честотите на въртене се преизчисляват така, че по-далечните позиции да „се поберат“ в обичайния за модела диапазон от ъгли. Така работят NTK-мащабирането и YaRN: същото онова „131 072 с YaRN“ от таблицата с контекстите не е отделно обучение върху дълъг контекст, а математическо разтягане на RoPE с леко дообучаване.

## Стъпка 4: Attention, Self-Attention (вектор на вниманието, Q,K,V-проекции) и разбиване на глави на вниманието (Multi-Head Attention)

<span id="9d27"></span>

Сега, когато токените имат не само „значение“, но и „позиция“, те минават през механизма self-attention, благодарение на който всяка дума гледа към другите думи, за да реши на кого от тях да обърне внимание, за да разбере по-добре собствения си смисъл.

<span id="ec0d"></span>

Това прилича на начина, по който човек чете изречение: той може да върне погледа си към предишните думи, за да разбере правилно контекста.

<span id="4d30"></span>

Self-Attention отговаря за разбирането на контекста – кои токени са важни един за друг, и позволява на всеки токен да гледа претеглено към всички останали в последователността и да определя към какво да обърне внимание при изграждането на смисъла.

### Softmax

това е функция, която приема на входа вектор от числа и го превръща в разпределение на вероятностите, в което всички стойности са неотрицателни и сборът им е 1.

```text
softmax(zᵢ) = exp(zᵢ) / ∑ⱼ exp(zⱼ)
```

<span id="5f51"></span>

**Моделът създава три представяния за всеки токен:**

<span id="8c06"></span>

**Q (въпрос):** какво търся?\
**K (ключ):** какво мога да предложа?\
**V (стойност):** каква информация нося?

<span id="79b2"></span>

Всяка дума сравнява своя Q с K на всички останали думи, за да разбере към кого си струва да гледа. След това събира нужната информация от V на онези думи, които са се оказали важни.

<span id="ea4d"></span>

За изчисляване на представянията се използва матрица с тегла на слоя – това е обучаем параметър на невронната мрежа, тоест тя се инициализира случайно при създаването на модела и се обучава заедно с останалите тегла. Първоначално е случайна, после става „умна“ благодарение на обучението.

<span id="d49d"></span>

**Как се обучават:**\
На етапа на обратното разпространение (backpropagation) моделът сравнява своите предсказания с правилния отговор (например следващия токен) и обновява Wq, Wk, Wv по градиента на грешката с помощта на оптимизатор (например Adam).

<span id="a092"></span>

Параметрите Wq, Wk и Wv могат да са общи или различни за всяка от главите, в зависимост от модела.

<span id="beee"></span>

**Wq (Query Projection):**\
Създава „въпроса“ – какво токенът иска да намери в другите токени.\
Определя посоката на вниманието.

```text
Q токена = Сумарен вектор на токена X * Тегло на слоя Wq
```

**Wk (Key Projection):**\
Създава „ключа“ – какво всеки токен предлага на другите.\
Използва се за сравнение с Q (доколко Qᵢ е „подобен“ на Kⱼ).

```text
K токена = Сумарен вектор на токена X * Тегло на слоя Wk
```

**Wv (Value Projection):**\
Създава „информацията“, която токенът може да предаде, ако към него е обърнато внимание.

```text
V токена = Сумарен вектор на токена X * Тегло на слоя Wv
```

<span id="4f04"></span>

**Глава на вниманието (attention-head):**\
При моделите съществува и глава на вниманието (attention-head). Всяка глава на вниманието (attention-head) обработва входния вектор по свой начин, през свои Q, K, V проекции, и гледа различни аспекти на изречението.

<span id="fe2e"></span>

**Всяка attention-head има своя гледна точка:**\
една глава може да следи граматиката (например подлог и сказуемо),\
друга – смисловите връзки (например кой върху какво действа),\
трета – позициите, контекста и т.н.\
Вместо една „гледна точка“ – веднага няколко.

<span id="e246"></span>

Всяка глава вижда целия текст, но го анализира по свой начин, през проекция и внимание.

<span id="157b"></span>

**Пример:**\
В изречението\
„Момчето, което държеше топката, избяга.“\
Различните глави могат да виждат\
Глава 1: „момчето“ ↔ „избяга“ → кой извършва действието\
Глава 2: „което“ ↔ „държеше“ → вградена граматична връзка\
Глава 3: „топката“ ↔ „държеше“ → обект на действието\
Всяка глава дава свое представяне за всеки токен, като отчита своите „наблюдения“.

<span id="f138"></span>

**Архитектура Multi-Head Attention (MHA):**\
Класическата реализация на self-attention, както в оригиналната статия *„Attention is All You Need“*.\
**Какво се случва:**\
Има няколко глави\
Всяка глава има свои Wq, Wk, Wv\
Всяка глава анализира по свой начин целия контекст\
Резултатите от всички глави се обединяват и минават през обща Wo\
**Плюсове:**\
Гъвкавост: всяка глава „гледа“ входа по свой начин\
Отлично работи при големи изчислителни ресурси\
**Минуси:**\
Много скъпо по памет и скорост при голям брой глави\
Особено при дълги последователности

<span id="6bf1"></span>

**Архитектура Multi-Query Attention (MQA):**\
Оптимизирана версия на вниманието, използвана в PaLM, Falcon, StarCoder и други, за да се намали натоварването на паметта и да се ускори инференсът.\
**Какво се случва:**\
По една Wq на глава\
Само един Wk и един Wv\
Всички глави използват едни и същи ключове и стойности\
**Плюсове:**\
По-малко памет: K и V се съхраняват в един екземпляр\
По-бърза генерация: по-малко данни се съхраняват между стъпките\
**Минуси:**\
По-малка гъвкавост (всички глави „гледат“ едни и същи K и V)\
Може леко да влоши качеството при сложни задачи

<span id="ce58"></span>

**Архитектура Grouped Query Attention (GQA):**\
комбиниран подход от предните два. Именно GQA се използва в повечето съвременни модели: Llama 3/4, Qwen 3, Gemma 2/3/4, GPT-OSS.

<span id="be3e"></span>

**Пример:**\
Например, в архитектурата на невронната мрежа Gemma 3 се използва Grouped Query Attention (GQA) – компромис между стандартния Multi-Head Attention (MHA) и Multi-Query Attention (MQA). В тази схема матриците Wq (за заявките) са различни за всяка глава, докато матриците Wk (за ключовете) и Wv (за стойностите) могат да са общи за групи от глави.\
В тази невронна мрежа:\
Wq: всяка глава има своя уникална матрица Wq, което позволява на всяка глава да се фокусира върху различни аспекти на входната последователност.\
Wk и Wv: главите се делят на групи и вътре във всяка група се използва обща матрица Wk и Wv. Това намалява обема на изчисленията и паметта, нужни за съхранение на ключовете и стойностите.\
В Gemma 3 27B – 32 глави за заявки (Wq) и 16 KV-глави: главите се делят на 16 групи по 2 и всяка група използва общи матрици на ключовете и стойностите (Wk и Wv).

<span id="2e52"></span>

**В крайна сметка във всеки от 62 слоя на модела Gemma 3 с размер 27B ще има:**\
32 различни Wq (Query Projection)\
16 различни Wk (Key Projection)\
16 различни Wv (Value Projection)\
1 обща Wo (Output Projection)\
1 MLP със свои тегла (Feed-Forward Network)\
свои параметри на нормализацията\
– и при всеки от 62-те слоя този набор от тегла е свой.

<span id="8365"></span>

**Брой глави на вниманието (attention heads; заявки Q / KV-глави) на популярните отворени модели:**

```text
| Модел                   | Q-глави / KV-глави     |
|-------------------------|------------------------|
| Llama 3.1 (8B)          | 32 / 8                 |
| Llama 3.1 (70B)         | 64 / 8                 |
| Llama 4 Scout           | 40 / 8                 |
| Gemma 3 (12B)           | 16 / 8                 |
| Gemma 3 (27B)           | 32 / 16                |
| Gemma 4 (31B)           | 32 / 16                |
| Qwen 3 (8B)             | 32 / 8                 |
| Qwen 3 (32B)            | 64 / 8                 |
| Qwen 3 (235B-A22B)      | 64 / 4                 |
| Qwen 3.5 (9B)           | 16 / 4                 |
| DeepSeek V3 / R1 / V4   | 128 (MLA)              |
| GPT-OSS (20B / 120B)    | 64 / 8                 |
```

При DeepSeek вместо класическия GQA се използва MLA (Multi-Head Latent Attention): ключовете и стойностите се свиват в общ латентен вектор, затова разделянето на KV-глави при него е неприложимо.

<span id="d0ab"></span>

**Каузално маскиране (Causal Masking):**\
прилага се в някои модели. Когато езиковият модел (LLM) генерира текст, той трябва да предсказва следващата дума въз основа само на предишните думи.\
Това означава, че токенът не трябва да има достъп до токените, които идват след него.\
За да се осигури това ограничение, се използва каузално маскиране – механизъм, който „забранява“ на вниманието да вижда бъдещите токени.

<span id="f521"></span>

**Как работи:**\
В обикновения self-attention всеки токен „гледа“ към всички токени в последователността, включително и към бъдещите.\
При генериране това е недопустимо, защото би било „мамене“ – моделът вижда отговора предварително.\
За да се предотврати това, при изчисляването на attention се прилага маска – специална матрица, наречена attention mask.\
За последователност с дължина n се създава триъгълна маска, в която:\
Стойностите над диагонала се заменят с -∞ или с голямо отрицателно число.\
След това се прилага softmax и тези стойности се превръщат в нулево внимание.

```text
Пример за маска (за n = 4 токена):
[
 [0, -∞, -∞, -∞],
 [0,  0, -∞, -∞],
 [0,  0,  0, -∞],
 [0,  0,  0,  0]
]

Това означава:
Токен 0 вижда само себе си.
Токен 1 вижда себе си и токен 0.
Токен 2 вижда себе си, токен 1 и токен 0.
Токен 3 вижда всичко, което е преди него.
```

<span id="f9aa"></span>

**Къде се използва Causal Masking:**\
GPT, LLaMA, Mistral, Gemma и други генеративни модели задължително използват каузално маскиране.\
BERT, обратно, използва bidirectional attention – токенът може да вижда целия контекст (вкл. бъдещето), защото задачата е друга – не генерация, а разбиране.

<span id="7cc3"></span>

**Защо това е важно, без causal masking:**\
Моделът при обучението ще „надзърта“ към правилния отговор (следващия токен).\
Това ще доведе до лоша генерация при използване на модела в inference, когато бъдещето е неизвестно.

<span id="c0be"></span>

**Ролята на позицията в Self-Attention:**\
Без Position Encoding Self-Attention вижда само смисъла на думите, но не и техния ред. С добавено Position Encoding Self-Attention започва да отчита не само какво е написано, но и къде се намира:\
„Момчето“ преди „четеше“ → вероятно е субект\
„Книгата“ до „четеше“ → най-вероятно е обект

<span id="bda3"></span>

**Какво в крайна сметка прави трансформърът:**\
Всяка дума получава информация за своята позиция\
Чрез self-attention всяка дума „пита“ всички останали:\
„Какво означавате за мен в този контекст?“\
Моделът събира тези отговори и получава дълбоко разбиране на смисъла на цялата фраза\
Повтаря това на всеки слой (обикновено 12–40 пъти), като задълбочава „разбирането“

```text
Текст на потребителя:
"Здравей, свят"

ID на токена от речника на модела:
"Здравей" = 1123
"," = 15
"свят" = 345
Получаваме масив от ID на токени:
["Здравей", ",", "свят"] = [1123, 15, 345]

Token embedding от базата на модела:
"Здравей" = ID 1123 = [0.034, 0.120, 0.905, ..., 0.028]
"," = ID 15 = [0.022, -0.010, -0.313, ..., 0.117]
"свят" = ID 345 = [-0.102, 0.241, 0.543, ..., 0.055]

Изчисляваме или вземаме от базата на модела вектора на позицията:
Вектор на позицията "Здравей" = [0.001, 0.087, -0.432, ..., 0.019]
Вектор на позицията "," = [0.005, -0.013, 0.021, ..., -0.012]
Вектор на позицията "свят" = [-0.003, 0.099, -0.082, ..., 0.003]

За "Здравей":
Token embedding [0.034, 0.120, 0.905, ..., 0.028] +
Вектор на позицията [0.001, 0.087, -0.432, ..., 0.019] =
Сумарен вектор X [0.035, 0.207, 0.473, ..., 0.047]

Изчисляваме Q, K, V за всеки токен и за всяка глава на вниманието:
Q = X * Wq на главата
K  = X * Wk на главата
V = X * Wv на главата
където X е сумарният вектор за токена
Wq, Wk, Wv - общи и еднакви за всички токени в този слой,
но различни или частично различни за всяка глава

Например:
Wq на главата = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9], [1.0, 1.1, 1.2]]
Wk на главата = [[0.12, 0.22, 0.32], [0.42, 0.52, 0.62], [0.72, 0.82, 0.92], [1.02, 1.12, 1.22]]
Wv на главата = [[0.11, 0.21, 0.31], [0.41, 0.51, 0.61], [0.71, 0.81, 0.91], [1.01, 1.11, 1.21]]

Тогава:

Q "Здравей" = x * Wq = [0.035, 0.207, 0.473, 0.047] * Wq

Q[0] = 0.035 * 0.1 + 0.207 * 0.4 + 0.473 * 0.7 + 0.047 * 1.0
     ≈ 0.0035 + 0.0828 + 0.3311 + 0.047
     ≈ 0.4644

Q[1] = 0.035*0.2 + 0.207*0.5 + 0.473*0.8 + 0.047*1.1
     = 0.007 + 0.1035 + 0.3784 + 0.0517
     ≈ 0.5406

Q[2] = 0.035*0.3 + 0.207*0.6 + 0.473*0.9 + 0.047*1.2
     = 0.0105 + 0.1242 + 0.4257 + 0.0564
     ≈ 0.6168

Q = [0.4644, 0.5406, 0.6168]

по аналогичен начин изчисляваме за "Здравей":
K = [0.47964, 0.55684, 0.63204]
V = [0.47299, 0.54822, 0.62442]
и за другите токени
```

<span id="5cbc"></span>

**Изчисляване на Attention Score между токените:**\
На този етап получихме Q, K и V за един токен и една глава на вниманието. След това можем да преминем към изчисляване на attention score между токените, например между „Здравей“ и „свят“, по формулата:

<span id="9c6a"></span>

**От гледна точка на математиката:**

```text
Q - матрица на заявките (Queries)
K - матрица на ключовете (Keys)
V - матрица на стойностите (Values)
d_k - размерност на вектора

Attention(Q, K, V) = softmax((Q * Kᵀ) / √d_k + mask) * V

Q * Kᵀ - матрично умножение на Q по транспонираната матрица K.
Това изчислява „суровите“ тегла на вниманието между всяка заявка и всеки ключ.

√d_k - корен квадратен от размерността на вектора на ключа (dimension of key vectors).
Използва се за мащабиране, за да се предотвратят твърде големи стойности
в softmax, което може да доведе до проблеми с градиентите.
Това се нарича Scaled Dot-Product Attention.

mask - това е матрица, която се използва за ограничаване на вниманието.
При генеративните модели се прилага causal mask (каузално маскиране) –
тя не позволява на токена да „вижда напред“ при изчисляване на вниманието.
Това е критично за задачи, при които моделът предсказва следващия токен.

softmax(...) - функцията softmax се прилага към резултата от деленето на Q ⋅ Kᵀ
на √d_k (и добавянето на mask, ако има такава). Това нормализира теглата на вниманието
така, че да се сумират до 1 за всяка заявка.

Умножението на резултата от softmax по V дава претеглена сума от стойностите,
където теглата се определят от attention score.
```

**Пример:**

```text
Входни данни:

| Токен    | Q-вектор                 | K-вектор                 |  V-вектор              |
|----------|--------------------------|--------------------------|------------------------|
| "Здравей"| [0.4644, 0.5406, 0.6168] | [0.4796, 0.5568, 0.6320] |  [0.473, 0.548, 0.624] |
|----------|--------------------------|--------------------------|------------------------|
| ","      |[0.2, 0.3, 0.1]           | [0.25, 0.35, 0.15]       |  [0.12, 0.09, 0.04]    |
|----------|--------------------------|--------------------------|------------------------|
| "свят"   | [0.55, 0.33, 0.77]       | [0.5213, 0.5967, 0.6721] |  [0.55, 0.65, 0.75]    |

За простота използваме размерност d_k за Q, K = 3 (за пример)

Изчисление:

Здравей–Здравей: 0.4644*0.4796 + 0.5406*0.5568 + 0.6168*0.6320 ≈ 0.2228 + 0.3011 + 0.3899 = 0.9138
Здравей–",": 0.4644*0.25 + 0.5406*0.35 + 0.6168*0.15 ≈ 0.1161 + 0.1892 + 0.0925 = 0.3978
Здравей–свят: 0.4644*0.5213 + 0.5406*0.5967 + 0.6168*0.6721 ≈ 0.2422 + 0.3225 + 0.4147 = 0.9794
","–Здравей: 0.2*0.4796 + 0.3*0.5568 + 0.1*0.6320 ≈ 0.0959 + 0.1670 + 0.0632 = 0.3261
","–",": 0.2*0.25 + 0.3*0.35 + 0.1*0.15 = 0.05 + 0.105 + 0.015 = 0.17
","–свят: 0.2*0.5213 + 0.3*0.5967 + 0.1*0.6721 ≈ 0.1043 + 0.1790 + 0.0672 = 0.3505
свят–Здравей: 0.55*0.4796 + 0.33*0.5568 + 0.77*0.6320 ≈ 0.2638 + 0.1837 + 0.4876 = 0.9351
свят–",": 0.55*0.25 + 0.33*0.35 + 0.77*0.15 ≈ 0.1375 + 0.1155 + 0.1155 = 0.3685
свят–свят: 0.55*0.5213 + 0.33*0.5967 + 0.77*0.6721 ≈ 0.2867 + 0.1969 + 0.5185 = 1.0021

Делим на √d_k = √3 ≈ 1.732

| From \ To | Здравей                 | ","                     | свят                    |
| --------- | ----------------------- | ----------------------- | ----------------------- |
| Здравей   | 0.9138 / 1.732 ≈ 0.5275 | 0.3978 / 1.732 ≈ 0.2296 | 0.9794 / 1.732 ≈ 0.5652 |
| ","       | 0.3261 / 1.732 ≈ 0.1882 | 0.17 / 1.732 ≈ 0.0982   | 0.3505 / 1.732 ≈ 0.2023 |
| свят      | 0.9351 / 1.732 ≈ 0.5397 | 0.3685 / 1.732 ≈ 0.2127 | 1.0021 / 1.732 ≈ 0.5786 |

Прилагаме експонентата за "Здравей":
exp(0.5275) ≈ 1.694
exp(0.2296) ≈ 1.258
exp(0.5652) ≈ 1.759

Сума на всички експоненти за "Здравей":

1.694 + 1.258 + 1.759 ≈ 4.711

softmax = exp(x) / сума на всички exp(x)

Softmax тегло за "Здравей" спрямо другите токени:

| Токен  | Softmax тегло        |
| ------ | -------------------- |
| Здравей | 1.694 / 4.711 ≈ 0.36 |
| ","    | 1.258 / 4.711 ≈ 0.27 |
| свят   | 1.759 / 4.711 ≈ 0.37 |

Когато "Здравей" генерира своето представяне (на изхода на attention слоя), той:
взема 36% информация от самия себе си,
27% от ",",
и 37% от думата "свят".

Претеглено сумиране:

Output= 0.36 ∗ V("Здравей") + 0.27 ∗ V(",") + 0.37 ∗ V("свят")

Координата 1 = 0.36∗0.473+0.27∗0.12+0.37∗0.55≈0.1703+0.0324+0.2035 ≈ 0.4062
Координата 2 = 0.36∗0.548+0.27∗0.09+0.37∗0.65≈0.1973+0.0243+0.2405 ≈ 0.4621
Координата 3 = 0.36∗0.624+0.27∗0.04+0.37∗0.75≈0.2246+0.0108+0.2775 ≈ 0.5129

Изход на вниманието за токена "Здравей": [0.4062, 0.4621, 0.5129]
```

<span id="a129"></span>

Този вектор – изходът на вниманието (attention output) – е новото представяне на токена „Здравей“, в което е отчетен контекстът му: и самият той, и съседите. Именно такива вектори след това отиват или в следващия attention слой, или на изхода на модела. Важно е да не се бърка с attention score: score са скаларните тегла от softmax (0.36, 0.27, 0.37 по-горе), а тук е претеглената с тях сума на V-векторите.

Обърнете внимание: за простота примерът е изчислен без каузална маска – „Здравей“ тук „вижда“ и стоящия след него токен „свят“. В истински LLM с causal masking токенът би събирал информация само от себе си и от предишните токени.

<span id="f44a"></span>

Сравняването на токена със самия себе си е нужно, защото в self-attention всеки токен „гледа“ към всички токени, включително и към себе си.\
Това е необходимо, за да:\
се запази информацията за самия токен – иначе той би „се загубил“ на фона на останалите.

<span id="c0c9"></span>

Да се научи да „усилва“ или „потиска“ себе си – например в някои езикови ситуации токенът е важен сам по себе си (например личното местоимение), а понякога контекстът му е по-важен.\
Матрицата на вниманието е квадратна (n × n) и на нейния диагонал стои именно self-to-self attention; при каузална маска от нея остава долният триъгълник.

<span id="8092"></span>

Формално attention са теглата, по които токенът агрегира информация от другите токени (вкл. от самия себе си), и w1 е вниманието на „Здравей“ към самия себе си. Ако то не се отчита, токенът „Здравей“ изобщо не би участвал в собствения си изход.

<span id="68fa"></span>

Пример:\
Той каза, че той ще дойде.\
Когато моделът обработва токена „той“, му е нужно:\
да „погледне“ към другите токени – за да разбере контекста,\
но и самият токен „той“ също е важен, за да не се загуби информацията за това кого се има предвид.

<span id="cfcc"></span>

**Материали:**

[Wikipedia: Attention (machine learning)](https://en.wikipedia.org/wiki/Attention_\(machine_learning\))

[Wikipedia: Softmax function](https://en.wikipedia.org/wiki/Softmax_function)

[Understanding Q,K,V In Transformer( Self Attention)](https://medium.com/analytics-vidhya/understanding-q-k-v-in-transformer-self-attention-9a5eddaa5960)

[What is Query, Key, and Value (QKV) in the Transformer Architecture and Why Are They Used?](https://epichka.com/blog/2023/qkv-transformer/)

## Стъпка 5: Обединяване на главите на вниманието (Head Concatenation, Concatenated Multi-Head Attention)

<span id="a6b5"></span>

Предишните изчисления бяха за всяка от главите на вниманието, сега е необходимо да се обединят резултатите. Напомням, че резултатите са различни поради различните Wq, Wk, Wv за различните глави.

<span id="2354"></span>

**От гледна точка на математиката:**

```text
h - брой на главите на вниманието
d_k - размерност на изхода на вниманието на всяка глава
HeadOutput_i - изход на вниманието на i-тата глава.

Concatenation (Обединяване на векторите в един вектор по координати):

concat = [HeadOutput_1, HeadOutput_2, ..., HeadOutput_h]
```

**Пример:**

```text
Входни данни:
Допустим, размерност: 6 (2 глави по 3 стойности)

Брой глави на вниманието h = 2
Размерност на вектора d_k = 3
"Здравей" за глава 1 HeadOutput_1 = [0.4062, 0.4621, 0.5129]
"Здравей" за глава 2 HeadOutput_2 = [0.22, 0.33, 0.44]

Изчисление:

Concatenation (Обединяване на векторите в един вектор по координати):

concat[0] = HeadOutput_1[0] = 0.4062
concat[1] = HeadOutput_1[1] = 0.4621
concat[2] = HeadOutput_1[2] = 0.5129
concat[3] = HeadOutput_2[0] = 0.22
concat[4] = HeadOutput_2[1] = 0.33
concat[5] = HeadOutput_2[2] = 0.44

concat = [0.4062, 0.4621, 0.5129, 0.22, 0.33, 0.44]
```

## Стъпка 6: Output Projection (Wo)

<span id="d827"></span>

След обединяването на attention-изходите от различните глави те обикновено се пропускат през линеен слой (Dense Layer), за да се върнат в изходното пространство на размерността на модела.

<span id="7e3e"></span>

**От гледна точка на математиката:**

```text
concat ∈ ℝ⁶ - входен вектор
Wₒ ∈ ℝ⁶ˣ³ - матрица на теглата на проекцията
output ∈ ℝ³ - изходен вектор след проекцията

Умножение:
output = concat • Wₒ

Подробна формула за всеки компонент на изходния вектор:
output[0] = concat[0]*W_o[0][0] + concat[1]*W_o[1][0] + concat[2]*W_o[2][0] + concat[3]*W_o[3][0] + concat[4]*W_o[4][0] + concat[5]*W_o[5][0]

Алтернативно, във вид на сума:
output_j = ∑_{i=1}^{6} concat_i ⋅ W_o[i][j]     за j = 1, 2, 3

Матрично:
output = concat (1x6) ⋅ W_o (6x3) = (1x3)
```

**Пример:**

```text
Входни данни:
Размерност на модела d_model = 3

Значи, проекционната матрица W_o ще има
размерност (6, 3), т.е. 6 входа → 3 изхода
W_o = [
  [0.1, 0.2, 0.3],
  [0.0, 0.1, 0.0],
  [0.2, 0.0, 0.1],
  [0.1, 0.2, 0.2],
  [0.0, 0.1, 0.3],
  [0.3, 0.0, 0.1]
]

Входен вектор след concat:
concat = [0.4062, 0.4621, 0.5129, 0.22, 0.33, 0.44]

Изчисление:

x = 0.4062*0.1 + 0.4621*0.0 + 0.5129*0.2 + 0.22*0.1 + 0.33*0.0 + 0.44*0.3
  = 0.04062 + 0 + 0.10258 + 0.022 + 0 + 0.132
  = 0.2972

y = 0.4062*0.2 + 0.4621*0.1 + 0.5129*0.0 + 0.22*0.2 + 0.33*0.1 + 0.44*0.0
  = 0.08124 + 0.04621 + 0 + 0.044 + 0.033 + 0
  = 0.2045

z = 0.4062*0.3 + 0.4621*0.0 + 0.5129*0.1 + 0.22*0.2 + 0.33*0.3 + 0.44*0.1
  = 0.12186 + 0 + 0.05129 + 0.044 + 0.099 + 0.044
  = 0.3602

Финален output вектор:
output = [0.2972, 0.2045, 0.3602]
```

## Стъпка 7: Добавяне на Residual (остатъчна връзка)

<span id="a262"></span>

това е сумиране на входа на слоя с неговия изход. Събираме изхода с входния вектор, който е бил подаден на входа на блока (input embedding или изходът на предишния слой). В реален модел входът и изходът на блока имат една и съща размерност d_model и се събират поелементно; в нашия сквозен пример входът беше 4-мерен, а изходът – 3-мерен, затова за илюстрация ще вземем първите три компонента на входа.

<span id="d1c3"></span>

**Това се използва, за да:**\
се запази изходната информация (градиентите се предават назад по-лесно).\
се избегне „затихването“ на сигнала през множество слоеве.\
се улесни обучението дори на много дълбоки невронни мрежи.

```text
Входни данни:

Входен вектор
input = [0.035, 0.207, 0.473]

Изходен вектор
output = [0.2972, 0.2045, 0.3602]

Изчисление:

residual = output + input = residual = [0.3322, 0.4115, 0.8332]
```

## Стъпка 8: Нормализация на слоя (Layer Normalization)

<span id="87df"></span>

нормализира стойностите вътре в един вектор от признаци, тоест по размерността на признаците (feature dimension).

<span id="a5b6"></span>

**Защо е нужно:**\
Премахва отместването и разликите в мащаба между признаците.\
Прави обучението по-стабилно.\
Ускорява сходимостта на невронната мрежа.\
Работи по-добре при малки партиди, за разлика от BatchNorm.

**От гледна точка на математиката:**

```text
Изчисляваме средната стойност (mean):
μ = (1/n) * ∑ xₙ

Изчисляваме стандартното отклонение (std):
σ = sqrt((1/n) * ∑ (xₙ - μ)^2 + ε)
Където ε - малко число, за да се избегне делене на нула.

Нормализираме всеки елемент:
̂xₙ = (xₙ - μ) / σ

По желание мащабираме и отместваме (обучаеми параметри):
yₙ = γ * ̂xₙ + β
```

**Пример:**

```text
Входни данни:

Вземаме изхода residual от предишната стъпка:
x = [0.3322, 0.4115, 0.8332]

Изчисление:

Изчисляваме средното:
μ = (0.3322 + 0.4115 + 0.8332) / 3 = 1.5769 / 3 ≈ 0.5256

Изчисляваме стандартното отклонение:
σ = sqrt(((0.3322 - 0.5256)^2 + (0.4115 - 0.5256)^2 + (0.8332 - 0.5256)^2) / 3)
   = sqrt((0.0374 + 0.0130 + 0.0946) / 3)
   = sqrt(0.0483) ≈ 0.22

Нормализираме:
̂x₁ = (0.3322 - 0.5256) / 0.22 ≈ -0.88
̂x₂ = (0.4115 - 0.5256) / 0.22 ≈ -0.52
̂x₃ = (0.8332 - 0.5256) / 0.22 ≈ 1.40

Ако обучаемите параметри са γ = 1 и β = 0, това е и резултатът:
[-0.88, -0.52, 1.40]
```

## Стъпка 9: FFN (Feed-Forward Network) и MLP (Multilayer Perceptron)

<span id="b3cd"></span>

**FFN (Feed-Forward Network):**\
това е компонент на трансформъра, който обработва всяка дума (или токен) поотделно, без да отчита другите токени.

<span id="79a8"></span>

Прилага се независимо към всеки токен след слоя за внимание и представлява двуслойна невронна мрежа с нелинейна активационна функция.

<span id="5ef6"></span>

Това позволява на модела да улавя по-сложни зависимости.

<span id="66f4"></span>

**MLP (Multilayer Perceptron):**\
това е тип невронна мрежа, състояща се от няколко слоя напълно свързани неврони. В контекста на LLM и трансформърите MLP често означава същата Feed-Forward Network (FFN).

<span id="93c6"></span>

MLP = общото название на архитектурата,\
FFN = частен случай на MLP, използван вътре в трансформърите.

<span id="f88b"></span>

**От гледна точка на математиката:**

```text
x - входен вектор с размерност d
W1-матрица на теглата на първия линеен слой с размерност (d_ff × d)
b1-отместване на първия слой с размерност d_ff
W2-матрица на теглата на втория линеен слой с размерност (d × d_ff)
b2-отместване на втория слой с размерност d
f-активационна функция (например ReLU или GELU)
d-размерност на входа/изхода
d_ff-размерност на скрития слой (обикновено d_ff = 4 × d)

Основна формула на FFN:
FFN(x) = W2 · f(W1 · x + b1) + b2

Разлагане стъпка по стъпка:

Линейно преобразуване, размерност на z1: (d_ff × 1):
z1 = W1 · x + b1

Нелинейна активация, прилага се поелементно, размерността се запазва:
z2 = f(z1)

Второ линейно преобразуване, крайният вектор е със същия размер като x: (d × 1):
y = W2 · z2 + b2
```

**Пример:**

```text
Входни данни:

Размерност на скрития слой d = 4,
Вътрешна размерност d_ff = 8,
Активация: ReLU,
Входен вектор:
x = [x[0], x[1], x[2], x[3]] = [1.0, -2.0, 0.5, 3.0]

матрица 8x4 W1 =
[
  [1, 0, 0, 0],
  [0, 1, 0, 0],
  [0, 0, 1, 0],
  [0, 0, 0, 1],
  [1, 1, 1, 1],
  [1, -1, 1, -1],
  [0.5, 0.5, 0.5, 0.5],
  [-1, -1, -1, -1]
]

Изчисление:

y[0] = 1 * x[0] + 0 * x[1] + 0 * x[2] + 0 * x[3] = 1.0 + 0 + 0 + 0 = 1.0
y[1] = 0 * x[0] + 1 * x[1] + 0 * x[2] + 0 * x[3] = 0 - 2.0 + 0 + 0 = -2.0
y[2] = 0 * x[0] + 0 * x[1] + 1 * x[2] + 0 * x[3] = 0 + 0 + 0.5 + 0 = 0.5
y[3] = 0 * x[0] + 0 * x[1] + 0 * x[2] + 1 * x[3] = 0 + 0 + 0 + 3.0 = 3.0
y[4] = 1 * x[0] + 1 * x[1] + 1 * x[2] + 1 * x[3] = 1.0 - 2.0 + 0.5 + 3.0 = 2.5
y[5] = 1 * x[0] + (-1) * x[1] + 1 * x[2] + (-1) * x[3] = 1.0 + 2.0 + 0.5 - 3.0 = 0.5
y[6] = 0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2] + 0.5 * x[3] = 0.5 - 1.0 + 0.25 + 1.5 = 1.25
y[7] = -1 * x[0] + (-1) * x[1] + (-1) * x[2] + (-1) * x[3] = -1.0 + 2.0 - 0.5 - 3.0 = -2.5

Вектор след линейния слой (преди активацията):
y = [1.0, -2.0, 0.5, 3.0, 2.5, 0.5, 1.25, -2.5]

Прилагаме ReLU:
ReLU(y[i]) = max(0, y[i])

ReLU(y) = [1.0, 0.0, 0.5, 3.0, 2.5, 0.5, 1.25, 0.0]
```

В съвременните модели FFN е малко по-сложен от описаното: използва се gated вариант (SwiGLU) с три матрици – две подготвят „кандидата“ и „вратата“ (gate), които се умножават поелементно, – а вместо ReLU се взема гладка активация SiLU/GELU; вътрешната размерност при това не е 4×, а около 2.7× от d_model. Същността не се променя: все така това са два линейни слоя и нелинейност, прилагани към всеки токен поотделно.

При MoE моделите точно на мястото на FFN стои набор от „експерти“: малък рутер избира за всеки токен няколко от тях (например 8 от 128) и работят само избраните – така Qwen 3 235B-A22B харчи за токен само 22 от своите 235 милиарда параметъра.

## Стъпка 10: Residual + LayerNorm (втори слой за нормализация)

След FFN към резултата отново се прибавя входът на този блок (изходът от стъпка 8 – това, което е подадено във FFN) и векторът отново се нормализира – това помага да се запази информацията и да се стабилизират изчисленията

## Стъпка 11: Подаване на изхода на блока към следващия трансформър блок

След като получи на изхода на един блок на трансформъра нормализиран вектор, моделът го предава на входа на следващия блок. Такива блокове може да са десетки – всеки добавя все повече „разбиране“ на контекста

## Стъпка 12: Финална LayerNorm нормализация след последния блок

След последния слой още веднъж се прилага LayerNorm – това е заключителен щрих преди генерирането на логити, за да се изгладят разликите в стойностите

## Стъпка 13: Линейна проекция в логити (Logits Projection) + Softmax

Нормализираният вектор от последния слой се умножава по матрицата на ембедингите (или по отделен линеен слой), за да се получат „сурови“ оценки (logits) за всеки токен от речника. След това тези оценки се превръщат във вероятности с помощта на Softmax или направо се подават на семплерите за подбор на токена

**Какво са logits:**\
„сурови“ стойности, които още не са нормализирани във вероятности.\
Например, ако речникът се състои от 50 000 токена, то logits са просто 50 000 числа, по едно за всеки токен.

За всеки токен в последователността моделът е формирал неговото представяне (hidden state).

Сега трябва този hidden state да се преобразува в логити по речника – оценка на вероятността за появата на всеки възможен следващ токен.

**Преобразуване чрез Linear Layer:**

```text
logits = hidden_state @ Wᵀ + b

hidden_state: последният вектор (или цялата последователност – но по-често интересува последният токен),
Wᵀ: транспонираната матрица на ембедингите (размер [vocab_size, hidden_dim]),
b: отместване (bias), често се пропуска за икономия.
```

В част от моделите се прилага weight tying – Wᵀ се взема от същия слой, от който са ембедингите на входа; това спестява памет. Така правят основно компактните модели (Gemma, по-малките Qwen), а например големите Llama и DeepSeek държат отделна изходна матрица.

При генериране това преобразуване се прави само за последната позиция: за да се избере следващият токен, са нужни логитите само на последния hidden state – няма смисъл да се изчисляват стотици хиляди логити за всеки токен от промпта.

## Стъпка 14: Избор на следващ токен (sampling)

Получените logits минават през верига от семплери: първо се прилагат наказания за повторения и температура, след това филтри (top-k, top-p и др.), а накрая – финален семплер (Mirostat, алчен избор или разпределение). Именно този токен се добавя в контекста и генерирането се повтаря от стъпка 1 до достигане на края или на нужната дължина.

В бекенда **LLama.cpp** може да се съставят вериги от семплери: първо междинните, след това веригата трябва да завършва с финален семплер.\
Редът на прилагане на семплерите в llama.cpp:

```text
1. Наказания за повторения:
   - repeat_penalty
   - frequency_penalty
   - presence_penalty
   - DRY (наказание за повторение на n-грами)

2. Филтриране на токени:
   - top_k
   - typical_p
   - top_p
   - min_p
   - XTC

3. Мащабиране на логитите:
   - temperature

4. Граматични ограничения:
   - grammar

5. Финален избор на токен:
   - mirostat (v1 или v2) или
   - greedy / dist (случайна извадка)
```

Пример от LLama.cpp (за разработчици):

```cpp
// подготовка на параметрите на веригата
struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params();
llama_sampler * smpl = llama_sampler_chain_init(sparams);

// междинни семплери:
llama_sampler_chain_add(smpl, llama_sampler_init_top_k (50)); // Top-K
llama_sampler_chain_add(smpl, llama_sampler_init_typical (0.95f, 1)); // Locally Typical
llama_sampler_chain_add(smpl, llama_sampler_init_top_p (0.9f, 1)); // Top-P (nucleus)
llama_sampler_chain_add(smpl, llama_sampler_init_min_p (0.05f, 1)); // Min-P
llama_sampler_chain_add(smpl, llama_sampler_init_xtc (0.9f, 1.0f, 1, LLAMA_DEFAULT_SEED)); // XTC
llama_sampler_chain_add(smpl, llama_sampler_init_top_n_sigma(1.5f)); // Top-nσ
llama_sampler_chain_add(smpl, llama_sampler_init_temp (0.7f)); // Temperature
llama_sampler_chain_add(smpl, llama_sampler_init_temp_ext (0.7f, 0.1f, 1.5f));  // Extended temp

llama_sampler_chain_add(smpl, llama_sampler_init_grammar(vocab, grammar, "root"));
llama_sampler_chain_add(smpl,llama_sampler_init_grammar_lazy_patterns(vocab, grammar, "root", patterns, 2, tokens, N));

// финален семплер, само 1 вариант от:
llama_sampler_chain_add(smpl, llama_sampler_init_mirostat(32000, LLAMA_DEFAULT_SEED, 5.0f, 0.1f, 100)); // Mirostat V1
llama_sampler_chain_add(smpl, llama_sampler_init_mirostat_v2(LLAMA_DEFAULT_SEED, 5.0f, 0.1f)); // Mirostat V2
llama_sampler_chain_add(smpl, llama_sampler_init_greedy()); // Greedy
llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED)); // Dist

// след генерирането не забравяйте да освободите веригата:
llama_sampler_free(smpl);
```

**За llama.cpp имам отделна статия:**

[/bg/llama-cpp/](/bg/llama-cpp/)

Пример за страница с настройки на семплерите от приложението [https://github.com/a-ghorbani/pocketpal-ai](https://github.com/a-ghorbani/pocketpal-ai)

![](/images/2019/02/Screenshot_2025-05-21-20-17-31-917_com.pocketpalai-1-461x1024.jpg)

![](/images/2019/02/Screenshot_2025-05-21-20-17-44-076_com.pocketpalai-461x1024.jpg)

## Междинни семплери

### Temperature (temp)

управлява степента на случайност при избора на следващ токен от разпределението на вероятностите, изчислено от модела. Температурата стеснява или разширява „фунията на избора“ на следващата дума. Колкото е по-ниска, толкова фунията е по-тясна, колкото е по-висока – толкова е по-широка.

След като моделът е предсказал логитите (суровите стойности на вероятностите) за всички възможни токени, те минават през softmax функцията. Температурата влияе на тази функция:

```text
P(token) = softmax(logits / temperature)
```

**Ефект на параметъра temperature:**\
T = 1 логитите остават такива, каквито са.\
T \< 1 (например 0.7) моделът усилва разликите между токените – вероятните токени стават още по-вероятни. Поведението става по-предвидимо. \
T > 1 (например 1.5) разликите между токените се изглаждат – нараства шансът да се избере по-малко вероятен токен. Поведението става по-разнообразно и рисковано.\
T → 0 softmax се превръща в argmax и винаги се избира един, най-вероятният токен.

```text
Начални логити:
Token A: 3.0
Token B: 2.5
Token C: 1.0

След softmax без промяна (temp = 1.0):
A: 57%
B: 35%
C: 8%

С понижена температура (temp = 0.5):
A: 72%
B: 27%
C: 1%

С повишена температура (temp = 1.5):
A: 51%
B: 36%
C: 13%
```

### Top-k

моделът избира следващия токен само измежду k-те най-вероятни. Всички останали токени се отхвърлят, независимо от абсолютната им вероятност.

```text
има 6 токена със следните вероятности:
A: 0.35
B: 0.30
C: 0.15
D: 0.10
E: 0.06
F: 0.04

Ако top_k = 3, ще оставим само:
A: 0.35
B: 0.30
C: 0.15
```

### Top-p (nucleus sampling)

избира се минималното множество токени с най-големи вероятности, чийто сбор надвишава зададения праг p. Останалите токени се отсичат напълно, дори ако са имали висок ранг.

```text
моделът е върнал вероятности на токените:
A: 0.50
B: 0.25
C: 0.15
D: 0.08
E: 0.02

При top_p = 0.9 набираме токените с най-големи вероятности,
докато сборът им достигне прага:
0.50 + 0.25 + 0.15 = 0.90 – остават A, B, C
D и E ще бъдат отхвърлени – после вероятностите на останалите се нормализират отново.
```

### Min-p

отсичат се всички токени, чиято вероятност е под праг, зададен като дял `p` от вероятността на най-вероятния токен. Например при min_p = 0.1 и вероятност на лидера 0.50 прагът ще бъде 0.05 – всичко под него се отхвърля. Прагът е относителен: когато моделът е уверен, филтърът е по-строг, когато разпределението е плоско – по-мек.

### Typical sampling

избира токени, близки до „типичното“ ниво на изненада (surprisal), като отсича твърде предвидимите и твърде редките. За разлика от top-k и top-p, той се ориентира по медианната стойност на surprisal, а не по вероятността на токените.

### Repetition penalties, Frequency penalty, Presence penalty

намалява шансовете за повторен избор на вече генерирани токени, като понижава техните логити. Това помага да се избегнат натрапчиви повторения в текста и прави изхода по-разнообразен.

### Logit bias

метод, който позволява ръчно да се промени вероятността на отделни токени преди прилагането на softmax, като към логитите им се добавят или изваждат стойности. Това дава точков контрол: например може да се накара моделът да избягва определени думи или, обратно, по-често да ги избира.

### Tail Free Sampling (TFS)

отхвърля дългата „опашка“ на разпределението, като регулира плътността на вероятностите.

### DRY (Don't Repeat Yourself)

наказва токени, които биха продължили вече срещани в текста n-грами: колкото по-дълго е полученото повторение, толкова по-силно е наказанието. Добре премахва зациклянето върху цели фрази, без да засяга единичните повторения на думи.

### Grammar

използва правила на граматика (обикновено във вид на CFG) за строго ограничаване на допустимите токени на всяка стъпка от генерирането. Вместо да променя логитите, той просто забранява всички токени, които не съответстват на текущото допустимо състояние на граматиката, като осигурява строго структуриран изход.

и други

## Финални семплери

### Greedy

най-простият начин за генериране, при който моделът винаги избира токена с най-висока вероятност (максимален логит). Бърз е и детерминиран, но често води до еднообразен и предвидим текст.

### Random

случаен избор на следващия токен, пропорционално на вероятността му след всички приложени семплери (top-k, top-p и т.н.). Това е противоположното на алчния избор (greedy), при който винаги се взема токенът с максимална вероятност.

### Dist

така в llama.cpp се нарича финалната случайна извадка: токенът се избира случайно, пропорционално на крайното разпределение на вероятностите след всички филтри – същото като Random по-горе, с фиксиран seed за възпроизводимост.

### Mirostat v1

поддържа зададено ниво на „изненада“ (перплексия), като динамично коригира избора на думи. Помага да се избегнат прекомерни повторения (капан на скуката) и несвързаност (капан на объркването), като осигурява балансирано и качествено генериране на текст.

### Mirostat v2

поддържа зададено ниво на неочакваност (перплексия) с по-точно управление, отколкото Mirostat v1. Използва разширен механизъм за обратна връзка, който позволява динамично да се коригира изборът на думи за постигане на стабилно качество на текста.

и други

## Стъпка 15: Итерация на генерирането

След като моделът е избрал следващия токен, неговият ID се добавя в края на входната последователност – повторна токенизация не е нужна, избраният токен вече е токен.

После цикълът се повтаря: моделът изчислява вниманието вече с отчитане на новия токен и предсказва следващия, и така докато не бъдат изпълнени условията за прекратяване.

Всички предишни токени обаче не е нужно да се преизчисляват отново: техните K- и V-вектори се пазят в KV кеша и на всяка итерация наистина се обработва само новият токен – именно затова стъпките на генерирането отнемат приблизително еднакво време.

Оттук идват двете фази на работа на модела, познати на всеки по усещането „помисли – и после пише равномерно“. Първо prefill: целият промпт се обработва с един паралелен проход, като се запълва KV кешът – това е паузата до първата дума и е толкова по-дълга, колкото по-дълъг е промптът. След това decode: токените се генерират един по един, всеки за приблизително еднакво време. Затова скоростта на модела се описва с две различни числа – време до първия токен и токени в секунда.

Такъв цикъл позволява да се построи цялата изходна последователност токен по токен.

### Условия за завършване

Генерирането завършва, когато моделът издаде специален токен за край на последователността (EOS) или когато се достигне предварително зададената максимална дължина на последователността.

Това ограничение предотвратява безкрайни или твърде дълги отговори.

Генерирането се спира и от стоп-последователности: предварително зададени низове (например маркер за начало на следващата реплика от диалога), при появата на които изходът се прекъсва.

## Стъпка 16: Преобразуване на токените в думи

След като моделът е генерирал нужния брой токени, те се преобразуват обратно в текст – това се нарича детокенизация. Токенизаторът взема последователност от числа (токени) и ги превръща в думи и препинателни знаци. На практика чат интерфейсите правят това в движение – това е и стрийминг: всеки избран токен веднага се детокенизира и се дописва на екрана, затова отговорът се появява дума по дума, а не изцяло накрая.

## Как е обучаван моделът

Всичко описано по-горе е работата на вече готов модел. Самият той се получава на няколко етапа.

**Предварително обучение (pretraining):**\
Моделът трилиони пъти решава една и съща задача – да предскаже следващия токен в текст от огромен корпус (уеб, книги, код). Не са нужни никакви етикети: правилният отговор е просто истинският следващ токен. На този етап моделът научава езика, фактите и закономерностите, но умее само да продължава текст.

**Инструкционно дообучаване (SFT, Supervised Fine-Tuning):**\
Моделът се дообучава върху примери от вида „заявка → добър отговор“, написани от хора или подбрани измежду генерации. След това той отговаря на въпроси, а не ги продължава.

**Подравняване (RLHF, DPO):**\
Хора сравняват двойки отговори на модела и избират по-добрия; върху тези предпочитания моделът се дообучава да дава полезни и безопасни отговори. Класическият начин е обучение с подкрепление по човешки оценки (RLHF); по-проста съвременна алтернатива е DPO, където предпочитанията се използват директно като обучаващи примери.

### Reasoning модели

„Мислещите“ модели от таблиците по-горе (DeepSeek R1, Qwen 3 в режим на разсъждение, Gemma 4) механично с нищо не се отличават: това са същите трансформъри, които генерират същите токени. Разликата е в обучението: допълнително са ги обучавали с подкрепление върху задачи с проверим отговор, като са ги награждавали за верния резултат – и моделът сам е научил преди отговора да пише дълга верига от разсъждения, чернова, която интерфейсът после скрива зад спойлер. За това се плаща с токени: „мислещият“ отговор може да е пъти по-дълъг и по-бавен от обикновения.

### Откъде се вземат халюцинациите

Направо от механиката на генерирането. Моделът е длъжен винаги да избере следващ токен – при него няма вградено състояние „не знам“, има само разпределение на вероятностите, от което нещо ще бъде избрано. Ако знанията за предмета в теглата са малко, разпределението пак ще даде правдоподобно звучащо продължение: правилната форма на отговора с измислено съдържание. Затова халюцинациите не са бъг, който някога ще бъде поправен, а свойство на самия подход; те се потискат с обучение (моделът се учи да отказва да отговаря), с инструменти като RAG и уеб търсене, но не се премахват напълно.

## Интересни въпроси и отговори

### Възможно ли е да се опише в няколко изречения как работи невронната мрежа?

Ще се опитам. Моделът е огромна функция, обучена за една задача: по началото на текста да предскаже кой токен (парченце от дума) най-вероятно идва след него. Вашият въпрос се превръща в последователност от токени, моделът изчислява вероятностите на всички възможни продължения, от тях се избира един токен, дописва се към текста – и всичко се повтаря, токен след токен, докато не се получи отговор. А цялото „разбиране“ се състои в това, че по време на обучението върху огромен корпус от текстове теглата на модела са се научили да предсказват продължението с оглед на смисъла и контекста на цялата фраза, а не просто по честотата на думите.

### Защо в LLM моделите на входа се подава въпрос, а на изхода се получава отговор, а не перифразиран въпрос?

Моделът е обучен върху текстове от вида „въпрос→отговор“, затова когато види въпрос, нататък се генерират не думите на самия въпрос, а най-вероятното продължение – отговорът.

По време на инструкционното обучение (SFT) и RLHF той се „поощрява“ за полезни отговори, а не за повторение на формулировката, затова параметрите се изместват към отговорите.

В термините на вероятностите: за един въпрос вероятността следващият токен да е от отговора е по-висока, отколкото да е токен, повтарящ въпроса, и декодерът избира именно него.

### Данните, върху които се обучава LLM моделът, остаряват – какви са възможностите, за да генерира моделът актуални отговори?

**Retrieval-Augmented Generation (RAG):**\
При заявка моделът търси в актуална външна база (търсене по векторен или текстов индекс) релевантни документи и използва техния контекст при генерирането на отговора. Така може да се получава свежа информация без повторно обучение на основната мрежа

**Параметрично ефективно дообучаване (LoRA, Adapters):**\
Вместо пълно преобучаване на модела в него се вграждат малки адаптери или low-rank матрици (LoRA), които се обучават върху новите данни. Това позволява бързо и евтино да се нагоди моделът към предметна област, стил или формат на отговорите. С новите факти е различно: дообучаването ги добавя зле и фактите, които трябва да са точни, е по-добре да се дават на модела чрез RAG.

**За LoRA направих отделна статия:**

[/bg/lora-fine-tuning/](/bg/lora-fine-tuning/)

**Целево редактиране на теглата (Model Editing):**\
Алгоритми от типа на MEMIT/ROME локално коригират теглата на модела, за да добавят или обновят конкретни факти, без да засягат останалата част от знанията

**Отделни бази от знания и графи:**\
Вместо да се съхраняват фактите вътре в параметрите на LLM, те се изнасят във външни KB или графи на знанията, които редовно се обновяват, а моделът само ги пита за информация

**Интеграция с уеб търсене и API:**\
Свързването на модела с живи източници – вградено уеб търсене в чат ботовете, извикване на външни API и инструменти (function calling, MCP сървъри) – директно връща свежо съдържание.

### По какъв начин LLM моделът разбира различни езици?

Моделът „разбира“ различните езици благодарение на това, че в процеса на обучение вижда текстове на много от тях и се учи да предсказва следващия фрагмент независимо от езика. Основните моменти:

**Голям мултиезиков корпус:**\
За обучението се събират текстове (уикипедия, книги, уеб страници от Common Crawl и други) на десетки и стотици езици. Например в отворения модел BLOOM е имало около 46 езика, като делът на всеки зависи от обема на наличните данни

**Обща токенизация subword:**\
Използват се алгоритми като BPE или SentencePiece, които разбиват думите на фрагменти (subword) и включват в речника символи и последователности от различни азбуки. Така моделът оперира с един-единствен набор от токени за всички езици

**Универсална архитектура на трансформъра:**\
В трансформъра едни и същи тегла участват в обработката на всякакви езици. Затова при обучение върху различни езици моделът намира общи закономерности (синтаксис, семантика) и използва кръстосан пренос на знания (cross-lingual transfer)

**Не всички езици и не във всички обеми:**\
Обучават се само върху онези езици, за които има достатъчно текстове. Редките или нискоресурсните езици попадат в отделни дообучавания или получават по-малък дял от данните, затова качеството на генериране за тях е по-ниско

**Специални дообучавания и адаптери:**\
За да се подобри познаването на малко известните езици, се прилага дообучаване (continual pretraining) върху локални данни или се вмъкват адаптери (adapters, LoRA), които фино коригират знанията на модела за конкретен език.

## Допълнителна информация

### Точност на вектора

<span id="27e7"></span>

Също така числата с плаваща запетая, от които се състои векторът, могат да имат различна точност:\
По-малката точност намалява размера на модела и ускорява обработката.

Обикновено моделите се обучават с пълна точност FP32 (float32), тоест векторът се състои от числа с дължина 32 бита.

### Квантуване (Quantization)

<span id="057d"></span>

за да се намали точността и да се олекоти моделът, се използва квантуване – процес на преобразуване на числата с плаваща запетая (например FP32) в по-компактни целочислени представяния (например INT8), със запазване на приблизителната стойност.\
При това се въвежда малка загуба на точност, но тя често не влияе критично на качеството на изхода.

```text
Формат: FP32 (float32), 32 бита = 4 байта
Предназначение: Пълна точност. Използва се при обучение на модели, а също и при точен инференс.
Осигурява максимална точност, но изисква много памет и изчислителни ресурси.
Двоична стойност: 01000000 01001001 00001111 11011011
Действителна стойност: 3.14159
Възможни типове квантуване: Не се използва – това е пълен (неквантуван) формат.

Формат: FP16 (float16), 16 бита = 2 байта
Предназначение: Половин точност. Използва се за ускорено обучение и инференс на GPU (например NVIDIA Tensor Cores).
По-бърз и 2 пъти по-икономичен по памет в сравнение с FP32.
Двоична стойност: 01000010 01001000
Действителна стойност: ≈ 3.140625
В GGUF файловете неквантуваните тегла с такава точност се маркират като F16.

Формат: BF16 (bfloat16), 16 бита = 2 байта
Предназначение: Алтернатива на FP16, използвана в TPU и някои GPU. Има същия експонент като FP32, но съкратена мантиса.
По-бърз и по-компактен е, като същевременно запазва диапазона на FP32.
Двоична стойност: 01000000 01001001
(това са просто първите два байта от представянето на FP32 – мантисата е съкратена)
Действителна стойност: ≈ 3.140625
В GGUF файловете се маркира като BF16.

Формат: INT8 (8 бита), scale = 0.125
Предназначение: Квантувано цяло число. Използва се в оптимизирани модели за инференс на CPU и мобилни устройства.
Изисква възстановяване на мащаба (scale) и на отместването (zero_point).
Квантувана стойност: 25
Двоична стойност: 00011001
Действителна стойност: 25 × 0.125 = 3.125
Възможни типове квантуване: Q8_0, Q8_1, Int8Affine, PerChannelQuant (ONNX), dynamic/int8 (TensorFlow Lite)

Формат: INT4 (Q4), 4 бита, scale = 0.5 (2 числа в 1 байт)
Предназначение: Много компресиран формат за езикови модели. Използва се в llama.cpp, GGUF и други системи.
Осигурява съществено намаляване на размера на модела. Изисква възстановяване (деквантуване) при стартиране.
Квантувана стойност:
7 (максималната стойност за signed 4-битов int: -8…+7)
Двоична стойност: 0111
Действителна стойност: 7 × 0.5 = 3.5
Възможни типове квантуване:
Q4_0, Q4_1, Q4_K_S, Q4_K_M (llama.cpp, GGUF)

Формат: INT2 (Q2), 2 бита, scale = 1.0 (4 числа в 1 байт)
Предназначение: Изключително компресиран формат за използване в LLM на устройства с ограничени ресурси.
Използва се в някои варианти на GGUF, MLC, а също и в експерименти с екстремално квантуване.
Квантувана стойност: 1 (максимум измежду стойностите: -2…+1)
Двоична стойност: 01
Действителна стойност: 1 × 1.0 = 1.0
Възможни типове квантуване: Q2_K (llama.cpp, GGUF)
Реалният Q2_K съхранява още блокови мащаби, затова на практика заема около 2.6 бита на тегло.

Формат: INT1 (Q1), 1 бит, scale = 2.0 (8 числа в 1 байт)
Предназначение: Минимално възможна точност. Използва се в бинарни невронни мрежи и прототипи.
Обикновено стойностите са -1 или +1. Прилага се рядко в LLM, но може да е полезен в BNN (Binary Neural Networks).
Квантувана стойност: 1
Двоична стойност: 1
Действителна стойност: 1 × 2.0 = 2.0
Възможни типове квантуване: BinaryNet, XNOR-Net (по-често в академични/експериментални BNN)
Съвременен пример за екстремално квантуване на LLM – BitNet b1.58 с тернарни тегла -1/0/+1 (~1.58 бита на тегло).
```

Също така моделите могат да имат допълнителни параметри на квантуване:

```text
_K
- Означава „K-блоково“ квантуване (K-Block Quantization).
- Теглата се разбиват на блокове с фиксирана дължина (например по 32 или 64 стойности).
- Във всеки блок се използват общи scale и zero_point.
- Това позволява съществено да се намали размерът на модела, като се запазва по-висока точност в сравнение с простото квантуване.
- Примери за формати: Q2_K, Q4_K, Q6_K, Q8_K

_0, _1
- Показват каква схема на квантуване се прилага:
  - _0: базова схема, без отмествания (bias), един scale на блок
  - _1: подобрена схема, с допълнителни bias или scale отмествания
- Използват се във формати: Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q8_1
- По правило _1 осигурява по-добра точност при незначително увеличение на размера

Етикети за вариант по размер: K_S, K_M, K_L

K_S
- Small – най-малкият и най-агресивно компресираният вариант
- Максимална икономия на памет за сметка на качеството

K_M
- Medium – среден компромис между точност и обем
- Най-популярният избор за локално стартиране

K_L
- Large – най-големият и най-точният от K-вариантите
- Малко повече памет, малко по-високо качество

Пример за име на модел:
„mistral-7b.Q4_K_M.gguf“ – това означава:
- Модел Mistral 7B
- Използва се квантуване Q4_K (4-битово K-блоково)
- Вариант по размер: Medium

Освен K-квантите в llama.cpp има по-нови I-кванти
(IQ2_XXS, IQ3_S, IQ4_XS и др.) – те дават по-добро качество
при същия размер за сметка на по-сложна схема на кодиране.
```

### Продуктово квантуване (Product Quantization)

Метод, който позволява силно да се компресират векторите на характеристиките (ембединги), като се разбиват на части и всяка част се кодира чрез най-близкия шаблон (клъстер). Използва се във векторни бази и при търсене на най-близки съседи (например FAISS); теглата на самите LLM обикновено не се компресират с него.

**Как работи:**

Има вектор (например с размер 128 числа). Това може да е ембединг на текст или изображение от невронна мрежа.

Разбиваме вектора на парчета – например 8 части по 16 числа (128 / 8 = 16).

За всяка позиция на парче обучаваме свой кодер (квантайзер) – върху големи обеми данни предварително се избира кои „шаблони“ (центрове на клъстери) приличат на възможните парчета.\
Например:\
първото парче може да прилича на шаблон №12,\
второто – на шаблон №3,\
третото – на шаблон №88,\
и така нататък.

Запазваме само номерата на тези шаблони. Вместо да съхраняваме 128 числа (float32 = 512 байта), съхраняваме например 8 числа (по 1 байт на шаблон) – общо 8 байта.

<span id="204e"></span>

**QAT (Quantization-Aware Training) квантуване:**\
това е метод за квантуване на невронни мрежи, при който квантуването се отчита още по време на обучението на модела. Той позволява да се постигне почти същата точност като при оригиналния модел с float параметри, като при това моделът ще използва по-компактни int8 или други нискоразредни формати, подходящи за ефективно стартиране на устройства с ограничени ресурси (например смартфони или микроконтролери).

<span id="7ea5"></span>

**Как работи QAT – стъпка по стъпка:**

<span id="ab05"></span>

**Модел във float32:**\
Обучението започва с обикновен модел, който използва числа с плаваща запетая (обикновено float32). Това осигурява висока точност и стабилност на обучението.\
**Имитация на квантуване по време на forward pass (fake quantization):**\
При всяко преминаване напред стойностите (тегла, активации) се емулират като квантувани, т.е. преобразуват се в int8, а след това обратно във float32. Това дава възможност на модела да „види“ грешките от квантуването още на етапа на обучение.

```text
float32 → int8 → float32
```

<span id="d07c"></span>

Така по време на обратното разпространение (backpropagation) градиентите се изчисляват по float32 версията, но грешките заради квантуването все пак влияят на обучението.\
**Обновяване на параметрите (backward pass):**\
Градиентите се изчисляват както обикновено, но с отчитане на изкривяванията от fake quantization. Това позволява на модела да се адаптира към това, че теглата и активациите впоследствие ще се използват с ниска точност.\
**Експорт на финалния модел в int8:**\
След приключване на обучението теглата наистина се квантуват в int8 и моделът може да бъде компилиран и пуснат в продукционна среда.

<span id="0584"></span>

**Какво точно се квантува:**\
Тегла (weights) – float32 → int8\
Активации (activations) – float32 → int8\
(Понякога се квантуват и градиентите и междинните състояния, но това е рядкост.)

<span id="fb18"></span>

**Защо да се използва QAT:**\
По-висока точност, отколкото при post-training quantization (PTQ)\
Ниска консумация на памет\
По-бързо изпълнение на CPU/GPU/NPUs с поддръжка на int8\
Особено важно за мобилни и embedded устройства (например Android)

<span id="82c4"></span>

### Колко памет е нужна на модела

Груба формула: брой параметри × байтове на тегло, плюс KV кеш, плюс малко за активациите.

```text
Модел 7-8B:
FP16 (2 байта на тегло)   ≈ 14-16 ГБ
Q8 (1 байт на тегло)      ≈ 7-8 ГБ
Q4 (~0.6 байта на тегло)  ≈ 4-5 ГБ

Модел 27-32B в Q4         ≈ 16-20 ГБ
Модел 70B в Q4            ≈ 40+ ГБ

Плюс KV кеш: зависи от дължината на контекста,
при десетки хиляди токена – още гигабайти.
```

Практическо правило за локално стартиране: да се гледа не „милиардите параметри“, а размерът на конкретния GGUF файл – той честно показва колко памет ще заемат теглата – и да се оставя запас за контекста.

### Начини за стартиране (бекенди) на LLM модели

**vLLM:**\
Основен сървърен двигател за LLM инференс: непрекъснато батчване на заявките и PagedAttention за икономичен KV кеш; де факто стандарт за внедряване на отворени модели на GPU.\
**Формати:** тегла на Hugging Face (.safetensors)

**SGLang:**\
Бързо растяща алтернатива на vLLM с повторно използване на KV кеша между заявките (RadixAttention); силен е при структурирано генериране и агентни натоварвания.\
**Формати:** тегла на Hugging Face (.safetensors)

**NVIDIA TensorRT-LLM:**\
Специализирана LLM надстройка над TensorRT: компилира модела за конкретни GPU на NVIDIA, максимална скорост за сметка на гъвкавостта.\
**Формат:** компилиран двигател

**ONNX Runtime:**\
Кросплатформен двигател за изпълнение на модели във формат ONNX.\
**Формат на файла:** .onnx

**TensorFlow (Inference API):**\
Стандартен двигател за модели на TensorFlow, поддържа SavedModel и замразен граф.\
**Формати:** папка SavedModel (saved_model.pb + variables/), единичен файл .pb

**LiteRT (бивш TensorFlow Lite):**\
Лек двигател за мобилни и вградени устройства, оптимизиран по размер и скорост.\
**Формат:** .tflite

**PyTorch (TorchScript):**\
Позволява да се сериализират и стартират модели без зависимост от Python, с JIT оптимизации.\
**Формати:** .pt, .pth

**NVIDIA TensorRT:**\
Хардуерно ускорен двигател за GPU на NVIDIA, компилира моделите (обикновено от ONNX) за конкретна карта.\
**Формат:** компилиран двигател .engine (или план UFF/ONNX → .engine)

**Intel OpenVINO:**\
Оптимизира и ускорява мрежи на CPU и Intel GPU/VPU, конвертира моделите в IR формат.\
**Формати:** .xml (структура) + .bin (тегла)

**Apple Core ML:**\
Фреймуърк за стартиране на модели на iOS/macOS, интегрира се с Xcode и се ускорява чрез Core ML Runtime.\
**Формати:** .mlpackage, остарял .mlmodel

**Microsoft ML.NET:**\
.NET двигател за инференс на CPU, подходящ за C# и F#.\
**Формат:** архив на модела .zip

**Apache TVM:**\
Компилира и оптимизира модели за разнообразен хардуер, създава нативни библиотеки.\
**Формати:** сериализиран Relay модул или компилирана библиотека (.so, .dll)

**Alibaba MNN:**\
Мобилен невронен двигател с широки оптимизации за ARM, поддържа и стартиране на сървър.\
**Формат:** .mnn

**Tencent NCNN:**\
Компактен двигател за мобилни CPU/GPU, без външни зависимости.\
**Формати:** .param (структура) + .bin (тегла)

**OpenCV DNN:**\
Модул за компютърно зрение с поддръжка на различни формати (ONNX, Caffe, TensorFlow, Darknet).\
**Формати:** зависи от изходния – .onnx, .pb, .caffemodel + .prototxt, .weights

**Unity Sentis (по-рано Barracuda):**\
Двигател за стартиране на невронни мрежи в игри на Unity, поддържа ONNX модели.\
**Формати:** .onnx, .sentis

**MLC LLM:**\
ML компилатор и двигател за LLM, компилира теглата в свой IR и ги разпределя на шардове.\
**Формат:** каталог …-MLC с .bin шардове и JSON конфигурации (например mlc-chat-config.json)

**MediaPipe:**\
Фреймуърк за създаване на мултимодални пайплайни (детекция, сегментация и др.), опира се на TFLite.\
**Формати:** .tflite (модел) + .pbtxt (граф)

**llama.cpp:**\
C++ двигател за LLaMA-подобни модели с поддръжка на квантуване на CPU/GPU.\
**Формат:** .gguf (старият GGML формат .bin е остарял)

**GGML:**\
Библиотека за ефективен инференс на модели (основа на llama.cpp и др.).\
**Формати:** .gguf, .bin

**NeuralMagic DeepSparse:**\
Оптимизиран двигател за разредени невронни мрежи на CPU.\
**Формат:** .onnx

**AWS Neuron SDK:**\
Двигател за ускоряване на инференса на AWS Inferentia, компилира моделите за Neuron.\
**Формати:** Neuron артефакти (.nef или библиотека)

По-нататък са изброени двигатели, които са по-скоро исторически: днес почти не се срещат, но се споменават в стари материали.

**Glow:**\
Компилатор и рънтайм от Facebook за невронни мрежи, входният формат е ONNX.\
**Формати:** междинен файл .bc или компилирана библиотека .so

**Apache MXNet:**\
Фреймуърк със собствен рънтайм за CPU/GPU, поддържа внедряване с Docker.\
**Формати:** .params (тегла) + .json (мрежа)

**Caffe:**\
Класически двигател за CV мрежи, често използван в изследвания.\
**Формати:** .caffemodel (тегла) + .prototxt (описание на мрежата)

### Приложения за стартиране на LLM модели

**LM Studio:**\
Десктоп приложение за Windows, macOS и Linux с GUI за стартиране на локални LLM (GPT-подобни).\
**Формати на моделите:** .gguf, .bin

**Ollama:**\
Лек CLI/GUI клиент за Windows, macOS и Linux, работи „от кутията“ с LLM.\
**Формати на моделите:** .gguf, .bin

**Jan:**\
Отворен десктоп клиент за Windows, macOS и Linux в духа на LM Studio, но изцяло open source; работи върху llama.cpp.\
**Формати на моделите:** .gguf

**Open WebUI:**\
Уеб интерфейс към Ollama и всякакви OpenAI-съвместими сървъри: чатове, RAG върху собствени документи, няколко потребители; инсталира се локално или на собствен сървър.\
**Формати на моделите:** тези, които поддържа свързаният бекенд

**KoboldCpp:**\
Еднофайлов билд на llama.cpp с уеб интерфейс и богати настройки на семплерите, популярен за творческо генериране.\
**Формати на моделите:** .gguf

**llama.cpp (prebuilt binaries):**\
Готови изпълними файлове за Windows/macOS/Linux, позволяват да се стартират LLaMA-подобни модели без инсталиране на зависимости.\
**Формати на моделите:** .bin, .gguf

Отделна група са приложенията за генериране на изображения (Stable Diffusion), не са LLM:

**Automatic1111 Stable Diffusion WebUI:**\
Най-популярният локален уеб интерфейс за генериране на изображения на Windows/macOS/Linux.\
**Формати на моделите:** .ckpt, .safetensors

**DiffusionBee:**\
Настолно приложение за macOS (има бета версии за Windows), „всичко в едно“ за Stable Diffusion.\
**Формати на моделите:** .ckpt, .safetensors

**InvokeAI:**\
Кросплатформен пакет с CLI и WebUI за генериране на изображения на базата на Stable Diffusion.\
**Формати на моделите:** .ckpt, .safetensors

### Формат за съхранение на LLM модели

<span id="b0af"></span>

**.pt / .pth**\
Използват се в PyTorch за запазване на обучени модели

<span id="b0af2"></span>

Съхраняват тегла и структура на модела (или само теглата).\
Основани са на сериализацията на Python (pickle), което ги прави не особено сигурни.\
Подходящи са за обучение и дообучаване.\
Не са оптимизирани за мобилен инференс или външен инференс\
Могат да заемат много памет (FP32).

<span id="d4ab"></span>

**.safetensors**\
Алтернатива на .pt, сигурен и бърз формат за PyTorch.

<span id="bcf5"></span>

Не използва pickle, затова е безопасен при зареждане.\
Поддържа паралелно зареждане, което ускорява работата.\
Само за съхранение на теглата (структурата е отделно).\
Подходящ е за инференс и дообучаване.\
Поддържа се от HuggingFace, PyTorch, JAX.

<span id="226a"></span>

**.bin**\
Универсален или частен двоичен формат за тегла на модели, особено в HuggingFace и в стария GGML.

<span id="c532"></span>

Може да е неформализиран (структурата зависи от фреймуърка).\
Може да съдържа пълни тегла или квантувани данни.\
Подходящ е за зареждане в персонализирани двигатели (например llama.cpp).\
Изисква точно да се знае как да се интерпретира съдържанието.\
Често се използва в стари проекти или персонализирани пайплайни.

<span id="0725"></span>

**.gguf**\
Стандарт на екосистемата GGML за стартиране на LLM в llama.cpp, Ollama, LM Studio.

<span id="da38"></span>

Включва всичко в един файл: тегла, речник, токенизатор, параметри на модела.\
Поддържа различни типове квантуване (Q2_K, Q4_0, Q8_1 и т.н.).\
Добре е компресиран и оптимизиран за стартиране на CPU, GPU, Android.\
Зарежда се бързо и лесно се обработва от C++ код.\
Основен избор за локално стартиране на LLM (Gemma, LLaMA, Mistral).

<span id="f864"></span>

**.onnx**\
Кросфреймуърк формат за стартиране на модел в различни среди (Windows, Web, C#, Java и др.).

<span id="e475"></span>

Стандартизиран е, поддържа се от много фреймуъркове (PyTorch, TF, Keras).\
Улеснява пренасянето на модели между платформи.\
Подходящ е за инференс (не за обучение).\
Оптимизира се със средствата на ONNX Runtime (компресиране, квантуване).\
Отличен избор за стартиране на модели в embedded среди и .NET.

<span id="65b9"></span>

**.tflite**\
Формат за стартиране на модели на TensorFlow на мобилни устройства (Android, iOS).

<span id="9d34"></span>

Много е компактен и бърз.\
Поддържа INT8 и FP16 квантуване.\
Работи с интерпретатора LiteRT (бивш TensorFlow Lite), лесно се вгражда в Android.\
Не може да се обучава или дообучава – само стартиране.\
Първоначално е бил предвиден за компактни CNN/RNN модели, но сега чрез LiteRT и MediaPipe LLM Inference върху него се стартират и малки LLM (например Gemma).

<span id="367f"></span>

**Формат MLC LLM**\
Стартиране на LLM на Android/iOS/GPU чрез компилация; единен файл-контейнер няма – моделът се съхранява като каталог с шардове на теглата и JSON конфигурации плюс компилирана библиотека.

<span id="cafb"></span>

Моделът предварително се компилира в ефективен байткод.\
Поддържа Vulkan/Metal/OpenCL за ускорение.\
Използва се за Gemma, Mistral, LLaMA на телефон.\
Изисква сложна подготовка (TVM, скриптове, настройка).\
Отлично подхожда за мобилни приложения без сървъри.

[ Copyright: Roman Kryvolapov ](https://t.me/RomanKryvolapov)
