Files
AIM-PIbd-31-Danilov-V-V/Lab8/Lab8.ipynb

300 KiB
Raw Blame History

Записки по ПМУ (кластеризация, антиплагиат)

Цель: Разработать модель, которая сможет автоматически анализировать текстовые данные, выделять ключевые особенности и сравнивать документы на предмет схожести содержания.

Методы: Для решения задачи будут использованы методы обработки естественного языка (NLP), включая предобработку текста, нормализацию, фильтрацию, векторизацию, а также методы машинного обучения для кластеризации документов.

In [18]:
import spacy
import num2words
import re
import unicodedata
import pandas as pd
from docx import Document
import os

sp = spacy.load("ru_core_news_lg")

def read_docx(file_path):
    try:
        doc = Document(file_path)
        full_text = []
        for paragraph in doc.paragraphs:
            full_text.append(paragraph.text)
        return "\n".join(full_text)
    except Exception as e:
        print(f"Файл {file_path} не подходит формату: {e}")
        return ""
def load_docs(dataset_path):
    df = pd.DataFrame(columns=["doc", "text"])
    for file_path in os.listdir(dataset_path):
        if file_path.startswith("~$"):
            continue
        text = read_docx(dataset_path + file_path)
        df.loc[len(df.index)] = [file_path, text]
    return df


df = load_docs("pmu/")
df["type"] = df.apply(
    lambda row: 0 if str(row["doc"]).startswith("tz_") else 1, axis=1
)
df.info()
df.sort_values(by=["doc"], inplace=True)
Файл pmu/31-МасенькинМС.pdf не подходит формату: Package not found at 'pmu/31-МасенькинМС.pdf'
Файл pmu/31-РазубаевСМ (1).pdf не подходит формату: Package not found at 'pmu/31-РазубаевСМ (1).pdf'
Файл pmu/31-ТерёхинАС.doc не подходит формату: Package not found at 'pmu/31-ТерёхинАС.doc'
Файл pmu/31-ШаныгинАВ (1).doc не подходит формату: Package not found at 'pmu/31-ШаныгинАВ (1).doc'
Файл pmu/32-КозловаАА (1).doc не подходит формату: Package not found at 'pmu/32-КозловаАА (1).doc'
Файл pmu/32-КозловаАА.doc не подходит формату: Package not found at 'pmu/32-КозловаАА.doc'
Файл pmu/32-ПучкинаАА (1).doc не подходит формату: Package not found at 'pmu/32-ПучкинаАА (1).doc'
Файл pmu/32-ПучкинаАА.doc не подходит формату: Package not found at 'pmu/32-ПучкинаАА.doc'
Файл pmu/32-СмирновАА.doc не подходит формату: Package not found at 'pmu/32-СмирновАА.doc'
Файл pmu/32-ЧубыкинаПП (1).doc не подходит формату: Package not found at 'pmu/32-ЧубыкинаПП (1).doc'
Файл pmu/32-ЧубыкинаПП.doc не подходит формату: Package not found at 'pmu/32-ЧубыкинаПП.doc'
Файл pmu/33-ПанинаАД (1).doc не подходит формату: Package not found at 'pmu/33-ПанинаАД (1).doc'
Файл pmu/33-СалинОА (1).doc не подходит формату: file 'pmu/33-СалинОА (1).doc' is not a Word file, content type is 'application/vnd.openxmlformats-officedocument.themeManager+xml'
Файл pmu/33-ТихоненковАЕ (1).doc не подходит формату: Package not found at 'pmu/33-ТихоненковАЕ (1).doc'
Файл pmu/PIbd33_Kislitsa_E_D.odt не подходит формату: "There is no item named '[Content_Types].xml' in the archive"
Файл pmu/Курсовая Данилов.pdf не подходит формату: Package not found at 'pmu/Курсовая Данилов.pdf'
<class 'pandas.core.frame.DataFrame'>
Index: 74 entries, 0 to 73
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   doc     74 non-null     object
 1   text    74 non-null     object
 2   type    74 non-null     int64 
dtypes: int64(1), object(2)
memory usage: 2.3+ KB

Предобработка текста включает в себя следующие этапы:

  1. Приведение текста к нижнему регистру.

  2. Удаление нежелательных символов (латиница, кириллица, пробелы).

  3. Нормализация Unicode (разбиение составных символов).

  4. Удаление комбинированных символов (акценты, диакритики).

  5. Замена чисел их текстовыми эквивалентами.

  6. Токенизация с удалением стоп-слов и знаков препинания.

In [19]:
def preprocess_text(text):
    text = text.lower()
    text = re.sub("[^a-zA-Zа-яА-Я ]", "", text)

    text = unicodedata.normalize("NFKD", text)
    text = "".join([char for char in text if not unicodedata.combining(char)])

    words: list[str] = text.split()
    words = [num2words(word, lang="ru") if word.isdigit() else word for word in words]
    text = " ".join(words)
    doc = sp(text)
    tokens = [token.text for token in doc if not token.is_stop and not token.is_punct]
    return " ".join(tokens)

df["preprocessed_text"] = df["text"].apply(preprocess_text)

Выделение частей речи

  1. Токенизация текста с помощью библиотеки spaCy.

  2. Для каждого токена в тексте извлекаются:

     text — сам токен (слово).
    
     pos — часть речи (например, существительное, глагол, прилагательное и т.д.).
    
     morph — морфологическая информация о токене (например, род, число, падеж и т.д.).
In [20]:
def morphological_analysis(text: str) -> list[dict]:
    doc = sp(text)
    tokens_info = [
        {
            "text": token.text,
            "pos": token.pos_,
            "morph": token.morph,
        }
        for token in doc
    ]
    return tokens_info

#df["pos_tagging_text"] = df["preprocessed_text"].apply(morphological_analysis)
#print(df.head)

Нормализация текста(Лемматизация)

  1. Токенизация текста с помощью spaCy, создание объекта doc.

  2. Для каждого токена из объекта doc извлекается его лемма (нормальная форма слова) с помощью атрибута lemma_.

  3. Леммы всех токенов соединяются в одну строку через пробел.

In [ ]:
from spacy.tokens.doc import Doc

def lemmatize_text(text: str) -> str:
    doc = sp(text)
    lemmas = [token.lemma_ for token in doc]
    return " ".join(lemmas)

df["normalized_text"] = df["preprocessed_text"].apply(lemmatize_text)

Фильтрация(длина от 5 до 15 символов)

  1. Текст разбивается на слова.

  2. Только те слова, которые соответствуют этому условию, сохраняются.

  3. Отфильтрованные слова соединяются обратно в строку с пробелами между ними.

In [22]:
def filter_text(text: str, min_length: int = 5, max_length: int = 15) -> str:
    words = text.split()
    filtered_words = [
        word for word in words 
        if min_length <= len(word) <= max_length
    ]
    return " ".join(filtered_words)

df["filtered_text"] = df["normalized_text"].apply(filter_text)

Формирование N-грамм

N-грамма это последовательность из N элементов (слов, букв или других символов) в тексте.

Виды N-грамм:

  1. Униграммы (Unigrams, N=1) последовательности из одного элемента (слова).
  2. Биграммы (Bigrams, N=2) последовательности из двух элементов.
  3. Триграммы (Trigrams, N=3) последовательности из трех элементов.
  4. N-граммы (для N > 3) последовательности из N элементов.
In [23]:
import nltk
from nltk.util import ngrams
from nltk.tokenize import word_tokenize

def generate_ngrams(text: str, n: int = 2) -> list[tuple]:
    tokens = word_tokenize(text, language="russian")
    n_grams: list[tuple] = list(ngrams(tokens, n))
    return n_grams

df["trigrams"] = df["filtered_text"].apply(lambda x: generate_ngrams(x, n=3))

Векторизация. Метод мешков

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

Метод мешка слов (Bag of Words, BoW) это простой и популярный способ векторизации текста. Основная идея заключается в том, что текст представляется как "мешок" (набор) слов без учета их порядка и грамматики. Каждое слово в тексте рассматривается как отдельная характеристика, а текст представляется в виде вектора, где каждый элемент соответствует частоте (или наличию) определенного слова.

In [24]:
import pandas as pd
from scipy import sparse
from sklearn.feature_extraction.text import CountVectorizer

counts_vectorizer = CountVectorizer()
counts_matrix = sparse.csr_matrix(counts_vectorizer.fit_transform(df["filtered_text"]))
counts_df = pd.DataFrame(
    counts_matrix.toarray(),
    columns=counts_vectorizer.get_feature_names_out(),
)

Векторизаций. Частотный портрет

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

  1. TF (Term Frequency) частота слова в тексте.
  2. IDF (Inverse Document Frequency) обратная частота документа, которая уменьшает вес слов, часто встречающихся в корпусе (например, стоп-слов).
In [25]:
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vectorizer = TfidfVectorizer(sublinear_tf=True)
tfidf_matrix = sparse.csr_matrix(tfidf_vectorizer.fit_transform(df["filtered_text"]))
tfidf_df = pd.DataFrame(
    tfidf_matrix.toarray(),
    columns=tfidf_vectorizer.get_feature_names_out(),
)

Метод локтя

Метод локтя это способ определения оптимального числа кластеров k в алгоритме K-Means. Он основывается на анализе инерции (суммы квадратов расстояний внутри кластеров).

Принцип работы алгоритма:

  1. Вычисление инерции: Для различных значений k вычисляется инерция W(k).
  2. Построение графика: Строится график зависимости инерции от числа кластеров k.
  3. Поиск "локтя": На графике ищется точка, где снижение инерции начинает замедляться. Эта точка указывает на оптимальное количество кластеров.
In [26]:
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

wcss = []
for i in range(1, 51):
    kmeans = KMeans(n_clusters=i, random_state=42)
    kmeans.fit(tfidf_matrix)
    wcss.append(kmeans.inertia_)

plt.plot(range(1, 51), wcss, marker='o')
plt.title('Метод локтя')
plt.xlabel('Число кластеров')
plt.ylabel('Сумма квадратов ошибок')
plt.show()
c:\Users\danil\AppData\Local\Programs\Python\Python312\Lib\site-packages\joblib\externals\loky\backend\context.py:136: UserWarning: Could not find the number of physical cores for the following reason:
[WinError 2] Не удается найти указанный файл
Returning the number of logical cores instead. You can silence this warning by setting LOKY_MAX_CPU_COUNT to the number of cores you want to use.
  warnings.warn(
  File "c:\Users\danil\AppData\Local\Programs\Python\Python312\Lib\site-packages\joblib\externals\loky\backend\context.py", line 257, in _count_physical_cores
    cpu_info = subprocess.run(
               ^^^^^^^^^^^^^^^
  File "c:\Users\danil\AppData\Local\Programs\Python\Python312\Lib\subprocess.py", line 548, in run
    with Popen(*popenargs, **kwargs) as process:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\danil\AppData\Local\Programs\Python\Python312\Lib\subprocess.py", line 1026, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "c:\Users\danil\AppData\Local\Programs\Python\Python312\Lib\subprocess.py", line 1538, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
No description has been provided for this image

Коэффициент силуэта

Коэффициент силуэта это метрика для оценки качества кластеризации, варьирующаяся от -1 до 1:

  1. 1: Объект хорошо кластеризован, далеко от других кластеров.
  2. 0: Объект на границе между кластерами.
  3. -1: Объект, вероятно, неправильно классифицирован.
In [27]:
from sklearn.metrics import silhouette_score
silhouette_scores = []
for i in range(2, 51): 
    kmeans = KMeans(n_clusters=i, random_state=42)
    kmeans.fit(tfidf_matrix)
    score = silhouette_score(tfidf_matrix, kmeans.labels_, random_state=42)
    silhouette_scores.append(score)
    #print(f"Число кластеров: {i}, Коэффициент силуэта: {score:.4f}")

plt.plot(range(2, 51), silhouette_scores, marker='o')
plt.title('Метод силуэта')
plt.xlabel('Число кластеров')
plt.ylabel('Silhouette Score')
plt.show()
No description has been provided for this image

Кластеризация с использованием Kmeans

In [28]:
from sklearn.cluster import KMeans

n_clusters = 40
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
kmeans.fit(tfidf_matrix)
df["cluster"] = kmeans.labels_
display(df[["doc", "cluster"]])
<style scoped=""> .dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } </style>
doc cluster
0 31-АнисинРС.docx 15
1 31-АфанасьевСС.docx 2
2 31-БакальскаяЕД.docx 12
3 31-БарсуковПО.docx 20
4 31-БелянинНН.docx 34
... ... ...
69 Курсовая ПМУ Волков Никита ПИбд-33.docx 7
70 ПИбд-31, Лёвушкина Анна, записка к кр.docx 10
71 ПИбд-32 Шабунов Олег.docx 1
72 Фирсов_Кирилл_Записка (1).docx 3
73 Фирсов_Кирилл_Записка.docx 3

74 rows × 2 columns

Визуализация с помощь PCA

In [29]:
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

n_clusters = 40
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
clusters = kmeans.fit_predict(tfidf_matrix)


pca = PCA(n_components=2)
X_pca = pca.fit_transform(tfidf_matrix.toarray())

# 
plt.figure(figsize=(12, 10))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap="viridis", s=50, alpha=0.6)


centers = pca.transform(kmeans.cluster_centers_)
plt.scatter(centers[:, 0], centers[:, 1], c="red", s=200, alpha=0.75, marker="X")

for i, doc in enumerate(df["doc"]):
    plt.text(X_pca[i, 0], X_pca[i, 1], doc, fontsize=8, alpha=0.8)

plt.title("Визуализация кластеров текстов с использованием PCA")
plt.xlabel("Компонента PCA 1")
plt.ylabel("Компонента PCA 2")
plt.colorbar(label="Кластер")
plt.show()
No description has been provided for this image