# Импорт необходимых библиотек
import sys
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PyQt5 import QtWidgets, QtGui, QtCore
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.utils.class_weight import compute_class_weight
import torch
import torch.nn as nn
import torch.optim as optim

# Класс однослойной нейронной сети на PyTorch
class SingleLayerNN(nn.Module):
    def __init__(self, input_size, num_classes):
        """
        Инициализация однослойной нейронной сети.
        :param input_size: размер входного слоя
        :param num_classes: количество классов для классификации
        """
        super(SingleLayerNN, self).__init__()
        self.fc1 = nn.Linear(input_size, num_classes)
    
    def forward(self, x):
        """
        Прямое распространение сигнала.
        :param x: входные данные
        :return: выход нейронной сети
        """
        out = self.fc1(x)
        return out

# Класс многослойного персептрона на PyTorch
class MultilayerPerceptron(nn.Module):
    def __init__(self, input_size, num_classes):
        """
        Инициализация многослойного персептрона.
        :param input_size: размер входного слоя
        :param num_classes: количество классов для классификации
        """
        super(MultilayerPerceptron, self).__init__()
        self.layer1 = nn.Linear(input_size, 1024)
        self.bn1 = nn.BatchNorm1d(1024)
        self.relu1 = nn.ReLU()
        self.dropout1 = nn.Dropout(0.5)

        self.layer2 = nn.Linear(1024, 512)
        self.bn2 = nn.BatchNorm1d(512)
        self.relu2 = nn.ReLU()
        self.dropout2 = nn.Dropout(0.5)

        self.layer3 = nn.Linear(512, 256)
        self.bn3 = nn.BatchNorm1d(256)
        self.relu3 = nn.ReLU()
        self.dropout3 = nn.Dropout(0.5)

        self.layer4 = nn.Linear(256, num_classes)

    def forward(self, x):
        """
        Прямое распространение сигнала в сети.
        :param x: входные данные
        :return: выход сети
        """
        out = self.layer1(x)
        out = self.bn1(out)
        out = self.relu1(out)
        out = self.dropout1(out)

        out = self.layer2(out)
        out = self.bn2(out)
        out = self.relu2(out)
        out = self.dropout2(out)

        out = self.layer3(out)
        out = self.bn3(out)
        out = self.relu3(out)
        out = self.dropout3(out)

        out = self.layer4(out)
        return out

# Функции для загрузки и подготовки данных
def load_dataset(file_path):
    """
    Функция для загрузки и подготовки датасета.
    :param file_path: путь к CSV-файлу датасета
    :return: тренировочные и тестовые данные, метки, маппинг классов
    """
    try:
        data = pd.read_csv(file_path, sep=';')
        X = data.iloc[:, 2:].values  # Изображения
        y = data.iloc[:, 1].values   # Классы

        # Преобразование данных в тензоры PyTorch
        X = torch.tensor(X, dtype=torch.float32)

        # Инвертирование цветов (если белый 0, а черный 1)
        X = 1 - X  # Инвертируем изображения

        # Создание маппинга классов в индексы
        class_labels = np.unique(y)
        class_to_idx = {label: idx for idx, label in enumerate(class_labels)}
        idx_to_class = {idx: label for label, idx in class_to_idx.items()}
        y_indices = np.array([class_to_idx[label] for label in y])
        y = torch.tensor(y_indices, dtype=torch.long)

        # Разделение на обучающую и тестовую выборки
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, random_state=42, stratify=y
        )

        return X_train, X_test, y_train, y_test, class_to_idx, idx_to_class
    except Exception as e:
        print(f"Ошибка при загрузке датасета: {e}")
        return None, None, None, None, None, None

# Функции для обучения и оценки модели
def train_model(model, criterion, optimizer, X_train, y_train, num_epochs=10):
    """
    Функция для обучения модели.
    :param model: модель нейронной сети
    :param criterion: функция потерь
    :param optimizer: оптимизатор
    :param X_train: обучающие данные
    :param y_train: обучающие метки
    :param num_epochs: количество эпох обучения
    :return: список значений функции потерь на каждой эпохе
    """
    loss_values = []

    # Scheduler для изменения learning rate
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)

    for epoch in range(num_epochs):
        model.train()

        # Прямой проход
        outputs = model(X_train)
        loss = criterion(outputs, y_train)

        # Обратное распространение
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        loss_values.append(loss.item())
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

        # Шаг изменения learning rate
        scheduler.step()

    return loss_values

def evaluate_model(model, X_test, y_test):
    """
    Функция для оценки модели на тестовой выборке.
    :param model: обученная модель
    :param X_test: тестовые данные
    :param y_test: тестовые метки
    :return: словарь с метриками качества
    """
    model.eval()
    with torch.no_grad():
        outputs = model(X_test)
        _, predicted = torch.max(outputs.data, 1)
        y_true = y_test.numpy()
        y_pred = predicted.numpy()

        accuracy = accuracy_score(y_true, y_pred)
        precision = precision_score(y_true, y_pred, average='weighted', zero_division=0)
        recall = recall_score(y_true, y_pred, average='weighted', zero_division=0)

    metrics = {
        'accuracy': accuracy,
        'precision': precision,
        'recall': recall
    }
    return metrics

# Функция для построения графика функции потерь
def plot_loss(loss_values, save_path=None):
    """
    Функция для построения графика изменения функции потерь.
    :param loss_values: список значений функции потерь на каждой эпохе
    :param save_path: путь для сохранения графика (если указан)
    """
    plt.figure()
    plt.plot(range(1, len(loss_values)+1), loss_values, marker='o')
    plt.title('График изменения функции потерь')
    plt.xlabel('Эпоха')
    plt.ylabel('Значение функции потерь')
    plt.grid(True)
    if save_path:
        plt.savefig(save_path)
    plt.show()

# Класс основного окна приложения с графическим интерфейсом
class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("Нейронная сеть для распознавания изображений")
        self.setGeometry(100, 100, 800, 600)

        # Элементы интерфейса
        self.create_widgets()

        # Переменные
        self.model = None
        self.X_train = None
        self.X_test = None
        self.y_train = None
        self.y_test = None
        self.class_to_idx = None
        self.idx_to_class = None
        self.last_img_tensor = None  # Для хранения последнего изображения

    def create_widgets(self):
        """
        Метод для создания виджетов интерфейса.
        """
        self.loadButton = QtWidgets.QPushButton("Загрузить датасет", self)
        self.loadButton.setGeometry(50, 50, 200, 40)
        self.loadButton.clicked.connect(self.load_dataset)

        self.classLabel = QtWidgets.QLabel("Выберите ожидаемый класс:", self)
        self.classLabel.setGeometry(270, 50, 200, 40)

        self.classComboBox = QtWidgets.QComboBox(self)
        self.classComboBox.setGeometry(470, 50, 150, 40)

        self.trainButton = QtWidgets.QPushButton("Обучить модель", self)
        self.trainButton.setGeometry(50, 110, 200, 40)
        self.trainButton.clicked.connect(self.train_model)

        self.statusLabel = QtWidgets.QLabel("Статус: готов", self)
        self.statusLabel.setGeometry(50, 170, 700, 30)

        self.metricsLabel = QtWidgets.QLabel("Метрики качества:", self)
        self.metricsLabel.setGeometry(50, 210, 700, 60)

        self.saveWeightsButton = QtWidgets.QPushButton("Сохранить модель", self)
        self.saveWeightsButton.setGeometry(50, 280, 200, 40)
        self.saveWeightsButton.clicked.connect(self.save_model)

        self.loadWeightsButton = QtWidgets.QPushButton("Загрузить модель", self)
        self.loadWeightsButton.setGeometry(50, 340, 200, 40)
        self.loadWeightsButton.clicked.connect(self.load_model)

        self.loadImageButton = QtWidgets.QPushButton("Загрузить изображение", self)
        self.loadImageButton.setGeometry(50, 400, 200, 40)
        self.loadImageButton.clicked.connect(self.load_image)

        self.resultLabel = QtWidgets.QLabel("Результат классификации:", self)
        self.resultLabel.setGeometry(50, 460, 700, 30)

        self.punishButton = QtWidgets.QPushButton("Наказать", self)
        self.punishButton.setGeometry(50, 500, 200, 40)
        self.punishButton.clicked.connect(self.punish_model)
        self.punishButton.setVisible(False)  # Изначально кнопка скрыта

        # Добавляем кнопку для обработки папки с изображениями
        self.processFolderButton = QtWidgets.QPushButton("Обработать папку с изображениями", self)
        self.processFolderButton.setGeometry(270, 110, 250, 40)
        self.processFolderButton.clicked.connect(self.process_image_folder)

    def load_dataset(self):
        """
        Слот для загрузки датасета.
        """
        options = QtWidgets.QFileDialog.Options()
        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            "Выбрать датасет",
            "",
            "CSV Files (*.csv);;All Files (*)",
            options=options
        )
        if file_name:
            self.X_train, self.X_test, self.y_train, self.y_test, self.class_to_idx, self.idx_to_class = load_dataset(file_name)
            if self.X_train is not None:
                self.statusLabel.setText("Датасет успешно загружен.")
                # Обновление выпадающего списка с классами
                self.classComboBox.clear()
                class_labels = list(self.class_to_idx.keys())
                self.classComboBox.addItems(class_labels)
            else:
                self.statusLabel.setText("Ошибка при загрузке датасета.")

    def train_model(self):
        """
        Слот для обучения модели.
        """
        if self.X_train is not None:
            input_size = self.X_train.shape[1]
            num_classes = len(self.class_to_idx)

            # Рассчитываем веса классов для борьбы с дисбалансом
            class_weights = compute_class_weight(
                class_weight='balanced',
                classes=np.unique(self.y_train.numpy()),
                y=self.y_train.numpy()
            )
            class_weights = torch.tensor(class_weights, dtype=torch.float32)

            # Сначала обучаем однослойную сеть
            self.model = SingleLayerNN(input_size, num_classes)
            criterion = nn.CrossEntropyLoss(weight=class_weights)
            optimizer = optim.SGD(self.model.parameters(), lr=0.01)

            loss_values = train_model(
                self.model, criterion, optimizer, self.X_train, self.y_train, num_epochs=10
            )

            # Отображение графика функции потерь
            plot_loss(loss_values)

            # Оценка модели
            metrics = evaluate_model(self.model, self.X_test, self.y_test)
            accuracy = metrics['accuracy']
            metrics_text = (f"Accuracy: {accuracy:.4f}\n"
                            f"Precision: {metrics['precision']:.4f}\n"
                            f"Recall: {metrics['recall']:.4f}")
            self.metricsLabel.setText(f"Метрики качества:\n{metrics_text}")
            self.statusLabel.setText("Однослойная модель успешно обучена.")

            # Проверка точности, если меньше 0.7, используем многослойный персептрон
            if accuracy < 0.7:
                self.statusLabel.setText("Точность менее 0.7, обучение многослойного персептрона...")

                # Обучение многослойного персептрона
                self.model = MultilayerPerceptron(input_size, num_classes)
                criterion = nn.CrossEntropyLoss(weight=class_weights)
                optimizer = optim.Adam(self.model.parameters(), lr=0.0001)

                loss_values = train_model(
                    self.model, criterion, optimizer, self.X_train, self.y_train, num_epochs=100
                )

                # Отображение графика функции потерь
                plot_loss(loss_values)

                # Оценка модели
                metrics = evaluate_model(self.model, self.X_test, self.y_test)
                metrics_text = (f"Accuracy: {metrics['accuracy']:.4f}\n"
                                f"Precision: {metrics['precision']:.4f}\n"
                                f"Recall: {metrics['recall']:.4f}")
                self.metricsLabel.setText(f"Метрики качества после дообучения:\n{metrics_text}")
                self.statusLabel.setText("Многослойный персептрон обучен.")

        else:
            self.statusLabel.setText("Пожалуйста, загрузите датасет перед обучением модели.")

    def save_model(self):
        """
        Слот для сохранения весовых коэффициентов модели.
        """
        if self.model is not None:
            options = QtWidgets.QFileDialog.Options()
            file_name, _ = QtWidgets.QFileDialog.getSaveFileName(
                self,
                "Сохранить модель",
                "",
                "Model Files (*.pth);;All Files (*)",
                options=options
            )
            if file_name:
                torch.save({
                    'model_state_dict': self.model.state_dict(),
                    'class_to_idx': self.class_to_idx,
                    'idx_to_class': self.idx_to_class
                }, file_name)
                self.statusLabel.setText("Модель успешно сохранена.")
        else:
            self.statusLabel.setText("Нет обученной модели для сохранения.")

    def load_model(self):
        """
        Слот для загрузки весовых коэффициентов модели.
        """
        options = QtWidgets.QFileDialog.Options()
        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            "Загрузить модель",
            "",
            "Model Files (*.pth);;All Files (*)",
            options=options
        )
        if file_name:
            try:
                checkpoint = torch.load(file_name)
                input_size = 1024  # Размер входа
                num_classes = len(checkpoint['class_to_idx'])

                # Определяем модель по наличию слоев
                if 'layer1.weight' in checkpoint['model_state_dict']:
                    self.model = MultilayerPerceptron(input_size, num_classes)
                else:
                    self.model = SingleLayerNN(input_size, num_classes)

                self.model.load_state_dict(checkpoint['model_state_dict'])
                self.class_to_idx = checkpoint['class_to_idx']
                self.idx_to_class = checkpoint['idx_to_class']
                self.statusLabel.setText("Модель успешно загружена.")

                # Обновление выпадающего списка с классами
                self.classComboBox.clear()
                class_labels = list(self.class_to_idx.keys())
                self.classComboBox.addItems(class_labels)
            except Exception as e:
                self.statusLabel.setText(f"Ошибка при загрузке модели: {e}")

    def load_image(self):
        """
        Слот для загрузки изображения и его классификации.
        """
        options = QtWidgets.QFileDialog.Options()
        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            "Загрузить изображение",
            "",
            "Image Files (*.png *.jpg *.bmp);;All Files (*)",
            options=options
        )
        if file_name and self.model is not None:
            # Загрузка и предобработка изображения
            img = QtGui.QImage(file_name)
            img = img.convertToFormat(QtGui.QImage.Format_Grayscale8)
            img = img.scaled(32, 32)
            img_bits = img.bits()
            img_bits.setsize(img.height() * img.width())
            img_data = np.frombuffer(img_bits, np.uint8).astype(np.float32) / 255.0

            # Инвертирование цветов
            img_data = 1 - img_data  # Инвертируем изображение

            img_tensor = torch.tensor(img_data, dtype=torch.float32)
            img_tensor = img_tensor.unsqueeze(0)  # Добавляем размерность батча

            self.last_img_tensor = img_tensor  # Сохраняем для наказания

            # Классификация изображения
            self.model.eval()
            with torch.no_grad():
                output = self.model(img_tensor)
                _, predicted = torch.max(output.data, 1)
                class_idx = predicted.item()
                class_label = self.idx_to_class.get(class_idx, "Неизвестный класс")
                self.resultLabel.setText(f"Результат классификации: {class_label}")

                expected_class = self.classComboBox.currentText()
                expected_idx = self.class_to_idx.get(expected_class)

                if expected_idx != class_idx:
                    self.statusLabel.setText("Классификация неверна. Вы можете наказать модель.")
                    self.punishButton.setVisible(True)
                else:
                    self.statusLabel.setText("Классификация верна.")
                    self.punishButton.setVisible(False)
        else:
            self.statusLabel.setText("Пожалуйста, загрузите модель и изображение для классификации.")

    def punish_model(self):
        """
        Слот для наказания модели в случае неверной классификации.
        """
        if self.model is not None:
            # Получаем ожидаемый класс из выпадающего списка
            expected_class = self.classComboBox.currentText()
            expected_idx = self.class_to_idx.get(expected_class)

            if self.last_img_tensor is not None and expected_idx is not None:
                # Превращаем изображение и метку в батч
                img_tensor = self.last_img_tensor
                label_tensor = torch.tensor([expected_idx], dtype=torch.long)

                # Обучаем модель на одном примере с повышенным весом (больший learning rate)
                criterion = nn.CrossEntropyLoss()
                optimizer = optim.SGD(self.model.parameters(), lr=0.1)  # Увеличенный lr

                self.model.train()

                # Переводим BatchNorm слои в режим оценки
                for module in self.model.modules():
                    if isinstance(module, nn.BatchNorm1d):
                        module.eval()

                optimizer.zero_grad()
                output = self.model(img_tensor)
                loss = criterion(output, label_tensor)
                loss.backward()
                optimizer.step()

                self.statusLabel.setText("Модель наказана и обновлена.")
                self.punishButton.setVisible(False)
            else:
                self.statusLabel.setText("Нет изображения для наказания модели или некорректный класс.")
        else:
            self.statusLabel.setText("Модель не загружена.")

    def process_image_folder(self):
        """
        Метод для обработки папки с изображениями.
        Прогоняет все изображения через модель, наказывает ее при неверной классификации и пересчитывает точность.
        """
        if self.model is not None and self.class_to_idx is not None:
            options = QtWidgets.QFileDialog.Options()
            folder_path = QtWidgets.QFileDialog.getExistingDirectory(
                self,
                "Выбрать папку с изображениями",
                options=options
            )
            if folder_path:
                all_true_labels = []
                all_predicted_labels = []
                num_images = 0
                num_correct = 0

                # Проходим по всем подкаталогам (классам)
                for class_name in os.listdir(folder_path):
                    class_dir = os.path.join(folder_path, class_name)
                    # Проверяем, что это директория
                    if os.path.isdir(class_dir):
                        rotated_images_dir = os.path.join(class_dir, "rotated_images")
                        if os.path.isdir(rotated_images_dir):
                            # Проходим по всем изображениям в папке rotated_images
                            for image_name in os.listdir(rotated_images_dir):
                                image_path = os.path.join(rotated_images_dir, image_name)
                                if os.path.isfile(image_path):
                                    num_images += 1
                                    # Загружаем и обрабатываем изображение
                                    img = QtGui.QImage(image_path)
                                    img = img.convertToFormat(QtGui.QImage.Format_Grayscale8)
                                    img = img.scaled(32, 32)
                                    img_bits = img.bits()
                                    img_bits.setsize(img.height() * img.width())
                                    img_data = np.frombuffer(img_bits, np.uint8).astype(np.float32) / 255.0

                                    # Инвертирование цветов
                                    img_data = 1 - img_data  # Инвертируем изображение

                                    img_tensor = torch.tensor(img_data, dtype=torch.float32)
                                    img_tensor = img_tensor.unsqueeze(0)  # Добавляем размерность батча

                                    # Классификация изображения
                                    self.model.eval()
                                    with torch.no_grad():
                                        output = self.model(img_tensor)
                                        _, predicted = torch.max(output.data, 1)
                                        class_idx = predicted.item()
                                        predicted_class = self.idx_to_class.get(class_idx, "Неизвестный класс")

                                    # Получаем истинный класс
                                    true_class = class_name
                                    true_idx = self.class_to_idx.get(true_class)

                                    if true_idx is None:
                                        print(f"Класс {true_class} не найден в маппинге классов.")
                                        continue

                                    all_true_labels.append(true_idx)
                                    all_predicted_labels.append(class_idx)

                                    # Если классификация неверна, наказываем модель
                                    if true_idx != class_idx:
                                        # Обучаем модель на данном примере
                                        label_tensor = torch.tensor([true_idx], dtype=torch.long)
                                        criterion = nn.CrossEntropyLoss()
                                        optimizer = optim.SGD(self.model.parameters(), lr=0.1)  # Увеличенный lr

                                        self.model.train()

                                        # Переводим BatchNorm слои в режим оценки
                                        for module in self.model.modules():
                                            if isinstance(module, nn.BatchNorm1d):
                                                module.eval()

                                        optimizer.zero_grad()
                                        output = self.model(img_tensor)
                                        loss = criterion(output, label_tensor)
                                        loss.backward()
                                        optimizer.step()
                                    else:
                                        num_correct += 1

                # Рассчитываем точность
                accuracy = num_correct / num_images if num_images > 0 else 0
                self.statusLabel.setText(f"Обработка завершена. Точность: {accuracy:.4f}")
                print(f"Обработано изображений: {num_images}")
                print(f"Правильно классифицировано: {num_correct}")
            else:
                self.statusLabel.setText("Папка с изображениями не выбрана.")
        else:
            self.statusLabel.setText("Пожалуйста, загрузите модель перед обработкой изображений.")

# Главный модуль
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

Проанализируй код, найди почему точность низкая и почему постоянно ошибается. Если не найдешь то скажи из-за чего может быть такое\