Статьи

Python / scikit-learn: определение того, какие предложения в транскрипте содержат говорящего

За последние пару месяцев я играл с транскриптами «Как я встретил вашу маму», и самая свежая вещь, над которой я работал, — это как выделить говорящего для конкретного предложения.

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

<speaker>: <sentence>

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

Подход, который я выбрал, основан на примере из книги NLTK .

Первая проблема с этим подходом заключалась в том, что у меня не было никаких помеченных данных для работы, поэтому я написал небольшое веб-приложение, которое упростило бы мне одновременную подготовку фрагментов предложений:

2015 02 20 00 44 38

Я сохранил обученные слова в файле JSON . Каждая запись выглядит так:

import json
with open("data/import/trained_sentences.json", "r") as json_file:
    json_data = json.load(json_file)
 
>>> json_data[0]
{u'words': [{u'word': u'You', u'speaker': False}, {u'word': u'ca', u'speaker': False}, {u'word': u"n't", u'speaker': False}, {u'word': u'be', u'speaker': False}, {u'word': u'friends', u'speaker': False}, {u'word': u'with', u'speaker': False}, {u'word': u'Robin', u'speaker': False}, {u'word': u'.', u'speaker': False}]}
 
>>> json_data[1]
{u'words': [{u'word': u'Robin', u'speaker': True}, {u'word': u':', u'speaker': False}, {u'word': u'Well', u'speaker': False}, {u'word': u'...', u'speaker': False}, {u'word': u'it', u'speaker': False}, {u'word': u"'s", u'speaker': False}, {u'word': u'a', u'speaker': False}, {u'word': u'bit', u'speaker': False}, {u'word': u'early', u'speaker': False}, {u'word': u'...', u'speaker': False}, {u'word': u'but', u'speaker': False}, {u'word': u'...', u'speaker': False}, {u'word': u'of', u'speaker': False}, {u'word': u'course', u'speaker': False}, {u'word': u',', u'speaker': False}, {u'word': u'I', u'speaker': False}, {u'word': u'might', u'speaker': False}, {u'word': u'consider', u'speaker': False}, {u'word': u'...', u'speaker': False}, {u'word': u'I', u'speaker': False}, {u'word': u'moved', u'speaker': False}, {u'word': u'here', u'speaker': False}, {u'word': u',', u'speaker': False}, {u'word': u'let', u'speaker': False}, {u'word': u'me', u'speaker': False}, {u'word': u'think', u'speaker': False}, {u'word': u'.', u'speaker': False}]}

Каждое слово в предложении представлено объектом JSON, который также указывает, было ли это слово говорящим в предложении.

Выбор функции

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

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

Это функция, которую я написал для преобразования слова в предложении в набор функций:

def pos_features(sentence, i):
    features = {}
    features["word"] = sentence[i]
    if i == 0:
        features["prev-word"] = "<START>"
    else:
        features["prev-word"] = sentence[i-1]
    if i == len(sentence) - 1:
        features["next-word"] = "<END>"
    else:
        features["next-word"] = sentence[i+1]
    return features

Давайте попробуем пару примеров:

import nltk
 
>>> pos_features(nltk.word_tokenize("Robin: Hi Ted, how are you?"), 0)
{'prev-word': '<START>', 'word': 'Robin', 'next-word': ':'}
 
>>> pos_features(nltk.word_tokenize("Robin: Hi Ted, how are you?"), 5)
{'prev-word': ',', 'word': 'how', 'next-word': 'are'}

Теперь давайте запустим эту функцию для нашего полного набора помеченных данных:

with open("data/import/trained_sentences.json", "r") as json_file:
    json_data = json.load(json_file)
 
tagged_sents = []
for sentence in json_data:
    tagged_sents.append([(word["word"], word["speaker"]) for word in sentence["words"]])
 
featuresets = []
for tagged_sent in tagged_sents:
    untagged_sent = nltk.tag.untag(tagged_sent)
    for i, (word, tag) in enumerate(tagged_sent):
        featuresets.append( (pos_features(untagged_sent, i), tag) )

Вот пример содержимого наборов функций :

>>> featuresets[:5]
[({'prev-word': '<START>', 'word': u'You', 'next-word': u'ca'}, False), ({'prev-word': u'You', 'word': u'ca', 'next-word': u"n't"}, False), ({'prev-word': u'ca', 'word': u"n't", 'next-word': u'be'}, False), ({'prev-word': u"n't", 'word': u'be', 'next-word': u'friends'}, False), ({'prev-word': u'be', 'word': u'friends', 'next-word': u'with'}, False)]

Пришло время обучать нашу модель, но сначала нам нужно разделить помеченные данные на обучающие и тестовые наборы, чтобы мы могли видеть, насколько хорошо наша модель работает с данными, которых она раньше не видела. Sci-Kit Learn имеет функцию, которая делает это для нас :

from sklearn.cross_validation import train_test_split
train_data,test_data = train_test_split(featuresets, test_size=0.20, train_size=0.80)
 
>>> len(train_data)
9480
 
>>> len(test_data)
2370

Теперь давайте обучим нашу модель. Я решил попробовать модели Наивного Байеса и дерева решений, чтобы увидеть, как они попали:

>>> classifier = nltk.NaiveBayesClassifier.train(train_data)
>>> print nltk.classify.accuracy(classifier, test_data)
0.977215189873
 
>>> classifier = nltk.DecisionTreeClassifier.train(train_data)
>>> print nltk.classify.accuracy(classifier, test_data)
0.997046413502

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

Если мы исследуем внутреннюю часть дерева решений, мы увидим, что это массовое переоснащение, что имеет смысл, учитывая наш небольшой набор обучающих данных и повторяемость данных:

>>> print(classifier.pseudocode(depth=2))
if next-word == u'!': return False
if next-word == u'$': return False
...
if next-word == u"'s": return False
if next-word == u"'ve": return False
if next-word == u'(':
  if word == u'!': return False
  ...
if next-word == u'*': return False
if next-word == u'*****': return False
if next-word == u',':
  if word == u"''": return False
  ...
if next-word == u'--': return False
if next-word == u'.': return False
if next-word == u'...':
  ...
  if word == u'who': return False
  if word == u'you': return False
if next-word == u'/i': return False
if next-word == u'1': return True
...
if next-word == u':':
  if prev-word == u"'s": return True
  if prev-word == u',': return False
  if prev-word == u'...': return False
  if prev-word == u'2030': return True
  if prev-word == '
 
  ': return True
  if prev-word == u'?': return False
...
if next-word == u'\u266a\u266a': return False
 

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

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

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

Полный код этого примера на GitHub , если вы хотите , чтобы играть с ним.

Любые предложения по улучшению всегда приветствуются в комментариях.