6  Лемматизация и тематическое моделирование

Основные этапы NLP включают в себя токенизацию, морфологический и синтаксический анализ, а также анализ семантики и прагматики. В этом уроке речь пойдет про первые три этапа. Мы научимся разбивать текст на токены (слова), определять морфологические характеристики слов и находить их начальные формы (леммы), а также анализировать структуру предложения с использованием синтаксических парсеров.

library(tidyverse)
library(tidytext)
library(udpipe)

6.1 Данные

За основу для всех эти вычислений мы возьмем три философских трактата, написанных на английском языке. Это хронологически и тематически близкие тексты:

  • “Опыт о человеческом разумении” Джона Локка (1690), первые две книги;
  • “Трактат о принципах человеческого знания” Джорджа Беркли (1710);
  • “Исследование о человеческом разумении” Дэвида Юма (1748).

Данные были извлечены из открытой библиотеки Gutengerg при помощи пакета {gutenbergr}и приведены к опрятному виду. Скачать подготовленный таким образом корпус можно здесь.

load("../data/emp_corpus.Rdata")

Делим корпус на слова уже известным вам способом.

corpus_words <- emp_corpus |> 
  unnest_tokens(word, text)

corpus_words

Большая часть слов, которые мы сейчас видим в корпусе – это шумовые слова, или стоп-слова, не несущие смысловой нагрузки.

author_word_counts <- corpus_words  |> 
  count(author, word, sort = T) 

author_word_counts |> 
  slice_head(n = 15) |> 
  ggplot(aes(reorder(word, n), n, fill = word)) +
  geom_col(show.legend = F) + 
  facet_wrap(~author, scales = "free") +
  coord_flip() +
  theme_light()

Обратите внимание: абсолютная частотность – плохой показатель для текстов разной длины. Чтобы тексты было проще сравнивать, следует разделить показатели частотности на общее число токенов в тексте.

6.2 Наиболее характерные слова

Наиболее частотные слова наименее подвержены влиянию тематики, поэтому их используют для стилометрического анализа. Если отобрать наиболее частотные после удаления стоп-слов, то мы получим достаточно адекватное отражение тематики документов.

Если же мы необходимо найти наиболее характерные для документов токены, то применяется другая мера, которая называется tf-idf (term frequency - inverse document frequency).

Логарифм единицы равен нулю, поэтому если слово встречается во всех документах, его tf-idf равно нулю. Чем выше tf-idf, тем более характерно некое слово для документа. При этом относительная частотность тоже учитывается! Например, Беркли один раз упоминает “сахарные бобы”, а Локк – “миндаль”, но из-за редкой частотности tf-idf для подобных слов будет низкой.

Функция bind_tf_idf() принимает на входе тиббл с абсолютной частотностью для каждого слова.

author_word_tfidf <- author_word_counts |> 
  bind_tf_idf(word, author, n)

author_word_tfidf

Вот так выглядят наиболее характерные слова для каждого автора

author_word_tfidf |> 
  group_by(author) |>
  arrange(-tf_idf) |> 
  top_n(15) |> 
  ungroup() |> 
  ggplot(aes(reorder_within(word, tf_idf, author), tf_idf, fill = author)) +
  geom_col(show.legend = F) +
  labs(x = NULL, y = "tf-idf") +
  facet_wrap(~author, scales = "free") +
  scale_x_reordered() +
  coord_flip()

На такой диаграмме авторы совсем не похожи друг на друга, но будьте осторожны: все то, что их сближает (а это не только служебные части речи!), сюда просто не попало. Можно также заметить, что ряд характерных слов связаны не столько с тематикой, сколько со стилем: чтобы этого избежать, можно использовать лемматизацию или задать правило для замены вручную.

6.3 Лемматизация и POS-тэггинг

Лемматизация – приведение слов к начальной форме (лемме). Как правило, она проводится одновременно с частеречной разметкой (POS-tagging). Все это умеет делать UDPipe – обучаемый конвейер (trainable pipeline), для которого существует одноименный пакет в R.

Основным форматом файла для него является CoNLL-U. Файлы в таком формате хранятся в так называемых трибанках, то есть коллекциях уже размеченных текстов (название объясняется тем, что синтаксическая структура предложений представлена в них в виде древовидных графов). Файлы CoNLL-U используются для обучения нейросетей, но для большинства языков доступны хорошие предобученные модели, работать с которыми достаточно просто.

Пакет udpipe позволяет работать со множеством языков (всего 65), для многих из которых представлено несколько моделей, обученных на разных трибанках. Прежде всего нужно выбрать и загрузить модель (список). Описания моделей доступны на сайте https://universaldependencies.org/.

# скачиваем модель в рабочую директорию
#udpipe_download_model(language = "english-gum")

# загружаем модель
english_gum <- udpipe_load_model(file = "english-gum-ud-2.5-191206.udpipe")

# аннотируем (это займет несколько минут)
emp_ann <- udpipe_annotate(english_gum, emp_corpus$text, doc_id = emp_corpus$author)

Результат возвращается в формате CoNLL-U; это широко применяемый формат представления результат морфологического и синтаксического анализа текстов.

Вот пример разбора предложения:

Cтроки слов содержат следующие поля:

  1. ID: индекс слова, целое число, начиная с 1 для каждого нового предложения; может быть диапазоном токенов с несколькими словами.
  2. FORM: словоформа или знак препинания.
  3. LEMMA: Лемма или основа словоформы.
  4. UPOSTAG: тег части речи из универсального набора проекта UD, который создавался для того, чтобы аннотации разных языков были сравнимы между собой.
  5. XPOSTAG: тег части речи, который выбрали исследователи под конкретные нужды языка
  6. FEATS: список морфологических характеристик.
  7. HEAD: идентификатор (номер) синтаксической вершины текущего токена. Если такой вершины нет, то ставят ноль (0).
  8. DEPREL: характер синтаксической зависимости.
  9. DEPS: Список вторичных зависимостей.
  10. MISC: любая другая аннотация.

Для работы данные удобнее трансформировать в прямоугольный формат.

emp_pos <- as_tibble(emp_ann) |> 
  select(-paragraph_id)

emp_pos 

6.3.1 Поля UPOS и XPOS

Морфологическая аннотация, которую мы получили, дает возможность выбирать и группировать различные части речи. Например, существительные.

emp_pos |> 
  filter(upos == "NOUN") |> 
  select(doc_id, token, lemma, upos, xpos)

Посчитать части речи можно так:

upos_counts <- emp_pos |>
  count(doc_id, upos, sort = TRUE) 

upos_counts

Относительные значения:

total_counts <- upos_counts |> 
 count(doc_id, wt = n, name = "sum_n")

pos_share <- upos_counts |> 
  left_join(total_counts) |> 
  mutate(share = round( (n/sum_n), 2))

pos_share |> 
  ggplot(aes(reorder(upos, share), share, fill = upos)) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  theme_light() +
  facet_wrap(~doc_id, scales = "free")

Отберем наиболее частотные имена существительные и имена собственные.

nouns <- emp_pos  |> 
  filter(upos %in% c("NOUN", "PROPN")) |> 
  count(doc_id, lemma, sort = TRUE) 

nouns
nouns |> 
  group_by(doc_id) |> 
  slice_head(n = 10) |> 
  ggplot(aes(reorder_within(lemma, n, doc_id), n, fill = lemma)) +
  geom_col(show.legend = FALSE) +
  theme_light() +
  coord_flip() +
  scale_x_reordered() +
  facet_wrap(~ doc_id, scales = "free") +
  xlab(NULL)

Сравните с результатом, который вы получили, считая TF-IDF.

В отличие от UPOS (Universal Part-Of-Speech), XPOS (Language-specific Part-Of-Speech) – это теги частей речи, используемые в национальных корпусах. Форматы тегов и их детализация могут значительно меняться от языка к языку.

6.3.2 Поля FEATS и DEP_REL

Допустим, нам нужны лишь определенные формы: например, прилагательные в превосходной степени.

superlatives <- emp_pos  |> 
  filter(str_detect(feats, "Degree=Sup") & upos == "ADJ") |> 
  select(doc_id, token)

superlatives_counts <- superlatives |> 
  count(doc_id, token, sort = TRUE) |> 
  group_by(doc_id) |> 
  slice_head(n = 15)
superlatives_counts |> 
  ggplot(aes(reorder_within(token, n, doc_id), n, fill = doc_id)) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  theme_light() +
  facet_wrap(~ doc_id, scales = "free") +
  scale_x_reordered() +
  xlab(NULL)

Аналогичным образом можно отбирать синтаксические признаки (DEP_REL) и их комбинации, а также визуализировать деревья зависимостей для отдельных предложений при помощи пакета {textplot}.

6.4 Совместная встречаемость слов

Функция cooccurence() из пакета udpipe позволяет выяснить, сколько раз некий термин встречается совместно с другим термином, например:

  • слова встречаются в одном и том же документе/предложении/параграфе;
  • слова следуют за другим словом;
  • слова находятся по соседству с другим словом на расстоянии n слов.

Выясним, какие существительные чаще встречаются в одном предложении у Локка:

locke_nouns <-  emp_pos |> 
  filter(doc_id == "Locke") |> 
  filter(upos == "NOUN")

locke_nouns
cooc <- cooccurrence(locke_nouns, term = "lemma", 
                     group = "sentence_id") |>
  as_tibble() |> 
  filter(cooc > 70) |> 
  filter(term1 != "idea", term2 != "idea")

cooc

Этот результат легко визуализировать, используя пакет ggraph:

library(igraph)
library(ggraph)

wordnetwork <- graph_from_data_frame(cooc)
ggraph(wordnetwork, layout = "fr") +
  geom_edge_arc(aes(width = cooc), alpha = 0.8, edge_colour = "grey90", show.legend=FALSE) +
  geom_node_label(aes(label = name), col = "#1f78b4", size = 4) +
  theme_void() +
  labs(title = "Совместная встречаемость существительных в предложении")
Warning in geom_node_label(aes(label = name), col = "#1f78b4", size = 4):
Ignoring unknown parameters: `label.size`
Warning: The `trans` argument of `continuous_scale()` is deprecated as of ggplot2 3.5.0.
ℹ Please use the `transform` argument instead.

Чтобы узнать, какие слова чаще стоят рядом, используем ту же функцию, но с другими аргументами:

cooc2 <- cooccurrence(locke_nouns$lemma, 
                      relevant = locke_nouns$upos %in% "NOUN", 
                      skipgram = 1) |> 
  as_tibble() |> 
  filter(cooc > 10)

cooc2
wordnetwork <- graph_from_data_frame(cooc2)

ggraph(wordnetwork, layout = "fr") +
  geom_edge_link(aes(width = cooc), edge_colour = "grey90", edge_alpha=0.8, show.legend = F) +
  geom_node_label(aes(label = name), col = "#1f78b4") +
  labs(title = "Слова, стоящие рядом в тексте") +
  theme_void()

6.5 Тематическое моделирование

Вы можете легко преобразовать аннотированный тибл в матрицу “документ-термин” (document-term-matrix), которая используется во многих других пакетах для анализа текста. В данном случае мы будем выделять темы (topics) на уровне предложений.

Преимущество {udpipe} по сравнению с другими пакетами заключается в следующем:

  • тематическое моделирование (topic modelling) можно проводить непосредственно по нужным словам, так как их легко определить с помощью тегов частей речи: чаще всего интересуют только существительные и глаголы или, например, только прилагательные, если вы хотите кластеризовать текст по тональности (sentiment clustering);
  • больше не нужно создавать длинные списки стоп-слов;
  • кроме того, можно работать с леммами, а не с исходными формами слов, что позволяет схожим словам лучше группироваться друг с другом;
  • вы легко можете включать составные ключевые слова.

Отбираем только существительные.

emp_pos$topic_level_id <- unique_identifier(emp_pos, fields = c("doc_id", "sentence_id"))

dtf <- emp_pos |> 
  filter(upos == "NOUN")
dtf <- document_term_frequencies(dtf, document = "topic_level_id", term = "lemma")
head(dtf)
dtm <- document_term_matrix(x = dtf)
dtm_clean <- dtm_remove_lowfreq(dtm, minfreq = 5)
sample(dtm_colsums(dtm_clean), 10)
 generality acknowledge     farther      patron        note      praise 
         14           8           8           7          33           6 
    probity        cure     uniform   spectator 
          5           9           9           5 
library(topicmodels)
m <- LDA(dtm_clean, k = 4, method = "Gibbs", 
         control = list(nstart = 5, burnin = 2000, best = TRUE, seed = 1:5))

Чтобы интерпретировать тематическую модель и подобрать подходящие названия темам, вы можете получить наиболее часто встречающиеся термины для каждой темы, указав минимальную вероятность появления термина и минимальное количество отображаемых терминов.

topicterminology <- predict(m, type = "terms", 
                            min_posterior = 0.005, 
                            min_terms = 3)
topicterminology
$topic_001
            term        prob
1           body 0.063299870
2           part 0.055656535
3         motion 0.042793360
4        thought 0.032819738
5           time 0.026481362
6      existence 0.022380060
7         number 0.021727580
8          space 0.020981889
9         matter 0.020049775
10         place 0.019490506
11      duration 0.019210872
12       nothing 0.019024449
13     extension 0.017719490
14         world 0.013338553
15      anything 0.012965708
16      distance 0.012313228
17        figure 0.011940382
18        person 0.011940382
19     something 0.010635422
20           end 0.010355788
21 consciousness 0.009144040
22         light 0.009050828
23    appearance 0.008864405
24          side 0.008025503
25       measure 0.007745868
26          rest 0.007745868
27            be 0.007652657
28          hand 0.007652657
29          year 0.007466234
30    difficulty 0.007186600
31           sun 0.007186600
32           eye 0.007000177
33        length 0.007000177
34    succession 0.006813754
35         think 0.006720543
36      infinity 0.006254486
37        moment 0.005974852
38      particle 0.005695217
39        animal 0.005602006
40      identity 0.005602006
41   supposition 0.005602006
42        change 0.005508794
43          line 0.005508794
44          mark 0.005508794
45         sight 0.005135949
46      solidity 0.005042737

$topic_002
         term        prob
1        idea 0.245654014
2        mind 0.109205398
3       thing 0.069582283
4        name 0.034144708
5       sense 0.030796276
6   substance 0.030145192
7        word 0.027261820
8     quality 0.018518691
9    relation 0.017960619
10  sensation 0.016100379
11     spirit 0.013775078
12 perception 0.013217006
13     colour 0.013123994
14       sort 0.011914838
15     memory 0.010519658
16   distinct 0.010426646
17 reflection 0.010240622
18       mode 0.007915322
19        one 0.007915322
20      sound 0.007729298
21    essence 0.006520142
22     notice 0.006520142
23   occasion 0.006055082
24   language 0.005869058

$topic_003
            term        prob
1          power 0.050329463
2         action 0.040791100
3         object 0.037384542
4         nature 0.035632598
5         effect 0.025996905
6          cause 0.025802244
7  understanding 0.018891798
8      operation 0.018599807
9     experience 0.018113156
10          pain 0.017918496
11          life 0.015874561
12          will 0.015095919
13        manner 0.013149314
14      pleasure 0.012857324
15      instance 0.012759993
16         event 0.012176012
17        degree 0.011786691
18       nothing 0.011494700
19          kind 0.011397370
20     happiness 0.010910719
21   philosopher 0.010618728
22          case 0.010229407
23       subject 0.010132077
24          view 0.010034747
25           law 0.009742756
26     connexion 0.009353435
27          good 0.009256105
28 consideration 0.008964114
29       liberty 0.008964114
30        desire 0.008769454
31         force 0.008380133
32  circumstance 0.008185472
33    uneasiness 0.007990812
34       passion 0.007017510
35        regard 0.006530859
36        course 0.006433528
37     necessity 0.006433528
38   observation 0.006433528
39         state 0.006433528
40       respect 0.006044207
41     inference 0.005557556
42      occasion 0.005460226
43     attention 0.005265566

$topic_004
          term        prob
1          man 0.112073717
2       reason 0.037138741
3    principle 0.032014155
4    knowledge 0.025342525
5        other 0.022635197
6          use 0.022345126
7        truth 0.022055055
8          way 0.021184843
9       notion 0.018960966
10 proposition 0.014029761
11    argument 0.013062858
12     opinion 0.012482717
13        soul 0.012386026
14        rule 0.011419123
15    question 0.009968769
16  philosophy 0.009872079
17       child 0.009775388
18        fact 0.009775388
19        term 0.009775388
20     mankind 0.009582008
21     faculty 0.009291937
22   reasoning 0.008711795
23  impression 0.008228344
24     miracle 0.008131654
25       order 0.007454821
26    evidence 0.007068060
27     variety 0.006971370
28 consequence 0.006874680
29         age 0.006777989
30         viz 0.006681299
31      answer 0.006584609
32  conclusion 0.006391228
33       doubt 0.006391228
34       proof 0.006197848
35       maxim 0.006004467
36   testimony 0.006004467
37     purpose 0.005907777
38  difference 0.005811087
39    doctrine 0.005811087
40   character 0.005714396
41  foundation 0.005521016
42   sentiment 0.005230945
43   authority 0.005134254
44   discourse 0.005134254
45    judgment 0.005134254
46      people 0.005134254

Кроме того, мы также можем легко использовать модель для предсказания, к какой теме принадлежит новый документ. Функция predict корректно возвращает значение NA для тех документов, в которых отсутствуют слова, использованные в модели, что обычно вызывает затруднения в других пакетах R. Также функция позволяет присваивать каждой теме метки и показывает разницу в вероятности между наиболее вероятной темой и следующей по вероятности, что полезно для оценки чёткости (разграниченности) ваших тем.

scores <- predict(m, newdata = dtm_clean, type = "topics", 
                  labels = c("physics", "psychology", "ethics", "epistemology"))

Аргумент labels присваивает пользовательские имена темам в итоговом результате предсказания.

Визуализируем топики.

emp_topics <- merge(emp_pos, scores, by.x="topic_level_id", by.y="doc_id")
wordnetwork <- subset(emp_topics, topic %in% 1 & lemma %in% topicterminology[[1]]$term)
wordnetwork <- cooccurrence(wordnetwork, group = c("topic_level_id"), term = "lemma")
wordnetwork <- graph_from_data_frame(wordnetwork)
ggraph(wordnetwork, layout = "fr") +
  geom_edge_link(aes(width = cooc, edge_alpha = cooc), edge_colour = "pink")  +
  geom_node_text(aes(label = name), col = "darkgreen", size = 4) +
  theme_graph(base_family = "Arial Narrow") +
  labs(title = "Words in topic Physics ", subtitle = "Nouns Cooccurrence")

6.6 Видео

6.7 Домашнее задание

По ссылке вы найдете датасет со сказками Салтыкова-Щедрина (в формате .Rdata). Вам необходимо аннотировать сказки, используя модель SynTagRus.

После этого ответьте на вопросы по ссылке. Задание считается выполненным, если верные ответы даны на 3 из 5 вопросов.

Ошибки лемматизации и морфологического анализа игнорируйте.

Дедлайн: 28 ноября, 21-00.