Files
AIM-PIbd-31-Potapov-N-S/lab_8/main.ipynb
2025-04-12 14:49:44 +04:00

150 KiB
Raw Permalink Blame History

Импорт библиотек

In [125]:
import os
import re
import numpy as np
import pandas as pd
from tqdm import tqdm

# Работа с .docx
import docx
import textract

# Обработка текста
import spacy
import nltk
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import TfidfVectorizer

# Инициализация
nltk.download('punkt')
nltk.download('stopwords')
nlp = spacy.load("ru_core_news_sm")
[nltk_data] Downloading package punkt to /home/vscode/nltk_data...
[nltk_data]   Package punkt is already up-to-date!
[nltk_data] Downloading package stopwords to /home/vscode/nltk_data...
[nltk_data]   Package stopwords is already up-to-date!

1. Загрузка данных

In [107]:
def extract_text_from_docx(path):
    doc = docx.Document(path)
    return "\n".join([p.text for p in doc.paragraphs])

def extract_text_from_doc(path):
    return textract.process(path).decode('utf-8')

# Сбор всех документов
folder_path = '../data/pmu/'
documents = []
filenames = []

for filename in tqdm(os.listdir(folder_path)):
    full_path = os.path.join(folder_path, filename)
    try:
        if filename.endswith('.docx'):
            text = extract_text_from_docx(full_path)
        elif filename.endswith('.doc'):
            text = extract_text_from_doc(full_path)
        else:
            continue
        documents.append(text)
        filenames.append(filename)
    except Exception as e:
        print(f"Ошибка при обработке {filename}: {e}")

df = pd.DataFrame({'filename': filenames, 'text': documents})
df.head()
100%|██████████| 70/70 [00:26<00:00,  2.62it/s]
Out[107]:
<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>
filename text
0 31-АнисинРС.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС...
1 31-АфанасьевСС.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС...
2 31-БакальскаяЕД.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС...
3 31-БарсуковПО.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС...
4 31-БелянинНН.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС...

2. Предобработка текста

In [124]:
def clean_text(text):
    text = text.lower()
    # Удаление латиницы
    text = re.sub(r"[a-zA-Z]+", "", text)
    # Удаление спецсимволов и чисел
    text = re.sub(r"[^а-яА-ЯёЁ\s]", "", text)
    return text

df['clean_text'] = df['text'].apply(clean_text)

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

In [109]:
stop_words = set(stopwords.words('russian'))

def preprocess_spacy(text):
    doc = nlp(text)
    return " ".join([token.lemma_ for token in doc if token.is_alpha and token.text not in stop_words])

tqdm.pandas()
df['lemmatized'] = df['clean_text'].progress_apply(preprocess_spacy)
100%|██████████| 70/70 [02:21<00:00,  2.02s/it]

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

In [123]:
from scipy import sparse

vectorizer = TfidfVectorizer()
counts_matrix = sparse.csr_matrix(vectorizer.fit_transform(df["lemmatized"]))
words = vectorizer.get_feature_names_out()
df["vector"] = df.apply(lambda row: counts_matrix.toarray()[row.name], axis=1)

X = np.vstack(df["vector"].values)  # Объединяем в единый массив

df
Out[123]:
<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>
filename text clean_text lemmatized vector cluster
0 31-АнисинРС.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС... \nминистерство науки и высшего образования рос... министерство наука высокий образование российс... [0.005608031637124367, 0.0, 0.0, 0.0, 0.002925... 22
1 31-АфанасьевСС.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС... \nминистерство науки и высшего образования рос... министерство наука высокий образование российс... [0.0035489013106689777, 0.0, 0.0, 0.0, 0.00370... 1
2 31-БакальскаяЕД.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС... \nминистерство науки и высшего образования рос... министерство наука высокий образование российс... [0.005985538687327219, 0.0, 0.0, 0.0, 0.009368... 25
3 31-БарсуковПО.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС... \nминистерство науки и высшего образования рос... министерство наука высокий образование российс... [0.005431808560115705, 0.0, 0.0, 0.0, 0.002833... 33
4 31-БелянинНН.docx \nМИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОС... \nминистерство науки и высшего образования рос... министерство наука высокий образование российс... [0.004966518090921509, 0.0, 0.0, 0.0, 0.002591... 12
... ... ... ... ... ... ...
65 33-Фирсов (1).docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... министерство науки и высшего образования росси... министерство наука высокий образование российс... [0.011411730273680816, 0.0, 0.0, 0.0, 0.015876... 10
66 33-Фирсов.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... министерство науки и высшего образования росси... министерство наука высокий образование российс... [0.011411730273680816, 0.0, 0.0, 0.0, 0.015876... 10
67 33-ЮнусовНН (1).docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... министерство науки и высшего образования росси... министерство наука высокий образование российс... [0.010977516764714526, 0.0, 0.0, 0.0, 0.015272... 7
68 33-ЯкобчукСВ.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... министерство науки и высшего образования росси... министерство наука высокий образование российс... [0.0070046267489162085, 0.0, 0.0, 0.0, 0.01461... 3
69 33-ЯшинАА (5).docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... министерство науки и высшего образования росси... министерство наука высокий образование российс... [0.006410091805019992, 0.0, 0.0, 0.0, 0.013376... 7

70 rows × 6 columns

5. Кластеризация: KMeans

In [126]:
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import TruncatedSVD
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# 1. Поиск оптимального k (число кластеров)
inertia = []
silhouette_scores = []
K = range(2, len(df) // 2)  # Пробуем разное кол-во кластеров

for k in K:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    inertia.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X, kmeans.labels_))

# 2. Выбор лучшего k по метрике силуэта
best_k = K[np.argmax(silhouette_scores)]

print(f"Оптимальное количество кластеров: {best_k}")

# 3. Повторная кластеризация с оптимальным числом кластеров
kmeans = KMeans(n_clusters=best_k, random_state=42)
clusters = kmeans.fit_predict(X)
df['cluster'] = clusters

# 4. Визуализация: метод локтя и силуэт
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.plot(K, inertia, marker='o')
plt.xlabel('Количество кластеров')
plt.ylabel('Inertia')
plt.title('Метод локтя')

plt.subplot(1, 2, 2)
plt.plot(K, silhouette_scores, marker='o')
plt.xlabel('Количество кластеров')
plt.ylabel('Silhouette Score')
plt.title('Silhouette-анализ')

plt.tight_layout()
plt.show()

# 5. Визуализация кластеров (через SVD)
svd = TruncatedSVD(n_components=2)
reduced = svd.fit_transform(X)

plt.figure(figsize=(8,6))
sns.scatterplot(x=reduced[:,0], y=reduced[:,1], hue=clusters, palette='tab10')
plt.title(f'Кластеризация с KMeans (k={best_k})')
plt.show()
Оптимальное количество кластеров: 22
No description has been provided for this image
No description has been provided for this image

Выводим список работ по кластерам

In [127]:
sims = df[['filename', 'cluster']]

for sim_cluster in range(best_k):
    print('Cluster', sim_cluster)
    sim_files = sims.loc[sims['cluster'] == sim_cluster]
    print(*sim_files['filename'], sep='\n')
    print()
Cluster 0
31-ЖирноваАЕ(1).docx
32-БулатоваКР.docx
32-КазначееваЕК.docx

Cluster 1
31-БелянинНН.docx
31-Блохин.docx
31-КозыревСС.docx
31-ЛобашовИД.docx
31-МалафеевЛС.docx
31-СагировММ (1).docx
31-ТабеевАП.docx
31-ЯковлевМГ.docx
32-ПетрушинЕА.docx.docx

Cluster 2
31-ИзотовАП.docx
32-ТёркинДВ.docx
33-РадаевАВ.docx
33-ЮнусовНН (1).docx

Cluster 3
31-АнисинРС.docx
31-Макаров.docx

Cluster 4
31-ШаныгинАВ (1).doc
32-КозловаАА (1).doc
32-КозловаАА.doc

Cluster 5
33-ДьяконовРР.docx
33-ИвановаСВ.docx
33-ЯшинАА (5).docx

Cluster 6
31-АфанасьевСС.docx
31-КрюковА.docx
31-КрюковАИ (1).docx
31-МедведковАД.docx
32-КамчароваКА (1).docx
32-КузинПС (1).docx
32-ПучкинаАА (1).doc
32-ПучкинаАА.doc
32-СафиуловаКН.docx
32-ФедоренкоГЮ.docx

Cluster 7
33-ВражкинС.docx
33-ИвановВН (2).docx
33-СтаростинИК.docx
33-Фирсов (1).docx
33-Фирсов.docx

Cluster 8
32-ИсаеваИА.docx
33-ДолговаДН.docx

Cluster 9
31-БакальскаяЕД.docx

Cluster 10
31-ЛевушкинаА.docx
33-ЯкобчукСВ.docx

Cluster 11
32-ЧубыкинаПП (1).doc
32-ЧубыкинаПП.doc

Cluster 12
31-ИевлеваМД (1).docx

Cluster 13
31-ЯрускинСА.docx
33-ЛеонтьеваВА (1).docx

Cluster 14
31-КувшиновТА.docx

Cluster 15
33-ВолковН (1).docx
33-ВолковН.docx

Cluster 16
31-ПотаповНС.docx
31-ПятаковКМ (1).docx
32-ГеримовичИМ.docx
32-СмирновАА.doc
32-СтроевВМ.docx
32-ШабуновО.docx
33-ВаловаАД.docx
33-ЗахаровРА (1).docx
33-НасыровАГ (1).docx
33-ПанинаАД (1).doc
33-РазинАА.docx
33-СалинОА (1).doc

Cluster 17
31-ТерёхинАС.doc

Cluster 18
32-ЧернышевГЯ.docx
33-БакшаеваЕА.docx

Cluster 19
32-КатышеваНЕ (1).docx

Cluster 20
31-БарсуковПО.docx

Cluster 21
33-ТихоненковАЕ (1).doc

Вычислим косинусное расстояние для схожих работ

In [139]:
from sklearn.metrics.pairwise import cosine_similarity

SIMILARITY_THRESHOLD = 0.8

# Для каждого кластера:
for cluster_id in sorted(df['cluster'].unique()):    
    cluster_indices = df[df['cluster'] == cluster_id].index
    cluster_texts = df.loc[cluster_indices, 'filename'].values
    cluster_vectors = X[cluster_indices]

    # Вычислим попарное сходство
    similarity_matrix = cosine_similarity(cluster_vectors)

    # Найдём наиболее похожие пары документов
    n_top_pairs = 10  # можно увеличить
    counted = 0
    printed_pairs = set()

    for i in range(len(cluster_indices)):
        for j in range(i + 1, len(cluster_indices)):
            sim = similarity_matrix[i, j]
            if sim > SIMILARITY_THRESHOLD:  # порог похожести
                doc1 = cluster_texts[i].replace('\n', ' ')
                doc2 = cluster_texts[j].replace('\n', ' ')
                pair_id = (min(i,j), max(i,j))
                if pair_id not in printed_pairs:
                    if counted == 0:
                        print(f"\n🔷 Кластер {cluster_id} — похожие документы:\n")
                    print(f"🔸 Пара {counted + 1} (похожесть: {sim:.2f}):")
                    print(f"   📄 Документ 1: {doc1}")
                    print(f"   📄 Документ 2: {doc2}\n")
                    counted += 1
                    printed_pairs.add(pair_id)
                    if counted >= n_top_pairs:
                        break
        if counted >= n_top_pairs:
            break
🔷 Кластер 0 — похожие документы:

🔸 Пара 1 (похожесть: 0.88):
   📄 Документ 1: 31-ЖирноваАЕ(1).docx
   📄 Документ 2: 32-БулатоваКР.docx

🔸 Пара 2 (похожесть: 0.88):
   📄 Документ 1: 31-ЖирноваАЕ(1).docx
   📄 Документ 2: 32-КазначееваЕК.docx

🔸 Пара 3 (похожесть: 0.93):
   📄 Документ 1: 32-БулатоваКР.docx
   📄 Документ 2: 32-КазначееваЕК.docx


🔷 Кластер 2 — похожие документы:

🔸 Пара 1 (похожесть: 0.92):
   📄 Документ 1: 31-ИзотовАП.docx
   📄 Документ 2: 32-ТёркинДВ.docx


🔷 Кластер 3 — похожие документы:

🔸 Пара 1 (похожесть: 0.87):
   📄 Документ 1: 31-АнисинРС.docx
   📄 Документ 2: 31-Макаров.docx


🔷 Кластер 4 — похожие документы:

🔸 Пара 1 (похожесть: 0.90):
   📄 Документ 1: 31-ШаныгинАВ (1).doc
   📄 Документ 2: 32-КозловаАА (1).doc

🔸 Пара 2 (похожесть: 0.90):
   📄 Документ 1: 31-ШаныгинАВ (1).doc
   📄 Документ 2: 32-КозловаАА.doc

🔸 Пара 3 (похожесть: 1.00):
   📄 Документ 1: 32-КозловаАА (1).doc
   📄 Документ 2: 32-КозловаАА.doc


🔷 Кластер 5 — похожие документы:

🔸 Пара 1 (похожесть: 0.84):
   📄 Документ 1: 33-ДьяконовРР.docx
   📄 Документ 2: 33-ИвановаСВ.docx


🔷 Кластер 6 — похожие документы:

🔸 Пара 1 (похожесть: 0.80):
   📄 Документ 1: 31-АфанасьевСС.docx
   📄 Документ 2: 31-МедведковАД.docx

🔸 Пара 2 (похожесть: 1.00):
   📄 Документ 1: 31-КрюковА.docx
   📄 Документ 2: 31-КрюковАИ (1).docx

🔸 Пара 3 (похожесть: 0.88):
   📄 Документ 1: 31-КрюковА.docx
   📄 Документ 2: 31-МедведковАД.docx

🔸 Пара 4 (похожесть: 0.89):
   📄 Документ 1: 31-КрюковАИ (1).docx
   📄 Документ 2: 31-МедведковАД.docx

🔸 Пара 5 (похожесть: 0.80):
   📄 Документ 1: 31-КрюковАИ (1).docx
   📄 Документ 2: 32-КамчароваКА (1).docx

🔸 Пара 6 (похожесть: 0.80):
   📄 Документ 1: 31-КрюковАИ (1).docx
   📄 Документ 2: 32-КузинПС (1).docx

🔸 Пара 7 (похожесть: 0.88):
   📄 Документ 1: 31-МедведковАД.docx
   📄 Документ 2: 32-КамчароваКА (1).docx

🔸 Пара 8 (похожесть: 0.89):
   📄 Документ 1: 31-МедведковАД.docx
   📄 Документ 2: 32-КузинПС (1).docx

🔸 Пара 9 (похожесть: 0.82):
   📄 Документ 1: 31-МедведковАД.docx
   📄 Документ 2: 32-ПучкинаАА (1).doc

🔸 Пара 10 (похожесть: 0.82):
   📄 Документ 1: 31-МедведковАД.docx
   📄 Документ 2: 32-ПучкинаАА.doc


🔷 Кластер 7 — похожие документы:

🔸 Пара 1 (похожесть: 1.00):
   📄 Документ 1: 33-Фирсов (1).docx
   📄 Документ 2: 33-Фирсов.docx


🔷 Кластер 10 — похожие документы:

🔸 Пара 1 (похожесть: 0.83):
   📄 Документ 1: 31-ЛевушкинаА.docx
   📄 Документ 2: 33-ЯкобчукСВ.docx


🔷 Кластер 11 — похожие документы:

🔸 Пара 1 (похожесть: 1.00):
   📄 Документ 1: 32-ЧубыкинаПП (1).doc
   📄 Документ 2: 32-ЧубыкинаПП.doc


🔷 Кластер 15 — похожие документы:

🔸 Пара 1 (похожесть: 1.00):
   📄 Документ 1: 33-ВолковН (1).docx
   📄 Документ 2: 33-ВолковН.docx