Пошаговое руководство по маркировке и классификации тем с использованием методов машинного обучения.

Обозначение тем для авиакомпаний - мой заключительный проект в 12-недельной интенсивной программе с General Assembly. Цель проекта - изучить, как мы можем улучшить процесс работы с неструктурированными текстовыми данными с помощью разметки тем.

Оглядываясь назад, я присоединился к программе с базовыми знаниями программирования на Python из курса, который я прошел на Udemy. На самом деле кривая обучения очень крутая для начинающего, я обнаружил, что сжигаю полуночное масло на протяжении всей программы, чтобы обобщить то, что я узнал за день, и жонглировать проектами.

Поскольку обучение происходит не только в классе, я должен признать, что за эти 12 недель я получил много пользы от онлайн-ресурсов, таких как Towards Data Science. Я считаю важным делиться знаниями в сообществе специалистов по науке о данных, поэтому решил задокументировать свой проект, являющийся краеугольным камнем, и надеюсь, что он может быть полезен другим.

Без промедления, давайте сразу же погрузимся в то, как использовать скрытое распределение Дирихле в маркировке тем, и на основе этого построим прогнозную модель!

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

В качестве завершающего камня я беру набор данных Skytrax Airline Review из Kaggle и набор данных Skytrax User Reviews из Github. Оба набора данных содержат 65 948 и 41 396 отзывов соответственно. Начнем с чтения обоих наборов данных.

Очистка данных

Давайте начнем с некоторой очистки данных перед объединением двух наборов данных. Подход к очистке данных очень похож для обоих наборов данных, поэтому я буду использовать только набор данных Kaggle в качестве примера.

Во-первых, я удалю один интервал между каждым обзором и всем тегом «Проверено» в столбцах отзывов клиентов, поскольку я не буду использовать его для этого проекта. А также преобразование рекомендованного в двоичный код для классификации в более поздней части, с Да = 1 и Нет = 0.

# Drop the spacing row
airline.dropna(axis = 0, how = 'all', inplace = True)
# Remove "Trip Verified"
airline['customer_review'] = airline.loc[:,'customer_review'].map(lambda exp:exp.split('| ')[1] if "Trip Verified" in exp else exp)
# Remove "not Verified"
airline['customer_review'] = airline.loc[:,'customer_review'].map(lambda exp:exp.split('|')[1] if 'not verified' in exp else exp)

# Remove"Verified review"
airline['customer_review'] = airline.loc[:,'customer_review'].map(lambda exp:exp.split('|')[1] if 'verified review' in exp else exp)
# Convert recommended columns to binary (yes=1,No=0)
airline['recommended'] = airline['recommended'].map({'yes':1,'no':0})

Теперь, когда я очистил свои данные, я продолжу и объединю оба набора данных.

# concatenate and name it airline_final
airline_final = pd.concat([airline,airline2],axis=0,join='outer')
# Reset index
airline_final.reset_index(level=0, inplace=True,drop=True)

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

Например, поскольку существует высокая корреляция 0,87 между total_ratings и value_money_rating, следовательно, мы будем вменять, используя общие значения рейтинга. Так как total_ratings находится по шкале от 1 до 10, а value_money_rating находится по шкале от 1 до 5, я уменьшу вдвое и округлю значение.

# filter those with overall ratings
airline_final['value_money_rating'].fillna((airline_final['overall_rating']/2),inplace=True)

# Round up the ratings
airline_final['value_money_rating'] = airline_final['value_money_rating'].map(lambda x: math.ceil(x) if pd.notnull(x) else x)

Во-вторых, есть пассажиры, которые не высказывают своих рекомендаций, поэтому мы будем использовать их общие рейтинговые баллы, чтобы определить, будут ли они их рекомендовать. Учитывая, что шкала общего рейтинга составляет от 1 до 10, мы предполагаем, что если их общий рейтинг меньше 5, они не будут рекомендовать, а если их общий рейтинг 5 и выше, они будут рекомендовать.

Согласно расчету с использованием данных, 95% обзоров с общей оценкой более 5 рекомендуют «Да» и общую оценку 5, а ниже рекомендуют «Нет», поэтому с моей стороны будет разумно вменять рекомендуемые столбцы с использованием общих оценок.

# Filter those overall rating 5 and above and impute with 1 in recommended
mask = (airline_final['recommended'].isnull()) & (airline_final['overall_rating'] > 5)
airline_final.loc[mask,'recommended'] = 1
# Filter those overall rating below 5 and impute with 1 in recommended
mask2 = (airline_final['recommended'].isnull()) & (airline_final['overall_rating'] <= 5)
airline_final.loc[mask2,'recommended'] = 0

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

# Filter and impute null values using mode
airline_final['cabin_flown'] = airline_final[airline_final.airline_name.isin(cf_counts[cf_counts > 1].index)].groupby('airline_name')['cabin_flown'].apply(lambda x: x.fillna(x.mode()[0]))

Наконец, также важно проверять и удалять любые повторяющиеся обзоры после объединения наборов данных.

# Drop duplicates and reset index
airline_final.drop_duplicates(subset=['content'],inplace=True)
airline_final.reset_index(level=0, inplace=True,drop=True)

Введение в маркировку тем

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

Предварительная обработка

Подход предварительной обработки заключается в использовании стоп-слов Nltk для удаления общих вспомогательных глаголов, а также сквозных инструментов простой предварительной обработки Gensim для отбрасывания любых токенов короче минимальной длины. из 2 знаков, а также знаков препинания. Поскольку в корпусе есть неанглийские слова, я буду использовать теги Spacy pos, чтобы отсеять существительные, прилагательные, глаголы и наречия. Наконец, я должен сказать, что SpaCy - отличный инструмент, когда дело доходит до лемматизации слов, поскольку он сохраняет правильное написание основного слова.

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

# Libraries
import gensim, logging, warnings
from gensim.utils import simple_preprocess
# create a function
def convert(sentences):
    for sentence in tqdm(sentences):
        yield(gensim.utils.simple_preprocess(str(sentence), 
                                             deacc=True)) 
# deacc=True removes punctuations

# collate values from all row and place into a list
data = df.values.tolist() 

# send to function and return list
data_words = list(convert(data))
print(data_words[:2])

Функция для стоп-слов NLTK и Spacy pos-тега и лемматизатора.

# Library
import nltk
from nltk.corpus import stopwords
import spacy
stopwords = stopwords.words('english')
# Define functions for stopwords and lemmatization

def process_words(texts, stop_words=stopwords,allowed_postags=['NOUN','ADJ','VERB','ADV']):
    
    # Remove stopwords
    texts = [[word for word in simple_preprocess(str(doc)) if word not in stopwords] for doc in tqdm(texts,desc='stopwords')]

    # Lemmatize using spacy
    texts_out = []
    nlp = spacy.load("en_core_web_sm", disable=['parser', 'ner'])
    for sent in tqdm(texts, desc='lemma'):
        doc = nlp(" ".join(sent))
        texts_out.append([token.lemma_ for token in doc if token.pos_ in allowed_postags])
  
    return texts_out

Скрытое распределение Дирихле (LDA)

Скрытое распределение Дирихле (LDA) - это тип алгоритма моделирования тем, используемый для определения тем в документах.

Устанавливая num_topics, мы выбираем заранее заданное количество тем. Затем LDA случайным образом назначит документы теме, затем вычислит вероятность того, что слово присутствует в теме, и вероятность появления темы для конкретного документа. Этот процесс повторяется в зависимости от количества итераций, которые улучшают производительность LDA.

# Library
from gensim.models import CoherenceModel, TfidfModel
import gensim.corpora as corpora
from pprint import pprint
# Create Dictionary
lexicon = corpora.Dictionary(data_ready)

# TF-IDF (0-1)
tfidf = TfidfModel(dictionary=lexicon, normalize=True)

# Create Corpus: Term Document Frequency
corpus = [tfidf[lexicon.doc2bow(text)] for text in tqdm(data_ready,desc='Corpus')]

# Build LDA model
lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                            id2word=lexicon,
                                            num_topics=3,
                                            random_state=42,
                                            update_every=1,
                                            passes=10,
                                            alpha='symmetric',
                                            iterations=100,
                                            per_word_topics=True)

pprint(lda_model.print_topics())

Вывод print_topics дает темы и ключевые слова с их индивидуальным весом. Советы: красивый шрифт (pprint) структурирует вывод таким образом, чтобы его было легче читать.

[(0,
  '0.009*"pay" + 0.007*"charge" + 0.007*"bag" + 0.006*"airline" + '
  '0.005*"carry" + 0.005*"seat" + 0.005*"check" + 0.005*"extra" + 0.004*"bad" '
  '+ 0.004*"luggage"'),
 (1,
  '0.010*"delay" + 0.008*"hour" + 0.008*"cancel" + 0.007*"day" + 0.007*"tell" '
  '+ 0.006*"wait" + 0.006*"customer" + 0.006*"get" + 0.006*"airport" + '
  '0.006*"call"'),
 (2,
  '0.008*"good" + 0.007*"crew" + 0.006*"food" + 0.006*"cabin" + 0.006*"meal" + '
  '0.005*"seat" + 0.005*"great" + 0.005*"friendly" + 0.005*"comfortable" + '
  '0.005*"class"')]

Я также распечатал, используя облака слов, чтобы лучше видеть ключевые слова.

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

import pyLDAvis.gensim
pyLDAvis.enable_notebook()
vis = pyLDAvis.gensim.prepare(lda_model, corpus, dictionary=lda_model.id2word)
vis

Оценка

Поскольку присвоение ярлыков темам является неконтролируемым методом, я выбрал согласованность тем в качестве показателей оценки. Согласованность темы оценивается по отдельной теме путем измерения степени семантического сходства (от 0 до 1) между ключевыми словами в этой теме. Ниже приводится простая иллюстрация того, что означает семантическое сходство. Моя модель TF-IDF имеет более высокий балл согласованности 0,537 по сравнению с CountVecterizer 0,485.

Метка слова

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

Тема 1 была помечена как развлечения в полете, так как слова с большим весом включают в себя бортпроводник, питание, развлечения и пространство для ног. Тема 2 как выполнение рейса, так как содержит такие ключевые слова, как задержка, отмена, бронирование и билет. Что касается темы 3, она содержит такие ключевые слова, как регистрация, багаж и багаж, а также посадка, поэтому я обозначил ее как обработка пассажиров, которая зависит от опыта пассажиров при регистрации на рейс и выходе на посадку. .

Классификация

С помеченными данными я перешел к построению модели прогнозирования для прогнозирования и разделения необработанных текстов по соответствующим отделам. Я также экспериментировал с использованием скрытого семантического анализа (LSA), Word2Vec и Doc2Vec для выявления сходства между документами, словами и фразами. Удивительно, но все модели показали хорошие результаты, показав высокий показатель точности обучения и тестирования, превышающий 90%, с минимальным переобучением. Показатель ROC AUC используется для оценки качества разделимости классов.

Скрытый семантический анализ (LSA)

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

Далее я доработал использование Gridsearch, чтобы найти оптимальное количество компонентов для прогнозирования.

# instantiate
tvec = TfidfVectorizer()
lr_tf = LogisticRegression(max_iter=1000)
# Setup pipeline pipe_lsa = make_pipeline(tvec,svd,lr)
# Set the pipe params
pipe_lsa_params = {
    'truncatedsvd__n_components': [500,1000],
    'logisticregression__C': [1.0,0.9]   
}
# Instantiate gridsearchCV
gs_lsa = GridSearchCV(pipe_lsa,
                     param_grid=pipe_lsa_params,
                     cv=5,
                     verbose=1)

Word2Vec

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

Чтобы найти контекст в обзорах, я использовал предварительно обученную модель Word2Vec из Векторы новостей Google для обучения своих наборов данных. Это 300-мерный вектор, обученный 3 миллионам слов и фраз. Я использую среднее представление Word2Vec, где я умножаю вектор пакета слов на матрицу вложения слов и делю на общее количество слов в документе, чтобы получить среднее представление Word2Vec. Выполняя усреднение, я также могу уменьшить размерность векторов. Коды можно найти здесь!

# Library
from gensim.models import Word2Vec
# Load the pretrained Google word2vec
wv = gensim.models.KeyedVectors.load_word2vec_format("GoogleNews-vectors-negative300.bin.gz",binary=True)
wv.init_sims(replace=True)

Doc2Vec

Подобно Word2Vec, вместо того, чтобы находить сходства в контексте между словами, Doc2Vec находит между фразами. Я решил обучить свою собственную модель Doc2Vec с нуля, построив свой собственный список слов, используя распределенный пакет слов (DBOW). DBOW в некоторых отношениях сравним со скип-граммой Word2Vec. Коды можно найти здесь!

# Library
import gensim
from gensim.models import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
# Instantiate and building a vocab
model_dbow = Doc2Vec(dm=0, vector_size=300, negative=5, hs=0, min_count=2, sample = 0)
model_dbow.build_vocab([x for x in tqdm(train_tagged.values)])
Train Doc2Vec
for epoch in range(10):
    model_dbow.train(utils.shuffle([x for x in tqdm(train_tagged.values)]), total_examples=len(train_tagged.values), epochs=1)
    model_dbow.alpha -= 0.002
    model_dbow.min_alpha = model_dbow.alpha

Вывод

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

Напутствие

Это все, чем я могу поделиться для своего проекта. Я надеюсь, что эта статья предназначена для тех, кто интересуется разметкой тем или только начинает заниматься наукой о данных. Обращайтесь ко мне, если у вас возникнут какие-либо вопросы или конструктивный отзыв! Наконец, не стесняйтесь подключаться к LinkedIn.