diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..038af1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,313 @@ +# local +files/* +logs/* +models/mnli_model +models/saved_models +notebooks/files +notebooks/results +notebooks/word_files +notebooks/document_topics.csv +notebooks/g.ipynb +notebooks/ocr_processing.ipynb +notebooks/topic_modeling_analysis.ipynb +results/* +tests/* +notebooks/logs +notebooks/output_files +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### JupyterNotebooks template +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/AI-Data-Security.iml b/.idea/AI-Data-Security.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/AI-Data-Security.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/Topic-Modeling.iml b/.idea/Topic-Modeling.iml new file mode 100644 index 0000000..4687102 --- /dev/null +++ b/.idea/Topic-Modeling.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/csv-editor.xml b/.idea/csv-editor.xml new file mode 100644 index 0000000..0153af6 --- /dev/null +++ b/.idea/csv-editor.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..49f715b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..348d480 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..ae77430 --- /dev/null +++ b/app.py @@ -0,0 +1,111 @@ +import datetime +import os + +import pandas as pd +import streamlit as st + +from config import Config +from logger import logger +from models.categorizer import Categorizer +from models.topic_modeler import TopicModeler +from processors.integrated_processor import IntegratedProcessor +from readers.word_document_reader import WordDocumentReader + + +def process_documents(folder_path, predefined_topics): + try: + logger.info("شروع اجرای برنامه مدل‌سازی موضوعات.") + document_reader = WordDocumentReader() + + logger.info("اجرای با موضوعات از پیش تعریف شده...") + topic_modeler_manual = TopicModeler(predefined_topics=predefined_topics) + categorizer_manual = Categorizer(topic_modeler_manual) + manual_processor = IntegratedProcessor( + topic_modeler=topic_modeler_manual, + categorizer=categorizer_manual, + document_reader=document_reader, + folder_path=folder_path, + predefined_topics=predefined_topics + ) + manual_results = manual_processor.run() + + logger.info("تمامی فرآیندهای مدل‌سازی موضوعات با موفقیت انجام شد.") + + # Combine results + combined_df = pd.concat([manual_results], ignore_index=True) + + if 'Topic Label' in combined_df.columns and 'Assigned Label' not in combined_df.columns: + combined_df.rename(columns={'Topic Label': 'Assigned Label'}, inplace=True) + + combined_df = combined_df.sort_values('Confidence', ascending=False).reset_index(drop=True) + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + os.makedirs(Config.RESULTS_DIR, exist_ok=True) + output_file = os.path.join(Config.RESULTS_DIR, f'topic_results_{timestamp}.csv') + combined_df.to_csv(output_file, index=False) + logger.info(f"نتایج در فایل '{output_file}' ذخیره شد.") + + label_column = 'Assigned Label' if 'Assigned Label' in combined_df.columns else 'Topic Label' + grouped = combined_df.groupby(label_column) + grouped_data = {label: group['Document'].tolist() for label, group in grouped} + + results = { + + "manual": manual_results, + + "combined_df": combined_df, + "output_file": output_file, + "grouped_data": grouped_data + } + + return results + + except Exception as e: + logger.error(f"خطایی در اجرای اصلی رخ داده است: {str(e)}") + st.error(f"خطایی رخ داده است. لطفاً فایل لاگ را بررسی کنید: {Config.LOG_DIR}") + return None + + +def main(): + st.set_page_config(page_title="سیستم مدل‌سازی موضوعات", layout="wide") + st.title("سیستم مدل‌سازی موضوعات برای فایل‌های PDF و Word") + + st.subheader("تعریف موضوعات از پیش تعریف شده") + st.write("هر موضوع را در یک خط جداگانه وارد کنید.") + predefined_topics_input = st.text_area( + "موضوعات از پیش تعریف شده (هر خط یک موضوع)", + value="فناوری\nسلامت و سبک زندگی\nهنر و سرگرمی\nسفر و مکان‌ها\nآموزش" + ) + predefined_topics = [{'label': topic.strip()} for topic in predefined_topics_input.split('\n') if topic.strip()] + folder_path = './notebooks/word_files/' + if st.button("پردازش فایل‌ها"): + if os.path.exists(folder_path): + with st.spinner("در حال پردازش فایل‌ها..."): + results = process_documents(folder_path, predefined_topics) + if results: + st.success("مدل‌سازی موضوعات با موفقیت انجام شد.") + + st.subheader("نتایج مدل‌سازی موضوعات") + st.dataframe(results['combined_df']) + + with open(results['output_file'], "rb") as file: + btn = st.download_button( + label="دانلود نتایج به صورت CSV", + data=file, + file_name=os.path.basename(results['output_file']), + mime="text/csv" + ) + + st.subheader("نتایج دسته‌بندی اسناد بر اساس دسته‌ها") + for label, documents in results['grouped_data'].items(): + with st.expander(f"دسته: {label}"): + for doc in documents: + st.write(f"- {doc}") + else: + st.error("خطایی در پردازش فایل‌ها رخ داده است.") + else: + st.error("مسیر پوشه مشخص شده وجود ندارد. لطفاً مسیر معتبر وارد کنید.") + + +if __name__ == "__main__": + main() diff --git a/config.py b/config.py new file mode 100644 index 0000000..acfcfc8 --- /dev/null +++ b/config.py @@ -0,0 +1,14 @@ +# config.py +import os + +from dotenv import load_dotenv + +load_dotenv() + + +class Config: + MODEL_NAME: str = os.getenv('MODEL_NAME', './models/saved_models') + LOG_DIR: str = os.getenv('LOG_DIR', 'logs') + RESULTS_DIR: str = os.getenv('RESULTS_DIR', 'results') + ZERO_SHOT_MODEL: str = os.getenv('ZERO_SHOT_MODEL', './models/mnli_model') + SUMMARIZER_MODEL:str=os.getenv('SUMMARIZER_MODEL','') diff --git a/interfaces/categorizer_interface.py b/interfaces/categorizer_interface.py new file mode 100644 index 0000000..a222d5c --- /dev/null +++ b/interfaces/categorizer_interface.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod +from typing import List, Dict, Tuple + +import numpy as np + + +class ICategorizer(ABC): + @abstractmethod + def assign_automatic_labels(self): + pass + + @abstractmethod + def assign_predefined_labels(self, embeddings: np.ndarray, predefined_topics: List[Dict]) -> Tuple[ + List[str], List[float]]: + pass diff --git a/interfaces/document_reader_interface.py b/interfaces/document_reader_interface.py new file mode 100644 index 0000000..4358583 --- /dev/null +++ b/interfaces/document_reader_interface.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod +from typing import List + + +class IDocumentReader(ABC): + + @abstractmethod + def read_documents(self, folder_path: str) -> List[str]: + pass diff --git a/interfaces/summarizer_interface.py b/interfaces/summarizer_interface.py new file mode 100644 index 0000000..932cfac --- /dev/null +++ b/interfaces/summarizer_interface.py @@ -0,0 +1,9 @@ +# interfaces/summarizer_interface.py +from abc import ABC, abstractmethod +from typing import List + + +class ISummarizer(ABC): + @abstractmethod + def summarize(self, texts: List[str]) -> List[str]: + pass diff --git a/interfaces/topic_modeler_interface.py b/interfaces/topic_modeler_interface.py new file mode 100644 index 0000000..b11f1eb --- /dev/null +++ b/interfaces/topic_modeler_interface.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import List, Dict, Optional, Tuple + +import numpy as np +import pandas as pd + + +class ITopicModeler(ABC): + @abstractmethod + def fit_transform(self, documents: List[str]) -> Tuple[List[int], np.ndarray, np.ndarray]: + pass + + @abstractmethod + def get_topic_info(self) -> Optional[pd.DataFrame]: + pass + + @abstractmethod + def get_topics(self) -> Optional[Dict[int, List[tuple]]]: + pass diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..5f9d806 --- /dev/null +++ b/logger.py @@ -0,0 +1,28 @@ +import datetime +import logging +import os + +from config import Config + + +def setup_logging(): + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + log_filename = f'topic_modeling_{timestamp}.log' + os.makedirs(Config.LOG_DIR, exist_ok=True) + log_path = os.path.join(Config.LOG_DIR, log_filename) + + logging.basicConfig( + filename=log_path, + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + console = logging.StreamHandler() + console.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + + return log_path +log_path = setup_logging() +logger = logging.getLogger(__name__) diff --git a/main.py b/main.py new file mode 100644 index 0000000..378b6ea --- /dev/null +++ b/main.py @@ -0,0 +1,66 @@ +import logging +from models.topic_modeler import TopicModeler +from models.categorizer import Categorizer +from readers.pdf_document_reader import WordDocumentReader +from summarizers.transformers_summarizer import TransformersSummarizer +from processors.integrated_processor import IntegratedProcessor +from logger import logger +from config import Config + +def main(): + pdf_folder_path = './notebooks/word_files/' + predefined_topics = [ + {'label': 'فناوری'}, + {'label': 'سلامت و سبک زندگی'}, + {'label': 'هنر و سرگرمی'}, + {'label': 'سفر و مکان‌ها'}, + {'label': 'آموزش'} + ] + + try: + logger.info("شروع اجرای برنامه مدل‌سازی موضوعات.") + document_reader = WordDocumentReader() + logger.info("اجرای تشخیص خودکار موضوعات...") + topic_modeler_auto = TopicModeler() + categorizer_auto = Categorizer(topic_modeler_auto) + auto_processor = IntegratedProcessor( + topic_modeler=topic_modeler_auto, + categorizer=categorizer_auto, + document_reader=document_reader, + #summarizer=summarizer, + folder_path=pdf_folder_path + ) + auto_results = auto_processor.run() + print(auto_results) + logger.info("اجرای با موضوعات از پیش تعریف شده...") + topic_modeler_manual = TopicModeler(predefined_topics=predefined_topics) + categorizer_manual = Categorizer(topic_modeler_manual) + manual_processor = IntegratedProcessor( + topic_modeler=topic_modeler_manual, + categorizer=categorizer_manual, + document_reader=document_reader, + #summarizer=summarizer, + folder_path=pdf_folder_path, + predefined_topics=predefined_topics + ) + manual_results = manual_processor.run() + logger.info("اجرای با طبقه‌بندی صفر-شات برای برچسب‌گذاری...") + topic_modeler_zero_shot = TopicModeler() + categorizer_zero_shot = Categorizer(topic_modeler_zero_shot) + zero_shot_processor = IntegratedProcessor( + topic_modeler=topic_modeler_zero_shot, + categorizer=categorizer_zero_shot, + document_reader=document_reader, + #summarizer=summarizer, + folder_path=pdf_folder_path + ) + zero_shot_results = zero_shot_processor.run(use_zero_shot=True) + + logger.info("تمامی فرآیندهای مدل‌سازی موضوعات با موفقیت انجام شد.") + + except Exception as e: + logger.error(f"خطایی در اجرای اصلی رخ داده است: {str(e)}") + print(f"خطایی رخ داده است. لطفاً فایل لاگ را بررسی کنید: {Config.LOG_DIR}") + +if __name__ == "__main__": + main() diff --git a/models/categorizer.py b/models/categorizer.py new file mode 100644 index 0000000..86987cf --- /dev/null +++ b/models/categorizer.py @@ -0,0 +1,106 @@ +# models/categorizer.py +import logging +from typing import List, Dict, Tuple + +import numpy as np +from transformers import pipeline + +from config import Config +from interfaces.categorizer_interface import ICategorizer + +logger = logging.getLogger(__name__) + + +class Categorizer(ICategorizer): + + def __init__(self, topic_modeler: 'ITopicModeler'): + + self.topic_modeler = topic_modeler + self.topic_labels = {} + self.zero_shot_classifier = None + self.load_zero_shot_classifier() + + def load_zero_shot_classifier(self): + try: + logger.info("بارگذاری مدل طبقه‌بندی صفر-شات.") + self.zero_shot_classifier = pipeline( + "zero-shot-classification", + model=Config.ZERO_SHOT_MODEL, + ) + logger.info("مدل طبقه‌بندی صفر-شات با موفقیت بارگذاری شد.") + except Exception as e: + logger.error(f"خطا در بارگذاری مدل طبقه‌بندی صفر-شات: {str(e)}") + raise + + def assign_automatic_labels(self): + try: + logger.info("شروع تخصیص خودکار برچسب‌ها.") + topic_info = self.topic_modeler.get_topic_info() + if topic_info is None: + raise ValueError("عدم توانایی در دریافت اطلاعات موضوعات.") + + for _, row in topic_info.iterrows(): + topic_id = row['Topic'] + + if topic_id == -1: + self.topic_labels[topic_id] = 'خارج از دسته‌بندی' + continue + + topic_words = [word for word, _ in self.topic_modeler.get_topics()[topic_id]][:10] + + labels = topic_words + + topic_desc = " ".join(topic_words) + + classification = self.zero_shot_classifier( + sequences=topic_desc, + candidate_labels=labels, + multi_label=True + ) + if classification['labels']: + best_label = classification['labels'][0] + else: + best_label = f"موضوع {topic_id}" + + self.topic_labels[topic_id] = best_label + + logger.debug(f"موضوع {topic_id} با برچسب '{best_label}' تخصیص داده شد.") + + logger.info("تخصیص خودکار برچسب‌ها با موفقیت انجام شد.") + except Exception as e: + logger.error(f"خطا در تخصیص خودکار برچسب‌ها: {str(e)}") + raise + + def assign_predefined_labels(self, embeddings: np.ndarray, predefined_topics: List[Dict]) -> Tuple[ + List[str], List[float]]: + try: + logger.info("شروع تخصیص برچسب‌های از پیش تعریف شده.") + if not predefined_topics: + raise ValueError("موضوعات از پیش تعریف شده‌ای ارائه نشده است.") + + topic_labels = [topic['label'] for topic in predefined_topics] + topic_label_embeddings = self.topic_modeler.embedding_model.encode( + topic_labels, show_progress_bar=False + ) + logger.debug(f"تعبیه‌سازی برچسب‌های موضوعات از پیش تعریف شده انجام شد.") + + embeddings_norm = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True) + topic_label_embeddings_norm = topic_label_embeddings / np.linalg.norm(topic_label_embeddings, axis=1, + keepdims=True) + + similarities = np.dot(embeddings_norm, topic_label_embeddings_norm.T) + logger.debug("محاسبه شباهت کسینوسی بین اسناد و برچسب‌ها انجام شد.") + + assigned_labels = [] + confidences = [] + for sim in similarities: + idx = np.argmax(sim) + assigned_labels.append(topic_labels[idx]) + confidences.append(sim[idx]) + + logger.info("تخصیص برچسب‌های از پیش تعریف شده با موفقیت انجام شد.") + return assigned_labels, confidences + + except Exception as e: + logger.error(f"خطا در تخصیص برچسب‌های از پیش تعریف شده: {str(e)}") + raise diff --git a/models/topic_modeler.py b/models/topic_modeler.py new file mode 100644 index 0000000..cfef77f --- /dev/null +++ b/models/topic_modeler.py @@ -0,0 +1,92 @@ +# models/topic_modeler.py +import logging +from typing import List, Dict, Optional, Tuple + +import numpy as np +import pandas as pd +from bertopic import BERTopic +from sentence_transformers import SentenceTransformer +from sklearn.feature_extraction.text import CountVectorizer + +from config import Config +from interfaces.topic_modeler_interface import ITopicModeler + +logger = logging.getLogger(__name__) + + +class TopicModeler(ITopicModeler): + + def __init__(self, predefined_topics: Optional[List[Dict]] = None): + + try: + logger.info("شروع به بارگذاری مدل تعبیه‌سازی جملات.") + self.embedding_model = SentenceTransformer(Config.MODEL_NAME) + logger.info(f"مدل تعبیه‌سازی '{Config.MODEL_NAME}' با موفقیت بارگذاری شد.") + + self.predefined_topics = predefined_topics + + persian_stop_words = [ + 'و', 'در', 'به', 'از', 'که', 'با', 'برای', 'این', 'را', 'آن', 'ها', 'یک', 'می', 'تا', 'بر', 'یا', 'اما', + 'دیگر', 'هم', 'کرده', 'کرد', 'بود', 'بودن', 'باشه', 'نیز', 'از', 'باشد' + ] + + vectorizer = CountVectorizer( + stop_words=persian_stop_words, + ngram_range=(1, 3), + max_features=5000 + ) + + self.topic_model = BERTopic( + embedding_model=self.embedding_model, + vectorizer_model=vectorizer, + calculate_probabilities=True, + verbose=False, + min_topic_size=2, + top_n_words=10, + nr_topics=10 + ) + + logger.info(f"BERTopic با مدل '{Config.MODEL_NAME}' مقداردهی اولیه شد.") + mode = 'موضوعات از پیش تعریف شده' if predefined_topics else 'تشخیص خودکار موضوعات' + logger.info(f"حالت مدل‌سازی: {mode}") + + except Exception as e: + logger.error(f"خطا در مقداردهی اولیه TopicModeler: {str(e)}") + raise + + def fit_transform(self, documents: List[str]) -> Tuple[List[int], np.ndarray, np.ndarray]: + + try: + logger.info("شروع فرآیند مدل‌سازی موضوعات.") + valid_docs = [str(doc).strip() for doc in documents if str(doc).strip()] + if not valid_docs: + raise ValueError("هیچ سند معتبر برای پردازش ارائه نشده است.") + + logger.info(f"{len(valid_docs)} سند برای پردازش موجود است.") + embeddings = self.embedding_model.encode(valid_docs, show_progress_bar=True) + logger.info("تعبیه‌سازی اسناد تکمیل شد.") + + topics, probs = self.topic_model.fit_transform(valid_docs, embeddings) + logger.info("مدل‌سازی موضوعات با BERTopic انجام شد.") + return topics, probs, embeddings + except Exception as e: + logger.error(f"خطا در fit_transform: {str(e)}") + raise + + def get_topic_info(self) -> Optional[pd.DataFrame]: + try: + topic_info = self.topic_model.get_topic_info() + logger.info("دریافت اطلاعات موضوعات موفقیت‌آمیز بود.") + return topic_info + except Exception as e: + logger.error(f"خطا در دریافت اطلاعات موضوعات: {str(e)}") + return None + + def get_topics(self) -> Optional[Dict[int, List[tuple]]]: + try: + topics = self.topic_model.get_topics() + logger.info("دریافت کلمات کلیدی موضوعات موفقیت‌آمیز بود.") + return topics + except Exception as e: + logger.error(f"خطا در دریافت موضوعات: {str(e)}") + return None diff --git a/notebooks/Vazir-Light.ttf b/notebooks/Vazir-Light.ttf new file mode 100644 index 0000000..e177452 Binary files /dev/null and b/notebooks/Vazir-Light.ttf differ diff --git a/notebooks/data_preprocessing.ipynb b/notebooks/data_preprocessing.ipynb new file mode 100644 index 0000000..54f657b --- /dev/null +++ b/notebooks/data_preprocessing.ipynb @@ -0,0 +1,37 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/feature_experiments.ipynb b/notebooks/feature_experiments.ipynb new file mode 100644 index 0000000..54f657b --- /dev/null +++ b/notebooks/feature_experiments.ipynb @@ -0,0 +1,37 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/processors/integrated_processor.py b/processors/integrated_processor.py new file mode 100644 index 0000000..a61ecae --- /dev/null +++ b/processors/integrated_processor.py @@ -0,0 +1,114 @@ +# processors/integrated_processor.py +import datetime +import logging +import os +from typing import List, Optional, Dict + +import pandas as pd + +from config import Config +from interfaces.categorizer_interface import ICategorizer +from interfaces.document_reader_interface import IDocumentReader +# from interfaces.summarizer_interface import ISummarizer +from interfaces.topic_modeler_interface import ITopicModeler + +logger = logging.getLogger(__name__) + + +class IntegratedProcessor: + + def __init__( + self, + topic_modeler: ITopicModeler, + categorizer: ICategorizer, + document_reader: IDocumentReader, + # summarizer: Optional[ISummarizer], + folder_path: str, + predefined_topics: Optional[List[Dict]] = None + ): + + self.folder_path = folder_path + self.predefined_topics = predefined_topics + self.topic_modeler = topic_modeler + self.categorizer = categorizer + self.document_reader = document_reader + # self.summarizer = summarizer + self.documents = self.document_reader.read_documents(self.folder_path) + + # if self.summarizer: + # logger.info("شروع خلاصه‌سازی اسناد.") + # self.documents = self.summarizer.summarize(self.documents) + + def run(self, use_zero_shot: bool = False) -> pd.DataFrame: + + try: + logger.info("شروع فرآیند یکپارچه مدل‌سازی موضوعات و دسته‌بندی.") + if not self.documents: + logger.warning("هیچ سندی برای پردازش یافت نشد.") + return pd.DataFrame() + + topics, probabilities, embeddings = self.topic_modeler.fit_transform(self.documents) + + if use_zero_shot: + logger.info("استفاده از طبقه‌بندی صفر-شات برای تخصیص برچسب‌ها.") + self.categorizer.assign_automatic_labels() + labeled_topics = [self.categorizer.topic_labels.get(topic, f"موضوع {topic}") for topic in topics] + assigned_topic_probs = [ + probabilities[i][topic] if topic in probabilities[i] else 0 + for i, topic in enumerate(topics) + ] + df = pd.DataFrame({ + 'Document': self.documents, + 'Topic': topics, + 'Assigned Label': labeled_topics, + 'Confidence': assigned_topic_probs + }) + elif self.predefined_topics: + logger.info("استفاده از برچسب‌های از پیش تعریف شده برای تخصیص برچسب‌ها.") + assigned_labels, confidences = self.categorizer.assign_predefined_labels(embeddings, + self.predefined_topics) + df = pd.DataFrame({ + 'Document': self.documents, + 'Assigned Label': assigned_labels, + 'Confidence': confidences + }) + else: + logger.info("استفاده از تخصیص برچسب خودکار بدون طبقه‌بندی صفر-شات.") + self.categorizer.assign_automatic_labels() + labeled_topics = [self.categorizer.topic_labels.get(topic, f"موضوع {topic}") for topic in topics] + assigned_topic_probs = [ + probabilities[i][topic] if topic in probabilities[i] else 0 + for i, topic in enumerate(topics) + ] + df = pd.DataFrame({ + 'Document': self.documents, + 'Topic': topics, + 'Topic Label': labeled_topics, + 'Confidence': assigned_topic_probs + }) + + # مرتب‌سازی و ذخیره نتایج + df = df.sort_values('Confidence', ascending=False).reset_index(drop=True) + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + os.makedirs(Config.RESULTS_DIR, exist_ok=True) + output_file = os.path.join(Config.RESULTS_DIR, f'topic_results_{timestamp}.csv') + df.to_csv(output_file, index=False) + logger.info(f"نتایج در فایل '{output_file}' ذخیره شد.") + + # گروه‌بندی و نمایش اسناد بر اساس دسته‌ها + label_column = 'Assigned Label' if 'Assigned Label' in df.columns else 'Topic Label' + grouped = df.groupby(label_column) + print("\nنتایج دسته‌بندی اسناد بر اساس دسته‌ها:") + print("=" * 80) + for label, group in grouped: + print(f"\nدسته: {label}") + print("-" * 40) + for doc in group['Document']: + print(f"- {doc}") + + logger.info("فرآیند یکپارچه با موفقیت پایان یافت.") + return df + + except Exception as e: + logger.error(f"خطا در پردازش اسناد: {str(e)}") + raise diff --git a/readers/pdf_document_reader.py b/readers/pdf_document_reader.py new file mode 100644 index 0000000..da191a6 --- /dev/null +++ b/readers/pdf_document_reader.py @@ -0,0 +1,39 @@ + #readers/pdf_document_reader.py +import logging +import os +from typing import List + +import pdfplumber + +from interfaces.document_reader_interface import IDocumentReader + +logger = logging.getLogger(__name__) + + +class PDFDocumentReader(IDocumentReader): + + def read_documents(self, folder_path: str) -> List[str]: + + logger.info(f"شروع خواندن فایل‌های PDF از فولدر: {folder_path}") + documents = [] + try: + for filename in os.listdir(folder_path): + if filename.lower().endswith('.pdf'): + file_path = os.path.join(folder_path, filename) + logger.info(f"خواندن فایل: {file_path}") + with pdfplumber.open(file_path) as pdf: + text = '' + for page in pdf.pages: + extracted_text = page.extract_text() + if extracted_text: + text += extracted_text + ' ' + if text.strip(): + documents.append(text.strip()) + logger.info(f"متن فایل '{filename}' با موفقیت استخراج شد.") + else: + logger.warning(f"هیچ متنی از فایل '{filename}' استخراج نشد.") + logger.info("تمامی فایل‌های PDF با موفقیت خوانده شدند.") + except Exception as e: + logger.error(f"خطا در خواندن فایل‌های PDF: {str(e)}") + raise + return documents diff --git a/readers/word_document_reader.py b/readers/word_document_reader.py new file mode 100644 index 0000000..61bcefa --- /dev/null +++ b/readers/word_document_reader.py @@ -0,0 +1,42 @@ +# readers/word_document_reader.py +import logging +import os +from typing import List + +from docx import Document + +from interfaces.document_reader_interface import IDocumentReader + +# interfaces/document_reader_interface.py + +logger = logging.getLogger(__name__) + + +class WordDocumentReader(IDocumentReader): + + def read_documents(self, folder_path: str) -> List[str]: + + logger.info(f"شروع خواندن فایل‌های Word از فولدر: {folder_path}") + documents = [] + try: + for filename in os.listdir(folder_path): + if filename.lower().endswith('.docx'): + file_path = os.path.join(folder_path, filename) + logger.info(f"خواندن فایل: {file_path}") + try: + doc = Document(file_path) + text = '' + for paragraph in doc.paragraphs: + text += paragraph.text + ' ' + if text.strip(): + documents.append(text.strip()) + logger.info(f"متن فایل '{filename}' با موفقیت استخراج شد.") + else: + logger.warning(f"هیچ متنی از فایل '{filename}' استخراج نشد.") + except Exception as e: + logger.error(f"خطا در خواندن فایل '{filename}': {str(e)}") + logger.info("تمامی فایل‌های Word با موفقیت خوانده شدند.") + except Exception as e: + logger.error(f"خطا در خواندن فایل‌های Word: {str(e)}") + raise + return documents diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..912a7df --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +bertopic==0.16.4 +fpdf==1.7.2 +numpy==1.24.3 +pandas==2.2.3 +pdfplumber==0.11.4 +python-dotenv==1.0.1 +python_docx==1.1.2 +scikit_learn==1.5.2 +sentence_transformers==3.1.0 +streamlit==1.39.0 +transformers==4.44.2 diff --git a/summarizers/test_transformers_summarizer.py b/summarizers/test_transformers_summarizer.py new file mode 100644 index 0000000..1dd0501 --- /dev/null +++ b/summarizers/test_transformers_summarizer.py @@ -0,0 +1,31 @@ +# tests/test_transformers_summarizer.py +import unittest + +from summarizers.transformers_summarizer import TransformersSummarizer + + +class TestTransformersSummarizer(unittest.TestCase): + + def setUp(self): + # استفاده از یک مدل خلاصه‌سازی کوچک برای تست + self.summarizer = TransformersSummarizer(model_name="facebook/bart-large-cnn") + self.texts = [ + "این یک متن طولانی برای تست خلاصه‌سازی است. هدف ما اطمینان از صحت عملکرد خلاصه‌ساز است.", + "پایتون یک زبان برنامه‌نویسی قدرتمند و منعطف است که در زمینه‌های مختلفی مورد استفاده قرار می‌گیرد." + ] + + def test_summarize(self): + summaries = self.summarizer.summarize(self.texts) + self.assertEqual(len(summaries), len(self.texts)) + for summary, original in zip(summaries, self.texts): + self.assertTrue(len(summary) < len(original)) + self.assertIsInstance(summary, str) + + def test_summarize_empty_text(self): + summaries = self.summarizer.summarize([""]) + self.assertEqual(len(summaries), 1) + self.assertEqual(summaries[0], "") + + +if __name__ == '__main__': + unittest.main() diff --git a/summarizers/transformers_summarizer.py b/summarizers/transformers_summarizer.py new file mode 100644 index 0000000..dbe7834 --- /dev/null +++ b/summarizers/transformers_summarizer.py @@ -0,0 +1,40 @@ +# summarizers/transformers_summarizer.py +import logging +from typing import List + +from transformers import pipeline + +from config import Config +from interfaces.summarizer_interface import ISummarizer + +logger = logging.getLogger(__name__) + + +class TransformersSummarizer(ISummarizer): + + + def __init__(self, model_name: str = Config.SUMMARIZER_MODEL): + + try: + logger.info(f"بارگذاری مدل خلاصه‌سازی: {model_name}") + self.summarizer = pipeline("summarization", model=model_name) + logger.info("مدل خلاصه‌سازی با موفقیت بارگذاری شد.") + except Exception as e: + logger.error(f"خطا در بارگذاری مدل خلاصه‌سازی: {str(e)}") + raise + + def summarize(self, texts: List[str]) -> List[str]: + + logger.info("شروع خلاصه‌سازی متون.") + summaries = [] + try: + for text in texts: + # توجه: برخی مدل‌ها محدودیت طول ورودی دارند. ممکن است نیاز به تقسیم متن به بخش‌های کوچکتر باشد. + summary = self.summarizer(text, max_length=130, min_length=30, do_sample=False) + summaries.append(summary[0]['summary_text']) + logger.debug(f"خلاصه متن: {summary[0]['summary_text']}") + logger.info("خلاصه‌سازی متون با موفقیت انجام شد.") + return summaries + except Exception as e: + logger.error(f"خطا در خلاصه‌سازی متون: {str(e)}") + raise