Этот проект предоставлен IBM skills network.

Откройте для себя мир безграничных возможностей обучения! В этом проекте вы погрузитесь в захватывающую сферу обработки естественного языка (NLP) и машинного обучения (ML), чтобы найти похожие курсы, как никогда раньше. От предварительной обработки текста до его векторизации с помощью передовых моделей НЛП, таких как BERT, вы овладеете искусством подготовки текста для анализа. Получите практический опыт работы с алгоритмами кластеризации и найдите оптимальное количество кластеров, используя научные методы, такие как оценка силуэта, метод Дэвиса Боулдина, метод Калински-Харабаса и X-средних. Откройте для себя красоту визуализации данных при построении похожих курсов в интерактивном 2D и 3D. И, наконец, ищите и рекомендуйте кластеры с учетом ваших конкретных интересов — и все это в одном проекте!

➜ Совет для профессионалов: если вы хотите учиться быстрее и запускать (или скачивать) этот проект в Jupyter Notebook бесплатно, посетите CognitiveClass.ai.

После завершения этого проекта вы сможете:

1. Подготовить набор данных и текст для анализа на различных этапах
2. Векторизовать текст для задач машинного обучения с использованием современных моделей НЛП, таких как модель BERT для встраивания
3. Определить оптимальную количество кластеров
4. Используйте алгоритм K-средних для кластеризации
5. Визуализируйте похожие курсы на 2D- и 3D-графиках
6. Рекомендуйте кластер курса, исходя из ваших интересов

Загрузка библиотек

# if you have not installed the following libraries, un-comment this code:
#pip install -U 'skillsnetwork' 'yellowbrick' 'sentence-transformers' 'seaborn' 'texthero' 'pyclustering' 'nltk' 'nmslib'

import string
import re
import nltk
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.cluster import KMeans
from nltk.stem import PorterStemmer

1. Подготовка набора данных

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

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

# Load the daset
data = pd.read_csv('course-catalog.csv')
print('==> List of all columns: \n\n',list(data),'\n')

# sort data by names and subset data
data = data.sort_values(by='Name')

# lets drop the duplicated data point in the description
data = data.drop_duplicates(subset='Description', keep="first")

# select columns the we are interested
data = data[['Subject','Name','Description']]
data.head()

Этот проект будет сосредоточен на наборе данных, содержащем 190 предметов, некоторые из которых имеют ограниченное количество точек данных. Чтобы обеспечить надежный анализ, мы выберем и сохраним только 15 наиболее часто встречающихся «субъектов» в данных. Затем мы исследуем, насколько эффективно кластеризация может дифференцировать эти 15 тематических групп на основе их описаний.

# counting the datapoints for each group of subject 
keep=data.groupby('Subject').count().sort_values(by='Name',ascending=False)['Name']
print(' ==> list of Subject and their counts:', keep)

# keep only first 15 most frequent
keep=keep[:15].index
print('\n ==> List of Subject to keep: \n\n', keep)

data = data[data['Subject'].isin(keep)]
data = data.reset_index(drop=True)
data.shape

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

Шаги для предварительной обработки текста следующие:

1. Преобразуйте весь текст в нижний регистр для единообразия и простоты обработки.
2. Удалите все знаки препинания и стоп-слова, поскольку они не влияют на смысл текста.
3. Используйте корень, чтобы сократить слова до их значения. базовая форма для лучшей группировки похожих слов.
4. Применить лемматизацию для дальнейшей нормализации слов, рассматривая варианты слова как одно и то же.
5. Преобразовать обработанный текст в числовые векторы для математического анализа и обработки .

# selecting the column that has text
text = data['Description']

# removing null values
text = text[~pd.isnull(text)] #removing missing values
text=text.tolist()

# make it lower case
text = [t.lower() for t in text]
text[:3]

# Strip all punctuation from each article
# This uses str.translate to map all punctuation to the empty string
table = str.maketrans('', '', string.punctuation)
text = [t.translate(table) for t in text]

# Convert all numbers in the article to the word 'num' using regular expressions
text = [re.sub(r'\d+', 'num', t) for t in text]
# Print the first article as a running example
text[:2]

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

# creating stemmer model and fit evey sentence to it
stemmer = PorterStemmer()
text_stem = [stemmer.stem(t) for t in text]

# for lemmatization, you need to download "wordnet" repository which contains family of words
nltk.download("wordnet")

# create lemmatizer and apply it for every sentence in the text
lem = nltk.stem.wordnet.WordNetLemmatizer()
text_lem = [lem.lemmatize(t) for t in text_stem]

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

2. Векторизация предложений

Здесь мы используем:
Sentence-BERT — это модификация платформы BERT, позволяющая проводить крупномасштабное семантическое сравнение, группировку и поиск информации.
SBERT имеет библиотеку Python под названием sentence_transformers, которые вы можете использовать для вычисления вложений предложений для более чем 100 языков. Затем эти вложения можно сравнить, например. с косинусным сходством или кластеризацией, чтобы найти предложения со схожими значениями. Это может быть полезно для семантического текстового сходства, семантического поиска или анализа парафраз.

В разделе Установка необходимых библиотек уже должны быть установлены предложения_трансформеров. Создадим список предложений.

Библиотека предлагает большую коллекцию предобученных моделей]() для различных задач. Например, модель all-mpnet-base-v2 обеспечивает наилучшее качество, тогда как all-MiniLM-L6-v2 работает в 5 раз быстрее и по-прежнему обеспечивает хорошее качество. Давайте загрузим модель all-MiniLM-L6-v2.

from sentence_transformers import SentenceTransformer
from sentence_transformers import util
model_SBERT = SentenceTransformer('all-MiniLM-L6-v2')

В этом разделе мы применим SBERT к описаниям наших курсов.

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

# this method encode the sentences into numpy vectors which can be used for clustering task
# the vectorization may take time
embeddings = model_SBERT.encode(text_lem, convert_to_numpy=True,show_progress_bar=True) # By default, convert_to_numpy = True
embeddings.shape
print(embeddings[0][:50])

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

➜ Совет: вы можете бесплатно скачать полную версию Jupyter Notebook с веб-сайта когнитивного класса.

3. Выбор количества кластеров (k) для кластеризации K-средних

Есть несколько других методов, которые можно использовать для поиска оптимального количества кластеров для кластеризации k-средних в Python, помимо метода локтя. Некоторые из этих методов включают в себя:

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

Индекс Калински-Харабаса. Этот метод вычисляет оценку для каждого значения k на основе отношения WCSS кластеров к сумме квадратов между кластерами. Оптимальное количество кластеров — это значение k, которое максимизирует индекс Калинского-Харабаша.

Индекс Дэвиса-Булдина: этот метод вычисляет оценку для каждого значения k на основе отношения WCSS кластеров к расстоянию между центрами кластеров. Оптимальное количество кластеров — это значение k, которое минимизирует индекс Дэвиса-Булдина.

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

# we append the best number of k into the list to reach our final desicion
optimal_Ks=[]
# Using Silhouette score
from sklearn.metrics import silhouette_score

# Create a list of possible numbers of clusters to try
num_clusters = list(range(2, 20))

# Calculate the silhouette scores for each number of clusters
silhouette_scores = []
for k in num_clusters:
    # run clustering for each k
    kmeans = KMeans(n_clusters=k,n_init=10)
    kmeans.fit(embeddings)
    labels = kmeans.predict(embeddings)
    #calculating the sihouetter score
    sil_score=silhouette_score(embeddings,labels)
    print("cluster:",k,  "score -> ",  sil_score)
    silhouette_scores.append(sil_score)

# displaying the optimum number of clusters
optimal_num_clusters = num_clusters[silhouette_scores.index(max(silhouette_scores))]
print("The highest s-score of the k-clusters: " , optimal_num_clusters)

#appending the result into the opmimum list
optimal_Ks.append(optimal_num_clusters)

Вы также можете построить кластеры в этом методе. Критерии выбора лучшего номера кластера на основе визуализации следующие:

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

Индекс Дэвиса Боулдинга:

# Davies Bouldin index
from sklearn.cluster import KMeans
from sklearn.metrics import davies_bouldin_score

# Create a list of possible numbers of clusters to try
num_clusters = list(range(2, 21))

# Calculate the silhouette scores for each number of clusters
dav_scores = []
for k in num_clusters:
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(embeddings)
    labels = kmeans.predict(embeddings)
    
    # calculating Davies score for each k
    dav_score=davies_bouldin_score(embeddings, labels)
    print("cluster:",k,  "value -> ",  dav_score)
    dav_scores.append(dav_score)

# displaying the optimum number of clusters
optimal_num_clusters = num_clusters[dav_scores.index(min(dav_scores))]
print("The optimum score of the k-clusters: " , optimal_num_clusters)

#appending the result into the opmimum list
optimal_Ks.append(optimal_num_clusters)

Х означает:

from pyclustering.cluster.xmeans import xmeans
from pyclustering.cluster.center_initializer import kmeans_plusplus_initializer

# Create an X-means object with the sample data
xmeans_instance = xmeans(embeddings, kmeans_plusplus_initializer(embeddings, 2).initialize())

# Perform the X-means algorithm
xmeans_instance.process()

# Get the optimal number of clusters
clusters = xmeans_instance.get_clusters()

# print the clusters
print('the identified optimum cluster ==> ',len(clusters))

#appending the result into the opmimum list
optimal_Ks.append(len(clusters))

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

# lets vote the median of the suggested clusters
int(np.median(optimal_Ks))

4. Кластеризация данных

#'How many clusters do you want to use? use utilze median to vote

# lets use median of the optimal number of clusters
true_k = int(np.median(optimal_Ks))
k_model = KMeans(n_clusters=true_k, init='k-means++', max_iter=200, n_init=10)
k_model.fit(embeddings)

# the labels given by clusters
labels=k_model.labels_

# adding the labels to the our original dataset
data['labels'] = pd.Categorical(labels)
data.head()

Давайте посмотрим, как даются «метки» по отношению к «Теме». Для этого мы создаем таблицу, которая включает перекрестные подсчеты групп «Тема» и «Ярлык».

# data.groupby(['labels','Subject']).size(): this groups the data by two columns 'labels' and 'Subject' and then it counts the number of occurances in each group.
# .unstack() : this unstacks the DataFrame, which means it pivots the dataframe from long format to wide format.
data_wide = data.groupby(['labels','Subject']).size().to_frame().unstack()
data_wide

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

5. График результатов

Наши векторизованные данные имеют большие размеры (384 столбца), и для их построения нам нужно уменьшить размеры.

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

В нашем наборе данных с 1205 точками данных, каждая из которых представлена ​​набором из 384 функций, было бы трудно визуализировать все эти функции одновременно и понять отношения между точками данных. t-SNE можно использовать для проецирования точек данных в 2D- или 3D-пространство, где вы можете увидеть кластеры похожих точек данных и понять их отношения.

Мы преобразуем две точки данных в 2D-пространство:

#reduce the dimension of data into two and then plot it
from sklearn.manifold import TSNE
X_embedded_2d = TSNE(n_components=2).fit_transform(embeddings)
# reduce the dimention to 3 for 3d plotting
X_embedded_3d = TSNE(n_components=3).fit_transform(embeddings)

Кроме того, мы можем использовать библиотеку `textero` для уменьшения размерности и интерактивного построения графиков.

import texthero as hero

# coverting the vectors to array
data_hero = data.copy()
data_hero['vectors'] = np.array(embeddings).tolist() 

# using TSNE method to reduce the dimensions
data_hero['tsnemodel'] = hero.tsne(data_hero['vectors'])

# plot the data
hero.scatterplot(data_hero, col='tsnemodel'
                 , color='labels'
                 , title="Course categories"
                 , hover_data = ['Subject','Name'])

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

import plotly.graph_objects as go  #for 3D plot
txt=[["Subject: " + data['Subject'][i], "Lables: " + str(data['labels'][i])] for i in range(len(data))]  

trace = go.Scatter3d(x=X_embedded_3d[:,0], y=X_embedded_3d[:,1], z=X_embedded_3d[:,2], 
                     mode='markers',
                     text=txt,
                     marker=dict(color = labels, size= 10, line=dict(color= 'black',width = 10)))
layout = go.Layout(margin=dict(l=0,r=0),height = 800,width = 800)
trace = [trace]
fig = go.Figure(data = trace, layout = layout)
fig.show()

6. Создание поискового индекса для рекомендации

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

  1. Инициализируйте новый индекс, передав «hnsw» в качестве метода и «cosinesimil» в качестве пробела.
  2. Добавьте вложения в индекс, используя метод addDataPointBatch().
  3. Создайте индекс с точками данных, используя createIndex method().
import nmslib
model_index = nmslib.init(method='hnsw', space='cosinesimil')
model_index.addDataPointBatch(embeddings)
model_index.createIndex({'post': 2})

Создание функции поиска для рекомендации
Наконец, мы создаем функцию поиска, чтобы найти лучшую бутылку, конечно, на основе индекса сходства между отзывами. Функция принимает два входа: DataFrame и UserQuery. Пользовательский запрос будет преобразован в вектор с помощью `model.encode()`, точно так же, как мы сделали для описаний курса. Затем можно использовать библиотеку *NMSLIB* для возврата k ближайших соседей (алгоритм классифицирует пользовательский ввод на основе меры подобия) вектора запроса пользователя. Мы можем установить k (количество соседей) равным 20.

def find_best_course(data, q_input):
    # Check if both inputs are not None
    if data is not None and q_input is not None:
        subset = data.copy()
        # Encode the q_input using the SBERT model
        query = model_SBERT.encode([q_input], convert_to_tensor=True)
        # Use the knnQuery function to find the 20 nearest neighbors to the encoded query
        ids, distances = model_index.knnQuery(query, k=20)
        
    # Initialize an empty list to store the best fits
    best_fits = []
    # Iterate through the returned ids and distances
    for i, j in zip(ids, distances):
        # Create a dictionary for each best fit containing information from the original data
        # Subject, Name, Description, cluster and distance returned by the knnQuery function
        best_fits.append({'Subject':subset.Subject.values[i]
                        , 'Name' : subset.Name.values[i]
                        , 'Description': subset.Description.values[i]
                        , 'cluster': subset.labels.values[i]
                        , 'distance': j
                       })
    return pd.DataFrame(best_fits)

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

most_similar=find_best_course(data, "exploring and writing about computer technologies")

# exteract the most similar cluster
most_similar[most_similar['cluster']==int(most_similar['cluster'].mode())]