Files
2025-06-17 12:27:34 +04:00

29 KiB
Raw Permalink Blame History

Лабораторная работа 8

Выбранный датасет: ТЗ и статьи по ИТ (кластеризация, классификация).

Выбранный метод машинного обучения: классификация.

Задача анализа текстов: разработка модели, которая сможет автоматически определять категорию, к которой относится текст (в данном случае, ТЗ или статья).

Импорт библиотеки и инициализация модуля для анализа текста:

In [1]:
import spacy

sp = spacy.load("ru_core_news_lg")

Загрузка текстов из файлов с расширением .docx в датафрейм:

In [2]:
import pandas as pd
from docx import Document
import os

def read_docx(file_path):
    doc = Document(file_path)
    full_text = []
    for paragraph in doc.paragraphs:
        full_text.append(paragraph.text)
    return "\n".join(full_text)

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("../../static/tz_itdocs/")
df["type"] = df.apply(
    lambda row: 0 if str(row["doc"]).startswith("tz_") else 1, axis=1
)
df.sort_values(by=["doc"], inplace=True)

print(df.iloc[15:25])
                                                  doc  \
15                                         tz_16.docx   
16                                         tz_17.docx   
17                                         tz_18.docx   
18                                         tz_19.docx   
19                                         tz_20.docx   
20               Архитектура, управляемая модель.docx   
21                  Введение в проектирование ИС.docx   
22                      Встроенные операторы SQL.docx   
23  Методологии разработки программного обеспечени...   
24  Методологии разработки программного обеспечени...   

                                                 text  type  
15  2.2\tТехническое задание\n2.2.1\tОбщие сведени...     0  
16  2.2 Техническое задание.\n2.2.1 Общие сведения...     0  
17  2.2. Техническое задание\nОбщие сведения:\nПол...     0  
18  2.2. Техническое задание\n2.2.1. Наименование ...     0  
19  2.2. Техническое задание\n2.2.1. Общие сведени...     0  
20  Архитектура, управляемая модель\nАббревиатура ...     1  
21  1. ВВЕДЕНИЕ В ПРОЕКТИРОВАНИЕ ИНФОРМАЦИОННЫХ СИ...     1  
22  Встроенные операторы SQL. \nКак было отмечено ...     1  
23  Методологии разработки программного обеспечени...     1  
24  Методологии разработки программного обеспечени...     1  

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

Трансформация:

In [ ]:
import re
import emoji
from num2words import num2words

def emojis_words(text):
    text = emoji.demojize(text, delimiters=(" ", " "))
    text = text.replace(":", "").replace("_", " ")
    return text

def transform_text(text):
    # Удаление из текста всех HTML-тегов
    text = re.sub(r'<[^<]+?>', '', text)
    
    # Удаление из текста всех URL и ссылок
    text = re.sub(r'http\S+', '', text)

    text = emojis_words(text)

    text = text.lower()

    text = re.sub(r'\s+', ' ', text)  
    
    text = text.replace("ё", "е")

    # Удаление всех специальных символов
    text = re.sub(r'[^a-zA-Zа-яА-Я0-9\s]', '', text)

    words: list[str] = text.split()
    words = [num2words(word, lang="ru") if word.isdigit() else word for word in words]
    text = " ".join(words)

    # Удаление из текста всех знаков препинания
    text = re.sub(r'[^\w\s]', '', text)

    return text

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

Для токенизации, выделения частей речи (POS tagging), нормализации (в данном случае была выбрана лемматизация) и фильтрации используем библиотеку spaCy. На этапе фильтрации для сокращения пространства признаков используем словарь стоп-слов, а также удалим все слова длиной больше 20 символов:

In [ ]:
from nltk.corpus import stopwords

stop_words = set(stopwords.words('russian'))

def preprocess_text(text):
    doc = sp(text)
    
    filtered_tokens = [
        f"{token.lemma_}_{token.pos_}_{token.morph}"
        for token in doc
        if token.text not in stop_words and len(token.text) <= 20 
    ]
    
    return " ".join(filtered_tokens)

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

first_text_tokens = df["preprocessed_text"].iloc[0].split()[:10]
print(" ".join(first_text_tokens))
двадцать_NUM_Case=Nom технический_ADJ_Case=Nom|Degree=Pos|Gender=Neut|Number=Sing задание_NOUN_Animacy=Inan|Case=Nom|Gender=Neut|Number=Sing двести_NUM_Case=Nom двадцать_NUM_Case=Nom общий_ADJ_Case=Nom|Degree=Pos|Number=Plur сведение_NOUN_Animacy=Inan|Case=Nom|Gender=Neut|Number=Plur полный_ADJ_Case=Nom|Degree=Pos|Gender=Neut|Number=Sing наименование_NOUN_Animacy=Inan|Case=Acc|Gender=Neut|Number=Sing система_NOUN_Animacy=Inan|Case=Gen|Gender=Fem|Number=Sing

Теперь перейдем к этапу формирования N-грамм:

In [ ]:
from nltk.util import ngrams
from nltk.tokenize import word_tokenize

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

df["bigrams"] = df["preprocessed_text"].apply(lambda x: generate_ngrams(x, n=2))

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

print(df.iloc[15:25])
                                                  doc  \
15                                         tz_16.docx   
16                                         tz_17.docx   
17                                         tz_18.docx   
18                                         tz_19.docx   
19                                         tz_20.docx   
20               Архитектура, управляемая модель.docx   
21                  Введение в проектирование ИС.docx   
22                      Встроенные операторы SQL.docx   
23  Методологии разработки программного обеспечени...   
24  Методологии разработки программного обеспечени...   

                                                 text  type  \
15  2.2\tТехническое задание\n2.2.1\tОбщие сведени...     0   
16  2.2 Техническое задание.\n2.2.1 Общие сведения...     0   
17  2.2. Техническое задание\nОбщие сведения:\nПол...     0   
18  2.2. Техническое задание\n2.2.1. Наименование ...     0   
19  2.2. Техническое задание\n2.2.1. Общие сведени...     0   
20  Архитектура, управляемая модель\nАббревиатура ...     1   
21  1. ВВЕДЕНИЕ В ПРОЕКТИРОВАНИЕ ИНФОРМАЦИОННЫХ СИ...     1   
22  Встроенные операторы SQL. \nКак было отмечено ...     1   
23  Методологии разработки программного обеспечени...     1   
24  Методологии разработки программного обеспечени...     1   

                                    preprocessed_text  \
15  двадцать_NUM_Case=Nom технический_ADJ_Case=Nom...   
16  двадцать_NUM_Case=Nom технический_ADJ_Case=Nom...   
17  двадцать_NUM_Case=Nom технический_ADJ_Case=Nom...   
18  двадцать_NUM_Case=Nom технический_ADJ_Case=Nom...   
19  двадцать_NUM_Case=Nom технический_ADJ_Case=Nom...   
20  архитектура_NOUN_Animacy=Inan|Case=Nom|Gender=...   
21  введение_NOUN_Animacy=Inan|Case=Nom|Gender=Neu...   
22  встроенные_ADJ_Case=Nom|Degree=Pos|Number=Plur...   
23  методология_NOUN_Animacy=Inan|Case=Nom|Gender=...   
24  методология_NOUN_Animacy=Inan|Case=Nom|Gender=...   

                                              bigrams  \
15  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...   
16  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...   
17  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...   
18  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...   
19  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...   
20  [(архитектура_NOUN_Animacy=Inan|Case=Nom|Gende...   
21  [(введение_NOUN_Animacy=Inan|Case=Nom|Gender=N...   
22  [(встроенные_ADJ_Case=Nom|Degree=Pos|Number=Pl...   
23  [(методология_NOUN_Animacy=Inan|Case=Nom|Gende...   
24  [(методология_NOUN_Animacy=Inan|Case=Nom|Gende...   

                                             trigrams  
15  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...  
16  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...  
17  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...  
18  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...  
19  [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...  
20  [(архитектура_NOUN_Animacy=Inan|Case=Nom|Gende...  
21  [(введение_NOUN_Animacy=Inan|Case=Nom|Gender=N...  
22  [(встроенные_ADJ_Case=Nom|Degree=Pos|Number=Pl...  
23  [(методология_NOUN_Animacy=Inan|Case=Nom|Gende...  
24  [(методология_NOUN_Animacy=Inan|Case=Nom|Gende...  

Также применим методы для векторизации текста.

Мешок слов:

In [6]:
from scipy import sparse
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np

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

random_columns = np.random.choice(counts_df.columns, size=10, replace=False)

print(counts_df.loc[15:25, random_columns])
    поручить_verb_aspect  попадание_noun_animacy  интерфейс_noun_animacy  \
15                     0                       0                       2   
16                     0                       0                       2   
17                     0                       0                       7   
18                     0                       0                       2   
19                     0                       0                       1   
20                     0                       0                       6   
21                     0                       0                       1   
22                     0                       0                       0   
23                     0                       0                       6   
24                     0                       0                       5   
25                     0                       0                       0   

    анатолиевич_propn_animacy  столкнуться_verb_aspect  скрытие_noun_animacy  \
15                          2                        0                     0   
16                          0                        0                     0   
17                          0                        0                     0   
18                          0                        0                     0   
19                          0                        0                     0   
20                          0                        0                     0   
21                          0                        1                     0   
22                          0                        0                     0   
23                          0                        1                     0   
24                          0                        0                     0   
25                          0                        0                     2   

    распространенной_adj_case  текстовый_adj_animacy  pipes_x_foreign  \
15                          0                      0                0   
16                          0                      0                0   
17                          0                      1                0   
18                          0                      0                0   
19                          0                      0                0   
20                          0                      0                0   
21                          0                      0                0   
22                          0                      0                0   
23                          0                      0                0   
24                          0                      0                0   
25                          0                      0                0   

    руководствоваться_verb_aspect  
15                              0  
16                              0  
17                              0  
18                              0  
19                              0  
20                              0  
21                              2  
22                              0  
23                              0  
24                              0  
25                              0  

Либо же можно использовать частотный портрет:

In [7]:
from sklearn.feature_extraction.text import TfidfVectorizer

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

print(tfidf_df.loc[15:25, random_columns])
    поручить_verb_aspect  попадание_noun_animacy  интерфейс_noun_animacy  \
15                   0.0                     0.0                0.033439   
16                   0.0                     0.0                0.028513   
17                   0.0                     0.0                0.044900   
18                   0.0                     0.0                0.043194   
19                   0.0                     0.0                0.023228   
20                   0.0                     0.0                0.038113   
21                   0.0                     0.0                0.007355   
22                   0.0                     0.0                0.000000   
23                   0.0                     0.0                0.026442   
24                   0.0                     0.0                0.028079   
25                   0.0                     0.0                0.000000   

    анатолиевич_propn_animacy  столкнуться_verb_aspect  скрытие_noun_animacy  \
15                   0.101196                 0.000000              0.000000   
16                   0.000000                 0.000000              0.000000   
17                   0.000000                 0.000000              0.000000   
18                   0.000000                 0.000000              0.000000   
19                   0.000000                 0.000000              0.000000   
20                   0.000000                 0.000000              0.000000   
21                   0.000000                 0.017215              0.000000   
22                   0.000000                 0.000000              0.000000   
23                   0.000000                 0.022169              0.000000   
24                   0.000000                 0.000000              0.000000   
25                   0.000000                 0.000000              0.040551   

    распространенной_adj_case  текстовый_adj_animacy  pipes_x_foreign  \
15                        0.0               0.000000              0.0   
16                        0.0               0.000000              0.0   
17                        0.0               0.035675              0.0   
18                        0.0               0.000000              0.0   
19                        0.0               0.000000              0.0   
20                        0.0               0.000000              0.0   
21                        0.0               0.000000              0.0   
22                        0.0               0.000000              0.0   
23                        0.0               0.000000              0.0   
24                        0.0               0.000000              0.0   
25                        0.0               0.000000              0.0   

    руководствоваться_verb_aspect  
15                       0.000000  
16                       0.000000  
17                       0.000000  
18                       0.000000  
19                       0.000000  
20                       0.000000  
21                       0.037685  
22                       0.000000  
23                       0.000000  
24                       0.000000  
25                       0.000000  

Обучение модели и проверка ее качества:

In [8]:
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

def train_and_evaluate(X, y, test_size=0.2, cv=5, optimize=False):
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=9)

    if optimize:
        param_grid = {
            "n_estimators": [10, 20, 30, 40, 50, 100, 150, 200, 250, 500],
            "max_features": ["sqrt", "log2", 2],
            "max_depth": [2, 3, 4, 5, 6, 7, 8, 9, 10],
            "criterion": ["gini", "entropy", "log_loss"],
            "class_weight": ["balanced", "balanced_subsample"]
        }

        grid_search = GridSearchCV(RandomForestClassifier(random_state=9), param_grid, scoring="f1", cv=cv, n_jobs=-1)
        grid_search.fit(X_train, y_train)
        model = grid_search.best_estimator_
        print(f"Лучшие параметры: {grid_search.best_params_}")
    else:
        model = RandomForestClassifier(n_estimators=100, random_state=9)
        model.fit(X_train, y_train)

    y_pred = model.predict(X_test)

    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    roc_auc = roc_auc_score(y_test, y_pred)

    print(f"Accuracy: {accuracy:.4f}")
    print(f"Precision: {precision:.4f}")
    print(f"Recall: {recall:.4f}")
    print(f"F1 Score: {f1:.4f}")
    print(f"ROC AUC: {roc_auc:.4f}")

    scores = cross_val_score(model, X_train, y_train, cv=cv, scoring='f1')
    f1_cv = scores.mean()
    print(f"Cross-validated F1 Score: {f1_cv:.4f}")

    return model

X_tfidf = tfidf_df
X_counts = counts_df
y = df["type"]

print("### TF-IDF Model ###")
model_tfidf = train_and_evaluate(X_tfidf, y)

print("\n### Count Vectorizer Model ###")
model_counts = train_and_evaluate(X_counts, y)
### TF-IDF Model ###
Accuracy: 0.8889
Precision: 0.7500
Recall: 1.0000
F1 Score: 0.8571
ROC AUC: 0.9167
Cross-validated F1 Score: 1.0000

### Count Vectorizer Model ###
Accuracy: 1.0000
Precision: 1.0000
Recall: 1.0000
F1 Score: 1.0000
ROC AUC: 1.0000
Cross-validated F1 Score: 0.8933

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

Кроме того, согласно заданию, попробуем оценить решение, используя другие гиперпараметры модели машинного обучения (подберем их методом поиска по сетке):

In [ ]:
print("### TF-IDF Model (Optimized) ###")
model_tfidf = train_and_evaluate(X_tfidf, y, optimize=True)
### TF-IDF Model (Optimized) ###
Лучшие параметры: {'class_weight': 'balanced', 'criterion': 'gini', 'max_depth': 2, 'max_features': 'sqrt', 'n_estimators': 10}
Accuracy: 1.0000
Precision: 1.0000
Recall: 1.0000
F1 Score: 1.0000
ROC AUC: 1.0000
Cross-validated F1 Score: 1.0000

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