Автоматическое извлечение ключевых слов из статей с использованием НЛП

Фон

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

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

И в этой статье мы объединим их - мы будем применять НЛП к коллекции статей (подробнее об этом ниже) для извлечения ключевых слов.

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

В этой статье мы будем извлекать ключевые слова из набора данных, который содержит около 3800 рефератов. Исходный набор данных взят из Kaggle - NIPS Paper. Системы обработки нейронной информации (NIPS) - одна из ведущих конференций по машинному обучению в мире. Этот набор данных включает заголовок и аннотации всех документов NIPS на сегодняшний день (начиная с первой конференции 1987 года и заканчивая текущей конференцией 2016 года).

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

Подход высокого уровня

Импорт набора данных

Набор данных, используемый в этой статье, является подмножеством набора данных paper.csv, представленного в бумажных наборах данных NIPS на Kaggle. Были использованы только те строки, которые содержат аннотацию. Заголовок и аннотация были объединены, после чего файл сохраняется как файл * .txt, разделенный табуляцией.

import pandas
# load the dataset
dataset = pandas.read_csv('papers2.txt', delimiter = '\t')
dataset.head()

Как видим, набор данных содержит идентификатор статьи, год публикации и аннотацию.

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

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

Получить количество слов для каждого резюме

#Fetch wordcount for each abstract
dataset['word_count'] = dataset['abstract1'].apply(lambda x: len(str(x).split(" ")))
dataset[['abstract1','word_count']].head()

##Descriptive statistics of word counts
dataset.word_count.describe()

Среднее количество слов в аннотации составляет около 156 слов. Количество слов варьируется от минимум 27 до максимум 325. Количество слов важно для того, чтобы дать нам представление о размере обрабатываемого нами набора данных, а также о различиях в количестве слов в строках.

Самые распространенные и необычные слова

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

#Identify common words
freq = pandas.Series(' '.join(dataset['abstract1']).split()).value_counts()[:20]
freq

#Identify uncommon words
freq1 =  pandas.Series(' '.join(dataset 
         ['abstract1']).split()).value_counts()[-20:]
freq1

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

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

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

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

Stemming нормализует текст, удаляя суффиксы.

Лемматизация - это более сложная техника, которая работает на основе корня слова.

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

from nltk.stem.porter import PorterStemmer
from nltk.stem.wordnet import WordNetLemmatizer
lem = WordNetLemmatizer()
stem = PorterStemmer()
word = "inversely"
print("stemming:",stem.stem(word))
print("lemmatization:", lem.lemmatize(word, "v"))

Чтобы выполнить предварительную обработку текста в нашем наборе данных, мы сначала импортируем необходимые библиотеки.

# Libraries for text preprocessing
import re
import nltk
#nltk.download('stopwords')
from nltk.corpus import stopwords
from nltk.stem.porter import PorterStemmer
from nltk.tokenize import RegexpTokenizer
#nltk.download('wordnet') 
from nltk.stem.wordnet import WordNetLemmatizer

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

В библиотеке python nltk есть список стоп-слов по умолчанию. Кроме того, мы могли бы добавить контекстно-зависимые стоп-слова, для которых будут полезны «наиболее распространенные слова», перечисленные в начале. Теперь мы увидим, как создать список игнорируемых слов и как добавить собственные стоп-слова:

##Creating a list of stop words and adding custom stopwords
stop_words = set(stopwords.words("english"))
##Creating a list of custom stopwords
new_words = ["using", "show", "result", "large", "also", "iv", "one", "two", "new", "previously", "shown"]
stop_words = stop_words.union(new_words)

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

corpus = []
for i in range(0, 3847):
    #Remove punctuations
    text = re.sub('[^a-zA-Z]', ' ', dataset['abstract1'][i])
    
    #Convert to lowercase
    text = text.lower()
    
    #remove tags
    text=re.sub("</?.*?>"," <> ",text)
    
    # remove special characters and digits
    text=re.sub("(\\d|\\W)+"," ",text)
    
    ##Convert to list from string
    text = text.split()
    
    ##Stemming
    ps=PorterStemmer()
    #Lemmatisation
    lem = WordNetLemmatizer()
    text = [lem.lemmatize(word) for word in text if not word in  
            stop_words] 
    text = " ".join(text)
    corpus.append(text)

Давайте теперь рассмотрим элемент из корпуса:

#View corpus item
corpus[222]

Исследование данных

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

#Word cloud
from os import path
from PIL import Image
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
import matplotlib.pyplot as plt
% matplotlib inline
wordcloud = WordCloud(
                          background_color='white',
                          stopwords=stop_words,
                          max_words=100,
                          max_font_size=50, 
                          random_state=42
                         ).generate(str(corpus))
print(wordcloud)
fig = plt.figure(1)
plt.imshow(wordcloud)
plt.axis('off')
plt.show()
fig.savefig("word1.png", dpi=900)

Подготовка текста

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

Токенизация - это процесс преобразования непрерывного текста в список слов. Список слов затем преобразуется в матрицу целых чисел в процессе векторизации. Векторизацию также называют извлечением признаков.

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

Создание вектора количества слов

В качестве первого шага преобразования мы будем использовать CountVectoriser для разметки текста и создания словаря известных слов. Сначала мы создаем переменную cv класса CountVectoriser, а затем вызываем функцию fit_transform для изучения и создания словаря.

from sklearn.feature_extraction.text import CountVectorizer
import re
cv=CountVectorizer(max_df=0.8,stop_words=stop_words, max_features=10000, ngram_range=(1,3))
X=cv.fit_transform(corpus)

Давайте теперь разберемся с параметрами, переданными в функцию:

  • cv = CountVectorizer (max_df = 0.8, stop_words = stop_words, max_features = 10000, ngram_range = (1,3))
  • max_df - при построении словаря игнорируйте термины, частота которых в документе строго превышает заданный порог (стоп-слова для конкретного корпуса). Это сделано для того, чтобы у нас были только слова, относящиеся к контексту, а не часто используемые слова.
  • max_features - определяет количество столбцов в матрице.
  • Диапазон n-грамм - мы хотели бы посмотреть список отдельных слов, двух слов (биграммы) и трех слов (триграммы) комбинаций.

Кодированный вектор возвращается с длиной всего словаря.

list(cv.vocabulary_.keys())[:10]

Визуализируйте верхние N униграмм, биграмм и триграмм

Мы можем использовать CountVectoriser для визуализации 20 лучших униграмм, биграмм и триграмм.

#Most frequently occuring words
def get_top_n_words(corpus, n=None):
    vec = CountVectorizer().fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in      
                   vec.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], 
                       reverse=True)
    return words_freq[:n]
#Convert most freq words to dataframe for plotting bar plot
top_words = get_top_n_words(corpus, n=20)
top_df = pandas.DataFrame(top_words)
top_df.columns=["Word", "Freq"]
#Barplot of most freq words
import seaborn as sns
sns.set(rc={'figure.figsize':(13,8)})
g = sns.barplot(x="Word", y="Freq", data=top_df)
g.set_xticklabels(g.get_xticklabels(), rotation=30)

#Most frequently occuring Bi-grams
def get_top_n2_words(corpus, n=None):
    vec1 = CountVectorizer(ngram_range=(2,2),  
            max_features=2000).fit(corpus)
    bag_of_words = vec1.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in     
                  vec1.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], 
                reverse=True)
    return words_freq[:n]
top2_words = get_top_n2_words(corpus, n=20)
top2_df = pandas.DataFrame(top2_words)
top2_df.columns=["Bi-gram", "Freq"]
print(top2_df)
#Barplot of most freq Bi-grams
import seaborn as sns
sns.set(rc={'figure.figsize':(13,8)})
h=sns.barplot(x="Bi-gram", y="Freq", data=top2_df)
h.set_xticklabels(h.get_xticklabels(), rotation=45)

#Most frequently occuring Tri-grams
def get_top_n3_words(corpus, n=None):
    vec1 = CountVectorizer(ngram_range=(3,3), 
           max_features=2000).fit(corpus)
    bag_of_words = vec1.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in     
                  vec1.vocabulary_.items()]
    words_freq =sorted(words_freq, key = lambda x: x[1], 
                reverse=True)
    return words_freq[:n]
top3_words = get_top_n3_words(corpus, n=20)
top3_df = pandas.DataFrame(top3_words)
top3_df.columns=["Tri-gram", "Freq"]
print(top3_df)
#Barplot of most freq Tri-grams
import seaborn as sns
sns.set(rc={'figure.figsize':(13,8)})
j=sns.barplot(x="Tri-gram", y="Freq", data=top3_df)
j.set_xticklabels(j.get_xticklabels(), rotation=45)

Преобразование в матрицу целых чисел

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

TF-IDF состоит из 2-х компонентов:

  • TF - частота сроков
  • IDF - частота обратных документов

from sklearn.feature_extraction.text import TfidfTransformer
 
tfidf_transformer=TfidfTransformer(smooth_idf=True,use_idf=True)
tfidf_transformer.fit(X)
# get feature names
feature_names=cv.get_feature_names()
 
# fetch document for which keywords needs to be extracted
doc=corpus[532]
 
#generate tf-idf for the given document
tf_idf_vector=tfidf_transformer.transform(cv.transform([doc]))

Основываясь на оценках TF-IDF, мы можем извлечь слова с наивысшими оценками, чтобы получить ключевые слова для документа.

#Function for sorting tf_idf in descending order
from scipy.sparse import coo_matrix
def sort_coo(coo_matrix):
    tuples = zip(coo_matrix.col, coo_matrix.data)
    return sorted(tuples, key=lambda x: (x[1], x[0]), reverse=True)
 
def extract_topn_from_vector(feature_names, sorted_items, topn=10):
    """get the feature names and tf-idf score of top n items"""
    
    #use only topn items from vector
    sorted_items = sorted_items[:topn]
 
    score_vals = []
    feature_vals = []
    
    # word index and corresponding tf-idf score
    for idx, score in sorted_items:
        
        #keep track of feature name and its corresponding score
        score_vals.append(round(score, 3))
        feature_vals.append(feature_names[idx])
 
    #create a tuples of feature,score
    #results = zip(feature_vals,score_vals)
    results= {}
    for idx in range(len(feature_vals)):
        results[feature_vals[idx]]=score_vals[idx]
    
    return results
#sort the tf-idf vectors by descending order of scores
sorted_items=sort_coo(tf_idf_vector.tocoo())
#extract only the top n; n here is 10
keywords=extract_topn_from_vector(feature_names,sorted_items,5)
 
# now print the results
print("\nAbstract:")
print(doc)
print("\nKeywords:")
for k in keywords:
    print(k,keywords[k])

Заключительные замечания

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

Это довольно простой подход для понимания фундаментальных концепций НЛП и предоставления хорошей практической практики с некоторыми кодами Python в реальных случаях использования. Тот же подход можно использовать для извлечения ключевых слов из лент новостей и социальных сетей.