initial commit

This commit is contained in:
bvn13 2024-10-29 00:17:54 +03:00
commit 61e345a961
19 changed files with 338 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
env
env/**
.idea
.idea/**

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

105
README.md Normal file
View File

@ -0,0 +1,105 @@
# AntiSpam complex
Комплекс ПО для борьбы со спамом
## Компоненты
### Диаграмма компонентов
![c4model.png](docs/c4model.png)
### Dataset
- необходим первоначальный датасет (ham/spam) в формате CSV
### Model
- использует датасет
- модели генерируются https://pypi.org/project/spam-detector-ai/
- naive_bayes_model.joblib
- random_forest_model.joblib
- svm_model.joblib
- logistic_regression_model.joblib
- xgb_model.joblib
### Decision Maker
- компонент, который принимает решения
- представлен в виде Web-сервера
- реализует API
- POST `/check-spam` - открытый для пользователей
- auth: none
- request
- body `{ "text": "SOME TEXT" }
- response
- status code 200
- body `{ "is_spam": true }`
- POST `/update-model` - служебный, добавляет текст в датасет
- auth: TOKEN
- request
- body: `{ "text": "SOME TEXT", "is_spam": true }`
- response
- status code 200
- body `{ "status": "OK" }`
- POST `/restart` - служебный, перезапускает данный компонент
- auth: TOKEN
- request
- body: none
- response
- status code 200
- body `{ "status": "OK" }`
#### Sequence diagram
![sequence](docs/sequence.png)
### Model Updater
- добавляет в датасет новые тексты из базы дообучения
- запускается по графику, выполняет
- сделать бэкап модели
- запустить дообучение
- при успешном результате дообучения
- вызывает POST `/restart` компонента Decision Maker
### Transport
- используется [Rabbit MQ](https://rabbitmq.com/)
## Use cases
### Телеграм-бот
- приходит сообщение в группу в телегу
- бот его читает (не входит в данный комплекс)
- отправляет на проверку
- получает результат: спам/не спам
- если спам - бот удаляет сообщение
### На почте
- та же схема, только должен быть работающий клиент почты, который читает каждое сообщение, и если обнаружился спам, то помечать его спамом (помещать в категорию спам)
## Дообучение с учителем
### Сбор доп.текстов через Телеграм
- владелец бота в телеге встречает сообщение, которое является спамом, но не было удалено
- это сообщение отправляется в ТГ бот спам-определителя (не реализовано)
- сообщение помещается в базу для дообучения
### Сбор доп.текстов через почту
- спам-письмо можно отправить на почту, которую читает бот (не реализовано)
- бот кладет текст письма в базу дообучения
### Дообучение
- 1 р/сут происходит вычитывание всех сообщений из базы дообучения за день
- если в базе дообучения за день что-то есть, то
- датасет обновляется
- запускается процесс дообучения
- перезапускается модель

BIN
docs/c4model.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
docs/sequence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

26
pyproject.toml Normal file
View File

@ -0,0 +1,26 @@
[tool.poetry]
name = "spam-detector-1"
version = "0.1.0"
description = ""
authors = ["bvn13 <from.github@bvn13.me>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
scikit-learn = "^1.5.2"
numpy = "^2.1.2"
spam-detector-ai = "^2.1.18"
nltk = "^3.9.1"
joblib = "^1.4.2"
xgboost = "^2.1.2"
imblearn = "^0.0"
requests = "^2.32.3"
googletrans = "^4.0.0rc1"
tqdm = "^4.66.5"
tornado = "^6.4.1"
asyncio = "^3.4.3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

0
src/__init__.py Normal file
View File

27
src/app.py Normal file
View File

@ -0,0 +1,27 @@
import argparse
import os
from dataset.preparer import download_words
from model.trainer import train
import web.server
import model.updater
parser = argparse.ArgumentParser(prog='Antispam complex')
parser.add_argument('-i', '--init', action=argparse.BooleanOptionalAction, help='Initializing, must be run beforehand, --dataset is required')
parser.add_argument('-m', '--decision-maker', action=argparse.BooleanOptionalAction, help='Start as Decision maker')
parser.add_argument('-d', '--dataset', required=False, help='Path to CSV (ham/spam) dataset')
parser.add_argument('-u', '--model-updater', required=False, help='Start as Model updater')
args = parser.parse_args()
_port = 8080 if os.getenv('PORT') is None else os.getenv('PORT')
if __name__ == '__main__':
if args.init:
assert args.dataset is not None, "Dataset is required, show --help"
download_words()
train(args.dataset)
elif args.decision_maker:
web.server.start(port=_port)
elif args.model_updater:
model.updater.start()

0
src/dataset/__init__.py Normal file
View File

5
src/dataset/preparer.py Normal file
View File

@ -0,0 +1,5 @@
import nltk
def download_words() -> None:
nltk.download('wordnet')
nltk.download('stopwords')

0
src/logging/__init__.py Normal file
View File

5
src/logging/logger.py Normal file
View File

@ -0,0 +1,5 @@
import logging
logging.basicConfig(format="%(asctime)s | %(name)s | %(levelname)s | %(message)s")
logger = logging.getLogger(__package__)
logger.setLevel(logging.INFO)

0
src/model/__init__.py Normal file
View File

40
src/model/trainer.py Normal file
View File

@ -0,0 +1,40 @@
# spam_detector_ai/trainer.py
import os
import sys
from pathlib import Path
from sklearn.model_selection import train_test_split
from spam_detector_ai.classifiers.classifier_types import ClassifierType
from spam_detector_ai.training.train_models import ModelTrainer
from src.logging.logger import logger
def _train_model(classifier_type, model_filename, vectoriser_filename, X_train, y_train):
logger.info(f'Training {classifier_type}')
trainer_ = ModelTrainer(data=None, classifier_type=classifier_type, logger=logger)
trainer_.train(X_train, y_train)
trainer_.save_model(model_filename, vectoriser_filename)
def train(data_path: str) -> None:
# Load and preprocess data once
# data_path = os.path.join(project_root, 'spam.csv')
initial_trainer = ModelTrainer(data_path=data_path, logger=logger)
processed_data = initial_trainer.preprocess_data_()
# Split the data once
X__train, _, y__train, _ = train_test_split(processed_data['processed_text'], processed_data['label'],
test_size=0.2, random_state=0)
# Configurations for each model
configurations = [
(ClassifierType.SVM, 'svm_model.joblib', 'svm_vectoriser.joblib'),
(ClassifierType.NAIVE_BAYES, 'naive_bayes_model.joblib', 'naive_bayes_vectoriser.joblib'),
(ClassifierType.RANDOM_FOREST, 'random_forest_model.joblib', 'random_forest_vectoriser.joblib'),
(ClassifierType.XGB, 'xgb_model.joblib', 'xgb_vectoriser.joblib'),
(ClassifierType.LOGISTIC_REGRESSION, 'logistic_regression_model.joblib', 'logistic_regression_vectoriser.joblib')
]
# Train each model with the pre-split data
logger.info(f"Train each model with the pre-split data\n")
for ct, mf, vf in configurations:
_train_model(ct, mf, vf, X__train, y__train)

5
src/model/updater.py Normal file
View File

@ -0,0 +1,5 @@
from src.logging.logger import logger
def start() -> None:
logger.info("Starting...")

75
src/translate-dataset.py Normal file
View File

@ -0,0 +1,75 @@
# https://thepythoncode.com/article/translate-text-in-python
from os import close
from googletrans import Translator
import csv
from tqdm import tqdm
import argparse
import sys
import os.path
import shutil
csv.field_size_limit(sys.maxsize)
parser = argparse.ArgumentParser(prog='translate-dataset')
parser.add_argument('-i', '--input', help='Source file')
parser.add_argument('-o', '--output', help='Destination file')
args = parser.parse_args()
#/home/bvn13/develop/spam-detector-1/spam.csv
translator = Translator()
translation = translator.translate("Hola Mundo", dest="ru")
print(f"{translation.origin} ({translation.src}) --> {translation.text} ({translation.dest})")
total = 0
with open(args.input, "r") as f:
reader = csv.reader(f)
for row in reader:
total += 1
skip = 0
bup = None
if os.path.exists(args.output):
bup = f"{args.output}.bup"
shutil.copyfile(args.output, bup)
with open(args.output, "r") as f:
reader = csv.reader(f)
for row in reader:
skip += 1
progress = tqdm(total=total, unit='row', unit_scale=2)
n = 0
with open(args.input, "r") as f:
with open(args.output, "w") as tf:
bupf = None
bupcsv = None
if bup is not None:
bupf = open(bup, "r")
bupcsv = csv.reader(bupf)
next(bupcsv)
try:
reader = csv.reader(f)
progress.update(1)
ru = csv.writer(tf, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
ru.writerow(['label', 'text'])
header = next(reader)
skipped = 1
for row in reader:
progress.update(1)
decision = row[0]
text = row[1]
if skipped < skip:
skipped += 1
already_translated = next(bupcsv)
ru.writerow(already_translated)
else:
try:
translated_text = translator.translate(text, dest='ru')
ru.writerow([decision] + [translated_text.text])
except Exception as e:
print(f"Skipping line: {e}")
except Exception as e:
print(e)
finally:
if bupf is not None:
close(bupf)

0
src/web/__init__.py Normal file
View File

42
src/web/server.py Normal file
View File

@ -0,0 +1,42 @@
import asyncio
import json
import os
import tornado
from spam_detector_ai.prediction.predict import VotingSpamDetector
from src.logging.logger import logger
_spam_detector = VotingSpamDetector()
def _json(data) -> str:
return json.dumps(data)
def start(port: int) -> None:
logger.info("Starting...")
class CheckSpamHandler(tornado.web.RequestHandler):
def set_default_headers(self):
self.set_header("Access-Control-Allow-Origin", "*")
def get(self):
body = json.loads(self.request.body)
if not 'text' in body:
self.write_error(400, body=_json({"error": "text is not specified"}))
else:
r = json.dumps({"is_spam": _spam_detector.is_spam(body['text'])})
self.write(r)
async def start_web_server():
logger.info(f"Starting web server on port {port}")
app = tornado.web.Application(
[
(r"/check-spam", CheckSpamHandler),
],
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
)
app.listen(port)
await asyncio.Event().wait()
asyncio.run(start_web_server())

1
version Normal file
View File

@ -0,0 +1 @@
0.0.1-beta