Расширенный частотный анализ. Биграммы и триграммы

Пора расширить наш частотный анализатор текстов, для того, чтобы подсчитывать частоту повторения не только отдельных букв, но и их сочетаний — биграмм и триграмм. Прошлый раз мы получили довольно простой анализатор, но и область применения его не так широка. В этот раз мы нарастим функционал. Здесь мы преследуем две цели, первая — это приобретение практических навыков программирования на языке common lisp. Вторую я озвучу в следующих заметках, поскольку мы ещё вернёмся к анализу текста.

Терминология

Биграмма это сочетание из двух букв алфавита. Триграмма из трёх. Из комбинаторики становится понятно, что количество всевозможных биграмм русского языка равно 1089 (33^2). Тогда как для триграмм это число равно 35937 (33^3). Однако, эти числа не совсем справедливы, например биграммы, состоящей из двух мягких знаков быть не может. Даже если считать биграмму как любые два символа в слепленном тексте без пробелов, то всё равно мы никогда не встретим два идущих подряд мягких знака, аналогично с твёрдыми знаками. Поэтому числа преведённые выше являются скорее верхними границами, чем реальными значениями.

Второй вопрос сразу приходящий на ум — почему мы ограничились цифрой три. Можно было пойти дальше и взять четверо-граммы, пяти-граммы и т.д. Можно было, но это не принесло бы нам пользы. К сожалению, сейчас я не могу сослаться на какую-либо работу, но могу сказать, что н-граммы после трёх уже не так эффективны, а число их слишком велико. Для простого анализа текста обычно хватает триграмм.

Алгоритм

Алгоритм для расчёта частоты триграмм отличается от расчёта частоты каждой буквы тем лишь, что каждый раз мы ищем в хэш-таблице не один символ, а строку из нескольких символов. Идея алгоритма такова, что мы заводим ещё одну переменную в цикле loop. Затем каждую итерацию мы добавляем в конец этой переменной символ и выбрасываем первый символ если нужно. Таким образом, у нас на каждой итерации, кроме нескольких первых, получается строка их нескольких символов. Вот пример кода:

(defun get-n-gramm(text &optional (n 2) (container nil))
  "Returns hash-table with downcased n-grams as keys and frequencies as values"
  (when (null container) (setf container (make-hash-table :test 'equal)))
  (loop
     for chr across text
     for ngram = ""
     then (let
              ((lngram ngram) (lchar (char-downcase chr)))
            (when (alpha-char-p lchar)
              (if (= (length lngram) n)
                  (setf lngram (concatenate 'string (subseq lngram 1) (list lchar)))
                  (setf lngram (concatenate 'string lngram (list lchar))))
              (if (= (length lngram) n)
                  (let ((frequency (gethash lngram container 0)))
                    (setf (gethash lngram container) (+ 1 frequency)))))
            lngram)
     finally (return-from get-n-gramm container)))

Здесь я сразу обернул всё в функцию, которая принимает строку в качестве текста и два опциональных параметра. Это количество букв в строке и хэш-таблица куда всё складывать.

Все исходные коды можно посмотреть в моём репозитории на github.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.