Перейти к содержимому
8/30Глава 8 из 30

Предсказание next-token: embeddings и что означает перплексия

Обучаем символьную модель на 32 033 именах и показываем, почему gradient descent заново открывает таблицу подсчетов.

На этой странице

Вот десять имен, созданных программой, которая никогда не видела ни одного слова:

TEXT
cexze   momakurailezitynn   konimittain   llayn   ka
da      moliellavo          emia          sade    ftlsp

Ни одно из них не является именем. Почти все они пытаются им быть. Они произносимы, заканчиваются там, где заканчиваются имена, а одно из них — emia — отличается от настоящего всего на одну букву. Программа, которая их создала, хранит 729 чисел, не имеет понятия о слове, слоге или человеке и была настроена одним проходом подсчета соседних пар букв.

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

Цель — это тождество, а не дизайнерский выбор

Ссылка на раздел: Цель — это тождество, а не дизайнерский выбор

Глава 7 оставила вас с последовательностью целых чисел и без причины, по которой одно должно следовать за другим. Вот эта причина, и она умещается в одной строке из Главы 2.

Языковая модель — это функция, которая берет уже имеющиеся tokens и возвращает распределение по тому, какой token будет следующим: одно число на каждую запись словаря, неотрицательное, в сумме равное единице. Больше ничего. Чтобы перейти от этого к вероятности целого документа, применим цепное правило вероятности:

P(x1,x2,,xT)=t=1TP(xtx1,,xt1)P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, \ldots, x_{t-1})

Это тождество, верное для любой последовательности чего угодно, без каких-либо дополнительных предположений. Значит, модель, которая решает маленькую задачу — следующий token при известных предыдущих, — уже решила большую задачу присвоения вероятности каждому возможному документу, точно и бесплатно. Популярное представление об этом как о дешевом трюке («она всего лишь предсказывает следующее слово») переворачивает логику: предсказывать следующий token и значит моделировать совместное распределение. Второй задачи никогда не было.

Loss следует из этого так же механически. В каждой позиции модель выдает распределение qq, а истина — один известный token, поэтому кросс-энтропия из Главы 4 применяется без изменений:

L=1Tt=1Tlogqθ(xtx<t)L = -\frac{1}{T}\sum_{t=1}^{T} \log q_\theta(x_t \mid x_{<t})

Это среднее отрицательное log-likelihood — рецепт Главы 2, только с категориальным распределением на месте гауссовского. А поскольку истинное распределение one-hot, его энтропия равна нулю, и по тождеству из Главы 4 кросс-энтропия равна KL-дивергенции: снижать это число и подтягивать убеждения модели к данным — одно и то же действие.

Одно следствие заслуживает отдельного предложения, потому что это экономический факт под всей областью. Метки — это данные, сдвинутые на одну позицию. Никто ничего не размечает. Триллион tokens текста — это триллион заранее размеченных примеров, поэтому обучающий корпус современной модели — это «интернет», а не «датасет, который кто-то собрал».

До любой сети — baseline: 32 033 имени, по одному в строке, и задача производить новые имена по одной букве за раз.1

Словарь состоит из 26 букв плюс граничный символ ., отмечающий и начало, и конец имени, так что модель должна выучить, где имена начинаются и где заканчиваются. Это 27 символов, и минимально возможная модель — таблица того, как часто каждый символ следовал за каждым другим символом.

bigram.pyPYTHON
N = torch.zeros((27, 27), dtype=torch.int32)
for w in words:
    cs = ["."] + list(w) + ["."]
    for a, b in zip(cs, cs[1:]):
        N[stoi[a], stoi[b]] += 1

P = N.float()
P = P / P.sum(1, keepdim=True)            # one distribution per row   

Две строки арифметики — и модель обучена. И это не эвристика: деление подсчетов на суммы строк — maximum-likelihood оценка для категориального распределения, то есть рецепт Главы 2 с уже проделанным дифференцированием.

TEXT
names: 32033        train/val/test: 25626 / 3203 / 3204
training bigrams: 182583

the six most likely letters after 'a':
    a -> '.'  0.1944   a -> 'n'  0.1600   a -> 'r'  0.0967
    a -> 'l'  0.0749   a -> 'h'  0.0690   a -> 'y'  0.0606

Сэмплируйте из нее — выбирайте букву из строки текущей буквы, переходите к этой строке, повторяйте, пока не появится граничный символ, — и получите имена из начала главы. Они терпят неудачу специфическим и информативным образом: локально правдоподобны, глобально бессмысленны. Каждая соседняя пара букв в momakurailezitynn встречается в настоящих именах; просто их семнадцать подряд. У модели память в одну букву, поэтому она не может знать, что тянется уже слишком долго.

Loss на отложенных именах — 2.4546 ната. Само по себе это число ничего не значит, поэтому существует перплексия:

PPL=exp ⁣(1Ttlogq(xtx<t))=eL\mathrm{PPL} = \exp\!\left(-\frac{1}{T}\sum_t \log q(x_t \mid x_{<t})\right) = e^{L}

В развернутом виде, без библиотеки, которая делает работу за нас:

perplexity.pyPYTHON
@torch.no_grad()
def perplexity(logits, Y):
    logp = F.log_softmax(logits, dim=1)          # log q for every symbol
    chosen = logp[torch.arange(len(Y)), Y]       # log q of the one that came next   
    return torch.exp(-chosen.mean())             

Возведение в экспоненту отменяет логарифм и возвращает число к единицам подсчета вещей. Чистый способ увидеть, что именно оно считает, — измерить модель, которая вообще ничего не знает: она назначает вероятность 1/271/27 каждому символу независимо от контекста:

TEXT
uniform over 27 symbols            loss 3.2958 nats   ppl  27.000
bigram counts, add-one smoothed    loss 2.4546 nats   ppl  11.642

Ровно 27.000, потому что elog27=27e^{\log 27} = 27. Перплексия — это эффективное число равновероятных вариантов, между которыми выбирает модель. Перплексия 27 означает «никакого понятия, может быть что угодно». Значение 11.642 у count-модели означает, что одна буква контекста оставляет ее столь же неуверенной, как человека, который вслепую выбирает примерно из двенадцати вариантов вместо двадцати семи, — поэтому цитируют перплексию, а не сырой loss.

С ней возникают две проблемы, и вторая встречается даже в опубликованных статьях.

Нулевые вероятности фатальны. Из 729 ячеек таблицы 113 ни разу не встречаются в обучении — 15,5 % таблицы пусто. Это не проблема, пока отложенная выборка не попадет в такую ячейку, а в validation семь биграмм именно так и делают, среди них dq, zj и qo дважды. Нулевая вероятность означает log -\infty, а значит бесконечный loss и бесконечную перплексию: одно имя из трех тысяч уничтожает метрику. Обычная заплатка — добавить 1 к каждому подсчету перед нормализацией; здесь это почти ничего не стоит (2.4546 вместо 2.4524). Но заплатка — это признание. Count-модель вообще не умеет обобщать. У нее нет способа заподозрить, что qo правдоподобно, потому что qu часто встречается, а o ведет себя как u в других местах: у нее нет понятия, что два символа могут быть похожи. Каждая ячейка выучивается отдельно, и исправлению этого посвящена остальная часть главы.

Перплексия — это цена за token, а token — свободный параметр. Именно эта ошибка постоянно всплывает при сравнении моделей, и ее легко заметить, стоит только посмотреть. Возьмите тот же корпус английской прозы из Главы 7, ту же интерполированную bigram-модель, и измените только способ разбиения текста:

единицасловарьtokens в тестекросс-энтропияперплексиябиты на символ
символы7614 4692.521712.453.6378
BPE, 512 merges3296 8713.854747.212.6407
BPE, 2 048 merges1 8204 2335.7468313.202.4254
слова2 9916 2843.562735.262.2322

Перплексия в этих строках меняется в 25 раз. В модели ничего не изменилось; изменился только размер того, что предсказывается. Предсказать целое слово труднее, чем букву, поэтому это стоит дороже за одно предсказание — зато самих предсказаний нужно меньше.

Теперь посмотрите на последний столбец: он вместо этого делит общую стоимость на число символов и переводит ее в биты. Он переупорядочивает таблицу. По перплексии рейтинг такой: символы, слова, BPE-512, BPE-2048; по битам на символ — слова, BPE-2048, BPE-512, символы. Символьная модель падает с первого места на последнее. Модель с 2 048 merges, которая по перплексии выглядит в 6,6 раза хуже модели с 512 merges, на самом деле лучше: 2.4254 бита против 2.6407.

Итак, перплексию можно сравнивать только у двух моделей с одним и тем же tokenizer, а модели с разными tokenizers можно сравнивать только в битах на символ — величине, которую Шеннон измерял в 1951 году, заставляя людей угадывать следующую букву английского текста, и ограничил примерно одним битом на символ.2 Наша лучшая bigram-модель находится на уровне 2,23 бита, что неплохо резюмирует, как далеко этой главе еще предстоит продвинуться.

Теперь построим ту же модель как сеть. Ей потребуются на порядки больше арифметики, чтобы прийти туда же, и именно это «туда же» здесь важно.

Заменим таблицу одной матрицей весов WW формы 27×2727 \times 27. Превратим текущую букву в one-hot вектор, умножим и назовем результат logits — ненормализованными оценками из Главы 4. Затем softmax, затем кросс-энтропия, затем gradient descent.

neural_bigram.pyPYTHON
W = torch.randn((27, 27), requires_grad=True)

for step in range(3000):
    logits = W[xs]                            
    loss = F.cross_entropy(logits, ys)
    W.grad = None
    loss.backward()
    W.data -= 50.0 * W.grad

Выделенная строка содержит определение, которое стоит запомнить. Умножение one-hot вектора на матрицу выбирает одну ее строку, так что умножение — это lookup, и каждая реализация пропускает арифметику и делает lookup напрямую; именно это и есть W[xs].

Это embedding-таблица. Матрица с одной строкой на каждую запись словаря, индексируемая по token id. Никакой геометрии, никакой семантики, никакого отдельного алгоритма: lookup-таблица, содержимое которой просто выучивается gradient descent вместе со всем остальным. Все мистические заявления об «embedding space» заканчиваются здесь.

Обучите ее и посмотрите, куда она приходит:

TEXT
  step     1   train 3.7550   val 3.3882   max gap to the count table 0.757269
  step   100   train 2.4732   val 2.4726   max gap to the count table 0.388354
  step  1000   train 2.4557   val 2.4549   max gap to the count table 0.041862
  step  3000   train 2.4547   val 2.4544   max gap to the count table 0.004048

Последний столбец — наибольшее абсолютное различие между любой ячейкой softmax(W) и соответствующей ячейкой таблицы подсчетов, и оно стремится к нулю. После 3 000 шагов самое большое расхождение среди всех 729 ячеек — 0.004048, а среднее — 0.000224. Худшая ячейка — qi, встречавшаяся всего двенадцать раз во всем обучающем наборе; среди 22 строк с более чем тысячей вхождений худшее расхождение — 0.000562.

TEXT
                 count table   network
    a -> '.'        0.1945     0.1945
    a -> 'n'        0.1601     0.1601
    a -> 'r'        0.0967     0.0967

Gradient descent, начиная со случайных чисел и не зная ничего, кроме «сделай log-вероятность следующей буквы большой», заново открыл таблицу подсчетов. И был обязан: подсчеты — это maximum-likelihood оценка, кросс-энтропия — отрицательный log-likelihood, значит обе процедуры оптимизируют одну и ту же цель, а у этой цели один оптимум. Сеть не выучила что-то похожее на подсчет. Она медленно сошлась к подсчету.

Возникает справедливый вопрос: зачем тогда вообще утруждаться? Потому что таблице подсчетов дальше идти некуда, а сети — есть куда.

Расширим модель, чтобы она смотрела более чем на один предыдущий символ. Это архитектура Bengio 2003 года, прямой предок каждой модели в оставшейся части курса:4 возьмем последние три символа, проведем каждый через embedding-таблицу в 10-мерную строку, склеим строки в 30 чисел, пропустим их через hidden layer из Главы 5 и завершим output layer, выдающим по одному logit на каждую запись словаря.

mlp.pyPYTHON
C  = torch.randn((27, 10))          # the embedding table
W1 = torch.randn((3 * 10, 200))     # the hidden layer from Chapter 5
W2 = torch.randn((200, 27))         # one output per vocabulary entry

emb = C[X].view(-1, 30)             # three lookups, concatenated   
h = torch.tanh(emb @ W1 + b1)
logits = h @ W2 + b2                
loss = F.cross_entropy(logits, Y)

Обратите внимание, что здесь новое, а что нет. Hidden layer — тот же, что в Главе 5, без изменений; loss — тот же, что в Главе 4, без изменений. Новизна — embedding-таблица в начале и output layer шириной со словарь из Главы 7. И именно вторая часть дорога в каждой когда-либо построенной языковой модели, потому что в реальном словаре 100 000 записей, а это матричное умножение выполняется в каждой позиции.

Тот же код, обученный идентично, меняется только размер context window:

contextпараметрыvalidation lossvalidation перплексия
подсчет, 1 символ7292.454611.642
neural, 1 символ7 8972.457711.678
neural, 3 символа11 8972.11458.285
neural, 8 символов21 8972.05067.773

Интересна вторая строка. Сеть с hidden layer на 200 единиц и в одиннадцать раз большим числом параметров, чем таблица подсчетов, работает ровно так же хорошо, как таблица подсчетов, и не лучше. Capacity никогда не было ограничением. Один символ context допускает определенный loss, и ничто добавленное сверху не может опуститься ниже, потому что нужной информации там нет.

Дайте ей три символа, и перплексия падает с 11.68 до 8.29 — снижение на 29 %, купленное 4 000 дополнительными параметрами. Она побеждает подсчет здесь именно по причине, диагностированной ранее: count-модели по трехсимвольным contexts нужны 273=19,68327^3 = 19{,}683 строк, большинство из них пусты или содержат одно наблюдение, и каждую она учит отдельно. Сеть делится информацией. Если a, e и i получают похожие embedding-строки, то то, что она выучила после bra, переносится на bre, даже если bre она никогда не видела. Этот перенос — вся ценность embedding-таблицы, и это разрыв между строками два и три.

Сэмплы улучшаются соответственно:

TEXT
deliah   nellara   joce     kael      quintis
salayson  reety    khyrmin  mahnen    madiaryxia

Это все еще не список настоящих имен. Но deliah, nellara и kael не выглядели бы в таком списке неуместно, а бесконечные монстры исчезли: самое длинное из двадцати сэмплов count-модели — девятнадцать букв, самое длинное из двадцати у этой модели — тринадцать.

Что на самом деле внутри embedding-таблицы

Ссылка на раздел: Что на самом деле внутри embedding-таблицы

Таблица — это 27×1027 \times 10: по одной строке из десяти чисел на символ, все инициализированы случайно и изменялись только gradient следующей буквы loss. Никто ничего туда не закладывал. Что же в итоге оказалось внутри?

Инструмент для вопроса — косинусное сходство, то есть скалярное произведение из Главы 1, деленное на длины:

cos(a,b)=abab\cos(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\lVert \mathbf{a} \rVert \, \lVert \mathbf{b} \rVert}

Оно измеряет угол между двумя векторами и игнорирует их длины — именно это нужно, когда длина строки отражает, как часто ее token появлялся, а не то, что он значит. Сначала нормализуйте каждый вектор до длины 1 — как реальные системы делают один раз при индексировании, — и косинусное сходство станет просто скалярным произведением.

Вот ближайшие соседи нескольких символов в обученной таблице:

TEXT
  'c' -> 'k':+0.598      'j' -> 'z':+0.650      'i' -> 'y':+0.541
  'u' -> 'e':+0.482      'a' -> 'h':+0.367      '.' -> 'q':+0.077

Часть этого соответствует фольклору. c и k взаимозаменяемы в именах, как и i с y; j и z — редкие, в основном начальные согласные, которые ведут себя похоже. Граничный символ . не близок почти ни к чему — 0.077 до ближайшей буквы, — потому что это единственный символ, который обозначает позицию, а не звук.

А часть — нет. Ближайший сосед ah, а не другая гласная. В среднем по всем парам:

TEXT
mean cosine, vowel to vowel         : +0.1889
mean cosine, consonant to consonant : +0.0765
mean cosine, vowel to consonant     : -0.0042

Гласные больше похожи друг на друга, чем на согласные, и эффект реален, но мал. При сравнении с 2 000 случайно выбранных групп по пять букв 58 из этих групп разделяются не хуже — разрыв значим примерно на уровне p=0.03p = 0.03. Значит, эффект реален, но совсем не похож на четкий геометрический остров, который подразумевают популярные рассказы об embeddings.

Это честное описание embedding-таблицы, и его стоит сохранить на весь оставшийся курс. Это не карта смысла. Это смена координат, выученная, а не спроектированная, чья единственная задача — облегчить работу следующего слоя: та же фраза, которую Глава 5 использовала для hidden layer, складывавшего плоскость, чтобы решить XOR. Любая структура, которую вы в ней находите, находится там потому, что снижала loss; структуры, которая loss не снижает, там просто нет.

word2vec, GloVe и арифметика, которую все цитируют

Ссылка на раздел: word2vec, GloVe и арифметика, которую все цитируют

Если полезная часть — это таблица, можно охотиться за ней напрямую. Это word2vec: оставить embedding lookup, выбросить языковую модель.5

Цель skip-gram with negative sampling — одна строка. Для настоящей пары (центр, context), взятой из корпуса, подтолкнуть их скалярное произведение вверх; для kk фальшивых пар, взятых из шумового распределения, — вниз:6

logσ(vcvo)+i=1klogσ(vcvni)\log \sigma(\mathbf{v}_c \cdot \mathbf{v}_o) + \sum_{i=1}^{k} \log \sigma(-\mathbf{v}_c \cdot \mathbf{v}_{n_i})

Это бинарная классификация — «эти два слова действительно встречались вместе?» — и она дешева именно потому, что никогда не касается полного словаря, что и сделало обучение на миллиардах слов практичным в 2013 году. GloVe приходит к похожим векторам с другой стороны: факторизует матрицу глобальных подсчетов совместной встречаемости вместо потокового прохода по локальным окнам.7 Оба метода подгоняются ровно под ту статистику, из которой была построена таблица подсчетов. Это подсчет, сжатый.

Обученные на text8 — 17 005 207 слов английской Wikipedia, 71 290 из которых встречаются минимум пять раз, 100 измерений, три прохода, — векторы получают свойство, которое сделало их знаменитыми:

TEXT
king     -> charles 0.700, son 0.693, queen 0.686, henry 0.669, throne 0.667
physics  -> chemistry 0.672, electromagnetism 0.661, quantum 0.654, theoretical 0.624
guitar   -> bass 0.733, vocals 0.732, acoustic 0.728, guitars 0.703, drums 0.685
three    -> seven 0.892, two 0.877, one 0.875, five 0.871, four 0.870

Никто не задавал категорию для инструментов или числительных. Теперь знаменитая часть: возьмите king, вычтите man, добавьте woman и найдите ближайший вектор к результату.

TEXT
king - man + woman
   nothing excluded : king 0.693, elizabeth 0.657, wife 0.629, woman 0.607
   a, b, c excluded : elizabeth 0.657, wife 0.629, mary 0.607   (queen is 4th, 0.604)

Ближайший вектор к king - man + womanking. Это не причуда одного примера. Набор оценок Mikolov задает вопросы вида a : b :: c : ? — 8 869 семантических (paris : france :: rome : italy) и 10 675 синтаксических (walking : walked :: swimming : swam), — и среди 4 103 семантических вопросов, на которые этот словарь может ответить, победителем оказывается одно из трех входных слов в 99,8 % случаев. В опубликованных демонстрациях об этом не говорят, потому что стандартное правило оценки удаляет a, b и c перед поиском. Это легитимное правило, и оно делает больше работы, чем арифметика:

как выбирается ответсемантическиесинтаксические
offset, с исключением входов (стандарт)17.0 %11.9 %
offset, без исключений0.1 %0.4 %
ближайший сосед только c, входы исключены13.1 %9.3 %
ближайший сосед только b, входы исключены2.3 %0.4 %

Третья строка — та, над которой стоит задержаться. Выбросьте a и b, не делайте никакой арифметики, верните то, что ближе всего к c, — и вы сохраняете 77 % семантической оценки. Большая часть того, что выглядит как аналогическое рассуждение, — это близость плюс правило, запрещающее очевидные ответы; именно это Linzen измерил на правильно обученных векторах, и именно это воспроизводят baseline выше.8 Эти конкретные векторы малы — 17 миллионов слов против миллиардов в опубликованных моделях, — поэтому проценты стоит читать как форму, а не как state of the art. Форма сохраняется в любом масштабе: арифметика реальна, но гораздо слабее той единственной демонстрации, которую цитируют все.

Статические и контекстные: один вектор на слово или один на вхождение

Ссылка на раздел: Статические и контекстные: один вектор на слово или один на вхождение

Во всем, что было до сих пор, в структуре данных встроено жесткое ограничение. У таблицы одна строка на token. Слово bank получает один вектор — тот же самый в предложении о реке и в предложении об ипотеке; иначе быть не может, потому что lookup по id не зависит ни от чего другого.

Исправление — перестать читать вектор из таблицы и начать вычислять его из предложения. Это contextual embedding, введенный ELMo в 2018 году и сделанный стандартом BERT в том же году.910 На реальной модели числа резче объяснения:

TEXT
sentence A: "He sat on the bank of the river and watched the water go by."
sentence B: "She deposited the cheque at the bank on the corner of the street."

static vector for 'bank' (a row of the input embedding table)
    cosine A vs B ........................ 1.000000

contextual vector for 'bank', layer by layer
    layer  |  A vs B  |  A vs another river sentence  |  B vs another money sentence
        0  |  0.9512  |            0.9512             |            0.9359
        4  |  0.5647  |            0.8987             |            0.7716
        9  |  0.4284  |            0.8699             |            0.7568
       12  |  0.5278  |            0.8702             |            0.7335

Первая строка точна, а не приблизительна: статический вектор для bank — одни и те же 768 чисел в обоих предложениях, поэтому cosine равен 1 по построению. Девять слоев спустя два вхождения находятся на 0.43, тогда как bank в двух разных речных предложениях остается на 0.87. Никто нигде в этом процессе не размечал значения; значения разделились, потому что их разделение упрощает выполнение обучающей цели — угадывать скрытый token по соседям.

Две детали стоят внимания. Layer 0 уже равен 0.9512, а не 1.0, потому что position embeddings добавлены, и слово стоит в разных местах двух предложений. И сходство снова растет на layers 11 и 12: финальные слои pretrained-модели специализированы под ее обучающую цель и часто не являются лучшим местом, откуда брать представление.

Показать детали

Опционально: weight tying.

В bert-base-uncased embedding-таблица — это 30,522×76830{,}522 \times 768, 23 440 896 чисел, 21,4 % от 109 482 240 параметров модели. В маленькой языковой модели эта доля еще больше, поэтому один прием стал почти универсальным: входная таблица и output layer, который производит logits, — это одна и та же матрица, используемая один раз как row lookup и один раз транспонированной.11 Output layer уже назначает каждой записи словаря вектор — он берет скалярное произведение с каждой, — а tying говорит, что вектор, используемый, чтобы читать token, и вектор, используемый, чтобы его писать, должны быть одним и тем же объектом. Это одновременно уменьшает число параметров и улучшает перплексию, что достаточно редко, чтобы заметить.

Чтобы искать по корпусу по смыслу, нужен один вектор на предложение. Если они есть, поиск тривиален — это вся суть semantic retrieval, а Глава 19 обо всем, что находится вокруг этого:

search.pyPYTHON
E = normalise(embed(sentences))       # (200, d), every row of length 1
q = normalise(embed([query]))         # (1, d)
scores = q @ E.T                      # one matrix multiply   
top5 = scores[0].argsort()[::-1][:5]

Так что единственный настоящий вопрос — откуда берется embed. Очевидный ход — взять pretrained языковую модель, прогнать через нее каждое предложение и усреднить token-векторы. Вот этот метод против четырех альтернатив, оцененный двумя способами: ранговая корреляция между cosine и человеческими оценками сходства на 1 379 парах STS benchmark, и top-1 retrieval по индексу, построенному из 200 самых сильных перефразированных пар — одна сторона каждой пары индексируется, другая используется как запрос.

как embedded предложениеранговая корреляцияtop-1 на индексе из 200 предложений
бинарное пересечение слов (вообще без модели)0.550089.0 %
среднее статических векторов, обученных выше0.526385.5 %
BERT, token [CLS]0.203067.0 %
BERT, среднее token-векторов0.472984.0 %
MiniLM, обученный контрастивно0.820392.0 %

Читайте средние три строки на фоне первых двух. Pretrained transformer на 109 миллионов параметров, использованный очевидным способом, хуже оценивает сходство предложений, чем подсчет числа общих слов — и хуже, чем усреднение 100-мерных векторов text8, обученных минуту назад. Token [CLS], который tutorials до сих пор рекомендуют, потому что BERT предобучался с привязанной к нему целью уровня предложения, хуже половины этого результата.

Это не дефект BERT. Это цель. Языковая модель обучается так, чтобы ее hidden states предсказывали token; ничто в этом не требует, чтобы две перефразировки оказались рядом, и ничто не вознаграждает геометрию, в которой cosine означает «один и тот же смысл». Последняя строка — модель в пять раз меньшего размера (22 713 216 параметров), обученная на совершенно другом loss: contrastive learning, где примеры — пары: вопрос и ответ, предложение и его перефразировка, — а цель притягивает истинные пары друг к другу и отталкивает сэмплированные отрицательные. Это вклад Sentence-BERT и начало всей индустрии embedding-моделей.12 Dense Passage Retrieval применяет тот же рецепт напрямую к поиску, с одним encoder для запросов и одним для passages.13

Итак, практическое правило:

Embedding model — это не языковая модель с удаленным последним слоем. Это другая модель с другой целью, обычно намного меньшая, чей cosine означает именно то, что вам нужно, потому что она обучалась на парах, где это было target. Таблица выше — цена подмены одного другим.

И это семейство плохо справляется с порядком слов. «The dog bit the man» и «the man bit the dog» имеют одинаковые мешки слов, поэтому word overlap и среднее статических векторов дают им cosine ровно 1.000000, а mean-pooled BERT, который видит позицию, все равно оказывается почти там же — и контрастивно обученный MiniLM тоже ставит их на 0.979. Если ваша retrieval-задача зависит от того, кто что кому сделал, никакой порог cosine вас не спасет.

Глава 19 строит production-систему retrieval на этой основе и приходит к конкретному порогу cosine. Последнее измерение в этой главе делает такое число обоснованным, а не магическим.

Проклятие размерности в одной таблице

Ссылка на раздел: Проклятие размерности в одной таблице

У реальных embeddings сотни или тысячи компонентов, и расстояния там ведут себя странно. Возьмите 1 000 случайных точек в единичном кубе размерности dd и посмотрите на отношение между самым большим и самым маленьким расстоянием среди любых двух точек:

измеренияближайшая парасамая дальняя параотношение
20.00071.36121921.66
100.23612.33979.91
1003.00475.17521.72
1 00011.780914.03061.19
10 00039.615242.01251.06

В десяти тысячах измерений самая дальняя пара точек всего на 6 % дальше самой близкой. Все примерно равноудалено от всего остального, «nearest neighbour» почти перестает нести информацию, и это проклятие размерности — а также одна из причин, почему большие vector databases не делают точный nearest-neighbour search. Другая сторона той же медали — то, что делает пороги cosine рабочими: по тысяче пар случайных единичных векторов средний cosine находится на 0.0052-0.0052 в 100 измерениях и +0.0003+0.0003 в 768, со стандартными отклонениями 0.0968 и 0.0357, — и в 768 измерениях только 0,2 % случайных пар превышают 0.1 по абсолютной величине. Измеренное сходство 0.4, следовательно, не означает «похожи на 40 %»; оно далеко за пределами всего, что дает случайность, поэтому пороги между 0.3 и 0.7 отделяют сигнал от шума, а не сидят посередине.

Модель в этой главе читает фиксированное число предыдущих символов, делает lookup каждого и склеивает результаты по порядку. У такого дизайна две проблемы, и это одна и та же проблема.

Посмотрите снова на таблицу context: переход от трех символов к восьми почти удвоил число параметров и дал 0.06 ната. Стоимость растет линейно с context — каждая дополнительная позиция требует собственного куска первой матрицы весов, — а польза нет. Доведите это до тысячи tokens, и один только первый слой станет тяжелее всей остальной модели, причем большая часть уйдет на позиции, которые не важны для данного предсказания.

И это вторая проблема: у модели нет способа решить, какие из предыдущих tokens важны. Позиция два получает свои веса, позиция семь — свои, навсегда, что бы в них ни находилось. Когда модель пишет nell, решающий символ — тот, что непосредственно перед ним. Когда предложение содержит местоимение, слово, определяющее его референт, может быть на сорок tokens назад — и никакой фиксированный слот нельзя назначить «сорок назад», потому что в следующий раз это будет шесть.

Нам нужна модель, которая для каждого предсказания вычисляет, насколько сильно должен учитываться каждый более ранний token, — веса по context, создаваемые содержанием, а не фиксируемые раскладкой. Если записать это аккуратно, все начинается с чего-то совершенно будничного: среднего по предыдущим tokens. Затем пусть веса этого среднего будут выучены, и пусть они зависят от того, какой token задает вопрос.

Это attention, и это Глава 9.


Также стоит читать параллельно: главу 3 книги Jurafsky and Martin Speech and Language Processing, где n-gram модели, smoothing и перплексия рассматриваются гораздо тщательнее, чем здесь позволяет место, включая объяснение, почему interpolation и back-off лучше добавления единицы; конспекты Stanford CS229 §17.1–17.2 о языковом моделировании с вероятностной стороны; и статью Linzen выше — она короткая, и ее стоит прочитать целиком.

  1. Пример генерации имен, датасет и переход от таблицы подсчетов к сети в стиле Bengio следуют серии Andrej Karpathy building makemore, первые две части которой — лучший спутник для этой главы.

  2. Shannon, C. E. Prediction and Entropy of Printed English. Bell System Technical Journal 30(1), pp. 50–64 (1951). Испытуемые-люди, угадывающие следующую букву английского текста, и исходное измерение битов на символ.

  3. Shannon, C. E. A Mathematical Theory of Communication. Bell System Technical Journal 27 (1948). Теорема кодирования источника и отождествление предсказания со сжатием.

  4. Bengio, Y., Ducharme, R., Vincent, P. and Jauvin, C. A Neural Probabilistic Language Model. Journal of Machine Learning Research 3, pp. 1137–1155 (2003). Архитектура, использованная выше: embedding на слово, конкатенированный по фиксированному окну, через hidden layer, к softmax по словарю.

  5. Mikolov, T., Chen, K., Corrado, G. and Dean, J. Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781 (2013). CBOW и skip-gram, а также набор аналогий, использованный выше.

  6. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. and Dean, J. Distributed Representations of Words and Phrases and their Compositionality. arXiv:1310.4546 (2013). Negative sampling, subsampling частых слов и шумовое распределение, возведенное в степень 3/4, использованные выше.

  7. Pennington, J., Socher, R. and Manning, C. GloVe: Global Vectors for Word Representation. EMNLP 2014. Векторы слов из факторизации глобальной матрицы совместной встречаемости вместо потоковых локальных окон.

  8. Linzen, T. Issues in evaluating semantic spaces using word analogies. RepEval 2016, arXiv:1606.07736. Источник offset-free baseline, воспроизведенных выше.

  9. Peters, M. et al. Deep contextualized word representations. arXiv:1802.05365 (2018). ELMo: один вектор на вхождение, вычисляемый bidirectional языковой моделью.

  10. Devlin, J., Chang, M.-W., Lee, K. and Toutanova, K. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv:1810.04805 (2018). Модель, измеренная в эксперименте с bank.

  11. Press, O. and Wolf, L. Using the Output Embedding to Improve Language Models. arXiv:1608.05859 (2016), and Inan, H., Khosravi, K. and Socher, R. Tying Word Vectors and Word Classifiers. arXiv:1611.01462 (2016). Два независимых аргумента в пользу одного и того же приема.

  12. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. arXiv:1908.10084 (2019). Его первое измерение — mean-pooled BERT уступает усредненным статическим векторам в сходстве предложений — воспроизведено таблицей выше.

  13. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2004.04906 (2020). Контрастивное обучение двух-encoder retriever; прямой предок retrieval-стека из Главы 19.

Готовы доверить выбор модели LIA?

Создавайте со всеми ИИ-моделями в одном месте — начните бесплатно уже сегодня.