Изучение мер сходства для задач НЛП

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

Чтобы продемонстрировать, как эти меры сходства работают на практике, мы будем использовать набор данных выступлений Сената США. Мы извлечем текст из каждой речи и предварительно обработаем текст, удалив стоп-слова и символы, не являющиеся словами, и выполнив лемматизацию. После этого мы изучим частоту слов в речах и векторизируем речи, используя подход векторизации TF-IDF (Term Frequency-Inverse Document Frequency). Затем мы рассчитаем меры сходства и проанализируем результаты.

Получение данных

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

import os
import pandas as pd
import chardet
from bs4 import BeautifulSoup

# Path to the folder containing the files
folder_path = "/Users/mukhamejan/Desktop/school/Winter_23/ECBS6253/ML-for-NLP-main/Inputs/105-extracted-date"

# Create an empty dictionary to store the text content with senator names as keys
senator_text = {}

# Loop through each file in the folder
for filename in os.listdir(folder_path):
    if filename.endswith(".txt") and filename.startswith("105-"):
        # Extract senator name from the filename
        senator_name = filename.split("-")[1]

        # Detect encoding and read the file content
        with open(os.path.join(folder_path, filename), "rb") as f:
            result = chardet.detect(f.read())
            file_encoding = result["encoding"]
        with open(os.path.join(folder_path, filename), "r", encoding=file_encoding) as f:
            xml_content = f.read()

        # Remove any extra content after the XML document
        xml_content = xml_content[:xml_content.rfind("</DOC>") + len("</DOC>")]

        # Parse the file content using BeautifulSoup
        soup = BeautifulSoup(xml_content, "xml")

        # Extract the text content from the <TEXT> element
        text = soup.find("TEXT").text.strip()

        # Store the text content with senator name as the key in the dictionary
        senator_text[senator_name] = text

# Create a Pandas DataFrame with a column for each senator's text content
df = pd.DataFrame.from_dict(senator_text, orient="index", columns=["Text"])

Словарь senator_text хранит текст речей с именами сенаторов в качестве ключей, а df DataFrame содержит текст речей с текстом каждого сенатора в виде столбца.

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

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

  1. Заменяет все несловесные символы в тексте пробелом с помощью функции re.sub из модуля re.
  2. Разбивает текст на отдельные слова с помощью функции word_tokenize из библиотеки nltk.
  3. Преобразует все токены в нижний регистр с помощью метода lower.
  4. Отфильтровывает любые стоп-слова (распространенные слова, такие как «a», «the» и т. д.), используя список стоп-слов из корпуса стоп-слов библиотеки nltk.
  5. Отфильтровывает любые токены, длина которых меньше 3.
  6. Соединяет оставшиеся токены в одну строку, используя метод join.
  7. Возвращает предварительно обработанный текст.
def text_preprocesser(text):
    text= re.sub(r'\W',' ', text)
    tokens = word_tokenize(text.lower())
    tokens = [token for token in tokens if token not in stopwords.words('english')]
    tokens = [word for word in tokens if len(word)>=3]
    preprocessed_text = ' '.join(tokens)
    return preprocessed_text 

Векторизация TF-IDF

Термин частотно-обратная частота документа (TF-IDF) представляет собой числовую статистику, которая отражает важность слова в документе. Значение TF-IDF увеличивается с частотой слова в документе и уменьшается с частотой слова в корпусе. Код реализует векторизацию TF-IDF с использованием класса TfidfVectorizer из модуля sklearn.feature_extraction.text. Векторизатор инициализируется функцией text_preprocessor, определенной ранее. Метод fit_transform векторизатора используется для преобразования предварительно обработанных текстовых данных в матрицу значений TF-IDF. Затем создается Pandas DataFrame с токенами и их значениями TF-IDF с каждым документом в виде столбцов.

# TFIDF Vectorize using the predefined preprocesser. min_df=2 here is not needed, but it does not change anything
# initialise the vectorizer
tfidf_vectorizer = TfidfVectorizer(preprocessor = text_preprocesser, min_df =2 )
# fit the vectorizer
tfidf = tfidf_vectorizer.fit_transform([roth, murray])
# build a data frame with the tokens and their tfidf value with each document as columns
tfidf_rm = pd.DataFrame(tfidf.toarray().transpose(), index=tfidf_vectorizer.get_feature_names())
tfidf_rm.columns = ['roth', 'murray']
# print to see
tfidf_rm

Чтобы лучше понять тексты двух речей, я построил частотное распределение первых 10 слов в объединенных выступлениях Рота и Мюррея, используя приведенный ниже код Python:

# Apply text preprocesser to a combined string
tokens = text_preprocesser(roth + " " + murray).split()
# Count the tokens
from collections import Counter
dict_counts = Counter(tokens)
dict_counts
# Plot the frequency of top 10 words
labels, values = zip(*dict_counts.items())
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline 
# sort your values in descending order
indSort = np.argsort(values)[::-1]

# rearrange your data and show top 10 words
labels = np.array(labels)[indSort][0:10]
values = np.array(values)[indSort][0:10]
indexes = np.arange(len(labels))

plt.bar(indexes, values, color="red")
# add labels
plt.xticks(indexes, labels, rotation=45)

Этот код сначала предварительно обрабатывает речи, очищая их, удаляя стоп-слова и сводя слова к их корневой форме. Затем он объединяет речи Рота и Мюррея и подсчитывает частоту каждого слова. Затем на гистограмму наносятся первые 10 слов с наибольшей частотой.

Полученный график показывает частотное распределение первых 10 слов в объединенных речах Рота и Мюррея. Ось x представляет первые 10 слов, а ось y показывает частоту этих слов.

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

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

Меры подобия

  1. Косинусное сходство:

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

Пример использования в коде:

from sklearn.metrics.pairwise import cosine_similarity

# Get the tfidf values and calculate the cosine similarity value
similarity_rm = cosine_similarity(tfidf_rm['roth'].values.reshape(1, -1), tfidf_rm['murray'].values.reshape(1, -1))[0][0]
print("Cosine similarity between Senators Roth and Murray's speeches (TFIDF): {:.2f}%".format(similarity_rm * 100))

2. Сходство Жаккара:

Сходство Жаккара измеряет сходство между двумя наборами элементов. Он рассчитывается как отношение пересечения двух множеств к их объединению. В контексте обработки естественного языка сходство Жаккара можно использовать для сравнения сходства словарного запаса между двумя документами.

Пример использования в коде:

from sklearn.metrics import jaccard_score

# Preprocess the documents and create a set of each
# sets contain only the unique elements in the preprocessed text
set1 = set(text_preprocesser(roth).split())
set2 = set(text_preprocesser(murray).split())

# Convert the sets to lists
list1 = list(set1)
list2 = list(set2)

# Define Jaccard Similarity function for two sets
# The measure is equal to the count of shared tokens over count of total tokens
def jaccard_set(list1, list2):
    intersection = len(list(set(list1).intersection(list2)))
    union = (len(list1) + len(list2)) - intersection
    return float(intersection) / union

# Apply function
similarity = jaccard_set(list1, list2)

# Print the similarity as a percentage
print("Jaccard similarity: {:.2f}%".format(similarity * 100))

3. Евклидово расстояние:

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

Пример использования в коде:

from scipy.spatial.distance import euclidean

# Calculate the Euclidean distance between the TFIDF vectors for each senator's speech
distance = euclidean(tfidf_rm['roth'].values.reshape(1, -1), tfidf_rm['murray'].values.reshape(1, -1))

# Print the distance
print("Euclidean distance: {:.2f}".format(distance))

4. Манхэттенское расстояние:

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

Пример использования в коде:

from scipy.spatial.distance import cityblock

# Calculate the Manhattan distance between the TFIDF vectors for each senator's speech
distance = cityblock(tfidf_rm['roth'].values.reshape(1, -1), tfidf_rm['murray'].values.reshape(1, -1))

# Print the distance
print("Manhattan distance: {:.2f}".format(distance))

5. Коэффициент корреляции Пирсона.

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

В контексте сходства текстов коэффициент корреляции Пирсона можно использовать для измерения сходства векторов TF-IDF для двух текстов. В данном коде вычисляется коэффициент корреляции Пирсона между векторами TF-IDF речей сенаторов Рота и Мюррея. Коэффициент вычисляется с помощью функции pearsonr из модуля scipy.stats, которая принимает на вход два массива и возвращает коэффициент корреляции и p-значение.

Коэффициент корреляции Пирсона между векторами TF-IDF выступлений сенаторов Рота и Мюррея выводится с использованием следующего кода:

from scipy.stats import pearsonr
correlation, p_value = pearsonr(tfidf_rm['roth'].values.reshape(1, -1)[0], tfidf_rm['murray'].values.reshape(1, -1)[0])
print("Pearson correlation coefficient: {:.2f}".format(correlation))

Выходные данные показывают коэффициент корреляции Пирсона между векторами TF-IDF для двух речей, который представляет собой значение от -1 до 1. Значение, близкое к 1, указывает на сильную положительную корреляцию, значение, близкое к -1, указывает на сильную отрицательную корреляцию. , а значение, близкое к 0, указывает на отсутствие корреляции. В этом случае мы находим коэффициент равным 0,13.

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

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

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

Наш анализ показывает, что речи двух сенаторов имеют очень низкий показатель сходства по Жаккару, составляющий 0,06%, что указывает на то, что в них не так много общих слов. Это может свидетельствовать о том, что у них очень разные политические программы и идеологии.

Евклидово расстояние между векторами TFIDF для каждого выступления сенатора составляет 0,83, что является относительно низким показателем, указывающим на некоторый уровень сходства между выступлениями. Точно так же манхэттенское расстояние 3,80 не особенно велико, что позволяет предположить, что у выступлений могут быть схожие темы и темы.

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

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

В поисках лучшей подруги Байдена

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

# Find the senator whose speeches are the most similar to Biden's speeches
# Combine all the senator texts into a single list
all_texts = list(senator_text.values())

# Vectorize the texts using TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer(preprocessor=text_preprocesser, min_df =2)
tfidf = tfidf_vectorizer.fit_transform(all_texts)

# Calculate the cosine similarity between each pair of texts
cosine_similarities = cosine_similarity(tfidf, tfidf_vectorizer.transform([senator_text['biden']]))

# Create a DataFrame with the cosine similarities
similarity_df = pd.DataFrame({
    'senator': list(senator_text.keys()),
    'cosine_similarity': cosine_similarities.flatten()
})

# Print the DataFrame
similarity_df = similarity_df.sort_values(by='cosine_similarity', ascending=False)
# Drop Biden
similarity_df = similarity_df.drop(similarity_df[similarity_df['senator'] == 'biden'].index)
# Print the DataFrame
similarity_df

Первый шаг — объединить все тексты сенаторов в единый список. Затем TfidfVectorizer используется для векторизации текстов. Напоминаем, что TfidfVectorizer — это метод, который создает матрицу терминов документа с оценками tf-idf. Tfidf означает термин частотно-обратная частота документа, и это числовая статистика, которая отражает, насколько важно слово для документа в наборе документов. В этом случае TfidfVectorizer используется для преобразования текста в матрицу, где каждая строка представляет сенатора, а каждый столбец представляет слово в тексте.

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

Затем код создает фрейм данных с косинусным сходством и сортирует его в порядке убывания. Сенатор с наивысшей оценкой сходства с речами Байдена имеет наивысшее значение косинусного сходства. Наконец, кадр данных печатается с сенатором и оценкой косинусного сходства.

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

Вывод показывает, что Беннет является наиболее похожим на Байдена сенатором со значением косинусного подобия 0,178762, за ним следует Эшкрофт с 0,170275 и Маккейн с 0,144165. Наименее похожим на Байдена сенатором является Стивенс со значением косинусного подобия 0,0000, за ним следуют Мурковски с 0,004488 и Хатчисон с 0,006821.

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

Вот общий график рассеяния косинусного сходства речей.

# merge the party alliance data with the initial datafram
merged_df = similarity_df.merge(parties, left_on='senator', right_on='lname').drop(['lname', 'cong', 'id', "dist"], axis=1)
merged_df
# Create the scatter plot with hue based on party affiliation
sns.scatterplot(data=merged_df, x="cosine_similarity", y="party", hue="party")
plt.savefig("scatter.png")

100 кодексов демократов и 200 республиканцев. Мы видим, что схожие ценности выступлений коллег Байдена менее разбросаны, чем у республиканцев. Довольно удивительно видеть, что правый хвост «распределения» республиканских баллов указывает на более высокую величину сходства. Мне не хватает знаний в предметной области, чтобы объяснить, в чем причина этого. Тем не менее, это интересное наблюдение.

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