library(tidyverse)4 Нормализация и оценка
4.1 Регулярные выражения
Есть старая шутка, ее приписывают программисту Джейми Завински: если у вас есть проблема, и вы собираетесь ее решать при помощи регулярных выражений, то у вас две проблемы. Регулярные выражения – это формальный язык, который используется для того, чтобы находить, извлекать и заменять части текста. Мы воспользуемся регулярными выражениями для того, чтобы привести в порядок распознанный текст.
Для работы нам понадобится пакет stringr из библиотеки tidyverse.
Загрузим распознанный текст элегии.
text <- readLines(con = "../ocr/rosalia_1.txt")
text [1] "ЭЭСЮЭЭЭЭЭСКЮЭЭЭЭЭСЮС»ЭЭЭ<Э<ЗС>Э(ІЭ(99ЭС933 э о э з о з э з э"
[2] ""
[3] ""
[4] ""
[5] ""
[6] " РАЗЛУКА."
[7] " ( Э л е г і я ,)"
[8] ""
[9] ""
[10] ""
[11] "Розалія, мой спутникъ неизмѣнный"
[12] " На полѣ радостей земныхъ!"
[13] "Розалія, мой другъ, хранитель несравненный!"
[14] "Когда я отдохну въ объятіяхъ твоихъ? . •."
[15] "Съ тобою горестей душа моя незнаетъ,"
[16] "И сердцу скорбному не измѣнитъ покой!"
[17] "Надежда мрачный путь звѣздою озаряетъ,"
[18] " И я мирюсь съ враждебною судьбой! . . •"
[19] "Теперь, за дальними, свирѣпыми морями"
[20] " Твой сладкій гласъ не оживитъ меня!"
[21] "Взойдетъ заря надъ злачными холмами,"
[22] " Появится въ лучахъ свѣтило дня —"
[23] " Напрасно! все кругомъ покрыто мглою."
[24] " Неслышится мнѣ сладкій ігівой привѣтъ."
[25] " Всѣ радости, надежды всѣ съ тобою —"
[26] " И опустѣлъ безъ милой свѣтъ!"
[27] "Подруга милая, скажи, что край прелестный,"
[28] " Что мирныя, тѣнисты я поля,"
[29] "Что своенравныя судьбы привѣтъ мнѣ лестный,"
[30] " Когда съ тобой въ разлукѣ я."
[31] " Но другъ мои! горесть отл етаетъ"
[32] ""
[33] " 243"
[34] ""
[35] ""
[36] " На быстрыхъ времени крылахъ,"
[37] " И радость сердце посѣщ аетъ. . . ."
[38] " Моя надежда — въ небесахъ!. . ."
[39] " Когдажъ опять смягченными судьбами"
[40] " Я въ радости къ подругѣ понесусь,"
[41] "Коснусь волшебныхъ струнъ волшебными пер"
[42] " стами"
[43] " И, съ рѣзвою мечтою примирюсь."
[44] ""
[45] " А, Б — фЪ."
[46] ""
4.1.1 Литералы и классы
Регулярные выражения (regex, regexp) объединяют буквальные символы (литералы) и метасимволы (символы-джокеры, англ. wildcard characters).
Для поиска используется строка-образец (англ. pattern, по-русски её часто называют “шаблоном”, “маской”), которая задает правило поиска. Строка замены также может содержать в себе специальные символы.
Буквальные символы – это то, что вы ожидаете увидеть (или не увидеть – для управляющих и пробельных символов); можно сказать, что это символы, которые ничего не “имеют в виду”. Их можно объединять в классы при помощи квадратных скобок.
Для поиска совпадений используются три функции: str_detect(), str_which() и str_subset(). Первая возвращает логический вектор (то есть вектор значений TRUE / FALSE); вторая – индексы элементов, а третья – сами эти элементы. Сравним.
# возвращает логический вектор
str_detect(text, "[ѣъ]") [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE TRUE
[13] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
[25] TRUE TRUE FALSE TRUE TRUE TRUE TRUE FALSE FALSE FALSE FALSE TRUE
[37] TRUE TRUE TRUE TRUE TRUE FALSE TRUE FALSE FALSE FALSE
Так мы нашли все строки, где есть еры или яти. Можно сохранить логический вектор и использовать его для индексации. Функция head() позволяет ограничить вывод первыми элементами вектора.
# создаем индекс
idx <- str_detect(text, "[ѣъ]")
# используем его для отбора строк
text[idx] Теперь узнаем, в каких строках находятся рядом буквы ія. Мы не объединяем их в класс при помощи квадратных скобок, поэтому фукнция ищет не что-то одно, а именно сочетание.
str_which(text, "ія")[1] 11 13 14
Эту функцию тоже можно использовать для индексации. На этот раз не сохраняем переменную idx.
text[str_which(text, "ія")][1] "Розалія, мой спутникъ неизмѣнный"
[2] "Розалія, мой другъ, хранитель несравненный!"
[3] "Когда я отдохну въ объятіяхъ твоихъ? . •."
Наконец, str_subset() сама индексирует вектор. Попробуем.
str_subset(text, "i")character(0)
Упс. Что произошло, куда делись все i? Дело в том, что я набрала i в латинской клавиатуры, а это другой знак в Юникоде. Проверим (первую i копирую из текста выше):
"і" == "i"[1] FALSE
Исправляем.
str_subset(text, "і")[1] " ( Э л е г і я ,)"
[2] "Розалія, мой спутникъ неизмѣнный"
[3] "Розалія, мой другъ, хранитель несравненный!"
[4] "Когда я отдохну въ объятіяхъ твоихъ? . •."
[5] " Твой сладкій гласъ не оживитъ меня!"
[6] " Неслышится мнѣ сладкій ігівой привѣтъ."
В некоторых случаях удобнее использовать непосредственно код буквы.
str_subset(text, "[\u0406\u0456]")[1] "ЭЭСЮЭЭЭЭЭСКЮЭЭЭЭЭСЮС»ЭЭЭ<Э<ЗС>Э(ІЭ(99ЭС933 э о э з о з э з э"
[2] " ( Э л е г і я ,)"
[3] "Розалія, мой спутникъ неизмѣнный"
[4] "Розалія, мой другъ, хранитель несравненный!"
[5] "Когда я отдохну въ объятіяхъ твоихъ? . •."
[6] " Твой сладкій гласъ не оживитъ меня!"
[7] " Неслышится мнѣ сладкій ігівой привѣтъ."
Обратите внимание, что у прописных и строчных букв свои коды, и в предыдущем случае мы упустили строку с буквой І.
Для некоторых классов есть специальные обозначения.
| Класс | Эквивалент | Значение |
|---|---|---|
| [:upper:] | [A-Z] | Символы верхнего регистра |
| [:lower:] | [a-z] | Символы нижнего регистра |
| [:alpha:] | [[:upper:][:lower:]] | Буквы |
| [:digit:] | [0-9], т. е. \d | Цифры |
| [:alnum:] | [[:alpha:][:digit:]] | Буквы и цифры |
| [:word:] | [[:alnum:]], т. е. \w | Символы, образующие «слово» |
| [:punct:] | [-!“#$%&’()*+,./:;<=>?@[\]_`{|}~] | Знаки пунктуации |
| [:blank:] | [\s\t] | Пробел и табуляция |
| [:space:] | [[:blank:]\v\r\n\f], т. е. \s | Пробельные символы |
| [:cntrl:] | Управляющие символы (перевод строки, табуляция и т.п.) | |
| [:graph:] | Печатные символы | |
| [:print:] | Печатные символы с пробелом |
Эти классы тоже можно задавать в качестве паттерна. Знак \\b означает любую границу слова (начало строки, конец строки, пробел, пунктуация).
str_subset(text, "[[:digit:]]")[1] "ЭЭСЮЭЭЭЭЭСКЮЭЭЭЭЭСЮС»ЭЭЭ<Э<ЗС>Э(ІЭ(99ЭС933 э о э з о з э з э"
[2] " 243"
Работы с регулярными выражениями требует навыка; поначалу, прежде чем преобразовывать строки, удобно просто посмотреть, что попало в ваш невод.
str_view(text[1:8], "[[:punct:]]", html = TRUE)Внутри квадратных скобор знак ^ означает отрицание. Сравните:
# удаляем всю пунктуацию
str_remove_all(text[1:8], "[[:punct:]]") [1] "ЭЭСЮЭЭЭЭЭСКЮЭЭЭЭЭСЮСЭЭЭ<Э<ЗС>ЭІЭ99ЭС933 э о э з о з э з э"
[2] ""
[3] ""
[4] ""
[5] ""
[6] " РАЗЛУКА"
[7] " Э л е г і я "
[8] ""
# удаляем все, кроме пунктуации
str_remove_all(text[1:8], "[^[:punct:]]") [1] "»((" "" "" "" "" "." "(,)" ""
В качестве классов можно рассматривать и следующие обозначения:
| Представление | Эквивалент | Значение |
|---|---|---|
| \d | [0-9] | Цифра |
| \D | [^\\d] | Любой символ, кроме цифры |
| \w | [A-Za-zА-Яа-я0-9_] | Символы, образующие «слово» (буквы, цифры и символ подчёркивания) |
| \W | [^\\w] | Символы, не образующие «слово» |
| \s | [ \t\v\r\n\f] | Пробельный символ |
| \S | [^\\s] | Непробельный символ |
Найдем все строки с числами и удалим их (в нашем случае либо номера страниц, либо ошибки распознавания). Также удалим все пустые строки.
# вторая косая черта "экранирует" первую
text2 <- text[!str_detect(text, "\\d") & nchar(text) != 0]
text2 [1] " РАЗЛУКА."
[2] " ( Э л е г і я ,)"
[3] "Розалія, мой спутникъ неизмѣнный"
[4] " На полѣ радостей земныхъ!"
[5] "Розалія, мой другъ, хранитель несравненный!"
[6] "Когда я отдохну въ объятіяхъ твоихъ? . •."
[7] "Съ тобою горестей душа моя незнаетъ,"
[8] "И сердцу скорбному не измѣнитъ покой!"
[9] "Надежда мрачный путь звѣздою озаряетъ,"
[10] " И я мирюсь съ враждебною судьбой! . . •"
[11] "Теперь, за дальними, свирѣпыми морями"
[12] " Твой сладкій гласъ не оживитъ меня!"
[13] "Взойдетъ заря надъ злачными холмами,"
[14] " Появится въ лучахъ свѣтило дня —"
[15] " Напрасно! все кругомъ покрыто мглою."
[16] " Неслышится мнѣ сладкій ігівой привѣтъ."
[17] " Всѣ радости, надежды всѣ съ тобою —"
[18] " И опустѣлъ безъ милой свѣтъ!"
[19] "Подруга милая, скажи, что край прелестный,"
[20] " Что мирныя, тѣнисты я поля,"
[21] "Что своенравныя судьбы привѣтъ мнѣ лестный,"
[22] " Когда съ тобой въ разлукѣ я."
[23] " Но другъ мои! горесть отл етаетъ"
[24] " На быстрыхъ времени крылахъ,"
[25] " И радость сердце посѣщ аетъ. . . ."
[26] " Моя надежда — въ небесахъ!. . ."
[27] " Когдажъ опять смягченными судьбами"
[28] " Я въ радости къ подругѣ понесусь,"
[29] "Коснусь волшебныхъ струнъ волшебными пер"
[30] " стами"
[31] " И, съ рѣзвою мечтою примирюсь."
[32] " А, Б — фЪ."
Теперь удалим лишние пробелы и заменим яти на е. Функция str_replace() заменяет только первое вхождение в каждом элементе вектора, поэтому в строке 16 осталось “привѣтъ” (ср. 17, 18 и 21).
text2 |>
str_squish() |>
str_replace("ѣ", "е") [1] "РАЗЛУКА."
[2] "( Э л е г і я ,)"
[3] "Розалія, мой спутникъ неизменный"
[4] "На поле радостей земныхъ!"
[5] "Розалія, мой другъ, хранитель несравненный!"
[6] "Когда я отдохну въ объятіяхъ твоихъ? . •."
[7] "Съ тобою горестей душа моя незнаетъ,"
[8] "И сердцу скорбному не изменитъ покой!"
[9] "Надежда мрачный путь звездою озаряетъ,"
[10] "И я мирюсь съ враждебною судьбой! . . •"
[11] "Теперь, за дальними, свирепыми морями"
[12] "Твой сладкій гласъ не оживитъ меня!"
[13] "Взойдетъ заря надъ злачными холмами,"
[14] "Появится въ лучахъ светило дня —"
[15] "Напрасно! все кругомъ покрыто мглою."
[16] "Неслышится мне сладкій ігівой привѣтъ."
[17] "Все радости, надежды всѣ съ тобою —"
[18] "И опустелъ безъ милой свѣтъ!"
[19] "Подруга милая, скажи, что край прелестный,"
[20] "Что мирныя, тенисты я поля,"
[21] "Что своенравныя судьбы приветъ мнѣ лестный,"
[22] "Когда съ тобой въ разлуке я."
[23] "Но другъ мои! горесть отл етаетъ"
[24] "На быстрыхъ времени крылахъ,"
[25] "И радость сердце посещ аетъ. . . ."
[26] "Моя надежда — въ небесахъ!. . ."
[27] "Когдажъ опять смягченными судьбами"
[28] "Я въ радости къ подруге понесусь,"
[29] "Коснусь волшебныхъ струнъ волшебными пер"
[30] "стами"
[31] "И, съ резвою мечтою примирюсь."
[32] "А, Б — фЪ."
Чтобы заменить все вхождения, используем str_replace_all(). Можно произвести сразу несколько замен, задав вектор соответствий:
text3 <- text2 |>
str_squish() |>
str_replace_all(c("і" = "и", "ѣ" = "е"))4.1.2 Якоря и квантификация
Якоря позволяют искать последовательности символов в начале или в конце строки. Знак ^ (вне квадратных скобок!) означает начало строки, а знак $ – конец. Мнемоническое правило: First you get the power (^) and then you get the money ($).
str_subset(text3, ",$")[1] "Съ тобою горестей душа моя незнаетъ,"
[2] "Надежда мрачный путь звездою озаряетъ,"
[3] "Взойдетъ заря надъ злачными холмами,"
[4] "Подруга милая, скажи, что край прелестный,"
[5] "Что мирныя, тенисты я поля,"
[6] "Что своенравныя судьбы приветъ мне лестный,"
[7] "На быстрыхъ времени крылахъ,"
[8] "Я въ радости къ подруге понесусь,"
str_subset(text3, "^Ч")[1] "Что мирныя, тенисты я поля,"
[2] "Что своенравныя судьбы приветъ мне лестный,"
Найдем строки, которые начинаются со строчной:
str_subset(text3, "^[а-я]")[1] "стами"
Теперь найдем все строки, в которых больше одного знака пунктуации или пробела в конце. Для этого нам нужны не только якоря, но и квантификаторы. Квантификатор после символа, символьного класса или группы определяет, сколько раз предшествующее выражение может встречаться.
| Представление | Число повторений | Эквивалент |
|---|---|---|
| ? | Ноль или одно | {0,1} |
| * | Ноль или более | {0,} |
| + | Одно или более | {1,} |
Точное число повторений (интервал) можно задать в фигурных скобках:
| Представление | Число повторений |
|---|---|
| {n} | Ровно n раз |
| {m,n} | От m до n включительно |
| {m,} | Не менее m |
| {,n} | Не более n |
str_subset(text3, "\\W{2,}$")[1] "( Э л е г и я ,)"
[2] "Когда я отдохну въ объятияхъ твоихъ? . •."
[3] "И я мирюсь съ враждебною судьбой! . . •"
[4] "Появится въ лучахъ светило дня —"
[5] "Все радости, надежды все съ тобою —"
[6] "И радость сердце посещ аетъ. . . ."
[7] "Моя надежда — въ небесахъ!. . ."
4.1.3 Метасимволы и экранирование
Все метасимволы представлены в таблице ниже.
| Описание | Символ |
|---|---|
| открывающая квадратная скобка | [ |
| закрывающая квадратная скобка | ] |
| обратная косая черта | \ |
| карет | ^ |
| знак доллара | $ |
| точка | . |
| вертикальная черта | | |
| знак вопроса | ? |
| астериск | * |
| плюс | + |
| открывающая фигурная скобка | { |
| закрывающая фигурная скобка | } |
| открывающая круглая скобка | ( |
| закрывающая круглая скобка | ) |
Квадратные скобки используются для создания классов, карет и знак доллара – это якоря, но карет внутри квадратных скобок может также быть отрицанием. Точка – это любой знак.
# любой символ после знака вопроса
str_subset(text3, "\\?.") [1] "Когда я отдохну въ объятияхъ твоихъ? . •."
Две косые черты перед знаком вопроса означают экранирование. Оно используется тогда, когда необходимо найти буквальную точку, буквальный знак вопроса и т.п., т.е. превратить метасимвол в литерал. Для этого перед знаком ставится косая черта. Но так как сама косая черта – это метасимвол, но нужно две косые черты, первая из которых экранирует вторую.
Часто используется последовательность .* для обозначения любого количества любых символов между двумя частями регулярного выражения. Вот так находим любой знак между двумя (буквальными) точками:
str_extract(text3, "\\..\\.") [1] NA NA NA NA NA NA NA NA NA ". ." NA NA
[13] NA NA NA NA NA NA NA NA NA NA NA NA
[25] ". ." ". ." NA NA NA NA NA NA
Вот так – любое число знаков между двумя точками.
str_extract(text3, "\\..*\\.") [1] NA NA NA NA NA ". •." NA
[8] NA NA ". ." NA NA NA NA
[15] NA NA NA NA NA NA NA
[22] NA NA NA ". . . ." ". . ." NA NA
[29] NA NA NA NA
В регулярных выражениях квантификаторам соответствует максимально длинная строка из возможных (квантификаторы являются жадными, англ. greedy). Чтобы этого избежать, надо поставить после квантификатора знак вопроса. Это сделает его ленивым.
| regex | значение |
|---|---|
| ?? | 0 или 1, лучше 0 |
| *? | 0 или больше, как можно меньше |
| +? | 1 или больше, как можно меньше |
| {n,m}? | от n до m, как можно меньше |
Пример:
str_extract(text3, "\\..*?\\.") [1] NA NA NA NA NA ". •." NA NA NA ". ."
[11] NA NA NA NA NA NA NA NA NA NA
[21] NA NA NA NA ". ." ". ." NA NA NA NA
[31] NA NA
4.1.4 Группировка и Look arounds
Допустим, мы нашли несколько подряд знаков препинания и пробелов и хотим удалить лишние. Это можно сделать при помощи группировки
text4 <- text3 |>
# чтобы не потерять тире в конце строки
str_replace(" —$", "—") |>
str_replace("(\\W)(\\W+)$", "\\1")
text4 [1] "РАЗЛУКА."
[2] "( Э л е г и я "
[3] "Розалия, мой спутникъ неизменный"
[4] "На поле радостей земныхъ!"
[5] "Розалия, мой другъ, хранитель несравненный!"
[6] "Когда я отдохну въ объятияхъ твоихъ?"
[7] "Съ тобою горестей душа моя незнаетъ,"
[8] "И сердцу скорбному не изменитъ покой!"
[9] "Надежда мрачный путь звездою озаряетъ,"
[10] "И я мирюсь съ враждебною судьбой!"
[11] "Теперь, за дальними, свирепыми морями"
[12] "Твой сладкий гласъ не оживитъ меня!"
[13] "Взойдетъ заря надъ злачными холмами,"
[14] "Появится въ лучахъ светило дня—"
[15] "Напрасно! все кругомъ покрыто мглою."
[16] "Неслышится мне сладкий игивой приветъ."
[17] "Все радости, надежды все съ тобою—"
[18] "И опустелъ безъ милой светъ!"
[19] "Подруга милая, скажи, что край прелестный,"
[20] "Что мирныя, тенисты я поля,"
[21] "Что своенравныя судьбы приветъ мне лестный,"
[22] "Когда съ тобой въ разлуке я."
[23] "Но другъ мои! горесть отл етаетъ"
[24] "На быстрыхъ времени крылахъ,"
[25] "И радость сердце посещ аетъ."
[26] "Моя надежда — въ небесахъ!"
[27] "Когдажъ опять смягченными судьбами"
[28] "Я въ радости къ подруге понесусь,"
[29] "Коснусь волшебныхъ струнъ волшебными пер"
[30] "стами"
[31] "И, съ резвою мечтою примирюсь."
[32] "А, Б — фЪ."
Нам осталось удалить твердые знаки в конце слов (то есть перед пробелами, пунктуацией или в конце строки). Используем для этого так называемые look arounds.
| Запись | Название на русском | Описание |
|---|---|---|
(?=...) |
Положительный просмотр вперёд | Совпадает, если … находится в текущей позиции (но не захватывает его в результат). |
(?!...) |
Отрицательный просмотр вперёд | Совпадает, если … не находится в текущей позиции (не захватывает в результат). |
(?<=...) |
Положительный просмотр назад | Совпадает, если … находится сразу перед текущей позицией (длина … должна быть ограниченной). |
(?<!...) |
Отрицательный просмотр назад | Совпадает, если … не находится сразу перед текущей позицией (длина … должна быть ограниченной). |
str_view(text4, "[ъЪ](?=\\W)", html = TRUE)Теперь удалим такие знаки препинания, которые следуют за другими знаками препинания в конце строки. Также добавим в контекст сам конец строки (иначе остается с твердым знаком слово “отлетает”).
text5 <- str_remove_all(text4, "[ъЪ](?=\\W|$)")
text5 [1] "РАЗЛУКА."
[2] "( Э л е г и я "
[3] "Розалия, мой спутник неизменный"
[4] "На поле радостей земных!"
[5] "Розалия, мой друг, хранитель несравненный!"
[6] "Когда я отдохну в объятиях твоих?"
[7] "С тобою горестей душа моя незнает,"
[8] "И сердцу скорбному не изменит покой!"
[9] "Надежда мрачный путь звездою озаряет,"
[10] "И я мирюсь с враждебною судьбой!"
[11] "Теперь, за дальними, свирепыми морями"
[12] "Твой сладкий глас не оживит меня!"
[13] "Взойдет заря над злачными холмами,"
[14] "Появится в лучах светило дня—"
[15] "Напрасно! все кругом покрыто мглою."
[16] "Неслышится мне сладкий игивой привет."
[17] "Все радости, надежды все с тобою—"
[18] "И опустел без милой свет!"
[19] "Подруга милая, скажи, что край прелестный,"
[20] "Что мирныя, тенисты я поля,"
[21] "Что своенравныя судьбы привет мне лестный,"
[22] "Когда с тобой в разлуке я."
[23] "Но друг мои! горесть отл етает"
[24] "На быстрых времени крылах,"
[25] "И радость сердце посещ ает."
[26] "Моя надежда — в небесах!"
[27] "Когдаж опять смягченными судьбами"
[28] "Я в радости к подруге понесусь,"
[29] "Коснусь волшебных струн волшебными пер"
[30] "стами"
[31] "И, с резвою мечтою примирюсь."
[32] "А, Б — ф."
4.1.5 Оборачиваем в функцию
normalize_text <- function(text) {
# подумайте, нужно ли вам удалять строки с цифрами!
text[!str_detect(text, "\\d") & nchar(text) != 0] |>
str_squish() |>
str_replace_all(c("і" = "и", "ѣ" = "е")) |>
str_replace(" —$", "—") |>
str_replace("(\\W)(\\W+)$", "\\1") |>
str_remove_all("[ъЪ](?=\\W|$)")
}
normalize_text(text) [1] "РАЗЛУКА."
[2] "( Э л е г и я "
[3] "Розалия, мой спутник неизменный"
[4] "На поле радостей земных!"
[5] "Розалия, мой друг, хранитель несравненный!"
[6] "Когда я отдохну в объятиях твоих?"
[7] "С тобою горестей душа моя незнает,"
[8] "И сердцу скорбному не изменит покой!"
[9] "Надежда мрачный путь звездою озаряет,"
[10] "И я мирюсь с враждебною судьбой!"
[11] "Теперь, за дальними, свирепыми морями"
[12] "Твой сладкий глас не оживит меня!"
[13] "Взойдет заря над злачными холмами,"
[14] "Появится в лучах светило дня—"
[15] "Напрасно! все кругом покрыто мглою."
[16] "Неслышится мне сладкий игивой привет."
[17] "Все радости, надежды все с тобою—"
[18] "И опустел без милой свет!"
[19] "Подруга милая, скажи, что край прелестный,"
[20] "Что мирныя, тенисты я поля,"
[21] "Что своенравныя судьбы привет мне лестный,"
[22] "Когда с тобой в разлуке я."
[23] "Но друг мои! горесть отл етает"
[24] "На быстрых времени крылах,"
[25] "И радость сердце посещ ает."
[26] "Моя надежда — в небесах!"
[27] "Когдаж опять смягченными судьбами"
[28] "Я в радости к подруге понесусь,"
[29] "Коснусь волшебных струн волшебными пер"
[30] "стами"
[31] "И, с резвою мечтою примирюсь."
[32] "А, Б — ф."
В зависимости от задачи, продумайте свою нормализацию. Она может включать в себя склейку переносов и другие преобразования.
writeLines(text5[-c(1,2,32)], con = "../ocr/rosalia_norm.txt")4.2 Оценка качества распознавания
_________________________________
<Этот раздел еще дорабатывается.>
---------------------------------
\
\
^__^
(oo)\ ________
(__)\ )\ /\
||------w|
|| ||
4.2.1 Метрики качества OCR: CER и WER
Для оценки качества распознавания текста (OCR) стандартно используются две метрики — CER (Character Error Rate) и WER (Word Error Rate). Обе рассчитываются на основе расстояния Левенштейна — минимального числа вставок, удалений или замен, необходимых для преобразования одного текста в другой.
CER (ошибки на символ):
CER = (редакционное расстояние по символам) / (число символов в эталонном тексте)
WER (ошибки на слова):
WER = (редакционное расстояние по словам) / (число слов в эталонном тексте)
Эталонный текст — это корректная разметка (ground truth), а распознанный текст — результат работы OCR.
Зачем нужны обе метрики:
- CER чувствительнее к отдельным опечаткам, диакритическим знакам и пунктуации.
- WER лучше отражает читабельность и пригодность текста для поиска, анализа и других практических задач — поэтому особенно полезна при оценке итогового качества корпуса.
Для работы нам понадобятся следующие пакеты.
library(stringdist)
library(stringi)Также загрузим для сравнения три текста: эталон и два результата распознавания (один из них очень плохой и не нормализованный).
ref <- readLines("../ocr/rosalia_gt.txt") |>
str_c(collapse = "\n")
hyp1 <- readLines("../ocr/rosalia_norm.txt") |>
str_c(collapse = "\n")
hyp2 <- readLines("../ocr/rosalia_3.txt") |>
str_c(collapse = "\n") 4.2.2 CER: ред. расстояние по символам / длина эталона
Напишем фунκцию, которая будет сравнивать эталон с гипотезой.
cer <- function(ref, hyp) {
dist <- stringdist(ref, hyp, method = "lv") # Левенштейн
nref <- nchar(ref, type = "chars")
if (nref == 0) return(NA_real_)
dist / nref
}cer(ref, hyp1)[1] 0.007494647
cer(ref, hyp2)[1] 0.2109208
4.2.3 WER: ред. расстояние по словам / число слов эталона
wer <- function(ref, hyp) {
# Разбиваем на слова, удаляя пустые строки
ref_words <- unlist(strsplit(ref, "\\s+"))
hyp_words <- unlist(strsplit(hyp, "\\s+"))
ref_words <- ref_words[ref_words != ""]
hyp_words <- hyp_words[hyp_words != ""]
# Создаем матрицу для расчета расстояния Левенштейна
n_ref <- length(ref_words)
n_hyp <- length(hyp_words)
# Инициализируем матрицу
d <- matrix(0, nrow = n_ref + 1, ncol = n_hyp + 1)
d[,1] <- 0:n_ref
d[1,] <- 0:n_hyp
# Заполняем матрицу расстояний
for (i in 1:n_ref) {
for (j in 1:n_hyp) {
if (ref_words[i] == hyp_words[j]) {
d[i+1, j+1] <- d[i, j] # слова совпадают
} else {
d[i+1, j+1] <- min(
d[i, j+1] + 1, # удаление
d[i+1, j] + 1, # вставка
d[i, j] + 1 # замена
)
}
}
}
# Возвращаем WER
d[n_ref + 1, n_hyp + 1] / n_ref
}wer(ref, hyp1)[1] 0.0625
wer(ref, hyp2)[1] 0.5486111
4.3 Видео
- Видео 2025 г.