Files
AIM-PIbd-31-Yaruskin-S-A/lab_8/laba8.ipynb
2025-05-23 21:07:22 +04:00

5.0 MiB
Raw Blame History

Лаба 8. Обработка текста

Буду использовать записки по ПМУ, которые были предоставлены к этой лабе.

In [60]:
import os
import pandas as pd
import spacy
from docx import Document
from docx.document import Document as DocumentType
from pandas import DataFrame

# читаем текст из файла
def read_docx(file_path: str) -> str:
    try:
        if not os.path.isfile(file_path):
            raise FileNotFoundError(f"Файл {file_path} не найден")

        document = Document(file_path)
        full_text: list[str] = [paragraph.text.strip() for paragraph in document.paragraphs if paragraph.text.strip()]
        return "\n".join(full_text)
    
    except Exception as e:
        print(f"Ошибка при чтении {file_path}: {e}")
        return ""
    
# загружаем текст из файлов
def load_docx(dataset_path: str) -> DataFrame:
    if not os.path.isdir(dataset_path):
        raise NotADirectoryError(f"Директория {dataset_path} не найдена")

    documents = []
    for file_name in sorted(os.listdir(dataset_path)):
        if file_name.startswith("~$") or not file_name.lower().endswith(".docx"):
            continue
        file_path = os.path.join(dataset_path, file_name)
        text = read_docx(file_path)
        if text:
            doc_type = 0 if file_name.startswith("tz_") else 1
            documents.append((file_name, text, doc_type))
        
    df = pd.DataFrame(documents, columns=["doc", "text", "type"])
    df.sort_values(by="doc", inplace=True)
    return df

# Пути
project_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
dataset_path: str = os.path.join(project_root, "static", "pmu")

try:
    df = load_docx(dataset_path)
    df.info()
    display(df.head())
except Exception as e:
    print(f"Ошибка загрузки данных: {e}")
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 58 entries, 0 to 57
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   doc     58 non-null     object
 1   text    58 non-null     object
 2   type    58 non-null     int64 
dtypes: int64(1), object(2)
memory usage: 1.5+ KB
<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 text type
0 31-АнисинРС.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1
1 31-АфанасьевСС.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1
2 31-БакальскаяЕД.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1
3 31-БарсуковПО.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1
4 31-БелянинНН.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1

Данные загружены.

Нужно их обработать. В этапы будут включены:

  • Трансформация
  • Токенизация
  • Выделение частей речи
  • Нормализация
  • Фильтрация
  • Сначала:

  • Приведем текст к нижнему регистру
  • Заменим переносы на пробелы
  • Удалим все символы, оставив только буквы и пробелы
  • Преобразуем числа в слова
  • Токанизируем с помощью библиотеки Spacy
  • Удалим стоп-слова и пунктуацию
  • In [61]:
    import nltk
    import re
    from typing import Set
    from num2words import num2words
    from nltk.corpus import stopwords
    from spacy.tokens.doc import Doc
    
    # Скачал модель Spacy для русского языка
    spacy = spacy.load("ru_core_news_lg")
    
    nltk.download("punkt")
    nltk.download("stopwords")
    stop_woards = set(stopwords.words("russian"))
    
    stop_woards: Set[str] = spacy.Defaults.stop_words
    
    # Метод для преобработки
    def preprocess_text(text):
        text = text.lower()
        text = text.replace("\n", " ")
        text = re.sub(r"[^a-zA-Za-яА-Я ]", "", text)
        words: list[str] = text.split()
        words = [num2words(word, lang="ru") if word.isdigit() else word for word in words]
        text = " ".join(words)
        document = spacy(text)
        tokens = [token.text for token in document if token.text not in stop_woards and not token.is_stop 
                  and not token.is_punct and not token.is_digit]
        
        return " ".join(tokens)
    
    df["preprocessed_text"] = df["text"].apply(preprocess_text)
    
    [nltk_data] Downloading package punkt to
    [nltk_data]     C:\Users\salih\AppData\Roaming\nltk_data...
    [nltk_data]   Package punkt is already up-to-date!
    [nltk_data] Downloading package stopwords to
    [nltk_data]     C:\Users\salih\AppData\Roaming\nltk_data...
    [nltk_data]   Package stopwords is already up-to-date!
    

    Выделение частей речи (POS tagging)

    Это позволит понять структуру слова и его роль в предложении
    In [62]:
    # Функция для морфологического анализа  
    def morphological_analysis(text: str) -> list[dict]:
        document = spacy(text)
        # Сбор информации о каждом токене: текст, лемма, часть речи, морфологические признаки
        tokens_info: list[dict] = [
            {
                "text": token.text,
                "lemma": token.lemma_,
                "pos": token.pos_,
                "morph": token.morph,
    
            }
            for token in document
        ]
        
        return tokens_info
    
    
    df["morphological_info"] = df["preprocessed_text"].apply(morphological_analysis)
    

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


    Буду использоваться лемматизацию для приведения слов к начальной форме. Стемминг я использовать не стал.
    In [63]:
    def lemmatization_text(text: str) -> str:
        doc = spacy(text)
        lemmas = [token.lemma_ for token in doc if not token.is_punct and not token.is_space]
        return " ".join(lemmas)
    
    df["lemmatized_text"] = df["preprocessed_text"].apply(lemmatization_text)
    

    Фильтрация текста

    Сокращаю пространства признаков за счет удаления слов.
    Стоп слова и различнеы символы кроме слов были удалены до этого.
    Сейчас удалю слова, где длина либо меньше минимального, либо больше
    In [64]:
    # Функция фильтрации текста
    def filter_text(text: str, min_length: int = 3, max_length: int = 15) -> str:
        words = text.split()
        return " ".join(word for word in words if min_length <= len(word) <= max_length)
    
    df["filtered_text"] = df["lemmatized_text"].apply(filter_text)
    display(df.head())
    
    <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 text type preprocessed_text morphological_info lemmatized_text filtered_text
    0 31-АнисинРС.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1 министерство науки высшего образования российс... [{'text': 'министерство', 'lemma': 'министерст... министерство наука высокий образование российс... министерство наука высокий образование российс...
    1 31-АфанасьевСС.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1 министерство науки высшего образования российс... [{'text': 'министерство', 'lemma': 'министерст... министерство наука высокий образование российс... министерство наука высокий образование российс...
    2 31-БакальскаяЕД.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1 министерство науки высшего образования российс... [{'text': 'министерство', 'lemma': 'министерст... министерство наука высокий образование российс... министерство наука высокий образование российс...
    3 31-БарсуковПО.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1 министерство науки высшего образования российс... [{'text': 'министерство', 'lemma': 'министерст... министерство наука высокий образование российс... министерство наука высокий образование российс...
    4 31-БелянинНН.docx МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИ... 1 министерство науки высшего образования российс... [{'text': 'министерство', 'lemma': 'министерст... министерство наука высокий образование российс... министерство наука высокий образование российс...

    Создание N-грамм

    N-граммы позволяют сгруппировать слова текста по их взаимному расположению
    Я буду использовать биграммы и триграммы
    In [65]:
    from nltk.util import ngrams
    from nltk.tokenize import word_tokenize
    
    # Загрузка ресурсов для токенизации
    nltk.download("punkt")
    nltk.download("punkt_tab")
    
    def generate_ngrams(text: str, n: int = 2) -> list[tuple]:
        # Токенизация текста
        tokens = word_tokenize(text, language="russian")
    
        # Формирование N-грамм
        n_grams: list[tuple] = list(ngrams(tokens, n))
        return n_grams
    
    df["bigrams"] = df["filtered_text"].apply(lambda x: generate_ngrams(x, n=2))
    
    df["trigrams"] = df["filtered_text"].apply(lambda x: generate_ngrams(x, n=3))
    
    [nltk_data] Downloading package punkt to
    [nltk_data]     C:\Users\salih\AppData\Roaming\nltk_data...
    [nltk_data]   Package punkt is already up-to-date!
    [nltk_data] Downloading package punkt_tab to
    [nltk_data]     C:\Users\salih\AppData\Roaming\nltk_data...
    [nltk_data]   Package punkt_tab is already up-to-date!
    

    Векторизация с использованием мешка слов (BoW)

    Я буду использова мешок слов. Я думаю, что для тематике моих документов этого подхода вполне хватит
    In [66]:
    from typing import Any, Union
    
    from numpy import ndarray
    from scipy.sparse._matrix import spmatrix
    from sklearn.feature_extraction.text import CountVectorizer
    
    # Создадим объект от CountVectorizer
    counter_vectorizer = CountVectorizer()
    
    # Применяем векторизацию к текстам отфильтрованным
    counts_matrix: Union[ndarray[Any, Any], spmatrix] = counter_vectorizer.fit_transform(df["filtered_text"])
    
    # Преобразуем результат в DataFrame для удобства
    counter_df = pd.DataFrame(counts_matrix.toarray(), columns=counter_vectorizer.get_feature_names_out())
    

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

    Также использую PCA, которая понижает размерность из ~1000 слов до 2 чисел, чтобы можно было визуализировать на 2D-графике
    In [ ]:
    from sklearn.cluster import KMeans
    from sklearn.decomposition import PCA
    
    n_clusters = 27
    kmeans = KMeans(n_clusters=n_clusters, random_state=42)
    
    kmeans.fit(counts_matrix)
    
    df["cluster"] = kmeans.labels_
    
    # display(df[["doc", "cluster"]])
    
    In [68]:
    from sklearn.decomposition import PCA
    import seaborn as sns
    import matplotlib.pyplot as plt
    
    # Уменьшаем размерность до 2D с помощью PCA
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(counts_matrix.toarray())
    
    clusters = kmeans.labels_
    
    cluster_df = pd.DataFrame(X_pca, columns=["PCA1", "PCA2"])
    cluster_df["Cluster"] = clusters
    cluster_df["Document"] = df["doc"]
    
    plt.figure(figsize=(14, 10))
    palette = sns.color_palette("viridis", n_colors=n_clusters)
    sns.scatterplot(
        x="PCA1",
        y="PCA2",
        hue="Cluster",
        palette=palette,
        data=cluster_df,
        legend="full",
        alpha=0.8
    )
    
    for i in range(len(cluster_df)):
        plt.text(
            cluster_df["PCA1"].iloc[i],
            cluster_df["PCA2"].iloc[i],
            cluster_df["Document"].iloc[i][:15],
            fontsize=8,
            alpha=0.75
        )
    
    plt.title("Кластеризация текстов с PCA")
    plt.xlabel("PCA Компонента 1")
    plt.ylabel("PCA Компонента 2")
    plt.legend(title="Кластеры")
    plt.grid(True)
    plt.show()
    
    No description has been provided for this image

    Странно выглядит, поэтому попробую воспользоваться TF-IDF (частным портретом)

    In [69]:
    from sklearn.feature_extraction.text import TfidfVectorizer
    
    # Создание объекта TfidVectorizer
    tfidf_vectorizer = TfidfVectorizer(max_df=0.85, sublinear_tf=True)
    
    tfidf_matrix: spmatrix = tfidf_vectorizer.fit_transform(df["filtered_text"])
    
    tfidf_df = pd.DataFrame(tfidf_matrix.toarray(), columns=tfidf_vectorizer.get_feature_names_out())
    
    In [70]:
    n_clusters = 27
    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 14
    1 31-АфанасьевСС.docx 6
    2 31-БакальскаяЕД.docx 12
    3 31-БарсуковПО.docx 17
    4 31-БелянинНН.docx 20
    5 31-ЖирноваАЕ(1).docx 18
    6 31-ИевлеваМД (1).docx 25
    7 31-КозыревСС.docx 14
    8 31-КрюковАИ (1).docx 1
    9 31-КувшиновТА.docx 14
    10 31-ЛобашовИД.docx 6
    11 31-МалафеевЛС.docx 17
    12 31-ПотаповНС.docx 12
    13 31-ПятаковКМ (1).docx 21
    14 31-СагировММ (1).docx 17
    15 31-ТабеевАП.docx 5
    16 31-ЯковлевМГ.docx 5
    17 31-ЯрускинСА.docx 5
    18 32-БулатоваКР.docx 0
    19 32-ГеримовичИМ.docx 13
    20 32-ИсаеваИА.docx 14
    21 32-КазначееваЕК.docx 0
    22 32-КамчароваКА (1).docx 4
    23 32-КатышеваНЕ (1).docx 10
    24 32-КузинПС (1).docx 4
    25 32-ПетрушинЕА.docx.docx 22
    26 32-СафиуловаКН.docx 19
    27 32-СтроевВМ.docx 16
    28 32-ТёркинДВ.docx 14
    29 32-ФедоренкоГЮ.docx 24
    30 32-ЧернышевГЯ.docx 11
    31 33-БакшаеваЕА.docx 17
    32 33-ВаловаАД.docx 14
    33 33-ДолговаДН.docx 15
    34 33-ДьяконовРР.docx 2
    35 33-ЗахаровРА (1).docx 8
    36 33-ИвановВН (2).docx 15
    37 33-ИвановаСВ.docx 2
    38 33-ЛеонтьеваВА (1).docx 8
    39 33-НасыровАГ (1).docx 24
    40 33-РадаевАВ.docx 26
    41 33-РазинАА.docx 24
    42 33-СтаростинИК.DOCX 5
    43 33-ЮнусовНН (1).docx 16
    44 33-ЯкобчукСВ.docx 26
    45 33-ЯшинАА (5).docx 23
    46 CourseWork_PMU_Izotov_A_P_PIbd-31.docx 3
    47 Kursovaya_Blokhin_PIbd31.docx 5
    48 PIbd-31_Medvedkov_A_D_curs.docx 1
    49 Вражкин Семён ПИбд-33.docx 2
    50 КР ПМУ Макаров ПИбд-31 (2).docx 14
    51 Курсовая Крюков Алексей ПИбд-31.docx 1
    52 Курсовая ПМУ Волков Никита ПИбд-33 (1).docx 9
    53 Курсовая ПМУ Волков Никита ПИбд-33.docx 9
    54 ПИбд-31, Лёвушкина Анна, записка к кр.docx 14
    55 ПИбд-32 Шабунов Олег.docx 7
    56 Фирсов_Кирилл_Записка (1).docx 10
    57 Фирсов_Кирилл_Записка.docx 10

    Отображение результата кластеризации

    Используется косинусовое сходство для определения схожих работ. Проверка сходимости до 70% совпадений
    In [71]:
    from sklearn.metrics.pairwise import cosine_similarity
    
    n_clusters = 27
    
    kmeans = KMeans(n_clusters=n_clusters, random_state=42)
    
    # Обучаем модель и предсказываем кластеры
    clusters = kmeans.fit_predict(tfidf_matrix)
    
    # Уменьшаем размерность данных до 2 компонент с помощью PCA
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(tfidf_matrix.toarray())
    
    cluster_df = pd.DataFrame(X_pca, columns=["PCA1", "PCA2"])
    cluster_df["Cluster"] = clusters
    cluster_df["Document"] = df["doc"]
    
    # Вычисляем косинусное сходство
    similarities = cosine_similarity(tfidf_matrix)
    
    plt.figure(figsize=(14, 10))
    palette = sns.color_palette("viridis", n_clusters)
    sns.scatterplot(
        x="PCA1",
        y="PCA2",
        hue="Cluster",
        palette=palette,
        data=cluster_df,
        legend="full",
        alpha=0.8
    )
    
    threshold = 0.3
    for i in range(len(cluster_df)):
        similar_docs = [j for j in range(len(cluster_df)) if similarities[i, j] > threshold and i != j]
        color = "red" if similar_docs else "black"
        plt.text(
            cluster_df["PCA1"].iloc[i],
            cluster_df["PCA2"].iloc[i],
            cluster_df["Document"].iloc[i][:15],  # Обрезаем текст для читаемости
            fontsize=8,
            alpha=0.75,
            color=color
        )
    
    plt.title("Кластеризация текстов с PCA")
    plt.xlabel("PCA Компонента 1")
    plt.ylabel("PCA Компонента 2")
    plt.legend(title="Кластеры")
    plt.grid(True)
    plt.show()
    
    No description has been provided for this image

    Тут уже больше похоже на правду
    Конец