Перейти к содержимому
ПереводыАндрей Карпатый

microgpt: GPT в 200 строк, разбор по блокам

Построчный разбор microgpt Андрея Карпатого — обучения и генерации GPT на чистом Python без зависимостей: данные, токенизатор, автоград Value, параметры, внимание с KV-кэшем, MLP, Adam, цикл обучения и сэмплирование — фрагментами, с русскими комментариями и проверенными числами.

microgpt — «арт-проект» Андрея Карпатого, как он сам его называет: один файл на 200 строк чистого Python без единой зависимости, который обучает GPT и генерирует ею текст. В файле есть всё: набор данных, токенизатор, движок автоматического дифференцированияавтоматическое дифференцирование (автоград)EN automatic differentiation (autograd)Программа записывает операции в граф и сама считает производные по цепному правилу. Для loss → параметры используют обратный режим (reverse mode) — это и есть backprop., архитектура в духе GPT-2, оптимизатор Adam, цикл обучения и генерация. Строка из шапки файла задаёт тон всему разбору: «This file is the complete algorithm. Everything else is just efficiency» — «Этот файл — весь алгоритм. Всё остальное — лишь эффективность».

Где взять оригинал: gist microgpt.py, та же программа на странице karpathy.ai/microgpt.html, ноутбук в Google Colab и пост «microgpt» в блоге автора с подробным объяснением.

  • Модель — однослойный трансформер шириной 16 с 4 головами внимания и контекстом 16 символов, всего 4192 параметра. Данные — 32 033 английских имени, токены — отдельные буквы плюс служебный токен BOS, итого словарь из 27 токенов.
  • Каждое число модели — объект Value, который помнит, из чего получен, и локальную производную. Вызов loss.backward() — то же, что в PyTorch, только по одному скаляру за раз.
  • Модель обрабатывает один токен за вызов и хранит ключи и значения прошлых токенов в списках — это KV-кэшKV-кэшEN KV cache, причём прямо во время обучения. Причинная маска не нужна: будущих токенов в кэше просто нет.
  • За 1000 шагов (по одному имени на шаг) loss падает примерно с 3,3 — это случайное угадывание из 27 вариантов, −ln⁡(1/27)≈3,30-\ln(1/27) \approx 3{,}30, — до примерно 2,37. На MacBook автора это около минуты. Потом модель «галлюцинирует» правдоподобные новые имена.
  • Карпатый показывает, что отличает microgpt от ChatGPT: данные, BPE, тензоры и GPU, масштаб, пост-обучение, инженерия инференса. Но, по его словам, если вы понимаете microgpt, вы понимаете алгоритмическую суть.

Нужны две вещи, которые в курсе уже есть: автоград из модуля 3.2 (класс Value и обратный проход) и внимание из модуля 4.4. Лучший порядок:

  1. Пройдите урок 4.6.1: там та же модель, собранная по шагам и запускаемая в браузере.
  2. Прочитайте этот разбор с оригиналом рядом. Вы будете узнавать каждую строку, а разница в деталях покажет, какие решения произвольны.
  3. После урока 4.6.4 «Генерация и KV-кэш» вернитесь к блоку о функции gpt: станет ясно, почему кэш в microgpt нужен даже при обучении.
СтрокиБлокЧто делаетУрок курса
1–12шапка, импорты, random.seed(42)только os, math, random—
14–21данныескачивает names.txt, перемешивает 32 033 имени4.1.2
23–27токенизаторбуквы → номера 0…25, BOS = 264.2.1
29–72класс Valueавтоград: прямой проход и backward()3.2
74–90параметрыгиперпараметры, матрицы весов, список params4.6.1
92–106кирпичикиlinear, softmax, rmsnorm4.5.1
108–144функция gptэмбеддинги, внимание с KV-кэшем, MLP, логиты4.4, 4.6.4
146–149Adamгиперпараметры и буферы моментов4.6.3
151–184цикл обученияимя → токены → loss → backward → шаг Adam4.6.3
186–200генерациясэмплирование с температурой 0,54.1.3
flowchart LR
A["Имя: emma"] --> B["Токены: BOS e m m a BOS"]
B --> C["gpt(): по одному токену, KV-кэш растёт"]
C --> D["27 логитов → softmax"]
D --> E["loss = среднее −log p(следующего токена)"]
E --> F["loss.backward(): градиенты всех 4192 Value"]
F --> G["Adam: сдвинуть параметры"]
G -->|"следующее имя"| A
Один шаг обучения microgpt

Шапка — три строки манифеста: «самый атомарный способ обучить GPT и запустить её на чистом Python без зависимостей», «этот файл — весь алгоритм», «всё остальное — лишь эффективность». Импортируются только три модуля стандартной библиотеки. Комментарии в стиле «Да будет…» проходят через весь файл — например, у фиксированного зерна случайности:

import os # os.path.exists
import math # math.log, math.exp
import random # random.seed, random.choices, random.gauss, random.shuffle
random.seed(42) # «Да будет порядок среди хаоса»

Фиксированное зерно делает запуск воспроизводимымвоспроизводимостьEN reproducibilityВозможность получить тот же результат, повторно запустив тот же код на тех же данных.: у всех получатся одни и те же начальные веса и тот же порядок имён. Мы проверили: первые значения loss совпадают с теми, что приведены в посте автора (3,3660 и 3,4243).

Если файла input.txt нет, программа скачивает список имён из репозитория makemore (ссылка закреплена на конкретном коммите). Каждая непустая строка — отдельный «документ», и список перемешивается:

docs = [line.strip() for line in open('input.txt') if line.strip()]
random.shuffle(docs)

Программа печатает num docs: 32033. Мысль Карпатого из поста: для модели вроде ChatGPT ваш разговор — тоже просто «документ» необычного вида, а ответ модели — статистическое продолжение документа.

uchars = sorted(set(''.join(docs))) # уникальные символы набора становятся токенами 0..n-1
BOS = len(uchars) # номер служебного токена BOS (Beginning of Sequence — «начало последовательности»)
vocab_size = len(uchars) + 1 # всего уникальных токенов: +1 — это BOS

В именах встречаются только строчные латинские буквы, поэтому uchars — это a…z, номера 0…25, а BOS = 26. Словарь — 27 токенов. Сами номера ничего не значат, это просто имена отдельных символов: «вместо 0, 1, 2 могли бы быть разные эмодзи», как пишет автор. BOS работает как разделитель: при обучении имя оборачивается им с двух сторон, [BOS, e, m, m, a, BOS]. Первый BOS значит «начинается имя», второй — «имя закончилось». Настоящие модели используют подсловный BPEBPE, кодирование пар байтовEN Byte Pair Encoding (BPE)Алгоритм токенизации — многократно сливает самую частую пару соседних токенов в новый токен. Изначально (Gage, 1994) — алгоритм сжатия данных. (см. лекцию о токенизаторе), но принцип тот же.

Это самая математическая часть, и у Карпатого есть на неё отдельная 2,5-часовая лекция о micrograd. Объект Value хранит одно число и помнит, как оно получено:

class Value:
__slots__ = ('data', 'grad', '_children', '_local_grads') # экономия памяти в Python
def __init__(self, data, children=(), local_grads=()):
self.data = data # значение узла, считается при прямом проходе
self.grad = 0 # производная loss по этому узлу, считается при обратном проходе
self._children = children # «дети» узла в графе вычислений
self._local_grads = local_grads # локальные производные узла по каждому из детей

Каждая операция — «кубик лего»: считает результат и сразу записывает локальные производные. Например, умножение:

def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data, (self, other), (other.data, self.data))

Для a⋅ba \cdot b производная по aa равна bb, а по bb — aa: это и есть кортеж (other.data, self.data). Так же устроены сложение (производные 1 и 1), степень (n⋅an−1n \cdot a^{n-1}), log (1/a1/a), exp (eae^a) и relu (1, если a>0a > 0, иначе 0). Вычитание, деление и унарный минус Карпатый выражает через уже имеющиеся операции: например, деление — это умножение на other**-1. Поэтому «настоящих» кубиков всего шесть.

Обратный проход — топологическая сортировкатопологическая сортировкаEN topological sortПорядок узлов ориентированного графа без циклов, в котором каждый узел идёт после всех, от кого зависит. backward() обходит его в обратную сторону. графа и цепное правило:

def backward(self):
topo = []
visited = set()
def build_topo(v): # обход в глубину: дети попадают в список раньше родителя
if v not in visited:
visited.add(v)
for child in v._children:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1 # dL/dL = 1
for v in reversed(topo): # от loss к параметрам
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad # цепное правило; += потому что путей может быть несколько

Формула в последней строке читается так: если узел vv зависит от ребёнка cc, то ∂L∂c+=∂v∂c⋅∂L∂v\frac{\partial L}{\partial c} \mathrel{+}= \frac{\partial v}{\partial c} \cdot \frac{\partial L}{\partial v}. Плюс-равно нужен потому, что одно число может участвовать в графе в нескольких местах, и вклады всех путей складываются. Пример из поста: a=2a = 2, b=3b = 3, L=a⋅b+a=8L = a \cdot b + a = 8. Тогда ∂L/∂a=b+1=4\partial L / \partial a = b + 1 = 4 (два пути через aa), а ∂L/∂b=a=2\partial L / \partial b = a = 2 — ровно то, что выдаст .backward() в PyTorch. Смысл числа 4: если увеличить aa на 0,001, то LL вырастет примерно на 0,004.

n_layer = 1 # глубина трансформера (число слоёв)
n_embd = 16 # ширина сети (размерность эмбеддингов)
block_size = 16 # максимальная длина контекста (самое длинное имя — 15 символов)
n_head = 4 # число голов внимания
head_dim = n_embd // n_head # размерность одной головы: 4
matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]

matrix создаёт список строк из случайных чисел нормального распределения со стандартным отклонением 0,08, каждое обёрнуто в Value. Дальше словарь state_dict (слово взято из PyTorch) собирает именованные матрицы: эмбеддинги токенов wte (27 × 16), позиций wpe (16 × 16), выходную матрицу lm_head (27 × 16) и для каждого слоя — четыре матрицы внимания attn_wq, attn_wk, attn_wv, attn_wo (16 × 16) и две матрицы MLP mlp_fc1 (64 × 16) и mlp_fc2 (16 × 64). Все числа затем выпрямляются в один список params, чтобы оптимизатору было что перебирать.

Посчитаем параметры сами: эмбеддинги и выход — 27⋅16+16⋅16+27⋅16=112027 \cdot 16 + 16 \cdot 16 + 27 \cdot 16 = 1120; внимание — 4⋅16⋅16=10244 \cdot 16 \cdot 16 = 1024; MLP — 2⋅64⋅16=20482 \cdot 64 \cdot 16 = 2048. Итого 1120+1024+2048=41921120 + 1024 + 2048 = 4192 — ровно столько печатает программа (num params: 4192). Для сравнения: у GPT-2 было 1,6 млрд параметров, у современных LLM — сотни миллиардов.

Блок 6. Кирпичики: linear, softmax, rmsnorm (строки 92–106)

Заголовок раздела «Блок 6. Кирпичики: linear, softmax, rmsnorm (строки 92–106)»

Комментарий перед блоком: архитектура следует GPT-2 «с небольшими отличиями: layernorm → rmsnorm, без смещений, GeLU → ReLU».

def linear(x, w): # умножение матрицы на вектор: по скалярному произведению на строку w
return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
def softmax(logits): # логиты -> вероятности (неотрицательные, сумма 1)
max_val = max(val.data for val in logits)
exps = [(val - max_val).exp() for val in logits] # вычитаем максимум, чтобы exp не переполнился
total = sum(exps)
return [e / total for e in exps]
def rmsnorm(x): # делим вектор на его среднеквадратичное значение
ms = sum(xi * xi for xi in x) / len(x)
scale = (ms + 1e-5) ** -0.5
return [xi * scale for xi in x]
  • linear — главный строительный блок нейросети: обучаемое линейное преобразование.
  • В softmax вычитание максимума математически ничего не меняет, но защищает от переполнения в exp. Обратите внимание: max_val взят как обычное число (.data), а не Value — через него градиент не течёт, и это корректно, потому что результат от сдвига не зависит.
  • rmsnorm приводит вектор к единичному среднеквадратичному значению: активации не раздуваются и не затухают от слоя к слою (подробно — урок 4.5.1). Обучаемого множителя, как у RMSNorm в больших моделях, здесь нет.

Модель — функция без состояния: на вход номер токена, номер позиции и кэши ключей и значений прошлых позиций, на выход — 27 логитов для следующего токена.

Эмбеддинги.

tok_emb = state_dict['wte'][token_id] # эмбеддинг токена: строка таблицы
pos_emb = state_dict['wpe'][pos_id] # эмбеддинг позиции
x = [t + p for t, p in zip(tok_emb, pos_emb)] # «что это за токен» + «где он стоит»
x = rmsnorm(x) # примечание автора: не лишнее из-за обратного прохода через остаточную связь

Современные LLM обычно обходятся без таблицы позиций и используют относительные схемы вроде RoPEротационные позиционные эмбеддинги (RoPE)EN rotary position embedding (RoPE)RoPE предложен в Китае (Су Цзяньлинь и соавторы, компания Zhuiyi). Сейчас используется почти во всех LLM. (так сделано, например, в nanochat).

Внимание. Для каждого слоя: запомнить вход для остаточной связи, нормализовать, получить запрос, ключ и значение (q, k, v — по linear с матрицами слоя), а k и v дописать в кэш слоя: keys[li].append(k). Затем для каждой из 4 голов:

for h in range(n_head):
hs = h * head_dim # начало «своего» куска вектора
q_h = q[hs:hs+head_dim] # запрос этой головы
k_h = [ki[hs:hs+head_dim] for ki in keys[li]] # ключи всех позиций 0..t
v_h = [vi[hs:hs+head_dim] for vi in values[li]] # значения всех позиций 0..t
attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 for t in range(len(k_h))]
attn_weights = softmax(attn_logits) # веса внимания по прошлым позициям
head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)]
x_attn.extend(head_out) # склеиваем выходы голов

Это ровно формула вниманиямасштабированное скалярное внимание (SDPA)EN scaled dot-product attention (SDPA)softmax(Q Kᵀ / √d_k) V — формула внимания трансформера (Vaswani и др., 2017). В PyTorch — F.scaled_dot_product_attention. softmax ⁣(qK⊤/d)V\mathrm{softmax}\!\left(qK^\top / \sqrt{d}\right) V, записанная циклами: скалярное произведение запроса на каждый ключ, деление на 4=2\sqrt{4} = 2, softmax и взвешенная сумма значений. Выходы голов склеиваются в вектор из 16 чисел, проходят через attn_wo и прибавляются к сохранённому входу (остаточная связь).

Где причинная маска? Её нет и не нужно: в кэше лежат только позиции 0…t0 \ldots t, будущее ещё не вычислено. Карпатый подчёркивает: блок внимания — единственное место, где токен на позиции tt «видит» прошлые токены. Внимание — это механизм общения токенов.

MLP. Опять остаточная связь и нормализация, затем расширение 16 → 64, ReLU и сжатие обратно 64 → 16:

x = linear(x, state_dict[f'layer{li}.mlp_fc1']) # 16 -> 64
x = [xi.relu() for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2']) # 64 -> 16
x = [a + b for a, b in zip(x, x_residual)] # + остаток

В отличие от внимания, MLP работает только с текущей позицией — здесь модель «думает» над тем, что собрало внимание. Трансформер чередует общение (внимание) и вычисление (MLP). Остаточные связи дают градиенту прямой путь через сеть и делают глубокие модели обучаемыми.

Выход. logits = linear(x, state_dict['lm_head']) — 27 чисел, по одному на токен словаря: чем больше, тем вероятнее, по мнению модели, этот токен следующий.

learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
m = [0.0] * len(params) # буфер первого момента (скользящее среднее градиентов)
v = [0.0] * len(params) # буфер второго момента (скользящее среднее квадратов градиентов)

Комментарий автора — «Да будет Adam, благословенный оптимизатор, и его буферы». Обратите внимание на необычные значения: β1=0,85\beta_1 = 0{,}85 и β2=0,99\beta_2 = 0{,}99 вместо привычных 0,9 и 0,999 — для короткого обучения на шумных одиночных примерах. О самом AdamAdamEN Adam (adaptive moment estimation)Momentum + RMSProp + поправка смещения. Стандартный оптимизатор нейросетей с 2015 года. — урок 4.6.3.

Один шаг — одно имя. Пять действий: взять документ, прогнать вперёд, посчитать loss, прогнать назад, обновить параметры.

doc = docs[step % len(docs)]
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS] # emma -> BOS e m m a BOS
n = min(block_size, len(tokens) - 1) # сколько предсказаний делаем

Затем для каждой позиции: вызвать gpt, превратить логиты в вероятности и взять минус логарифм вероятности правильного следующего токена — это кросс-энтропияперекрёстная энтропияEN cross-entropy:

logits = gpt(token_id, pos_id, keys, values)
probs = softmax(logits)
loss_t = -probs[target_id].log() # насколько модель «удивлена» правильным ответом
losses.append(loss_t)
loss = (1 / n) * sum(losses) # итоговый средний loss по имени. «Пусть ваш будет низким»

Если модель дала правильному токену вероятность 1, loss равен 0. Если почти 0 — loss уходит в бесконечность. Один вызов loss.backward() проводит градиент через весь граф — softmax, модель, все 4192 параметра.

Обновление Adam с линейным затуханием learning rate:

lr_t = learning_rate * (1 - step / num_steps) # линейное затухание learning rate
for i, p in enumerate(params):
m[i] = beta1 * m[i] + (1 - beta1) * p.grad # «инерция»: среднее последних градиентов
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2 # средний квадрат: свой масштаб шага для каждого параметра
m_hat = m[i] / (1 - beta1 ** (step + 1)) # поправка на то, что m и v стартуют с нуля
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
p.grad = 0 # обнулить градиент перед следующим шагом

Последняя строка важна: градиенты в Value накапливаются через +=, и без обнуления следующий шаг сложил бы их со старыми. Это та же причина, по которой в PyTorch вызывают optimizer.zero_grad().

Что увидите. Программа печатает номер шага и loss. Начало — около 3,3 (случайное угадывание из 27 вариантов: −ln⁡(1/27)≈3,30-\ln(1/27) \approx 3{,}30), за 1000 шагов loss опускается примерно до 2,37. Один шаг занимает заметное время: каждое сложение — вызов функции Python и новый объект. Тот же расчёт на GPU обрабатывает миллионы чисел параллельно.

Параметры заморожены. Начинаем с BOS и повторяем: прогнать токен через gpt, разделить логиты на температурутемпература (сэмплирования)EN temperatureЛогиты делятся на T перед softmax. T < 1 — распределение острее, T > 1 — площе, T → 0 — greedy., выбрать следующий токен случайно по вероятностям, подать его на вход снова — пока модель не выдаст BOS («имя закончилось») или не кончится контекст:

temperature = 0.5 # в (0, 1]: «креативность» текста, от низкой к высокой
...
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
if token_id == BOS:
break

Температура 1 — сэмплировать прямо из выученного распределения. Меньше 1 (здесь 0,5) — распределение «острее», модель осторожнее. Близко к 0 — всегда самый вероятный токен (жадное декодирование). Больше 1 — разнообразнее, но бессвязнее. Подробно — урок 4.1.3. В посте автор приводит 20 сгенерированных имён — от вполне настоящих («anna», «lara», «anton») до новых («kamon», «karai», «yeran»).

В разделе поста «Real stuff» («Настоящее») Карпатый проходит те же блоки и перечисляет, что меняется в боевых системах. Алгоритм и общая схема при этом остаются.

БлокmicrogptНастоящие LLM
Данные32 тысячи имёнтриллионы токенов интернета, кода, книг; дедупликация, фильтрация, смеси
Токенизаторбуквы, 27 токеновBPE, около 100 тысяч токенов
Автоградскаляры Value на Pythonтензоры на GPU/TPU, PyTorch, ядра вроде FlashAttention
Архитектура4192 параметра, 1 слой, ширина 16сотни миллиардов параметров, 100+ слоёв, ширина 10 000+; RoPE, GQA, «воротные» активации, MoE
Обучениеодно имя за шагбатчи по миллионам токенов, накопление градиентов, bf16/fp8, тысячи GPU месяцами
ОптимизацияAdam, линейное затуханиетщательно подобранные расписания, законы масштабирования (Chinchilla), много пробных запусков
Пост-обучениенетSFT на диалогах, затем RL
Инференсцикл по токенамбатчинг запросов, страничный KV-кэш (vLLM), спекулятивное декодирование, квантизация

Каждой строке таблицы в курсе посвящён свой модуль: от 4.2 и 5.4 до 6.1 и 7.1.

  • «Понимает» ли модель что-нибудь? Механически — никакой магии: большая математическая функция, превращающая токены в распределение вероятностей следующего токена. Называть ли это пониманием — решать вам, но весь механизм лежит в этих 200 строках.
  • Почему это работает? Оптимизатор понемногу сдвигает тысячи параметров так, чтобы loss падал. Параметры оседают в значениях, которые ловят статистику данных: имена часто начинаются с согласной, «qu» ходят вместе, три согласные подряд редки. Явных правил модель не учит — только распределение, которое их отражает.
  • Как это связано с ChatGPT? Тот же цикл «предсказать токен — выбрать — повторить», только огромный и с пост-обучением для диалога. Системный промпт, ваше сообщение и ответ — просто токены одной последовательности.
  • Откуда галлюцинации? Модель выбирает токены по вероятностям и не знает, что истинно. Выдуманное имя «karia» и уверенно сказанная неправда у ChatGPT — одно и то же явление: правдоподобное продолжение, которого нет в реальности.
  • Как получить имена лучше? Обучать дольше (num_steps), увеличить модель (n_embd, n_layer, n_head), взять больше данных — те же ручки, что и в больших моделях.
  • А если сменить данные? Модель выучит то, что в них есть: названия городов, покемонов, английские слова, короткие стихи. Остальной код менять не нужно.
  1. Скачайте microgpt.py из gist и запустите: python microgpt.py. Нужен только Python. Проверьте, что программа печатает num docs: 32033, vocab size: 27, num params: 4192, а первые значения loss — 3,3660 и 3,4243.
  2. Для быстрого опыта уменьшите num_steps до 100 и посмотрите на имена. Потом верните 1000 и сравните.
  3. Смените данные: положите в input.txt русские имена (по одному в строке, например из урока 4.1.2). Сколько теперь токенов в словаре и параметров в модели? Пересчитайте по формуле из блока 5.
  4. Поставьте n_layer = 2 и n_embd = 32: как изменятся число параметров, время шага и итоговый loss?
  5. Пройдите эволюцию файла слоями «луковицы» в gist build_microgpt.py: по ревизиям видно, как таблица биграмм (train0) превращается в MLP с ручными градиентами, затем получает автоград, внимание с одной головой, многоголовое внимание и, наконец, Adam.
  6. Сделайте задания урока 4.6.1 — там та же модель с проверкой прямо в браузере.
ENRU
dependency-freeбез зависимостей (только стандартная библиотека Python)
autograd, computation graphавтоматическое дифференцирование, граф вычислений
local gradient, chain ruleлокальная производная, цепное правило
topological orderтопологический порядок
BOS (Beginning of Sequence)токен начала последовательности
state dictсловарь параметров модели
token / position embeddingэмбеддинг токена / позиции
query, key, valueзапрос, ключ, значение
KV cacheKV-кэш
residual connectionостаточная связь
logitsлогиты
cross-entropy lossкросс-энтропия
first / second moment, bias correctionпервый / второй момент, поправка смещения
linear learning rate decayлинейное затухание learning rate
temperature, greedy decodingтемпература, жадное декодирование
hallucinationгаллюцинация