29 KiB
Лабораторная работа 8¶
Выбранный датасет: ТЗ и статьи по ИТ (кластеризация, классификация).
Выбранный метод машинного обучения: классификация.
Задача анализа текстов: разработка модели, которая сможет автоматически определять категорию, к которой относится текст (в данном случае, ТЗ или статья).
Импорт библиотеки и инициализация модуля для анализа текста:
import spacy
sp = spacy.load("ru_core_news_lg")
Загрузка текстов из файлов с расширением .docx в датафрейм:
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])
В первую очередь будут использованы методы для предобработки текста.¶
Трансформация:
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 символов:
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))
Теперь перейдем к этапу формирования N-грамм:¶
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])
Также применим методы для векторизации текста.¶
Мешок слов:
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])
Либо же можно использовать частотный портрет:
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])
Обучение модели и проверка ее качества:¶
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)
Как можно заметить, обе модели показывают очень хорошие результаты, а вторая модель даже практически идеальные. Возможно это связано с малым количеством данных в выборке (всего 41 документ), которые модель просто запомнила и в итоге переобучилась.
Кроме того, согласно заданию, попробуем оценить решение, используя другие гиперпараметры модели машинного обучения (подберем их методом поиска по сетке):
print("### TF-IDF Model (Optimized) ###")
model_tfidf = train_and_evaluate(X_tfidf, y, optimize=True)
Можно сделать вывод о том, что в данном случае имееется возможность подобрать модель с такими гиперпараметрами, которая согласно метрикам покажет даже идеальный результат