Постройте языковую модель на основе своих чатов WhatsApp

Создайте языковую модель, основанную на своих WhatsApp чатах

Визуальное руководство по архитектуре GPT с применением

Фото Владимира Грищенко на Unsplash

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

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

В этой статье я расскажу вам о своем путешествии по созданию (небольшой) языковой модели, которая генерирует синтетические разговоры, используя мои сообщения в WhatsApp в качестве входных данных. По пути я постараюсь разобрать работу архитектуры GPT визуально и, надеюсь, доступно, дополненную фактической реализацией на Python. Вы можете найти полный проект на моем GitHub.

Примечание: Сам класс модели в значительной степени взят из видео-серии Андрея Карпати и адаптирован для моих нужд. Очень рекомендую его уроки.

lad-gpt

Обучите языковую модель с нуля на основе групповых чатов в WhatsApp.

github.com

Оглавление

  1. Выбранный подход
  2. Источник данных
  3. Токенизация
  4. Индексирование
  5. Архитектура модели
  6. Обучение модели
  7. Режим чата

1. Выбранный подход

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

  1. Построение модели: Это включает создание и обучение модели с нуля, обеспечивая максимальную гибкость в выборе архитектуры модели и данных для обучения.
  2. Донастройка: Этот подход использует существующую предварительно обученную модель, корректируя ее веса для более тесного соответствия конкретным данным.
  3. Инженерия запросов: Здесь также используется существующая предварительно обученная модель, но уникальный корпус данных включается непосредственно в запрос, без изменения весов модели.

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

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

2. Источник данных

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

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

pattern = r'\[(.*?)\] (.*?): (.*)'matches = re.findall(pattern, text)text = [(x1, x2.lower()) for x0, x1, x2 in matches]

[    (2018-03-12 16:03:59, "Alice", "Привет, как вы, ребята?"),    (2018-03-12 16:05:36, "Tom", "У меня все хорошо, спасибо!"),    ...]

Теперь каждый элемент был обработан отдельно.

  • Дата отправки: Кроме преобразования в объект datetime, эта информация не использовалась. Однако можно посмотреть на временные дельты, чтобы выделить начало и конец обсуждений темы.
  • Имя контакта: При токенизации текста каждое имя контакта рассматривается как отдельный токен. Это гарантирует, что сочетание имени и фамилии все еще считается одной сущностью.
  • Сообщение в чате: В конце каждого сообщения добавляется специальный токен “<END>”.

3. Токенизация

Для обучения языковой модели нам необходимо разбить язык на части (так называемые токены) и постепенно передавать их модели. Токенизацию можно выполнять на нескольких уровнях.

  • На уровне символов: Текст воспринимается как последовательность отдельных символов (включая пробелы). Такой подход позволяет формировать все возможные слова из последовательности символов. Однако сложнее улавливать семантические связи между словами.
  • На уровне слов: Текст представлен в виде последовательности слов. Однако словарь модели ограничен имеющимися словами в обучающих данных.
  • На уровне подслов: Текст разбивается на подслова, которые меньше слов, но больше символов.

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

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

from nltk.tokenize import RegexpTokenizerdef custom_tokenizer(txt: str, spec_tokens: List[str], pattern: str="|\d|\\w+|[^\\s]") -> List[str]:    """    Разбивает текст на слова или символы с использованием токенизатора RegexpTokenizer из NLTK,    учитывая заданные специальные сочетания как отдельные токены.    :param txt: Корпус в виде одной строковой единицы.    :param spec_tokens: Список специальных токенов (например, конец, неизвестное слово).    :param pattern: По умолчанию корпус токенизируется по уровню слов (разделение по пробелам).                    Числа считаются одиночными токенами.    >> Примечание: Шаблон для токенизации на уровне символов - '|.'    """    pattern = "|".join(spec_tokens) + pattern    tokenizer = RegexpTokenizer(pattern)    tokens = tokenizer.tokenize(txt)    return tokens

["Алиса:", "Привет", "как", "вы", "парни", "?", "<END>", "Том:", ... ]

Оказалось, что в моих обучающих данных есть словарь из ~70 000 уникальных слов. Однако так как многие слова встречаются всего один или два раза, я решил заменить такие редкие слова на специальный токен “<UNK>”. Это привело к уменьшению словаря до ~25 000 слов, что позволит обучать более компактную модель.

from collections import Counterdef get_infrequent_tokens(tokens: Union[List[str], str], min_count: int) -> List[str]:    """    Идентифицирует токены, которые встречаются менее минимального количества раз.        :param tokens: Если это исходный текст в виде строки, подсчет частот выполняется на уровне символов.                   Если это токенизированный корпус в виде списка, подсчет частот выполняется на уровне токенов.    :param min_count: Порог вхождения для выделения токена.    :return: Список токенов, которые встречаются редко.     """    counts = Counter(tokens)    infreq_tokens = set([k for k,v in counts.items() if v<=min_count])    return infreq_tokensdef mask_tokens(tokens: List[str], mask: Set[str]) -> List[str]:    """    Проходит по всем токенам. Любой токен, входящий в множество, заменяется неизвестным токеном.    :param tokens: Токенизированный корпус.    :param mask: Множество токенов, которые должны быть замаскированы в корпусе.    :return: Список токенизированного корпуса после операции маскировки.    """    return [t.replace(t, unknown_token) if t in mask else t for t in tokens]infreq_tokens = get_infrequent_tokens(tokens, min_count=2)tokens = mask_tokens(tokens, infreq_tokens)

["Алиса:", "Привет", "как", "вы", "<UNK>", "?", "<END>", "Том:", ... ]

4. Индексирование

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

import torchdef encode(s: list, vocab: list) -> torch.tensor:    """    Кодирует список токенов в тензор из целых чисел, используя фиксированный словарь.     Когда токен не найден в словаре, ему присваивается специальный токен неизвестного значения.     Если тренировочный набор не использовал этот специальный токен, ему присваивается случайный токен.    """    rand_token = random.randint(0, len(vocab))    map = {s:i for i,s in enumerate(vocab)}    enc = [map.get(c, map.get(unknown_token, rand_token)) for c in s]    enc = torch.tensor(enc, dtype=torch.long)    return enc

torch.tensor([8127, 115, 2363, 3, ..., 14028])

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

Изображение автора

5. Архитектура модели

Я решил применить архитектуру GPT, которая была продвинута влиятельной статьей “Attention is All you Need”. Поскольку я пытался создать генератор языка, а не бота для вопросов и ответов, только декодирующая (правая) архитектура была достаточной для этой цели.

“Attention is All you Need” A. Vaswani и др. (Извлечено из arXiv: 1706.03762)

В следующих разделах я разберу каждый компонент архитектуры GPT, объяснив его роль и основные матричные операции. Начиная с подготовленного тренировочного набора, я прослежу примерный контекст из 3 слов через модель до того, как он приведет к предсказанию следующего токена.

5.1. Цель модели

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

Изображение автора

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

Оценка модели:

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

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

Оптимизация:

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

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

5.2. Выходное вложение

До этого момента каждый токен в нашей последовательности был представлен целочисленным индексом. Однако эта упрощенная форма не отражает отношения или сходство слов. Чтобы решить эту проблему, мы повышаем одномерные индексы в высшие размерности через встраивание (embedding).

  • Словное встраивание (word-embeddings): Сущность слова отражается n-мерным вектором чисел с плавающей запятой.
  • Позиционное встраивание (positional-embeddings): Эти встраивания подчеркивают важность позиции слова в предложении и также представлены n-мерными векторами чисел с плавающей запятой.

Для каждого токена мы ищем его словное встраивание и позиционное встраивание, а затем суммируем их поэлементно. Это приводит к выходному встраиванию каждого токена в контексте.

В приведенном ниже примере контекст состоит из 3 токенов. По завершении процесса встраивания каждый токен представлен n-мерным вектором (где n – размер встраивания, настраиваемый гиперпараметр).

Изображение от автора

PyTorch предлагает специальные классы для таких встраиваний. Внутри класса нашей модели мы определяем словное и позиционное встраивания следующим образом (передавая размеры матриц в качестве аргументов):

self.word_embedding = nn.Embedding(vocab_size, embed_size)self.pos_embedding = nn.Embedding(block_size, embed_size)

5.3. Self-Attention Head

Хотя словные встраивания обеспечивают общее представление сходства слов, истинное значение слова часто зависит от окружающего контекста. Например, “bat” может относиться к животному или спортивному снаряжению в зависимости от предложения. Здесь в игру вступает механизм самовнимания, ключевой компонент архитектуры GPT.

Механизм самовнимания сосредоточен на трех основных концепциях: запрос (Q), ключ (K) и значение (V).

  1. Запрос (Q): Запрос представляет текущий токен, для которого требуется вычислить внимание. Это похоже на вопрос: “На что, как текущий токен, мне нужно обратить внимание в остальном контексте?”
  2. Ключи (K): Ключи представляют каждый токен во входной последовательности. Они сопряжены с запросом для определения оценок внимания. Это сравнение измеряет, насколько токен запроса должен обращать внимание на другие токены в контексте. Высокие оценки означают, что следует обратить большее внимание.
  3. Значение (V): Значения также представляют каждый токен во входной последовательности. Однако их роль отличается, поскольку они применяют окончательное взвешивание к оценкам внимания.
Изображение от автора

Пример:

В нашем примере каждый токен контекста уже находится во встроенной форме, как n-мерные векторы (e1, e2, e3). Входными данными для механизма самовнимания служат эти векторы, чтобы выводить контекстуализированную версию для каждого из них по очереди.

  1. При оценке токена “name” вектор запроса q получается путем умножения его встроенного вектора v2 на обучаемую матрицу M_Q.
  2. В то же время векторы ключей (k1, k2, k3) вычисляются для каждого токена в контексте, путем умножения каждого встроенного вектора (e1, e2, e3) на обучаемую матрицу M_K.
  3. Векторы значений (v1, v2, v3) получаются аналогичным образом, только умножены на другую обучаемую матрицу M_V.
  4. Оценки внимания w вычисляются как скалярное произведение между вектором запроса и каждым вектором ключа по отдельности.
  5. Наконец, мы объединяем все векторы значений в матрицу и умножаем ее на оценки внимания, чтобы получить контекстуализированный вектор для токена “name”.
class Head(nn.Module):    """    Этот модуль выполняет операции само-внимания на входном тензоре, создавая     выходной тензор с теми же временными шагами, но разными каналами.         :param head_size: Размер головы в механизме многоголового внимания.    """    def __init__(self, head_size):        super().__init__()        self.key = nn.Linear(embed_size, head_size, bias=False)        self.query = nn.Linear(embed_size, head_size, bias=False)        self.value = nn.Linear(embed_size, head_size, bias=False)    def forward(self, x):        """        # вход размером (пакет, временной шаг, канал)        # выход размером (пакет, временной шаг, размер головы)        """        B,T,C = x.shape        k = self.key(x)                                             q = self.query(x)                                           # вычисляем оценки внимания        wei = q @ k.transpose(-2,-1)                                wei /= math.sqrt(k.shape[-1])                                       # избегаем предвосхождения        tril = torch.tril(torch.ones(T, T))        wei = wei.masked_fill(tril == 0, float("-inf"))            wei = F.softmax(wei, dim=-1)                # взвешенное суммирование значений        v = self.value(x)            out = wei @ v        return out

5.4. Много-головное внимание с маскировкой

Язык сложный, и уловить все его нюансы не так просто. Одних только вычислений внимания часто недостаточно, чтобы заметить тонкости того, как слова работают вместе. Вот где приходит на помощь идея многоголовного внимания в модели GPT.

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

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

Изображение автора
class MultiHeadAttention(nn.Module):    """    В этом классе содержится несколько объектов `Head`, которые выполняют само-внимание     операции параллельно.    """    def __init__(self):        super().__init__()        head_size = embed_size // n_heads        heads_list = [Head(head_size) for _ in range(n_heads)]                self.heads = nn.ModuleList(heads_list)        self.linear = nn.Linear(n_heads * head_size, embed_size)        self.dropout = nn.Dropout(dropout)    def forward(self, x):        heads_list = [h(x) for h in self.heads]        out = torch.cat(heads_list, dim=-1)        out = self.linear(out)        out = self.dropout(out)        return out

5.5. Прямое распространение

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

Изображение автора

В первом линейном слое мы увеличиваем размерность (в нашем случае в 4 раза), что позволяет сети учиться и представлять более сложные особенности. На каждом элементе полученной матрицы применяется функция ReLU, что позволяет распознавать нелинейные паттерны.

Затем, второй линейный слой действует как компрессор, сокращая расширенные размеры обратно к исходной форме (размер блока x размер вложения). Слой dropout завершает процесс, случайно деактивируя элементы матрицы для обобщения модели.

class FeedFoward(nn.Module):    """    Этот модуль проходит входной тензор через серию линейных преобразований     и нелинейных активаций.    """    def __init__(self):        super().__init__()        self.net = nn.Sequential(            nn.Linear(embed_size, 4 * embed_size),             nn.ReLU(),            nn.Linear(4 * embed_size, embed_size),            nn.Dropout(dropout),        )    def forward(self, x):        return self.net(x)

5.6. Добавить и нормировать

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

  • Соединения остатка (Добавить): Эти соединения выполняют поэлементное сложение выхода одного слоя и его неизмененного входа. Во время обучения модель корректирует акцент на преобразования слоев на основе их полезности. Если преобразование считается несущественным, его веса и, следовательно, выход слоя стремятся к нулю. В этом случае через соединение остатка проходит, по крайней мере, неизмененный вход. Эта техника помогает устранить проблему исчезающего градиента (vanishing gradient problem).
  • Нормализация слоя (Норма): Этот метод нормализует каждый встроенный вектор в контексте путем вычитания его среднего и деления на его стандартное отклонение. Этот процесс также гарантирует, что градиенты во время обратного распространения не разрываются и не исчезают.
Image by author

Цепочка слоев многоголового внимания и прямой подачи, связанная с “Добавить и нормализовать”, объединяется в блок. Такое модульное проектирование позволяет нам создавать последовательность блоков. Количество этих блоков является гиперпараметром, который определяет глубину архитектуры модели.

class Block(nn.Module):    """    Этот модуль содержит один блок трансформера, который состоит из многоголового     механизма самовнимания, за которым следуют прямые нейронные сети.    """    def __init__(self):        super().__init__()        self.sa = MultiHeadAttention()        self.ffwd = FeedFoward()        self.ln1 = nn.LayerNorm(embed_size)        self.ln2 = nn.LayerNorm(embed_size)    def forward(self, x):        x = x + self.sa(self.ln1(x))        x = x + self.ffwd(self.ln2(x))        return x

5.7. Softmax

После прохождения через несколько блоков получается матрица размером (размер блока x размер встроенного вектора). Чтобы изменить форму этой матрицы до требуемой (размер блока x размер словаря), мы пропускаем ее через последний линейный слой. Эта форма представляет собой запись для каждого слова из словаря на каждой позиции в контексте.

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

6. Обучение модели

Для обучения языковой модели я выбрал токенные последовательности из случайных позиций в моих данных для обучения. Учитывая быстроту WhatsApp-разговоров, я определил, что длина контекста в 32 слова будет достаточной. Таким образом, я выбрал случайные фрагменты из 32 слов в качестве входа контекста и использовал соответствующие векторы, сдвинутые на одно слово, в качестве целей для сравнения.

Процесс обучения состоял из следующих шагов:

  1. Выбор нескольких пакетов контекста.
  2. Подача этих образцов в модель для вычисления текущей потери.
  3. Применение обратного распространения на основе текущей потери и весов модели.
  4. Более всесторонняя оценка потери каждую 500-ю итерацию.

После того, как все остальные гиперпараметры модели (например, размер вложения, количество голов многоголового самовнимания и т. д.) были зафиксированы, я завершил модель с числом параметров 2,5 миллиона. Учитывая мои ограничения по размеру вводных данных и вычислительным ресурсам, я считаю этот оптимальным настроенным вариантом для меня.

Процесс обучения занял примерно 12 часов для 10 000 итераций. Можно увидеть, что обучение можно было остановить раньше, так как разница между потерей на валидационной и обучающей выборках увеличивается.

Image by author
import json
import torch
from config import eval_interval, learn_rate, max_iters
from src.model import GPTLanguageModel
from src.utils import current_time, estimate_loss, get_batch

def model_training(update: bool) -> None:
    """
    Обучает или обновляет модель GPTLanguageModel, используя предварительно загруженные данные.
    Эта функция либо инициализирует новую модель, либо загружает существующую модель на основе параметра `update`.
    Затем модель обучается с использованием оптимизатора AdamW на наборах данных обучения и валидации.
    Наконец, обученная модель сохраняется.
    
    :param update: Булевый флаг для указания, необходимо ли обновить существующую модель.
    """
    # ЗАГРУЗКА ДАННЫХ --------------------------------------------------------
    train_data = torch.load("assets/output/train.pt")
    valid_data = torch.load("assets/output/valid.pt")
    with open("assets/output/vocab.txt", "r", encoding="utf-8") as f:
        vocab = json.loads(f.read())
        
    # ИНИЦИАЛИЗАЦИЯ / ЗАГРУЗКА МОДЕЛИ ----------------------------------------
    if update:
        try:
            model = torch.load("assets/models/model.pt")
            print("Загружена существующая модель для продолжения обучения.")
        except FileNotFoundError:
            print("Существующая модель не найдена. Инициализация новой модели.")
            model = GPTLanguageModel(vocab_size=len(vocab))
    else:
        print("Инициализация новой модели.")
        model = GPTLanguageModel(vocab_size=len(vocab))
        
    # Инициализация оптимизатора
    optimizer = torch.optim.AdamW(model.parameters(), lr=learn_rate)
    
    # количество параметров модели
    n_params = sum(p.numel() for p in model.parameters())
    print(f"Параметры для оптимизации: {n_params}\n")
    
    # ОБУЧЕНИЕ МОДЕЛИ --------------------------------------------------------
    for i in range(max_iters):
        # вычисляем потери на тренировочном и валидационном наборах данных каждые 'eval_interval' шагов
        if i % eval_interval == 0 or i == max_iters - 1:
            train_loss = estimate_loss(model, train_data)
            valid_loss = estimate_loss(model, valid_data)
            time = current_time()
            print(f"{time} | шаг {i}: потери на тренировочном наборе {train_loss:.4f}, потери на валидационном наборе {valid_loss:.4f}")
            
        # получаем пакет данных
        x_batch, y_batch = get_batch(train_data)
        
        # вычисляем потери
        logits, loss = model(x_batch, y_batch)
        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        optimizer.step()
        
    torch.save(model, "assets/models/model.pt")
    print("Модель сохранена")
    

# 7. Режим чата --------------------------------------------------------------

Для взаимодействия с обученной моделью я создал функцию, которая позволяет выбирать имя контакта с помощью выпадающего меню и вводить сообщение для модели для получения ответа. Параметр "n_chats" определяет количество сгенерированных моделью ответов одновременно. Модель завершает сгенерированное сообщение, когда предсказывает токен 'end_token' как следующий токен.

import json
import random
import torch
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from config import end_token, n_chats
from src.utils import custom_tokenizer, decode, encode, print_delayed

def conversation() -> None:
    """
    Имитирует разговоры в чате, с помощью выборки из предварительно обученной модели GPTLanguageModel.
    Эта функция загружает обученную модель GPTLanguageModel вместе с словарем и списком специальных токенов.
    Затем она входит в цикл, где пользователь указывает имя контакта. По данному входу модель генерирует пример ответа.
    Диалог продолжается, пока пользователь не введет токен завершения.
    """
    with open("assets/output/vocab.txt", "r", encoding="utf-8") as f:
        vocab = json.loads(f.read())
        
    with open("assets/output/contacts.txt", "r", encoding="utf-8") as f:
        contacts = json.loads(f.read())
        spec_tokens = contacts + [end_token]
        
    model = torch.load("assets/models/model.pt")
    completer = WordCompleter(spec_tokens, ignore_case=True)
    
    input = prompt("сообщение >> ", completer=completer, default="")
    output = torch.tensor([], dtype=torch.long)
    print()
    
    while input != end_token:
        for _ in range(n_chats):
            add_tokens = custom_tokenizer(input, spec_tokens)
            add_context = encode(add_tokens, vocab)
            context = torch.cat((output, add_context)).unsqueeze(1).T
            n0 = len(output)
            output = model.generate(context, vocab)
            n1 = len(output)
            print_delayed(decode(output[n0-n1:], vocab))
            
        input = random.choice(contacts)
        input = prompt("\nответ >> ", completer=completer, default="")
        print()

Заключение:

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

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

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

Чтобы хотя бы немного продемонстрировать выходные данные, вы можете увидеть, как справочная модель работает на 200 обучающих сообщениях 😉

Изображение автора