initial commit
This commit is contained in:
commit
61e345a961
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
env
|
||||
env/**
|
||||
.idea
|
||||
.idea/**
|
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
105
README.md
Normal file
105
README.md
Normal 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
BIN
docs/c4model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
BIN
docs/sequence.png
Normal file
BIN
docs/sequence.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
26
pyproject.toml
Normal file
26
pyproject.toml
Normal 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
0
src/__init__.py
Normal file
27
src/app.py
Normal file
27
src/app.py
Normal 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
0
src/dataset/__init__.py
Normal file
5
src/dataset/preparer.py
Normal file
5
src/dataset/preparer.py
Normal file
@ -0,0 +1,5 @@
|
||||
import nltk
|
||||
|
||||
def download_words() -> None:
|
||||
nltk.download('wordnet')
|
||||
nltk.download('stopwords')
|
0
src/logging/__init__.py
Normal file
0
src/logging/__init__.py
Normal file
5
src/logging/logger.py
Normal file
5
src/logging/logger.py
Normal 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
0
src/model/__init__.py
Normal file
40
src/model/trainer.py
Normal file
40
src/model/trainer.py
Normal 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
5
src/model/updater.py
Normal file
@ -0,0 +1,5 @@
|
||||
from src.logging.logger import logger
|
||||
|
||||
|
||||
def start() -> None:
|
||||
logger.info("Starting...")
|
75
src/translate-dataset.py
Normal file
75
src/translate-dataset.py
Normal 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
0
src/web/__init__.py
Normal file
42
src/web/server.py
Normal file
42
src/web/server.py
Normal 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())
|
Loading…
Reference in New Issue
Block a user