Увидел пост о конкурсе, когда прошло уже две недели после начала. Но задача показалась крайне увлекательной, и я не ошибся в этом, нырнув в решение с головой. Хочу поделиться решением на 80+% и своими впечатлениями в этом посте.

Всё моё участие прошло под вопросом «где взять ещё один процент?», но в ответ я чаще получал сотые доли процента или ничего. Итак, обо всём по порядку.

Писал на C++, поскольку JS совсем не знаю, а готовое решение переводил на JS с помощью коллеги.

Первое, что я попробовал — это блум-фильтр. Сократил словарь: привёл к нижнему регистру все слова, заменил все слова в словаре оканчивающиеся на 's на префикс, отвёл под блум фильтр 60 кб. В качестве хэш-функции выбрал линейный генератор h[i] = 115249 * (h[i-1] + w[i]) + 33391. В итоге точность получилась не очень высокая, порядка 60%, но с чего-то нужно начинать.

Второе. Добавил фильтрацию по редким сочетаниям символов: не содержит 6 гласных подряд, не содержит 6 согласных подряд. Если длина слова больше трёх, то не должно идти подряд три одинаковых буквы.

Третье. Посмотрел на соотношение слова/мусор в зависимости от длины и начал возвращать результат «мусор» на все тесты длиннее 13 символов.

Четвёртое. Мусор содержит большее количество слов с кавычкой. Новое правило: на всё, что содержит кавычку (не считая 's) говорить «не слово». После этого точность была в районе ~74%.

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

image

Очень обрадовался увиденным графикам, но в итоге это дало лишь 76.4% после ручной подгонки коэффициентов. Из недостатков: минус 1400 байт для блума.

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

Седьмое. Заметил, что слова из треша генерируются неравномерно и повторов намного чаще. Добавил стоп-лист из топа (20 штук). Память пошла в ущерб блум-фильтру, но суммарная точность увеличилась на +0.1-0.2%. В этом месте я подумал, что потенциал блум фильтра ещё не до конца использовал.

Восьмое. Решил сохранять в блум-фильтр не слова, а только префиксы длинной 3-6. Их намного меньше из-за повторов и точность блум-фильтра сильно выросла. 77.8%. Решил, что можно хранить не стоп-лист треша, а сохранить это прямо в фильтр. Завёл целочисленный массив, на каждое слово добавлял 1, на каждый повторяющийся треш уменьшал на количество повторов, потом установил в фильтре только те биты, где было положительное число. Перестал устанавливать биты на те слова, которые не проходят проверку по частотности биграм или другие: освободилось место в блум-фильтре. Заодно поднял границу из п.3. Итого получил 79%+

Девятое. Попробовал считать триграмы. Для экономии памяти учитывал только первый и третий символ, построил частоты/графики аналогично пятому пункту. Точность подросла на 0.05%, но уменьшение размера фильтра на ещё 1500 символов вызывало закономерное уменьшение точности. В итоге отказался от этого пункта.

Десятое. Заметил, что вероятности биграм зависят от положения в слове. Наиболее явно это заметно для последних двух символов и для первых двух. Добавил соответствующие эвристики. А заодно «слово не заканчивается на Q». А вот идеи, позаимствованные из лингвистики и правил словообразования (ing форма, множественное число, и тд) дали только ухудшения.

Одиннадцатое. Много подгонял все константы в коде. Думаю, что правильнее было написать какое-то автоматизированное средство, но времени осталось уже мало. Итого взял размер блум-фильтра 502122 бит; в него ложил префиксы длиной 8 символов; а фильтровал по длине слова > 18. Получилось 80.5+%

Двенадцатое. Мусор, который не случайный, выглядит как слова с ошибкой. Поэтому появилась идея: добавить в тестируемое одну ошибку и проверить, получился ли совсем мусор или что-то похожее на слово? Собрал кучу статистики, провёл кучу экспериментов, но нулевой или отрицательный результат. Удалось ли кому-нибудь довести до ума такую проверку?

Тринадцатое. Обратил внимание на неравномерность не-слов. Они появляются либо слишком часто, либо лишь однажды (на выборке разумного размера). Начал сохранять все запросы и после 1.5млн проверять, было ли оно уже? Если раньше никогда не было — значит это мусор. Если раньше было более 5 — значит тоже мусор. Ну и заодно раз в 10 миллионов слов прорежал свой массив удаляя уникальные слова, чтобы память не закончилась. Итого на каждый миллион у меня получилось увеличение точности около 1%. Например, на выборке из 3 500 000 слов я получил 84%, а на 5 000 000 уже более 85%.

На этом время подошло к концу, начал дёргаться глаз каждый раз как я видел, что точность падает на 1-5 % вместо роста, да и действительно хороших идей не появилось. Интересно, возможно ли набрать 83-85% без 13го пункта? С каждой добытой десятой и сотой эти числа кажутся всё более реальным, но всё ещё далёкими. Такими же далёкими как 80% во второй день.

Итоговое решение лежит здесь: github.com/knst/word-classifier

Для JS нужно подготовить данные, после выполнения С++ программы сделать:

$ cat bigrams.bin bloom.bin > data && gzip data
Поделиться с друзьями
-->

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


  1. Don_Eric
    31.05.2016 12:18
    +2

    1. knstqq
      31.05.2016 12:30

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


      1. Antelle
        31.05.2016 12:38

        Появятся в пятницу скоро все решения. Что мне ещё дало заментые улучшения:


        • ограничение по длине после отрезания суффиксов
        • очистка фильтра от редко используемых бит
        • длина фильтра простое число
        • определение последовательностей редко-используемых 2грам


        1. knstqq
          31.05.2016 12:47

          Я тоже сначала брал простые числа как длину фильтра, но перед сабмитом выяснил, что 502122 даёт на 0.2-0.3% больше точности, чем несколько соседних простых.

          Что дала очистка редко используемых бит? Для сжатия данных?


          1. Antelle
            31.05.2016 12:52

            Если в фильтре один бит используется для кодирования только одного слова, при этом он ещё вносит какой-то % false positives. Удалив его, мы получим false negative для одного слова, но уменьшим число false positives для тех, что туда попадают.


      1. sarytchev
        31.05.2016 14:59
        -5

        можно ваше решение отправить и получить бабки?


        1. knstqq
          31.05.2016 15:00
          +2

          27го мая в 23:59:59 UTC крайний срок.


          1. sarytchev
            31.05.2016 15:11
            -6

            sheet )


      1. Joshua
        31.05.2016 18:00
        +1

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


  1. slavap
    01.06.2016 08:43

    Я не понимаю оптимизацию с суффиксами. Если например отрезать все 's или заменить, то елементарный тест с приписыванием 's ко всем правильным словам даст гору неправильных true. Или это просто подстройка под конкретный генератор, где 's встречается довольно редко?


    1. knstqq
      01.06.2016 08:51
      +1

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

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

      Но также это подстройка под конкретный выданный словарь из 600к+ слов: в нём словоформы 's встречаются достаточно часто, но не так много слов, которые есть с суффиксом 's, но не существуют без этого суффикса.

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


      1. slavap
        01.06.2016 11:48

        Всё равно не понимаю. Оптимизируя 's можно выиграть ~140000 слов. Это теоретически улучшает фильтр на 0.07, т.е. 15% против 18.5%, итого 3.5% улучшения. НО остается ~490000 слов для которых НЕТ формы с 's, если взять их малую часть и тупо дописать 's то будет не выигрыш, а серьёзный проигрыш от такой оптимизации.


        1. knstqq
          01.06.2016 12:32

          Откуда числа 15, 18.5 и 0.07?

          Вот так вычисляется вероятность ложно положительных срабатываний: image
          При длине фильтра 500 000 бит и k = 1 это даёт примерно 63% для 500 000 слов против 72% для 630 000 слов. Разница существена.
          Но если бы генератор треша был другой, то это улучшение могло бы стать ухудшением.


          1. slavap
            01.06.2016 13:05

            Берём тест с 50% правильных и 50% неправильных слов. Соответственно false positive влияет только на половину, 50*0.7 и 50*0.63, т.е. получаем 65% и 68.5% правильных ответов. http://hur.st/bloomfilter?n=630000&p=0.7 И http://hur.st/bloomfilter?n=490000&p=0.63


            1. knstqq
              01.06.2016 13:44

              Да, +4.5% итоговой точности. И это оказалось больше, чем потери из-за специфики генератора и словаря: я выиграл на этом 1-2%, а не 4.5, как хотелось бы. Даже если прирост составил бы 0.1% я бы использовал этот метод


              1. slavap
                01.06.2016 13:49
                -1

                Стоит чуток изменить генератор и будет только хуже от этой эвристики. Она работает БЕЗ блума, если словарь сжать без потерь. С блумом не работает, только кривой тест спасает дело.


                1. alex3d
                  01.06.2016 20:33

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


                  1. slavap
                    01.06.2016 21:28

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


  1. iKest
    02.06.2016 12:18

    Интересно, почему все пользуются блум-фильтром? Ведь Golomb-coded sets занимают меньше места.


    1. ArtemS
      02.06.2016 12:31

      Это не так. Тут биты «1» стоят очень близко друг другу, экономии никакой.


    1. Antelle
      02.06.2016 12:32

      *когда распределение бит геометрическое.
      То есть, когда фильтр достаточно разрежен и обладает меньшим % ошибок, это даст хороший результат. Я пробовал, получается так же или даже больше, потому что в нашем случае фильтр забит до предела.
      Тут есть графики: http://algo2.iti.kit.edu/singler/publications/cacheefficientbloomfilters-wea2007.pdf