Введение

Я был заинтригован, прочитав на прошлой неделе эту удивительную статью о построении модели классификации изображений с несколькими ярлыками. Специалист по обработке данных во мне начал изучать возможности преобразования этой идеи в проблему Обработки естественного языка (НЛП).

В этой статье демонстрируются методы компьютерного зрения для предсказания жанра фильма. Поэтому мне пришлось найти способ преобразовать эту формулировку проблемы в текстовые данные. Сейчас в большинстве руководств по НЛП рассматривается решение задач классификации по одной метке (когда для каждого наблюдения используется только одна метка).

Но фильмы не одномерные. Один фильм может охватывать несколько жанров. Вот ЭТО вызов, который я люблю принимать как специалист по данным. Я извлек кучу кратких описаний сюжетов фильмов и приступил к работе, используя эту концепцию многокомпонентной классификации. И результаты даже на простой модели действительно впечатляют.

В этой статье мы рассмотрим практический подход к пониманию многокомпонентной классификации в НЛП. Мне было очень интересно строить модель прогнозирования жанров кино с помощью НЛП, и я уверен, что вы тоже это сделаете. Давайте копаться!

Оглавление

  1. Краткое введение в многокомпонентную классификацию
  2. Настройка нашей постановки задачи классификации по нескольким меткам
  3. О наборе данных
  4. Наша стратегия построения модели прогнозирования жанров кино
  5. Реализация: использование классификации по нескольким меткам для построения модели прогнозирования жанра фильма (на Python)

Краткое введение в многокомпонентную классификацию

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

Основная концепция очевидна в названии - многокомпонентная классификация. Здесь у экземпляра / записи может быть несколько меток, и количество меток на экземпляр не фиксировано.

Позвольте мне объяснить это на простом примере. Взгляните на приведенные ниже таблицы, где «X» представляет входные переменные, а «y» представляет целевые переменные (которые мы прогнозируем):

  • «Y» - двоичная целевая переменная в таблице 1. Следовательно, есть только две метки - t1 и t2.
  • «Y» содержит более двух ярлыков в таблице 2. Но обратите внимание, что в обеих этих таблицах только один ярлык для каждого ввода
  • Вы, должно быть, догадались, чем выделяется таблица 3. У нас есть несколько тегов, не только для всей таблицы, но и для отдельных входов.

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

Вы можете получить более подробное представление о проблемах классификации с несколькими метками в следующей статье:

Настройка нашей постановки задачи классификации по нескольким меткам

Есть несколько способов построения системы рекомендаций. Когда дело доходит до жанров фильмов, вы можете нарезать данные на части на основе нескольких переменных. Но вот простой подход - построить модель, которая может автоматически предсказывать жанровые теги! Я уже могу представить себе возможности добавления такой опции в рекомендатель. Беспроигрышный вариант для всех.

Наша задача - построить модель, которая может предсказать жанр фильма, используя только детали сюжета (доступные в текстовой форме).

Взгляните на приведенный ниже снимок с IMDb и выберите разные элементы на дисплее:

В таком крошечном пространстве МНОГО информации:

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

Жанры говорят нам, чего ожидать от фильма. И поскольку эти жанры кликабельны (по крайней мере, на IMDb), они позволяют нам открывать другие похожие фильмы того же типа. То, что казалось простой функцией продукта, внезапно обрело так много многообещающих вариантов. 🙂

О наборе данных

Мы будем использовать открытый набор данных CMU Movie Summary Corpus для нашего проекта. Вы можете скачать набор данных прямо по этой ссылке.

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

  • movie.metadata.tsv: метаданные для 81741 фильма, извлеченные из дампа Freebase от 4 ноября 2012 г. Теги жанра фильма доступны в этом файле
  • plot_summaries.txt: Сюжетные сводки 42 306 фильмов, извлеченных из дамп англоязычной Википедии от 2 ноября 2012 года. Каждая строка содержит идентификатор фильма из Википедии (который индексируется в movie.metadata.tsv), за которым следует краткое изложение сюжета.

Наша стратегия построения модели прогнозирования жанров кино

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

Здесь X и y - это функции и метки, соответственно - это набор данных с несколькими метками. Теперь мы воспользуемся подходом двоичной релевантности для преобразования нашей целевой переменной y. Сначала мы извлечем уникальные метки из нашего набора данных:

Уникальные ярлыки = [t1, t2, t3, t4, t5]

В данных 5 уникальных тегов. Затем нам нужно заменить текущую целевую переменную несколькими целевыми переменными, каждая из которых принадлежит уникальным меткам набора данных. Поскольку имеется 5 уникальных меток, будет 5 новых целевых переменных со значениями 0 и 1, как показано ниже:

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

Реализация: использование классификации по нескольким меткам для построения модели прогнозирования жанра фильма (на Python)

Мы поняли постановку проблемы и разработали логическую стратегию для разработки нашей модели. Соберем все вместе и начнем кодить!

Импортируйте необходимые библиотеки

Начнем с импорта необходимых для нашего проекта библиотек:

Загрузить данные

Давайте сначала загрузим файл метаданных фильма. Используйте "\ t" в качестве разделителя, так как это файл, разделенный табуляцией (.tsv):

meta = pd.read_csv("movie.metadata.tsv", sep = '\t', header = None) meta.head()

Ой, подождите - в этом наборе данных нет заголовков. Первый столбец - это уникальный идентификатор фильма, третий столбец - название фильма, а последний столбец содержит жанры фильма . Мы не будем использовать остальные столбцы в этом анализе.

Давайте добавим имена столбцов к трем вышеупомянутым переменным:

# rename columns 
meta.columns = ["movie_id",1,"movie_name",3,4,5,6,7,"genre"]

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

Затем разделите идентификаторы фильмов и сюжеты на два отдельных списка. Мы будем использовать эти списки для формирования фрейма данных:

Давайте посмотрим, что у нас есть во фрейме данных "фильмы":

movies.head()

Идеально! У нас есть как идентификатор фильма, так и соответствующий сюжет фильма.

Исследование и предварительная обработка данных

Давайте добавим названия фильмов и их жанры из файла метаданных фильма, объединив последние с первым на основе столбца movie_id:

Здорово! Мы добавили названия фильмов и жанры. Однако жанры даны в словарной записи. С ними будет проще работать, если мы сможем преобразовать их в список Python. Сделаем это с помощью первой строки:

movies['genre'][0]

Вывод:

'{"/m/07s9rl0": "Drama", "/m/03q4nz": "World cinema"}'

Мы не можем получить доступ к жанрам в этой строке, используя только .values ​​(). Вы можете догадаться, почему? Это потому, что этот текст представляет собой строку, а не словарь. Придется преобразовать эту строку в словарь. Здесь мы воспользуемся помощью библиотеки json:

type(json.loads(movies['genre'][0]))

Вывод:

dict

Теперь мы можем легко получить доступ к жанрам этой строки:

json.loads(movies['genre'][0]).values()

Вывод:

dict_values(['Drama', 'World cinema'])

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

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

# remove samples with 0 genre tags 
movies_new = movies[~(movies['genre_new'].str.len() == 0)]
movies_new.shape, movies.shape

Вывод:

((41793, 5), (42204, 5))

Только 411 сэмплов не имели жанровых тегов. Давайте еще раз взглянем на фрейм данных:

movies.head()

Обратите внимание, что жанры теперь представлены в формате списка. Вам интересно узнать, сколько жанров фильмов охвачено этим набором данных? Приведенный ниже код отвечает на этот вопрос:

# get all genre tags in a list 
all_genres = sum(genres,[]) 
len(set(all_genres))

Вывод:

363

В нашем наборе данных более 363 уникальных тегов жанров. Это довольно большая цифра. Я могу вспомнить 5–6 жанров! Давайте узнаем, что это за теги. Мы будем использовать FreqDist () из библиотеки nltk для создания словаря жанров и количества их встречаемости в наборе данных:

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

Далее мы немного очистим наши данные. Я буду использовать несколько очень простых шагов по очистке текста (поскольку это не является основной темой данной статьи):

Давайте применим эту функцию к сюжетам фильма, используя дуэт apply-lambda:

movies_new['clean_plot'] = movies_new['plot'].apply(lambda x:  
                                                    clean_text(x))

Не стесняйтесь сравнивать новые и старые сюжеты фильмов. Ниже я привел несколько случайных примеров:

В столбце clean_plot весь текст в нижнем регистре, а также без знаков препинания. Наша очистка текста работает отлично.

Функция ниже визуализирует слова и их частоту в наборе документов. Давайте воспользуемся им, чтобы найти наиболее частые слова в столбце сюжетов фильмов:

Большинство терминов в приведенном выше сюжете - это игнорируемые слова. Эти стоп-слова несут гораздо меньше смысла, чем другие ключевые слова в тексте (они просто добавляют шум к данным). Я собираюсь удалить их из текста сюжетов. Вы можете скачать список игнорируемых слов из библиотеки nltk:

nltk.download('stopwords')

Давайте удалим игнорируемые слова:

Отметьте наиболее часто употребляемые термины без игнорируемых слов:

freq_words(movies_new['clean_plot'], 100)

Выглядит намного лучше, не так ли? Появились гораздо более интересные и значимые слова, такие как «полиция», «семья», «деньги», «город» и т. Д.

Я упоминал ранее, что мы будем рассматривать эту проблему классификации с несколькими метками как проблему двоичной релевантности. Следовательно, теперь мы будем горячо кодировать целевую переменную, то есть genre_new, используя MultiLabelBinarizer () в sklearn. Поскольку существует 363 уникальных тега жанра, будет 363 новых целевых переменных.

Теперь пора сосредоточиться на извлечении функций из очищенной версии данных сюжетов фильма. В этой статье я буду использовать возможности TF-IDF. Не стесняйтесь использовать любой другой метод извлечения функций, который вам удобен, например Bag-of-Words, word2vec, GloVe или ELMo.

Я рекомендую прочитать следующие статьи, чтобы узнать больше о различных способах создания функций из текста:

tfidf_vectorizer = TfidfVectorizer(max_df=0.8, max_features=10000)

В качестве характеристик я использовал 10 000 наиболее часто встречающихся слов в данных. Вы также можете попробовать любое другое число для параметра max_features.

Теперь, прежде чем создавать функции TF-IDF, мы разделим наши данные на наборы для обучения и проверки для обучения и оценки производительности нашей модели. Я собираюсь разделить 80–20 - 80% выборок данных в наборе поездов, а остальные - в наборе проверки:

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

# create TF-IDF features 
xtrain_tfidf = tfidf_vectorizer.fit_transform(xtrain) 
xval_tfidf = tfidf_vectorizer.transform(xval)

Постройте модель прогнозирования жанра фильма

Мы все настроены на создание модели! Это то, чего мы ждали.

Помните, что нам нужно будет построить модель для каждой целевой переменной с горячим кодированием. Поскольку у нас есть 363 целевых переменных, нам нужно будет подогнать 363 различных модели с одним и тем же набором предикторов (функции TF-IDF).

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

from sklearn.linear_model import LogisticRegression 
# Binary Relevance 
from sklearn.multiclass import OneVsRestClassifier 
# Performance metric 
from sklearn.metrics import f1_score

Мы воспользуемся классом OneVsRestClassifier от sk-learn, чтобы решить эту проблему как двоичную релевантность или задачу «один против всех»:

lr = LogisticRegression() 
clf = OneVsRestClassifier(lr)

Наконец, поместите модель на поезд:

# fit model on train data 
clf.fit(xtrain_tfidf, ytrain)

Прогнозируйте жанры фильмов по проверочному набору:

# make predictions for validation set 
y_pred = clf.predict(xval_tfidf)

Давайте посмотрим на пример этих прогнозов:

y_pred[3]

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

К счастью, sk-learn снова приходит нам на помощь. Мы будем использовать функцию inverse_transform () вместе с объектом MultiLabelBinarizer () для преобразования прогнозируемых массивов в теги жанра фильма:

multilabel_binarizer.inverse_transform(y_pred)[3]

Вывод:

('Action', 'Drama')

Вау! Это было гладко.

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

# evaluate performance 
f1_score(yval, y_pred, average="micro")

Вывод:

0.31539641943734015

Мы получаем приличный результат в F1 - 0,315. Эти прогнозы были сделаны на основе порогового значения 0,5, что означает, что вероятности больше или равные 0,5 были преобразованы в единицы, а остальные - в 0.

Давайте попробуем изменить это пороговое значение и посмотрим, улучшит ли это оценку нашей модели:

# predict probabilities 
y_pred_prob = clf.predict_proba(xval_tfidf)

Теперь установите пороговое значение:

t = 0.3 # threshold value 
y_pred_new = (y_pred_prob >= t).astype(int)

Я пробовал 0,3 в качестве порогового значения. Вам следует попробовать и другие значения. Давайте еще раз проверим счет F1 на основе этих новых прогнозов.

# evaluate performance 
f1_score(yval, y_pred_new, average="micro")

Вывод:

0.4378456703198025

Это довольно большой прирост производительности нашей модели. Лучшим подходом к поиску правильного порогового значения было бы использование k-кратной перекрестной проверки и пробовать разные значения.

Создать функцию вывода

Подождите - проблема еще не решена. Мы также должны позаботиться о новых данных или новых сюжетах фильмов, которые появятся в будущем, верно? Наша система прогнозирования жанров фильмов должна иметь возможность принимать в качестве входных данных сюжет фильма в необработанном виде и генерировать тег (теги) жанра.

Для этого давайте создадим функцию вывода. Потребуется текст сюжета фильма и выполните следующие действия:

  • Очистить текст
  • Удалить стоп-слова из очищенного текста
  • Извлечь особенности из текста
  • Делать предсказания
  • Вернуть теги прогнозируемого жанра фильма

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

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

Куда пойти отсюда?

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

Ссылки на курс приведены ниже для справки:

Конечные заметки

Мне бы хотелось увидеть разные подходы и методы нашего сообщества для достижения лучших результатов. Попробуйте использовать разные методы извлечения признаков, построить разные модели, настроить эти модели и т. Д. Есть так много вещей, которые вы можете попробовать. Не останавливайтесь здесь - продолжайте экспериментировать!

Не стесняйтесь обсуждать и комментировать в разделе комментариев ниже. Полный код доступен здесь.

Вы также можете прочитать эту статью о приложении Google Analytics Vidhya для Android.

Статьи по Теме

Первоначально опубликовано на https://www.analyticsvidhya.com 22 апреля 2019 г.