Часть 2. Предварительная обработка текста и кластеризация

Привет!

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

Теперь в этой части мы выполним следующие шаги:

1. Textual Data Preprocessing and Vectorization

2. Dimensionality Reduction

3. Clustering Analysis (Unsupervised ML)

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

1. Предварительная обработка и векторизация текстовых данных

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

Что такое предварительная обработка естественного языка?

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

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

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

data.head()

#merging all text column to single text column to work with

data['organized'] =  data['description'] + ' ' + data['listed_in'] + ' ' + data['country']+ ' ' + data['cast'] + ' '+ data['director']

#filled all the missing value with empty strings
data['organized'] = data['organized'].fillna("")

Теперь давайте проверим некоторые наблюдения нашей новой функции organized с помощью следующего кода:

data['organized'][2]

Выход:

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

Фильмы ужасов Международные фильмы для взрослых Сингапур Тедд Чан, Стелла Чанг, Хенли Хии, Лоуренс Кох, Томми Куан, Джош Лай, Марк Ли, Сьюзан Леонг, Бенджамин Лим Гилберт Чан

Итак, теперь у нас есть вся информация в одной функции, то есть описание фильма, имена актеров, имена режиссеров, жанры и страна.

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

Теперь мы применим некоторые естественные методы обработки к нашей функции с именем organized..

1. Очистка текста с помощью регулярных выражений(re)

#text cleaning
import re
def cleaned(x):
    return re.sub(r"[^a-zA-Z ]","",str(x))   #to remove all non-alphabetic characters
data['organized'] = data['organized'].apply(cleaned)

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

Здесь функция re.sub() принимает три аргумента:

  • Шаблон регулярного выражения для поиска во входной строке
  • Строка замены для замены неалфавитного символа
  • Входная строка для выполнения замены

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

а. Уменьшение регистра символов

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

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

data['organized'][0]

До:

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

Международные сериалы Драмы Сериалы Научная фантастика Фэнтези Для взрослых Бразилия Джу Мигель Бьянка Компарато Мишель Гомеш Родольфо Валенте Ванеса Оливейра Рафаэль Лозано Вивиан Порто Мел Фронковяк Серджио Мамберти Зез Мотта Челсо Фратески

# Lower Casing
data['organized']= data['organized'].str.lower()

После:

в будущем, где элита населяет райский остров вдали от переполненных трущоб, у вас есть один шанс присоединиться к спасенным от нищеты международным телешоу, телевизионным драмам, научно-фантастическому фэнтези, взрослым, бразилии, джу, мигель, бьянка, компарато, мишель гомес, родольфо, валенте, ванеса, оливейра, рафаэль, лозано, вивиан, порто мель. Фронковяк Серджио Мамберти Зез Мотта Челсо Фратески

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

б. Удаление знаков препинания

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

import string

def remove_punctuation(text):
    # Create a translation table to remove punctuation using the string module
    translator = str.maketrans('', '', string.punctuation)
    # Apply the translation table to remove punctuation from the text
    text = text.translate(translator)
    return text

data['organized'] = data['organized'].apply(remove_punctuation)

Здесь мы создали функцию, которая удаляет все знаки препинания из текста, используя модуль string и модуль str.translate() method..

string.punctuation содержит все знаки препинания, а метод str.maketrans() создает таблицу перевода, которая поможет сопоставить и удалить знаки препинания из нашей строки.

Затем мы применили эту функцию к нашей переменной с именем organized..

data['organized'][0]

Выход:

в будущем, где элита населяет райский остров вдали от переполненных трущоб, у вас есть один шанс присоединиться к спасенным от нищеты международным телешоу, телевизионным драмам, научно-фантастическому фэнтези, взрослым, бразилии, джу, мигель, бьянка, компарато, мишель гомес, родольфо, валенте, ванеса, оливейра, рафаэль, лозано, вивиан, порто мель. Фронковяк Серджио Мамберти Зез Мотта Челсо Фратески

Как видно из приведенного выше вывода, в наших данных теперь нет знаков препинания.

в. Удаление стоп-слов

Теперь давайте удалим стоп-слова. Помните, что стоп-слова — это слова, которые обычно используются в английском языке. Некоторыми примерами стоп-слов являются «the», «будет», «есть», «а», «из», «на», «в» и т. д. Нам нужно удалить эти слова, потому что они обычно не имеют смысла.

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

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

#Importing necessary libraries that help in removing stopwords
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
Removing Stopwords
def remove_stopwords(text):
    # Tokenizing the text into words
    words = nltk.word_tokenize(text)
    # Removing stopwords from the list of words
    words = [word for word in words if word.lower() not in stopwords.words('english')]
    # Joining the remaining words back into a single string
    text = ' '.join(words)
    return text

data['organized'] = data['organized'].apply(remove_stopwords)

nltk — известная библиотека обработки естественного языка, часто используемая в задачах NLP. Мы импортировали и загрузили стоп-слова из библиотеки этого инструментария.

Затем мы написали функцию удаления стоп-слов, которая принимает текст (одно наблюдение за раз) и разбивает текст на отдельные слова с помощью метода nltk.word tokenize().

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

Теперь давайте проверим одно наблюдение и посмотрим на наши изменения:

data['organized'][0]

Выход:

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

д. Нормализация текста

Этот шаг включает в себя нормализацию текста, что означает применение лемматизации и стемминга:

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

Ключевое различие между лемматизацией и стеммингом заключается в том, что:

Стемминг преобразует слово в его корневую форму, в то время как лемматизация проверяет его еще глубже, поскольку лемматизация проверяет контекст, в котором используется слово.

#stemming
stemmer = SnowballStemmer('english')
stop_words = set(stopwords.words('english'))

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

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

def stem_text(text):
    words = nltk.word_tokenize(text)  # tokenizing the text into words
    stemmed_words = [stemmer.stem(word) for word in words]  # applying the Snowball stemmer to each word
    return ' '.join(stemmed_words)  # joining the stemmed words back into a single string

# Apply the stem_text function to a column of a pandas DataFrame, such as a column called 'text'
data['org_new'] = data['organized'].apply(stem_text)

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

Здесь stemmer.stem() используется для выделения текста с помощью Snowball Stemmer.

До:

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

После:

futur elit населять остров paradis далеко толпа трущобы получить один шанс присоединиться спасти убожество стажер телешоу tv драма tv scifi fantasi взрослый бразилия joo miguel bianca comparato michel gome родольфо валент vaneza Oliveira rafael lozano vivian porto mel fronckoviak sergio mamberti zez motta celso frateschi

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

е. Векторизация текста

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

new_df = data[['title', 'org_new']]
new_df.head()

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

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

Он рассчитывал оценку tf-idf для термина, и термин с более высокой оценкой приобретал большее значение. Чтобы узнать больше о методах векторизации текста, вы можете прочитать эту замечательную статью от Analytics Vidya.

#using tfidf
from sklearn.feature_extraction.text import TfidfVectorizer

t_vectorizer = TfidfVectorizer(max_features=20000)
X= t_vectorizer.fit_transform(new_df['org_new'])

Мы создали новую переменную с именем t_vectorizer, в которой мы сохранили наш векторизатор TFIDF, указав для параметра max_features значение 20000, чтобы сохранить наиболее часто встречающиеся слова.

Затем мы создали наш метод X, где применен метод t_vectorizer.fit_transform() к нашему столбцу org_new.

Здесь мы используем fit_transform() вместо того, чтобы вызывать fit() и transform() по отдельности. Обратите внимание, что мы можем вызвать fit_transform() один раз, чтобы изучить преобразование и применить его к данным за один шаг.

X.shape

Проверив форму X, мы видим, что сейчас в наших данных 7770 наблюдений и 20 000 переменных.

X.toarray()

Здесь мы использовали X.toarray() для преобразования нашей разреженной матрицы в плотный массив numpy. Вот наш результат:

Наконец, мы преобразовали наши текстовые данные в числа.

2. Уменьшение размерности

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

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

Применим PCA:

from sklearn.decomposition import PCA
pca = PCA()
pca.fit(X.toarray())

# Lets plot explained var v/s comp to check how many components to be considered.
plt.figure(figsize=(14,5), dpi=120)
plt.plot(np.cumsum(pca.explained_variance_ratio_))
plt.xlabel('number of components')
plt.ylabel('cumulative explained variance')
plt.show()

np.cumsum(pca.explained_variance_ratio_) — это функция NumPy, которая вычисляет кумулятивную сумму объясненных коэффициентов дисперсии основных компонентов, полученных из PCA.

Это объясненное отношение основного компонента представляет собой долю общей дисперсии данных, захваченных этим конкретным компонентом.

pca.explained_variance_ratio_ и возвращает массив объясненных коэффициентов дисперсии каждого главного компонента. Затем np.cumsum() вычисляет совокупную сумму этих коэффициентов.

  • Из приведенного выше графика видно, что 5000 компонентов могут объяснить почти 95% дисперсии.
  • Поскольку выбор 5000 может быть сложным, мы установим значение 95% в sklearn, так как он автоматически будет знать, что он должен захватить 95% дисперсии. Таким образом, он выберет только эти компоненты.
pca_tuned = PCA(n_components=0.95)
pca_tuned.fit(X.toarray())
X_transformed = pca_tuned.transform(X.toarray())
X_transformed.shape

После настройки нашего PCA наша форма будет (7770, 5538), что указывает на то, что теперь у нас есть 7770 наблюдений и 5538 признаков, в отличие от 20 000 признаков ранее. Это указывает на то, что мы поймали максимальную дисперсию наших данных только с 5538 функциями.

3. Кластерный анализ (неконтролируемое машинное обучение)

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

Кластеризация группирует похожие точки данных на основе их функций или атрибутов.

В этом проекте мы применим к нашим данным два алгоритма кластеризации:

  • K означает кластеризацию
  • Агломеративная кластеризация

Давайте сначала применим KMeans Clustering.

K означает кластеризацию

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

sum_of_sq_dist = {} 
for k in range(1,22):
    km = KMeans(n_clusters= k, init= 'k-means++', max_iter= 300, n_init=10, random_state=0)
    km = km.fit(X_transformed)
    sum_of_sq_dist[k] = km.inertia_
    
#Plot the graph for the sum of square distance values and Number of Clusters
sns.pointplot(x = list(sum_of_sq_dist.keys()), y = list(sum_of_sq_dist.values()))
plt.xlabel('Number of Clusters(k)')
plt.ylabel('Sum of Square Distances')
plt.title('Elbow Method For Optimal k')
plt.show()

Во-первых, был создан пустой словарь для хранения значения k и соответствующей ему суммы квадратов значений. Затем алгоритму KMeans были переданы различные параметры, и под него были подобраны данные X_transformed. Сумма квадратов значений для определенного значения k была сохранена с использованием km.inertia.

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

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

Здесь мы выберем количество кластеров равным 10, так как после этого резкой разницы не видно.

plt.figure(figsize=(10,6), dpi=120)

kmeans= KMeans(n_clusters=10, init= 'k-means++', random_state=9)
kmeans.fit(X_transformed)

#predicting the labels of clusters.
label = kmeans.fit_predict(X_transformed)
#Getting unique labels
unique_labels = np.unique(label)
 
#plotting the results:
for i in unique_labels:
    plt.scatter(X_transformed[label == i , 0] , X_transformed[label == i , 1] , label = i)
plt.legend()
plt.show()
# Add cluster values to the dateframe.
data['cluster_number'] = kmeans.labels_

Здесь результаты нашей модели KMeans нанесены с использованием точечной диаграммы. Для каждой уникальной метки создается точечная диаграмма со связанными с ней точками данных. Значения оси x и оси y для каждой точки данных получаются с использованием массива X_transformed.

Мы создали новый столбец с именем cluster_number, в котором указано, какие наблюдения относятся к какому кластеру.

#word cloud for user rating review
def func_select_Category(cluster_label,column_of_choice):
  df_word_cloud = data[['cluster_number',column_of_choice]].dropna() #dropping null values
  df_word_cloud = df_word_cloud[df_word_cloud['cluster_number']==cluster_label] #dividing into different clusters
  text = " ".join(word for word in df_word_cloud[column_of_choice]) #rejoining words
  # Create stopword list:
  stopwords = set(STOPWORDS) #to remove stopwords if any
  # Generate a word cloud image
  wordcloud = WordCloud(stopwords=stopwords, background_color="black").generate(text)
  # Plot the wordclouds
  plt.imshow(wordcloud, interpolation='blackman')  #imshow function displays the image on the current axes.
  plt.title(f'Cluster: {i}', fontsize=18, fontweight='bold')
  plt.axis("off")
  plt.show()

for i in range(10):
  func_select_Category(i,'listed_in')

Приведенный выше код генерирует облака слов для функции listed_in в нашем наборе данных на основе присвоенных им меток кластеров.

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

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

Мы провели углубленный анализ в нашей записной книжке. Вы можете проверить это в нашем репозитории на GitHub. Ссылка дана в конце статьи.

Агломеративная кластеризация

k_range = range(5, 12)  #specifying the range for k values

# Compute Silhouette score for each k
from sklearn.cluster import AgglomerativeClustering 
for k in k_range:
    model = AgglomerativeClustering(n_clusters=k) #creating an object of agglomerative clustering and setting number of clusters to k
    labels = model.fit_predict(X_transformed) #apply agglomerative clustering model to our X_transformed
    score = silhouette_score(X, labels) #cal silhouette score using sihouette_score function
    print("k=%d, Silhouette score=%f" % (k, score)) 

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

И снова мы получили k = 10 с максимальной оценкой силуэта. Итак, давайте применим агломеративную кластеризацию к нашему X_transformed.

from sklearn.cluster import AgglomerativeClustering
model = AgglomerativeClustering(n_clusters = 10, affinity = 'euclidean', linkage = 'ward')
labels = model.fit_predict(X_transformed)
# Create a scatter plot with different colors for different clusters
plt.figure(figsize=(10,6), dpi=120)
plt.scatter(X_transformed[:, 0], X_transformed[:, 1], c=labels, cmap='tab10')

'''The first argument to the function, X_transformed[:, 0], specifies the 
x-coordinates of the data points, while X_transformed[:, 1] specifies the y-coordinates.'''

plt.legend()
plt.show()

data['cluster_number_agg_clustering'] = model.labels_

Мы создали новый столбец с именем cluster_number_agg_clustering, в котором указано, какие наблюдения относятся к какому кластеру. Это поможет нам построить облако слов, чтобы лучше понять распределение между различными кластерами.

#word cloud for user rating review
def func_select_Category_agg_clustering(cluster_label,column_of_choice):
  df_word_cloud = data[['cluster_number_agg_clustering',column_of_choice]].dropna()
  df_word_cloud = df_word_cloud[df_word_cloud['cluster_number_agg_clustering']==cluster_label]
  text = " ".join(word for word in df_word_cloud[column_of_choice])
  # Create stopword list:
  stopwords = set(STOPWORDS)
  # Generate a word cloud image
  wordcloud = WordCloud(stopwords=stopwords, background_color="black").generate(text)
  # Plot the wordclouds
  plt.imshow(wordcloud, interpolation='blackman')
  plt.title(f'Cluster: {i}', fontsize=18, fontweight='bold')
  plt.axis("off")
  plt.show()

for i in range(10):
  func_select_Category_agg_clustering(i,'listed_in')

##Here we have used the same code we used above for plotting, we have just replaced some names 

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

  • Кластер 0 содержит максимальное количество документальных фильмов, мюзиклов и спорта.
  • Кластер 1 содержит максимальное количество независимых, международных фильмов и фильмов ужасов.
  • Кластер 2 содержит максимальное количество телешоу
  • Кластер 3 содержит максимальное количество семейных и детских фильмов.
  • Кластер 4 содержит максимальное количество корейских криминальных триллеров.
  • Кластер 5 содержит максимальное количество стендап-шоу.
  • Кластер 6 содержит максимальное количество объектов природы и науки.
  • Кластер 7 содержит максимальное количество драматических, комедийных и международных фильмов.
  • Кластер 8 содержит максимальное количество аниме-фильмов и аниме-сериалов.
  • Кластер 9 содержит максимальное количество боевиков, комедий, драм и зарубежных фильмов.

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

Заключение

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

Ссылка на набор данных: набор данных, используемый в этом проекте, взят из Kaggle.

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

В последнем сезоне этой серии мы создадим рекомендательную систему и развернем ее на платформе Hugging Face. А пока внимательно прочитайте эти две статьи и ознакомьтесь с данными.

Ссылка на 1 сезон. Большое спасибо за чтение. Я надеюсь, что вы нашли его информативным и приятным.