5.0 MiB
5.0 MiB
Лаба 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}")
Данные загружены.
Нужно их обработать. В этапы будут включены:
Сначала:
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)
Выделение частей речи (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())
Создание 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))
Векторизация с использованием мешка слов (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()
Странно выглядит, поэтому попробую воспользоваться 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"]])
Отображение результата кластеризации
Используется косинусовое сходство для определения схожих работ. Проверка сходимости до 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()
Тут уже больше похоже на правду
Конец