Статьи

Анализ НЛП в Python с использованием модальных глаголов

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

« Обработка естественного языка с Python » ( прочитайте мой обзор ) есть пример того, как начать этот процесс, сравнивая частоты глаголов в разных жанрах текста, используя коричневый корпус, известную коллекцию текстов, собранных в 60-х годах для языка. исследовательская работа.

Я расширил пример, включив в него дополнительный корпус судебных дел и дополнительные вспомогательные глаголы. Это включает в себя содержание ~ 15 000 юридических документов.

Сначала мы определяем функцию для поиска жанров литературы, а затем — для поиска слов из жанра. Для юридических документов я читаю из индекса, который я ранее построил из n-грамм [ссылка] (т.е. количество слов / фраз).

import nltk
import os
 
def get_genres():
yield 'legal'
for genre in brown.categories():
yield genre
 
modals = ['can', 'could', 'may', 'might', 'must', 'will', 'would', 'should']
 
def get_words(genre):
  if (genre == 'legal'):
    grams = open('1gram', 'rU')
    for line in grams:
      vals = line.split(' ')
      word = vals[0]
      count = int(vals[1])
      if (word in modals):
        for index in range(0, count):
          yield word
        else:
          yield word
    grams.close()
  else:
    for word in brown.words(categories=genre):
      yield word

Natural Language Toolkit предоставляет класс для отслеживания частоты результатов «эксперимента» — здесь мы отслеживаем использование различных времен глаголов.

cfd = nltk.ConditionalFreqDist(
  (genre, word)
  for genre in get_genres()
  for word in get_words(genre)
)
 
genres = [g for g in get_genres()]
cfd.tabulate(conditions=genres, samples=modals)
 
cfd.tabulate(conditions=genres, samples=modals)

Метод tabulate предоставляется NTLK и создает красиво отформатированный график (в командной строке он выстраивает все аккуратно)

может мог мая может быть должен буду бы должен
правовой 13059 7849 26968 1762 15974 20757 19931 13916
приключение 46 151 5 58 27 50 191 15
belles_lettres 246 213 207 113 170 236 392 102
редакционный 121 56 74 39 53 233 180 88
художественная литература 37 166 8 44 55 52 287 35
правительство 117 38 153 13 102 244 120 112
хобби 268 58 131 22 83 264 78 73
юмор 16 30 8 8 9 13 56 7
научился 365 159 324 128 202 340 319 171
уздечка 170 141 165 49 96 175 186 76
тайна 42 141 13 57 30 20 186 29
Новости 93 86 66 38 50 389 244 59
религия 82 59 78 12 54 71 68 45
отзывы 45 40 45 26 19 58 47 18
романс 74 193 11 51 45 43 244 32
научная фантастика 16 49 4 12 8 16 79 3

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

Класс распределения частот существует для подсчета вещей, и я не нашел хорошего способа нормализации строк. Я переписал функцию tabulate для этого — она ​​просто находит максимум для каждой строки, делит на это и умножает на 100.

def tabulate(cfd, conditions, samples):
  max_len = max(len(w) for w in conditions)
  sys.stdout.write(" " * (max_len + 1))
  for c in samples:
    sys.stdout.write("%-s\t" % c)
    sys.stdout.write("\n")
  for c in conditions:
    sys.stdout.write(" " * (max_len - len(c)))
    sys.stdout.write("%-s" % c)
    sys.stdout.write(" ")
    dist = cfd[c]
    norm = sum([dist[w] for w in modals])
    for s in samples:
      value = 100 * dist[s] / norm
      sys.stdout.write("%-d\t" % value)
      sys.stdout.write("\n")
 
tabulate(cfd, genres, modals)

Это облегчает сканирование вверх и вниз по графику.

может мог мая может быть должен буду бы должен
правовой 10 6 22 1 13 17 16 11
приключение 8 27 0 10 4 9 35 2
belles_lettres 14 12 12 6 10 14 23 6
редакционный 14 6 8 4 6 27 21 10
художественная литература 5 24 1 6 8 7 41 5
правительство 13 4 17 1 11 27 13 12
хобби 27 5 13 2 8 27 7 7
юмор 10 20 5 5 6 8 38 4
научился 18 7 16 6 10 16 15 8
уздечка 16 13 15 4 9 16 17 7
тайна 8 27 2 11 5 3 35 5
Новости 9 8 6 3 4 37 23 5
религия 17 12 16 2 11 15 14 9
отзывы 15 13 15 8 6 19 15 6
романс 10 27 1 7 6 6 35 4
научная фантастика 8 26 2 6 4 8 42 1

Это ясно из того, что большинство жанров имеют многочисленные ссылки на «хотел», и немногие имеют «должен».

Было бы неплохо увидеть их в масштабе 1-10 — видя, что столбцы чисел сообщают что-то длинное.

def tabulate(cfd, conditions, samples):
  max_len = max(len(w) for w in conditions)
  sys.stdout.write(" " * (max_len + 1))
  for c in samples:
    sys.stdout.write("%-s\t" % c)
    sys.stdout.write("\n")
    for c in conditions:
      sys.stdout.write(" " * (max_len - len(c)))
      sys.stdout.write("%-s" % c)
      sys.stdout.write(" ")
      dist = cfd[c]
      norm = sum([dist[w] for w in modals])
  for s in samples:
    value = 10 * float(dist[s]) / norm
    sys.stdout.write("%.1f\t" % value)
    sys.stdout.write("\n")
 
tabulate(cfd, genres, modals)
может мог мая может быть должен буду бы должен
правовой 1,1 0.7 2,2 0,1 1,3 1,7 1,7 1.2
приключение 0.8 2,8 0,1 1,1 0,5 0.9 3,5 0,3
belles_lettres 1,5 1,3 1.2 0.7 1,0 1.4 2,3 0.6
редакционный 1.4 0.7 0.9 0,5 0.6 2,8 2,1 1,0
художественная литература 0,5 2,4 0,1 0.6 0.8 0.8 4,2 0,5
правительство 1,3 0,4 1,7 0,1 1,1 2,7 1,3 1.2
хобби 2,7 0.6 1,3 0.2 0.8 2,7 0.8 0.7
юмор 1,1 2,0 0,5 0,5 0.6 0.9 3,8 0,5
научился 1,8 0.8 1,6 0.6 1,0 1,7 1,6 0.9
уздечка 1,6 1,3 1,6 0,5 0.9 1,7 1,8 0.7
тайна 0.8 2,7 0,3 1,1 0.6 0,4 3,6 0.6
Новости 0.9 0.8 0.6 0,4 0,5 3,8 2,4 0.6
религия 1,7 1,3 1,7 0,3 1.2 1,5 1.4 1,0
отзывы 1,5 1,3 1,5 0.9 0.6 1,9 1,6 0.6
романс 1,1 2,8 0.2 0.7 0.6 0.6 3,5 0,5
научная фантастика 0.9 2,6 0.2 0.6 0,4 0.9 4,2 0.2

Было бы неплохо увидеть, насколько похожи эти жанры — мы можем вычислить это, представив количество модалов как описание векторов. Угол между векторами приближается к «подобию». Хорошая особенность этой меры состоит в том, что она удаляет другие слова (слова, которые могут существовать только в одном тексте — отчасти это будет связано с тем, насколько хорошо очищены данные, что не отражается на жанре литературы).

import math
 
def distance(cfd, conditions, samples, base):
  base_cond = cfd[base]
  base_vector = [base_cond[w] for w in samples]
  base_length = math.sqrt(sum(a * a for a in base_vector))
  for c in conditions:
    cond = cfd[c]
    cond_vector = [cond[w] for w in samples]
    dotp = sum(a * b for (a,b) in zip(base_vector, cond_vector))
    cond_length = math.sqrt(sum(a * a for a in cond_vector))
    angle = math.acos(dotp / (cond_length * base_length))
    percent = (math.pi / 2 - angle) / (math.pi / 2) * 100
    print "%-s similarity to %-s: %-.1f" % (c, base, percent)

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

В качестве интересного дополнения belles_lettres означает «хорошее письмо», то есть стихи, драма, художественная литература

legal similarity to legal: 100.0
adventure similarity to legal: 41.6
belles_lettres similarity to legal: 72.4
editorial similarity to legal: 68.8
fiction similarity to legal: 42.9
government similarity to legal: 80.6
hobbies similarity to legal: 63.5
humor similarity to legal: 50.1
learned similarity to legal: 80.6
lore similarity to legal: 78.6
mystery similarity to legal: 41.3
news similarity to legal: 58.1
religion similarity to legal: 81.2
reviews similarity to legal: 73.5
romance similarity to legal: 42.9
science_fiction similarity to legal: 41.8

Некоторые жанры похожи на юридические документы — возможно, однако, что некоторые глаголы не являются независимыми. Например, вы можете увидеть «может» и «может» с одинаковым сходством. Один из способов проверить это, возможно, состоит в том, чтобы отразить то, что мы отслеживаем для расстояния (сделать вектор для каждого модального, а не жанра)

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

def distance(cfd, conditions, samples):
  base_vector = [0.0 for w in conditions]
  norm = {}
  for c_i in range(0, len(conditions)):
    cond_name = conditions[c_i]
    cond = cfd[cond_name]
    norm[cond_name] = float(sum(cond[s] for s in samples))
    for s in samples:
      base_vector[c_i] = base_vector[c_i] + float(cond[s]) / norm[cond_name]
      base_length = math.sqrt(sum(a * a for a in base_vector))
  for s in samples: # compute each vector - which, might, etc
    sample_vector = []
    for c in conditions: # find condition for each vector
      sample_vector.append(cfd[c][s] / norm[c])
      dotp = sum(a * b for (a,b) in zip(base_vector, sample_vector))
      sample_length = math.sqrt(sum(a * a for a in sample_vector))
      angle = math.acos(dotp / (sample_length * base_length))
      percent = (math.pi / 2 - angle) / (math.pi / 2) * 100
      print "%-s similarity to mean: %-.1f" % (s, percent)
 
distance(cfd, genres, modals)

Из этого я могу сделать вывод, что наименее полезным глаголом для различения жанров является «должен», а самым полезным — «может».

can similarity to mean: 76.0
could similarity to mean: 67.6
may similarity to mean: 61.5
might similarity to mean: 70.0
must similarity to mean: 79.7
will similarity to mean: 67.7
would similarity to mean: 73.6
should similarity to mean: 74.2