4  Нормализация и оценка

4.1 Регулярные выражения

Есть старая шутка, ее приписывают программисту Джейми Завински: если у вас есть проблема, и вы собираетесь ее решать при помощи регулярных выражений, то у вас две проблемы. Регулярные выражения – это формальный язык, который используется для того, чтобы находить, извлекать и заменять части текста. Мы воспользуемся регулярными выражениями для того, чтобы привести в порядок распознанный текст.

Для работы нам понадобится пакет stringr из библиотеки tidyverse.

library(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] ""                                                            
На заметку

Чаще всего используемые в дореформенной русской орфографии:

  • Ять: Ѣ (U+0462), ѣ (U+0463)
  • И десятеричное: І (U+0406), і (U+0456)
  • Фита: Ѳ (U+0472), ѳ (U+0473)
  • Ижица: Ѵ (U+0474), ѵ (U+0475)
  • Твёрдый знак, еръ: Ъ (U+042A), ъ (U+044A)

4.1.1 Литералы и классы

Регулярные выражения (regex, regexp) объединяют буквальные символы (литералы) и метасимволы (символы-джокеры, англ. wildcard characters).

Для поиска используется строка-образец (англ. pattern, по-русски её часто называют “шаблоном”, “маской”), которая задает правило поиска. Строка замены также может содержать в себе специальные символы.

На заметку

Отличный путеводитель по миру регулярных выражений в R можно найти здесь.

Буквальные символы – это то, что вы ожидаете увидеть (или не увидеть – для управляющих и пробельных символов); можно сказать, что это символы, которые ничего не “имеют в виду”. Их можно объединять в классы при помощи квадратных скобок.

Для поиска совпадений используются три функции: 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("і" = "и", "ѣ" = "е"))
Задание

В пакете stringr есть небольшой датасет words. Найдите все слова с последовательностью символов wh. Сколько слов содержат два гласных после w? Найдите все слова в words, в которых за w следует согласный. Замените всю пунктуацию в строке “tomorrow?and-tomorrow_and!tomorrow” на пробелы.

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] "Моя надежда — въ небесахъ!. . ."          
Задание

Найдите все слова в words, которые заканчиваются на x. Найдите все слова, которые начинаются (или не начинаются) на b или на g.

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    
Задание

Дана строка “tomorrow (and) tomorrow (and) tomorrow”. Необходимо удалить первые скобки с их содержанием. Узнайте, все ли предложения в sentences (входит в stringr) кончаются на точку. Найдите все слова в words, в которых есть любые два символа между b и k.

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 Видео