Files
AIM-PIbd-31-Anisin-R-S/lab_8/lab8.ipynb
2025-04-04 20:39:20 +04:00

48 KiB
Raw Blame History

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

Датасет: ТЗ и статьи по ИТ (кластеризация, классификация).

In [29]:
import spacy

sp = spacy.load("ru_core_news_lg")

Загружаем тексты из файлов датафрейм:

In [30]:
import pandas as pd
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/text/")
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)

display(df.head(), df.tail())
<class 'pandas.core.frame.DataFrame'>
Index: 41 entries, 0 to 40
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   doc     41 non-null     object
 1   text    41 non-null     object
 2   type    41 non-null     int64 
dtypes: int64(1), object(2)
memory usage: 1.3+ 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 tz_01.docx 2.2 Техническое задание\n2.2.1 Общие сведения\... 0
1 tz_02.docx 2.2 Техническое задание\n2.2.1 Общие сведения\... 0
2 tz_03.docx 2.2. Техническое задание\nОбщие сведения:\nВ д... 0
3 tz_04.docx Техническое задание\n2.2.1 Общие сведения\nИнт... 0
4 tz_05.docx 2.2 Техническое задание\n2.2.1 Общие сведения.... 0
<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
36 Этапы разработки проекта2.docx Этапы разработки проекта: заключительные стади... 1
37 Этапы разработки проекта3.docx Этапы разработки проекта: определение стратеги... 1
38 Этапы разработки проекта4.docx Этапы разработки проекта: реализация, тестиров... 1
39 Этапы разработки проекта5.docx Этапы разработки проекта: стратегия и анализ\n... 1
40 Язык манипуляции данными.docx 2.1.3. Язык манипуляции данными (ЯМД)\nЯзык ма... 1

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

In [31]:
import re
from num2words import num2words

def transform_text(text):
    text = re.sub(r'<[^<]+?>', '', text)
    
    text = re.sub(r'http\S+', '', text)

    text = text.lower()

    text = re.sub(r'\s+', ' ', text)

    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)

Токенизация, выделения частей речи, лемматизация и фильтрация

In [32]:
from nltk.corpus import stopwords

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

def is_valid_token(token):
    return token.text not in stop_words and len(token.text) <= 20

def preprocess_text(text):
    doc = sp(text)
    
    filtered_tokens = map(
        lambda token: f"{token.lemma_}_{token.pos_}_{token.morph}", 
        filter(is_valid_token, doc)
    )
    
    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 [33]:
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")
    return list(ngrams(tokens, n))

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))

df.head(10)
Out[33]:
<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 bigrams trigrams
0 tz_01.docx 2.2 Техническое задание\n2.2.1 Общие сведения\... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
1 tz_02.docx 2.2 Техническое задание\n2.2.1 Общие сведения\... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
2 tz_03.docx 2.2. Техническое задание\nОбщие сведения:\nВ д... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
3 tz_04.docx Техническое задание\n2.2.1 Общие сведения\nИнт... 0 технический_ADJ_Case=Nom|Degree=Pos|Gender=Neu... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N...
4 tz_05.docx 2.2 Техническое задание\n2.2.1 Общие сведения.... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
5 tz_06.docx 2.2 Техническое задание\t\n1.Общие сведения\nП... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
6 tz_07.docx Техническое задание\nОбщие сведения\nВ данном ... 0 технический_ADJ_Case=Nom|Degree=Pos|Gender=Neu... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N...
7 tz_08.docx Техническое задание\n1 Общие сведения\n1.1 Пол... 0 технический_ADJ_Case=Nom|Degree=Pos|Gender=Neu... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N... [(технический_ADJ_Case=Nom|Degree=Pos|Gender=N...
8 tz_09.docx 2.2. Техническое задание\n2.2.1.\n\nОбщие свед... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...
9 tz_10.docx 2.2. Техническое задание\n2.2.1. Общие сведени... 0 двадцать_NUM_Case=Nom технический_ADJ_Case=Nom... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=... [(двадцать_NUM_Case=Nom, технический_ADJ_Case=...

Векторизация текста. Мешок слов:

In [34]:
from scipy import sparse
from sklearn.feature_extraction.text import CountVectorizer

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(),
)

counts_df.head()
Out[34]:
<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>
000001log_x_foreign 000001ver001_propn_foreign 012n_propn_foreign 100_num_ 1024х768_num_ 10мегабайт_noun_animacy 13а_num_ 13гбайт_num_ 17а_num_ 1998_adj_ ... язвительный_adj_case язык_noun_animacy ямд_noun_animacy ямд_propn_animacy яод_noun_animacy яода_noun_animacy ясность_noun_animacy ясный_adj_case ясный_adj_degree ящик_noun_animacy
0 0 0 0 0 0 0 0 0 0 0 ... 0 2 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 1 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 ... 0 1 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 2 0 0 0 0 0 0 0 0

5 rows × 7362 columns

Частотный портрет:

In [35]:
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(),
)

tfidf_df.head(10)
Out[35]:
<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>
000001log_x_foreign 000001ver001_propn_foreign 012n_propn_foreign 100_num_ 1024х768_num_ 10мегабайт_noun_animacy 13а_num_ 13гбайт_num_ 17а_num_ 1998_adj_ ... язвительный_adj_case язык_noun_animacy ямд_noun_animacy ямд_propn_animacy яод_noun_animacy яода_noun_animacy ясность_noun_animacy ясный_adj_case ясный_adj_degree ящик_noun_animacy
0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.024156 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.020046 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.015726 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.026435 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
5 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.048546 0.0 ... 0.0 0.014539 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
6 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.019315 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
7 0.0 0.0 0.0 0.0 0.039209 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
8 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.040475 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
9 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 ... 0.0 0.049977 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

10 rows × 7362 columns

Обучение и оценка модели:

In [43]:
from sklearn.model_selection import train_test_split, cross_val_score
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=42)

    model = RandomForestClassifier(n_estimators=100, random_state=42)
    
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)

    metrics = {
        "Accuracy": accuracy_score(y_test, y_pred),
        "Precision": precision_score(y_test, y_pred),
        "Recall": recall_score(y_test, y_pred),
        "F1 Score": f1_score(y_test, y_pred),
        "ROC AUC": roc_auc_score(y_test, y_pred)
    }

    f1_cv = cross_val_score(model, X_train, y_train, cv=cv, scoring='f1').mean()

    for metric, value in metrics.items():
        print(f"{metric}: {value:.4f}")
    
    print(f"Средний 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("Count Vectorizer Model")
model_counts = train_and_evaluate(X_counts, y)
TF-IDF Model
Accuracy: 1.0000
Precision: 1.0000
Recall: 1.0000
F1 Score: 1.0000
ROC AUC: 1.0000
Средний F1 Score (кросс-валидация): 0.9714
Count Vectorizer Model
Accuracy: 0.8889
Precision: 1.0000
Recall: 0.8000
F1 Score: 0.8889
ROC AUC: 0.9000
Средний F1 Score (кросс-валидация): 0.9600

TF-IDF показывает признаки переобучения, так как результаты теста идеальны, а на кросс-валидации чуть хуже. Count Vectorizer более сбалансирован, но уступает по точности.