Попался мне пост на Хабре, где автор классифицирует товары по их названиям: похожим, но не одинаковым. Для этого он написал генератор паттернов регулярных выражений strtree. Я специально такие генераторы никак не мог найти, а тут он мне сам под пальцы попался.

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

Вы спросите меня: «Дед, ты забыл, какой год сейчас? Ну какие паттерны на регулярках, когда есть БЯМки?». А я вам с умным лицом напомню про первое правило машинного обучения: начать без машинного обучения. В более широком смысле, чем больше методов вы знаете, тем эффективнее вы сможете решать задачи. И не делать из БЯМ затычкой в бочке, где ей могла бы стать регулярка. Потом сможете толсто намекать, что хорошо бы часть сэкономленных денех вам в виде премии оформить.

Да ну и интересно мне в конце концов, че вы мне сделаете? Решил я протестировать на датасете, который мы собирали в Пситехлабе по суицидникам. Работать нужно с короткими строками, иначе паттерны будут перебираться часами. Я отбирал по первому квартилю длины текстов, который у меня получился в 38 символов, и считалось довольно быстро в итоге. Пробовал работать с медианой в 60 символов — терпимо, но это предел комфорта. В датасете несколько классов, и пусть библиотека вроде поддерживает мультикласс, я решил тестить по бинарной схеме one-vs-all. Тестовый датасет семплировал поровну по 300 примеров с разделением на трейн/тест как 0.7/0.3.

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

Вот что будет при длине паттерна 1:


.+ю.+ (n_matches: 257)
^о.+ (n_matches: 10)
^я н.+в.+а$ (n_matches: 2)
.+шка$ (n_matches: 3)
^м.+ь.+я .+то.+ (n_matches: 1)
^а.+ь .+ (n_matches: 1)
.+а.+м.+ь.+я$ (n_matches: 2)
^и.+ч.+л.+ (n_matches: 2)
.+п.+в$ (n_matches: 1)
^м.+я.+я.+ь.+ (n_matches: 1)
^в.+ь$ (n_matches: 1)
.+ж.+ю$ (n_matches: 1)
.+ч.+ц.+ (n_matches: 1)
^я.+б.+ь$ (n_matches: 1)
^м.+на$ (n_matches: 1)

А вот что будет при 5:


.+ любл.+ (n_matches: 153)
.+любил.+ (n_matches: 44)
^о.+ м.+нрави.+с.+ (n_matches: 6)
.+ .+ .+ .+ люби.+ь$ (n_matches: 2)
.+нрави.+е.+ (n_matches: 4)
.+м.+ .+ люби.+ (n_matches: 3)
.+ влюб.+ (n_matches: 4)
.+ для .+ .+ (n_matches: 5)
.+ень п.+ (n_matches: 4)
^я.+ люби.+ (n_matches: 2)
.+ восх.+ (n_matches: 2)
.+ обня.+ (n_matches: 2)
.+ любо.+ (n_matches: 2)
.+е одн.+ (n_matches: 2)
.+я обо.+ (n_matches: 2)
.+гда е.+ (n_matches: 1)
^я нев.+ (n_matches: 1)
^там м.+ (n_matches: 1)
.+е поп.+ (n_matches: 1)
^мой а.+ (n_matches: 1)
^я гор.+ (n_matches: 1)
^вы вс.+ (n_matches: 1)
.+сто х.+ (n_matches: 1)
^я е п.+ (n_matches: 1)
^я нра.+ (n_matches: 1)

А вот при 10:


.+ очень люб.+ (n_matches: 22)
.+ сильно лю.+ (n_matches: 13)
.+ люблю его$ (n_matches: 11)
.+ полюбила .+ (n_matches: 11)
.+ его люблю$ (n_matches: 12)
.+ люблю его.+ (n_matches: 8)
.+я влюбилас.+ (n_matches: 6)
.+о я люблю .+ (n_matches: 6)
.+ люблю теб.+ (n_matches: 5)
.+ понравила.+ (n_matches: 5)
.+ого челове.+ (n_matches: 5)
.+ люблю сво.+ (n_matches: 6)
.+его люблю .+ (n_matches: 4)
.+ я ее любл.+ (n_matches: 3)
(и еще куча паттернов по 3,2,1 срабатываний, всего 99)

Вот табличка с качеством каждого набора паттернов:

Мин. длина токена Кол-во листьев Accuracy
1 15 0.780
5 25 0.873
10 99 0.733

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

Кстати, сам класс называется “защитные факторы/выражение любви”. В него попадаются тексты, где люди говорят, что им кто-то не безразличен. Чаще всего, конечно, говорят о любви. К сожалению, «отрицательных» паттернов, указывающих на шум, я не нашел ни для этого класса, ни для других, с которыми тыкался. Возможно, мои данные достаточно чисты. Зато точно можно сказать, что для второго применения — как дополнительный шаг EDA — метод вполне себе сгодится. При желании, алгоритм можно докрутить. Если посмотрите в код, то автор в методе генерации начальных токенов прямо коммент написал “You may add other tokens here, f.e. with tokens “.*”, etc”. Алгоритм комбинации и аугментации паттернов, думаю, можно подшаманить, чтобы быстрее работал. Сравнил с логистической регрессией — дает результат на пару пунктов лучше.

В общем, пока что будем знать, что такое есть. Авось когда-нибудь выстрелит.