├── .black ├── .flake8 ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE.MD ├── README.MD ├── assets ├── autoML.gif ├── classicML.gif ├── datasets.gif ├── demo_main.gif ├── image_ai.gif └── sound_ai.gif ├── datasets └── classification │ └── README.MD ├── models ├── image_classification │ ├── notebooks │ │ ├── README.MD │ │ └── image_classification.ipynb │ └── utils │ │ ├── class_to_idx.json │ │ ├── classification_dataset.zip │ │ ├── classification_neural_network.py │ │ ├── config.yaml │ │ ├── hyperparams.json │ │ ├── inference.py │ │ └── requirements.txt └── sound_classification │ ├── notebooks │ ├── README.MD │ └── sound_classification.ipynb │ └── utils │ ├── class_to_idx.json │ ├── config.yaml │ ├── hyperparams.json │ ├── inference.py │ ├── requirements.txt │ ├── sound_classification_datasphere.py │ ├── sound_dataset.zip │ ├── test │ └── test.wav │ └── trained_model_sound_classification.pt ├── paper ├── КР_Ли_Владислав │ ├── Lee_Vladislav_KR.pdf │ └── Lee_Vladislav_KR.rar ├── КР_Мухаметшин_Руслан │ ├── Mukhametshin_Ruslan_source.zip │ └── КР_Мухаметшин_Руслан.pdf └── КР_Пестов_Лев │ ├── Pestov_Lev_source.zip │ └── КР_Пестов_Лев.pdf ├── pre-commit ├── requirements.txt └── web_service ├── __init__.py ├── app ├── AutoMl │ ├── README.md │ ├── __init__.py │ ├── automl │ │ ├── __init__.py │ │ ├── comparison.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── bayesian_optimization.py │ │ │ └── evolution_strategy.py │ └── test │ │ ├── __init__.py │ │ ├── test_classification.py │ │ └── test_regression.py ├── ML_snippet.py ├── __init__.py ├── automlFitter.py ├── exceptions │ └── folderNotFoundError.py ├── extentions.py ├── fitter.py ├── models.py ├── routes │ ├── auto_ml.py │ ├── dataset_manager.py │ ├── image.py │ ├── image_predictor.py │ ├── main.py │ ├── ml_manager.py │ ├── sound_classification.py │ ├── sound_predictor.py │ ├── table_processor.py │ └── tracking.py └── templates │ ├── auto_ml.html │ ├── base.html │ ├── dataset_manager.html │ ├── image_prediction.html │ ├── image_prediction_result.html │ ├── index.html │ ├── ml_manager.html │ ├── sound_classification.html │ ├── sound_prediction.html │ ├── sound_prediction_result.html │ ├── structure.txt │ ├── table_processing.html │ ├── tracking.html │ ├── tracking_automl_detail.html │ ├── tracking_detail.html │ └── upload_image.html ├── requirements.txt ├── run.py └── utils ├── image_classification ├── classification_neural_network.py ├── config.yaml ├── inference.py └── requirements.txt └── sound_classification ├── config.yaml ├── inference.py ├── requirements.txt └── sound_classification_datasphere.py /.black: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,__pycache__, 3 | max-line-length = 79 4 | ignore = E203, W503, E501 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | .idea/ 164 | 165 | 166 | # utils 167 | /web_service/images 168 | /web_service/datasets 169 | /web_service/app/static 170 | /web_service/mlruns 171 | /web_service/predictions 172 | /web_service/sounds 173 | /web_service/utils/image_classification/trained_model_classification.pt 174 | /web_service/utils/image_classification/class_to_idx.json 175 | /web_service/utils/image_classification/classification_dataset.zip 176 | /web_service/utils/image_classification/hyperparams.json 177 | /web_service/utils/sound_classification/trained_model_sound_classification.pt 178 | /web_service/utils/sound_classification/class_to_idx.json 179 | /web_service/utils/sound_classification/sound_dataset.zip 180 | /web_service/utils/sound_classification/hyperparams.json 181 | 182 | #dataset for classification 183 | datasets/classification/dogvscat_small 184 | 185 | #trained model 186 | models/image_classification/utils/trained_model_classification.pt 187 | 188 | #predictions 189 | models/image_classification/utils/predictions 190 | /models/sound_classification/utils/predictions/ 191 | /models/sound_classification/utils/predictions_sound/ 192 | /models/sound_classification/utils/trained_model_sound_classification.pt 193 | /models/sound_classification/utils/test/ 194 | 195 | /web_service/utils/trained_model_classification.pt 196 | /web_service/utils/hyperparams.json 197 | /web_service/utils/classification_dataset.zip 198 | /web_service/utils/class_to_idx.json 199 | !/README.MD 200 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | include_trailing_comma = true 3 | line_length = 79 4 | lines_after_imports = 2 -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.8.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/pycqa/isort 8 | rev: 5.13.2 9 | hooks: 10 | - id: isort 11 | 12 | - repo: https://github.com/pycqa/flake8 13 | rev: 6.0.0 14 | hooks: 15 | - id: flake8 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | WORKDIR /app 3 | COPY web_service/requirements.txt ./requirements.txt 4 | RUN pip install --no-cache-dir --upgrade pip \ 5 | && pip install --no-cache-dir -r requirements.txt 6 | COPY . . 7 | ENV FLASK_APP=web_service/run.py 8 | ENV PYTHONPATH=/app/web_service 9 | ENV PYTHONUNBUFFERED=1 10 | EXPOSE 5001 11 | CMD ["flask", "run", "--host=0.0.0.0", "--port=5001"] 12 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [lpestov] [MRuslanR] [VlaLee] [vard3nz] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Interactive AI Model Builder 🤖✨ 2 | 3 | Interactive AI Model Builder is a user-friendly web application that empowers you to train custom AI models for image, sound classification and different classic ML + autoML experiments directly in your browser. No deep coding expertise required – just bring your data, and let the platform guide you through the process! 4 | 5 | ![Demo](assets/demo_main.gif) 6 | 7 | ## Key Features 8 | 9 | * 🌐 **Intuitive Web Interface:** Easily define classes, upload your datasets, and configure training parameters. 10 | * ⚡ **On-the-Fly Model Training:** Train models for image and sound classification and classic ml tasks. 11 | * 📊 **Real-time Training Logs:** Monitor the training progress directly in the application. 12 | * 📥 **Downloadable Models:** Export your trained models and usage instructions for local inference. 13 | * 💻 **GPU not required:** The application is designed to run efficiently on CPU, making it accessible for users without high-end hardware. 14 | * 🚀 **Powered by PyTorch & Scikit-learn & Yandex DataSphere:** Leverages robust ML & DL frameworks and cloud computing for efficient training. 15 | 16 | ## 🖼️ Image Classification 17 | 18 | Train your own image classifiers by providing example images for different categories. The application handles the complexities of model training, allowing you to focus on your data. 19 | 20 | ![image_ai](assets/image_ai.gif) 21 | 22 | ### How It Works: 23 | 24 | 1. **Define Classes:** Specify the categories you want to classify (e.g., "Cat", "Dog", "Car"). 25 | 2. **Upload Images:** Provide at least 15 images per class in JPG, JPEG, or PNG format. 26 | 3. **Configure Hyperparameters:** Adjust settings like epochs, learning rate, and batch size (sensible defaults are provided). 27 | 4. **Train Model:** The backend uses a pre-trained model (EfficientNet B0) as a feature extractor and trains a custom classification head on your data using Yandex DataSphere. 28 | 5. **Predict & Download:** Test your model with new images via the web interface or download the trained model (`.pt`), class mappings (`.json`), and a Python script for local predictions. 29 | 30 | ## 🎵 Sound Classification 31 | 32 | Classify various sound events by training a model on your audio samples. From environmental sounds to specific audio cues, build a classifier tailored to your needs. 33 | 34 | ![sound_ai](assets/sound_ai.gif) 35 | 36 | ### How It Works: 37 | 38 | 1. **Define Classes:** Specify the sound categories (e.g., "Dog Bark", "Siren", "Keyboard Click"). 39 | 2. **Upload Audio:** Provide at least 15 audio samples per class (WAV, MP3, FLAC). 40 | 3. **Configure Hyperparameters:** Adjust training parameters, including data augmentation settings. 41 | 4. **Train Model:** The system uses Pre-trained Audio Neural Networks (PANNs) for feature extraction and trains a custom MLP classifier on your audio data using Yandex DataSphere. 42 | 5. **Predict & Download:** Analyze new audio files through the web UI or download the trained MLP model (`.pt`), class mappings (`.json`), and a Python script for local classification using PANNs embeddings. 43 | 44 | ## 📊 Classic ML & AutoML (Tabular Data) 45 | 46 | Easily create and deploy predictive models for structured data. This section enables users to solve classification and regression problems through intuitive workflows that balance manual customization with automated algorithms. 47 | 48 | 49 | ### How It Works: 50 | 51 | * **Upload dataset in CSV format and specify the task type** (supported types: classification, regression) 52 | ![Dataset](assets/datasets.gif) 53 | 54 | * **Feature Engineering:** - Perform dataset processing steps or apply automated data processing (*Coming Soon*) 55 | * **Model Selection & Training:** - Choose desired model and configure hyperparameters, or select an AutoML algorithm to handle it automatically! 56 | **AutoML:** 57 | ![autoML](assets/autoML.gif) 58 | **Or classic ML:** 59 | ![classicML](assets/classicML.gif) 60 | * **Model Tracking & Export:** - Track model performance and view detailed metrics on the tracking page. Download trained models (.pt format) with usage code. 61 | 62 | 63 | ## 🚀 Getting Started 64 | 65 | * **Clone Repository:** `git clone git@github.com:lpestov/interactive_ai_model_builder.git` 66 | * **Install Dependencies:** `pip install -r requirements.txt` 67 | * **Configuration:** `.env` file setup (Yandex Cloud credentials, etc.) 68 | > ⚠️ **IMPORTANT:** You need to get access to Yandex DataSphere for the application to work. Without this access, model training will not be possible! 69 | * **Running the App:** `flask run` 70 | * **Docker:** `docker build -t interactive-ai-builder .` then `docker run -p 5001:5001 interactive-ai-builder` 71 | 72 | ## 📬 Contacts 73 | 74 | If you have any questions or need help with the project: 75 | 76 | * **Telegram:** 77 | * [@lpestov](https://t.me/KersolWIs) - AI Builder Pipeline Engineer (For Yandex DataSphere access) 78 | * [@MRuslanR](https://t.me/Whoisaltyn) - Classic ML & AutoML Engineer 79 | * [@VlaLee](https://t.me/v7lee) - Full Stack Developer 80 | 81 | ## ❤️ Made with Love 82 | -------------------------------------------------------------------------------- /assets/autoML.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/autoML.gif -------------------------------------------------------------------------------- /assets/classicML.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/classicML.gif -------------------------------------------------------------------------------- /assets/datasets.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/datasets.gif -------------------------------------------------------------------------------- /assets/demo_main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/demo_main.gif -------------------------------------------------------------------------------- /assets/image_ai.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/image_ai.gif -------------------------------------------------------------------------------- /assets/sound_ai.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/assets/sound_ai.gif -------------------------------------------------------------------------------- /datasets/classification/README.MD: -------------------------------------------------------------------------------- 1 | ## Инструкции по загрузке датасета 2 | 3 | Для использования датасета для классификации, пожалуйста, скачайте его по следующей ссылке: 4 | 5 | [Скачать датасет](https://drive.google.com/file/d/1dWzozDWPf_MF2sc7LQm7buy-QYGzzFZh/view?usp=sharing) 6 | 7 | Кроме того, вы можете использовать `gdown` для загрузки датасета напрямую из командной строки: 8 | 9 | ```bash 10 | !gdown 1dWzozDWPf_MF2sc7LQm7buy-QYGzzFZh 11 | ``` -------------------------------------------------------------------------------- /models/image_classification/notebooks/README.MD: -------------------------------------------------------------------------------- 1 | ## Открыть в Google Colab 2 | 3 | Чтобы открыть этот ноутбук в Colab, нажмите кнопку ниже: 4 | 5 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://drive.google.com/file/d/1mKX3VZRPfFSa-JW0ApkZY-K0YQjsP7Ds/view?usp=sharing) -------------------------------------------------------------------------------- /models/image_classification/utils/class_to_idx.json: -------------------------------------------------------------------------------- 1 | { 2 | "dog": 0, 3 | "cat": 1, 4 | "chinchilla": 2 5 | } -------------------------------------------------------------------------------- /models/image_classification/utils/classification_dataset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/models/image_classification/utils/classification_dataset.zip -------------------------------------------------------------------------------- /models/image_classification/utils/classification_neural_network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # -- Данный код предназначен для загрузки модели в Yandex DataSphere, и не ориентирован для дальнейших экспериментов -- 5 | 6 | 7 | """classification.ipynb 8 | 9 | Automatically generated by Colab. 10 | 11 | Original file is located at 12 | https://colab.research.google.com/drive/1mKX3VZRPfFSa-JW0ApkZY-K0YQjsP7Ds 13 | 14 | ## Выбор модели для задач классификации 15 | 16 | Для задач классификации существует несколько способов выбора модели: 17 | 18 | 1. **Обучить маленькую модель с нуля**: Выбор легковесной архитектуры модели и полное обучение её на нашем наборе данных. 19 | 2. **Fine-Tuning предобученной модели**: Используем модель, которая была предобучена авторами на другом наборе данных, и только последние слои переобучается на нашем конкретном наборе данных. 20 | 3. **Использовать предобученные веса напрямую**: В этом методе используется предобученная модель без дополнительного обучения. 21 | 22 | Лучшим вариантом является Fine-Tuning, поэтому в качестве бейзлайна возьмем маленькую (5.3M) EfficientNet B0 с претрейновыми параметрами 23 | 24 | [Оригинальная статья](https://arxiv.org/abs/1905.11946) 25 | """ 26 | 27 | import json 28 | import os 29 | import sys 30 | import zipfile 31 | 32 | import torch 33 | import torch.nn as nn 34 | import torch.optim as optim 35 | from PIL import Image 36 | from torch.utils.data import DataLoader, Dataset 37 | from torchvision import datasets, transforms 38 | from torchvision.models import EfficientNet_B0_Weights, efficientnet_b0 39 | 40 | 41 | if __name__ == "__main__": 42 | print("Starting") 43 | 44 | # Загрузка гиперпараметров из JSON (создайте папку classification, если её еще нет в корне диска Google Drive) 45 | with open("hyperparams.json", "r") as f: 46 | hyperparams = json.load(f) 47 | 48 | # Загрузка названий классов из JSON 49 | with open("class_to_idx.json", "r") as f: 50 | class_to_idx = json.load(f) 51 | 52 | # Создание обратного словаря idx_to_class 53 | idx_to_class = {v: k for k, v in class_to_idx.items()} 54 | 55 | """ 56 | **Структура** classification_dataset.zip*: (внутри папка с таким же названием) 57 | 58 | ``` 59 | classification_dataset/ 60 | ├── train/ 61 | │ ├── class_name1/ 62 | │ └── class_name2/ 63 | │ ... 64 | ``` 65 | """ 66 | local_zip = "classification_dataset.zip" 67 | zip_ref = zipfile.ZipFile(local_zip, "r") 68 | zip_ref.extractall() 69 | zip_ref.close() 70 | 71 | # Класс для аугментации данных 72 | class AugmentedDataset(Dataset): 73 | def __init__(self, original_folder, target_size=50, transform=None): 74 | self.transform = transform 75 | self.samples = [] 76 | self.class_to_idx = class_to_idx 77 | 78 | # Проверяем, что все классы из JSON есть в папке 79 | class_names = os.listdir(original_folder) 80 | missing_classes = [ 81 | cls for cls in class_to_idx.keys() if cls not in class_names 82 | ] 83 | if missing_classes: 84 | raise ValueError( 85 | f"Классы {missing_classes} из class_to_idx.json отсутствуют в папке {original_folder}" 86 | ) 87 | 88 | # Собираем пути к изображениям для каждого класса 89 | for class_name in class_names: # Используем отсортированные имена классов 90 | class_path = os.path.join(original_folder, class_name) 91 | images = [ 92 | os.path.join(class_path, img) for img in os.listdir(class_path) 93 | ] 94 | # Повторяем изображения до достижения целевого размера 95 | for i in range(target_size): 96 | self.samples.append((images[i % len(images)], class_name)) 97 | 98 | # Возвращает общее количество элементов в наборе данных 99 | def __len__(self): 100 | return len(self.samples) 101 | 102 | # Возвращает одно изображение и его метку по индексу 103 | def __getitem__(self, idx): 104 | img_path, class_name = self.samples[idx] 105 | image = Image.open(img_path).convert("RGB") 106 | 107 | # Применяем аугментации 108 | if self.transform: 109 | image = self.transform(image) 110 | 111 | # Преобразуем метку класса в числовой формат 112 | label = self.class_to_idx[class_name] # Преобразуем имя класса в индекс 113 | return image, label # Оставляем числовую метку для обучения 114 | 115 | # Трансформы с аугментациями для тренировочных данных 116 | train_transform = transforms.Compose( 117 | [ 118 | transforms.Resize((224, 224)), 119 | transforms.RandomHorizontalFlip(), 120 | transforms.RandomRotation(15), 121 | transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), 122 | transforms.ToTensor(), 123 | transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), 124 | ] 125 | ) 126 | 127 | # Создание аугментированных датасетов 128 | train_dataset = AugmentedDataset( 129 | original_folder="classification_dataset/train", 130 | target_size=50, 131 | transform=train_transform, 132 | ) 133 | 134 | # DataLoader 135 | train_loader = DataLoader( 136 | train_dataset, batch_size=hyperparams["batch_size"], shuffle=True 137 | ) 138 | 139 | # Проверим классы 140 | print(train_dataset.class_to_idx) 141 | 142 | # Загрузка модели EfficientNet 143 | model = efficientnet_b0(EfficientNet_B0_Weights.DEFAULT) 144 | 145 | # Заморозка всех слоев, кроме последнего 146 | for param in model.parameters(): 147 | param.requires_grad = False 148 | 149 | # Замена финального классификатора 150 | num_features = model.classifier[1].in_features 151 | num_classes = len(class_to_idx) # Количество классов из JSON 152 | model.classifier = nn.Sequential( 153 | nn.Linear(num_features, 128), 154 | nn.ReLU(), 155 | nn.Linear(128, num_classes), # Многоклассовая классификация 156 | # На выходе оставляем сырые логиты, так как nn.CrossEntropyLoss() их ожидает на вход 157 | ) 158 | 159 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 160 | model.to(device) 161 | 162 | criterion = nn.CrossEntropyLoss() # Для многоклассовой классификации 163 | optimizer = torch.optim.Adam( 164 | model.parameters(), 165 | lr=hyperparams["learning_rate"], 166 | weight_decay=hyperparams["weight_decay"], 167 | ) 168 | 169 | # Обучение модели 170 | print("Validation on the Train Dataset!") 171 | for epoch in range(hyperparams["num_epochs"]): 172 | model.train() 173 | train_loss = 0.0 174 | 175 | for images, labels in train_loader: 176 | images, labels = images.to(device), labels.to(device) 177 | 178 | optimizer.zero_grad() 179 | outputs = model(images) 180 | loss = criterion(outputs, labels) 181 | loss.backward() 182 | optimizer.step() 183 | 184 | train_loss += loss.item() * images.size(0) 185 | 186 | # Валидация (НА TRAIN ДАТАСЕТЕ!) 187 | model.eval() 188 | correct = 0 189 | total = 0 190 | 191 | with torch.no_grad(): 192 | for images, labels in train_loader: 193 | images, labels = images.to(device), labels.to(device) 194 | 195 | outputs = model(images) 196 | loss = criterion(outputs, labels) 197 | 198 | _, predicted = torch.max( 199 | outputs.data, 1 200 | ) # Получаем индекс максимального значения 201 | correct += (predicted == labels).sum().item() 202 | total += labels.size(0) 203 | 204 | # Вывод статистики 205 | train_loss = train_loss / len(train_loader.dataset) 206 | accuracy = correct / total 207 | 208 | print(f"Epoch {epoch+1}/{hyperparams['num_epochs']}") 209 | print(f"Train Loss: {train_loss:.4f}, Accuracy: {accuracy:.4f}") 210 | 211 | # Сохранение модели 212 | torch.save(model, "trained_model_classification.pt") 213 | 214 | print("Модель сохранена") 215 | sys.exit(0) 216 | -------------------------------------------------------------------------------- /models/image_classification/utils/config.yaml: -------------------------------------------------------------------------------- 1 | name: classification-neural-network 2 | desc: "Training a classification model in DataSphere" 3 | 4 | # Параметры точки входа для запуска вычислений 5 | cmd: > 6 | python3 classification_neural_network.py 7 | 8 | # Файлы с входными данными (Исполняемый скрипт должен отсутствовать!) 9 | inputs: 10 | - classification_dataset.zip 11 | - hyperparams.json 12 | - class_to_idx.json 13 | 14 | # Файлы с результатами 15 | outputs: 16 | - trained_model_classification.pt 17 | 18 | # Параметры окружения 19 | env: 20 | python: 21 | type: manual 22 | version: 3.11 23 | pip: 24 | index-url: https://pypi.org/simple 25 | trusted-hosts: 26 | - download.pytorch.org 27 | extra-index-urls: 28 | - https://download.pytorch.org/whl/cu121 29 | requirements-file: requirements.txt 30 | local-paths: 31 | - classification_neural_network.py 32 | 33 | # Флаг, дающий доступ к хранилищу проекта для чтения/записи дополнительных данных 34 | flags: 35 | - attach-project-disk 36 | 37 | # Выбор типов инстансов для вычислений (Расположены по приоритетности) 38 | cloud-instance-types: 39 | - g1.1 40 | # (Если имеется доступ) 41 | # - gt4.1 42 | # - g2.1 43 | 44 | -------------------------------------------------------------------------------- /models/image_classification/utils/hyperparams.json: -------------------------------------------------------------------------------- 1 | { 2 | "num_epochs": 10, 3 | "learning_rate": 0.0001, 4 | "batch_size": 8, 5 | "weight_decay": 0.0001 6 | } -------------------------------------------------------------------------------- /models/image_classification/utils/inference.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | import matplotlib.pyplot as plt 7 | import torch 8 | import torch.nn.functional as F 9 | from PIL import Image 10 | from torchvision import transforms 11 | 12 | 13 | def load_model_and_classes(model_path, class_mapping_path): 14 | """ 15 | Загрузка модели и Json-а классов. 16 | 17 | Args: 18 | model_path: Путь к файлу модели (.pt) 19 | class_mapping_path: Путь к JSON файлу с классами 20 | 21 | Returns: 22 | Tuple containing: 23 | - PyTorch model 24 | - Dictionary mapping class indices to class names 25 | - Device (cuda/cpu) 26 | """ 27 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 28 | model = torch.load(model_path, map_location=device, weights_only=False) 29 | model.eval() 30 | 31 | with open(class_mapping_path, "r") as f: 32 | class_to_idx = json.load(f) 33 | idx_to_class = {v: k for k, v in class_to_idx.items()} 34 | 35 | return model, idx_to_class, device 36 | 37 | 38 | def prepare_image(image_path): 39 | """ 40 | Подготовка изображения для подачи в модель. 41 | 42 | Args: 43 | image_path (str): Путь к файлу изображения 44 | 45 | Returns: 46 | tuple: Кортеж из двух элементов 47 | - image_tensor (torch.Tensor): Предобработанный тензор изображения 48 | - image (PIL.Image): Исходное изображение в формате PIL 49 | 50 | """ 51 | transform = transforms.Compose( 52 | [ 53 | transforms.Resize((224, 224)), 54 | transforms.ToTensor(), 55 | transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), 56 | ] 57 | ) 58 | 59 | image = Image.open(image_path).convert("RGB") 60 | image_tensor = transform(image) 61 | image_tensor = image_tensor.unsqueeze(0) 62 | return image_tensor, image 63 | 64 | 65 | def predict_image(model, image_tensor, device): 66 | """ 67 | Получение предсказаний модели для изображения. 68 | 69 | Args: 70 | model (torch.nn.Module): PyTorch модель 71 | image_tensor (torch.Tensor): Предобработанный тензор изображения 72 | device (torch.device): Устройство для вычислений (CPU/GPU) 73 | 74 | Returns: 75 | numpy.ndarray: Массив вероятностей принадлежности к каждому классу 76 | 77 | Notes: 78 | - Модель должна быть в режиме eval() 79 | - Тензор изображения должен иметь размерность [1, C, H, W] 80 | - Возвращаемые вероятности нормализованы (сумма = 1) 81 | """ 82 | with torch.no_grad(): 83 | image_tensor = image_tensor.to(device) 84 | outputs = model(image_tensor) 85 | probabilities = F.softmax(outputs, dim=1) 86 | 87 | probs = probabilities[0].cpu().numpy() 88 | return probs 89 | 90 | 91 | def visualize_predictions(image, probs, idx_to_class, image_path, output_dir): 92 | """ 93 | Визуализация изображения и предсказаний модели с сохранением результата. 94 | 95 | Args: 96 | image (PIL.Image): Исходное изображение 97 | probs (numpy.ndarray): Массив вероятностей для каждого класса 98 | idx_to_class (dict): Словарь для преобразования индекса в название класса 99 | image_path (str): Путь к исходному изображению 100 | output_dir (str): Директория для сохранения результатов 101 | 102 | Returns: 103 | str: Путь к сохраненному файлу визуализации 104 | 105 | Notes: 106 | - Создает график с двумя частями: исходное изображение и график вероятностей 107 | - Сохраняет результат в формате PNG 108 | - Имя выходного файла содержит временную метку 109 | """ 110 | # Создаем директорию, если её нет 111 | os.makedirs(output_dir, exist_ok=True) 112 | 113 | # Создаем график 114 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) 115 | 116 | # Отображаем исходное изображение 117 | ax1.imshow(image) 118 | ax1.axis("off") 119 | ax1.set_title("Input Image") 120 | 121 | # Создаем столбчатую диаграмму 122 | classes = list(idx_to_class.values()) 123 | ax2.bar(classes, probs * 100) 124 | ax2.set_ylabel("Probability (%)") 125 | ax2.set_title("Class Probabilities") 126 | plt.xticks(rotation=45) 127 | 128 | # Добавляем подписи значений 129 | for i, v in enumerate(probs): 130 | ax2.text( 131 | i, 132 | v * 100, 133 | f"{v * 100:.1f}%", 134 | horizontalalignment="center", 135 | verticalalignment="bottom", 136 | ) 137 | 138 | # Настраиваем layout 139 | plt.tight_layout() 140 | 141 | # Генерируем имя файла с той же временной меткой 142 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 143 | base_image_name = os.path.splitext(os.path.basename(image_path))[0] 144 | plot_filename = f"prediction_{base_image_name}_{timestamp}_image.png" 145 | plot_path = os.path.join(output_dir, plot_filename) 146 | 147 | # Сохраняем график 148 | plt.savefig(plot_path, bbox_inches="tight", dpi=300) 149 | 150 | # Откладка 151 | # plt.show() 152 | 153 | return plot_path 154 | 155 | 156 | def save_predictions_to_json(image_path, probabilities, idx_to_class, output_dir): 157 | """ 158 | Сохранение результатов предсказаний в JSON файл. 159 | 160 | Args: 161 | image_path (str): Путь к исходному изображению 162 | probabilities (numpy.ndarray): Массив вероятностей для каждого класса 163 | idx_to_class (dict): Словарь для преобразования индекса в название класса 164 | output_dir (str): Директория для сохранения результатов 165 | 166 | Returns: 167 | str: Путь к сохраненному JSON файлу 168 | 169 | Notes: 170 | - Создает JSON с метаданными (время, пользователь, путь) и предсказаниями 171 | - Вероятности не ограничены в точности 172 | - Имя выходного файла содержит временную метку 173 | """ 174 | # Создаем директорию для предсказаний 175 | os.makedirs(output_dir, exist_ok=True) 176 | 177 | # Формируем результаты предсказаний 178 | predictions = { 179 | idx_to_class[idx]: float(prob) * 100 for idx, prob in enumerate(probabilities) 180 | } 181 | 182 | # Создаем словарь с метаданными и результатами 183 | result = { 184 | "metadata": { 185 | "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 186 | "user": os.getenv("USER", "unknown"), 187 | "image_path": os.path.abspath(image_path), 188 | }, 189 | "predictions": predictions, 190 | } 191 | 192 | # Генерируем имя файла на основе времени 193 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 194 | base_image_name = os.path.splitext(os.path.basename(image_path))[0] 195 | json_filename = f"prediction_{base_image_name}_{timestamp}.json" 196 | json_path = os.path.join(output_dir, json_filename) 197 | 198 | # Сохраняем результаты в JSON 199 | with open(json_path, "w", encoding="utf-8") as f: 200 | json.dump(result, f, indent=2, ensure_ascii=False) 201 | 202 | return json_path 203 | 204 | 205 | def main(): 206 | # Проверка кол-ва введенных аргументов 207 | if len(sys.argv) != 2: 208 | print("Используйте: python inference.py ") 209 | sys.exit(1) 210 | # Константы 211 | MODEL_PATH = "trained_model_classification.pt" 212 | CLASS_MAPPING_PATH = "class_to_idx.json" 213 | IMAGE_PATH = sys.argv[1] 214 | OUTPUT_DIR = "predictions" 215 | 216 | # Проверка существования файлов 217 | if not os.path.exists(MODEL_PATH): 218 | print(f"Ошибка: Файл модели не найден: {MODEL_PATH}") 219 | sys.exit(1) 220 | if not os.path.exists(CLASS_MAPPING_PATH): 221 | print(f"Ошибка: Файл маппинга классов не найден: {CLASS_MAPPING_PATH}") 222 | sys.exit(1) 223 | if not os.path.exists(IMAGE_PATH): 224 | print(f"Ошибка: Изображение не найдено: {IMAGE_PATH}") 225 | sys.exit(1) 226 | 227 | try: 228 | # Загрузка модели и классов 229 | model, idx_to_class, device = load_model_and_classes( 230 | MODEL_PATH, CLASS_MAPPING_PATH 231 | ) 232 | 233 | # Подготовка изображения 234 | image_tensor, original_image = prepare_image(IMAGE_PATH) 235 | 236 | # Получение предсказаний 237 | probabilities = predict_image(model, image_tensor, device) 238 | 239 | # Вывод результатов в консоль 240 | print("\nPredictions:") 241 | for idx, prob in enumerate(probabilities): 242 | class_name = idx_to_class[idx] 243 | print(f"{class_name}: {prob * 100:.1f}%") 244 | 245 | # Визуализация и сохранение графика 246 | plot_path = visualize_predictions( 247 | original_image, 248 | probabilities, 249 | idx_to_class, 250 | IMAGE_PATH, 251 | output_dir=OUTPUT_DIR, 252 | ) 253 | print(f"Plot saved to: {plot_path}") 254 | 255 | # Сохранение результатов в JSON 256 | json_path = save_predictions_to_json( 257 | IMAGE_PATH, probabilities, idx_to_class, output_dir=OUTPUT_DIR 258 | ) 259 | print(f"\nPredictions saved to: {json_path}") 260 | 261 | except KeyboardInterrupt: 262 | print("\nПрограмма прервана пользователем") 263 | sys.exit(0) 264 | 265 | except Exception as e: 266 | print(f"Ошибка: {str(e)}") 267 | sys.exit(1) 268 | 269 | 270 | if __name__ == "__main__": 271 | main() 272 | -------------------------------------------------------------------------------- /models/image_classification/utils/requirements.txt: -------------------------------------------------------------------------------- 1 | torch==2.5.1+cu121 2 | torchvision==0.20.1+cu121 3 | Pillow==11.1.0 4 | numpy==1.26.4 5 | Brotli==1.0.9 6 | PySocks==1.7.1 7 | certifi==2024.12.14 8 | charset-normalizer==3.3.2 9 | defusedxml==0.7.1 10 | idna==3.7 11 | requests==2.32.3 12 | typing-extensions==4.12.2 13 | urllib3==2.3.0 14 | -------------------------------------------------------------------------------- /models/sound_classification/notebooks/README.MD: -------------------------------------------------------------------------------- 1 | ## Открыть в Google Colab 2 | 3 | Чтобы открыть этот ноутбук в Colab, нажмите кнопку ниже: 4 | 5 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1nOR4zIx4qqknglNMyxa_dGt_Ai-I9KJt?usp=sharing) -------------------------------------------------------------------------------- /models/sound_classification/utils/class_to_idx.json: -------------------------------------------------------------------------------- 1 | { 2 | "vacuum_cleaner": 0, 3 | "airplane": 1 4 | } -------------------------------------------------------------------------------- /models/sound_classification/utils/config.yaml: -------------------------------------------------------------------------------- 1 | name: classification-sound-neural-network 2 | desc: "Training a classification sound model in DataSphere" 3 | 4 | # Параметры точки входа для запуска вычислений 5 | cmd: > 6 | python3 sound_classification_datasphere.py 7 | 8 | # Файлы с входными данными (Исполняемый скрипт должен отсутствовать!) 9 | inputs: 10 | - sound_dataset.zip 11 | - hyperparams.json 12 | - class_to_idx.json 13 | 14 | # Файлы с результатами 15 | outputs: 16 | - trained_model_sound_classification.pt 17 | 18 | # Параметры окружения 19 | env: 20 | python: 21 | type: manual 22 | version: 3.11 23 | pip: 24 | index-url: https://pypi.org/simple 25 | trusted-hosts: 26 | - download.pytorch.org 27 | extra-index-urls: 28 | - https://download.pytorch.org/whl/cu121 29 | requirements-file: requirements.txt 30 | local-paths: 31 | - sound_classification_datasphere.py 32 | 33 | # Флаг, дающий доступ к хранилищу проекта для чтения/записи дополнительных данных 34 | flags: 35 | - attach-project-disk 36 | 37 | # Выбор типов инстансов для вычислений (Расположены по приоритетности) 38 | cloud-instance-types: 39 | - g1.1 40 | # (Если имеется доступ) 41 | # - gt4.1 42 | # - g2.1 43 | 44 | -------------------------------------------------------------------------------- /models/sound_classification/utils/hyperparams.json: -------------------------------------------------------------------------------- 1 | { 2 | "num_epochs": 10, 3 | "learning_rate": 0.0001, 4 | "batch_size": 8, 5 | "weight_decay": 0.0001, 6 | "target_size": 50 7 | } -------------------------------------------------------------------------------- /models/sound_classification/utils/inference.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | import torch 7 | import torch.nn as nn 8 | import torch.nn.functional as F 9 | import torchaudio 10 | from torchaudio.transforms import Resample 11 | import numpy as np 12 | import matplotlib.pyplot as plt 13 | 14 | from panns_inference import AudioTagging 15 | 16 | import warnings 17 | warnings.simplefilter(action="ignore", category=FutureWarning) 18 | 19 | # --- Константы из скрипта обучения --- 20 | SR = 16000 # Целевая частота дискретизации 21 | DURATION = 5 # Длительность в секундах 22 | SAMPLES = SR * DURATION # Количество сэмплов 23 | PANNS_EMBEDDING_DIM = 2048 # Размерность эмбеддинга PANNs (Cnn14) 24 | 25 | # --- Вспомогательная функция для подготовки аудио --- 26 | def prepare_audio_for_inference(wav, sr, target_sr=SR, target_samples=SAMPLES): 27 | if wav.ndim > 1 and wav.shape[0] > 1: 28 | wav = wav.mean(dim=0, keepdim=True) 29 | elif wav.ndim == 1: 30 | wav = wav.unsqueeze(0) 31 | 32 | if sr != target_sr: 33 | wav = Resample(sr, target_sr)(wav) 34 | 35 | current_samples = wav.size(1) 36 | if current_samples > target_samples: 37 | wav = wav[:, :target_samples] 38 | elif current_samples < target_samples: 39 | pad_size = target_samples - current_samples 40 | wav = nn.functional.pad(wav, (0, pad_size)) 41 | return wav 42 | 43 | def load_model_and_classes_sound(mlp_model_path, class_mapping_path, device): 44 | """ 45 | Загрузка MLP-модели, Json-а классов и инициализация PANNs. 46 | 47 | Args: 48 | mlp_model_path: Путь к файлу с весами MLP-модели (.pt) 49 | class_mapping_path: Путь к JSON файлу с классами 50 | device: Устройство для вычислений 51 | 52 | Returns: 53 | Tuple containing: 54 | - mlp_model (torch.nn.Module): MLP модель 55 | - audio_tagger (AudioTagging): Инициализированная модель PANNs 56 | - idx_to_class (dict): Словарь отображения индексов на имена классов 57 | """ 58 | # Инициализация PANNs AudioTagger для извлечения эмбеддингов 59 | print("Initializing PANNs AudioTagger...") 60 | audio_tagger = AudioTagging(checkpoint_path=None, device=str(device)) 61 | print("PANNs AudioTagger initialized.") 62 | 63 | # Загрузка отображения классов 64 | with open(class_mapping_path, "r", encoding="utf-8") as f: 65 | class_to_idx = json.load(f) 66 | idx_to_class = {v: k for k, v in class_to_idx.items()} 67 | num_classes = len(idx_to_class) 68 | 69 | # Определение архитектуры MLP-модели 70 | mlp_model = torch.load(mlp_model_path, map_location=device) 71 | mlp_model.to(device) 72 | mlp_model.eval() 73 | 74 | return mlp_model, audio_tagger, idx_to_class 75 | 76 | def extract_audio_embedding(audio_path, audio_tagger_model, device): 77 | """ 78 | Подготовка аудиофайла и извлечение PANNs эмбеддинга. 79 | 80 | Args: 81 | audio_path (str): Путь к аудиофайлу 82 | audio_tagger_model (AudioTagging): Инициализированная модель PANNs 83 | device (torch.device): Устройство для вычислений 84 | 85 | Returns: 86 | torch.Tensor: Эмбеддинг аудио (формат [1, embedding_dim]) 87 | torch.Tensor: Предобработанная волновая форма аудио (для возможной визуализации) 88 | """ 89 | try: 90 | wav, sr = torchaudio.load(audio_path) 91 | except Exception as e: 92 | raise ValueError(f"Error loading audio file {audio_path}: {e}") 93 | 94 | wav_prepared = prepare_audio_for_inference(wav, sr) 95 | wav_prepared_for_panns = wav_prepared.to(device) 96 | 97 | wav_np = wav_prepared_for_panns.squeeze(0).cpu().numpy() 98 | 99 | with torch.no_grad(): 100 | _, embedding = audio_tagger_model.inference(wav_np[None, :]) 101 | 102 | embedding_tensor = torch.from_numpy(embedding).to(device) 103 | 104 | return embedding_tensor, wav_prepared 105 | 106 | def predict_sound(mlp_model, embedding_tensor, device): 107 | """ 108 | Получение предсказаний MLP-модели для аудио эмбеддинга. 109 | 110 | Args: 111 | mlp_model (torch.nn.Module): MLP модель 112 | embedding_tensor (torch.Tensor): Эмбеддинг аудио [1, embedding_dim] 113 | device (torch.device): Устройство для вычислений 114 | 115 | Returns: 116 | numpy.ndarray: Массив вероятностей принадлежности к каждому классу 117 | """ 118 | with torch.no_grad(): 119 | embedding_tensor = embedding_tensor.to(device) 120 | outputs = mlp_model(embedding_tensor) 121 | probabilities = F.softmax(outputs, dim=1) 122 | 123 | probs_np = probabilities[0].cpu().numpy() 124 | return probs_np 125 | 126 | def visualize_sound_predictions(audio_waveform, probs, idx_to_class, audio_path, output_dir): 127 | """ 128 | Визуализация спектрограммы аудио и предсказаний модели с сохранением результата. 129 | 130 | Args: 131 | audio_waveform (torch.Tensor): Предобработанная волновая форма [1, SAMPLES] 132 | probs (numpy.ndarray): Массив вероятностей для каждого класса 133 | idx_to_class (dict): Словарь для преобразования индекса в название класса 134 | audio_path (str): Путь к исходному аудиофайлу 135 | output_dir (str): Директория для сохранения результатов 136 | 137 | Returns: 138 | str: Путь к сохраненному файлу визуализации 139 | """ 140 | os.makedirs(output_dir, exist_ok=True) 141 | 142 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5)) 143 | 144 | 145 | spec_transform = torchaudio.transforms.MelSpectrogram( 146 | sample_rate=SR, 147 | n_fft=1024, 148 | hop_length=512, 149 | n_mels=128 150 | ) 151 | mel_spectrogram = spec_transform(audio_waveform.cpu()) 152 | db_spectrogram = torchaudio.transforms.AmplitudeToDB()(mel_spectrogram) 153 | 154 | ax1.imshow(db_spectrogram.squeeze().numpy(), aspect='auto', origin='lower', cmap='viridis') 155 | ax1.set_title("Audio spectrogram") 156 | ax1.set_xlabel("Time (frames)") 157 | ax1.set_ylabel("Frequency (Mel bins)") 158 | 159 | 160 | classes = list(idx_to_class.values()) 161 | if len(classes) != len(probs): 162 | print(f"Warning: number of classes ({len(classes)}) does not match number of probabilities ({len(probs)}). The plot may be incorrect.") 163 | 164 | 165 | bar_positions = np.arange(len(classes)) 166 | ax2.bar(bar_positions, probs * 100, tick_label=classes) 167 | ax2.set_ylabel("Probability (%)") 168 | ax2.set_title("Class probabilities") 169 | plt.xticks(bar_positions, classes, rotation=45, ha="right") 170 | 171 | 172 | for i, v in enumerate(probs): 173 | ax2.text( 174 | i, 175 | v * 100 + 1, 176 | f"{v * 100:.1f}%", 177 | horizontalalignment="center", 178 | verticalalignment="bottom", 179 | ) 180 | ax2.set_ylim(0, 110) 181 | 182 | plt.tight_layout() 183 | 184 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 185 | base_audio_name = os.path.splitext(os.path.basename(audio_path))[0] 186 | plot_filename = f"prediction_{base_audio_name}_{timestamp}_sound.png" 187 | plot_path = os.path.join(output_dir, plot_filename) 188 | 189 | plt.savefig(plot_path, bbox_inches="tight", dpi=300) 190 | plt.close(fig) 191 | 192 | return plot_path 193 | 194 | 195 | def save_sound_predictions_to_json(audio_path, probabilities, idx_to_class, output_dir): 196 | """ 197 | Сохранение результатов предсказаний для аудио в JSON файл. 198 | 199 | Args: 200 | audio_path (str): Путь к исходному аудиофайлу 201 | probabilities (numpy.ndarray): Массив вероятностей для каждого класса 202 | idx_to_class (dict): Словарь для преобразования индекса в название класса 203 | output_dir (str): Директория для сохранения результатов 204 | 205 | Returns: 206 | str: Путь к сохраненному JSON файлу 207 | """ 208 | os.makedirs(output_dir, exist_ok=True) 209 | 210 | predictions = { 211 | idx_to_class[idx]: float(prob) * 100 for idx, prob in enumerate(probabilities) 212 | } 213 | 214 | result = { 215 | "metadata": { 216 | "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 217 | "user": os.getenv("USER", "unknown"), 218 | "audio_path": os.path.abspath(audio_path), 219 | }, 220 | "predictions": predictions, 221 | } 222 | 223 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 224 | base_audio_name = os.path.splitext(os.path.basename(audio_path))[0] 225 | json_filename = f"prediction_{base_audio_name}_{timestamp}_sound.json" 226 | json_path = os.path.join(output_dir, json_filename) 227 | 228 | with open(json_path, "w", encoding="utf-8") as f: 229 | json.dump(result, f, indent=2, ensure_ascii=False) 230 | 231 | return json_path 232 | 233 | 234 | def main_sound(): 235 | if len(sys.argv) != 2: 236 | print("Usage: python inference_sound.py ") 237 | sys.exit(1) 238 | 239 | MLP_MODEL_PATH = "trained_model_sound_classification.pt" 240 | CLASS_MAPPING_PATH = "class_to_idx.json" 241 | AUDIO_PATH = sys.argv[1] 242 | OUTPUT_DIR = "predictions_sound" 243 | 244 | if not os.path.exists(MLP_MODEL_PATH): 245 | print(f"Error: MLP model file not found: {MLP_MODEL_PATH}") 246 | sys.exit(1) 247 | if not os.path.exists(CLASS_MAPPING_PATH): 248 | print(f"Error: Class mapping file not found: {CLASS_MAPPING_PATH}") 249 | sys.exit(1) 250 | if not os.path.exists(AUDIO_PATH): 251 | print(f"Error: Audio file not found: {AUDIO_PATH}") 252 | sys.exit(1) 253 | 254 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 255 | print(f"Using device: {device}") 256 | 257 | try: 258 | mlp_model, audio_tagger, idx_to_class = load_model_and_classes_sound( 259 | MLP_MODEL_PATH, CLASS_MAPPING_PATH, device 260 | ) 261 | 262 | audio_embedding, audio_waveform_prepared = extract_audio_embedding(AUDIO_PATH, audio_tagger, device) 263 | 264 | probabilities = predict_sound(mlp_model, audio_embedding, device) 265 | 266 | print("\nPredictions:") 267 | for idx, prob in enumerate(probabilities): 268 | class_name = idx_to_class[idx] 269 | print(f" {class_name}: {prob * 100:.1f}%") 270 | 271 | plot_path = visualize_sound_predictions( 272 | audio_waveform_prepared, 273 | probabilities, 274 | idx_to_class, 275 | AUDIO_PATH, 276 | output_dir=OUTPUT_DIR, 277 | ) 278 | print(f"Plot saved to: {plot_path}") 279 | 280 | json_path = save_sound_predictions_to_json( 281 | AUDIO_PATH, probabilities, idx_to_class, output_dir=OUTPUT_DIR 282 | ) 283 | print(f"Predictions saved to JSON: {json_path}") 284 | 285 | except KeyboardInterrupt: 286 | print("\nProgram interrupted by user.") 287 | sys.exit(0) 288 | except Exception as e: 289 | import traceback 290 | print(f"An error occurred: {str(e)}") 291 | print("Traceback:") 292 | traceback.print_exc() 293 | sys.exit(1) 294 | 295 | if __name__ == "__main__": 296 | main_sound() -------------------------------------------------------------------------------- /models/sound_classification/utils/requirements.txt: -------------------------------------------------------------------------------- 1 | torch==2.5.1+cu121 2 | torchaudio==2.5.1+cu121 3 | panns-inference==0.1.1 4 | Pillow==11.1.0 5 | numpy==1.26.4 6 | Brotli==1.0.9 7 | PySocks==1.7.1 8 | certifi==2024.12.14 9 | charset-normalizer==3.3.2 10 | defusedxml==0.7.1 11 | idna==3.7 12 | requests==2.32.3 13 | typing-extensions==4.12.2 14 | urllib3==2.3.0 15 | -------------------------------------------------------------------------------- /models/sound_classification/utils/sound_classification_datasphere.py: -------------------------------------------------------------------------------- 1 | # -- Данный код предназначен для загрузки модели в Yandex DataSphere и не ориентирован для дальнейших экспериментов -- 2 | 3 | import json 4 | import os 5 | import sys 6 | import zipfile 7 | import random 8 | 9 | import torch 10 | import torch.nn as nn 11 | import torch.optim as optim 12 | from torch.utils.data import DataLoader, Dataset 13 | import torchaudio 14 | from torchaudio.transforms import Resample 15 | from panns_inference import AudioTagging 16 | 17 | 18 | 19 | 20 | if __name__ == "__main__": 21 | SR = 16000 # Целевая частота дискретизации 22 | DURATION = 5 # Длительность в секундах 23 | SAMPLES = SR * DURATION # Количество сэмплов 24 | 25 | # --- Классы аугментации волновой формы --- 26 | class GaussianNoise(nn.Module): 27 | def __init__(self, snr_db=15, p=0.5): 28 | super().__init__() 29 | self.snr_db = snr_db 30 | self.p = p 31 | 32 | def forward(self, waveform): 33 | if torch.rand(1) < self.p: 34 | signal_power = waveform.norm(p=2) 35 | noise = torch.randn_like(waveform) 36 | noise_power = noise.norm(p=2) 37 | if noise_power == 0: 38 | return waveform 39 | snr = 10**(self.snr_db / 10) 40 | scale = (signal_power / (noise_power * snr)).sqrt() 41 | return waveform + scale * noise 42 | return waveform 43 | 44 | class RandomVolume(nn.Module): 45 | def __init__(self, min_gain_db=-6, max_gain_db=6, p=0.5): 46 | super().__init__() 47 | self.min_gain_db = min_gain_db 48 | self.max_gain_db = max_gain_db 49 | self.p = p 50 | 51 | def forward(self, waveform): 52 | if random.random() < self.p: 53 | gain_db = random.uniform(self.min_gain_db, self.max_gain_db) 54 | gain_factor = 10 ** (gain_db / 20) 55 | gain_factor = max(gain_factor, 1e-5) 56 | return waveform * gain_factor 57 | return waveform 58 | 59 | # --- Вспомогательная функция для подготовки аудио --- 60 | def prepare_audio(wav, sr, target_sr=SR, target_samples=SAMPLES): 61 | if wav.ndim > 1 and wav.shape[0] > 1: 62 | wav = wav.mean(dim=0, keepdim=True) 63 | elif wav.ndim == 1: 64 | wav = wav.unsqueeze(0) 65 | 66 | if sr != target_sr: 67 | wav = Resample(sr, target_sr)(wav) 68 | 69 | if wav.size(1) > target_samples: 70 | wav = wav[:, :target_samples] 71 | else: 72 | pad_size = target_samples - wav.size(1) 73 | wav = nn.functional.pad(wav, (0, pad_size)) 74 | return wav 75 | 76 | # --- Класс Dataset для эмбеддингов PANNs --- 77 | class PANNsEmbedDatasetDS(Dataset): 78 | def __init__(self, original_folder, class_to_idx_map, audio_tagger_model, 79 | waveform_augment_transform=None, target_size_per_class=50): 80 | self.original_folder = original_folder 81 | self.class_to_idx = class_to_idx_map 82 | self.audio_tagger = audio_tagger_model 83 | self.waveform_augment = waveform_augment_transform 84 | self.target_size_per_class = target_size_per_class 85 | self.samples = [] 86 | 87 | # Проверка на отсутствующие классы 88 | class_names_in_folder = os.listdir(original_folder) 89 | missing_classes = [ 90 | cls for cls in self.class_to_idx.keys() if cls not in class_names_in_folder 91 | ] 92 | if missing_classes: 93 | raise ValueError( 94 | f"Классы {missing_classes} из class_to_idx.json отсутствуют в папке {original_folder}" 95 | ) 96 | 97 | for class_name in class_names_in_folder: 98 | if class_name not in self.class_to_idx: 99 | print(f"Warning: Folder {class_name} not found in class_to_idx.json, skipping.") 100 | continue 101 | 102 | label = self.class_to_idx[class_name] 103 | class_path = os.path.join(original_folder, class_name) 104 | if not os.path.isdir(class_path): 105 | continue 106 | 107 | audio_files = [os.path.join(class_path, f) for f in os.listdir(class_path) 108 | if f.lower().endswith(('.wav', '.mp3', '.flac'))] 109 | 110 | if not audio_files: 111 | print(f"Warning: Audio files not found in {class_path} for class {class_name}") 112 | continue 113 | 114 | for i in range(self.target_size_per_class): 115 | file_path = audio_files[i % len(audio_files)] 116 | self.samples.append((file_path, label)) 117 | 118 | def __len__(self): 119 | return len(self.samples) 120 | 121 | def __getitem__(self, idx): 122 | file_path, label = self.samples[idx] 123 | try: 124 | wav, sr = torchaudio.load(file_path) 125 | except Exception as e: 126 | print(f"Error loading audio file {file_path}: {e}") 127 | dummy_embedding = torch.zeros(2048) 128 | return dummy_embedding, -1 129 | 130 | wav_prepared = prepare_audio(wav, sr) 131 | 132 | if self.waveform_augment: 133 | wav_prepared = self.waveform_augment(wav_prepared) 134 | 135 | wav_np = wav_prepared.squeeze(0).cpu().numpy() 136 | 137 | with torch.no_grad(): 138 | _, embedding = self.audio_tagger.inference(wav_np[None, :]) 139 | 140 | embedding = torch.from_numpy(embedding).squeeze(0) 141 | 142 | return embedding, label 143 | 144 | print("Starting sound classification training for DataSphere") 145 | DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") 146 | print(f"Using device: {DEVICE}") 147 | 148 | # Загрузка гиперпараметров 149 | with open("hyperparams.json", "r") as f: 150 | hyperparams = json.load(f) 151 | 152 | # Загрузка отображения классов на индексы 153 | with open("class_to_idx.json", "r") as f: 154 | class_to_idx = json.load(f) 155 | 156 | num_classes = len(class_to_idx) 157 | print(f"Number of classes: {num_classes}") 158 | print(f"Class mapping: {class_to_idx}") 159 | 160 | 161 | dataset_zip_name = "sound_dataset.zip" 162 | dataset_extract_folder = "sound_dataset_extracted" 163 | train_folder_path = os.path.join(dataset_extract_folder, "sound_dataset/train") 164 | 165 | if os.path.exists(dataset_extract_folder): 166 | import shutil 167 | shutil.rmtree(dataset_extract_folder) 168 | print(f"Removed existing directory: {dataset_extract_folder}") 169 | 170 | print(f"Unpacking {dataset_zip_name}...") 171 | with zipfile.ZipFile(dataset_zip_name, "r") as zip_ref: 172 | zip_ref.extractall(dataset_extract_folder) 173 | print("Unpacking completed.") 174 | 175 | if not os.path.exists(train_folder_path): 176 | potential_base_list = os.listdir(dataset_extract_folder) 177 | if not potential_base_list: 178 | raise FileNotFoundError(f"Folder {dataset_extract_folder} is empty after unpacking.") 179 | potential_base = os.path.join(dataset_extract_folder, potential_base_list[0]) 180 | if os.path.isdir(potential_base) and "train" in os.listdir(potential_base): 181 | train_folder_path = os.path.join(potential_base, "train") 182 | else: 183 | raise FileNotFoundError(f"Train folder not found at path {train_folder_path} or in expected subdirectories.") 184 | print(f"Using training data from: {train_folder_path}") 185 | 186 | 187 | 188 | print("Initializing PANNs AudioTagger...") 189 | audio_tagger = AudioTagging(checkpoint_path=None, device=str(DEVICE)) 190 | print("PANNs AudioTagger initialized.") 191 | panns_embedding_dim = 2048 192 | 193 | # Определение аугментаций волновой формы 194 | waveform_augment_transform = nn.Sequential( 195 | RandomVolume(min_gain_db=-6, max_gain_db=6, p=0.5), 196 | GaussianNoise(snr_db=30, p=0.5), 197 | ).to(DEVICE) 198 | 199 | # Создание Dataset и DataLoader 200 | print("Creating dataset...") 201 | train_dataset = PANNsEmbedDatasetDS( 202 | original_folder=train_folder_path, 203 | class_to_idx_map=class_to_idx, 204 | audio_tagger_model=audio_tagger, 205 | waveform_augment_transform=waveform_augment_transform, 206 | target_size_per_class=hyperparams.get("target_size", 50) # из hyperparams или по умолчанию 207 | ) 208 | print(f"Dataset created with {len(train_dataset)} samples.") 209 | 210 | if len(train_dataset) == 0: 211 | print("Error: Dataset is empty. Check paths and data.") 212 | sys.exit(1) 213 | 214 | train_loader = DataLoader( 215 | train_dataset, 216 | batch_size=hyperparams["batch_size"], 217 | shuffle=True 218 | ) 219 | 220 | # Определение MLP-модели 221 | mlp_model = nn.Sequential( 222 | nn.Linear(panns_embedding_dim, 512), 223 | nn.ReLU(), 224 | nn.Dropout(0.3), 225 | nn.Linear(512, 256), 226 | nn.ReLU(), 227 | nn.Dropout(0.2), 228 | nn.Linear(256, num_classes) 229 | ).to(DEVICE) 230 | 231 | # Функция потерь и оптимизатор 232 | criterion = nn.CrossEntropyLoss() 233 | optimizer = optim.Adam( 234 | mlp_model.parameters(), 235 | lr=hyperparams["learning_rate"], 236 | weight_decay=hyperparams.get("weight_decay", 0.0) 237 | ) 238 | 239 | # Цикл обучения 240 | print("Starting training...") 241 | for epoch in range(hyperparams["num_epochs"]): 242 | mlp_model.train() 243 | epoch_train_loss = 0.0 244 | 245 | for embeddings, labels in train_loader: 246 | if embeddings.nelement() == 0: 247 | print("Skipping empty batch.") 248 | continue 249 | if -1 in labels: 250 | print("Skipping batch with data loading error.") 251 | continue 252 | 253 | embeddings, labels = embeddings.to(DEVICE), labels.to(DEVICE) 254 | 255 | optimizer.zero_grad() 256 | outputs = mlp_model(embeddings) 257 | loss = criterion(outputs, labels) 258 | loss.backward() 259 | optimizer.step() 260 | 261 | epoch_train_loss += loss.item() * embeddings.size(0) 262 | 263 | avg_epoch_train_loss = epoch_train_loss / len(train_loader.dataset) 264 | 265 | # Валидация на обучающем наборе 266 | mlp_model.eval() 267 | correct = 0 268 | total = 0 269 | with torch.no_grad(): 270 | for embeddings, labels in train_loader: # Повторная итерация по train_loader для "валидации" 271 | if embeddings.nelement() == 0 or -1 in labels: continue 272 | embeddings, labels = embeddings.to(DEVICE), labels.to(DEVICE) 273 | outputs = mlp_model(embeddings) 274 | _, predicted = torch.max(outputs.data, 1) 275 | total += labels.size(0) 276 | correct += (predicted == labels).sum().item() 277 | 278 | accuracy = correct / total if total > 0 else 0 279 | 280 | print(f"Epoch {epoch+1}/{hyperparams['num_epochs']} - " 281 | f"Loss: {avg_epoch_train_loss:.4f}, " 282 | f"Accuracy (On TRAIN Dataset): {accuracy:.4f}") 283 | 284 | # Сохранение обученной MLP-модели 285 | model_save_path = "trained_model_sound_classification.pt" 286 | torch.save(mlp_model, model_save_path) 287 | print(f"MLP-model saved in {model_save_path}") 288 | 289 | print("Studing has been ended.") 290 | sys.exit(0) -------------------------------------------------------------------------------- /models/sound_classification/utils/sound_dataset.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/models/sound_classification/utils/sound_dataset.zip -------------------------------------------------------------------------------- /models/sound_classification/utils/test/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/models/sound_classification/utils/test/test.wav -------------------------------------------------------------------------------- /models/sound_classification/utils/trained_model_sound_classification.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/models/sound_classification/utils/trained_model_sound_classification.pt -------------------------------------------------------------------------------- /paper/КР_Ли_Владислав/Lee_Vladislav_KR.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Ли_Владислав/Lee_Vladislav_KR.pdf -------------------------------------------------------------------------------- /paper/КР_Ли_Владислав/Lee_Vladislav_KR.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Ли_Владислав/Lee_Vladislav_KR.rar -------------------------------------------------------------------------------- /paper/КР_Мухаметшин_Руслан/Mukhametshin_Ruslan_source.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Мухаметшин_Руслан/Mukhametshin_Ruslan_source.zip -------------------------------------------------------------------------------- /paper/КР_Мухаметшин_Руслан/КР_Мухаметшин_Руслан.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Мухаметшин_Руслан/КР_Мухаметшин_Руслан.pdf -------------------------------------------------------------------------------- /paper/КР_Пестов_Лев/Pestov_Lev_source.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Пестов_Лев/Pestov_Lev_source.zip -------------------------------------------------------------------------------- /paper/КР_Пестов_Лев/КР_Пестов_Лев.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/paper/КР_Пестов_Лев/КР_Пестов_Лев.pdf -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | isort . 2 | black . 3 | flake8 . -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.9.0 2 | click==8.1.8 3 | Flask==3.1.0 4 | Flask-SQLAlchemy==3.1.1 5 | itsdangerous==2.2.0 6 | Jinja2==3.1.5 7 | joblib==1.4.2 8 | MarkupSafe==3.0.2 9 | numpy==2.2.2 10 | pandas==2.2.3 11 | python-dateutil==2.9.0.post0 12 | pytz==2024.2 13 | scikit-learn==1.6.1 14 | scipy==1.15.1 15 | six==1.17.0 16 | SQLAlchemy==2.0.37 17 | threadpoolctl==3.5.0 18 | typing_extensions==4.12.2 19 | Werkzeug==3.1.3 20 | torch==2.5.1 21 | torchvision==0.20.1 22 | Pillow==11.1.0 23 | Brotli==1.0.9 24 | PySocks==1.7.1 25 | certifi==2024.12.14 26 | charset-normalizer==3.3.2 27 | defusedxml==0.7.1 28 | idna==3.7 29 | requests==2.32.3 30 | urllib3==2.3.0 31 | matplotlib==3.10.1 32 | python-dotenv==1.1.0 33 | fastapi>=0.101.0 34 | datasphere 35 | black 36 | flake8 37 | isort -------------------------------------------------------------------------------- /web_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/web_service/__init__.py -------------------------------------------------------------------------------- /web_service/app/AutoMl/README.md: -------------------------------------------------------------------------------- 1 | # AutoHPO: Automated Hyperparameter Optimization Framework 2 | 3 | A Python framework for comparing hyperparameter optimization (HPO) methods. Currently implements Bayesian Optimization and Evolutionary Strategy approaches. Designed for easy extension with new optimization algorithms and machine learning models. 4 | 5 | **Note:** This project is under active development. New features and HPO methods will be added periodically. 6 | 7 | ## Features 8 | 9 | - **Diverse HPO Methods** 10 | - Bayesian Optimization with Gaussian Processes 11 | - Evolutionary Strategy with mutation/crossover operations 12 | - **Model Agnostic** - Works with any scikit-learn compatible estimator 13 | - **Multi-task Support** - Handles both classification and regression problems 14 | - **Extensible Architecture** - Easy to add new HPO algorithms 15 | - **Optimization History Tracking** - Compare convergence patterns across methods 16 | -------------------------------------------------------------------------------- /web_service/app/AutoMl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/web_service/app/AutoMl/__init__.py -------------------------------------------------------------------------------- /web_service/app/AutoMl/automl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/web_service/app/AutoMl/automl/__init__.py -------------------------------------------------------------------------------- /web_service/app/AutoMl/automl/comparison.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from sklearn.model_selection import cross_val_score 4 | from warnings import catch_warnings, simplefilter 5 | 6 | def compare_hpo_methods(config, X, y): 7 | task_type = config['task_type'] 8 | model_config = config['model'] 9 | scoring = config['scoring'] 10 | hpo_methods_config = config['hpo_methods'] 11 | 12 | # Определение целевой функции 13 | def objective_function(**params): 14 | model_class = model_config['class'] 15 | model_params = model_config['fixed_params'].copy() 16 | 17 | for param in model_config['param_space']: 18 | name = param['name'] 19 | value = params[name] 20 | if param['type'] == 'integer': 21 | model_params[name] = int(round(value)) 22 | elif param['type'] == 'float': 23 | model_params[name] = value 24 | elif param['type'] == 'categorical': 25 | model_params[name] = value 26 | 27 | model = model_class(**model_params) 28 | with catch_warnings(): 29 | simplefilter("ignore") 30 | scores = cross_val_score(model, X, y, cv=5, scoring=scoring) 31 | return np.mean(scores) 32 | 33 | # Сравнение методов HPO 34 | results = {} 35 | for method_name, method_config in hpo_methods_config.items(): 36 | print(f"=== {method_name} ({task_type}) ===") 37 | HPOClass = method_config['hpo_class'] 38 | hpo_params = method_config.get('hpo_params', {}) 39 | 40 | # Инициализация оптимизатора 41 | hpo = HPOClass( 42 | f=objective_function, 43 | param_space=model_config['param_space'], 44 | **hpo_params 45 | ) 46 | 47 | # Запуск оптимизации 48 | start_time = time.time() 49 | best_params, history = hpo.optimize() 50 | 51 | best_score = max(history) 52 | time_taken = time.time() - start_time 53 | 54 | results[method_name] = { 55 | 'best_score': best_score, 56 | 'time': time_taken, 57 | 'history': history, 58 | 'best_params': best_params 59 | } 60 | 61 | # Вывод результатов метода 62 | print(f"Best Score: {best_score:.4f}, Time: {time_taken:.2f}s") 63 | print("=" * 30 + '\n') 64 | 65 | # Сводка результатов 66 | print(f"\n=== Final Results ===") 67 | for method, res in results.items(): 68 | print(f"\n**{method}**") 69 | print(f" Best Score: {res['best_score']:.4f}") 70 | print(f" Time: {res['time']:.2f}s") 71 | print(" Best Parameters:") 72 | for param, value in res['best_params'].items(): 73 | print(f" {param}: {value}") 74 | print("\n" + "=" * 50) 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /web_service/app/AutoMl/automl/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .bayesian_optimization import BayesianOptimizationHPO 2 | from .evolution_strategy import EvolutionaryStrategyHPO -------------------------------------------------------------------------------- /web_service/app/AutoMl/automl/models/bayesian_optimization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.stats import norm 3 | from sklearn.gaussian_process import GaussianProcessRegressor 4 | from sklearn.gaussian_process.kernels import Matern 5 | from warnings import catch_warnings, simplefilter 6 | import warnings 7 | 8 | class BayesianOptimizationHPO: 9 | def __init__(self, f, param_space, verbose=0, random_state=None, 10 | init_points=10, n_iter=50, acq='ucb', kappa=2.576, xi=0.0, 11 | n_candidates=500): 12 | """ 13 | f: Функция цели для минимизации/максимизации. Принимает параметры из param_space как kwargs. 14 | param_space: Список словарей, описывающих параметры поиска (name, type, low/high/categories). 15 | verbose: Уровень подробности (0 = без вывода, 1 = базовый, 2 = подробный). По умолчанию: 1. 16 | random_state: Seed для генератора случайных чисел. По умолчанию: None. 17 | init_points: Количество начальных случайных точек перед началом байесовской оптимизации. По умолчанию: 10. 18 | n_iter: Количество итераций байесовской оптимизации. По умолчанию: 50. 19 | acq: Тип функции приобретения ('ucb' или 'ei'). По умолчанию: 'ucb'. 20 | kappa: Параметр баланса исследования и эксплуатации для UCB. По умолчанию: 2.576. 21 | xi: Параметр исследования для EI, контролирует порог улучшения. По умолчанию: 0.0. 22 | n_candidates: Количество кандидатов для оптимизации функции приобретения. По умолчанию: 500. 23 | """ 24 | self.f = f 25 | self.param_space = param_space 26 | self.verbose = verbose 27 | self.random_state = random_state 28 | self.init_points = init_points 29 | self.n_iter = n_iter 30 | self.acq = acq 31 | self.kappa = kappa 32 | self.xi = xi 33 | self.n_candidates = n_candidates 34 | 35 | self._space = [] 36 | self._values = [] 37 | self._history = [] # Здесь будем сохранять лучший результат до i-й итерации 38 | self.rng = np.random.RandomState(random_state) 39 | self.pbounds = {} 40 | self.categorical_mapping = {} 41 | 42 | for param in param_space: 43 | name = param['name'] 44 | param_type = param['type'] 45 | if param_type == 'categorical': 46 | if 'categories' not in param: 47 | raise ValueError(f"Parameter {name} of type 'categorical' must have 'categories' list.") 48 | categories = param['categories'] 49 | if not isinstance(categories, list) or len(categories) == 0: 50 | raise ValueError(f"Parameter {name} 'categories' must be a non-empty list.") 51 | self.categorical_mapping[name] = categories.copy() 52 | low = 0 53 | high = len(categories) - 1 54 | self.pbounds[name] = (low, high) 55 | else: 56 | if 'low' not in param or 'high' not in param: 57 | raise ValueError(f"Parameter {name} missing 'low' or 'high'") 58 | low = param['low'] 59 | high = param['high'] 60 | if low >= high: 61 | raise ValueError(f"Invalid range for {name} (low >= high)") 62 | if param_type not in ['integer', 'float']: 63 | raise ValueError(f"Unsupported parameter type {param_type} for {name}") 64 | self.pbounds[name] = (low, high) 65 | 66 | # Вывод предупреждения при наличии категориальных параметров 67 | if self.categorical_mapping: 68 | warnings.warn( 69 | "Warning: The use of categorical parameters is strongly discouraged in Bayesian Optimization. " 70 | "Categorical variables are converted to integers, which may not be suitable for the underlying Gaussian Process model. " 71 | "Consider using continuous or integer variables instead.", UserWarning 72 | ) 73 | 74 | self.gp = GaussianProcessRegressor( 75 | kernel=Matern(nu=2.5), 76 | n_restarts_optimizer=25, 77 | random_state=random_state 78 | ) 79 | self._prime_subscriptions() 80 | 81 | def optimize(self): 82 | self.init(self.init_points) 83 | 84 | for i in range(self.n_iter): 85 | with catch_warnings(): 86 | simplefilter("ignore") 87 | self.probe(self._next()) 88 | 89 | if self.verbose >= 1: 90 | print(f"Iteration {i + 1}/{self.n_iter} | Best so far: {self._history[-1]:.4f}") 91 | 92 | best_params = {} 93 | for param in self.param_space: 94 | name = param['name'] 95 | value = self.max['params'][name] 96 | if param['type'] == 'integer': 97 | best_params[name] = int(round(value)) 98 | elif param['type'] == 'categorical': 99 | categories = param['categories'] 100 | index = int(np.round(value)) 101 | index = np.clip(index, 0, len(categories) - 1) 102 | best_params[name] = categories[index] 103 | else: 104 | best_params[name] = value 105 | 106 | history = self._history.copy() 107 | 108 | print("═" * 50) 109 | print("Best params for Bayesian Optimization:") 110 | for k, v in best_params.items(): 111 | print(f"▸ {k:20} : {v}") 112 | print(f"\nBest result: {self.max['target']:.4f}") 113 | print("═" * 50) 114 | 115 | return best_params, history 116 | 117 | def _next(self): 118 | self.gp.fit(self._space, self._values) 119 | best_acq = -np.inf 120 | best_x = None 121 | 122 | for _ in range(self.n_candidates): 123 | candidate = np.array([self.rng.uniform(low, high) for (low, high) in self._bounds]) 124 | 125 | mu, sigma = self.gp.predict([candidate], return_std=True) 126 | 127 | if self.acq == 'ucb': 128 | acq_value = mu + self.kappa * sigma 129 | elif self.acq == 'ei': 130 | improvement = mu - self.max['target'] - self.xi 131 | z = improvement / sigma if sigma > 1e-12 else 0.0 132 | acq_value = (improvement * norm.cdf(z) + sigma * norm.pdf(z)) if sigma > 1e-12 else 0.0 133 | 134 | if acq_value > best_acq: 135 | best_acq = acq_value 136 | best_x = candidate 137 | 138 | return best_x 139 | 140 | def _prime_subscriptions(self): 141 | self._bounds = np.array([v for k, v in self.pbounds.items()]) 142 | self.dim = len(self.pbounds) 143 | self.initialized = False 144 | 145 | def init(self, init_points): 146 | for _ in range(init_points): 147 | x = np.array([self.rng.uniform(low, high) for (low, high) in self._bounds]) 148 | self.probe(x) 149 | 150 | def probe(self, x): 151 | params = dict(zip(self.pbounds.keys(), x)) 152 | for param in self.param_space: 153 | name = param['name'] 154 | if name in self.categorical_mapping: 155 | # Обработка категориальных переменных 156 | continuous_val = params[name] 157 | categories = self.categorical_mapping[name] 158 | index = int(np.round(continuous_val)) 159 | index = np.clip(index, 0, len(categories) - 1) 160 | params[name] = categories[index] 161 | else: 162 | # Обработка целочисленных и вещественных переменных 163 | param_type = param['type'] 164 | if param_type == 'integer': 165 | params[name] = int(round(params[name])) 166 | elif param_type == 'float': 167 | params[name] = float(params[name]) 168 | 169 | target = self.f(**params) 170 | 171 | self._space.append(x) 172 | self._values.append(target) 173 | # Обновляем историю: сохраняем лучший результат, достигнутый до текущей итерации 174 | self._history.append(max(self._values)) 175 | 176 | if self.verbose > 1: 177 | print(f"Probe: {params} → Score: {target:.4f}") 178 | 179 | @property 180 | def max(self): 181 | idx = np.argmax(self._values) 182 | params_dict = dict(zip(self.pbounds.keys(), self._space[idx])) 183 | return { 184 | 'params': params_dict, 185 | 'target': self._values[idx] 186 | } 187 | -------------------------------------------------------------------------------- /web_service/app/AutoMl/automl/models/evolution_strategy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from warnings import catch_warnings, simplefilter 3 | 4 | class EvolutionaryStrategyHPO: 5 | def __init__(self, f, param_space, population_size=20, 6 | mutation_variance=0.1, verbose=0, random_state=None, 7 | generations=10, mutation_rate=0.25, mutation_ratio=0.75, elite_ratio=0.2): 8 | """ 9 | f: Objective function to minimize/maximize. Must accept parameters from param_space as kwargs 10 | param_space: List of dictionaries defining search space parameters (name, type, low/high/categories) 11 | population_size: Number of individuals in each generation. Default: 50 12 | mutation_variance: Scaling factor for mutation step size. Default: 0.1 13 | verbose: Verbosity level (0 = silent, 1 = basic, 2 = detailed). Default: 0 14 | random_state: Seed for random number generator. Default: None 15 | generations: Number of evolutionary iterations. Default: 20 16 | mutation_rate: Probability of mutating individual parameter. Default: 0.25 17 | mutation_ratio: Probability of applying mutation to offspring. Default: 0.75 18 | elite_ratio: Proportion of top performers preserved between generations. Default: 0.2 19 | """ 20 | 21 | self.f = f 22 | self.param_space = param_space 23 | self.population_size = population_size 24 | self.mutation_variance = mutation_variance 25 | self.verbose = verbose 26 | self.random_state = random_state 27 | self.generations = generations 28 | self.mutation_rate = mutation_rate 29 | self.mutation_ratio = mutation_ratio 30 | self.elite_ratio = elite_ratio 31 | 32 | self.rng = np.random.RandomState(random_state) 33 | self.history = [] 34 | self.fitness_cache = {} 35 | 36 | # Валидация параметров 37 | for param in self.param_space: 38 | if param['type'] in ['integer', 'float']: 39 | if param['low'] >= param['high']: 40 | raise ValueError(f"Invalid range for {param['name']}") 41 | elif param['type'] == 'categorical' and not param.get('categories'): 42 | raise ValueError(f"Invalid categories for {param['name']}") 43 | 44 | def _initialize_individual(self): 45 | individual = {} 46 | for param in self.param_space: 47 | name = param['name'] 48 | if param['type'] == 'float': 49 | individual[name] = self.rng.uniform(param['low'], param['high']) 50 | elif param['type'] == 'integer': 51 | individual[name] = self.rng.randint(param['low'], param['high'] + 1) 52 | elif param['type'] == 'categorical': 53 | individual[name] = self.rng.choice(param['categories']) 54 | return individual 55 | 56 | def _mutate(self, individual): 57 | mutated = individual.copy() 58 | for param in self.param_space: 59 | name = param['name'] 60 | if self.rng.rand() < self.mutation_rate: 61 | if param['type'] == 'float': 62 | delta = self.mutation_variance * (param['high'] - param['low']) 63 | new_val = mutated[name] + self.rng.uniform(-delta, delta) 64 | mutated[name] = np.clip(new_val, param['low'], param['high']) 65 | elif param['type'] == 'integer': 66 | delta = max(1, int(self.mutation_variance * (param['high'] - param['low']))) 67 | new_val = mutated[name] + self.rng.randint(-delta, delta + 1) 68 | mutated[name] = int(np.clip(new_val, param['low'], param['high'])) 69 | elif param['type'] == 'categorical': 70 | mutated[name] = self.rng.choice(param['categories']) 71 | return mutated 72 | 73 | def _crossover(self, parent1, parent2): 74 | child = parent1.copy() 75 | crossover_params = self.rng.choice( 76 | self.param_space, 77 | size=self.rng.randint(1, len(self.param_space) + 1), 78 | replace=False 79 | ) 80 | for param in crossover_params: 81 | child[param['name']] = parent2[param['name']] 82 | return child 83 | 84 | def _evaluate(self, individual): 85 | params = {} 86 | for param in self.param_space: 87 | name = param['name'] 88 | val = individual[name] 89 | params[name] = val 90 | 91 | key = tuple(sorted(params.items())) 92 | if key in self.fitness_cache: 93 | return self.fitness_cache[key] 94 | 95 | with catch_warnings(): 96 | simplefilter("ignore") 97 | score = self.f(**params) 98 | 99 | self.fitness_cache[key] = score 100 | if self.verbose > 1 : 101 | print(f"Probe: {params} → Score: {score:.4f}") 102 | return score 103 | 104 | def optimize(self): 105 | population = [self._initialize_individual() for _ in range(self.population_size)] 106 | elite_size = max(2, int(self.population_size * self.elite_ratio)) 107 | 108 | for gen in range(self.generations): 109 | fitness = [self._evaluate(ind) for ind in population] 110 | elite_indices = np.argsort(fitness)[-elite_size:] 111 | elites = [population[i] for i in elite_indices] 112 | 113 | offspring = [] 114 | for _ in range(self.population_size - elite_size): 115 | parents = self.rng.choice(elites, 2, replace=False) 116 | child = self._crossover(parents[0], parents[1]) 117 | if self.rng.rand() < self.mutation_ratio: 118 | child = self._mutate(child) 119 | offspring.append(child) 120 | 121 | population = elites + offspring 122 | best_fitness = np.max(fitness) 123 | self.history.append(best_fitness) 124 | if self.verbose: 125 | print(f"Generation {gen + 1}/{self.generations} | Best: {best_fitness:.4f}") 126 | 127 | 128 | best_idx = np.argmax([self._evaluate(ind) for ind in population]) 129 | best_params = population[best_idx] 130 | 131 | print("═" * 50) 132 | print("Best params for Evolution Strategy:") 133 | for k, v in best_params.items(): 134 | print(f"▸ {k:20} : {v}") 135 | print(f"Best result: {max(self.history):.4f}") 136 | print("═" * 50) 137 | 138 | return best_params, self.history -------------------------------------------------------------------------------- /web_service/app/AutoMl/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpestov/interactive_ai_model_builder/01a12618ec2eab7e390187124c5836502d97976a/web_service/app/AutoMl/test/__init__.py -------------------------------------------------------------------------------- /web_service/app/AutoMl/test/test_classification.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.join(os.path.dirname(__file__), '../AutoML')) 5 | from web_service.app.AutoMl.automl.models import BayesianOptimizationHPO, EvolutionaryStrategyHPO 6 | from web_service.app.AutoMl.automl.comparison import compare_hpo_methods 7 | 8 | from sklearn.ensemble import RandomForestClassifier 9 | config_classification = { 10 | 'task_type': 'classification', 11 | 'model': { 12 | 'class': RandomForestClassifier, 13 | 'fixed_params': {'random_state': 42}, 14 | 'param_space': [ 15 | {'name': 'n_estimators', 'type': 'integer', 'low': 50, 'high': 200}, 16 | {'name': 'max_depth', 'type': 'integer', 'low': 3, 'high': 20}, 17 | {'name': 'min_samples_split', 'type': 'integer', 'low': 2, 'high': 10}, 18 | {'name': 'min_samples_leaf', 'type': 'integer', 'low': 1, 'high': 4}, 19 | {'name': 'min_impurity_decrease', 'type': 'float', 'low': 0.0, 'high': 1.0} 20 | ] 21 | }, 22 | 'scoring': 'accuracy', 23 | 'hpo_methods': { 24 | 'Bayesian': { 25 | 'hpo_class': BayesianOptimizationHPO, 26 | 'hpo_params': { 27 | 'verbose': 1, 28 | 'random_state': 42, 29 | } 30 | }, 31 | 'Evolutionary': { 32 | 'hpo_class': EvolutionaryStrategyHPO, 33 | 'hpo_params': { 34 | 'verbose': 1, 35 | 'random_state': 42 36 | } 37 | } 38 | } 39 | } 40 | 41 | from sklearn.datasets import load_wine 42 | dataset = load_wine() 43 | X, y = dataset['data'], dataset['target'] 44 | compare_hpo_methods(config_classification, X, y) 45 | -------------------------------------------------------------------------------- /web_service/app/AutoMl/test/test_regression.py: -------------------------------------------------------------------------------- 1 | from ..automl.models import BayesianOptimizationHPO, EvolutionaryStrategyHPO 2 | from ..automl.comparison import compare_hpo_methods 3 | 4 | 5 | from sklearn.ensemble import GradientBoostingRegressor 6 | config_regression = { 7 | 'task_type': 'regression', 8 | 'model': { 9 | 'class': GradientBoostingRegressor, 10 | 'fixed_params': { 11 | 'random_state': 53, 12 | 'loss': 'squared_error' 13 | }, 14 | 'param_space': [ 15 | {'name': 'n_estimators', 'type': 'integer', 'low': 50, 'high': 300}, 16 | {'name': 'learning_rate', 'type': 'float', 'low': 0.01, 'high': 0.3}, 17 | {'name': 'max_depth', 'type': 'integer', 'low': 3, 'high': 8}, 18 | {'name': 'min_samples_split', 'type': 'integer', 'low': 2, 'high': 15}, 19 | {'name': 'min_samples_leaf', 'type': 'integer', 'low': 1, 'high': 5}, 20 | {'name': 'subsample', 'type': 'float', 'low': 0.6, 'high': 1.0}, 21 | ] 22 | }, 23 | 'scoring': 'neg_mean_squared_error', 24 | 'hpo_methods': { 25 | 'Bayesian': { 26 | 'hpo_class': BayesianOptimizationHPO, 27 | 'hpo_params': { 28 | 'verbose': 1, 29 | 'random_state': 152, 30 | } 31 | }, 32 | 'Evolutionary': { 33 | 'hpo_class': EvolutionaryStrategyHPO, 34 | 'hpo_params': { 35 | 'verbose': 1, 36 | 'random_state': 152, 37 | } 38 | } 39 | } 40 | } 41 | 42 | from sklearn.datasets import make_regression 43 | X, y = make_regression(n_samples=1000, noise=0.1, n_features=8, random_state=50) 44 | compare_hpo_methods(config_regression,X,y) -------------------------------------------------------------------------------- /web_service/app/ML_snippet.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pickle 3 | 4 | csv_path = '/path/to/your/data.csv' 5 | data = pd.read_csv(csv_path) 6 | 7 | model_path = '/path/to/downloaded/model.pkl' 8 | with open(model_path, 'rb') as f: 9 | model = pickle.load(f) 10 | 11 | 12 | predictions = model.predict(data) 13 | 14 | print(predictions) -------------------------------------------------------------------------------- /web_service/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .extentions import db 3 | 4 | from .routes.main import main_bp 5 | from .routes.image import image_bp 6 | from .routes.ml_manager import ml_manager_bp 7 | from .routes.dataset_manager import dataset_manager_bp 8 | from .routes.table_processor import table_processor_bp 9 | from .routes.tracking import tracking_bp 10 | from .routes.image_predictor import image_predictor_bp 11 | from .routes.auto_ml import auto_ml_bp 12 | from .routes.sound_classification import sound_classification_bp 13 | from .routes.sound_predictor import sound_predictor_bp 14 | 15 | def create_app(): 16 | # Инициализация приложения 17 | app = Flask(__name__, template_folder='templates', static_folder='static') 18 | 19 | app.secret_key = 'kak i zaebalsya' 20 | 21 | # Инициализация базы данных 22 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db' 23 | db.init_app(app) 24 | with app.app_context(): 25 | db.create_all() 26 | 27 | app.register_blueprint(main_bp) 28 | app.register_blueprint(image_bp) 29 | app.register_blueprint(ml_manager_bp) 30 | app.register_blueprint(dataset_manager_bp) 31 | app.register_blueprint(table_processor_bp) 32 | app.register_blueprint(tracking_bp) 33 | app.register_blueprint(image_predictor_bp) 34 | app.register_blueprint(auto_ml_bp) 35 | app.register_blueprint(sound_classification_bp) 36 | app.register_blueprint(sound_predictor_bp) 37 | 38 | import pandas as pd 39 | @app.template_filter('datetime') 40 | def datetime_filter(value): 41 | return pd.to_datetime(value).strftime('%Y-%m-%d %H:%M:%S') 42 | 43 | return app 44 | -------------------------------------------------------------------------------- /web_service/app/automlFitter.py: -------------------------------------------------------------------------------- 1 | # automlFitter.py 2 | import sys 3 | import os 4 | import time 5 | import pandas as pd 6 | import mlflow 7 | import mlflow.sklearn 8 | 9 | from sklearn.ensemble import ( 10 | RandomForestClassifier, 11 | RandomForestRegressor, 12 | GradientBoostingClassifier, 13 | GradientBoostingRegressor 14 | ) 15 | from sklearn.svm import SVC, SVR 16 | from sklearn.linear_model import LogisticRegression 17 | from sklearn.model_selection import train_test_split 18 | from sklearn.metrics import accuracy_score, f1_score, r2_score, mean_squared_error 19 | 20 | 21 | from .AutoMl.automl.models import BayesianOptimizationHPO, EvolutionaryStrategyHPO 22 | 23 | 24 | class AutoMLFitter: 25 | def __init__(self, hpo_method, hpo_params, model_name, param_space_list, dataset, task_type, split_ratio, 26 | target_column, scoring): 27 | self.hpo_method = hpo_method 28 | self.hpo_params = hpo_params 29 | self.model_name = model_name 30 | self.model_param_space = param_space_list 31 | self.task_type = task_type 32 | self.split_ratio = split_ratio 33 | self.target_column = target_column 34 | self.scoring = scoring 35 | 36 | df = pd.read_csv(dataset.file_path) 37 | self.dataset = dataset 38 | 39 | X = df.drop(target_column, axis=1) 40 | y = df[target_column] 41 | self.X_train, self.X_test, self.y_train, self.y_test = train_test_split( 42 | X, y, train_size=float(split_ratio) / 100) 43 | 44 | # Маппинг для моделей sklearn 45 | self.model_mapping = { 46 | # Classification models 47 | "RandomForestClassifier": RandomForestClassifier, 48 | "SVC": SVC, 49 | "LogisticRegression": LogisticRegression, 50 | "GradientBoostingClassifier": GradientBoostingClassifier, 51 | 52 | # Regression models 53 | "RandomForestRegressor": RandomForestRegressor, 54 | "SVR": SVR, 55 | "GradientBoostingRegressor": GradientBoostingRegressor 56 | } 57 | 58 | # Маппинг для методов оптимизации гиперпараметров AutoML 59 | self.hpo_mapping = { 60 | "Bayesian": BayesianOptimizationHPO, 61 | "Evolutionary": EvolutionaryStrategyHPO 62 | } 63 | 64 | def _f(self, **param_space): 65 | model = self.model_class(**param_space) 66 | model.fit(self.X_train, self.y_train) 67 | return self._calculate_score(model) 68 | 69 | def _calculate_score(self, model): 70 | y_pred = model.predict(self.X_test) 71 | if self.task_type == 'classification': 72 | if self.scoring == 'accuracy': 73 | score = accuracy_score(self.y_test, y_pred) 74 | elif self.scoring == 'f1': 75 | score = f1_score(self.y_test, y_pred, average='weighted') 76 | else: 77 | score = model.score(self.X_test, self.y_test) 78 | else: 79 | if self.scoring == 'r2': 80 | score = r2_score(self.y_test, y_pred) 81 | elif self.scoring == 'mse': 82 | score = mean_squared_error(self.y_test, y_pred) 83 | else: 84 | score = model.score(self.X_test, self.y_test) 85 | return score 86 | 87 | def fit(self): 88 | start_time = time.time() 89 | with mlflow.start_run() as run: 90 | mlflow.set_tag("status", "training") 91 | mlflow.log_param("scoring", self.scoring) 92 | mlflow.log_param("model_name", self.model_name) 93 | mlflow.log_param("dataset", f"{self.dataset.file_name} (id:{self.dataset.id})") 94 | mlflow.log_param("task_type", self.task_type) 95 | mlflow.log_param("train/test ratio", self.split_ratio) 96 | mlflow.log_param("target_column", self.target_column) 97 | mlflow.log_param("hpo_method", self.hpo_method) 98 | mlflow.log_param("hpo_params", self.hpo_params) 99 | mlflow.set_tag("model_type", "auto_ml") 100 | 101 | self.model_class = self.model_mapping.get(self.model_name) 102 | self.hpo_class = self.hpo_mapping.get(self.hpo_method) 103 | hpo_model = self.hpo_class(self._f, self.model_param_space, **self.hpo_params) 104 | 105 | best_params, history = hpo_model.optimize() 106 | training_time = time.time() - start_time 107 | 108 | model = self.model_class(**best_params) 109 | model.fit(self.X_train, self.y_train) 110 | best_score = self._calculate_score(model) 111 | 112 | mlflow.log_metric("score", best_score) 113 | mlflow.log_metric("training_time", training_time) 114 | 115 | mlflow.log_param("best_params", best_params) 116 | mlflow.sklearn.log_model(model, "model") 117 | mlflow.set_tag("status", "finished") -------------------------------------------------------------------------------- /web_service/app/exceptions/folderNotFoundError.py: -------------------------------------------------------------------------------- 1 | class FolderNotFoundError(Exception): 2 | pass -------------------------------------------------------------------------------- /web_service/app/extentions.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() -------------------------------------------------------------------------------- /web_service/app/fitter.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import mlflow 4 | import mlflow.sklearn 5 | import pandas as pd 6 | from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor 7 | from sklearn import svm 8 | from sklearn.linear_model import LogisticRegression, LinearRegression 9 | from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor 10 | from sklearn.model_selection import train_test_split 11 | from sklearn.metrics import accuracy_score, f1_score, r2_score, mean_squared_error 12 | 13 | class Fitter: 14 | def __init__(self, task_type, model_name, params, dataset, split_ratio, target_column, scoring): 15 | self.task_type = task_type 16 | self.model_name = model_name 17 | self.params = params 18 | self.dataset = dataset 19 | self.scoring = scoring 20 | self.target_column = target_column 21 | self.split_ratio = split_ratio 22 | 23 | def fit(self): 24 | with mlflow.start_run() as run: 25 | mlflow.set_tag("status", "training") 26 | mlflow.log_params(self.params) 27 | mlflow.log_param("scoring", self.scoring) 28 | mlflow.log_param("model_name", self.model_name) 29 | mlflow.log_param("dataset", f"{self.dataset.file_name} (id:{self.dataset.id})") 30 | mlflow.log_param("task_type", self.task_type) 31 | mlflow.log_param("train/test ratio", self.split_ratio) 32 | mlflow.log_param("target_column", self.target_column) 33 | 34 | 35 | file_path = self.dataset.file_path 36 | df = pd.read_csv(file_path) 37 | X = df.drop(self.target_column, axis = 1) 38 | y = df[self.target_column] 39 | if self.task_type == 'regression': 40 | y = pd.to_numeric(y, errors='raise') 41 | 42 | X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=self.split_ratio / 100) 43 | if self.task_type == 'classification': 44 | model_constructors = { 45 | 'RandomForestClassifier': lambda p: RandomForestClassifier(**p), 46 | 'SVM': lambda p: svm.SVC(**p), 47 | 'LogisticRegression': lambda p: LogisticRegression(**p), 48 | 'KNN': lambda p: KNeighborsClassifier(**p) 49 | } 50 | else: 51 | model_constructors = { 52 | 'RandomForestRegressor': lambda p: RandomForestRegressor(**p), 53 | 'SVR': lambda p: svm.SVR(**p), 54 | 'LinearRegression': lambda p: LinearRegression(**p), 55 | 'KNNRegressor': lambda p: KNeighborsRegressor(**p) 56 | } 57 | if self.model_name not in model_constructors: 58 | raise ValueError(f"Model {self.model_name} is not supported.") 59 | model = model_constructors[self.model_name](self.params) 60 | 61 | start_time = time.time() 62 | model.fit(X_train, y_train) 63 | training_time = time.time() - start_time 64 | 65 | y_pred = model.predict(X_test) 66 | 67 | if self.task_type == 'classification': 68 | if self.scoring == 'accuracy': 69 | score = accuracy_score(y_test, y_pred) 70 | elif self.scoring == 'f1': 71 | score = f1_score(y_test, y_pred, average='weighted') 72 | else: 73 | score = model.score(X_test, y_test) 74 | else: 75 | if self.scoring == 'r2': 76 | score = r2_score(y_test, y_pred) 77 | elif self.scoring == 'mse': 78 | score = mean_squared_error(y_test, y_pred) 79 | else: 80 | score = model.score(X_test, y_test) 81 | 82 | 83 | mlflow.sklearn.log_model(model, "model") 84 | mlflow.log_metric("score", score) 85 | mlflow.log_metric("training_time", training_time) 86 | mlflow.set_tag("status", "finished") 87 | mlflow.set_tag("model_type", "classic_ml") 88 | 89 | classification_models = { 90 | 'RandomForestClassifier': { 91 | 'n_estimators': { 92 | 'type': 'int', 93 | 'default': 100, 94 | 'description': 'Number of trees in the forest' 95 | }, 96 | 'max_depth': { 97 | 'type': 'int', 98 | 'default': 20, 99 | 'description': 'Maximum tree depth' 100 | }, 101 | 'min_samples_split' : { 102 | 'type' : 'int', 103 | 'default' : 2, 104 | 'description' : 'Minimum number of samples required to split an internal node' 105 | } 106 | }, 107 | 'SVM': { 108 | 'C': { 109 | 'type': 'float', 110 | 'default': 1.0, 111 | 'description': 'Regularization parameter' 112 | }, 113 | 'kernel': { 114 | 'type': 'str', 115 | 'default': 'rbf', 116 | 'description': 'SVM kernel', 117 | 'options': ['rbf', 'linear', 'poly', 'sigmoid'] 118 | } 119 | }, 120 | 'LogisticRegression': { 121 | 'C': { 122 | 'type': 'float', 123 | 'default': 1.0, 124 | 'description': 'Inverse regularization strength' 125 | }, 126 | 'max_iter': { 127 | 'type': 'int', 128 | 'default': 100, 129 | 'description': 'Maximum number of iterations' 130 | } 131 | }, 132 | 'KNN': { 133 | 'n_neighbors': { 134 | 'type': 'int', 135 | 'default': 5, 136 | 'description': 'Number of neighbors' 137 | }, 138 | 'weights': { 139 | 'type': 'str', 140 | 'default': 'uniform', 141 | 'description': 'Weighting method', 142 | 'options': ['uniform', 'distance'] 143 | } 144 | } 145 | } 146 | 147 | regression_models = { 148 | 'RandomForestRegressor': { 149 | 'n_estimators': { 150 | 'type': 'int', 151 | 'default': 100, 152 | 'description': 'Number of trees in the forest' 153 | }, 154 | 'max_depth': { 155 | 'type': 'int', 156 | 'default': 20, 157 | 'description': 'Maximum tree depth' 158 | } 159 | }, 160 | 'SVR': { 161 | 'C': { 162 | 'type': 'float', 163 | 'default': 1.0, 164 | 'description': 'Regularization parameter' 165 | }, 166 | 'kernel': { 167 | 'type': 'str', 168 | 'default': 'rbf', 169 | 'description': 'SVR kernel', 170 | 'options': ['rbf', 'linear', 'poly', 'sigmoid'] 171 | } 172 | }, 173 | 'LinearRegression': {}, 174 | 'KNNRegressor': { 175 | 'n_neighbors': { 176 | 'type': 'int', 177 | 'default': 5, 178 | 'description': 'Number of neighbors' 179 | }, 180 | 'weights': { 181 | 'type': 'str', 182 | 'default': 'uniform', 183 | 'description': 'Weighting method', 184 | 'options': ['uniform', 'distance'] 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /web_service/app/models.py: -------------------------------------------------------------------------------- 1 | from .extentions import db 2 | 3 | class Dataset(db.Model): 4 | __tablename__ = 'datasets' 5 | 6 | id = db.Column(db.Integer, primary_key=True) 7 | file_name = db.Column(db.String, nullable=False) 8 | file_path = db.Column(db.String, nullable=False) 9 | problem_type = db.Column(db.String, nullable=False) 10 | process_status = db.Column(db.Boolean, nullable=False) 11 | 12 | def __init__(self, file_name, file_path, problem_type): 13 | self.file_name = file_name 14 | self.file_path = file_path 15 | self.problem_type = problem_type 16 | if self.check_dataset() != True: 17 | self.process_status = False 18 | 19 | 20 | def check_dataset(self): 21 | script = True 22 | if script: 23 | self.process_status = True 24 | return True 25 | 26 | return False 27 | 28 | def __repr__(self): 29 | return {"id" : self.id, "file_name" : self.file_name, "file_path" : self.file_path, "problem_type" : self.problem_type, "process_status" : self.process_status} 30 | -------------------------------------------------------------------------------- /web_service/app/routes/dataset_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | 4 | from flask import Blueprint, render_template, redirect, request, flash, url_for, send_file 5 | 6 | from ..extentions import db 7 | from ..models import Dataset 8 | 9 | 10 | dataset_manager_bp = Blueprint('dataset_manager', __name__) 11 | 12 | @dataset_manager_bp.route('/dataset_manager', methods=['GET']) 13 | def index(): 14 | dataset_list = Dataset.query.all() 15 | return render_template('dataset_manager.html', datasets=dataset_list, active_page='table_datasets') 16 | 17 | 18 | @dataset_manager_bp.route('/upload', methods=['POST']) 19 | def upload_file(): 20 | if 'file' not in request.files: 21 | flash('Нет файла для загрузки') 22 | return redirect(url_for('dataset_manager.index')) 23 | 24 | file = request.files['file'] 25 | if file.filename == '': 26 | flash('Нет выбранного файла') 27 | return redirect(url_for('dataset_manager.index')) 28 | 29 | if file and file.filename.endswith('.csv'): 30 | # Генерируем уникальное имя файла 31 | unique_id = str(uuid.uuid4()) 32 | file_extension = os.path.splitext(file.filename)[1] 33 | new_file_name = f"{unique_id}{file_extension}" 34 | 35 | if not os.path.exists('datasets'): 36 | os.makedirs('datasets') 37 | 38 | file_path = os.path.join('datasets', new_file_name) 39 | file_name = file.filename 40 | 41 | file.save(file_path) 42 | 43 | problem_type = request.form['problem_type'] 44 | new_dataset = Dataset(file_name = file_name, file_path = file_path, problem_type = problem_type) 45 | db.session.add(new_dataset) 46 | db.session.commit() 47 | flash('Файл успешно загружен') 48 | return redirect(url_for('dataset_manager.index')) 49 | else: 50 | flash('Неподдерживаемый формат файла. Пожалуйста, загрузите CSV файл.') 51 | return redirect(url_for('dataset_manager.index')) 52 | 53 | 54 | @dataset_manager_bp.route('/delete/', methods=['POST']) 55 | def delete_dataset(dataset_id): 56 | dataset = Dataset.query.get(dataset_id) 57 | if dataset: 58 | os.remove(dataset.file_path) 59 | db.session.delete(dataset) 60 | db.session.commit() 61 | flash('Датасет удален') 62 | else: 63 | flash('Датасет не найден') 64 | 65 | return redirect(url_for('dataset_manager.index')) 66 | 67 | @dataset_manager_bp.route('/download/', methods=['GET']) 68 | def download_dataset(dataset_id): 69 | dataset = Dataset.query.get(dataset_id) 70 | if dataset and os.path.exists(dataset.file_path): 71 | return send_file("../" + dataset.file_path, as_attachment=True, download_name=dataset.file_name) 72 | else: 73 | flash('Файл не найден') 74 | return redirect(url_for('dataset_manager.index')) -------------------------------------------------------------------------------- /web_service/app/routes/image.py: -------------------------------------------------------------------------------- 1 | # image.py 2 | from flask import Blueprint, jsonify, redirect, render_template, request, url_for, flash, Response 3 | from dotenv import load_dotenv 4 | import os 5 | import shutil 6 | import json 7 | import subprocess 8 | import uuid 9 | 10 | from app.exceptions.folderNotFoundError import FolderNotFoundError 11 | 12 | load_dotenv() 13 | 14 | ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'} 15 | UPLOAD_FOLDER_PATH = 'images/classification_dataset/train' 16 | UTILS_PATH = 'utils/image_classification' 17 | YC_TOKEN = os.environ.get('YC_TOKEN') 18 | PROJECT_ID = os.environ.get('PROJECT_ID') 19 | CONFIG_YAML_FILE = os.environ.get('CONFIG_YAML_FILE') 20 | 21 | # Словарь для хранения активных процессов обучения 22 | # Ключ: job_id, Значение: {'process': Popen_object, 'status': 'running'/'completed'/'failed'} 23 | active_training_processes = {} 24 | 25 | # Функция проверки расширения загруженного файла 26 | def file_extension_validation(filename): 27 | return "." in filename and filename.split(".", 1)[1].lower() in ALLOWED_EXTENSIONS 28 | 29 | # Функция для создания zip-архива из имеющейся папки 30 | def create_zip(source_folder, zip_name): 31 | if not os.path.exists(source_folder): 32 | raise FolderNotFoundError(f"Folder {source_folder} not found") 33 | shutil.make_archive(os.path.join(UTILS_PATH, zip_name), 'zip', source_folder) 34 | 35 | image_bp = Blueprint('image', __name__) 36 | 37 | @image_bp.route('/image_page', methods=['GET']) 38 | def index(): 39 | return render_template('upload_image.html', active_page='image') 40 | 41 | @image_bp.route('/upload_images', methods=['POST']) 42 | def upload_images(): 43 | os.makedirs(UPLOAD_FOLDER_PATH, exist_ok=True) 44 | 45 | if not request.files: 46 | return jsonify({"success": False, "message": "No files were uploaded."}), 400 47 | 48 | class_file_count = {} 49 | for class_id in request.files: 50 | class_name = class_id.replace("[]", "").lower() 51 | files = request.files.getlist(class_id) 52 | valid_files = [f for f in files if f.filename and file_extension_validation(f.filename)] 53 | if class_name not in class_file_count: 54 | class_file_count[class_name] = 0 55 | class_file_count[class_name] += len(valid_files) 56 | 57 | invalid_classes = {name: count for name, count in class_file_count.items() if count < 15} 58 | if invalid_classes: 59 | error_message = "There are not enough images for the following classes: " 60 | error_message += ", ".join([f"{name} ({count}/15)" for name, count in invalid_classes.items()]) 61 | return jsonify({"success": False, "message": error_message}), 400 62 | 63 | class_mapping = {} 64 | class_index = 0 65 | for class_id in request.files: 66 | class_name = class_id.replace("[]", "").lower() 67 | if class_name not in class_mapping: 68 | class_mapping[class_name] = class_index 69 | class_index += 1 70 | class_folder = os.path.join(UPLOAD_FOLDER_PATH, f"{class_name}") 71 | os.makedirs(class_folder, exist_ok=True) 72 | for file in request.files.getlist(class_id): 73 | if not file.filename: 74 | continue 75 | if file_extension_validation(file.filename): 76 | filepath = os.path.join(class_folder, file.filename) 77 | file.save(filepath) 78 | 79 | try: 80 | create_zip('images', 'classification_dataset') 81 | except FolderNotFoundError as e: 82 | return jsonify({"success": False, "message": str(e)}), 500 83 | finally: 84 | if os.path.exists('images/classification_dataset'): 85 | shutil.rmtree('images/classification_dataset') 86 | 87 | 88 | json_data = json.dumps(class_mapping, ensure_ascii=False, indent=2) 89 | json_filename = os.path.join(UTILS_PATH, 'class_to_idx.json') 90 | with open(json_filename, "w", encoding='utf-8') as json_file: 91 | json_file.write(json_data) 92 | 93 | job_id = str(uuid.uuid4()) 94 | command = ['datasphere', 'project', 'job', 'execute', 95 | '-p', PROJECT_ID, '-c', CONFIG_YAML_FILE] 96 | process_cwd = UTILS_PATH 97 | 98 | try: 99 | process = subprocess.Popen( 100 | command, 101 | cwd=process_cwd, 102 | stdout=subprocess.PIPE, 103 | stderr=subprocess.STDOUT, 104 | text=True, 105 | bufsize=1, 106 | universal_newlines=True 107 | ) 108 | active_training_processes[job_id] = {'process': process, 'status': 'running'} 109 | return jsonify({"success": True, "message": "Training process started.", "job_id": job_id}) 110 | except Exception as e: 111 | return jsonify({"success": False, "message": f"Failed to start training process: {str(e)}"}), 500 112 | 113 | def generate_log_stream_for_job(job_id, redirect_url_on_success, processes_dict): 114 | if job_id not in processes_dict or not processes_dict[job_id].get('process'): 115 | yield f"data: Error: Job ID {job_id} not found or process not started.\n\n" 116 | yield f"data: EVENT:STREAM_ENDED\n\n" 117 | return 118 | 119 | process_info = processes_dict[job_id] 120 | process = process_info['process'] 121 | 122 | yield f"data: Starting training log for job {job_id}...\n\n" 123 | 124 | try: 125 | for line in iter(process.stdout.readline, ''): 126 | processed_line = line.rstrip('\n\r') 127 | yield f"data: {processed_line}\n\n" 128 | 129 | process.stdout.close() 130 | return_code = process.wait() 131 | 132 | if return_code == 0: 133 | process_info['status'] = 'completed' 134 | yield f"data: EVENT:TRAINING_SUCCESS\n\n" 135 | yield f"data: Training completed successfully. You will be redirected shortly.\n\n" 136 | yield f"data: REDIRECT:{redirect_url_on_success}\n\n" 137 | else: 138 | process_info['status'] = 'failed' 139 | yield f"data: EVENT:TRAINING_FAILED\n\n" 140 | yield f"data: Training failed with exit code {return_code}.\n\n" 141 | except Exception as e: 142 | process_info['status'] = 'failed' 143 | yield f"data: EVENT:TRAINING_ERROR\n\n" 144 | yield f"data: An error occurred while streaming logs: {str(e)}\n\n" 145 | finally: 146 | if job_id in processes_dict: 147 | processes_dict[job_id]['process'] = None 148 | yield f"data: EVENT:STREAM_ENDED\n\n" 149 | 150 | @image_bp.route('/stream_image_logs/') 151 | def stream_image_logs(job_id): 152 | redirect_url = url_for('image_predictor.index') 153 | return Response(generate_log_stream_for_job(job_id, redirect_url, active_training_processes), mimetype='text/event-stream') 154 | 155 | @image_bp.route('/save_hyperparameters', methods=['POST']) 156 | def save_hyperparameters(): 157 | try: 158 | data = request.get_json() 159 | hyperparams = { 160 | "num_epochs": int(data.get('num_epochs', 10)), 161 | "learning_rate": float(data.get('learning_rate', 0.0001)), 162 | "batch_size": int(data.get('batch_size', 8)), 163 | "weight_decay": float(data.get('weight_decay', 0.0001)) 164 | } 165 | hyperparams_path = os.path.join(UTILS_PATH, 'hyperparams.json') 166 | with open(hyperparams_path, 'w') as f: 167 | json.dump(hyperparams, f, indent=4) 168 | return jsonify({ 169 | "success": True, 170 | "message": "Hyperparameters saved successfully", 171 | "hyperparameters": hyperparams 172 | }), 200 173 | except Exception as e: 174 | return jsonify({ 175 | "success": False, 176 | "message": f"Failed to save hyperparameters: {str(e)}" 177 | }), 500 -------------------------------------------------------------------------------- /web_service/app/routes/image_predictor.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, flash, render_template, request, redirect, url_for, send_file, abort 2 | import subprocess 3 | import os 4 | import re 5 | import shutil 6 | import tempfile 7 | import zipfile 8 | 9 | 10 | from app.routes.image import file_extension_validation 11 | 12 | INFERENCE_SCRIPT_PATH = 'utils/image_classification/inference.py' 13 | UPLOAD_FOLDER_PATH = 'images' 14 | STATIC_FOLDER = 'app/static' 15 | MODEL_PATH = 'utils/image_classification/trained_model_classification.pt' 16 | СLASS_TO_IDX_PATH = 'utils/image_classification/class_to_idx.json' 17 | HYPERPARAMS_PATH = 'utils/image_classification/hyperparams.json' 18 | 19 | image_predictor_bp = Blueprint('image_predictor', __name__) 20 | 21 | 22 | @image_predictor_bp.route('/image_prediction', methods=['GET']) 23 | def index(): 24 | return render_template('image_prediction.html') 25 | 26 | 27 | @image_predictor_bp.route('/download_model', methods=['GET']) 28 | def download_model(): 29 | print(f"Attempting to download image model package...") 30 | 31 | model_path = MODEL_PATH 32 | class_to_idx_path = СLASS_TO_IDX_PATH 33 | hyperparams_path = HYPERPARAMS_PATH 34 | 35 | missing_files = [] 36 | if not os.path.exists(model_path): 37 | missing_files.append('модель') 38 | if not os.path.exists(class_to_idx_path): 39 | missing_files.append('сопоставление классов') 40 | if not os.path.exists(hyperparams_path): 41 | missing_files.append('гиперпараметры') 42 | 43 | if missing_files: 44 | error_msg = f'Файлы не найдены: {", ".join(missing_files)}' 45 | print(error_msg) 46 | flash(error_msg) 47 | return redirect(url_for('image_predictor.index')) 48 | 49 | try: 50 | 51 | temp_file = tempfile.NamedTemporaryFile(suffix='.zip', delete=False) 52 | with zipfile.ZipFile(temp_file.name, 'w') as zipf: 53 | zipf.write(model_path, arcname='trained_model_classification.pt') 54 | zipf.write(class_to_idx_path, arcname='class_to_idx.json') 55 | zipf.write(hyperparams_path, arcname='hyperparameters.json') 56 | 57 | return send_file( 58 | temp_file.name, 59 | as_attachment=True, 60 | download_name="image_classification_package.zip" 61 | ) 62 | except Exception as e: 63 | error_msg = f'Ошибка при создании архива: {str(e)}' 64 | print(error_msg) 65 | flash(error_msg) 66 | return redirect(url_for('image_predictor.index')) 67 | 68 | 69 | @image_predictor_bp.route('/predict_image_class', methods=['POST']) 70 | def predict(): 71 | if not os.path.exists(UPLOAD_FOLDER_PATH): 72 | os.makedirs(UPLOAD_FOLDER_PATH) 73 | 74 | if 'image' not in request.files: 75 | flash('Файл не выбран') 76 | return redirect(url_for('image_predictor.index')) 77 | 78 | file = request.files['image'] 79 | 80 | if not file_extension_validation(file.filename): 81 | flash('Неверное расширение у файла') 82 | return redirect(url_for('image_predictor.index')) 83 | 84 | file_path = os.path.join(UPLOAD_FOLDER_PATH, file.filename) 85 | file.save(file_path) 86 | 87 | try: 88 | result = subprocess.run( 89 | ['python', INFERENCE_SCRIPT_PATH, file_path], 90 | capture_output=True, text=True 91 | ) 92 | 93 | if result.returncode != 0: 94 | flash(f"Ошибка при исполнении скрипта для предсказания: {result.stderr}") 95 | return redirect(url_for('image_predictor.index')) 96 | 97 | # Получаем данные из inference-скрипта 98 | output = result.stdout 99 | match = re.search(r'Plot saved to: (.*\.png)', output) 100 | if not match: 101 | flash('Could not find path to class diagram image') 102 | return redirect(url_for('image_predictor.index')) 103 | 104 | # Получаем путь к .png файлу 105 | image_path = match.group(1) 106 | image_filename = os.path.basename(image_path) 107 | 108 | if not os.path.exists(STATIC_FOLDER): 109 | os.makedirs(STATIC_FOLDER) 110 | 111 | # Перемещаем изображение в папку static 112 | static_image_path = os.path.join(STATIC_FOLDER, image_filename) 113 | shutil.move(image_path, static_image_path) 114 | 115 | # Формируем URL для изображения (относительный путь от папки static) 116 | image_url = os.path.join(image_filename) 117 | 118 | return render_template('image_prediction_result.html', image_url=image_url) 119 | 120 | except Exception as e: 121 | flash(f"Ошибка при попытке предсказания: {str(e)}") 122 | return redirect(url_for('image_predictor.index')) 123 | -------------------------------------------------------------------------------- /web_service/app/routes/main.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | 4 | main_bp = Blueprint('main', __name__) 5 | 6 | 7 | @main_bp.route('/') 8 | def index(): 9 | return render_template('index.html', is_home = True) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web_service/app/routes/ml_manager.py: -------------------------------------------------------------------------------- 1 | import time 2 | import csv 3 | import threading 4 | from flask import Blueprint, jsonify, render_template, request, redirect, url_for 5 | from ..fitter import classification_models, regression_models, Fitter 6 | from ..models import Dataset 7 | 8 | ml_manager_bp = Blueprint('ml_manager', __name__) 9 | 10 | @ml_manager_bp.route('/ml_manager', methods=['GET']) 11 | def index(): 12 | datasets = Dataset.query.all() 13 | 14 | # Вычисляем, доступны ли датасеты для каждого типа задач 15 | classification_available = any(d.problem_type == 'classification' for d in datasets) 16 | regression_available = any(d.problem_type == 'regression' for d in datasets) 17 | 18 | classification_metrics = [ 19 | {'value': 'accuracy', 'label': 'Accuracy'}, 20 | {'value': 'f1', 'label': 'F1 Score'} 21 | ] 22 | regression_metrics = [ 23 | {'value': 'r2', 'label': 'R2 Score'}, 24 | {'value': 'mse', 'label': 'Mean Squared Error'} 25 | ] 26 | 27 | datasets_json = [{ 28 | 'id': d.id, 29 | 'file_name': d.file_name, 30 | 'problem_type': d.problem_type, 31 | 'process_status': d.process_status 32 | } for d in datasets] 33 | 34 | return render_template( 35 | 'ml_manager.html', 36 | classification_models=classification_models, 37 | regression_models=regression_models, 38 | datasets_json=datasets_json, 39 | classification_metrics=classification_metrics, 40 | regression_metrics=regression_metrics, 41 | classification_available=classification_available, 42 | regression_available=regression_available, 43 | active_page='classic_ml' 44 | ) 45 | 46 | @ml_manager_bp.route('/get_model_params') 47 | def get_model_params(): 48 | model_name = request.args.get('model') 49 | if model_name in classification_models: 50 | params = classification_models[model_name] 51 | elif model_name in regression_models: 52 | params = regression_models[model_name] 53 | else: 54 | params = {} 55 | return jsonify(params) 56 | 57 | @ml_manager_bp.route('/get_target_columns') 58 | def get_target_columns(): 59 | dataset_id = request.args.get('dataset_id') 60 | dataset = Dataset.query.get(dataset_id) 61 | if not dataset: 62 | return jsonify([]) 63 | try: 64 | with open(dataset.file_path, newline='', encoding='utf-8') as csvfile: 65 | reader = csv.reader(csvfile) 66 | columns = next(reader) 67 | except Exception as e: 68 | return jsonify({'error': str(e)}), 500 69 | return jsonify(columns) 70 | 71 | @ml_manager_bp.route('/train', methods=['POST']) 72 | def train_model(): 73 | dataset_id = request.form.get('dataset') 74 | task_type = request.form.get('task_type') 75 | target_column = request.form.get('target_column') 76 | 77 | if not dataset_id or not task_type or not target_column: 78 | return jsonify({'error': 'Не выбраны обязательные параметры: датасет, тип задачи или целевая колонка'}), 400 79 | 80 | dataset = Dataset.query.get(dataset_id) 81 | if not dataset: 82 | return jsonify({'error': 'Выбранный датасет не найден'}), 400 83 | 84 | model_name = request.form['model'] 85 | scoring = request.form['scoring'] 86 | split_ratio = float(request.form.get('split_ratio', 70)) 87 | 88 | params = {} 89 | for key in request.form: 90 | if key.startswith('param_'): 91 | param_name = key.replace('param_', '') 92 | model_dict = classification_models if task_type == 'classification' else regression_models 93 | param_config = model_dict.get(model_name, {}).get(param_name, {}) 94 | raw_value = request.form[key] 95 | if raw_value == "" or raw_value is None: 96 | params[param_name] = param_config.get('default') 97 | else: 98 | if param_config.get('type') == 'int': 99 | params[param_name] = int(raw_value) 100 | elif param_config.get('type') == 'float': 101 | params[param_name] = float(raw_value) 102 | else: 103 | params[param_name] = raw_value 104 | 105 | def async_train(): 106 | try: 107 | fitter = Fitter( 108 | task_type=task_type, 109 | model_name=model_name, 110 | params=params, 111 | dataset=dataset, 112 | split_ratio=split_ratio, 113 | target_column=target_column, 114 | scoring=scoring 115 | ) 116 | fitter.fit() 117 | except Exception as e: 118 | print(f"Training failed: {str(e)}") 119 | 120 | train_thread = threading.Thread(target=async_train) 121 | train_thread.start() 122 | time.sleep(0.2) 123 | 124 | return redirect(url_for('tracking.index')) 125 | -------------------------------------------------------------------------------- /web_service/app/routes/sound_classification.py: -------------------------------------------------------------------------------- 1 | # sound_classification.py 2 | from flask import Blueprint, jsonify, redirect, render_template, request, url_for, flash, Response 3 | from dotenv import load_dotenv 4 | import os 5 | import shutil 6 | import json 7 | import subprocess 8 | import uuid 9 | 10 | 11 | from app.exceptions.folderNotFoundError import FolderNotFoundError 12 | 13 | 14 | load_dotenv() 15 | 16 | ALLOWED_EXTENSIONS = {'wav', 'mp3', 'flac'} 17 | UPLOAD_FOLDER_PATH = 'sounds/sound_dataset/train' 18 | UTILS_PATH = 'utils/sound_classification' 19 | YC_TOKEN = os.environ.get('YC_TOKEN') 20 | PROJECT_ID = os.environ.get('PROJECT_ID') 21 | CONFIG_YAML_FILE = os.environ.get('CONFIG_YAML_FILE') 22 | 23 | active_sound_training_processes = {} # Отдельный словарь для звуковых процессов 24 | 25 | def file_extension_validation(filename): 26 | return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS 27 | 28 | 29 | def create_zip(source_folder, zip_name): 30 | if not os.path.exists(source_folder): 31 | raise FolderNotFoundError(f"Folder {source_folder} not found") 32 | shutil.make_archive(os.path.join(UTILS_PATH, zip_name), 'zip', source_folder) 33 | 34 | 35 | sound_classification_bp = Blueprint('sound_classification', __name__) 36 | 37 | @sound_classification_bp.route('/sound_classification', methods=['GET']) 38 | def index(): 39 | return render_template('sound_classification.html', active_page='sound') 40 | 41 | @sound_classification_bp.route('/upload_sounds', methods=['POST']) 42 | def upload_sounds(): 43 | os.makedirs(UPLOAD_FOLDER_PATH, exist_ok=True) 44 | 45 | if not request.files: 46 | return jsonify({"success": False, "message": "No files were uploaded."}), 400 47 | 48 | class_file_count = {} 49 | for class_id in request.files: 50 | class_name = class_id.replace("[]", "").lower() 51 | files = request.files.getlist(class_id) 52 | valid_files = [f for f in files if f.filename and file_extension_validation(f.filename)] 53 | if class_name not in class_file_count: 54 | class_file_count[class_name] = 0 55 | class_file_count[class_name] += len(valid_files) 56 | 57 | invalid_classes = {name: count for name, count in class_file_count.items() if count < 15} 58 | if invalid_classes: 59 | error_message = "Not enough audio files for classes: " 60 | error_message += ", ".join([f"{name} ({count}/15)" for name, count in invalid_classes.items()]) 61 | return jsonify({"success": False, "message": error_message}), 400 62 | 63 | class_mapping = {} 64 | class_index = 0 65 | for class_id in request.files: 66 | class_name = class_id.replace("[]", "").lower() 67 | if class_name not in class_mapping: 68 | class_mapping[class_name] = class_index 69 | class_index += 1 70 | class_folder = os.path.join(UPLOAD_FOLDER_PATH, f"{class_name}") 71 | os.makedirs(class_folder, exist_ok=True) 72 | for file in request.files.getlist(class_id): 73 | if not file.filename: 74 | continue 75 | if file_extension_validation(file.filename): 76 | filepath = os.path.join(class_folder, file.filename) 77 | file.save(filepath) 78 | 79 | try: 80 | create_zip('sounds', 'sound_dataset') 81 | except FolderNotFoundError as e: 82 | return jsonify({"success": False, "message": str(e)}), 500 83 | finally: 84 | if os.path.exists('sounds/sound_dataset'): 85 | shutil.rmtree('sounds/sound_dataset') 86 | 87 | 88 | json_data = json.dumps(class_mapping, ensure_ascii=False, indent=2) 89 | json_filename = os.path.join(UTILS_PATH, 'class_to_idx.json') 90 | with open(json_filename, "w", encoding='utf-8') as json_file: 91 | json_file.write(json_data) 92 | 93 | job_id = str(uuid.uuid4()) 94 | command = ['datasphere', 'project', 'job', 'execute', 95 | '-p', PROJECT_ID, '-c', CONFIG_YAML_FILE] 96 | process_cwd = UTILS_PATH 97 | 98 | try: 99 | process = subprocess.Popen( 100 | command, 101 | cwd=process_cwd, 102 | stdout=subprocess.PIPE, 103 | stderr=subprocess.STDOUT, 104 | text=True, 105 | bufsize=1, 106 | universal_newlines=True 107 | ) 108 | active_sound_training_processes[job_id] = {'process': process, 'status': 'running'} 109 | return jsonify({"success": True, "message": "Training process started.", "job_id": job_id}) 110 | except Exception as e: 111 | return jsonify({"success": False, "message": f"Failed to start training process: {str(e)}"}), 500 112 | 113 | 114 | def generate_log_stream_for_job_sound(job_id, redirect_url_on_success, processes_dict): 115 | if job_id not in processes_dict or not processes_dict[job_id].get('process'): 116 | yield f"data: Error: Job ID {job_id} not found or process not started.\n\n" 117 | yield f"data: EVENT:STREAM_ENDED\n\n" 118 | return 119 | 120 | process_info = processes_dict[job_id] 121 | process = process_info['process'] 122 | 123 | yield f"data: Starting training log for job {job_id}...\n\n" 124 | 125 | try: 126 | for line in iter(process.stdout.readline, ''): 127 | processed_line = line.rstrip('\n\r') 128 | yield f"data: {processed_line}\n\n" 129 | 130 | process.stdout.close() 131 | return_code = process.wait() 132 | 133 | if return_code == 0: 134 | process_info['status'] = 'completed' 135 | yield f"data: EVENT:TRAINING_SUCCESS\n\n" 136 | yield f"data: Training completed successfully. You will be redirected shortly.\n\n" 137 | yield f"data: REDIRECT:{redirect_url_on_success}\n\n" 138 | else: 139 | process_info['status'] = 'failed' 140 | yield f"data: EVENT:TRAINING_FAILED\n\n" 141 | yield f"data: Training failed with exit code {return_code}.\n\n" 142 | except Exception as e: 143 | process_info['status'] = 'failed' 144 | yield f"data: EVENT:TRAINING_ERROR\n\n" 145 | yield f"data: An error occurred while streaming logs: {str(e)}\n\n" 146 | finally: 147 | if job_id in processes_dict: 148 | processes_dict[job_id]['process'] = None 149 | yield f"data: EVENT:STREAM_ENDED\n\n" 150 | 151 | 152 | @sound_classification_bp.route('/stream_sound_logs/') 153 | def stream_sound_logs(job_id): 154 | redirect_url = url_for('sound_predictor.index') 155 | return Response(generate_log_stream_for_job_sound(job_id, redirect_url, active_sound_training_processes), mimetype='text/event-stream') 156 | 157 | 158 | @sound_classification_bp.route('/save_sound_hyperparameters', methods=['POST']) 159 | def save_hyperparameters(): 160 | try: 161 | data = request.get_json() 162 | hyperparams = { 163 | "num_epochs": int(data.get('num_epochs', 10)), 164 | "learning_rate": float(data.get('learning_rate', 0.0001)), 165 | "batch_size": int(data.get('batch_size', 8)), 166 | "weight_decay": float(data.get('weight_decay', 0.0001)), 167 | "target_size": int(data.get('target_size', 50)) 168 | } 169 | hyperparams_path = os.path.join(UTILS_PATH, 'hyperparams.json') 170 | with open(hyperparams_path, 'w') as f: 171 | json.dump(hyperparams, f, indent=4) 172 | return jsonify({ 173 | "success": True, 174 | "message": "Hyperparameters saved successfully", 175 | "hyperparameters": hyperparams 176 | }), 200 177 | except Exception as e: 178 | return jsonify({ 179 | "success": False, 180 | "message": f"Failed to save hyperparameters: {str(e)}" 181 | }), 500 -------------------------------------------------------------------------------- /web_service/app/routes/sound_predictor.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, flash, render_template, request, redirect, url_for, send_file 2 | import subprocess 3 | import os 4 | import re 5 | import shutil 6 | 7 | INFERENCE_SCRIPT_PATH = 'utils/sound_classification/inference.py' 8 | UPLOAD_FOLDER_PATH = 'sounds' 9 | STATIC_FOLDER = 'app/static' 10 | MODEL_PATH = 'utils/sound_classification/trained_model_sound_classification.pt' 11 | 12 | sound_predictor_bp = Blueprint('sound_predictor', __name__) 13 | 14 | def sound_extension_validation(filename): 15 | allowed_extensions = {'wav', 'mp3', 'ogg'} 16 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions 17 | 18 | @sound_predictor_bp.route('/sound_prediction', methods=['GET']) 19 | def index(): 20 | return render_template('sound_prediction.html') 21 | 22 | @sound_predictor_bp.route('/download_sound_model', methods=['GET']) 23 | def download_model(): 24 | print(f"Attempting to download sound model package...") 25 | 26 | model_path = 'utils/sound_classification/trained_model_sound_classification.pt' 27 | class_to_idx_path = 'utils/sound_classification/class_to_idx.json' 28 | hyperparams_path = 'utils/sound_classification/hyperparams.json' 29 | 30 | missing_files = [] 31 | if not os.path.exists(model_path): 32 | missing_files.append('модель') 33 | if not os.path.exists(class_to_idx_path): 34 | missing_files.append('сопоставление классов') 35 | if not os.path.exists(hyperparams_path): 36 | missing_files.append('гиперпараметры') 37 | 38 | if missing_files: 39 | error_msg = f'Файлы не найдены: {", ".join(missing_files)}' 40 | print(error_msg) 41 | flash(error_msg) 42 | return redirect(url_for('sound_predictor.index')) 43 | 44 | try: 45 | import tempfile 46 | import zipfile 47 | 48 | temp_file = tempfile.NamedTemporaryFile(suffix='.zip', delete=False) 49 | with zipfile.ZipFile(temp_file.name, 'w') as zipf: 50 | zipf.write(model_path, arcname='trained_model_sound_classification.pt') 51 | 52 | zipf.write(class_to_idx_path, arcname='class_to_idx.json') 53 | 54 | zipf.write(hyperparams_path, arcname='hyperparameters.json') 55 | 56 | return send_file( 57 | temp_file.name, 58 | as_attachment=True, 59 | download_name="sound_classification_package.zip" 60 | ) 61 | except Exception as e: 62 | error_msg = f'Ошибка при создании архива: {str(e)}' 63 | print(error_msg) 64 | flash(error_msg) 65 | return redirect(url_for('sound_predictor.index')) 66 | 67 | @sound_predictor_bp.route('/predict', methods=['POST']) 68 | def predict(): 69 | if not os.path.exists(UPLOAD_FOLDER_PATH): 70 | os.makedirs(UPLOAD_FOLDER_PATH) 71 | 72 | if 'sound' not in request.files: 73 | flash('No file selected') 74 | return redirect(url_for('sound_predictor.index')) 75 | 76 | file = request.files['sound'] 77 | 78 | if not sound_extension_validation(file.filename): 79 | flash('Invalid file format') 80 | return redirect(url_for('sound_predictor.index')) 81 | 82 | file_path = os.path.join(UPLOAD_FOLDER_PATH, file.filename) 83 | file.save(file_path) 84 | 85 | try: 86 | result = subprocess.run( 87 | ['python', INFERENCE_SCRIPT_PATH, file_path], 88 | capture_output=True, 89 | text=True 90 | ) 91 | 92 | print(result.returncode) 93 | print(result.stderr) 94 | print(result.stdout) 95 | 96 | if result.returncode != 0: 97 | flash(f"Prediction error: {result.stderr}") 98 | return redirect(url_for('sound_predictor.index')) 99 | 100 | # Получаем данные из inference-скрипта 101 | output = result.stdout 102 | match = re.search(r'Plot saved to: (.*\.png)', output) 103 | if not match: 104 | flash('Не удалось найти путь к изображению с диаграммой классов') 105 | return redirect(url_for('sound_predictor.index')) 106 | 107 | # Получаем путь к .png файлу 108 | spectrogram_path = match.group(1) 109 | spectrogram_filename = os.path.basename(spectrogram_path) 110 | 111 | print(spectrogram_path) 112 | 113 | if not os.path.exists(STATIC_FOLDER): 114 | os.makedirs(STATIC_FOLDER) 115 | 116 | # Перемещаем изображение в папку static 117 | static_spectrogram_path = os.path.join(STATIC_FOLDER, spectrogram_filename) 118 | shutil.move(spectrogram_path, static_spectrogram_path) 119 | 120 | # Формируем URL для изображения (относительный путь от папки static) 121 | spectrogram_url = spectrogram_filename 122 | 123 | return render_template('sound_prediction_result.html', spectrogram_url=spectrogram_url) 124 | 125 | except Exception as e: 126 | flash(f"Prediction failed: {str(e)}") 127 | return redirect(url_for('sound_predictor.index')) -------------------------------------------------------------------------------- /web_service/app/routes/table_processor.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | import pandas as pd 3 | 4 | from ..models import Dataset 5 | 6 | table_processor_bp = Blueprint('table_processor', __name__) 7 | 8 | @table_processor_bp.route('/process_dataset/', methods=['GET']) 9 | def process_dataset(dataset_id): 10 | dataset = Dataset.query.get(dataset_id) 11 | if dataset: 12 | df = pd.read_csv(dataset.file_path) 13 | return render_template('table_processing.html', tables=[df.to_html(classes='data', header="true", index=False)]) 14 | 15 | return "Датасет не найден", 404 16 | 17 | -------------------------------------------------------------------------------- /web_service/app/routes/tracking.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import ast 4 | import mlflow 5 | from flask import Blueprint, render_template, send_file, redirect, url_for, request 6 | from mlflow.tracking import MlflowClient 7 | from ..fitter import classification_models, regression_models 8 | 9 | tracking_bp = Blueprint('tracking', __name__) 10 | client = MlflowClient() 11 | 12 | @tracking_bp.route('/tracking', methods=['GET']) 13 | def index(): 14 | runs_df = mlflow.search_runs() 15 | classic_experiments = [] 16 | automl_experiments = [] 17 | for _, run in runs_df.iterrows(): 18 | model_name = run.get('params.model_name', 'Unknown Model') 19 | allowed_params = {} 20 | if model_name in classification_models: 21 | allowed_params = classification_models.get(model_name, {}) 22 | elif model_name in regression_models: 23 | allowed_params = regression_models.get(model_name, {}) 24 | status = run.get('tags.status', 'finished') 25 | model_type = run.get('tags.model_type', 'classic_ml') 26 | if model_type == 'auto_ml': 27 | best_params = {} 28 | best_params_dict = run.get("params.best_params", "{}") 29 | if best_params_dict: 30 | for k, v in ast.literal_eval(best_params_dict.replace("np.float64", "")).items(): 31 | best_params[k] = v 32 | automl_exp_data = { 33 | 'run_id': run.run_id, 34 | 'status': status, 35 | 'model_name': model_name, 36 | 'score': run.get('metrics.score', 0), 37 | 'training_time': run.get('metrics.training_time', 0), 38 | 'dataset': run.get('params.dataset', 'N/A'), 39 | 'params': best_params, 40 | 'task_type': run.get('params.task_type', 'N/A'), 41 | 'scoring': run.get('params.scoring', 'N/A'), 42 | 'optimizer': run.get('params.hpo_method', 'N/A') 43 | } 44 | automl_experiments.append(automl_exp_data) 45 | else: 46 | params = {} 47 | for k, v in run.items(): 48 | if k.startswith('params.') and k not in ['params.model_name', 'params.dataset']: 49 | param_key = k.replace('params.', '') 50 | if param_key in allowed_params: 51 | params[param_key] = v 52 | classic_exp_data = { 53 | 'run_id': run.run_id, 54 | 'status': status, 55 | 'model_name': model_name, 56 | 'score': run.get('metrics.score', 0), 57 | 'training_time': run.get('metrics.training_time', 0), 58 | 'dataset': run.get('params.dataset', 'N/A'), 59 | 'params': params, 60 | 'task_type': run.get('params.task_type', 'N/A'), 61 | 'scoring': run.get('params.scoring', 'N/A') 62 | } 63 | classic_experiments.append(classic_exp_data) 64 | return render_template("tracking.html", 65 | classic_experiments=classic_experiments, 66 | automl_experiments=automl_experiments, 67 | active_page='tracking') 68 | 69 | @tracking_bp.route('/tracking/', methods=['GET']) 70 | def tracking_detail(run_id): 71 | run = client.get_run(run_id) 72 | try: 73 | current_dir = os.path.dirname(os.path.abspath(__file__)) 74 | snippet_path = os.path.join(current_dir, '..', 'ML_snippet.py') 75 | with open(snippet_path, 'r') as f: 76 | code_snippet = f.read() 77 | except Exception: 78 | code_snippet = "Snippet not found in the app directory" 79 | model_type = run.data.tags.get("model_type", "classic_ml") 80 | if model_type == "auto_ml": 81 | return render_template("tracking_automl_detail.html", run=run, code_snippet=code_snippet) 82 | else: 83 | return render_template("tracking_detail.html", run=run, code_snippet=code_snippet) 84 | 85 | @tracking_bp.route('/download/') 86 | def download_model(run_id): 87 | try: 88 | path = client.download_artifacts(run_id, "model/model.pkl") 89 | return send_file(path, as_attachment=True) 90 | except Exception as e: 91 | return str(e), 404 92 | 93 | @tracking_bp.route('/delete/', methods=['POST']) 94 | def delete_run(run_id): 95 | MLRUNS_PATH = os.path.join(os.path.dirname(__file__), '../../mlruns/0') 96 | folder_path = os.path.join(MLRUNS_PATH, run_id) 97 | try: 98 | client.delete_run(run_id) 99 | if os.path.exists(folder_path) and os.path.isdir(folder_path): 100 | shutil.rmtree(folder_path) 101 | return redirect(url_for('tracking.index')) 102 | except Exception as e: 103 | return str(e), 404 104 | -------------------------------------------------------------------------------- /web_service/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %} {% endblock %} 7 | {% block head %} {% endblock %} 8 | 9 | 10 | 11 | 70 | 71 | 72 | {% if not is_home %} 73 | 109 | {% endif %} 110 | 111 | {% block body %} {% endblock %} 112 | 113 | 114 | 115 | 116 | 122 | 123 | -------------------------------------------------------------------------------- /web_service/app/templates/dataset_manager.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Datasets{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 |

9 | Datasets 10 |

11 |
12 | 13 | {% if not datasets %} 14 |
15 | 16 |
No datasets available. Upload your first dataset below.
17 |
18 | {% else %} 19 |
20 |
21 |
Uploaded Datasets
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for dataset in datasets %} 37 | 38 | 39 | 40 | 41 | 50 | 68 | 69 | {% endfor %} 70 | 71 |
#IDDataset NameProblem TypeProcess StatusActions
{{ dataset.id }}{{ dataset.file_name }}{{ dataset.problem_type|capitalize }} 42 | {% if dataset.process_status %} 43 | 45 | {% else %} 46 | 48 | {% endif %} 49 | 51 |
52 | 54 | Process 55 | 56 |
58 | 61 |
62 | 64 | Download 65 | 66 |
67 |
72 |
73 |
74 |
75 | {% endif %} 76 | 77 |
78 |
79 |
Upload New Dataset
80 |
81 |
82 |
83 |
84 | 85 | 86 |
Only .csv files are supported
87 |
88 |
89 | 90 | 95 |
96 | 99 |
100 |
101 |
102 | 103 | {% with messages = get_flashed_messages() %} 104 | {% if messages %} 105 |
106 | {% for message in messages %} 107 |
108 | 109 |
{{ message }}
110 |
111 | {% endfor %} 112 |
113 | {% endif %} 114 | {% endwith %} 115 |
116 | {% endblock %} -------------------------------------------------------------------------------- /web_service/app/templates/image_prediction_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Prediction result{% endblock %} 4 | 5 | {% block head %} 6 | 27 | {% endblock %} 28 | 29 | {% block body %} 30 |
31 |

Prediction result

32 | 33 |
34 |

Input Image

35 |
36 | Prediction result 37 |
38 |
39 | 40 |
41 | Back 42 |
43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /web_service/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | InteractiveAI 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | 10 | 282 | {% endblock %} 283 | 284 | {% block body %} 285 |
286 |
287 |

InteractiveAI Platform

288 |

No-code Machine Learning Solutions

289 |
290 |
291 | 292 | 345 | {% endblock %} -------------------------------------------------------------------------------- /web_service/app/templates/sound_prediction_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}sound Prediction Result{% endblock %} 4 | 5 | {% block head %} 6 | 39 | {% endblock %} 40 | 41 | {% block body %} 42 |
43 |

Sound Analysis Result

44 | 45 |
46 |

sound Analysis

47 |
48 | {% if spectrogram_url %} 49 | sound spectrogram 52 | {% endif %} 53 |
54 |
55 | 56 | 61 |
62 | {% endblock %} -------------------------------------------------------------------------------- /web_service/app/templates/structure.txt: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block head %} 9 | 10 | {% endblock %} 11 | 12 | 13 | {% block body %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /web_service/app/templates/table_processing.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | Table Processing 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 19 | {% endblock %} 20 | 21 | 22 | {% block body %} 23 |

Содержимое вашего CSV файла

24 | {% for table in tables %} 25 | {{ table|safe }} 26 | {% endfor %} 27 |
28 | Back to Dataset 29 | {% endblock %} 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web_service/app/templates/tracking_automl_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}AutoML Tracking Detail{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 12 | {% endblock %} 13 | {% block body %} 14 |
15 |
16 |
17 | AutoML Tracking Detail - {{ run.info.run_id[:8] }} 18 |
19 |
20 |
21 |
General Information
22 |
23 |
Task Type: {{ run.data.params.task_type }}
24 |
Scoring Metric: {{ run.data.params.scoring }}
25 |
26 |
27 |
Model Name: {{ run.data.params.model_name }}
28 |
Dataset: {{ run.data.params.dataset }}
29 |
30 |
31 |
Score: {{ "%.2f"|format(run.data.metrics.score) }}
32 |
Training Time: {{ "%.2f"|format(run.data.metrics.training_time) }}s
33 |
34 |
35 |
36 |
Parameters
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% for key, value in run.data.params.items() %} 46 | 47 | 48 | 49 | 50 | {% endfor %} 51 | 52 |
ParameterValue
{{ key }}{{ value }}
53 |
54 |
55 |
Metrics
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% for key, value in run.data.metrics.items() %} 65 | 66 | 67 | 68 | 69 | {% endfor %} 70 | 71 |
MetricValue
{{ key }}{{ value }}
72 |
73 |
74 |
Inference Example
75 |
76 | 79 |
{{ code_snippet }}
80 |
81 |
82 | 90 |
91 |
92 |
93 | 105 | {% endblock %} 106 | -------------------------------------------------------------------------------- /web_service/app/templates/tracking_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Tracking Detail{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 12 | {% endblock %} 13 | {% block body %} 14 |
15 |
16 |
17 | Tracking Detail - {{ run.info.run_id[:8] }} 18 |
19 |
20 |
21 |
General Information
22 |
23 |
Task Type: {{ run.data.params.task_type }}
24 |
Scoring Metric: {{ run.data.params.scoring }}
25 |
26 |
27 |
Model Name: {{ run.data.params.model_name }}
28 |
Dataset: {{ run.data.params.dataset }}
29 |
30 |
31 |
Score: {{ "%.2f"|format(run.data.metrics.score) }}
32 |
Training Time: {{ "%.2f"|format(run.data.metrics.training_time) }}s
33 |
34 |
35 |
36 |
Parameters
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% for key, value in run.data.params.items() %} 46 | 47 | 48 | 49 | 50 | {% endfor %} 51 | 52 |
ParameterValue
{{ key }}{{ value }}
53 |
54 |
55 |
Metrics
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% for key, value in run.data.metrics.items() %} 65 | 66 | 67 | 68 | 69 | {% endfor %} 70 | 71 |
MetricValue
{{ key }}{{ value }}
72 |
73 |
74 |
Inference Example
75 |
76 | 79 |
{{ code_snippet }}
80 |
81 |
82 | 90 |
91 |
92 |
93 | 105 | {% endblock %} 106 | -------------------------------------------------------------------------------- /web_service/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.15.1 2 | annotated-types==0.7.0 3 | anyio==4.8.0 4 | attrs==25.3.0 5 | beautifulsoup4==4.13.3 6 | blinker==1.9.0 7 | cachetools==5.5.2 8 | certifi==2025.1.31 9 | charset-normalizer==3.4.1 10 | click==8.1.8 11 | cloudpickle==3.1.1 12 | contourpy==1.3.1 13 | cycler==0.12.1 14 | databricks-sdk==0.46.0 15 | datasphere==0.10.0 16 | Deprecated==1.2.18 17 | docker==7.1.0 18 | envzy==0.3.1 19 | fastapi==0.115.11 20 | Flask==3.1.0 21 | Flask-SQLAlchemy==3.1.1 22 | fonttools==4.56.0 23 | gitdb==4.0.12 24 | GitPython==3.1.44 25 | google-auth==2.38.0 26 | googleapis-common-protos==1.69.1 27 | graphene==3.4.3 28 | graphql-core==3.2.6 29 | graphql-relay==3.2.0 30 | grpcio==1.71.0 31 | gunicorn==23.0.0 32 | h11==0.14.0 33 | idna==3.10 34 | importlib-metadata==6.11.0 35 | itsdangerous==2.2.0 36 | Jinja2==3.1.5 37 | joblib==1.4.2 38 | kiwisolver==1.4.8 39 | mailbits==0.2.2 40 | Mako==1.3.9 41 | Markdown==3.7 42 | MarkupSafe==3.0.2 43 | matplotlib==3.10.1 44 | mlflow==2.21.0 45 | mlflow-skinny==2.21.0 46 | numpy==2.2.2 47 | opentelemetry-api==1.31.0 48 | opentelemetry-sdk==1.31.0 49 | opentelemetry-semantic-conventions==0.52b0 50 | packaging==23.2 51 | pandas==2.2.3 52 | pillow==11.1.0 53 | protobuf==5.29.3 54 | pyarrow==19.0.1 55 | pyasn1==0.6.1 56 | pyasn1_modules==0.4.1 57 | pydantic==2.10.6 58 | pydantic_core==2.27.2 59 | pyparsing==3.2.1 60 | pypi-simple==1.6.1 61 | python-dateutil==2.9.0.post0 62 | pytz==2024.2 63 | PyYAML==6.0.2 64 | requests==2.32.3 65 | rsa==4.9 66 | scikit-learn==1.6.1 67 | scipy==1.15.1 68 | six==1.17.0 69 | smmap==5.0.2 70 | sniffio==1.3.1 71 | soupsieve==2.6 72 | SQLAlchemy==2.0.37 73 | sqlparse==0.5.3 74 | starlette==0.46.1 75 | tabulate==0.9.0 76 | threadpoolctl==3.5.0 77 | typing_extensions==4.12.2 78 | tzdata==2025.1 79 | urllib3==2.3.0 80 | uvicorn==0.34.0 81 | Werkzeug==3.1.3 82 | wrapt==1.17.2 83 | zipp==3.21.0 84 | python-dotenv==1.1.0 -------------------------------------------------------------------------------- /web_service/run.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run(debug=True, port = "5001") -------------------------------------------------------------------------------- /web_service/utils/image_classification/classification_neural_network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # -- Данный код предназначен для загрузки модели в Yandex DataSphere, и не ориентирован для дальнейших экспериментов -- 5 | 6 | 7 | """ 8 | 9 | ## Выбор модели для задач классификации 10 | 11 | Для задач классификации существует несколько способов выбора модели: 12 | 13 | 1. **Обучить маленькую модель с нуля**: Выбор легковесной архитектуры модели и полное обучение её на нашем наборе данных. 14 | 2. **Fine-Tuning предобученной модели**: Используем модель, которая была предобучена авторами на другом наборе данных, и только последние слои переобучается на нашем конкретном наборе данных. 15 | 3. **Использовать предобученные веса напрямую**: В этом методе используется предобученная модель без дополнительного обучения. 16 | 17 | Лучшим вариантом является Fine-Tuning, поэтому в качестве бейзлайна возьмем маленькую (5.3M) EfficientNet B0 с претрейновыми параметрами 18 | 19 | [Оригинальная статья](https://arxiv.org/abs/1905.11946) 20 | """ 21 | 22 | import json 23 | import os 24 | import sys 25 | import zipfile 26 | 27 | import torch 28 | import torch.nn as nn 29 | import torch.optim as optim 30 | from PIL import Image 31 | from torch.utils.data import DataLoader, Dataset 32 | from torchvision import datasets, transforms 33 | from torchvision.models import EfficientNet_B0_Weights, efficientnet_b0 34 | 35 | 36 | if __name__ == "__main__": 37 | print("Starting") 38 | 39 | # Загрузка гиперпараметров из JSON (создайте папку classification, если её еще нет в корне диска Google Drive) 40 | with open("hyperparams.json", "r") as f: 41 | hyperparams = json.load(f) 42 | 43 | # Загрузка названий классов из JSON 44 | with open("class_to_idx.json", "r") as f: 45 | class_to_idx = json.load(f) 46 | 47 | # Создание обратного словаря idx_to_class 48 | idx_to_class = {v: k for k, v in class_to_idx.items()} 49 | 50 | """ 51 | **Структура** classification_dataset.zip*: (внутри папка с таким же названием) 52 | 53 | ``` 54 | classification_dataset/ 55 | ├── train/ 56 | │ ├── class_name1/ 57 | │ └── class_name2/ 58 | │ ... 59 | ``` 60 | """ 61 | local_zip = "classification_dataset.zip" 62 | zip_ref = zipfile.ZipFile(local_zip, "r") 63 | zip_ref.extractall() 64 | zip_ref.close() 65 | 66 | # Класс для аугментации данных 67 | class AugmentedDataset(Dataset): 68 | def __init__(self, original_folder, target_size=50, transform=None): 69 | self.transform = transform 70 | self.samples = [] 71 | self.class_to_idx = class_to_idx 72 | 73 | # Проверяем, что все классы из JSON есть в папке 74 | class_names = os.listdir(original_folder) 75 | missing_classes = [ 76 | cls for cls in class_to_idx.keys() if cls not in class_names 77 | ] 78 | if missing_classes: 79 | raise ValueError( 80 | f"Классы {missing_classes} из class_to_idx.json отсутствуют в папке {original_folder}" 81 | ) 82 | 83 | # Собираем пути к изображениям для каждого класса 84 | for class_name in class_names: # Используем отсортированные имена классов 85 | class_path = os.path.join(original_folder, class_name) 86 | images = [ 87 | os.path.join(class_path, img) for img in os.listdir(class_path) 88 | ] 89 | # Повторяем изображения до достижения целевого размера 90 | for i in range(target_size): 91 | self.samples.append((images[i % len(images)], class_name)) 92 | 93 | # Возвращает общее количество элементов в наборе данных 94 | def __len__(self): 95 | return len(self.samples) 96 | 97 | # Возвращает одно изображение и его метку по индексу 98 | def __getitem__(self, idx): 99 | img_path, class_name = self.samples[idx] 100 | image = Image.open(img_path).convert("RGB") 101 | 102 | # Применяем аугментации 103 | if self.transform: 104 | image = self.transform(image) 105 | 106 | # Преобразуем метку класса в числовой формат 107 | label = self.class_to_idx[class_name] # Преобразуем имя класса в индекс 108 | return image, label # Оставляем числовую метку для обучения 109 | 110 | # Трансформы с аугментациями для тренировочных данных 111 | train_transform = transforms.Compose( 112 | [ 113 | transforms.Resize((224, 224)), 114 | transforms.RandomHorizontalFlip(), 115 | transforms.RandomRotation(15), 116 | transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), 117 | transforms.ToTensor(), 118 | transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), 119 | ] 120 | ) 121 | 122 | # Создание аугментированных датасетов 123 | train_dataset = AugmentedDataset( 124 | original_folder="classification_dataset/train", 125 | target_size=50, 126 | transform=train_transform, 127 | ) 128 | 129 | # DataLoader 130 | train_loader = DataLoader( 131 | train_dataset, batch_size=hyperparams["batch_size"], shuffle=True 132 | ) 133 | 134 | # Проверим классы 135 | print(train_dataset.class_to_idx) 136 | 137 | # Загрузка модели EfficientNet 138 | model = efficientnet_b0(EfficientNet_B0_Weights.DEFAULT) 139 | 140 | # Заморозка всех слоев, кроме последнего 141 | for param in model.parameters(): 142 | param.requires_grad = False 143 | 144 | # Замена финального классификатора 145 | num_features = model.classifier[1].in_features 146 | num_classes = len(class_to_idx) # Количество классов из JSON 147 | model.classifier = nn.Sequential( 148 | nn.Linear(num_features, 128), 149 | nn.ReLU(), 150 | nn.Linear(128, num_classes), # Многоклассовая классификация 151 | # На выходе оставляем сырые логиты, так как nn.CrossEntropyLoss() их ожидает на вход 152 | ) 153 | 154 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 155 | model.to(device) 156 | 157 | criterion = nn.CrossEntropyLoss() # Для многоклассовой классификации 158 | optimizer = torch.optim.Adam( 159 | model.parameters(), 160 | lr=hyperparams["learning_rate"], 161 | weight_decay=hyperparams["weight_decay"], 162 | ) 163 | 164 | # Обучение модели 165 | print("Validation on the Train Dataset!") 166 | for epoch in range(hyperparams["num_epochs"]): 167 | model.train() 168 | train_loss = 0.0 169 | 170 | for images, labels in train_loader: 171 | images, labels = images.to(device), labels.to(device) 172 | 173 | optimizer.zero_grad() 174 | outputs = model(images) 175 | loss = criterion(outputs, labels) 176 | loss.backward() 177 | optimizer.step() 178 | 179 | train_loss += loss.item() * images.size(0) 180 | 181 | # Валидация (НА TRAIN ДАТАСЕТЕ!) 182 | model.eval() 183 | correct = 0 184 | total = 0 185 | 186 | with torch.no_grad(): 187 | for images, labels in train_loader: 188 | images, labels = images.to(device), labels.to(device) 189 | 190 | outputs = model(images) 191 | loss = criterion(outputs, labels) 192 | 193 | _, predicted = torch.max( 194 | outputs.data, 1 195 | ) # Получаем индекс максимального значения 196 | correct += (predicted == labels).sum().item() 197 | total += labels.size(0) 198 | 199 | # Вывод статистики 200 | train_loss = train_loss / len(train_loader.dataset) 201 | accuracy = correct / total 202 | 203 | print(f"Epoch {epoch+1}/{hyperparams['num_epochs']}") 204 | print(f"Train Loss: {train_loss:.4f}, Accuracy: {accuracy:.4f}") 205 | 206 | # Сохранение модели 207 | torch.save(model, "trained_model_classification.pt") 208 | 209 | print("Модель сохранена") 210 | sys.exit(0) 211 | -------------------------------------------------------------------------------- /web_service/utils/image_classification/config.yaml: -------------------------------------------------------------------------------- 1 | name: classification-neural-network 2 | desc: "Training a classification model in DataSphere" 3 | 4 | # Параметры точки входа для запуска вычислений 5 | cmd: > 6 | python3 classification_neural_network.py 7 | 8 | # Файлы с входными данными (Исполняемый скрипт должен отсутствовать!) 9 | inputs: 10 | - classification_dataset.zip 11 | - hyperparams.json 12 | - class_to_idx.json 13 | 14 | # Файлы с результатами 15 | outputs: 16 | - trained_model_classification.pt 17 | 18 | # Параметры окружения 19 | env: 20 | python: 21 | type: manual 22 | version: 3.11 23 | pip: 24 | index-url: https://pypi.org/simple 25 | trusted-hosts: 26 | - download.pytorch.org 27 | extra-index-urls: 28 | - https://download.pytorch.org/whl/cu121 29 | requirements-file: requirements.txt 30 | local-paths: 31 | - classification_neural_network.py 32 | 33 | # Флаг, дающий доступ к хранилищу проекта для чтения/записи дополнительных данных 34 | flags: 35 | - attach-project-disk 36 | 37 | # Выбор типов инстансов для вычислений (Расположены по приоритетности) 38 | cloud-instance-types: 39 | - g1.1 40 | # (Если имеется доступ) 41 | # - gt4.1 42 | # - g2.1 43 | 44 | -------------------------------------------------------------------------------- /web_service/utils/image_classification/inference.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | import matplotlib.pyplot as plt 7 | import torch 8 | import torch.nn.functional as F 9 | from PIL import Image 10 | from torchvision import transforms 11 | 12 | 13 | def load_model_and_classes(model_path, class_mapping_path): 14 | """ 15 | Загрузка модели и Json-а классов. 16 | 17 | Args: 18 | model_path: Путь к файлу модели (.pt) 19 | class_mapping_path: Путь к JSON файлу с классами 20 | 21 | Returns: 22 | Tuple containing: 23 | - PyTorch model 24 | - Dictionary mapping class indices to class names 25 | - Device (cuda/cpu) 26 | """ 27 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 28 | model = torch.load(model_path, map_location=device, weights_only=False) 29 | model.eval() 30 | 31 | with open(class_mapping_path, "r") as f: 32 | class_to_idx = json.load(f) 33 | idx_to_class = {v: k for k, v in class_to_idx.items()} 34 | 35 | return model, idx_to_class, device 36 | 37 | 38 | def prepare_image(image_path): 39 | """ 40 | Подготовка изображения для подачи в модель. 41 | 42 | Args: 43 | image_path (str): Путь к файлу изображения 44 | 45 | Returns: 46 | tuple: Кортеж из двух элементов 47 | - image_tensor (torch.Tensor): Предобработанный тензор изображения 48 | - image (PIL.Image): Исходное изображение в формате PIL 49 | 50 | """ 51 | transform = transforms.Compose( 52 | [ 53 | transforms.Resize((224, 224)), 54 | transforms.ToTensor(), 55 | transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), 56 | ] 57 | ) 58 | 59 | image = Image.open(image_path).convert("RGB") 60 | image_tensor = transform(image) 61 | image_tensor = image_tensor.unsqueeze(0) 62 | return image_tensor, image 63 | 64 | 65 | def predict_image(model, image_tensor, device): 66 | """ 67 | Получение предсказаний модели для изображения. 68 | 69 | Args: 70 | model (torch.nn.Module): PyTorch модель 71 | image_tensor (torch.Tensor): Предобработанный тензор изображения 72 | device (torch.device): Устройство для вычислений (CPU/GPU) 73 | 74 | Returns: 75 | numpy.ndarray: Массив вероятностей принадлежности к каждому классу 76 | 77 | Notes: 78 | - Модель должна быть в режиме eval() 79 | - Тензор изображения должен иметь размерность [1, C, H, W] 80 | - Возвращаемые вероятности нормализованы (сумма = 1) 81 | """ 82 | with torch.no_grad(): 83 | image_tensor = image_tensor.to(device) 84 | outputs = model(image_tensor) 85 | probabilities = F.softmax(outputs, dim=1) 86 | 87 | probs = probabilities[0].cpu().numpy() 88 | return probs 89 | 90 | 91 | def visualize_predictions( 92 | image, probs, idx_to_class, image_path, output_dir 93 | ): 94 | """ 95 | Визуализация изображения и предсказаний модели с сохранением результата. 96 | 97 | Args: 98 | image (PIL.Image): Исходное изображение 99 | probs (numpy.ndarray): Массив вероятностей для каждого класса 100 | idx_to_class (dict): Словарь для преобразования индекса в название класса 101 | image_path (str): Путь к исходному изображению 102 | output_dir (str): Директория для сохранения результатов 103 | 104 | Returns: 105 | str: Путь к сохраненному файлу визуализации 106 | 107 | Notes: 108 | - Создает график с двумя частями: исходное изображение и график вероятностей 109 | - Сохраняет результат в формате PNG 110 | - Имя выходного файла содержит временную метку 111 | """ 112 | # Создаем директорию, если её нет 113 | os.makedirs(output_dir, exist_ok=True) 114 | 115 | # Создаем график 116 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) 117 | 118 | # Отображаем исходное изображение 119 | ax1.imshow(image) 120 | ax1.axis("off") 121 | ax1.set_title("Input Image") 122 | 123 | # Создаем столбчатую диаграмму 124 | classes = list(idx_to_class.values()) 125 | ax2.bar(classes, probs * 100) 126 | ax2.set_ylabel("Probability (%)") 127 | ax2.set_title("Class Probabilities") 128 | plt.xticks(rotation=45) 129 | 130 | # Добавляем подписи значений 131 | for i, v in enumerate(probs): 132 | ax2.text( 133 | i, 134 | v * 100, 135 | f"{v * 100:.1f}%", 136 | horizontalalignment="center", 137 | verticalalignment="bottom", 138 | ) 139 | 140 | # Настраиваем layout 141 | plt.tight_layout() 142 | 143 | # Генерируем имя файла с той же временной меткой 144 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 145 | base_image_name = os.path.splitext(os.path.basename(image_path))[0] 146 | plot_filename = f"prediction_{base_image_name}_{timestamp}_image.png" 147 | plot_path = os.path.join(output_dir, plot_filename) 148 | 149 | # Сохраняем график 150 | plt.savefig(plot_path, bbox_inches="tight", dpi=300) 151 | 152 | # Откладка 153 | # plt.show() 154 | 155 | return plot_path 156 | 157 | 158 | def save_predictions_to_json( 159 | image_path, probabilities, idx_to_class, output_dir 160 | ): 161 | """ 162 | Сохранение результатов предсказаний в JSON файл. 163 | 164 | Args: 165 | image_path (str): Путь к исходному изображению 166 | probabilities (numpy.ndarray): Массив вероятностей для каждого класса 167 | idx_to_class (dict): Словарь для преобразования индекса в название класса 168 | output_dir (str): Директория для сохранения результатов 169 | 170 | Returns: 171 | str: Путь к сохраненному JSON файлу 172 | 173 | Notes: 174 | - Создает JSON с метаданными (время, пользователь, путь) и предсказаниями 175 | - Вероятности не ограничены в точности 176 | - Имя выходного файла содержит временную метку 177 | """ 178 | # Создаем директорию для предсказаний 179 | os.makedirs(output_dir, exist_ok=True) 180 | 181 | # Формируем результаты предсказаний 182 | predictions = { 183 | idx_to_class[idx]: float(prob) * 100 for idx, prob in enumerate(probabilities) 184 | } 185 | 186 | # Создаем словарь с метаданными и результатами 187 | result = { 188 | "metadata": { 189 | "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 190 | "user": os.getenv("USER", "unknown"), 191 | "image_path": os.path.abspath(image_path), 192 | }, 193 | "predictions": predictions, 194 | } 195 | 196 | # Генерируем имя файла на основе времени 197 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 198 | base_image_name = os.path.splitext(os.path.basename(image_path))[0] 199 | json_filename = f"prediction_{base_image_name}_{timestamp}.json" 200 | json_path = os.path.join(output_dir, json_filename) 201 | 202 | # Сохраняем результаты в JSON 203 | with open(json_path, "w", encoding="utf-8") as f: 204 | json.dump(result, f, indent=2, ensure_ascii=False) 205 | 206 | return json_path 207 | 208 | 209 | def main(): 210 | # Проверка кол-ва введенных аргументов 211 | if len(sys.argv) != 2: 212 | print("Используйте: python inference.py ") 213 | sys.exit(1) 214 | # Константы 215 | MODEL_PATH = "utils/image_classification/trained_model_classification.pt" 216 | CLASS_MAPPING_PATH = "utils/image_classification/class_to_idx.json" 217 | IMAGE_PATH = sys.argv[1] 218 | OUTPUT_DIR = "predictions/images" 219 | 220 | 221 | # Проверка существования файлов 222 | if not os.path.exists(MODEL_PATH): 223 | print(f"Ошибка: Файл модели не найден: {MODEL_PATH}") 224 | sys.exit(1) 225 | if not os.path.exists(CLASS_MAPPING_PATH): 226 | print(f"Ошибка: Файл маппинга классов не найден: {CLASS_MAPPING_PATH}") 227 | sys.exit(1) 228 | if not os.path.exists(IMAGE_PATH): 229 | print(f"Ошибка: Изображение не найдено: {IMAGE_PATH}") 230 | sys.exit(1) 231 | 232 | try: 233 | # Загрузка модели и классов 234 | model, idx_to_class, device = load_model_and_classes( 235 | MODEL_PATH, CLASS_MAPPING_PATH 236 | ) 237 | 238 | # Подготовка изображения 239 | image_tensor, original_image = prepare_image(IMAGE_PATH) 240 | 241 | # Получение предсказаний 242 | probabilities = predict_image(model, image_tensor, device) 243 | 244 | # Вывод результатов в консоль 245 | print("\nPredictions:") 246 | for idx, prob in enumerate(probabilities): 247 | class_name = idx_to_class[idx] 248 | print(f"{class_name}: {prob * 100:.1f}%") 249 | 250 | # Визуализация и сохранение графика 251 | plot_path = visualize_predictions( 252 | original_image, probabilities, idx_to_class, IMAGE_PATH, output_dir=OUTPUT_DIR 253 | ) 254 | print(f"Plot saved to: {plot_path}") 255 | 256 | # Сохранение результатов в JSON 257 | json_path = save_predictions_to_json(IMAGE_PATH, probabilities, idx_to_class, output_dir=OUTPUT_DIR) 258 | print(f"\nPredictions saved to: {json_path}") 259 | 260 | except KeyboardInterrupt: 261 | print("\nПрограмма прервана пользователем") 262 | sys.exit(0) 263 | 264 | except Exception as e: 265 | print(f"Ошибка: {str(e)}") 266 | sys.exit(1) 267 | 268 | 269 | if __name__ == "__main__": 270 | main() 271 | -------------------------------------------------------------------------------- /web_service/utils/image_classification/requirements.txt: -------------------------------------------------------------------------------- 1 | torch==2.5.1+cu121 2 | torchvision==0.20.1+cu121 3 | Pillow==11.1.0 4 | numpy==1.26.4 5 | Brotli==1.0.9 6 | PySocks==1.7.1 7 | certifi==2024.12.14 8 | charset-normalizer==3.3.2 9 | defusedxml==0.7.1 10 | idna==3.7 11 | requests==2.32.3 12 | typing-extensions==4.12.2 13 | urllib3==2.3.0 14 | -------------------------------------------------------------------------------- /web_service/utils/sound_classification/config.yaml: -------------------------------------------------------------------------------- 1 | name: classification-sound-neural-network 2 | desc: "Training a classification sound model in DataSphere" 3 | 4 | # Параметры точки входа для запуска вычислений 5 | cmd: > 6 | python3 sound_classification_datasphere.py 7 | 8 | # Файлы с входными данными (Исполняемый скрипт должен отсутствовать!) 9 | inputs: 10 | - sound_dataset.zip 11 | - hyperparams.json 12 | - class_to_idx.json 13 | 14 | # Файлы с результатами 15 | outputs: 16 | - trained_model_sound_classification.pt 17 | 18 | # Параметры окружения 19 | env: 20 | python: 21 | type: manual 22 | version: 3.11 23 | pip: 24 | index-url: https://pypi.org/simple 25 | trusted-hosts: 26 | - download.pytorch.org 27 | extra-index-urls: 28 | - https://download.pytorch.org/whl/cu121 29 | requirements-file: requirements.txt 30 | local-paths: 31 | - sound_classification_datasphere.py 32 | 33 | # Флаг, дающий доступ к хранилищу проекта для чтения/записи дополнительных данных 34 | flags: 35 | - attach-project-disk 36 | 37 | # Выбор типов инстансов для вычислений (Расположены по приоритетности) 38 | cloud-instance-types: 39 | - g1.1 40 | # (Если имеется доступ) 41 | # - gt4.1 42 | # - g2.1 43 | 44 | -------------------------------------------------------------------------------- /web_service/utils/sound_classification/inference.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | import torch 7 | import torch.nn as nn 8 | import torch.nn.functional as F 9 | import torchaudio 10 | from torchaudio.transforms import Resample 11 | import numpy as np 12 | import matplotlib.pyplot as plt 13 | 14 | from panns_inference import AudioTagging 15 | 16 | import warnings 17 | warnings.simplefilter(action="ignore", category=FutureWarning) 18 | 19 | # --- Константы из скрипта обучения --- 20 | SR = 16000 # Целевая частота дискретизации 21 | DURATION = 5 # Длительность в секундах 22 | SAMPLES = SR * DURATION # Количество сэмплов 23 | PANNS_EMBEDDING_DIM = 2048 # Размерность эмбеддинга PANNs (Cnn14) 24 | 25 | # --- Вспомогательная функция для подготовки аудио --- 26 | def prepare_audio_for_inference(wav, sr, target_sr=SR, target_samples=SAMPLES): 27 | if wav.ndim > 1 and wav.shape[0] > 1: 28 | wav = wav.mean(dim=0, keepdim=True) 29 | elif wav.ndim == 1: 30 | wav = wav.unsqueeze(0) 31 | 32 | if sr != target_sr: 33 | wav = Resample(sr, target_sr)(wav) 34 | 35 | current_samples = wav.size(1) 36 | if current_samples > target_samples: 37 | wav = wav[:, :target_samples] 38 | elif current_samples < target_samples: 39 | pad_size = target_samples - current_samples 40 | wav = nn.functional.pad(wav, (0, pad_size)) 41 | return wav 42 | 43 | def load_model_and_classes_sound(mlp_model_path, class_mapping_path, device): 44 | """ 45 | Загрузка MLP-модели, Json-а классов и инициализация PANNs. 46 | 47 | Args: 48 | mlp_model_path: Путь к файлу с весами MLP-модели (.pt) 49 | class_mapping_path: Путь к JSON файлу с классами 50 | device: Устройство для вычислений 51 | 52 | Returns: 53 | Tuple containing: 54 | - mlp_model (torch.nn.Module): MLP модель 55 | - audio_tagger (AudioTagging): Инициализированная модель PANNs 56 | - idx_to_class (dict): Словарь отображения индексов на имена классов 57 | """ 58 | # Инициализация PANNs AudioTagger для извлечения эмбеддингов 59 | print("Initializing PANNs AudioTagger...") 60 | audio_tagger = AudioTagging(checkpoint_path=None, device=str(device)) 61 | print("PANNs AudioTagger initialized.") 62 | 63 | # Загрузка отображения классов 64 | with open(class_mapping_path, "r", encoding="utf-8") as f: 65 | class_to_idx = json.load(f) 66 | idx_to_class = {v: k for k, v in class_to_idx.items()} 67 | num_classes = len(idx_to_class) 68 | 69 | # Определение архитектуры MLP-модели 70 | mlp_model = torch.load(mlp_model_path, map_location=device) 71 | mlp_model.to(device) 72 | mlp_model.eval() 73 | 74 | return mlp_model, audio_tagger, idx_to_class 75 | 76 | def extract_audio_embedding(audio_path, audio_tagger_model, device): 77 | """ 78 | Подготовка аудиофайла и извлечение PANNs эмбеддинга. 79 | 80 | Args: 81 | audio_path (str): Путь к аудиофайлу 82 | audio_tagger_model (AudioTagging): Инициализированная модель PANNs 83 | device (torch.device): Устройство для вычислений 84 | 85 | Returns: 86 | torch.Tensor: Эмбеддинг аудио (формат [1, embedding_dim]) 87 | torch.Tensor: Предобработанная волновая форма аудио (для возможной визуализации) 88 | """ 89 | try: 90 | wav, sr = torchaudio.load(audio_path) 91 | except Exception as e: 92 | raise ValueError(f"Error loading audio file {audio_path}: {e}") 93 | 94 | wav_prepared = prepare_audio_for_inference(wav, sr) 95 | wav_prepared_for_panns = wav_prepared.to(device) 96 | 97 | wav_np = wav_prepared_for_panns.squeeze(0).cpu().numpy() 98 | 99 | with torch.no_grad(): 100 | _, embedding = audio_tagger_model.inference(wav_np[None, :]) 101 | 102 | embedding_tensor = torch.from_numpy(embedding).to(device) 103 | 104 | return embedding_tensor, wav_prepared 105 | 106 | def predict_sound(mlp_model, embedding_tensor, device): 107 | """ 108 | Получение предсказаний MLP-модели для аудио эмбеддинга. 109 | 110 | Args: 111 | mlp_model (torch.nn.Module): MLP модель 112 | embedding_tensor (torch.Tensor): Эмбеддинг аудио [1, embedding_dim] 113 | device (torch.device): Устройство для вычислений 114 | 115 | Returns: 116 | numpy.ndarray: Массив вероятностей принадлежности к каждому классу 117 | """ 118 | with torch.no_grad(): 119 | embedding_tensor = embedding_tensor.to(device) 120 | outputs = mlp_model(embedding_tensor) 121 | probabilities = F.softmax(outputs, dim=1) 122 | 123 | probs_np = probabilities[0].cpu().numpy() 124 | return probs_np 125 | 126 | def visualize_sound_predictions(audio_waveform, probs, idx_to_class, audio_path, output_dir): 127 | """ 128 | Визуализация спектрограммы аудио и предсказаний модели с сохранением результата. 129 | 130 | Args: 131 | audio_waveform (torch.Tensor): Предобработанная волновая форма [1, SAMPLES] 132 | probs (numpy.ndarray): Массив вероятностей для каждого класса 133 | idx_to_class (dict): Словарь для преобразования индекса в название класса 134 | audio_path (str): Путь к исходному аудиофайлу 135 | output_dir (str): Директория для сохранения результатов 136 | 137 | Returns: 138 | str: Путь к сохраненному файлу визуализации 139 | """ 140 | os.makedirs(output_dir, exist_ok=True) 141 | 142 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5)) 143 | 144 | 145 | spec_transform = torchaudio.transforms.MelSpectrogram( 146 | sample_rate=SR, 147 | n_fft=1024, 148 | hop_length=512, 149 | n_mels=128 150 | ) 151 | mel_spectrogram = spec_transform(audio_waveform.cpu()) 152 | db_spectrogram = torchaudio.transforms.AmplitudeToDB()(mel_spectrogram) 153 | 154 | ax1.imshow(db_spectrogram.squeeze().numpy(), aspect='auto', origin='lower', cmap='viridis') 155 | ax1.set_title("Audio spectrogram") 156 | ax1.set_xlabel("Time (frames)") 157 | ax1.set_ylabel("Frequency (Mel bins)") 158 | 159 | 160 | classes = list(idx_to_class.values()) 161 | if len(classes) != len(probs): 162 | print(f"Warning: number of classes ({len(classes)}) does not match number of probabilities ({len(probs)}). The plot may be incorrect.") 163 | 164 | 165 | bar_positions = np.arange(len(classes)) 166 | ax2.bar(bar_positions, probs * 100, tick_label=classes) 167 | ax2.set_ylabel("Probability (%)") 168 | ax2.set_title("Class probabilities") 169 | plt.xticks(bar_positions, classes, rotation=45, ha="right") 170 | 171 | 172 | for i, v in enumerate(probs): 173 | ax2.text( 174 | i, 175 | v * 100 + 1, 176 | f"{v * 100:.1f}%", 177 | horizontalalignment="center", 178 | verticalalignment="bottom", 179 | ) 180 | ax2.set_ylim(0, 110) 181 | 182 | plt.tight_layout() 183 | 184 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 185 | base_audio_name = os.path.splitext(os.path.basename(audio_path))[0] 186 | plot_filename = f"prediction_{base_audio_name}_{timestamp}_sound.png" 187 | plot_path = os.path.join(output_dir, plot_filename) 188 | 189 | plt.savefig(plot_path, bbox_inches="tight", dpi=300) 190 | plt.close(fig) 191 | 192 | return plot_path 193 | 194 | 195 | def save_sound_predictions_to_json(audio_path, probabilities, idx_to_class, output_dir): 196 | """ 197 | Сохранение результатов предсказаний для аудио в JSON файл. 198 | 199 | Args: 200 | audio_path (str): Путь к исходному аудиофайлу 201 | probabilities (numpy.ndarray): Массив вероятностей для каждого класса 202 | idx_to_class (dict): Словарь для преобразования индекса в название класса 203 | output_dir (str): Директория для сохранения результатов 204 | 205 | Returns: 206 | str: Путь к сохраненному JSON файлу 207 | """ 208 | os.makedirs(output_dir, exist_ok=True) 209 | 210 | predictions = { 211 | idx_to_class[idx]: float(prob) * 100 for idx, prob in enumerate(probabilities) 212 | } 213 | 214 | result = { 215 | "metadata": { 216 | "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 217 | "user": os.getenv("USER", "unknown"), 218 | "audio_path": os.path.abspath(audio_path), 219 | }, 220 | "predictions": predictions, 221 | } 222 | 223 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 224 | base_audio_name = os.path.splitext(os.path.basename(audio_path))[0] 225 | json_filename = f"prediction_{base_audio_name}_{timestamp}_sound.json" 226 | json_path = os.path.join(output_dir, json_filename) 227 | 228 | with open(json_path, "w", encoding="utf-8") as f: 229 | json.dump(result, f, indent=2, ensure_ascii=False) 230 | 231 | return json_path 232 | 233 | 234 | def main_sound(): 235 | if len(sys.argv) != 2: 236 | print("Usage: python inference_sound.py ") 237 | sys.exit(1) 238 | 239 | MLP_MODEL_PATH = "utils/sound_classification/trained_model_sound_classification.pt" 240 | CLASS_MAPPING_PATH = "utils/sound_classification/class_to_idx.json" 241 | AUDIO_PATH = sys.argv[1] 242 | OUTPUT_DIR = "predictions/sounds" 243 | 244 | if not os.path.exists(MLP_MODEL_PATH): 245 | print(f"Error: MLP model file not found: {MLP_MODEL_PATH}") 246 | sys.exit(1) 247 | if not os.path.exists(CLASS_MAPPING_PATH): 248 | print(f"Error: Class mapping file not found: {CLASS_MAPPING_PATH}") 249 | sys.exit(1) 250 | if not os.path.exists(AUDIO_PATH): 251 | print(f"Error: Audio file not found: {AUDIO_PATH}") 252 | sys.exit(1) 253 | 254 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 255 | print(f"Using device: {device}") 256 | 257 | try: 258 | mlp_model, audio_tagger, idx_to_class = load_model_and_classes_sound( 259 | MLP_MODEL_PATH, CLASS_MAPPING_PATH, device 260 | ) 261 | 262 | audio_embedding, audio_waveform_prepared = extract_audio_embedding(AUDIO_PATH, audio_tagger, device) 263 | 264 | probabilities = predict_sound(mlp_model, audio_embedding, device) 265 | 266 | print("\nPredictions:") 267 | for idx, prob in enumerate(probabilities): 268 | class_name = idx_to_class[idx] 269 | print(f" {class_name}: {prob * 100:.1f}%") 270 | 271 | plot_path = visualize_sound_predictions( 272 | audio_waveform_prepared, 273 | probabilities, 274 | idx_to_class, 275 | AUDIO_PATH, 276 | output_dir=OUTPUT_DIR, 277 | ) 278 | print(f"Plot saved to: {plot_path}") 279 | 280 | json_path = save_sound_predictions_to_json( 281 | AUDIO_PATH, probabilities, idx_to_class, output_dir=OUTPUT_DIR 282 | ) 283 | print(f"Predictions saved to JSON: {json_path}") 284 | 285 | except KeyboardInterrupt: 286 | print("\nProgram interrupted by user.") 287 | sys.exit(0) 288 | except Exception as e: 289 | import traceback 290 | print(f"An error occurred: {str(e)}") 291 | print("Traceback:") 292 | traceback.print_exc() 293 | sys.exit(1) 294 | 295 | if __name__ == "__main__": 296 | main_sound() -------------------------------------------------------------------------------- /web_service/utils/sound_classification/requirements.txt: -------------------------------------------------------------------------------- 1 | torch==2.5.1+cu121 2 | torchaudio==2.5.1+cu121 3 | panns-inference==0.1.1 4 | Pillow==11.1.0 5 | numpy==1.26.4 6 | Brotli==1.0.9 7 | PySocks==1.7.1 8 | certifi==2024.12.14 9 | charset-normalizer==3.3.2 10 | defusedxml==0.7.1 11 | idna==3.7 12 | requests==2.32.3 13 | typing-extensions==4.12.2 14 | urllib3==2.3.0 15 | -------------------------------------------------------------------------------- /web_service/utils/sound_classification/sound_classification_datasphere.py: -------------------------------------------------------------------------------- 1 | # -- Данный код предназначен для загрузки модели в Yandex DataSphere и не ориентирован для дальнейших экспериментов -- 2 | 3 | import json 4 | import os 5 | import sys 6 | import zipfile 7 | import random 8 | import logging 9 | import contextlib 10 | import io 11 | 12 | import torch 13 | import torch.nn as nn 14 | import torch.optim as optim 15 | from torch.utils.data import DataLoader, Dataset 16 | import torchaudio 17 | from torchaudio.transforms import Resample 18 | from panns_inference import AudioTagging 19 | 20 | if __name__ == "__main__": 21 | SR = 16000 # Целевая частота дискретизации 22 | DURATION = 5 # Длительность в секундах 23 | SAMPLES = SR * DURATION # Количество сэмплов 24 | 25 | 26 | # --- Классы аугментации волновой формы --- 27 | class GaussianNoise(nn.Module): 28 | def __init__(self, snr_db=15, p=0.5): 29 | super().__init__() 30 | self.snr_db = snr_db 31 | self.p = p 32 | 33 | def forward(self, waveform): 34 | if torch.rand(1) < self.p: 35 | signal_power = waveform.norm(p=2) 36 | noise = torch.randn_like(waveform) 37 | noise_power = noise.norm(p=2) 38 | if noise_power == 0: 39 | return waveform 40 | snr = 10 ** (self.snr_db / 10) 41 | scale = (signal_power / (noise_power * snr)).sqrt() 42 | return waveform + scale * noise 43 | return waveform 44 | 45 | 46 | class RandomVolume(nn.Module): 47 | def __init__(self, min_gain_db=-6, max_gain_db=6, p=0.5): 48 | super().__init__() 49 | self.min_gain_db = min_gain_db 50 | self.max_gain_db = max_gain_db 51 | self.p = p 52 | 53 | def forward(self, waveform): 54 | if random.random() < self.p: 55 | gain_db = random.uniform(self.min_gain_db, self.max_gain_db) 56 | gain_factor = 10 ** (gain_db / 20) 57 | gain_factor = max(gain_factor, 1e-5) 58 | return waveform * gain_factor 59 | return waveform 60 | 61 | 62 | # --- Вспомогательная функция для подготовки аудио --- 63 | def prepare_audio(wav, sr, target_sr=SR, target_samples=SAMPLES): 64 | if wav.ndim > 1 and wav.shape[0] > 1: 65 | wav = wav.mean(dim=0, keepdim=True) 66 | elif wav.ndim == 1: 67 | wav = wav.unsqueeze(0) 68 | 69 | if sr != target_sr: 70 | wav = Resample(sr, target_sr)(wav) 71 | 72 | if wav.size(1) > target_samples: 73 | wav = wav[:, :target_samples] 74 | else: 75 | pad_size = target_samples - wav.size(1) 76 | wav = nn.functional.pad(wav, (0, pad_size)) 77 | return wav 78 | 79 | 80 | # --- Класс Dataset для эмбеддингов PANNs --- 81 | class PANNsEmbedDatasetDS(Dataset): 82 | def __init__(self, original_folder, class_to_idx_map, audio_tagger_model, 83 | waveform_augment_transform=None, target_size_per_class=50): 84 | self.original_folder = original_folder 85 | self.class_to_idx = class_to_idx_map 86 | self.audio_tagger = audio_tagger_model 87 | self.waveform_augment = waveform_augment_transform 88 | self.target_size_per_class = target_size_per_class 89 | self.samples = [] 90 | 91 | # Проверка на отсутствующие классы 92 | class_names_in_folder = os.listdir(original_folder) 93 | missing_classes = [ 94 | cls for cls in self.class_to_idx.keys() if cls not in class_names_in_folder 95 | ] 96 | if missing_classes: 97 | raise ValueError( 98 | f"Классы {missing_classes} из class_to_idx.json отсутствуют в папке {original_folder}" 99 | ) 100 | 101 | for class_name in class_names_in_folder: 102 | if class_name not in self.class_to_idx: 103 | print(f"Warning: Folder {class_name} not found in class_to_idx.json, skipping.") 104 | continue 105 | 106 | label = self.class_to_idx[class_name] 107 | class_path = os.path.join(original_folder, class_name) 108 | if not os.path.isdir(class_path): 109 | continue 110 | 111 | audio_files = [os.path.join(class_path, f) for f in os.listdir(class_path) 112 | if f.lower().endswith(('.wav', '.mp3', '.flac'))] 113 | 114 | if not audio_files: 115 | print(f"Warning: Audio files not found in {class_path} for class {class_name}") 116 | continue 117 | 118 | for i in range(self.target_size_per_class): 119 | file_path = audio_files[i % len(audio_files)] 120 | self.samples.append((file_path, label)) 121 | 122 | def __len__(self): 123 | return len(self.samples) 124 | 125 | def __getitem__(self, idx): 126 | file_path, label = self.samples[idx] 127 | try: 128 | wav, sr = torchaudio.load(file_path) 129 | except Exception as e: 130 | print(f"Error loading audio file {file_path}: {e}") 131 | dummy_embedding = torch.zeros(2048) 132 | return dummy_embedding, -1 133 | 134 | wav_prepared = prepare_audio(wav, sr) 135 | 136 | if self.waveform_augment: 137 | wav_prepared = self.waveform_augment(wav_prepared) 138 | 139 | wav_np = wav_prepared.squeeze(0).cpu().numpy() 140 | 141 | with torch.no_grad(): 142 | _, embedding = self.audio_tagger.inference(wav_np[None, :]) 143 | 144 | embedding = torch.from_numpy(embedding).squeeze(0) 145 | 146 | return embedding, label 147 | 148 | class suppress_stdout_stderr(object): 149 | def __init__(self): 150 | self.null_fds = [os.open(os.devnull, os.O_RDWR) for x in range(2)] 151 | self.save_fds = [os.dup(1), os.dup(2)] 152 | 153 | def __enter__(self): 154 | os.dup2(self.null_fds[0], 1) 155 | os.dup2(self.null_fds[1], 2) 156 | 157 | def __exit__(self, *_): 158 | os.dup2(self.save_fds[0], 1) 159 | os.dup2(self.save_fds[1], 2) 160 | for fd in self.null_fds + self.save_fds: 161 | os.close(fd) 162 | 163 | print("Starting sound classification training for DataSphere") 164 | DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") 165 | print(f"Using device: {DEVICE}") 166 | 167 | # Загрузка гиперпараметров 168 | with open("hyperparams.json", "r") as f: 169 | hyperparams = json.load(f) 170 | 171 | # Загрузка отображения классов на индексы 172 | with open("class_to_idx.json", "r") as f: 173 | class_to_idx = json.load(f) 174 | 175 | num_classes = len(class_to_idx) 176 | print(f"Number of classes: {num_classes}") 177 | print(f"Class mapping: {class_to_idx}") 178 | 179 | dataset_zip_name = "sound_dataset.zip" 180 | dataset_extract_folder = "sound_dataset_extracted" 181 | train_folder_path = os.path.join(dataset_extract_folder, "sound_dataset/train") 182 | 183 | if os.path.exists(dataset_extract_folder): 184 | import shutil 185 | 186 | shutil.rmtree(dataset_extract_folder) 187 | print(f"Removed existing directory: {dataset_extract_folder}") 188 | 189 | print(f"Unpacking {dataset_zip_name}...") 190 | with zipfile.ZipFile(dataset_zip_name, "r") as zip_ref: 191 | zip_ref.extractall(dataset_extract_folder) 192 | print("Unpacking completed.") 193 | 194 | if not os.path.exists(train_folder_path): 195 | potential_base_list = os.listdir(dataset_extract_folder) 196 | if not potential_base_list: 197 | raise FileNotFoundError(f"Folder {dataset_extract_folder} is empty after unpacking.") 198 | potential_base = os.path.join(dataset_extract_folder, potential_base_list[0]) 199 | if os.path.isdir(potential_base) and "train" in os.listdir(potential_base): 200 | train_folder_path = os.path.join(potential_base, "train") 201 | else: 202 | raise FileNotFoundError( 203 | f"Train folder not found at path {train_folder_path} or in expected subdirectories.") 204 | print(f"Using training data from: {train_folder_path}") 205 | 206 | logging.getLogger("torch").setLevel(logging.ERROR) 207 | logging.getLogger("torchaudio").setLevel(logging.ERROR) 208 | 209 | print("Initializing PANNs AudioTagger...") 210 | ## Delete PANNs logs 211 | with suppress_stdout_stderr(): 212 | audio_tagger = AudioTagging(checkpoint_path=None, device=str(DEVICE)) 213 | print("PANNs AudioTagger initialized.") 214 | panns_embedding_dim = 2048 215 | 216 | # Определение аугментаций волновой формы 217 | waveform_augment_transform = nn.Sequential( 218 | RandomVolume(min_gain_db=-6, max_gain_db=6, p=0.5), 219 | GaussianNoise(snr_db=30, p=0.5), 220 | ).to(DEVICE) 221 | 222 | # Создание Dataset и DataLoader 223 | print("Creating dataset...") 224 | train_dataset = PANNsEmbedDatasetDS( 225 | original_folder=train_folder_path, 226 | class_to_idx_map=class_to_idx, 227 | audio_tagger_model=audio_tagger, 228 | waveform_augment_transform=waveform_augment_transform, 229 | target_size_per_class=hyperparams.get("target_size", 50) # из hyperparams или по умолчанию 230 | ) 231 | print(f"Dataset created with {len(train_dataset)} samples.") 232 | 233 | if len(train_dataset) == 0: 234 | print("Error: Dataset is empty. Check paths and data.") 235 | sys.exit(1) 236 | 237 | train_loader = DataLoader( 238 | train_dataset, 239 | batch_size=hyperparams["batch_size"], 240 | shuffle=True 241 | ) 242 | 243 | # Определение MLP-модели 244 | mlp_model = nn.Sequential( 245 | nn.Linear(panns_embedding_dim, 512), 246 | nn.ReLU(), 247 | nn.Dropout(0.3), 248 | nn.Linear(512, 256), 249 | nn.ReLU(), 250 | nn.Dropout(0.2), 251 | nn.Linear(256, num_classes) 252 | ).to(DEVICE) 253 | 254 | # Функция потерь и оптимизатор 255 | criterion = nn.CrossEntropyLoss() 256 | optimizer = optim.Adam( 257 | mlp_model.parameters(), 258 | lr=hyperparams["learning_rate"], 259 | weight_decay=hyperparams.get("weight_decay", 0.0) 260 | ) 261 | 262 | # Цикл обучения 263 | print("Starting training...") 264 | for epoch in range(hyperparams["num_epochs"]): 265 | mlp_model.train() 266 | epoch_train_loss = 0.0 267 | 268 | for embeddings, labels in train_loader: 269 | if embeddings.nelement() == 0: 270 | print("Skipping empty batch.") 271 | continue 272 | if -1 in labels: 273 | print("Skipping batch with data loading error.") 274 | continue 275 | 276 | embeddings, labels = embeddings.to(DEVICE), labels.to(DEVICE) 277 | 278 | optimizer.zero_grad() 279 | outputs = mlp_model(embeddings) 280 | loss = criterion(outputs, labels) 281 | loss.backward() 282 | optimizer.step() 283 | 284 | epoch_train_loss += loss.item() * embeddings.size(0) 285 | 286 | avg_epoch_train_loss = epoch_train_loss / len(train_loader.dataset) 287 | 288 | # Валидация на обучающем наборе 289 | mlp_model.eval() 290 | correct = 0 291 | total = 0 292 | with torch.no_grad(): 293 | for embeddings, labels in train_loader: 294 | if embeddings.nelement() == 0 or -1 in labels: continue 295 | embeddings, labels = embeddings.to(DEVICE), labels.to(DEVICE) 296 | outputs = mlp_model(embeddings) 297 | _, predicted = torch.max(outputs.data, 1) 298 | total += labels.size(0) 299 | correct += (predicted == labels).sum().item() 300 | 301 | accuracy = correct / total if total > 0 else 0 302 | 303 | print(f"Epoch {epoch + 1}/{hyperparams['num_epochs']} - " 304 | f"Loss: {avg_epoch_train_loss:.4f}, " 305 | f"Accuracy (On TRAIN Dataset): {accuracy:.4f}") 306 | 307 | # Сохранение обученной MLP-модели 308 | model_save_path = "trained_model_sound_classification.pt" 309 | torch.save(mlp_model, model_save_path) 310 | print(f"MLP-model saved in {model_save_path}") 311 | 312 | print("Studing has been ended.") 313 | sys.exit(0) 314 | --------------------------------------------------------------------------------