From 512afc28bdb9bd758ce5493cd561a61a374a1a95 Mon Sep 17 00:00:00 2001 From: mansoor hamidzadeh Date: Thu, 31 Oct 2024 11:43:09 +0330 Subject: [PATCH] init --- .gitignore | 313 ++++++++++++++++++++ .idea/.gitignore | 8 + .idea/AI-Data-Security.iml | 8 + .idea/Topic-Modeling.iml | 8 + .idea/csv-editor.xml | 16 + .idea/misc.xml | 10 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + app.py | 111 +++++++ config.py | 14 + interfaces/categorizer_interface.py | 15 + interfaces/document_reader_interface.py | 9 + interfaces/summarizer_interface.py | 9 + interfaces/topic_modeler_interface.py | 19 ++ logger.py | 28 ++ main.py | 66 +++++ models/categorizer.py | 106 +++++++ models/topic_modeler.py | 92 ++++++ notebooks/Vazir-Light.ttf | Bin 0 -> 105892 bytes notebooks/data_preprocessing.ipynb | 37 +++ notebooks/feature_experiments.ipynb | 37 +++ processors/integrated_processor.py | 114 +++++++ readers/pdf_document_reader.py | 39 +++ readers/word_document_reader.py | 42 +++ requirements.txt | 11 + summarizers/test_transformers_summarizer.py | 31 ++ summarizers/transformers_summarizer.py | 40 +++ 27 files changed, 1197 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AI-Data-Security.iml create mode 100644 .idea/Topic-Modeling.iml create mode 100644 .idea/csv-editor.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 app.py create mode 100644 config.py create mode 100644 interfaces/categorizer_interface.py create mode 100644 interfaces/document_reader_interface.py create mode 100644 interfaces/summarizer_interface.py create mode 100644 interfaces/topic_modeler_interface.py create mode 100644 logger.py create mode 100644 main.py create mode 100644 models/categorizer.py create mode 100644 models/topic_modeler.py create mode 100644 notebooks/Vazir-Light.ttf create mode 100644 notebooks/data_preprocessing.ipynb create mode 100644 notebooks/feature_experiments.ipynb create mode 100644 processors/integrated_processor.py create mode 100644 readers/pdf_document_reader.py create mode 100644 readers/word_document_reader.py create mode 100644 requirements.txt create mode 100644 summarizers/test_transformers_summarizer.py create mode 100644 summarizers/transformers_summarizer.py 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 0000000000000000000000000000000000000000..e17745276f516f843038ac781c0e5282c8857f0c GIT binary patch literal 105892 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 LGmQbOQnCIG+^>6J literal 0 HcmV?d00001 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