Створення частотного словника з урахуванням аналізу бібліотеки художньої литературы

Нещодавно для шліфування морфологічного словника, здатного (імовірно) генерувати всі можливі форми слова з інфінітиву, мені знадобився досить об'ємний частотний словник української мови. Частотний словник - річ дуже проста, слова в ньому впорядковані за частотою, з якою вони зустрічаються в тексті, що аналізується.

Завдання, начебто дуже проста і напевно вирішується щодо програмування у перших рядах. Але для аналізу такої громіздкої бібліотеки, а бібліотека, що використовується мною, простягається на 157 гектарів, коштів середнього домашнього комп'ютера вистачає з натягом. Якщо сказати точніше, бібліотека зберігається в п'ятдесяти zip архівах по 0.5 - 2 Гб, у кожному з яких 20-30 тис. творів у форматі fb2. У стислому вигляді вона важить 60 Гб.

Вирішувалося завдання мовою c#. Результат потрібно отримати за адекватний час, як я вибрав не більше 8 годин, щоб можна було запустити виконання ввечері і отримати результат вранці.

Пошук рішення

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

Тоді я почав шукати — де ж саме вузьке місце, яке не дає програмі зітхнути. Вузьке місце опинилося в момент додавання нового слова. Щоб зберегти масив упорядкованим – слово доводитьсявставляти десь посередині, а іноді й насамперед. Для цього доводиться зрушувати кожен елемент масиву розташований правіше від обраної позиції вправо. Коли йдеться про мільйони слів — це заняття стає дуже стомлюючим для процесора і він мстить, відкладаючи завершення програми на тижні вперед.

Упорядкований список

Довелося шукати таку структуру даних, яка дозволить додати кожен елемент просто на кінець цієї структури, але дозволить у той же час їх упорядкувати. Тут же я натрапив на впорядковані списки. У комірці впорядкованого списку зберігається сам шматочок даних та покажчик на іншу комірку. Все чудово, ми просто додаємо нове слово і змінюємо 2 індекси, індекс попереднього слова, що вказує на це та індекс цього слова, що вказують на наступне. Але, ось невдача, пошук у такому списку дуже обчислювально вимогливий. Якщо в упорядкованому масиві ми могли починати пошук із середини і ділити його навпіл за одну ітерацію, то в упорядкованому списку доводиться дертися за покажчиками від одного до іншого, як по ниточці, поки не знайдемо потрібне місце у всьому клубку. Пробувати цей варіант я не став, навчений попереднім провалом я одразу пронюхав засідку.

Бінарне дерево пошуку

Наступну структуру даних я знайшов лише за кілька годин. Мені потрапили бінарні дерева пошуку. Трохи почитавши про різні варіанти я зупинився на дереві, що самобалансується AVL, створеному, до речі радянськими вченими Адельсон-Вельським Георгієм Максимовичем і Ландисом Євгеном Михайловичем, і успадкував від їх прізвищ свою назву.

Структура бінарного дерева схожа на впорядкований список, але кожен елемент, крім кількох кінцевих (т.зв. листя) посилається на два елементи. Кореневий елемент - це той, що знаходився б усередині упорядкованого масиву. Він посилається на елемент менший (лівий) і більший (правий), крім того - гарантовано, що всі елементи лівої гілки будуть меншими від даного, а всі елементи правої гілки - більше. Розглянувши лівий або правий елемент, до якого ми прийшли - ми побачимо ту ж ієрархію, він також посилається на два елементи, ліва гілка також менше, права - більше. Щоб зрозуміти спосіб балансування такого дерева, найкраще прочитати відповідну статтю, наприклад тут: АВЛ-дерева. Важливо зауважити, що двійкове дерево пошуку повністю задовольнило мої вимоги. Швидкий пошук та швидке додавання нового елемента.

В результаті, витративши ще кілька годин на оптимізацію, я отримав багатопотоковий додаток, який розпаковує архів в оперативну пам'ять, вважає частоту різних слів і за допомогою AVL дерева обробляє отримані дані.

Ось кілька рядків за результатами роботи програми, ліворуч – слово, праворуч – частота. Це найпоширеніші слова різної довжини:

  • я 34361402
  • а 36192092
  • з 52479822
  • у 126422246
  • та 158458227

  • за 23978868
  • він 32602346
  • то 42163693
  • на 83001625
  • не 97183097

  • усі 19576264
  • це 21318968
  • його 27719894
  • як 30228770
  • що 63106338

  • навіть 6789652
  • була 6832494
  • якщо 7750404
  • мене 12381969
  • було 15450767

  • може 5455561
  • дуже 5477013
  • час 6317279
  • коли 9788599
  • щоб 9987225

  • людиноненависництва 296
  • висококваліфікованого 350
  • високоповажності 384
  • високопревосходительства 489
  • високопревосходительство 3739

  • людиноненависницького 46
  • людиноненависництвом 52
  • приватнопідприємницької 60
  • високопревосходительством 91
  • націоналсоціалістичного 96

Загалом отримано 9.5 млн. слів, аналіз тривав 8482 сек., середня швидкість обробки розпакованого тексту — 18.57 мб/сек.

Тепер можна використовувати отримані дані для доопрацювання мого морфологічного словничка, а отримавши словничок можна буде покращити частотний аналізатор, т.к. з'явиться можливість угруповання однокорінних слів. Крім того, доопрацювання вимагає робота «частотного аналізатора» з різними кодуваннями тощо. Далі – синтаксичний аналіз пропозиції. Зрештою хочу отримати якось адекватну семантичну мережу.

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