Az.js: JavaScript-бібліотека для обробки текстів російською мовою

Як чудовий і глибокий російська курлык
— Генератор постів
Обробка природної мови (natural language processing, NLP) — тема, на мій погляд, дуже цікава. По-перше, завдання тут чисто алгоритмічні: на вхід приймаємо зовсім примітивний об'єкт, рядок, а витягти намагаємося вкладений в нього зміст (ну або хоча б частинку сенсу). По-друге, необов'язково бути професійним лінгвістом, щоб вирішувати ці завдання: досить знати рідну мову на більш-менш пристойному рівні і любити його.

А ще з невеликими витратами можна зробити якогось безглуздого чат-бота — або, як ось я, генератор постів на основі того, що ви писали на своїй сторінці в соцмережі. Можливо, хтось з вас вже бачив це додаток — воно досить дурне, найчастіше видає безглуздий і нескладний текст, але зрідка все ж дає привід посміхнутися.

Незв'язність текстів у нинішній версії «Генератора» викликана тим, що насправді ніякого аналізу він робити не вміє. Просто в одних випадках «пророкує» продовження пропозиції по зібраним биграммам, а в інших — замінює в готовому реченні деякі слова на інші, які закінчуються схоже. Ось і вся начинка.

Звичайно, хочеться зробити що-небудь цікавіше. Біда в тому, що модні зараз нейромережі не дуже-то застосовні тут: їм потрібно багато ресурсів, велику навчальну вибірку, а в браузері користувача соцмережі всього цього немає. Тому я вирішив вивчити питання роботи з текстами за допомогою алгоритмів. На жаль, готових інструментів для роботи з російською мовою JavaScript знайти не вдалося, і я вирішив зробити свій маленький велосипед.

Az
Свою бібліотеку я назвав Az. З одного боку — це перша буква кирилиці, «азъ», ну а з іншого — перша і остання літери латиниці. Відразу дам посилання:
GitHub;
— Документація: або на гітхабі ж або на Doclets.io;
Демо.
Встановити бібліотеки ви можете як з npm, так і з bower (в обох місцях вона має назву az). На свій страх і ризик — вона ще не покрита повністю тестами і до виходу першої версії (який станеться незабаром, я сподіваюся) у неї можуть змінитися публічні API. Ліцензія: MIT.

На даний момент бібліотека вміє дві речі: токенизацию і аналіз морфології. В деякому віддаленому майбутньому (але не в першій версії) передбачається реалізувати синтаксичний аналіз і витяг смислів з пропозицій.

Az.Маркери
Суть токенизации дуже проста: як я згадував вище, на вході ми приймаємо рядок — а на виході отримуємо «токени», групи символів, які (ймовірно) є окремими сутностями в цьому рядку.

Зазвичай для цієї мети використовується що-небудь типу одного виклику split за простою регулярці, але мені цього здалося мало. Наприклад, якщо розділити рядок за прогалин, ми втратимо самі прогалини — іноді це зручно, але не завжди. Ще гірше, якщо ми захочемо попередньо розбити рядок по точках, питальним і знаків оклику (сподіваючись виділити так пропозиції): мало того, що втрачаються конкретні знаки пунктуації, так ще й точки насправді не завжди завершують пропозиції. А потім ми розуміємо, що в реальних текстах можуть зустрітися, скажімо, посилання і точки в них точно ніякого відношення до пунктуації не мають) і регулярка стає зовсім страшною.

Az пропонує своє, не-деструктивне, рішення цієї задачі. Гарантується, що після токенизации кожен символ буде належати деякому токена і тільки одному. Прогалини, переклади рядків та інші невидимі символи об'єднуються в один тип токенів, слова — в інший, пунктуація — в третій. Якщо все токени склеїти разом — вийде в точності вихідна рядок.

При цьому токенизатор досить розумний, щоб розуміти, що дефіс, обрамлений пробілами — це знак (ймовірно, на його місці малося на увазі тире), а притиснутий хоча б з однієї сторони до речі — частина самого слова. Чи що «habrahabr.ru» — це посилання, а «mail@example.com — це, напевно, емейл (так, повна підтримка відповідного RFC не гарантується). #hashtag — це хештег, user — згадка.

І нарешті — раз вже RegExp'и для цієї мети використовувати не варто — Az.Tokens вміє парсити HTML (а заодно — вікі і Markdown). Точніше кажучи, ніякої деревоподібної структури на виході не буде, але всі теги будуть виділені в свої токени. Для тегів <script> <style> зроблено додаткове виняток: їхній уміст перетвориться на один великий токен (ви ж не збиралися розбивати на слова свої скрипти?).

А ось і приклад обробки Markdown-розмітки:



Зверніть увагу, дужки в одних випадках перетворюються в пунктуацію (світло-сині прямокутники), а в інших — у Markdown-розмітку (показана зеленим кольором).

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

Az.Morph
Після того, як текст розбитий на слова (за допомогою Az.Tokens або будь-яким іншим чином), з них можна спробувати витягти морфологічні атрибути: граммемы. Граммемой може бути частина мови, рід або відмінок — у таких граммем є значення, які самі по собі є граммемами, тільки «булевими операторами» (наприклад, чоловічий рід, орудний відмінок). «Булеві» граммемы можуть і не належати до якоїсь батьківського граммеме, а бути присутнім самі по собі, як прапори — скажімо, посліду застаріле або поворотний (дієслово).

Повний список граммем можна знайти на на сторінці проекту OpenCorpora. Майже всі використовувані бібліотекою значення відповідають позначенням цього проекту. Крім того, для аналізу використовується словник OpenCorpora, упакований в спеціальному форматі, але про це нижче. Взагалі творці проекту OpenCorpora великі молодці і я вам рекомендую не тільки ознайомитися з ним, але і прийняти участь в колаборативної розмітці корпусу — це також допоможе й іншим опенсорсным проектів.

Кожен конкретний набір граммем у слова називається тегом. Всіляких тегів значно менше, ніж слів — тому вони пронумеровані і зберігаються в окремому файлі. Щоб вміти відмінювати слова (а Az.Morph це теж вміє), треба якось уміти змінювати їх теги. Для цього існують парадигми слів: вони ставлять у відповідність тегам певні префікси і суфікси (тобто один і той же тег в різних парадигмах має різні пари префікс+суфікс). Знаючи парадигму слова, досить «відкусити» від нього префікс і суфікс, в поточному тегу, і додати префікс/суфікс того тега, в який ми його хочемо перевести. Як і тегів, парадигм у російській мові відносно небагато — це дозволяє в словнику зберігати для кожного слова тільки пару індексів: номер парадигми і номер тега в ній.

Ось приклад: слово «міцна» тег має, по-російськи коротко позначається як «ПРИЛ, кач жр, од, їм» — тобто це якісне прикметник жіночого роду однини називному відмінку. Цього тегу (в тій парадигмі, якій належить слово «міцна»), відповідає порожній префікс і суфікс «-кая». Припустимо, ми хочемо отримати з цього слова його порівняльну ступінь, та ще й не звичайну, а особливу, з префіксом «»: «КОМП, кач сравн2». У неї в цій парадигмі, як неважко здогадатися, префікс «» суфікс «-че». Відрізаємо «-кая», додаємо «» і «-че» — отримуємо шукану форму «міцніше».

Таке от щодо сумбурний виклад внутрішнього механізму відмін в Az.Morph. По суті ця частина бібліотеки — порт чудового морфологічного аналізатора pymorphy2 за авторством kmike (на Хабре була пара статей про цій бібліотеці). Крім самого аналізатора, рекомендую ознайомитися з документації — там багато корисної інформації, яка повністю застосовна і до Az теж. Крім того, Az використовує формат словників, аналогічний словників pymorphy2, за винятком невеликих деталей (які дозволили зробити словника на 25% компактніше). З цієї причини, на жаль, самостійно зібрати їх не вийде — але в майбутньому така можливість, звичайно, з'явиться.

Як я вже згадав, основні словники зберігаються у хитрому форматі DAWG (у вікі є стаття про directed acyclic word graph, як про абстрактну структури даних, але про конкретну реалізації інформації мало). Реалізуючи його підтримку в JS, я оцінив фічу pymorphy2, що дозволяє при пошуку слова відразу перевіряти варіант з «е» замість «є» — це не дає особливих втрат в продуктивності з-за того, що при спуску по префіксному дерева легко обійти гілки, що відповідають обом буквах. Але мені цього здалося мало і я аналогічним чином додав можливість нечіткого пошуку слів з помилками (тобто можна задати максимальну відстань Дамерау-Левенштейна, на якому повинно знаходитися шукане слово від заданого). Крім того, можна знаходити «розтягнуті» слова — за запитами «гоооол» або «го-о-о-ол» знайдеться словникове «гол». Зрозуміло, ці особливості також опціональні: якщо ви працюєте в «тепличних умовах», з грамотними, вичитаними текстами — пошук помилок варто заборонити. А ось для написаних користувачами записів, це може бути досить актуально. У планах — заодно ловити і найбільш поширені помилки, що не є помилками.

Як бачите, по заданому слову бібліотека може повернути різні варіанти розбору зі словника. І це пов'язано не тільки з помилками: класичний приклад граматичної омонімії — слово «сталі», яке може виявитися формою іменника «сталь», так і дієслова «стати». Щоб вирішити однозначно (зняти омонимию) — потрібно дивитися на контекст, на сусідні слова. Az.Morph цього поки не вміє (та й завдання це вже не для морфологічного модуля), тому поверне обидва варіанти.

Більше того: навіть якщо у словнику не знайшлося нічого підходящого, бібліотека застосує евристики (звані провісниками або парсерами), які зможуть передбачити, як слово схиляється — наприклад, по його закінченню. Тут би вставити веселу історію з башорга про аналізатор, який вважає слово «ліжко» дієсловом, та одна біда — воно, звичайно ж, є в словнику, і тільки як іменник :)

Втім, у мене знайшлися свої курйози. Наприклад, у слова «філософськи» серед інших варіантів знайшлися якісь «филососки» (з виправленою помилкою). Але найдивніше виявилося, що слово «мемас» стабільно розуміється як дієслово (!), інфінітив у якого — «мемасти» (!!!). Не відразу вдається зрозуміти, як таке взагалі можливо — але таку ж парадигму має, наприклад, слово «пасти». Ну і форми типи «мемасем», «мемасемте», «мемасенный», «мемасено», «мемасши», по-моєму, прекрасні.

Незважаючи на ці дивацтва, зазвичай результати виявляються досить адекватними. Цьому сприяє те, що (як і в pymorphy2) кожному варіанту присвоюється оцінка «правдоподібності» і вони сортуються по спаданню цієї оцінки. Так що якщо робите алгоритм швидше — можна брати перший варіант розбору, а якщо хочеться точності — варто перебрати всі.

Продуктивність
Що стосується швидкості, то в цілому тут все не дуже райдужно. Ліба писалася без упору на цей фактор. Передбачається, що додатками на JS (особливо браузерних) рідше доводиться стикатися з особливо великими обсягами даних. Якщо хочеться швидко аналізувати масивні колекції документів — варто приділити увагу pymorphy2 (особливо його оптимізованої версії, що використовує реалізацію на C для роботи зі словником).

За моїми грубим вимірами, конкретні цифри (в браузері Chrome) приблизно такі:
  • Токенизация: 0.7–1.0 млн символів в секунду
  • Морфологія без помилок: 210 слів в секунду
  • Морфологія з помилками: 180 слів в секунду
Втім, серйозних бенчмарків поки не проводилося. Крім того, бібліотеці бракує тестів — тому запрошую поганяти її на згаданій вище демці. Сподіваюся, ваша допомога наблизить реліз першої версії :)

Подальші плани
Головний пункт в roadmap бібліотеки — експерименти з синтаксичним аналізом. Маючи варіанти розбору кожного слова в реченні, можна будувати більш складні гіпотези про їх взаємозв'язках. Наскільки мені відомо, в опенсорсе таких інструментів зовсім небагато. Тим складніше і цікавіше буде думати над цим завданням.

Крім того, не знімається питання оптимізації — JS навряд чи зможе встигнути за кодом на C, але щось поліпшити напевно можна.

Інші плани, згадані в статті: інструмент для самостійного складання словників, пошук слів з помилками (тобто, скажімо, щоб для слів на «т(ь)ся» повертався варіант і з м'яким знаком, так і без, а синтаксичний аналізатор обирав би правильний).

І, зрозуміло, я відкритий для ваших ідей, пропозицій, багрепортам, форкам і пулреквестам.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.