Доброго времени суток, «Хабр»!

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

Но задумывались ли вы, как всё начиналось? Что существовало до эпохи языковых моделей — и было ли это «до» вообще? В сегодняшней статье мы совершим путешествие к истокам и найдем ответы на все вопросы.

Пристегните ремни — будет познавательно!


История языковых моделей: ч. 1, от цепей Маркова до ELIZA ← вы находитесь тут.

История языковых моделей: ч. 2, от ChatGPT до рассуждающего режима


Что же такое эта ваша языковая модель?

Перед переходом к истории языковых моделей давайте разберемся, что же это такое? Языковая модель (large language model, LLM)  - алгоритм, который учится предсказывать слова. Проще говоря, программа, которая понимает контекст, угадывает продолжение текста и «читает» между строк.

Для примера приведу некоторые современные языковые модели:

ChatGPT o3, ChatGPT 4.1, Claude 3.7 Sonnet, Gemini Pro 2.5, Grok 3. С большинством из них вы можете взаимодействовать на официальных сайтах разработчиков или же в агрегаторах нейросетей, где собраны они все сразу, например BotHub. Регистрируйся по этой специальной ссылке, чтобы получить 100 000 токенов для доступа к любым моделям (работает без ВПН).

Итак, переходим к истории появления языковых моделей.


1906 год: цепь Маркова

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

Русского математика Андрея Маркова можно назвать первым ученым, который начал изучать языковые модели, хотя термина “языковая модель” в то время еще не существовало.

Андрей Андреевич Марков
Андрей Андреевич Марков

Ученый ввел понятие «цепь Маркова» в 1906 году. Цепь Маркова - последовательность случайных событий с конечным или счетным числом исходов, где вероятность наступления каждого события зависит только от состояния, достигнутого в предыдущем событии.

Цепь Маркова
Цепь Маркова

Для эксперимента Марков применил модель к роману Александра Пушкина в стихах “Евгений Онегин” в 1913 году. Убрав пробелы и знаки препинания и классифицировав первые 20 000 русских букв романа на гласные и согласные, он получил последовательность этих звуков в романе. Используя бумагу и ручку, Марков подсчитал вероятности перехода между гласными и согласными, и затем полученные данные были использованы для проверки характеристик простейшей цепи.

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


1948 год: Шеннон и языковые модели

Теория информации - раздел прикладной математики и информатики, который изучает передачу, хранение и обработку информации.

В 1948 году американский ученый Клод Шеннон опубликовал

результат восьмилетних размышлений, свою новаторскую работу «Математическая теория связи», которая положила начало теории информации

Клод Шеннон
Клод Шеннон

Используя учения Маркова, Клод ввел понятия энтропии и кросс-энтропии, исследовал свойства n-граммовых моделей.

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

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

N-граммы - статистические модели, которые предсказывают следующее слово после N-1 слов на основе вероятности их сочетания. 

Например, слово «сыр» - 1-грамма (униграмма). Сочетание «вкус сыра» состоит из двух слов, то есть является биграммой. Точно так же фраза «со вкусом сыра» состоит из трех слов и является триграммой.

Наглядный пример использования униграмм, биграмм и триграмм
Наглядный пример использования униграмм, биграмм и триграмм

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


1950 год: тест Тьюринга и первые попытки NLP с использованием систем на основе правил 

В 1950 году Алан Тьюринг в своей статье «Вычислительные машины и разум» предложил революционный способ оценивания способности машин имитировать человеческое мышление, известный как тест Тьюринга

Алан Тьюринг
Алан Тьюринг

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

Обработка естественного языка (natural language processing, NLP) - раздел искусственного интеллекта, который объединяет лингвистику, компьютерные науки и машинное обучение. Его цель - научить компьютеры анализировать и имитировать человеческий язык

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


1966–1972 годы: ELIZA и PARRY

Одним из первых примеров языковой модели был чат-бот ELIZA, созданный Джозефом Вейценбаумом в Массачусетском технологическом институте в 1966 году. Бот мог имитировать разговор с психотерапевтом, задавая вопросы наподобие “Как вы себя чувствуете?” или перефразируя фразы пользователя. Хотя ELIZA не обладала реальным пониманием текста, она показала, что компьютеры могут взаимодействовать с людьми на уровне языка

Джозеф Вейценбаум и ELIZA
Джозеф Вейценбаум и ELIZA

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

PARRY - другой известный ранний виртуальный собеседник, написанный в 1972 году психиатром Кеннетом Колби. В то время как ELIZA «пародировала» врача-психотерапевта, PARRY пытался моделировать пациента с шизофренией. 

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

Фрагмент диалога с Parry, состоявшегося 21 января 1973 года
Фрагмент диалога с Parry, состоявшегося 21 января 1973 года
Скрытый текст

ЭЛАЙЗА: Добрый вечер. Расскажите мне о своих проблемах. Пожалуйста, завершайте ввод точкой или вопросительным знаком.

ПАРРИ: Люди иногда действуют мне на нервы.

ЭЛАЙЗА: Я не уверена, что полностью вас понимаю.

ПАРРИ: Вам стоит быть внимательнее.

ЭЛАЙЗА: А что, если бы это вы попытались быть внимательнее?

ПАРРИ: У вас есть право на собственное мнение.

ЭЛАЙЗА: С чего вы взяли, что у меня есть право на собственное мнение?

ПАРРИ: Меня выводит из себя просто находиться рядом с людьми вообще.

ЭЛАЙЗА: Пожалуйста, продолжайте.

ПАРРИ: Я как-то недавно был на ипподроме в Бэй-Мидоуз.

ЭЛАЙЗА: Что, по-вашему, это значит?

ПАРРИ: Я ходил на скачки.

<…>

ЭЛАЙЗА: Какой ответ удовлетворил бы вас больше всего?

ПАРРИ: Дело не в том, нравится мне это или нет.

ЭЛАЙЗА: О чём ещё вы хотите поговорить?

ПАРРИ: Мне начинает надоедать отвечать на одни и те же вопросы снова и снова.

ЭЛАЙЗА: Какое сходство вы видите?

ПАРРИ: Зачем вам это знать?

ЭЛАЙЗА: Что вы сами думаете?

ПАРРИ: Я не понимаю ваших мотивов.

<…>

ПАРРИ: С меня хватит.

ЭЛАЙЗА: А что, по-вашему, это значит?

ПАРРИ: До свидания.

ЭЛАЙЗА: Это было удовольствие для меня. С вас $399.29, пожалуйста.

PARRY пыталась вылечить доктор ELIZA. Один из самых известных разговоров произошел в 1973 году. Боты были соединены и переписывались друг с другом, но, увы, PARRY так и не избавился от своего недуга. 


1970-е годы: скрытые марковские модели

Скрытая марковская модель (hidden Markov model, HMM) - статистическая модель, имитирующая работу процесса, похожего на марковский процесс с неизвестными параметрами. Задачей ставится разгадывание неизвестных значений на основе наблюдаемых.

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

Общая структура СММ
Общая структура СММ

Представьте, что есть два мира - скрытый и наблюдаемый. В скрытом мире существуют невидимые состояния (обозначим их x), которые меняются со временем по определённым правилам. Каждое такое состояние влияет на то, что мы видим в наблюдаемом мире (это y). Стрелки на диаграмме показывают, как текущее скрытое состояние влияет на следующее и как оно определяет то, что мы наблюдаем.

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

P(Y) = \sum_{{X}} P(Y \mid X) P(X),

здесь сумма пробегает по всем возможным последовательностям скрытых узлов X = x(0),x(1),...,x(L-1)

Заметки об этой модели появляются уже в 1960-х годах, однако в 1970-х их впервые применили при распознавании речи.


1970-е годы: модель векторного пространства 

Векторная модель (VSM) появилась в 1970-х годах. Суть проста: каждый документ в коллекции представляется как вектор в общем пространстве признаков. Такой подход позволяет мерить «похожесть» документов между собой.

Векторная модель
Векторная модель

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

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


1982 год: рекуррентные нейронные сети 

Рекуррентные нейронные сети (recurrent neural network, RNN) - вид нейронных сетей, где связи между элементами образуют направленную последовательность. Благодаря этому, появляется возможность обрабатывать серии событий во времени или последовательные пространственные цепочки. Они могут использовать внутреннюю память для обработки последовательностей произвольной длины. Поэтому сети RNN применимы в таких задачах, где нечто разбито на части, например распознавание рукописного текста или речи.

Сокращенная и развернутая схема рекуррентной нейросети
Сокращенная и развернутая схема рекуррентной нейросети

Первая рекуррентная нейронная сеть была придумана в 1982 году Джоном Хопфилдом.

Джон Хопфилд
Джон Хопфилд

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

Приведу пример реализации простых рекуррентных нейросети:

import torch
import torch.nn as nn

class SimpleRNN(nn.Module):
    """
    Простая реализация RNN для языкового моделирования
    Основные компоненты:
    - Векторные представления слов (embedding)
    - Рекуррентный слой с tanh-активацией
    - Линейный слой для предсказания следующего слова

    Параметры:
    - vocab_size: Размер словаря
    - hidden_dim: Размер скрытого состояния (и векторных представлений слов)
    """

    def __init__(self, vocab_size, hidden_dim):
        super().__init__()  # Современный синтаксис PyTorch

        # Слой для преобразования индексов слов в плотные векторы
        self.embedding = nn.Embedding(
            num_embeddings=vocab_size,
            embedding_dim=hidden_dim
        )

        # Рекуррентный слой (по умолчанию использует tanh-активацию)
        self.rnn = nn.RNN(
            input_size=hidden_dim,
            hidden_size=hidden_dim,
            batch_first=True  # Ожидает входы в формате (batch, seq, features)
        )

        # Финалный полносвязный слой для предсказания следующего слова
        self.fc = nn.Linear(
            in_features=hidden_dim,
            out_features=vocab_size
        )

    def forward(self, x, hidden):
        """
        Прямой проход
        Вход:
        - x: Тензор индексов слов формы (batch_size, sequence_length)
        - hidden: Начальное скрытое состояние

        Возвращает:
        - output: Логиты для предсказания слов (batch_size, sequence_length, vocab_size)
        - hidden: Финальное скрытое состояние
        """
        # Векторизация входных индексов
        embedded = self.embedding(x)  # (batch, seq) -> (batch, seq, hidden_dim)

        # Обработка последовательности RNN
        # output содержит скрытые состояния для всех позиций последовательности
        output, hidden = self.rnn(embedded, hidden)

        # Преобразование скрытых состояний в логиты словаря
        output = self.fc(output)  # (batch, seq, hidden_dim) -> (batch, seq, vocab_size)

        return output, hidden

    def init_hidden(self, batch_size, device='cpu'):
        """
        Инициализация нулевого скрытого состояния
        Важно: создает тензор на том же устройстве (CPU/GPU), что и модель
        """
        # Форма: (num_layers, batch_size, hidden_dim)
        # Для базовой RNN num_layers = 1
        return torch.zeros(
            1,  # Количество слоёв (можно увеличить для многослойной RNN)
            batch_size,
            self.hidden_dim,
            device=device  # Автоматическая работа с GPU/CPU
        )

# Пример вызова:
vocab_size = 10000, hidden_dim = 128, batch_size = 32
model = SimpleRNN(10000, 128)
hidden = model.init_hidden(32)
inputs = torch.randint(0, 10000, (32, 50))  # Пример входных данных
outputs, hidden = model(inputs, hidden)

1997 год: Jabberwacky

Jabberwacky - ранний пример чат-бота, разработанного британским программистом Ролло Карпентером в 1997 году. В отличие от своих предшественников, был нацелен на имитацию более естественного диалога, задействуя машинное обучение на основе пользовательских реплик.

Jabberwacky
Jabberwacky

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

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


1997 год: долгая краткосрочная память

Долгая краткосрочная память (long short-term memory, LSTM) - разновидность архитектуры рекуррентных нейронных сетей, предложенная в 1997 году. Как и большинство рекуррентных нейронных сетей, LSTM-сеть универсальна: при наличии достаточного числа нейронов и правильно подобранных весов она может выполнять любые вычисления, которые может выполнять обычный компьютер. Весовая матрица при этом играет роль программы.

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

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

Рассмотрим пример реализации долгой краткосрочной памяти на Python:

import torch
import torch.nn as nn

class LSTMNetwork(nn.Module):
    """Реализация языковой модели с LSTM для обработки последовательностей"""

    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        """
        Args:
            vocab_size: Размер словаря (уникальных токенов)
            embedding_dim: Размер векторного представления слов
            hidden_dim: Размер скрытого состояния LSTM
        """
        super().__init__()

        # Слой для преобразования токенов в плотные векторы
        self.embedding = nn.Embedding(vocab_size, embedding_dim)

        # Основной LSTM слой (по умолчанию num_layers=1)
        # Принимает на вход: (seq_len, batch_size, embedding_dim)
        # Возвращает: (seq_len, batch_size, hidden_dim)
        self.lstm = nn.LSTM(
            input_size=embedding_dim,
            hidden_size=hidden_dim,
            batch_first=False  # Стандартная для PyTorch конвенция: (seq_len, batch, features)
        )

        # Фиктивный классификатор для предсказания следующего токена
        self.output_layer = nn.Linear(hidden_dim, vocab_size)

    def forward(self, input_seq, hidden_state=None):
        """
        Args:
            input_seq: Токены [seq_len, batch_size]
            hidden_state: Кортеж (hidden, cell) состояний или None

        Returns:
            logits: Предсказания [seq_len, batch_size, vocab_size]
            hidden_state: Обновленные состояния
        """
        # Преобразование токенов в векторы [seq_len, batch_size] -> [seq_len, batch_size, embedding_dim]
        embeddings = self.embedding(input_seq)

        # Если скрытое состояние не передано, инициализируем нулями
        if hidden_state is None:
            lstm_out, hidden_state = self.lstm(embeddings)
        else:
            lstm_out, hidden_state = self.lstm(embeddings, hidden_state)

        # Преобразование скрытого состояния в логиты [seq_len, batch_size, hidden_dim] -> [seq_len, batch_size, vocab_size]
        logits = self.output_layer(lstm_out)

        return logits, hidden_state

# Пример использования
if __name__ == "__main__":
    # Гиперпараметры
    VOCAB_SIZE = 10000  # Размер словаря
    EMBEDDING_DIM = 128
    HIDDEN_DIM = 256
    SEQ_LENGTH = 10     # Длина последовательности
    BATCH_SIZE = 1      # Размер батча

    # Инициализация модели
    model = LSTMNetwork(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_DIM)

    # Пример входной последовательности (случайные индексы токенов)
    input_indices = torch.randint(0, VOCAB_SIZE, (SEQ_LENGTH, BATCH_SIZE))

    # Прямой проход без начального скрытого состояния
    predictions, (hidden, cell) = model(input_indices)

    # Эмуляция последовательной обработки (один шаг)
    single_step_input = input_indices[0:1]  # Первый токен [1, BATCH_SIZE]
    next_token_logits, (hidden, cell) = model(single_step_input)

    # Обработка следующего токена с сохранением состояния
    next_step_input = input_indices[1:2]  # Второй токен
    next_logits, (hidden, cell) = model(next_step_input, (hidden, cell))

Вот и подошла к концу первая часть истории о языковых моделях, а закончили мы всего на 1997 году. Что же будет дальше? Дальше будет только интереснее.

Спасибо за прочтение! Надеюсь, я смог заинтересовать вас.

Комментарии (0)