Языковые модели для завершения предложений

Языковые модели для завершения предложений' (Language models for sentence completion)

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

Фото от Brett Jordan на Unsplash

Совместное авторство с Нарешем Сингхом.

Содержание

ВведениеПостановка проблемыМозговой штурм решения

  • Алгоритмы и структуры данных
  • Обработка естественного языка (NLP)
  • Глубокое обучение (нейронные сети)

Модель LSTM

  • Токенизация
  • Модель PyTorch
  • Использование модели для отсечения недопустимых предложений
  • Вычисление вероятности следующего слова

Модель Transformer

Заключение

Введение

Модели языка, такие как GPT, недавно стали очень популярными и используются для различных задач генерации текста, например, в ChatGPT или других системах разговорного искусственного интеллекта. Эти языковые модели огромны, часто содержат десятки миллиардов параметров и требуют больших вычислительных ресурсов и денег для работы.

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

Полный код для выполнения вывода на обученной модели можно найти в этом блокноте.

Постановка проблемы

Предположим, что мы создаем систему свайп-клавиатуры, которая пытается предсказать следующее слово, которое вы набираете на своем мобильном телефоне. Исходя из шаблона, следуемого свайпом, существует множество возможностей для предполагаемого слова пользователя. Однако многие из этих возможных слов не являются действительными словами на английском языке и могут быть исключены. Даже после этого начального этапа отсечения и исключения остается много кандидатов, и нам нужно выбрать одного в качестве предложения для пользователя.

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

Например, если пользователь ввел предложение “Я запланировал это”, а затем провел свайп, показанный ниже

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

  1. messing
  2. meeting

Однако, если подумать, вероятно, пользователь имел в виду “meeting”, а не “messing” из-за слова “scheduled” в предыдущей части предложения.

Что мы можем сделать программно для дальнейшего отсечения на основе всей известной информации? Давайте продумаем некоторые решения в следующем разделе.

Мозговой штурм решения

Алгоритмы и структуры данных

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

  1. Использование пространства: В английском языке может быть от 250 тысяч до 1 миллиона слов, не включая многочисленные собственные имена, которые постоянно увеличиваются в объеме. Следовательно, любое традиционное программное решение, моделирующее вероятность встречи пары слов, должно поддерживать таблицу поиска с 250 тысячами * 250 тысячами = 62,5 миллиарда пар слов, что является чрезмерным. Вероятно, многие пары встречаются не очень часто и могут быть отсечены. Даже после отсечения остается много пар, о которых нужно беспокоиться.
  2. Полнота: Кодирование вероятности встречи пары слов не учитывает всю проблему. Например, контекст предложения полностью теряется, когда вы смотрите только на самую последнюю пару слов. В предложении “Как проходит ваш день” если вы хотите проверить слово после “проходит”, у вас будет много пар, начинающихся с “проходит”. Это упускает весь контекст предложения перед этим словом. Можно представить себе использование слов-троек и т. д., но это усугубляет проблему использования пространства, упомянутую выше.

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

NLP (Natural Language Processing)

Исторически область NLP (обработка естественного языка) включала в себя понимание частей речи (POS) предложения и использование этой информации для выполнения таких операций обрезки и прогнозирования. Можно представить, что используется метка POS, связанная с каждым словом, чтобы определить, является ли следующее слово в предложении допустимым.

Однако процесс вычисления частей речи для предложения является самостоятельным сложным процессом и требует специализированного понимания языка, как показано на этой странице о маркировке частей речи в NLTK.

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

Глубокое обучение (нейронные сети)

Область NLP была перевернута с появлением глубокого обучения. С изобретением языковых моделей на основе LSTM и Transformer решение, как правило, заключается в подаче на модель некоторых высококачественных данных и обучении ее предсказывать следующее слово.

По сути, это то, что делает модель GPT. Модели GPT (Generative Pre-Trained Transformer) обучены предсказывать следующее слово (токен) при данном префиксе предложения.

Для префикса предложения “It is such a wonderful” модель, скорее всего, предоставит следующее как высоковероятные предсказания для следующего слова.

  1. день
  2. опыт
  3. мир
  4. жизнь

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

  1. красный
  2. мышь
  3. линия

Архитектура модели Transformer является основой систем, таких как ChatGPT. Однако для более ограниченного использования в области изучения семантики английского языка мы можем использовать более доступную модель архитектуры, такую ​​как модель LSTM (долгая краткосрочная память).

Модель LSTM

Давайте построим простую модель LSTM и обучим ее предсказывать следующий токен при данном префиксе токенов. Теперь вы можете спросить, что такое токен.

Токенизация

Обычно для языковых моделей токен может означать

  1. Один символ (или один байт)
  2. Целое слово в целевом языке
  3. Что-то между 1 и 2. Это обычно называется под-словом

Отображение одного символа (или байта) в токен очень ограничено, поскольку мы перегружаем этот токен, чтобы он содержал много контекста о том, где он встречается. Это потому, что символ “c”, например, встречается во многих разных словах, и чтобы предсказать следующий символ после символа “c”, нам нужно действительно внимательно посмотреть на предшествующий контекст.

Отображение одного слова в токен также проблематично, поскольку сам английский язык имеет от 250 тысяч до 1 миллиона слов. Кроме того, что происходит, когда добавляется новое слово в язык? Нужно ли нам вернуться и повторно обучить всю модель, чтобы учесть это новое слово?

Токенизация под-слов является промышленным стандартом в 2023 году. Она назначает подстроки байтов, часто встречающихся вместе, уникальным токенам. Обычно языковые модели имеют от нескольких тысяч (скажем, 4 000) до десятков тысяч (скажем, 60 000) уникальных токенов. Алгоритм определения, что является токеном, определяется алгоритмом BPE (кодирование байтовых пар).

Чтобы выбрать количество уникальных токенов в нашем словаре (называемое размером словаря), мы должны быть внимательными к нескольким вещам:

  1. Если мы выберем слишком мало токенов, мы вернемся к режиму токена на каждый символ, и модели будет трудно научиться чему-либо полезному.
  2. Если мы выберем слишком много токенов, мы окажемся в ситуации, когда таблицы вложений модели затмевают остальные веса модели, и ее становится сложно развернуть в ограниченной среде. Размер таблицы вложений будет зависеть от количества измерений, которые мы используем для каждого токена. Не редкость использовать размер 256, 512, 786 и т. д. Если мы используем размер вложения токена 512, и у нас есть 100 тысяч токенов, мы получим таблицу вложений, которая использует 200 МиБ памяти.

Следовательно, нам необходимо найти баланс при выборе размера словаря. В этом примере мы выбираем 6600 токенов и обучаем наш токенизатор с размером словаря 6600. Теперь давайте посмотрим на само определение модели.

Модель PyTorch

Сама модель довольно проста. У нас есть следующие слои:

  1. Векторное представление токенов (размер словаря = 6600, размерность вектора = 512), общий размер около 15 МБ (предполагая, что тип данных таблицы векторного представления – float32 с 4-байтовой размерностью)
  2. LSTM (количество слоев = 1, скрытая размерность = 786), общий размер около 16 МБ
  3. Многослойный перцептрон (размерности 786, 3144, 6600) общим размером около 93 МБ

Полная модель содержит около 31 млн обучаемых параметров, общим размером около 120 МБ.

Вот код PyTorch для модели.

class WordPredictionLSTMModel(nn.Module):    def __init__(self, num_embed, embed_dim, pad_idx, lstm_hidden_dim, lstm_num_layers, output_dim, dropout):        super().__init__()        self.vocab_size = num_embed        self.embed = nn.Embedding(num_embed, embed_dim, pad_idx)        self.lstm = nn.LSTM(embed_dim, lstm_hidden_dim, lstm_num_layers, batch_first=True, dropout=dropout)        self.fc = nn.Sequential(            nn.Linear(lstm_hidden_dim, lstm_hidden_dim * 4),            nn.LayerNorm(lstm_hidden_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(lstm_hidden_dim * 4, output_dim),        )    #        def forward(self, x):        x = self.embed(x)        x, _ = self.lstm(x)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

Вот сводка модели с использованием torchinfo.

Сводка модели LSTM

=================================================================Слой (тип:глубина-индекс) Параметры #=================================================================WordPredictionLSTMModel - ├─Вложение: 1–1 3,379,200├─LSTM: 1–2 4,087,200├─Sequential: 1–3 - │ └─Линейный: 2–1 2,474,328│ └─Нормализация слоя: 2–2 6,288│ └─LeakyReLU: 2–3 - │ └─Dropout: 2–4 - │ └─Линейный: 2–5 20,757,000=================================================================Всего параметров: 30,704,016Обучаемые параметры: 30,704,016Необучаемые параметры: 0=================================================================

Интерпретация точности: После обучения этой модели на 12 млн предложений на английском языке в течение примерно 8 часов на GPU P100, мы получили потерю 4.03, точность top-1 29% и точность top-5 49%. Это означает, что в 29% случаев модель смогла правильно предсказать следующий токен, а в 49% случаев следующий токен в обучающем наборе был одним из пяти наиболее вероятных предсказаний модели.

Каковы должны быть наши критерии успеха? Хотя точность top-1 и top-5 для нашей модели не впечатляют, они не так важны для нашей задачи. Нашим кандидатам на слова соответствует небольшой набор возможных слов, подходящих для жеста свайпа. Мы хотим, чтобы наша модель могла выбрать оптимального кандидата для завершения предложения таким образом, чтобы оно было синтаксически и семантически связным. Поскольку наша модель изучает природу языка через тренировочные данные, мы ожидаем, что она будет присваивать более высокую вероятность согласованным предложениям. Например, если у нас есть предложение “Бейсболист” и возможные завершения предложения (“бежал”, “плыл”, “прятался”), то слово “бежал” лучше подходит в качестве следующего слова, чем другие два. Поэтому, если наша модель предсказывает слово “бежал” с более высокой вероятностью, чем остальные, это работает для нас.

Интерпретация потери: Потеря 4.03 означает, что отрицательный логарифмический правдоподобие предсказания составляет 4.03, что означает, что вероятность правильного предсказания следующего токена равна e^-4.03 = 0.0178 или 1/56. Обычно у случайно инициализированной модели потеря составляет около 8.8, что равно -log_e(1/6600), поскольку модель случайным образом предсказывает 1/6600 токенов (6600 – размер словаря). В то время как потеря 4.03 может не казаться великой, важно помнить, что обученная модель примерно в 120 раз лучше неподготовленной (или случайно инициализированной) модели.

Далее давайте рассмотрим, как мы можем использовать эту модель, чтобы улучшить предложения нашей свайп-клавиатуры.

Использование модели для отсева недопустимых предложений

Давайте рассмотрим реальный пример. Предположим, у нас есть неполное предложение «I think» («Я думаю»), и пользователь делает свайп-жест, показанный синим цветом ниже, начиная с «o», проходя между буквами «c» и «v» и заканчивая между буквами «e» и «v».

Некоторые возможные слова, которые могут быть представлены этим свайп-жестом:

  1. Over
  2. Oct (сокращение от October)
  3. Ice
  4. I’ve (с апострофом, предполагается)

Из этих предложений наиболее вероятным, вероятно, будет «I’ve». Давайте подадим эти предложения в нашу модель и посмотрим, что она выдаст.

[I think] [I've] = 0.00087[I think] [over] = 0.00051[I think] [ice] = 0.00001[I think] [Oct] = 0.00000

Значение после знака = является вероятностью того, что слово является допустимым завершением префикса предложения. В этом случае мы видим, что слову «I’ve» была присвоена наивысшая вероятность. Следовательно, это наиболее вероятное слово для продолжения префикса предложения «I think».

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

Вычисление вероятности следующего слова

Для вычисления вероятности того, что слово является допустимым завершением префикса предложения, мы запускаем модель в режиме eval (inference) и подаем токенизированный префикс предложения. Мы также токенизируем слово, добавив в начало пробел. Это делается потому, что предварительный токенизатор HuggingFace разделяет слова с пробелами в начале слова, поэтому мы хотим убедиться, что наши входные данные согласуются с стратегией токенизации, используемой HuggingFace Tokenizers.

Предположим, что кандидатское слово состоит из 3-х токенов T0, T1 и T2.

  1. Сначала мы запускаем модель с исходным токенизированным префиксом предложения. Для последнего токена мы проверяем вероятность предсказания токена T0. Мы добавляем это в список «probs».
  2. Затем мы выполняем предсказание на префиксе+T0 и проверяем вероятность токена T1. Мы добавляем эту вероятность в список «probs».
  3. Затем мы выполняем предсказание на префиксе+T0+T1 и проверяем вероятность токена T2. Мы добавляем эту вероятность в список «probs».

В списке «probs» содержатся отдельные вероятности генерации токенов T0, T1 и T2 последовательно. Поскольку эти токены соответствуют токенизации кандидатского слова, мы можем перемножить эти вероятности, чтобы получить совокупную вероятность того, что кандидат является завершением префикса предложения.

Код для вычисления вероятностей завершения приведен ниже.

 def get_completion_probability(self, input, completion, tok):      self.model.eval()      ids = tok.encode(input).ids      ids = torch.tensor(ids, device=self.device).unsqueeze(0)      completion_ids = torch.tensor(tok.encode(completion).ids, device=self.device).unsqueeze(0)      probs = []      for i in range(completion_ids.size(1)):          y = self.model(ids)          y = y[0,:,-1].softmax(dim=0)          # prob is the probability of this completion.          prob = y[completion_ids[0,i]]          probs.append(prob)          ids = torch.cat([ids, completion_ids[:,i:i+1]], dim=1)      #      return torch.tensor(probs)  #

Ниже приведены еще несколько примеров.

[That ice-cream looks] [really] = 0.00709[That ice-cream looks] [delicious] = 0.00264[That ice-cream looks] [absolutely] = 0.00122[That ice-cream looks] [real] = 0.00031[That ice-cream looks] [fish] = 0.00004[That ice-cream looks] [paper] = 0.00001[That ice-cream looks] [atrocious] = 0.00000[Since we're heading] [toward] = 0.01052[Since we're heading] [away] = 0.00344[Since we're heading] [against] = 0.00035[Since we're heading] [both] = 0.00009[Since we're heading] [death] = 0.00000[Since we're heading] [bubble] = 0.00000[Since we're heading] [birth] = 0.00000[Did I make] [a] = 0.22704[Did I make] [the] = 0.06622[Did I make] [good] = 0.00190[Did I make] [food] = 0.00020[Did I make] [color] = 0.00007[Did I make] [house] = 0.00006[Did I make] [colour] = 0.00002[Did I make] [pencil] = 0.00001[Did I make] [flower] = 0.00000[We want a candidate] [with] = 0.03209[We want a candidate] [that] = 0.02145[We want a candidate] [experience] = 0.00097[We want a candidate] [which] = 0.00094[We want a candidate] [more] = 0.00010[We want a candidate] [less] = 0.00007[We want a candidate] [school] = 0.00003[This is the definitive guide to the] [the] = 0.00089[This is the definitive guide to the] [complete] = 0.00047[This is the definitive guide to the] [sentence] = 0.00006[This is the definitive guide to the] [rapper] = 0.00001[This is the definitive guide to the] [illustrated] = 0.00001[This is the definitive guide to the] [extravagant] = 0.00000[This is the definitive guide to the] [wrapper] = 0.00000[This is the definitive guide to the] [miniscule] = 0.00000[Please can you] [check] = 0.00502[Please can you] [confirm] = 0.00488[Please can you] [cease] = 0.00002[Please can you] [cradle] = 0.00000[Please can you] [laptop] = 0.00000[Please can you] [envelope] = 0.00000[Please can you] [options] = 0.00000[Please can you] [cordon] = 0.00000[Please can you] [corolla] = 0.00000[I think] [I've] = 0.00087[I think] [over] = 0.00051[I think] [ice] = 0.00001[I think] [Oct] = 0.00000[Please] [can]

Эти примеры показывают вероятность того, что слово завершит предложение раньше. Кандидаты сортируются в порядке убывания вероятности.

Поскольку модели на основе трансформаторов медленно заменяют модели LSTM и RNN для задач, связанных с последовательностями, давайте посмотрим, как выглядит модель трансформатора для той же цели.

Модель трансформатора

Модели, основанные на трансформаторах, являются очень популярной архитектурой для обучения языковых моделей для предсказания следующего слова в предложении. Конкретная техника, которую мы будем использовать, - это механизм причинно-следственного внимания. Мы будем обучать только слой кодировщика трансформатора в PyTorch с использованием причинно-следственного внимания. Причинно-следственное внимание означает, что каждый токен в последовательности может смотреть только на токены перед ним. Это напоминает информацию, которую использует однонаправленный слой LSTM при обучении только в прямом направлении.

Модель трансформатора, которую мы увидим здесь, основана непосредственно на nn.TransformerEncoder и nn.TransformerEncoderLayer в PyTorch.

import mathdef generate_src_mask(sz, device):    return torch.triu(torch.full((sz, sz), True, device=device), diagonal=1)#class PositionalEmbedding(nn.Module):    def __init__(self, sequence_length, embed_dim):        super().__init__()        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.pos_embed = nn.Parameter(torch.empty((1, sequence_length, embed_dim)))        nn.init.uniform_(self.pos_embed, -1.0, 1.0)    #        def forward(self, x):        return x * self.sqrt_embed_dim + self.pos_embed[:,:x.size(1)]    ##class WordPredictionTransformerModel(nn.Module):    def __init__(self, sequence_length, num_embed, embed_dim, pad_idx, num_heads, num_layers, output_dim, dropout, norm_first, activation):        super().__init__()        self.vocab_size = num_embed        self.sequence_length = sequence_length        self.embed_dim = embed_dim        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.embed = nn.Sequential(            nn.Embedding(num_embed, embed_dim, pad_idx),            PositionalEmbedding(sequence_length, embed_dim),            nn.LayerNorm(embed_dim),            nn.Dropout(p=0.1),        )        encoder_layer = nn.TransformerEncoderLayer(            d_model=embed_dim, nhead=num_heads, dropout=dropout, batch_first=True, norm_first=norm_first, activation=activation,        )        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)        self.fc = nn.Sequential(            nn.Linear(embed_dim, embed_dim * 4),            nn.LayerNorm(embed_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(embed_dim * 4, output_dim),        )    #        def forward(self, x):        src_attention_mask = generate_src_mask(x.size(1), x.device)        x = self.embed(x)        x = self.encoder(x, is_causal=True, mask=src_attention_mask)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

Мы можем подключить эту модель вместо модели LSTM, которую мы использовали ранее, поскольку ее API совместим. Эта модель требует больше времени для обучения на тех же объемах обучающих данных и имеет сопоставимую производительность.

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

Заключение

Мы увидели, как мы можем решать практические проблемы обработки естественного языка с использованием техник глубокого обучения на основе LSTM (RNN) и моделей трансформаторов. Не каждая языковая задача требует использования моделей с миллиардами параметров. Специализированные приложения, требующие моделирования самого языка, а не запоминания больших объемов информации, могут быть обработаны с помощью гораздо более компактных моделей, которые могут быть легко развернуты и эффективно использованы по сравнению с массовыми языковыми моделями, с которыми мы привыкли сталкиваться в наши дни.

Все изображения, кроме первого, были созданы автором(ами).