Даже на русском языке игра не самая простая

Все началось с коллеги, который закинул в локальный чат сообщение, что он сыграл в игру #59 и угадал слово с 33 попыток и одной подсказки. Игра оказалась простая и сложная одновременно: сайт загадал слово и нужно его отгадать. В поле ввода отправляешь слово, а искусственный интеллект на сайте определяет, насколько отправленное слово близко по смыслу к загаданному.

Интересная игра, тренирующая ассоциативное мышление и умение строить связи. Новое слово появляется каждый день, что в некотором смысле выглядит ограничителем. Также игра доступна только на португальском и английском языках. С одной стороны, это дополнительная практика, а с другой — сомнения «а знаю ли я это слово?» смазывают впечатления от игры.

Так я задумался о локализации игры на русский язык. Свою игру «Русо контексто» я разместил на объектном хранилище, которое более устойчиво примет читателей Хабра.

Дисклеймер. Оригинальная игра расположена по адресу contexto.me. В процессе подготовки статьи я узнал о существовании русскоязычной версии guess-word.com. Но эта версия имеет более ограниченную функциональность.

Как работает игра?


У сайта минималистичный интерфейс:


  • Сведения об игре: номер, количество попыток и количество подсказок.
  • Поле ввода слова.
  • Список отгаданных слов в виде полосы загрузки. Чем ближе, тем более она заполнена. Номер справа обозначает расстояние в словах, но его можно отключить.

В выпадающем меню есть настройки и дополнительные игровые опции:

  • Выбрать игру.
  • Взять подсказку.
  • Сдаться.

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

Это наводит на мысль, что список ближайших слов формируется отдельно, а игра просто обращается к списку. Остается вопрос: как составить список ближайших слов?

Текстовые эмбеддинги


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

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

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

После операции сопоставления появляется модель — файл, который описывает соответствие «слово — вектор» или как-то описывает правила сопоставления или вычисления. Для работы модели нужно программное обеспечение, которое понимает формат модели.

Проще и быстрее всего «потрогать» эмбеддинги на языке Python. Библиотека gensim реализует один из самых популярных подходов — word2vec. Для работы необходима модель, обученная на достаточном количестве текстов. В документации gensim есть ссылки на англоязычные модели, но нас это не устраивает.

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

Я использовал модель, обученную на Национальном Корпусе Русского Языка (НКРЯ), ее название — ruscorpora_upos_cbow_300_20_2019. Скачиваем архив и распаковываем. Модель представлена в двух видах: бинарном (model.bin) и текстовом (model.txt).

Попробуем воспользоваться этой моделью. Сперва загружаем.

from gensim.models import KeyedVectors
model = KeyedVectors.load_word2vec_format("model.txt", binary=False)

Теперь можем найти слова, ближайшие к слову «провайдер»:

>>> model.most_similar(positive=["провайдер"])
…
KeyError: "Key 'провайдер' not present in vocabulary"

К сожалению, такого слова не нашлось. Дело в том, что данная модель принимает слова вместе с меткой, которая определяет часть слова. Это сделано для различия слов с одинаковым написанием. Например, «печь» можно представить как «печь_NOUN» и «печь_VERB», то есть как существительное и глагол соответственно.

>>> model.most_similar(positive=["провайдер_NOUN"])
[
  ('ip_PROPN', 0.677890419960022), 
  ('internet_PROPN', 0.6627045273780823), 
  ('интернет_PROPN', 0.6595873832702637), 
  ('интернет_NOUN', 0.6567919850349426), 
  ('веб_NOUN', 0.6510902047157288), 
  ('сервер_NOUN', 0.6460723280906677), 
  ('модем_NOUN', 0.6433334946632385), 
  ('трафик_NOUN', 0.6332165002822876), 
  ('безлимитный_ADJ', 0.6230701208114624),
  ('ритейлер_NOUN', 0.6218529939651489)
]

Также возьмем более простой пример с несколькими словами. Зададим два слова: король и женщина. Человек догадается, что женщина-король — это скорее всего королева.

>>> model.most_similar(positive=["король_NOUN", "женщина_NOUN"], topn=1)
[
  ('королева_NOUN', 0.6674807071685791), 
  ('королева_ADV', 0.6368524432182312), 
  ('принцесса_NOUN', 0.6262999176979065), 
  ('герцог_NOUN', 0.613500714302063), 
  ('герцогиня_NOUN', 0.5999450087547302)
]

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

Аргумент topn позволяет задать количество слов, которые мы хотим получить. Таким образом можно запросить какое-нибудь большое количество слов и получить список, необходимый для создания игры. Давайте зададим более современное слово «киберпространство» и посмотрим на ближайшее слово и на слово, например, на десятитысячной позиции.

>>> result = model.most_similar(positive=["киберпространство_NOUN"], topn=10000)
>>> result[0]
('виртуальный_ADJ', 0.39892229437828064)
>>> result[9998]
('европбыть_VERB', 0.12139307707548141)
>>> result[9999]
('татуировкий_NOUN', 0.12139236181974411)

Татуировкий_NOUN. Кажется, это новый химический элемент. Наличие специфичных слов, которые могут шуткой, опечаткой, ошибкой в парсинге или локальным жаргонизмом, неприятно влияет на игру.

Более того, использование некорректного тега приведет к интересным результатам
 >>> model.most_similar(positive=["европа_NOUN"], topn=10)
[
  ('максимилиан::александрович_PROPN', 0.3658076822757721), 
  ('фамилие_NOUN', 0.36153605580329895), 
  ('санюшка_NOUN', 0.35595449805259705), 
  ('емельян::ильич_PROPN', 0.35401633381843567), 
  ('автостоп_NOUN', 0.35294172167778015), 
  ('юрген_PROPN', 0.3491175174713135), 
  ('чарльз::диккенс_PROPN', 0.3454093337059021), 
  ('когда-тотец_NOUN', 0.3360745906829834), 
  ('городбыть_VERB', 0.3332841098308563), 
  ('владлен_VERB', 0.33179953694343567)
]

Пояснение: Европа — имя собственное, поэтому тег должен быть PROPN.


Нужно очистить словарь от странных слов и оставить только существительные.

Если вам понравится этот текст, у меня есть еще:

Подбираем скины в Counter-Strike: Global Offensive в цвет сумочки
Делаем тетрис в QR-коде, который работает
Делаем радио из Cyberpunk 2077

Обработка словаря


Один из способов хранения модели word2vec — текстовый. Формат прост: в первой строке задаются два числа — количество строк в документе и количество чисел в векторе. Далее на каждой строке задается слово и далее числа, обозначающие вектор.

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

Это эмбеддинг Navec (навек) из проекта Natasha. Ссылку на русскоязычную модель можно увидеть в репозитории проекта. Скачиваем и загружаем модель:

from navec import Navec
path = 'navec_hudlit_v1_12B_500K_300d_100q.tar'
navec = Navec.load(path)

Теперь можно проверять слова простым синтаксисом:

>>> "виртуальный" in navec
True
>>> "европбыть" in navec
False
>>> "татуировкий" in navec
False

Таким образом можно отсеять немалое количество слов, которым в игре не место.

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


Но вместе с тем теряются и настоящие слова
агрокомплекс
кейтеринг
фемтосекунда
углепластик
электромашиностроение
мурмолка
реанимобиль


Алгоритм очистки модели следующий:

  • Если у слова тег не NOUN, то отбрасываем это слово.
  • Удаляем из слова последовательность _NOUN.
  • Проверяем «чистое слово» на наличие в эмбеддинге Navec. Если его там нет, слово отбрасываем.
  • Слово, которое прошло все проверки, записываем в файл.

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

model = KeyedVectors.load_word2vec_format("noun_model.txt", binary=False)

Стало ли после этого лучше?

>>> result = model.most_similar(positive=["киберпространство"], topn=10000)
>>> result[0]
('виртуальность', 0.4715898633003235)
>>> result[9998]
('компаунд', 0.15783849358558655)
>>> result[9999]
('хитрость', 0.15783214569091797)

Определенно. Для статистики: исходная модель содержит 248 978 токенов, из них 59 104 токенов имеют метку существительног. И только 36 269 прошли «сито» второго эмбеддинга.

Время заняться бэкэндом и фронтендом игры.

Умный бэкэнд


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

Здесь можно воспользоваться инструментом MyStem. Для Python есть обертка pymystem3. Крайне простой инструмент для получения начальной формы слова:

import pymystem3
mystem = pymystem3.Mystem()

Метод lemmatize принимает на вход строку-предложение и возвращает список слов в начальной форме.

>>> mystem.lemmatize("кот коты котов котах кота")
['кот', ' ', 'кот', ' ', 'кот', ' ', 'кот', ' ', 'кот', '\n']

На первый взгляд даже производительность на достойном уровне: на моей виртуальной машине лемматизация одного слова занимает до 10 мс. По меркам современного веба это достаточно быстро.

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

Игра на объектном хранилище


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

  • Сдаться нельзя.
  • Список топ-500 ближайших слов получить можно, только предоставив загаданное слово.
  • Подсказку можно получить по слову и позиции.

Но вскоре мне показалось это слишком суровым.

На данный момент единственное назначение бэкэнда — приведение слов к начальной форме. Правда, как показало тестирование на коллегах, и это не обязательно: все и так старались писать начальные формы слов. Да и модель эмбеддингов не лемматизирована, то есть игра понимает слова не только в начальной форме.

Получается, игру можно полностью перенести в браузер?

Так как я бэкэнд-разработчик, то отказ от бэкэнда в угоду фронтэнду — это стресс. Однако от бэкэнда полностью отказаться не получится: генератор близких слов где-то нужно запускать. Генератор принимает на вход загаданное слово и формирует текстовый файл, где на каждой строке по одному слову в порядке смыслового убывания от загаданного. Содержимое этого файла также дублируется в JSON-словарь, где каждому слову соответствует его дистанция от загаданного слова.

JSON-файл на каждую игру занимает до 2 МБ. При открытии игры файл скачивается в браузер и JavaScript реализует логику игры. Этот способ не самый производительный, но после загрузки файла позволяет играть без подключения к интернету.

Я разместил игру в облачном хранилище Selectel, которое более устойчиво к наплыву посетителей.



Заключение


Итоговый результат доступен по адресу words.f1remoon.com, а исходный код — в репозитории.

Дополнительное чтение


Как работают текстовые эмбеддинги?

Чудесный мир Word Embeddings: какие они бывают и зачем нужны? (от пользователя madrugado)
→ Word2vec в картинках (от пользователя m1rko)

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


  1. atepaevm
    14.12.2022 17:50
    +3

    Спасибо за отличную статью!


  1. Kwent
    14.12.2022 18:28
    +2

    Интересная игра, тренирующая ассоциативное мышление и умение строить связи.

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

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


    1. EgorovM
      14.12.2022 21:18

      Вместо Word2Vec можно использовать GloVe, который заточен под контексное отображение. По ощущениям он справляется лучше Word2Vec, да и голого Bert-а. А вот среднее расстояние между Bert и GloVe дает получше результаты.

      Думаю скоро выпустим статью про эти задумки


      1. Firemoon Автор
        15.12.2022 00:30

        Я хотел использовать GloVe, но word2vec подкупил своими тегами...


        1. EgorovM
          15.12.2022 01:44

          У нас очень много жалоб на прилагательные были, как будто бы их стоит вообще убрать)
          Сейчас думаем как бы лучше поступить


  1. gennayo
    14.12.2022 19:11
    +3

    Веселая шизофрения... Без подсказок не решить за вменяемое время.


    1. YMA
      14.12.2022 21:50
      +1

      Однозначно... Только настоящий шизофреник может угадать вот так:

      Моя попытка поиграть


      1. iddqda
        14.12.2022 22:27

        ахаха спсб за ответ. я дошел до слова "раз" и застрял
        при этом английскую контексту щелкаю достаточно легко и без подсказок


      1. Mirzapch
        15.12.2022 08:58
        +1

        Неплохо. Как перезапустить игру, не удаляя Cookies?

        Hidden text


        1. minalexpro
          15.12.2022 10:51

          Насколько я понял - загадывается одно слово в день для всех. По ссылке "Игры" можно выбрать другой день и разгадывать слово, загаданное в тот день.


          1. Firemoon Автор
            15.12.2022 11:32
            +1

            Всё так. Вкладка игры и другой день.

            Каждый день одно слово.


    1. GennPen
      14.12.2022 22:00

      Да даже с подсказками типа "правеж", "недолга" пришлось гуглить что они означают.


  1. EgorovM
    14.12.2022 20:59

    Прикольно!

    Занимались тем же самым, запустились правда не в Selectel, но хотим рассмотреть в дальнейшем такую возможность)

    https://guess-word.com


    1. delicious
      14.12.2022 22:00
      +1

      Жесть


      1. minalexpro
        15.12.2022 07:10
        +1

        Гардероб


    1. Mirzapch
      15.12.2022 09:10

      Просто ради интереса... Сколько слов в словаре?


      1. pryanin
        15.12.2022 13:56
        +1

        Наверное в системе есть носки, носок в единственном числе применяется реже


        1. Mirzapch
          16.12.2022 05:38

          Увы, поттериану в систему не загрузили.


    1. SpectrumOS
      15.12.2022 10:22
      +1

      Сделайте, пожалуйста, кнопку "Сдаться" или "Показать ответ". Я понятия не имею, что тут загадано


  1. iddqda
    14.12.2022 22:31

    немного странно, что ваш ИИ оценивает слова "раз" и "один" настолько по разному


    1. iddqda
      14.12.2022 22:35

      но вообще набор слов вызывает вопросы
      кто нибудь в курсе что такое гарнец?


      1. Firemoon Автор
        15.12.2022 00:28

        Отдельной забавы стоит загадать слово, словоформы которого имеют другое значение.

        Коллега загадала слово «базили́к» (трава-приправа такая). И по мнению word2vec это слово максимально близко к слову «бази́лика» (эт такой царский дом), что логично, родительный падеж для первого слова пишется так же.

        Таким образом в топ-500 слов можно встретить как перечисление приправ, так и перечисление строений. Не повезло тем, кто выбрался в «зеленую» зону на строениях!


      1. bay73
        15.12.2022 03:14

        Гарнец - это четверть ведра. Вполне на месте смотрится в одном ряду с другими числительными и мерами.


    1. Tarakanator
      15.12.2022 10:41
      +1

      Возможно потому, что раз может применятьсян не только как число, напимер "в самый раз"?


  1. rPman
    14.12.2022 23:26

    Планируете использовать собранный датасет предположений игроков для дальнейшего обучения? статью напишете?


    1. Firemoon Автор
      15.12.2022 00:22

      О таком я не думал. Х)

      Хотя идея-то хорошая...


    1. EgorovM
      15.12.2022 01:43
      +1

      Вот была идея скорректировать вектора ассоциаций по последним 10 попыток людей. У нас где-то 5 миллионов попыток пользователей набралось. Попробуем что-нибудь сами порисовать, потом выложим унифицированные данные в открытый источник:)


  1. minalexpro
    15.12.2022 07:06
    +3


  1. minalexpro
    15.12.2022 11:17

    Тема интересная. А как улучшать словарь - добавлять новые слова и удалять ненужные? Как новым словам будет назначаться вектор?


  1. VitalySh
    15.12.2022 14:19

    Ребят, это хардкор - 100 подсказок, 150 попыток. Это просто перебор, вам надо фильтровать редкие слова, глаголы, неправильные формы, иначе это просто упоротая игра


    1. VitalySh
      15.12.2022 14:20

      И да, я до сих пор без понятия, что там за фазис с разлитием прячется на нулевом месте


      1. VitalySh
        15.12.2022 14:28

        А итоговое слово Течение - и какое же слово к нему ближе? Какой к черту месяц и течение, какое они вообще имеют друг другу отношение. Боже как у меня горит, столько времени убить на угадывание.
        Поменяйте свою сетку на openAI, или это игра в рулетку а не угадывание.


        1. YMA
          15.12.2022 14:46

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


  1. janatem
    15.12.2022 16:57

    ('королева_ADV', 0.6368524432182312),

    Теперь мне не дает покоя наречие «королева», пытаюсь придумать, что оно могло бы означать и с какими глаголами употребляться.


  1. samec2011
    15.12.2022 23:35

    Теперь напишите "разгадывателя" и пусть бьются друг с другом.


  1. combo_breaker
    15.12.2022 23:35

    Просил ChatGPT загадать что угодно и отвечать да/нет на вопросы вроде "это живое?". И наоборот, когда отгадывал он. Понимание позволяет играть как с человеком. Хотя ваша игра сложнее и мозг прогревает лучше.


  1. Gvozdod
    16.12.2022 16:46

    Подскажите слово от 13-12-2022. Уже всю голову сломал, все слова открыл с 1 по 20. Ответ так и не понимаю


    1. YMA
      16.12.2022 19:03

      Шел - животное - вещество - воздух - мяч - и далее как на скрине...

      Открыл честно