Пошаговое руководство по маркировке и классификации тем с использованием методов машинного обучения.
Обозначение тем для авиакомпаний - мой заключительный проект в 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.