{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"}],"dockerImageVersionId":31042,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-08T07:54:24.211606Z","iopub.execute_input":"2025-12-08T07:54:24.211827Z","iopub.status.idle":"2025-12-08T07:54:34.599798Z","shell.execute_reply.started":"2025-12-08T07:54:24.211805Z","shell.execute_reply":"2025-12-08T07:54:34.599157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! pip install -U albumentations tensorflow opencv-python","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T07:54:34.600622Z","iopub.execute_input":"2025-12-08T07:54:34.601106Z","iopub.status.idle":"2025-12-08T07:55:49.597478Z","shell.execute_reply.started":"2025-12-08T07:54:34.601078Z","shell.execute_reply":"2025-12-08T07:55:49.596583Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ячейка 1: Импорт библиотек и настройка окружения\nimport os\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport cv2\nimport albumentations as A\nfrom sklearn.model_selection import train_test_split, StratifiedKFold\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras import callbacks as kcallbacks\nfrom sklearn.utils import class_weight\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings('ignore')\n\n\n# настройка GPU\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(f\" Обнаружено GPU: {len(gpus)} устройств\")\n    except RuntimeError as e:\n        print(f\" Ошибка настройки GPU: {e}\")\nelse:\n    print(\" GPU не обнаружены, будет использоваться CPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:17:31.651955Z","iopub.execute_input":"2025-12-08T08:17:31.652268Z","iopub.status.idle":"2025-12-08T08:17:31.658754Z","shell.execute_reply.started":"2025-12-08T08:17:31.652248Z","shell.execute_reply":"2025-12-08T08:17:31.658026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# загрузка и анализ данных\ntry:\n    train_df = pd.read_csv('/kaggle/input/aptos2019-blindness-detection/train.csv')\n    train_df['id_code'] = train_df['id_code'] + '.png'\n    print(f\" Данные успешно загружены\")\n    print(f\"  - Размер датасета: {train_df.shape}\")\n    print(f\"  - Колонки: {list(train_df.columns)}\")\nexcept FileNotFoundError:\n    print(\"- Файл train.csv не найден!\")\n    # показать доступные файлы\n    print(\"\\nДоступные файлы в input:\")\n    for dirname, _, filenames in os.walk('/kaggle/input'):\n        for filename in filenames:\n            print(f\"  {os.path.join(dirname, filename)}\")\n    raise\n\n# анализ баланса классов\nclass_distribution = train_df['diagnosis'].value_counts().sort_index()\nfor class_id, count in class_distribution.items():\n    percentage = (count / len(train_df)) * 100\n    print(f\"  Класс {class_id}: {count:4d} изображений ({percentage:.1f}%)\")\n\n# визуализация распределения\nplt.figure(figsize=(12, 4))\n\nplt.subplot(1, 2, 1)\nbars = plt.bar(class_distribution.index, class_distribution.values)\nplt.title('Распределение классов', fontsize=14, fontweight='bold')\nplt.xlabel('Класс (степень ретинопатии)', fontsize=12)\nplt.ylabel('Количество изображений', fontsize=12)\nplt.xticks(range(5))\nplt.grid(True, alpha=0.3)\n\n# добавление значений на столбцы\nfor bar in bars:\n    height = bar.get_height()\n    plt.text(bar.get_x() + bar.get_width()/2., height,\n             f'{int(height)}', ha='center', va='bottom')\n\nplt.subplot(1, 2, 2)\ncolors = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#c2c2f0']\nplt.pie(class_distribution.values, labels=class_distribution.index, \n        autopct='%1.1f%%', colors=colors, startangle=90)\nplt.title('Процентное распределение', fontsize=14, fontweight='bold')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:18:30.452638Z","iopub.execute_input":"2025-12-08T08:18:30.453195Z","iopub.status.idle":"2025-12-08T08:18:30.733963Z","shell.execute_reply.started":"2025-12-08T08:18:30.453173Z","shell.execute_reply":"2025-12-08T08:18:30.733188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# разделение данных на train/val\n\n# стратифицированное разделение для сохранения пропорций классов\ntrain_df, val_df = train_test_split(\n    train_df,\n    test_size=0.15,\n    random_state=42,\n    stratify=train_df['diagnosis']\n)\n\nprint(f\" Данные успешно разделены:\")\nprint(f\"  - Тренировочный набор: {len(train_df):5d} изображений ({len(train_df)/len(train_df)*100:.1f}%)\")\nprint(f\"  - Валидационный набор: {len(val_df):5d} изображений ({len(val_df)/len(train_df)*100:.1f}%)\")\n\n# проверка распределения классов в наборах\nprint(\"Тренировочный набор:\")\ntrain_class_dist = train_df['diagnosis'].value_counts().sort_index()\nfor class_id in range(5):\n    count = train_class_dist.get(class_id, 0)\n    percentage = (count / len(train_df)) * 100\n    print(f\"  Класс {class_id}: {count:4d} ({percentage:.1f}%)\")\n\nprint(\"\\nВалидационный набор:\")\nval_class_dist = val_df['diagnosis'].value_counts().sort_index()\nfor class_id in range(5):\n    count = val_class_dist.get(class_id, 0)\n    percentage = (count / len(val_df)) * 100\n    print(f\"  Класс {class_id}: {count:4d} ({percentage:.1f}%)\")\n\n#  визуализация\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\naxes[0].bar(train_class_dist.index, train_class_dist.values, color='steelblue', alpha=0.7)\naxes[0].set_title('Распределение в тренировочном наборе', fontsize=12, fontweight='bold')\naxes[0].set_xlabel('Класс')\naxes[0].set_ylabel('Количество')\naxes[0].grid(True, alpha=0.3)\naxes[0].set_xticks(range(5))\n\naxes[1].bar(val_class_dist.index, val_class_dist.values, color='lightcoral', alpha=0.7)\naxes[1].set_title('Распределение в валидационном наборе', fontsize=12, fontweight='bold')\naxes[1].set_xlabel('Класс')\naxes[1].set_ylabel('Количество')\naxes[1].grid(True, alpha=0.3)\naxes[1].set_xticks(range(5))\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:19:29.355938Z","iopub.execute_input":"2025-12-08T08:19:29.356562Z","iopub.status.idle":"2025-12-08T08:19:29.666788Z","shell.execute_reply.started":"2025-12-08T08:19:29.356540Z","shell.execute_reply":"2025-12-08T08:19:29.666038Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# функции предобработки изображений\n\ndef preprocess_retina_image(image_path, size=256, augment=False):\n    \"\"\"\n    Улучшенная предобработка изображений глазного дна\n    Включает удаление черных границ, CLAHE, изменение размера и нормализацию\n    \"\"\"\n    # проверка существования файла\n    if not os.path.exists(image_path):\n        print(f\"  ⚠ Файл не найден: {os.path.basename(image_path)}\")\n        return np.zeros((size, size, 3), dtype=np.float32)\n    \n    try:\n        # загрузка изображения\n        image = cv2.imread(image_path)\n        if image is None:\n            print(f\"  ⚠ Не удалось загрузить: {os.path.basename(image_path)}\")\n            return np.zeros((size, size, 3), dtype=np.float32)\n        \n        # конвертация в RGB\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # удаление черных границ\n        gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)\n        _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)\n        \n        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        \n        if contours:\n            cnt = max(contours, key=cv2.contourArea)\n            x, y, w, h = cv2.boundingRect(cnt)\n            \n            # добавляем небольшой отступ\n            pad = 5\n            x = max(0, x - pad)\n            y = max(0, y - pad)\n            w = min(image.shape[1] - x, w + 2*pad)\n            h = min(image.shape[0] - y, h + 2*pad)\n            \n            if w > 10 and h > 10:  # проверка минимального размера\n                image = image[y:y+h, x:x+w]\n        \n        # улучшение контраста с помощью CLAHE\n        lab = cv2.cvtColor(image, cv2.COLOR_RGB2LAB)\n        l, a, b = cv2.split(lab)\n        \n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n        l = clahe.apply(l)\n        \n        lab = cv2.merge([l, a, b])\n        image = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)\n        \n        # изменение размера с сохранением пропорций\n        h, w = image.shape[:2]\n        scale = size / max(h, w)\n        new_h, new_w = int(h * scale), int(w * scale)\n        \n        image = cv2.resize(image, (new_w, new_h))\n        \n        # добавление паддинга для квадрата\n        pad_h = (size - new_h) // 2\n        pad_w = (size - new_w) // 2\n        \n        image = cv2.copyMakeBorder(\n            image, \n            pad_h, size - new_h - pad_h,\n            pad_w, size - new_w - pad_w,\n            cv2.BORDER_CONSTANT, \n            value=[0, 0, 0]\n        )\n        \n        # нормализация\n        image = image.astype(np.float32) / 255.0\n        \n        # аугментация (только для тренировочных данных)\n        if augment:\n            transform = A.Compose([\n                A.HorizontalFlip(p=0.5),\n                A.VerticalFlip(p=0.5),\n                A.Rotate(limit=15, p=0.5),\n                A.RandomBrightnessContrast(\n                    brightness_limit=0.1, \n                    contrast_limit=0.1, \n                    p=0.5\n                ),\n                A.OneOf([\n                    A.GaussianBlur(blur_limit=(3, 5), p=0.3),\n                    A.MotionBlur(blur_limit=(3, 7), p=0.3),\n                ], p=0.2),\n            ])\n            image = transform(image=image)['image']\n        \n        return image\n        \n    except Exception as e:\n        print(f\"  Ошибка обработки {os.path.basename(image_path)}: {str(e)[:50]}...\")\n        return np.zeros((size, size, 3), dtype=np.float32)\n\nprint(\" Функции предобработки определены\")\nprint(\"  - preprocess_retina_image - основная функция обработки\")\nprint(\"  - Поддерживает аугментацию и CLAHE контраст\")\n\n# тестирование функции на примере\n# проверяем путь к изображениям\ntrain_path = '/kaggle/input/aptos2019-blindness-detection/train_images'\ntest_image_path = os.path.join(train_path, train_df.iloc[0]['id_code'])\n\nprint(f\"  Путь к изображениям: {train_path}\")\nprint(f\"  Пример изображения: {test_image_path}\")\nprint(f\"  Файл существует: {os.path.exists(test_image_path)}\")\n\nif os.path.exists(test_image_path):\n    # обработка без аугментации\n    image_no_aug = preprocess_retina_image(test_image_path, augment=False)\n    \n    # обработка с аугментацией\n    image_aug = preprocess_retina_image(test_image_path, augment=True)\n    \n    print(f\" Тест успешен\")\n    print(f\"    Размер изображения: {image_no_aug.shape}\")\n    print(f\"    Диапазон значений: [{image_no_aug.min():.3f}, {image_no_aug.max():.3f}]\")\n    print(f\"    Тип данных: {image_no_aug.dtype}\")\n    \n    # визуализация результатов обработки\n    fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n    \n    axes[0].imshow(image_no_aug)\n    axes[0].set_title('Без аугментации', fontweight='bold')\n    axes[0].axis('off')\n    \n    axes[1].imshow(image_aug)\n    axes[1].set_title('С аугментацией', fontweight='bold')\n    axes[1].axis('off')\n    \n    plt.suptitle('Результаты предобработки изображения', fontsize=14, fontweight='bold')\n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\" Тестовый файл не найден!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:21:26.180507Z","iopub.execute_input":"2025-12-08T08:21:26.181013Z","iopub.status.idle":"2025-12-08T08:21:26.575837Z","shell.execute_reply.started":"2025-12-08T08:21:26.180970Z","shell.execute_reply":"2025-12-08T08:21:26.575150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# создание генератора данных\nclass RetinaDataGenerator(tf.keras.utils.Sequence):\n    \"\"\"\n    Генератор данных для изображений глазного дна\n    Поддерживает кэширование, аугментацию и балансировку классов\n    \"\"\"\n    \n    def __init__(self, df, base_path, batch_size=16, augment=False, shuffle=True):\n        self.df = df.reset_index(drop=True)\n        self.base_path = base_path\n        self.batch_size = batch_size\n        self.augment = augment\n        self.shuffle = shuffle\n        self.image_cache = {}  # кэш для ускорения загрузки\n        self.on_epoch_end()\n        \n        print(f\"  Создан генератор с параметрами:\")\n        print(f\"    • Изображений: {len(self.df)}\")\n        print(f\"    • Batch size: {batch_size}\")\n        print(f\"    • Аугментация: {'Да' if augment else 'Нет'}\")\n        print(f\"    • Перемешивание: {'Да' if shuffle else 'Нет'}\")\n    \n    def __len__(self):\n        return int(np.ceil(len(self.df) / self.batch_size))\n    \n    def __getitem__(self, idx):\n        batch_indices = self.indices[idx * self.batch_size:(idx + 1) * self.batch_size]\n        batch_df = self.df.iloc[batch_indices]\n        \n        images = []\n        labels = []\n        \n        for _, row in batch_df.iterrows():\n            img_key = row['id_code']\n            \n            # используем кэш для неаугментированных изображений\n            if img_key in self.image_cache and not self.augment:\n                image = self.image_cache[img_key]\n            else:\n                img_path = os.path.join(self.base_path, img_key)\n                image = preprocess_retina_image(img_path, augment=self.augment)\n                if not self.augment:  # кэшируем только оригинальные\n                    self.image_cache[img_key] = image\n            \n            images.append(image)\n            labels.append(row['diagnosis'])\n        \n        # преобразуем в массивы\n        images_array = np.array(images, dtype=np.float32)\n        labels_array = tf.keras.utils.to_categorical(labels, num_classes=5)\n        \n        return images_array, labels_array\n    \n    def on_epoch_end(self):\n        self.indices = np.arange(len(self.df))\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n    \n    def show_batch_info(self, batch_idx=0):\n        \"\"\"Показать информацию о батче\"\"\"\n        images, labels = self[batch_idx]\n        \n        print(f\"\\n ИНФОРМАЦИЯ О БАТЧЕ {batch_idx}:\")\n        print(f\"  Размер батча: {len(images)} изображений\")\n        print(f\"  Размер изображения: {images[0].shape}\")\n        print(f\"  Диапазон значений пикселей: [{images.min():.3f}, {images.max():.3f}]\")\n        print(f\"  Распределение классов в батче:\")\n        \n        class_counts = np.bincount(np.argmax(labels, axis=1))\n        for class_id, count in enumerate(class_counts):\n            if count > 0:\n                print(f\"    Класс {class_id}: {count} изображений\")\n\nprint(\" Класс RetinaDataGenerator определен\")\n\n# тестирование генератора\n\n# создаем тестовые генераторы\ntrain_gen = RetinaDataGenerator(\n    train_df.iloc[:100],  # берем только 100 для теста\n    train_path,\n    batch_size=8,\n    augment=False,\n    shuffle=True\n)\n\n# получаем первый батч\ntest_batch_idx = 0\nimages, labels = train_gen[test_batch_idx]\n\n# выводим информацию\ntrain_gen.show_batch_info(test_batch_idx)\n\n# визуализация батча\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\naxes = axes.ravel()\n\nfor i in range(min(8, len(images))):\n    ax = axes[i]\n    ax.imshow(images[i])\n    true_class = np.argmax(labels[i])\n    ax.set_title(f'Класс: {true_class}', fontweight='bold')\n    ax.axis('off')\n\nplt.suptitle('Примеры изображений из батча (без аугментации)', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:22:55.665174Z","iopub.execute_input":"2025-12-08T08:22:55.665791Z","iopub.status.idle":"2025-12-08T08:22:57.923571Z","shell.execute_reply.started":"2025-12-08T08:22:55.665768Z","shell.execute_reply":"2025-12-08T08:22:57.922545Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# создание и компиляция модели\ndef build_retinopathy_model(input_shape=(256, 256, 3)):\n    \"\"\"\n    Создание модели для классификации диабетической ретинопатии\n    на основе EfficientNetB3 с кастомными слоями\n    \"\"\"\n    \n    print(\" Строим архитектуру модели...\")\n    \n    # загрузка предобученной базовой модели\n    base_model = tf.keras.applications.EfficientNetB3(\n        weights='imagenet',\n        include_top=False,\n        input_shape=input_shape,\n        pooling=None\n    )\n    \n    # замораживаем веса базовой модели (на первых эпохах)\n    base_model.trainable = False\n    print(f\"  • Базовая модель: EfficientNetB3\")\n    print(f\"  • Заморожено слоев: {len(base_model.layers)}\")\n    \n    # создаем кастомные слои\n    inputs = tf.keras.Input(shape=input_shape)\n    \n    # добавляем аугментацию как часть модели\n    x = layers.RandomFlip(\"horizontal_and_vertical\")(inputs)\n    x = layers.RandomRotation(0.1)(x)\n    x = layers.RandomContrast(0.1)(x)\n    \n    # предобработка для EfficientNet\n    x = tf.keras.applications.efficientnet.preprocess_input(x)\n    \n    # пропускаем через базовую модель\n    x = base_model(x)\n    \n    # глобальный средний пулинг\n    x = layers.GlobalAveragePooling2D()(x)\n    \n    # регуляризация\n    x = layers.Dropout(0.5)(x)\n    \n    # полносвязные слои\n    x = layers.Dense(512, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.001))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.3)(x)\n    \n    x = layers.Dense(256, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.001))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.3)(x)\n    \n    # выходной слой\n    outputs = layers.Dense(5, activation='softmax')(x)\n    \n    # создаем модель\n    model = tf.keras.Model(inputs=inputs, outputs=outputs, name=\"Retinopathy_Classifier\")\n    \n    print(f\"  • Всего слоев в модели: {len(model.layers)}\")\n    print(f\"  • Параметры модели: {model.count_params():,}\")\n    \n    return model\n\n# создаем модель\nprint(\"\\n СОЗДАНИЕ МОДЕЛИ:\")\nmodel = build_retinopathy_model()\n\n# выводим архитектуру\nprint(\"\\n АРХИТЕКТУРА МОДЕЛИ:\")\nmodel.summary()\n\n# компиляция модели\nprint(\"\\n КОМПИЛЯЦИЯ МОДЕЛИ:\")\n\n# Learning rate schedule\n#lr_schedule = tf.keras.optimizers.schedules.CosineDecayRestarts(\n#    initial_learning_rate=1e-4,\n#    first_decay_steps=500,\n#    t_mul=2.0,\n#    m_mul=0.5,\n#    alpha=1e-6\n#)\n\n# оптимизатор\noptimizer = tf.keras.optimizers.Adam(\n    learning_rate=1e-4,\n    beta_1=0.9,\n    beta_2=0.999,\n    epsilon=1e-07\n)\n\n# метрики\nmetrics = [\n    'accuracy',\n    tf.keras.metrics.AUC(name='auc', multi_label=True),\n    tf.keras.metrics.Precision(name='precision'),\n    tf.keras.metrics.Recall(name='recall'),\n    tf.keras.metrics.TopKCategoricalAccuracy(k=2, name='top_2_accuracy')\n]\n\n# компиляция\nmodel.compile(\n    optimizer=optimizer,\n    loss='categorical_crossentropy',\n    metrics=metrics\n)\n\nprint(\" Модель успешно скомпилирована\")\nprint(f\"  • Оптимизатор: Adam с CosineDecayRestarts\")\nprint(f\"  • Начальный LR: {1e-4}\")\nprint(f\"  • Функция потерь: categorical_crossentropy\")\nprint(f\"  • Метрики: {len(metrics)} метрик\")\n\n# проверка возможности предсказания\nprint(\"\\n ТЕСТ ПРЕДСКАЗАНИЯ МОДЕЛИ:\")\ntest_input = np.random.randn(2, 256, 256, 3).astype(np.float32)\ntest_output = model.predict(test_input, verbose=0)\n\nprint(f\"  • Вход: {test_input.shape}\")\nprint(f\"  • Выход: {test_output.shape}\")\nprint(f\"  • Сумма вероятностей по классам: {test_output.sum(axis=1)}\")\nprint(\" Модель работает корректно\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T09:25:49.621493Z","iopub.execute_input":"2025-12-08T09:25:49.622098Z","iopub.status.idle":"2025-12-08T09:25:54.333239Z","shell.execute_reply.started":"2025-12-08T09:25:49.622076Z","shell.execute_reply":"2025-12-08T09:25:54.332447Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# расчет весов классов и создание генераторов\n\n# расчет весов классов для балансировки\n\nclass_weights = class_weight.compute_class_weight(\n    class_weight='balanced',\n    classes=np.unique(train_df['diagnosis']),\n    y=train_df['diagnosis']\n)\n\n# преобразуем в словарь\nclass_weights_dict = {i: float(w) for i, w in enumerate(class_weights)}\n\n# выводим информацию\nfor class_id, weight in class_weights_dict.items():\n    original_count = train_df[train_df['diagnosis'] == class_id].shape[0]\n    print(f\"  Класс {class_id}: {original_count:4d} изображений → вес {weight:.2f}\")\n\n# визуализация весов\nfig, ax = plt.subplots(figsize=(8, 5))\nbars = ax.bar(class_weights_dict.keys(), class_weights_dict.values(), color='skyblue')\nax.set_xlabel('Класс', fontsize=12)\nax.set_ylabel('Вес класса', fontsize=12)\nax.set_title('Веса классов для балансировки', fontsize=14, fontweight='bold')\nax.set_xticks(range(5))\nax.grid(True, alpha=0.3, axis='y')\n\n# добавляем значения на столбцы\nfor bar in bars:\n    height = bar.get_height()\n    ax.text(bar.get_x() + bar.get_width()/2., height,\n            f'{height:.2f}', ha='center', va='bottom', fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n\n# создание генераторов данных\ntrain_gen = RetinaDataGenerator(\n    df=train_df,\n    base_path=train_path,\n    batch_size=16,  # уменьшенный размер батча для стабильности\n    augment=True,   # аугментация для тренировки\n    shuffle=True\n)\n\nval_gen = RetinaDataGenerator(\n    df=val_df,\n    base_path=train_path,\n    batch_size=16,\n    augment=False,  # без аугментации для валидации\n    shuffle=False   # без перемешивания для стабильной оценки\n)\n\nprint(\" Генераторы созданы:\")\nprint(f\"  • Тренировочный генератор: {len(train_gen)} батчей\")\nprint(f\"  • Валидационный генератор: {len(val_gen)} батчей\")\nprint(f\"  • Размер батча: 16 изображений\")\n\n\n# тестовый батч из тренировочного генератора\ntrain_images, train_labels = train_gen[0]\nval_images, val_labels = val_gen[0]\n\nprint(\"Тренировочный генератор:\")\nprint(f\"  • Размер батча: {train_images.shape}\")\nprint(f\"  • Диапазон значений: [{train_images.min():.3f}, {train_images.max():.3f}]\")\n\nprint(\"\\nВалидационный генератор:\")\nprint(f\"  • Размер батча: {val_images.shape}\")\nprint(f\"  • Диапазон значений: [{val_images.min():.3f}, {val_images.max():.3f}]\")\n\n# сравнение распределения классов\ntrain_classes = np.argmax(train_labels, axis=1)\nval_classes = np.argmax(val_labels, axis=1)\n\nprint(\"\\nРаспределение классов в первом батче:\")\nfor class_id in range(5):\n    train_count = np.sum(train_classes == class_id)\n    val_count = np.sum(val_classes == class_id)\n    print(f\"  Класс {class_id}: train={train_count}, val={val_count}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T08:27:11.705426Z","iopub.execute_input":"2025-12-08T08:27:11.705711Z","iopub.status.idle":"2025-12-08T08:27:16.487649Z","shell.execute_reply.started":"2025-12-08T08:27:11.705691Z","shell.execute_reply":"2025-12-08T08:27:16.487037Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# обучение модели\n\ncallbacks = [\n    # сохранение лучшей модели\n    kcallbacks.ModelCheckpoint(\n        filepath='best_model_retinopathy.keras',\n        monitor='val_auc',\n        mode='max',\n        save_best_only=True,\n        save_weights_only=False,\n        verbose=1\n    ),\n    \n    # ранняя остановка\n    kcallbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=8,\n        restore_best_weights=True,\n        verbose=1,\n        min_delta=0.001\n    ),\n    \n    # динамическое изменение learning rate\n    kcallbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-7,\n        verbose=1\n    ),\n\n    kcallbacks.LearningRateScheduler(\n        lambda epoch: 1e-4 * (0.5 ** (epoch // 10)),  # уменьшаем LR каждые 10 эпох\n        verbose=1\n    ),\n    \n    # логирование в CSV\n    kcallbacks.CSVLogger(\n        'training_history.csv',\n        separator=',',\n        append=False\n    ),\n    \n    # TensorBoard (опционально)\n    # kcallbacks.TensorBoard(\n    #     log_dir='./logs',\n    #     histogram_freq=1,\n    #     write_graph=True,\n    #     write_images=True\n    # )\n]\n\nprint(\" Callback'ы настроены:\")\nfor i, callback in enumerate(callbacks, 1):\n    print(f\"  {i}. {callback.__class__.__name__}\")\n\n# обучение модели\nprint(\"\\n ЗАПУСК ОБУЧЕНИЯ:\")\nprint(f\"  • Эпох: 30\")\nprint(f\"  • Batch size: 16\")\nprint(f\"  • Размер тренировочного набора: {len(train_df)}\")\nprint(f\"  • Размер валидационного набора: {len(val_df)}\")\nprint(f\"  • Шагов за эпоху: {len(train_gen)}\")\n\nhistory = model.fit(\n    train_gen,\n    epochs=30,\n    validation_data=val_gen,\n    class_weight=class_weights_dict,\n    callbacks=callbacks,\n    verbose=1\n)\n\nprint(\"\\n ОБУЧЕНИЕ ЗАВЕРШЕНО!\")\nprint(f\"  • Фактическое количество эпох: {len(history.history['loss'])}\")\nprint(f\"  • Лучшая модель сохранена как: 'best_model_retinopathy.keras'\")\n\n# Сохранение полной модели\nprint(\"\\n СОХРАНЕНИЕ МОДЕЛИ:\")\nmodel.save('retinopathy_final_model.keras')\nprint(\" Модель сохранена как 'retinopathy_final_model.keras'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T09:27:25.329097Z","iopub.execute_input":"2025-12-08T09:27:25.329654Z","iopub.status.idle":"2025-12-08T13:12:43.464966Z","shell.execute_reply.started":"2025-12-08T09:27:25.329634Z","shell.execute_reply":"2025-12-08T13:12:43.464233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# визуализация результатов обучения\n\n# загрузка истории обучения если нужно\nif 'history' not in locals():\n    try:\n        # Пытаемся загрузить из CSV\n        history_df = pd.read_csv('training_history.csv')\n        history = type('History', (), {'history': history_df.to_dict('list')})()\n        print(\" История обучения загружена из CSV файла\")\n    except:\n        print(\" История обучения не найдена\")\n        history = None\n\nif history:\n    print(f\"\\n ИСТОРИЯ ОБУЧЕНИЯ ({len(history.history['loss'])} эпох):\")\n    \n    # находим лучшую эпоху\n    val_auc_values = history.history.get('val_auc', [])\n    if val_auc_values:\n        best_epoch = np.argmax(val_auc_values)\n        print(f\"  • Лучшая эпоха: {best_epoch + 1}\")\n        print(f\"  • Лучший val_auc: {val_auc_values[best_epoch]:.4f}\")\n    \n    # создаем графики\n    fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n    \n    # 1. Loss\n    axes[0, 0].plot(history.history['loss'], label='Train Loss', linewidth=2)\n    axes[0, 0].plot(history.history['val_loss'], label='Val Loss', linewidth=2)\n    axes[0, 0].set_title('Функция потерь', fontsize=12, fontweight='bold')\n    axes[0, 0].set_xlabel('Эпоха')\n    axes[0, 0].set_ylabel('Loss')\n    axes[0, 0].legend()\n    axes[0, 0].grid(True, alpha=0.3)\n    \n    # 2. Accuracy\n    axes[0, 1].plot(history.history['accuracy'], label='Train Accuracy', linewidth=2)\n    axes[0, 1].plot(history.history['val_accuracy'], label='Val Accuracy', linewidth=2)\n    axes[0, 1].set_title('Точность', fontsize=12, fontweight='bold')\n    axes[0, 1].set_xlabel('Эпоха')\n    axes[0, 1].set_ylabel('Accuracy')\n    axes[0, 1].legend()\n    axes[0, 1].grid(True, alpha=0.3)\n    \n    # 3. AUC\n    if 'auc' in history.history:\n        axes[0, 2].plot(history.history['auc'], label='Train AUC', linewidth=2)\n        axes[0, 2].plot(history.history['val_auc'], label='Val AUC', linewidth=2)\n        axes[0, 2].set_title('AUC (площадь под кривой)', fontsize=12, fontweight='bold')\n        axes[0, 2].set_xlabel('Эпоха')\n        axes[0, 2].set_ylabel('AUC')\n        axes[0, 2].legend()\n        axes[0, 2].grid(True, alpha=0.3)\n    \n    # 4. Precision\n    if 'precision' in history.history:\n        axes[1, 0].plot(history.history['precision'], label='Train Precision', linewidth=2)\n        axes[1, 0].plot(history.history['val_precision'], label='Val Precision', linewidth=2)\n        axes[1, 0].set_title('Precision (точность)', fontsize=12, fontweight='bold')\n        axes[1, 0].set_xlabel('Эпоха')\n        axes[1, 0].set_ylabel('Precision')\n        axes[1, 0].legend()\n        axes[1, 0].grid(True, alpha=0.3)\n    \n    # 5. Recall\n    if 'recall' in history.history:\n        axes[1, 1].plot(history.history['recall'], label='Train Recall', linewidth=2)\n        axes[1, 1].plot(history.history['val_recall'], label='Val Recall', linewidth=2)\n        axes[1, 1].set_title('Recall (полнота)', fontsize=12, fontweight='bold')\n        axes[1, 1].set_xlabel('Эпоха')\n        axes[1, 1].set_ylabel('Recall')\n        axes[1, 1].legend()\n        axes[1, 1].grid(True, alpha=0.3)\n    \n    # 6. Learning Rate\n    if 'lr' in history.history:\n        axes[1, 2].plot(history.history['lr'], linewidth=2, color='purple')\n        axes[1, 2].set_title('Learning Rate', fontsize=12, fontweight='bold')\n        axes[1, 2].set_xlabel('Эпоха')\n        axes[1, 2].set_ylabel('Learning Rate')\n        axes[1, 2].set_yscale('log')\n        axes[1, 2].grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.show()\n    \n    # таблица с лучшими значениями метрик\n    print(\"\\n ЛУЧШИЕ МЕТРИКИ МОДЕЛИ:\")\n    \n    metrics_data = []\n    for metric in ['loss', 'accuracy', 'val_loss', 'val_accuracy']:\n        if metric in history.history:\n            values = history.history[metric]\n            if 'val_' in metric:\n                best_idx = np.argmin(values) if 'loss' in metric else np.argmax(values)\n            else:\n                best_idx = len(values) - 1  # последняя эпоха для train\n                \n            best_value = values[best_idx]\n            metrics_data.append([metric, f\"{best_value:.4f}\", best_idx + 1])\n    \n    # добавляем AUC если есть\n    for metric in ['auc', 'val_auc']:\n        if metric in history.history:\n            values = history.history[metric]\n            best_idx = np.argmax(values)\n            best_value = values[best_idx]\n            metrics_data.append([metric, f\"{best_value:.4f}\", best_idx + 1])\n    \n    # выводим таблицу\n    print(f\"{'Метрика':<15} {'Значение':<10} {'Эпоха':<6}\")\n    print(\"-\"*35)\n    for row in metrics_data:\n        print(f\"{row[0]:<15} {row[1]:<10} {row[2]:<6}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T13:12:43.466684Z","iopub.execute_input":"2025-12-08T13:12:43.467150Z","iopub.status.idle":"2025-12-08T13:12:44.446473Z","shell.execute_reply.started":"2025-12-08T13:12:43.467131Z","shell.execute_reply":"2025-12-08T13:12:44.445509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# оценка модели и предсказания\n\n# загрузка лучшей модели если нужно\ntry:\n    best_model = tf.keras.models.load_model('best_model_retinopathy.keras')\n    print(\" Загружена лучшая модель из 'best_model_retinopathy.keras'\")\nexcept:\n    print(\" Лучшая модель не найдена, используем текущую модель\")\n    best_model = model\n\n# Оценка модели\nprint(\"\\n ОЦЕНКА МОДЕЛИ НА ВАЛИДАЦИОННОМ НАБОРЕ:\")\n\nresults = best_model.evaluate(val_gen, verbose=0)\n\nprint(\"Метрики модели:\")\n\nmetrics_names = best_model.metrics_names\nfor name, value in zip(metrics_names, results):\n    print(f\"{name:20}: {value:.4f}\")\n\n# сбор предсказаний для confusion matrix\nprint(\"\\n АНАЛИЗ ПРЕДСКАЗАНИЙ:\")\ny_true_all = []\ny_pred_all = []\ny_pred_probs_all = []\n\nprint(\"Сбор предсказаний...\")\nfor i in range(len(val_gen)):\n    images, labels = val_gen[i]\n    predictions = best_model.predict(images, verbose=0)\n    \n    y_true_all.extend(np.argmax(labels, axis=1))\n    y_pred_all.extend(np.argmax(predictions, axis=1))\n    y_pred_probs_all.extend(predictions)\n    \n    if (i + 1) % 5 == 0:\n        print(f\"  Обработано {i + 1}/{len(val_gen)} батчей\")\n\nprint(f\" Собрано {len(y_true_all)} предсказаний\")\n\n# Confusion Matrix\nprint(\"\\n CONFUSION MATRIX:\")\ncm = confusion_matrix(y_true_all, y_pred_all)\n\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=[f'Class {i}' for i in range(5)],\n            yticklabels=[f'Class {i}' for i in range(5)])\nplt.title('Confusion Matrix', fontsize=14, fontweight='bold')\nplt.ylabel('Истинный класс', fontsize=12)\nplt.xlabel('Предсказанный класс', fontsize=12)\nplt.tight_layout()\nplt.show()\n\n# Classification Report\nprint(\"\\n CLASSIFICATION REPORT:\")\n\nreport = classification_report(y_true_all, y_pred_all, \n                               target_names=[f'Class {i}' for i in range(5)],\n                               digits=4)\nprint(report)\n\n# визуализация примеров предсказаний\nprint(\"\\n ВИЗУАЛИЗАЦИЯ ПРЕДСКАЗАНИЙ:\")\n\n# берем примеры из валидационного набора\nsample_images, sample_labels = val_gen[0]\nsample_predictions = best_model.predict(sample_images, verbose=0)\n\n# выбираем 6 примеров для визуализации\nnum_samples = min(6, len(sample_images))\nfig, axes = plt.subplots(2, 3, figsize=(15, 10))\naxes = axes.ravel()\n\nfor i in range(num_samples):\n    ax = axes[i]\n    \n    # показываем изображение\n    ax.imshow(sample_images[i])\n    \n    # получаем истинный и предсказанный класс\n    true_class = np.argmax(sample_labels[i])\n    pred_class = np.argmax(sample_predictions[i])\n    pred_prob = np.max(sample_predictions[i])\n    \n    # определяем цвет текста (зеленый если правильно, красный если нет)\n    color = 'green' if true_class == pred_class else 'red'\n    \n    # добавляем информацию\n    ax.set_title(f'Истинный: {true_class} | Предсказанный: {pred_class}\\nВероятность: {pred_prob:.2%}', \n                 color=color, fontweight='bold')\n    ax.axis('off')\n    \n    # добавляем рамку соответствующего цвета\n    for spine in ax.spines.values():\n        spine.set_edgecolor(color)\n        spine.set_linewidth(3)\n\n# скрываем неиспользованные оси\nfor i in range(num_samples, len(axes)):\n    axes[i].axis('off')\n\nplt.suptitle('Примеры предсказаний модели', fontsize=16, fontweight='bold', y=1.02)\nplt.tight_layout()\nplt.show()\n\n# статистика предсказаний\nprint(\"\\n СТАТИСТИКА ПРЕДСКАЗАНИЙ:\")\n\naccuracy = np.mean(np.array(y_true_all) == np.array(y_pred_all))\nprint(f\"Общая точность: {accuracy:.2%}\")\n\n# точность по классам\nprint(\"\\nТочность по классам:\")\nfor class_id in range(5):\n    class_indices = np.where(np.array(y_true_all) == class_id)[0]\n    if len(class_indices) > 0:\n        class_accuracy = np.mean(np.array(y_pred_all)[class_indices] == class_id)\n        print(f\"  Класс {class_id}: {class_accuracy:.2%} ({len(class_indices)} примеров)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T13:12:44.447334Z","iopub.execute_input":"2025-12-08T13:12:44.447550Z","iopub.status.idle":"2025-12-08T13:13:09.266655Z","shell.execute_reply.started":"2025-12-08T13:12:44.447534Z","shell.execute_reply":"2025-12-08T13:13:09.265777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# заключение и рекомендации\nprint(\"\\n РЕЗУЛЬТАТЫ ОБУЧЕНИЯ:\")\nprint(\"-\"*40)\n\nif 'history' in locals():\n    final_accuracy = history.history['val_accuracy'][-1] if 'val_accuracy' in history.history else 0\n    final_loss = history.history['val_loss'][-1] if 'val_loss' in history.history else 0\n    \n    print(f\" Финальная точность на валидации: {final_accuracy:.2%}\")\n    print(f\" Финальные потери на валидации: {final_loss:.4f}\")\n    \n    if final_accuracy > 0.85:\n        print(\" Отличный результат! Модель хорошо обучилась.\")\n    elif final_accuracy > 0.70:\n        print(\" Хороший результат. Есть возможности для улучшения.\")\n    else:\n        print(\" Результат можно улучшить\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T13:13:09.267859Z","iopub.execute_input":"2025-12-08T13:13:09.268101Z","iopub.status.idle":"2025-12-08T13:13:09.273820Z","shell.execute_reply.started":"2025-12-08T13:13:09.268085Z","shell.execute_reply":"2025-12-08T13:13:09.273040Z"}},"outputs":[],"execution_count":null}]}