import pyperclip


def help():
    """Выводит справку о всех доступных методах."""
    help_message = "Справка по методам:\n"
    methods_info = {
        "mlp": "1. Модель перцептрона. Проблема линейно неразделимых множеств и ее решение. Логика построения многослойных ИНС. Линейные слои (Linear Layers) в PyTorch.",
        "activ_func": "2. Функции активации. Требования к функциям активации. Популярные функции активации. Популярные функции активации. Слои нелинейной активации в PyTorch. Пример использования нелинейной активации в модели",
        "dl_spring": "3. Вторая весна. Причины второй весны.",
        'grad': "4. Градиент функции многих переменных. Логика обучения нейронной сети. Методы вычисления градиента. Проблемы поиска градиента",
        'cross_val': "5. Кросс-валидация. Выборки train, validation, test. Проблема переобучения. Ранняя остановка.",
        'softmax': "6. Преобразование Softmax и функция потерь Cross Entropy Loss",
        'backprop': "7. Механизм обратного распространения ошибки (Backpropagation). Принципиальная логика основного цикла обучения нейронной сети в PyTorch. Слои функций потерь (Loss Functions) в PyTorch",
        'dynamic': "8. Дифференцируемое программирование и автоматическое дифференцирование в PyTorch",
        'sgd': "9. Стохастический градиентный спуск и батчи обучающей выборки",
        'adagrad': "10. Адаптивные методы градиентного спуска, метод импульсов и метод Нестерова",
        'weight_init': "11. Проблема инициализации весов при обучении ИНС. Инициализация Ксавье",
        'dropout': "12. Переобучение модели и регуляризация. Принцип Dropout. Слои регуляризации в PyTorch",
        'bn': "13. Мини-батчи, нормализация по мини-батчам и слои нормализации в PyTorch",
        'nnmodule': "14. Многослойные сети, граф потока вычислений и класс nn.Module в PyTorch",
        'cnn': "15. Машинное обучение на изображениях, принцип работы сверточных сетей и их преимущества",
        'conv_pool': "16. Архитектура многослойной ИНС для распознавания изображений, сверточные и сжимающие слои в PyTorch",
        'cnn_ex': "17. Приемы для глубокого обучения на небольших наборах изображений",
        'cnn_cls': "18. Схема работы сверточной сети, операции свертки, пуллинга и общий вид сети для классификации изображений",
        'kinds_rl': "19. Виды задач машинного обучения и постановка задачи обучения с подкреплением",
        'rl_policy': "20. Подходы к определению стратегии в RL, вознаграждение, дисконтированное вознаграждение, Q- и V-функции, уравнение Беллмана",
        'policy_grad': "21. Метод Policy Gradient, улучшения и модель Actor-Critic",
        'q_learning': "22. Определение метода Q-Learning. Обновление Q-функции. Алгоритм Q-Learning",
        'vgg16': "23. Типы задач машинного зрения, архитектура VGG16: преимущества и недостатки",
        'google_net': "24. Архитектура GoogLeNet, описание Inception module, свертка \(1 \times 1\), DepthConcat и сравнение с другими архитектурами",
        'resnet': "25. Архитектуры с residual connections, основные принципы, нарушение симметрии, сравнение с другими архитектурами",
        'unet': "26. Архитектура U-Net: общие принципы, описание, residual connections и применение в задачах сегментации",
        'gradcam': "27. Механизмы внимания в машинном зрении и метод Grad-CAM",
        'vit_swin': "28. Применение Transformer в компьютерном зрении и архитектура Swin Transformer",
        'decode': "29. Векторная интерпретация весов скрытого слоя многослойного перцептрона и задача восстановления входного сигнала после сжатия",
        'autoencoder': "30. Автоэнкодеры: линейные, нелинейные, латентное пространство, интерполяция и экстраполяция",
        'discr_models': "31. Дискриминативные и генеративные модели: задачи, сравнение, классификация и применение",
        'gan': "32. Архитектура GAN: общая архитектура, обучение и пример с одномерной функцией распределения",
        'vae': "33. Вариационный автоэнкодер (VAE): цели, архитектура, байесовское моделирование и функция потерь",
        'diffusion': "35. Denoising Diffusion Models: принцип работы, прямой и обратный процессы",
        'mlp_reg': 'Регрессия полносвязная. Датасет airline_passengers',
        'mlp_reg_2': 'Регрессия полносвязная. Датасет gold',
        'mlp_reg_3': 'Регрессия полносвязная. Датасет movie_regression',
        'mlp_class': 'Классификация полносвязная. Датасет bank.csv',
        'cnn_class': 'Классификация CNN. Датасет chars',
        'cnn_class_2': 'Класификация CNN. Датасет sign_language',
        'cnn_class_3': 'Классификация CNN. Датасет flower_classification'
    }
    for method, description in methods_info.items():
        help_message += f"- {method}: {description}\n"
    pyperclip.copy(help_message)


def mlp(idx: int = 0):
    """1. Модель перцептрона. Проблема линейно неразделимых множеств и ее решение. Логика построения многослойных ИНС. Линейные слои (Linear Layers) в PyTorch."""
    if idx == 0:
        code = """
### 1. Модель перцептрона
Перцептрон — это одна из первых моделей искусственных нейронных сетей. Его основная идея заключается в следующем:
1. **Функция активации**: вычисляется взвешенная сумма входов и добавляется смещение (bias). Применяется нелинейная функция активации, например, ступенчатая функция (sign).
2. **Обучение**: используется алгоритм коррекции ошибок, обновляя веса в зависимости от ошибки на каждом шаге.

#### Формула:
$\[
y = f\left(\sum_{i=1}^n w_i x_i + b\right)
\]$
где \(w_i\) — веса, \(x_i\) — входы, \(b\) — смещение, а \(f\) — функция активации.

---

### 2. Проблема линейно неразделимых множеств
Перцептрон справляется только с **линейно разделимыми данными**. Например, он не может решить задачу XOR, так как классы нельзя разделить прямой линией в двумерном пространстве. Это ключевое ограничение классического перцептрона.

#### Решение проблемы:
- Добавление **скрытых слоев** с нелинейными функциями активации (например, ReLU, sigmoid). Это позволяет модели представлять более сложные зависимости между входами и выходами.
- Многослойные нейронные сети (MLP) обучаются с использованием **обратного распространения ошибки (backpropagation)**.

---

### 3. Логика построения многослойных ИНС
Многослойный перцептрон (MLP) состоит из:
1. **Входного слоя**: принимает данные.
2. **Скрытых слоев**: выполняют вычисления и преобразования для обучения нелинейным зависимостям.
3. **Выходного слоя**: возвращает результат классификации или регрессии.

Каждый слой преобразует входы в выходы с помощью линейного преобразования и нелинейной функции активации:
$\[
y = f(Wx + b)
\]$

---

### 4. Линейные слои (Linear Layers) в PyTorch
В PyTorch линейный слой реализуется с помощью класса `torch.nn.Linear`. Он выполняет операцию \(y = Wx + b\).
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

# Создаем модель
class SimplePerceptron(nn.Module):
    def __init__(self, input_size, output_size):
        super(SimplePerceptron, self).__init__()
        self.linear = nn.Linear(input_size, output_size)  # Линейный слой
    
    def forward(self, x):
        return self.linear(x)  # Линейное преобразование

# Создаем данные
X = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])  # XOR данные
y = torch.tensor([0, 1, 1, 0])  # Метки классов

# Параметры модели
input_size = 2
output_size = 1

# Инициализация модели
model = SimplePerceptron(input_size, output_size)

# Определяем функцию потерь и оптимизатор
criterion = nn.BCEWithLogitsLoss()  # Для бинарной классификации
optimizer = optim.SGD(model.parameters(), lr=0.1)

# Обучение
epochs = 1000
for epoch in range(epochs):
    # Прямой проход
    outputs = model(X)
    loss = criterion(outputs.squeeze(), y.float())
    
    # Обратное распространение
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
"""
    pyperclip.copy(code)


def activ_func(idx: int = 0):
    if idx == 0:
        code = """
### 1. Функции активации

Функции активации преобразуют линейное выходное значение нейронов в нелинейное, что позволяет модели обучаться сложным зависимостям. Они вносят нелинейность, без которой нейронная сеть была бы эквивалентна линейной регрессии, даже при наличии нескольких слоев.

---

### 2. Требования к функциям активации
1. **Нелинейность**: должна преобразовывать линейные комбинации входов в нелинейные значения.
2. **Дифференцируемость**: для обучения методом обратного распространения градиента.
3. **Диапазон значений**: ограничение диапазона может улучшить устойчивость сети.
4. **Неусложненный градиент**: для предотвращения проблемы исчезающих/взрывающихся градиентов.
5. **Распределение значений**: избежать ситуаций, когда большая часть нейронов становится неактивной.

---

### 3. Популярные функции активации
1. **ReLU (Rectified Linear Unit)**:
   - Формула: \( f(x) = \max(0, x) \)
   - Преимущества: простота, работает хорошо для глубоких сетей, избегает исчезающих градиентов.
   - Недостатки: "мертвые нейроны" (активация остаётся равной 0 для всех входов \(x < 0\)).

2. **Sigmoid**:
   - Формула: \( f(x) = \frac{1}{1 + e^{-x}} \)
   - Преимущества: ограничивает значения в диапазоне \([0, 1]\), подходит для вероятностных выходов.
   - Недостатки: проблема исчезающих градиентов.

3. **Tanh (гиперболический тангенс)**:
   - Формула: \( f(x) = \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} \)
   - Преимущества: значения в диапазоне \([-1, 1]\), центрирование около 0.
   - Недостатки: подвержен исчезающим градиентам.

4. **Leaky ReLU**:
   - Формула: \( f(x) = x, \text{ если } x > 0; \ \alpha x, \text{ если } x \leq 0 \)
   - Преимущества: решает проблему "мертвых нейронов".
   - Параметр \(\alpha\) обычно небольшой (\(0.01\)).

5. **Softmax**:
   - Формула: \( f_i(x) = \frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}} \)
   - Применяется в последнем слое для задач многоклассовой классификации, нормализуя выходные значения в вероятности.

6. **Swish**:
   - Формула: \( f(x) = x \cdot \text{sigmoid}(x) \)
   - Преимущества: плавная активация, улучшенная производительность для некоторых задач.
"""
    else:
        code = """
### 4. Слои нелинейной активации в PyTorch
В PyTorch функции активации реализуются в модуле `torch.nn` либо как простые функции (`torch.nn.functional`).

#### Примеры популярных функций активации:
import torch
import torch.nn as nn
import torch.nn.functional as F

# Примеры функций активации
x = torch.tensor([-1.0, 0.0, 1.0])

# ReLU
relu = nn.ReLU()
print("ReLU:", relu(x))

# Sigmoid
sigmoid = nn.Sigmoid()
print("Sigmoid:", sigmoid(x))

# Tanh
tanh = nn.Tanh()
print("Tanh:", tanh(x))

# Leaky ReLU
leaky_relu = nn.LeakyReLU(0.01)
print("Leaky ReLU:", leaky_relu(x))

# Softmax (для многоклассовой классификации)
softmax = nn.Softmax(dim=0)
print("Softmax:", softmax(x))

### 5. Пример использования нелинейной активации в модели
class NeuralNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(NeuralNet, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)  # Линейный слой
        self.relu = nn.ReLU()  # Нелинейная активация
        self.fc2 = nn.Linear(hidden_size, output_size)  # Линейный слой

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)  # Применяем ReLU
        x = self.fc2(x)
        return x

# Инициализация и прямой проход
model = NeuralNet(input_size=3, hidden_size=5, output_size=2)
sample_input = torch.randn(1, 3)
output = model(sample_input)
print("Output:", output)
"""
    pyperclip.copy(code)


def dl_spring(idx: int = 0):
    code = """
1.
"Вторая весна" искусственного интеллекта относится к периоду с 2010-х годов, когда глубокое обучение начало активно применяться в научных исследованиях, индустрии и решении реальных задач. Этот подъем был обусловлен успехами глубоких нейронных сетей в таких областях, как компьютерное зрение, обработка естественного языка (NLP), распознавание речи и другие
Ключевые этапы:
В 2006 году Джеффри Хинтон и его команда предложили глубокие сети с предварительным обучением (Deep Belief Networks).
Прорыв в компьютерном зрении произошел в 2012 году, когда архитектура AlexNet (Кризевский, Суцкевер, Хинтон) выиграла ImageNet с огромным отрывом.
Развитие других архитектур, таких как VGG, ResNet, Transformer, ускорило рост популярности глубокого обучения.

2. Причины «второй весны» глубокого обучения
1. Увеличение вычислительных мощностей
Появление графических процессоров (GPU) и их использование для обучения моделей глубокого обучения.
Развитие специализированных процессоров, таких как TPU (Tensor Processing Units).
2. Доступность больших объемов данных
Современные приложения (социальные сети, сенсоры, камеры) генерируют огромные объемы данных.
Развитие открытых наборов данных, таких как ImageNet, COCO, Wikipedia и других.
3. Улучшение алгоритмов и архитектур
Архитектуры, такие как Convolutional Neural Networks (CNN), Recurrent Neural Networks (RNN), Long Short-Term Memory (LSTM), Transformer.
Методы оптимизации, такие как Adam, RMSProp, SGD с импульсом, позволили улучшить сходимость и стабильность обучения.
4. Прогресс в обучении больших моделей
Методы регуляризации, такие как Dropout, Batch Normalization, позволили обучать глубокие модели без сильного переобучения.
Использование предварительного обучения и техники transfer learning.
5. Развитие программных библиотек
Популярные библиотеки, такие как TensorFlow, PyTorch, Keras, Theano, упростили разработку моделей глубокого обучения.
6. Привлечение внимания со стороны индустрии
Компании, такие как Google, Facebook, OpenAI, активно инвестировали в исследования и внедрение глубокого обучения.
Успешные коммерческие приложения, такие как голосовые помощники (Siri, Alexa), системы рекомендаций (Netflix, YouTube).
"""
    pyperclip.copy(code)


def grad(idx: int = 0):
    if idx == 0:
        code = """
Градиенты играют ключевую роль в обучении нейронных сетей, так как они используются для обновления параметров (весов) модели. Основная задача — найти направление и величину изменения весов, чтобы минимизировать функцию потерь.

---

### 1. Градиент функции многих переменных

Градиент функции \(f(x_1, x_2, \ldots, x_n)\) — это вектор частных производных по каждой переменной:
\[
\text{grad} \, f = \left[ \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \ldots, \frac{\partial f}{\partial x_n} \right]
\]

Градиент указывает направление наибольшего возрастания функции. Для минимизации функции (в нашем случае, функции потерь) веса обновляются в противоположном направлении градиента.

---

### 2. Логика обучения нейронной сети

1. **Прямой проход (forward pass)**:
   - Входные данные проходят через слои сети, и на каждом слое выполняются линейные преобразования и активации.
   - В конце вычисляется значение функции потерь \(L\).

2. **Обратное распространение ошибки (backpropagation)**:
   - Вычисляются градиенты функции потерь \(L\) по параметрам сети с использованием правила цепочки.
   - Параметры обновляются с учетом вычисленных градиентов.

3. **Обновление весов**:
   - Используется метод градиентного спуска или его варианты.

---

### 3. Методы вычисления градиента

#### 1. **Численный градиент**:
   - Использует приближение производной с помощью конечных разностей:
     \[
     \frac{\partial f}{\partial x} \approx \frac{f(x + \epsilon) - f(x - \epsilon)}{2\epsilon}
     \]
   - Преимущества: прост в реализации.
   - Недостатки: медленный и подвержен численным ошибкам.

#### 2. **Символьный градиент**:
   - Вычисляется аналитически с использованием символьной алгебры.
   - Преимущества: точное значение.
   - Недостатки: сложно применить к сложным функциям.

#### 3. **Автоматическое дифференцирование (autograd)**:
   - Использует граф вычислений для автоматического нахождения градиентов.
   - Применяется в библиотеках глубокого обучения, таких как PyTorch и TensorFlow.
   - Преимущества: вычисляет градиенты эффективно и точно.

---

### 4. Проблемы поиска градиента

1. **Исчезающие градиенты**:
   - В глубоких сетях градиенты на ранних слоях становятся очень малыми, что замедляет обучение.
   - Решение: использование функций активации, таких как ReLU, и методов, таких как Batch Normalization.

2. **Взрывающиеся градиенты**:
   - Градиенты становятся слишком большими, что приводит к нестабильности обучения.
   - Решение: использование градиентного клиппинга (gradient clipping).

3. **Численные ошибки**:
   - При работе с очень большими или малыми числами возникают ошибки точности.
   - Решение: корректное масштабирование данных и нормализация.
"""

    else:
        code = """
import torch

# Определяем функцию
x = torch.tensor(2.0, requires_grad=True)  # Переменная, для которой нужен градиент
y = x**2 + 3*x + 5  # Функция от x

# Прямой проход
print("Function value (y):", y.item())

# Обратное распространение
y.backward()  # Вычисление градиента
print("Gradient (dy/dx):", x.grad.item())

import torch
import torch.nn as nn

# Данные
X = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[2.0], [4.0], [6.0]])

# Модель
model = nn.Linear(1, 1)  # Линейная модель y = wx + b
criterion = nn.MSELoss()  # Функция потерь (MSE)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)  # Градиентный спуск

# Обучение
for epoch in range(100):
    # Прямой проход
    predictions = model(X)
    loss = criterion(predictions, y)
    
    # Обратное распространение
    optimizer.zero_grad()
    loss.backward()  # Градиенты вычислены
    optimizer.step()  # Обновление параметров
    
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')
"""

    pyperclip.copy(code)


def cross_val(idx: int = 0):
    if idx == 0:
        code = """
1. Кросс-валидация
Кросс-валидация — это метод оценки модели, который помогает уменьшить риск переобучения и получить более стабильные результаты. Идея заключается в разбиении данных на несколько частей (фолдов) и обучении модели на разных подмножествах данных.

Классический подход: 

k-fold кросс-валидация.
Данные делятся на 
k фолдов.
В каждом цикле один фолд используется для тестирования, а остальные — для обучения.
Итоговая метрика вычисляется как среднее значение метрик по всем фолдам.
Преимущества:
Оценка модели более надежна, особенно на малых наборах данных.
Уменьшение влияния случайного разбиения данных.


5. Кросс-валидация. Выборки train, validation, test. Проблема переобучения. Ранняя остановка.
1. Кросс-валидация
Кросс-валидация — это метод оценки модели, который помогает уменьшить риск переобучения и получить более стабильные результаты. Идея заключается в разбиении данных на несколько частей (фолдов) и обучении модели на разных подмножествах данных.

Классический подход: 
𝑘
k-fold кросс-валидация.
Данные делятся на 
𝑘
k фолдов.
В каждом цикле один фолд используется для тестирования, а остальные — для обучения.
Итоговая метрика вычисляется как среднее значение метрик по всем фолдам.
Преимущества:
Оценка модели более надежна, особенно на малых наборах данных.
Уменьшение влияния случайного разбиения данных.
Пример кода:
python
Copy
Edit
from sklearn.model_selection import KFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import numpy as np

# Данные
X = np.random.rand(100, 1)
y = 3 * X.squeeze() + np.random.rand(100)

kf = KFold(n_splits=5)
model = LinearRegression()

for train_index, val_index in kf.split(X):
    X_train, X_val = X[train_index], X[val_index]
    y_train, y_val = y[train_index], y[val_index]
    
    # Обучение модели
    model.fit(X_train, y_train)
    
    # Оценка
    predictions = model.predict(X_val)
    print("MSE:", mean_squared_error(y_val, predictions))

2. Выборки train, validation, test
Train (обучающая выборка): Используется для обучения модели. Модель минимизирует функцию потерь на этих данных.

Validation (валидационная выборка): Используется для оценки производительности модели во время обучения. Помогает подобрать гиперпараметры и избежать переобучения.

Test (тестовая выборка): Отдельный набор данных, используемый только для финальной оценки модели. Тестовая выборка не должна быть использована на этапе обучения или подбора параметров.

3. Проблема переобучения
Переобучение (overfitting) возникает, когда модель слишком хорошо подстраивается под обучающие данные, но плохо обобщает их на новые данные. Признаки переобучения:

Высокая точность на тренировочных данных.
Низкая точность на валидационных/тестовых данных.
Причины:
Слишком сложная модель (например, большое количество параметров).
Недостаток данных для обучения.
Долгое обучение.
Способы борьбы:
Добавление данных: увеличение объема обучающей выборки.
Регуляризация: методы, такие как L1/L2-регуляризация или Dropout.
Прерывание обучения: использование метода ранней остановки (early stopping).

4. Ранняя остановка (Early Stopping)
Ранняя остановка — это метод предотвращения переобучения. Обучение останавливается, если ошибка на валидационной выборке перестает уменьшаться в течение нескольких эпох.

Алгоритм:
Следите за метрикой на валидационной выборке (например, loss или accuracy).
Если метрика перестала улучшаться (пороговое количество эпох — patience), остановите обучение.
"""

    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

# Генерация данных
X = torch.randn(100, 10)
y = torch.randn(100, 1)

# Модель
model = nn.Sequential(
    nn.Linear(10, 50),
    nn.ReLU(),
    nn.Linear(50, 1)
)

criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# Early stopping
patience = 5
best_loss = float('inf')
trigger_times = 0

for epoch in range(100):
    # Прямой проход
    predictions = model(X)
    loss = criterion(predictions, y)
    
    # Проверка улучшения
    if loss.item() < best_loss:
        best_loss = loss.item()
        trigger_times = 0  # Сбрасываем счетчик
    else:
        trigger_times += 1
        if trigger_times >= patience:
            print(f"Early stopping at epoch {epoch}")
            break
    
    # Обратное распространение
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
"""

    pyperclip.copy(code)


def softmax(idx: int = 0):
    if idx == 0:
        code = """
### 6. Преобразование Softmax и функция потерь Cross Entropy Loss

---

### 1. **Преобразование Softmax**

**Softmax** — это функция, которая преобразует вектор произвольных действительных чисел в вероятностное распределение. Она часто используется в последнем слое нейронной сети для задач многоклассовой классификации.

#### Формула Softmax:
Для вектора входов \(z = [z_1, z_2, \ldots, z_n]\):
\[
\text{Softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^n e^{z_j}}
\]
где \(e^{z_i}\) — экспоненциальное преобразование, а знаменатель нормализует значения, чтобы их сумма была равна \(1\).

#### Свойства Softmax:
1. Значения находятся в диапазоне \([0, 1]\).
2. Сумма всех выходов равна \(1\), что делает их интерпретируемыми как вероятности.

---

### 2. **Функция потерь Cross Entropy Loss**

**Кросс-энтропия (Cross Entropy)** измеряет разницу между двумя вероятностными распределениями:
1. Истинным распределением меток классов (\(y\), представляется как one-hot encoding).
2. Распределением, предсказанным моделью (\(p\)).

#### Формула:
Для одного примера:
\[
\text{Loss} = -\sum_{i=1}^n y_i \log(p_i)
\]
где \(y_i\) — истинная метка (обычно \(1\) для правильного класса и \(0\) для остальных), а \(p_i\) — предсказанная вероятность для класса \(i\).

Если рассматривать истинную метку как \(y_c = 1\) только для правильного класса \(c\), то:
\[
\text{Loss} = -\log(p_c)
\]

---

### 3. **Softmax + Cross Entropy**

Часто Softmax и Cross Entropy объединяются в одну операцию:
1. На этапе вычисления потерь применяется логарифм вероятностей (после Softmax) для вычисления кросс-энтропии.
2. Это эффективно, поскольку объединение этих операций улучшает численную стабильность.

### 5. **Интерпретация результатов**

1. **Softmax**:
   Преобразует логиты (не нормализованные предсказания) в вероятности:
   \[
   \text{logits} = [2.0, 1.0, 0.1] \implies \text{probabilities} = [0.6590, 0.2424, 0.0986]
   \]

2. **Cross Entropy Loss**:
   Потеря для истинного класса \(0\) вычисляется как:
   \[
   \text{Loss} = -\log(0.6590) \approx 0.4161
   \]

"""

    else:
        code = """
#### Пример:
import torch
import torch.nn as nn

# Модельные предсказания (логиты, до Softmax)
logits = torch.tensor([[2.0, 1.0, 0.1]])  # Выходы модели для одного примера (3 класса)
labels = torch.tensor([0])  # Истинный класс: класс 0

# 1. Преобразование Softmax
softmax = nn.Softmax(dim=1)
probabilities = softmax(logits)
print("Probabilities:", probabilities)

# 2. Кросс-энтропийная функция потерь
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, labels)  # logits передаются напрямую
print("Cross Entropy Loss:", loss.item())

# Пример модели с CrossEntropyLoss
model = nn.Linear(10, 3)  # 10 входов, 3 класса
criterion = nn.CrossEntropyLoss()

# Входные данные
inputs = torch.randn(5, 10)  # 5 примеров, каждый с 10 признаками
labels = torch.tensor([0, 1, 2, 1, 0])  # Истинные метки классов

# Прямой проход
outputs = model(inputs)

# Вычисление потерь
loss = criterion(outputs, labels)
print("Loss:", loss.item())
"""

    pyperclip.copy(code)


def backprop(idx: int = 0):
    if idx == 0:
        code = """
### 1. **Механизм обратного распространения ошибки (Backpropagation)**

Обратное распространение ошибки — это метод, используемый для вычисления градиентов функции потерь относительно параметров нейронной сети. Этот механизм основан на **правиле цепочки производных**.

#### Этапы обратного распространения:
1. **Прямой проход (Forward Pass)**:
   - Данные проходят через сеть, вычисляются выходы каждого слоя и итоговая функция потерь.

2. **Вычисление ошибки**:
   - Сравнивается предсказание модели с истинными метками, и на основе этого вычисляется значение функции потерь.

3. **Обратный проход (Backward Pass)**:
   - Вычисляются градиенты функции потерь относительно параметров модели, используя правило цепочки. Это шаг backward propagation.

4. **Обновление параметров**:
   - Используя вычисленные градиенты, параметры сети обновляются с помощью методов оптимизации, таких как градиентный спуск.

---

### 2. **Принципиальная логика основного цикла обучения нейронной сети в PyTorch**

Цикл обучения модели в PyTorch состоит из следующих шагов:

1. **Инициализация модели, функции потерь и оптимизатора**.
2. **Цикл по эпохам**:
   - Прямой проход: предсказание на основе входных данных.
   - Вычисление функции потерь.
   - Обратное распространение: вычисление градиентов с помощью `loss.backward()`.
   - Обновление параметров с использованием оптимизатора (`optimizer.step()`).
3. **Оценка на валидационной выборке** (опционально).

#### Пример основного цикла:
```python
import torch
import torch.nn as nn
import torch.optim as optim

# Данные
X = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[2.0], [4.0], [6.0]])

# Модель
model = nn.Linear(1, 1)

# Функция потерь и оптимизатор
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# Обучение
epochs = 100
for epoch in range(epochs):
    # Прямой проход
    predictions = model(X)
    loss = criterion(predictions, y)

    # Обратное распространение
    optimizer.zero_grad()  # Обнуляем градиенты
    loss.backward()  # Вычисляем градиенты
    optimizer.step()  # Обновляем параметры

    # Вывод информации каждые 10 эпох
    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
```

---

### 3. **Слои функций потерь (Loss Functions) в PyTorch**

В PyTorch функции потерь предоставляются в модуле `torch.nn`. Они измеряют отклонение между предсказанием модели и истинными значениями.

#### Часто используемые функции потерь:

1. **MSELoss (Mean Squared Error)**:
   - Используется для задач регрессии.
   - Формула:
     \[
     \text{Loss} = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2
     \]
   ```python
   criterion = nn.MSELoss()
   ```

2. **CrossEntropyLoss**:
   - Используется для многоклассовой классификации.
   - Включает Softmax внутри.
   ```python
   criterion = nn.CrossEntropyLoss()
   ```

3. **BCELoss (Binary Cross-Entropy)**:
   - Используется для бинарной классификации.
   - Требует предварительного применения `Sigmoid`.
   ```python
   criterion = nn.BCELoss()
   ```

4. **BCEWithLogitsLoss**:
   - Комбинирует `Sigmoid` и `BCELoss` в одну функцию.
   ```python
   criterion = nn.BCEWithLogitsLoss()
   ```

5. **SmoothL1Loss**:
   - Применяется в задачах регрессии, где нужно быть менее чувствительным к выбросам.
   ```python
   criterion = nn.SmoothL1Loss()
   ```

---

### Ключевые моменты:
1. **Обратное распространение ошибки** вычисляет градиенты через правило цепочки.
2. Основной цикл обучения включает этапы прямого прохода, вычисления потерь, обратного прохода и обновления параметров.
3. **Функции потерь** в PyTorch (например, MSE, CrossEntropy) являются центральным элементом для оценки качества модели.
"""

    else:
        code = """
# Данные
inputs = torch.randn(3, 5)  # 3 примера, 5 классов
labels = torch.tensor([0, 2, 1])  # Истинные классы

# Модель
model = nn.Linear(5, 5)

# CrossEntropyLoss (для классификации)
criterion = nn.CrossEntropyLoss()

# Прямой проход
outputs = model(inputs)

# Вычисление потерь
loss = criterion(outputs, labels)
print("Cross Entropy Loss:", loss.item())
```

#### Пример: Задача регрессии
```python
# Данные
inputs = torch.tensor([[1.0], [2.0], [3.0]])
labels = torch.tensor([[2.0], [4.0], [6.0]])

# Модель
model = nn.Linear(1, 1)

# MSELoss (для регрессии)
criterion = nn.MSELoss()

# Прямой проход
outputs = model(inputs)

# Вычисление потерь
loss = criterion(outputs, labels)
print("MSE Loss:", loss.item())
"""

    pyperclip.copy(code)


def dynamic(idx: int = 0):
    if idx == 0:
        code = """
### 8. Дифференцируемое программирование и автоматическое дифференцирование в PyTorch

---

### 1. **Дифференцируемое программирование**

Дифференцируемое программирование — это подход, при котором компоненты программируемой системы (например, модели) можно дифференцировать с использованием автоматического дифференцирования. Этот подход используется для обучения нейронных сетей, где вычисления (включая функции потерь, активации и т. д.) представляются в виде графа вычислений.

#### Ключевые особенности:
- **Автоматическое вычисление градиентов**: производные вычисляются для каждого параметра в системе.
- **Гибкость в разработке моделей**: любые пользовательские функции, если они дифференцируемы, могут быть включены в граф вычислений.

---

### 2. **Обратное распространение ошибки**

Обратное распространение ошибки (backpropagation) — это реализация правила цепочки для вычисления производных сложных функций:
1. На **прямом проходе** вычисляется значение функции (например, потерь).
2. На **обратном проходе** градиенты выходных значений передаются назад через граф, чтобы вычислить производные всех параметров.

---

### 3. **Автоматическое дифференцирование в PyTorch**

PyTorch предоставляет мощный механизм автоматического дифференцирования через модуль **`torch.autograd`**. Он строит граф вычислений динамически, то есть на основе текущего выполнения кода.

#### Принципы работы:
1. **Обратный граф**:
   - PyTorch автоматически строит граф вычислений, связывая операции над тензорами.
2. **`requires_grad`**:
   - Если тензор создан с `requires_grad=True`, PyTorch будет отслеживать операции с этим тензором.
3. **`backward()`**:
   - Вызывает вычисление градиентов для всех параметров, участвующих в графе.

---

### 4. **Пример автоматического дифференцирования**

#### Пример 1: Вычисление градиента вручную
```python
import torch

# Создаем тензор с включенным автоматическим дифференцированием
x = torch.tensor(2.0, requires_grad=True)

# Вычисляем функцию
y = x**2 + 3*x + 5  # y = x^2 + 3x + 5

# Вычисляем градиент
y.backward()  # Автоматическое дифференцирование
print(f"Gradient (dy/dx): {x.grad}")  # dy/dx = 2x + 3
```

Вывод:
```
Gradient (dy/dx): 7.0
```
---

### 5. **Применение в обучении ИНС**

#### Ключевые этапы:
1. **Прямой проход**:
   - Входные данные проходят через нейронную сеть.
   - Выход сети сравнивается с истинными метками с использованием функции потерь.

2. **Обратное распространение**:
   - Градиенты функции потерь вычисляются для каждого параметра сети.

3. **Обновление параметров**:
   - Градиенты используются для обновления параметров с помощью оптимизаторов, таких как SGD или Adam.

---

### 6. **Преимущества автоматического дифференцирования в PyTorch**
1. **Динамическое построение графа**:
   - Граф вычислений строится "на лету" и адаптируется к структуре кода.

2. **Гибкость**:
   - Легко изменять архитектуру моделей и использовать пользовательские функции.

3. **Численная точность**:
   - Избегает проблем, связанных с ручным вычислением градиентов.

---

### Вывод
- **Дифференцируемое программирование** позволяет эффективно обучать нейронные сети.
- PyTorch предоставляет мощный инструмент для автоматического дифференцирования через `autograd`.
- Основные этапы обучения — это прямой проход, обратное распространение и обновление параметров.
"""

    else:
        code = """ 
#### Пример сложной сети с PyTorch:
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(2, 10)  # Вход -> скрытый слой
        self.relu = nn.ReLU()       # Активация
        self.fc2 = nn.Linear(10, 1)  # Скрытый слой -> выход

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# Данные
inputs = torch.tensor([[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]])
targets = torch.tensor([[1.0], [2.0], [3.0]])

# Модель, функция потерь и оптимизатор
model = SimpleNet()
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# Обучение
for epoch in range(50):
    predictions = model(inputs)
    loss = criterion(predictions, targets)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/50], Loss: {loss.item():.4f}")
"""

    pyperclip.copy(code)


def sgd(idx: int = 0):
    if idx == 0:
        code = """
### 9. Стохастический градиентный спуск и батчи обучающей выборки

---

### 1. **Стохастический градиентный спуск (SGD)**

**Градиентный спуск** — это алгоритм оптимизации, который используется для минимизации функции потерь путём изменения параметров модели в направлении её градиента.

#### Варианты градиентного спуска:
1. **Пакетный градиентный спуск (Batch Gradient Descent)**:
   - Использует весь обучающий набор данных для вычисления градиентов.
   - Преимущества: точные градиенты.
   - Недостатки: медленный, особенно на больших данных.

2. **Стохастический градиентный спуск (Stochastic Gradient Descent, SGD)**:
   - Использует только **один случайный пример** на каждой итерации для вычисления градиента.
   - Преимущества: быстрее обновляет параметры, лучше избегает локальных минимумов.
   - Недостатки: шумные обновления, возможны колебания.

3. **Мини-батч градиентный спуск (Mini-batch Gradient Descent)**:
   - Использует **маленькие подмножества данных (батчи)** для вычисления градиентов.
   - Преимущества: компромисс между точностью и скоростью.

---

### 2. **Батчи обучающей выборки**

#### Что такое батч?
- Батч (mini-batch) — это подмножество обучающей выборки, которое используется для одной итерации обновления параметров.
- Размер батча (\(B\)) определяет количество примеров, используемых для вычисления градиентов.

#### Типичные размеры батча:
- **Маленькие батчи** (\(B = 32\), \(B = 64\)): позволяют быстрее обновлять параметры, но менее точные.
- **Большие батчи** (\(B = 256\), \(B = 512\)): обеспечивают более точные градиенты, но требуют больше памяти и могут медленнее сходиться.

---

### 3. **Этапы обучения с использованием батчей**

1. **Разбиение данных**:
   - Обучающая выборка разбивается на батчи заданного размера.
2. **Обновление параметров**:
   - Для каждого батча выполняется прямой проход, вычисляются потери и градиенты.
   - Параметры обновляются на основе среднего значения градиентов по батчу.

---

### 4. **Алгоритм SGD с мини-батчами**

1. Перемешайте обучающие данные (shuffling).
2. Разбейте данные на батчи.
3. Для каждого батча:
   - Выполните прямой проход.
   - Вычислите функцию потерь.
   - Выполните обратное распространение ошибки.
   - Обновите параметры.

---

### 6. **Преимущества использования батчей**

1. **Компромисс между точностью и скоростью**:
   - Мини-батчи обеспечивают более точные градиенты, чем SGD, и быстрее, чем пакетный метод.
2. **Эффективность для GPU**:
   - Обработка батчей позволяет более эффективно использовать параллельные вычисления на GPU.
3. **Стабильность обучения**:
   - Шум в оценке градиента сглаживается, что улучшает сходимость.

---

### Выводы:
- Стохастический градиентный спуск — эффективный метод оптимизации, который хорошо работает с большими данными.
- Использование батчей помогает сбалансировать точность и производительность.
- PyTorch предоставляет удобные инструменты (`DataLoader`) для работы с батчами, что упрощает реализацию SGD.
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# Данные
X = torch.randn(100, 1)  # 100 примеров, 1 признак
y = 3 * X + torch.randn(100, 1) * 0.5  # Истинная функция с шумом

# Датасет и загрузчик данных
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=10, shuffle=True)  # Батч размером 10

# Модель
model = nn.Linear(1, 1)

# Функция потерь и оптимизатор
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# Обучение
epochs = 50
for epoch in range(epochs):
    for batch_X, batch_y in dataloader:
        # Прямой проход
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)
        
        # Обратное распространение
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
"""

    pyperclip.copy(code)


def adagrad(idx: int = 0):
    if idx == 0:
        code = """
### 10. Адаптивные методы градиентного спуска, метод импульсов и метод Нестерова

---

### 1. **Адаптивные методы градиентного спуска**

Адаптивные методы градиентного спуска автоматически регулируют скорость обучения (learning rate) на основе изменений в градиентах. Это позволяет ускорить сходимость и улучшить обучение в сложных задачах.

#### Основные адаптивные методы:
1. **Adagrad**:
   - Регулирует скорость обучения для каждого параметра, уменьшая шаги для часто обновляемых параметров и увеличивая для редко обновляемых.
   - Формула обновления:
     \[
     \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t
     \]
     где \(G_t\) — накопленные квадраты градиентов.

2. **RMSProp**:
   - Модификация Adagrad, которая учитывает только недавние градиенты вместо всех предыдущих.
   - Формула обновления:
     \[
     G_t = \rho G_{t-1} + (1 - \rho) g_t^2
     \]
     \[
     \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t
     \]

3. **Adam (Adaptive Moment Estimation)**:
   - Комбинирует идеи Adagrad и RMSProp, включая моменты первого (\(m_t\)) и второго порядка (\(v_t\)).
   - Формулы:
     \[
     m_t = \beta_1 m_{t-1} + (1 - \beta_1) g_t
     \]
     \[
     v_t = \beta_2 v_{t-1} + (1 - \beta_2) g_t^2
     \]
     \[
     \hat{m_t} = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v_t} = \frac{v_t}{1 - \beta_2^t}
     \]
     \[
     \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{\hat{v_t}} + \epsilon} \cdot \hat{m_t}
     \]

---

### 2. **Метод импульсов (Momentum)**

Метод импульсов добавляет накопленный момент к обновлению параметров, чтобы ускорить сходимость, особенно на сильно вытянутых поверхностях функции потерь.

#### Идея:
- Градиенты из прошлых шагов учитываются для определения текущего направления движения.
- Это помогает избежать локальных минимумов и улучшает сходимость.

#### Формулы:
1. Обновление импульса:
   \[
   v_t = \gamma v_{t-1} + \eta g_t
   \]
   где \(v_t\) — скорость (momentum), \(\gamma\) — коэффициент импульса (обычно \(\gamma \in [0.9, 0.99]\)).

2. Обновление параметров:
   \[
   \theta_t = \theta_{t-1} - v_t
   \]

#### Преимущества:
- Ускоряет сходимость, особенно на вытянутых поверхностях.
- Уменьшает вероятность застревания в локальных минимумах.

---

### 3. **Метод Нестерова (Nesterov Accelerated Gradient, NAG)**

Метод Нестерова улучшает метод импульсов, предсказывая положение параметров в будущем и вычисляя градиент не в текущей точке, а в точке, скорректированной с учетом импульса.

#### Формулы:
1. Вычисление "предсказанной" позиции:
   \[
   \theta_{\text{lookahead}} = \theta_{t-1} - \gamma v_{t-1}
   \]

2. Градиент в новой точке:
   \[
   v_t = \gamma v_{t-1} + \eta g(\theta_{\text{lookahead}})
   \]

3. Обновление параметров:
   \[
   \theta_t = \theta_{t-1} - v_t
   \]

#### Преимущества:
- Более точная оценка направления градиента.
- Улучшает сходимость за счёт дополнительной информации о будущем состоянии параметров.

---

### 5. **Сравнение методов**

| Метод              | Преимущества                                      | Недостатки                                   |
|---------------------|---------------------------------------------------|---------------------------------------------|
| **SGD**            | Простота, подходит для больших данных             | Медленная сходимость                        |
| **Momentum**        | Ускоряет сходимость, уменьшает колебания          | Требует настройки гиперпараметров (\(\gamma\)) |
| **Nesterov**        | Более точное направление градиента                | Чуть более сложная реализация               |
| **Adagrad**         | Хорошо для разреженных данных                     | Слишком малый шаг обучения со временем      |
| **RMSProp**         | Подходит для задач с шумными градиентами          | Не учитывает моменты первого порядка        |
| **Adam**            | Быстрая сходимость, подходит для сложных задач    | Может переобучаться, требует настройки      |

---

### Выводы:

- **Momentum** ускоряет SGD, помогая преодолевать локальные минимумы.
- **Метод Нестерова** предсказывает направление градиента, улучшая эффективность.
- **Адаптивные методы**, такие как Adam, предоставляют мощные инструменты для обучения сложных моделей, автоматически регулируя шаг обучения.    
"""

    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

# Данные
X = torch.randn(100, 1)
y = 3 * X + torch.randn(100, 1) * 0.5

# Модель
model = nn.Linear(1, 1)

# Функция потерь
criterion = nn.MSELoss()

# Оптимизаторы
adam_optimizer = optim.Adam(model.parameters(), lr=0.01)
momentum_optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)

# Выбор Adam для примера
optimizer = adam_optimizer

# Обучение
epochs = 100
for epoch in range(epochs):
    predictions = model(X)
    loss = criterion(predictions, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
"""
    pyperclip.copy(code)


def weight_init(idx: int = 0):
    if idx == 0:
        code = """
### 11. Проблема инициализации весов при обучении ИНС. Инициализация Ксавье

---

### 1. **Проблема инициализации весов при обучении нейронных сетей**

Инициализация весов в нейронной сети играет важную роль, так как плохая начальная настройка может привести к проблемам в обучении.

#### Основные проблемы:
1. **Затухание градиентов**:
   - Если начальные веса слишком малы, то сигналы, передаваемые через сеть, становятся очень малыми.
   - Это приводит к тому, что градиенты в процессе обратного распространения также затухают.

2. **Взрывающиеся градиенты**:
   - Если начальные веса слишком большие, то сигналы, передаваемые через сеть, становятся слишком большими.
   - Это приводит к нестабильным градиентам, из-за чего обучение становится невозможным.

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

---

### 2. **Решение: Инициализация Ксавье (Xavier Initialization)**

Инициализация Ксавье была предложена в работе "Understanding the difficulty of training deep feedforward neural networks" (2010). Цель — сбалансировать дисперсию входов и выходов на каждом уровне нейронной сети.

#### Идея:
- Весовые значения инициализируются так, чтобы дисперсия выходных значений каждого нейрона была примерно одинаковой.
- Это предотвращает затухание или взрыв сигналов.

#### Формула:
- Для равномерного распределения:
  \[
  W \sim U\left[-\frac{\sqrt{6}}{\sqrt{n_{\text{in}} + n_{\text{out}}}}, \frac{\sqrt{6}}{\sqrt{n_{\text{in}} + n_{\text{out}}}}\right]
  \]
- Для нормального распределения:
  \[
  W \sim \mathcal{N}\left(0, \frac{2}{n_{\text{in}} + n_{\text{out}}}\right)
  \]
где \(n_{\text{in}}\) — число входов нейрона, \(n_{\text{out}}\) — число выходов.

---

### 3. **Преимущества инициализации Ксавье**
1. Ускоряет сходимость обучения.
2. Уменьшает вероятность затухания или взрыва градиентов.
3. Работает хорошо для функций активации, симметричных относительно 0 (например, sigmoid, tanh).

---

### 4. **Инициализация весов в PyTorch**

PyTorch предоставляет методы для инициализации весов, включая инициализацию Ксавье, через модуль `torch.nn.init`.

---

### 5. **Когда использовать инициализацию Ксавье**

- **Рекомендуется для функций активации sigmoid или tanh**, поскольку эти функции имеют симметричные диапазоны, и Ксавье помогает поддерживать сигналы в оптимальном диапазоне.
- Для **ReLU** и её модификаций лучше подходит **инициализация He**, так как она учитывает только активные нейроны.

### Выводы

- Инициализация весов важна для стабильного и быстрого обучения нейронной сети.
- Инициализация Ксавье эффективна для функций активации, симметричных относительно нуля.
- PyTorch предоставляет встроенные инструменты для применения инициализации, что упрощает реализацию.        
"""
    else:
        code = """
#### Пример:
import torch
import torch.nn as nn
import torch.nn.init as init

# Модель
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(10, 50)  # Линейный слой
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# Создание модели
model = SimpleNet()

# Инициализация весов с помощью Ксавье
def initialize_weights(m):
    if isinstance(m, nn.Linear):
        init.xavier_uniform_(m.weight)  # Ксавье для равномерного распределения
        init.zeros_(m.bias)             # Сброс смещения в ноль

# Применение инициализации к модели
model.apply(initialize_weights)

# Данные
X = torch.randn(100, 10)  # 100 примеров, 10 признаков
y = torch.randint(0, 2, (100,))  # Бинарные метки (0 или 1)

# Модель
model = SimpleNet()

# Инициализация весов
model.apply(initialize_weights)

# Функция потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# Обучение
epochs = 50
for epoch in range(epochs):
    # Прямой проход
    predictions = model(X)
    loss = criterion(predictions, y)
    
    # Обратное распространение
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
"""

    pyperclip.copy(code)


def dropout(idx: int = 0):
    if idx == 0:
        code = """
### 12. Переобучение модели и регуляризация. Принцип Dropout. Слои регуляризации в PyTorch

---

### 1. **Проблема переобучения модели**

**Переобучение (overfitting)** возникает, когда модель хорошо подстраивается под обучающие данные, включая шум и случайные зависимости, но плохо обобщает их на новые данные.

#### Основные признаки переобучения:
1. Высокая точность на обучающей выборке.
2. Низкая точность на валидационной/тестовой выборке.

#### Причины переобучения:
1. Недостаток данных для обучения.
2. Слишком сложная модель с большим количеством параметров.
3. Длительное обучение без подходящей регуляризации.

---

### 2. **Регуляризация**

**Регуляризация** — это методы, уменьшающие вероятность переобучения за счёт ограничения сложности модели или внесения шума.

#### Основные методы:
1. **L1/L2-регуляризация**:
   - Добавляет штраф к функции потерь, пропорциональный значениям весов.
   - L1-регуляризация (Lasso): \(\lambda \sum |w_i|\).
   - L2-регуляризация (Ridge): \(\lambda \sum w_i^2\).
   
2. **Dropout**:
   - Исключает случайные нейроны из обучения на каждой итерации.

3. **Раннее прекращение обучения (Early Stopping)**:
   - Останавливает обучение, если ошибка на валидационной выборке перестаёт уменьшаться.

---

### 3. **Принцип механизма Dropout**

Dropout — это техника регуляризации, предложенная в 2014 году. Основная идея:
- На каждой итерации случайно "отключать" определённую долю нейронов.
- Это предотвращает зависимость одного нейрона от другого и заставляет сеть обучаться более устойчивым представлениям.

#### Механизм работы:
1. На этапе обучения:
   - С вероятностью \(p\) каждый нейрон временно исключается из сети.
   - Это приводит к более "шумному" процессу обучения, улучшая обобщение.
2. На этапе тестирования:
   - Dropout отключается, и сеть использует все нейроны.
   - Выходы нейронов масштабируются, чтобы учесть использование всех нейронов.

#### Формула:
Для нейрона \(h_i\) с включённым Dropout:
\[
h_i^{\text{dropout}} = \begin{cases} 
0, & \text{если отключён} \\
\frac{h_i}{1-p}, & \text{если включён (на этапе обучения)} 
\end{cases}
\]

---

### 4. **Слои Dropout в PyTorch**

В PyTorch Dropout реализуется как слой `torch.nn.Dropout`.

#### Пример использования Dropout:
```python
import torch
import torch.nn as nn

# Модель с использованием Dropout
class DropoutNet(nn.Module):
    def __init__(self):
        super(DropoutNet, self).__init__()
        self.fc1 = nn.Linear(10, 50)
        self.dropout = nn.Dropout(p=0.5)  # Вероятность отключения 50%
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)  # Dropout применяется только на этапе обучения
        x = self.fc2(x)
        return x

# Создание модели
model = DropoutNet()
```
---

### 6. **Ключевые моменты о Dropout**

1. **Dropout**:
   - Простая, но мощная техника регуляризации.
   - Эффективно предотвращает переобучение, особенно в глубоких нейронных сетях.

2. **Параметры Dropout**:
   - Значение \(p\) обычно выбирается в диапазоне \(0.2 \leq p \leq 0.5\).
   - Для выходного слоя часто используется меньшее значение \(p\).

3. **Сочетание с другими методами**:
   - Dropout может комбинироваться с L1/L2-регуляризацией и ранней остановкой для улучшения обобщающей способности.

---

### Выводы

- Dropout — это простой и эффективный метод регуляризации, который улучшает обобщающую способность модели.
- PyTorch предоставляет удобный слой Dropout, который легко интегрировать в нейронные сети.
- Совмещение Dropout с другими методами регуляризации (например, L2-регуляризацией) помогает бороться с переобучением.    
"""

    else:
        code = """
### 5. **Пример обучения модели с Dropout**

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# Данные
X = torch.randn(100, 10)  # 100 примеров, 10 признаков
y = torch.randint(0, 2, (100,))  # Бинарные метки

# Датасет и загрузчик данных
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=10, shuffle=True)

# Модель
model = DropoutNet()

# Функция потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# Обучение
epochs = 50
for epoch in range(epochs):
    model.train()  # Включаем режим обучения (Dropout активен)
    for batch_X, batch_y in dataloader:
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    # Оценка модели
    model.eval()  # Отключаем Dropout
    with torch.no_grad():
        val_loss = criterion(model(X), y)
    print(f"Epoch [{epoch+1}/{epochs}], Training Loss: {loss.item():.4f}, Validation Loss: {val_loss.item():.4f}")
"""

    pyperclip.copy(code)


def bn(idx: int = 0):
    if idx == 0:
        code = """
### 13. Мини-батчи, нормализация по мини-батчам и слои нормализации в PyTorch

---

### 1. **Мини-батчи — причина использования**

Мини-батч (mini-batch) — это подмножество обучающих данных, которое используется на одной итерации обучения нейронной сети.

#### Причины использования мини-батчей:
1. **Ускорение обучения**:
   - Использование батчей позволяет эффективно задействовать GPU для параллельной обработки данных.
   - Полный градиент на всей выборке (batch gradient descent) медленный, а градиенты на одном примере (stochastic gradient descent) шумные.

2. **Компромисс между скоростью и точностью**:
   - Мини-батчи обеспечивают баланс между скоростью обновления параметров и стабильностью градиента.

3. **Меньшее использование памяти**:
   - Полный градиентный спуск требует загрузки всей выборки в память, что невозможно для больших данных.
   - Батчи снижают требования к памяти, обрабатывая данные частями.

4. **Сглаживание градиентов**:
   - Средний градиент по мини-батчу помогает сгладить шумы, что делает обучение более устойчивым.

---

### 2. **Нормализация по мини-батчам (Batch Normalization)**

**Batch Normalization (BatchNorm)** — это техника, которая стабилизирует и ускоряет обучение нейронных сетей, нормализуя входы каждого слоя.

#### Идея:
- Для каждого нейрона в слое нормализовать выходы по статистике внутри мини-батча (среднее и стандартное отклонение).

#### Формулы:
Для входа \(x_i\) нейрона:
1. Вычисляем среднее и стандартное отклонение по мини-батчу:
   \[
   \mu = \frac{1}{m} \sum_{i=1}^m x_i, \quad \sigma^2 = \frac{1}{m} \sum_{i=1}^m (x_i - \mu)^2
   \]
   где \(m\) — размер батча.

2. Нормализация:
   \[
   \hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}}
   \]
   где \(\epsilon\) — небольшое число для предотвращения деления на 0.

3. Линейное преобразование:
   \[
   y_i = \gamma \hat{x}_i + \beta
   \]
   где \(\gamma\) и \(\beta\) — обучаемые параметры.

---

### 3. **Преимущества Batch Normalization**
1. **Стабилизирует градиенты**:
   - Предотвращает затухание или взрыв градиентов, особенно в глубоких сетях.
   
2. **Ускоряет сходимость**:
   - Нормализация позволяет использовать большие значения скорости обучения.

3. **Регуляризация**:
   - Вносит небольшой шум из-за зависимости от мини-батча, что помогает предотвратить переобучение.

4. **Уменьшает чувствительность к инициализации весов**:
   - Упрощает выбор начальных параметров модели.

---

### 4. **Слои нормализации в PyTorch**

PyTorch предоставляет готовые реализации Batch Normalization, а также других видов нормализации.

#### 1. Batch Normalization
- Используется для нормализации данных внутри слоев нейронной сети.
- Реализован как `nn.BatchNorm1d`, `nn.BatchNorm2d`, и `nn.BatchNorm3d` для данных с разными размерами.

Пример для данных с двумя измерениями:
```python
import torch
import torch.nn as nn

# Пример данных: мини-батч с 3 примерами, 5 признаков
x = torch.randn(3, 5)

# Batch Normalization
batch_norm = nn.BatchNorm1d(5)  # Нормализация по 5 признакам
output = batch_norm(x)

print("Input:", x)
print("Normalized Output:", output)
```

---

#### 2. Layer Normalization
- Нормализация выполняется по всем признакам каждого примера, а не по мини-батчу.
- Реализовано как `nn.LayerNorm`.

Пример:
```python
layer_norm = nn.LayerNorm(5)  # Нормализация по 5 признакам
output = layer_norm(x)

print("Layer Normalized Output:", output)
```

---

#### 3. Instance Normalization
- Применяется к изображениям или временным рядам, нормализуя данные отдельно для каждого примера.
- Реализовано как `nn.InstanceNorm2d`.

### 6. **Когда использовать Batch Normalization?**

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

---

### Выводы

1. **Мини-батчи** помогают сбалансировать точность градиента и скорость обучения, а также уменьшают использование памяти.
2. **Batch Normalization** ускоряет обучение, улучшает стабильность и помогает бороться с затухающими градиентами.
3. PyTorch предоставляет удобные слои для нормализации (`BatchNorm`, `LayerNorm`), которые легко интегрировать в модель.
"""
    else:
        code = """
class NetWithBatchNorm(nn.Module):
    def __init__(self):
        super(NetWithBatchNorm, self).__init__()
        self.fc1 = nn.Linear(10, 50)
        self.bn1 = nn.BatchNorm1d(50)  # BatchNorm для 50 нейронов
        self.fc2 = nn.Linear(50, 10)
        self.bn2 = nn.BatchNorm1d(10)  # BatchNorm для 10 нейронов

    def forward(self, x):
        x = torch.relu(self.bn1(self.fc1(x)))  # BatchNorm после слоя
        x = self.bn2(self.fc2(x))
        return x

# Инициализация модели
model = NetWithBatchNorm()

# Пример данных
inputs = torch.randn(32, 10)  # Мини-батч из 32 примеров, каждый с 10 признаками
outputs = model(inputs)

print("Output shape:", outputs.shape)
"""

    pyperclip.copy(code)


def nnmodule(idx: int = 0):
    code = """
### 14. Многослойные сети, граф потока вычислений и класс `nn.Module` в PyTorch

---

### 1. **Многослойные нейронные сети**

Многослойные нейронные сети (Multilayer Perceptrons, MLP) состоят из нескольких слоев:
1. **Входной слой**: принимает входные данные.
2. **Скрытые слои**: преобразуют данные, обучая сложные зависимости.
3. **Выходной слой**: возвращает результат классификации или регрессии.

Каждый слой состоит из:
- Линейного преобразования (\(Wx + b\)).
- Нелинейной функции активации (например, ReLU, Tanh).

---

### 2. **Граф потока вычислений**

Граф потока вычислений представляет собой структуру, где узлы — это операции, а рёбра — это данные (тензоры), передаваемые между операциями.

#### Характеристики:
1. **Динамическое построение**:
   - В PyTorch граф вычислений создаётся во время выполнения программы (динамический граф).
   - Это делает PyTorch гибким и удобным для работы с моделями.

2. **Использование графа**:
   - **Прямой проход (Forward Pass)**: вычисления от входных данных до выхода сети.
   - **Обратное распространение (Backward Pass)**: вычисление градиентов с помощью автоматического дифференцирования.

---

### 3. **Класс `nn.Module` в PyTorch**

`nn.Module` — это базовый класс для построения нейронных сетей в PyTorch. Он предоставляет структуру для определения и управления слоями, параметрами и вычислениями модели.

#### Назначение:
- Упрощает создание моделей.
- Управляет слоями и параметрами модели.
- Обеспечивает методы для прямого прохода, обучения и сохранения модели.

---

### 4. **Основные поля и методы класса `nn.Module`**

#### Поля:
1. **`parameters`**:
   - Содержит параметры (веса и смещения) модели.
   - Например: веса линейного слоя, обучаемые параметры BatchNorm.

2. **`modules`**:
   - Список вложенных модулей (слоев).
   - Позволяет рекурсивно организовывать сложные архитектуры.

#### Методы:
1. **`__init__()`**:
   - Конструктор для определения слоёв и параметров модели.

2. **`forward()`**:
   - Определяет прямой проход модели.

3. **`parameters()`**:
   - Возвращает итератор всех параметров модели.

4. **`train()` и `eval()`**:
   - Переключают модель между режимами обучения и оценки (включают/выключают Dropout, BatchNorm и т. д.).

5. **`state_dict()`**:
   - Возвращает словарь параметров и буферов модели.

6. **`load_state_dict()`**:
   - Загружает параметры модели из словаря.

---

### 5. **Пример использования `nn.Module`**

#### Создание многослойной модели:
```python
import torch
import torch.nn as nn

class MultiLayerNet(nn.Module):
    def __init__(self):
        super(MultiLayerNet, self).__init__()
        # Определяем слои
        self.fc1 = nn.Linear(10, 50)  # Линейный слой: 10 -> 50
        self.relu = nn.ReLU()         # Функция активации
        self.fc2 = nn.Linear(50, 10)  # Линейный слой: 50 -> 10

    def forward(self, x):
        # Прямой проход
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# Создание модели
model = MultiLayerNet()

# Проверка структуры
print(model)
```

Вывод:
```
MultiLayerNet(
  (fc1): Linear(in_features=10, out_features=50, bias=True)
  (relu): ReLU()
  (fc2): Linear(in_features=50, out_features=10, bias=True)
)
```

---

#### Работа с параметрами:
```python
# Параметры модели
for name, param in model.named_parameters():
    print(f"{name}: {param.shape}")

# Вывод:
# fc1.weight: torch.Size([50, 10])
# fc1.bias: torch.Size([50])
# fc2.weight: torch.Size([10, 50])
# fc2.bias: torch.Size([10])
```

---

#### Сохранение и загрузка модели:
```python
# Сохранение параметров
torch.save(model.state_dict(), "model.pth")

# Загрузка параметров
model.load_state_dict(torch.load("model.pth"))
```

---

#### Пример обучения:
```python
# Данные
X = torch.randn(100, 10)
y = torch.randint(0, 10, (100,))

# Функция потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# Обучение
epochs = 50
for epoch in range(epochs):
    predictions = model(X)  # Прямой проход
    loss = criterion(predictions, y)  # Вычисление функции потерь
    
    optimizer.zero_grad()  # Обнуляем градиенты
    loss.backward()  # Обратное распространение
    optimizer.step()  # Обновление параметров
    
    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
```

---

### Выводы:

1. **Многослойные сети** позволяют моделировать сложные зависимости в данных, используя комбинацию линейных преобразований и нелинейных активаций.
2. **Граф потока вычислений** в PyTorch динамический, что делает библиотеку гибкой для построения моделей.
3. **Класс `nn.Module`** — основа для создания нейронных сетей в PyTorch, предоставляющая удобные инструменты для управления слоями, параметрами и обучением.
"""
    pyperclip.copy(code)


def cnn(idx: int = 0):
    if idx == 0:
        code = """
### 15. Машинное обучение на изображениях, принцип работы сверточных сетей и их преимущества

---

### 1. **Специфика задач машинного обучения на изображениях**

Работа с изображениями в задачах машинного обучения имеет свои особенности:

#### Особенности изображений:
1. **Высокая размерность данных**:
   - Изображения часто представляются в виде больших массивов пикселей (например, \(224 \times 224 \times 3\) для цветного изображения), что требует значительных вычислительных ресурсов.
   
2. **Локальная структура**:
   - Близкие пиксели связаны и формируют локальные паттерны, такие как края, текстуры и формы.

3. **Инвариантность к преобразованиям**:
   - Алгоритмы должны быть устойчивы к изменениям в масштабе, поворотах и сдвигах изображений.

4. **Большие объемы данных**:
   - Для обучения моделей, таких как глубокие нейронные сети, требуются большие наборы данных (например, ImageNet).

#### Примеры задач:
- Классификация изображений (например, распознавание объектов).
- Сегментация (выделение областей на изображении).
- Обнаружение объектов (поиск объектов и их локализация на изображении).
- Генерация изображений (например, с помощью GAN).

---

### 2. **Принцип работы сверточных сетей (CNN)**

**Сверточные нейронные сети (Convolutional Neural Networks, CNN)** предназначены для эффективной обработки данных с сетчатой структурой, такой как изображения.

#### Основные элементы CNN:

1. **Сверточный слой (Convolutional Layer)**:
   - Выполняет свертку, используя фильтры (ядра), которые извлекают локальные признаки (края, текстуры и т. д.).
   - Принцип:
     \[
     (I * K)(x, y) = \sum_m \sum_n I(x+m, y+n) \cdot K(m, n)
     \]
     где \(I\) — входное изображение, \(K\) — фильтр, \(x, y\) — координаты пикселя.

2. **Функция активации**:
   - Например, ReLU (\(\max(0, x)\)) добавляет нелинейность.

3. **Слои подвыборки (Pooling Layers)**:
   - Уменьшают размерность данных, сохраняя основные признаки (например, MaxPooling берет максимальное значение в окне).

4. **Полносвязные слои (Fully Connected Layers)**:
   - В конце сети используется для предсказания результата.

5. **Dropout**:
   - Применяется для регуляризации.

#### Пример структуры:
\[
\text{Вход (изображение)} \rightarrow \text{Свертка} \rightarrow \text{ReLU} \rightarrow \text{Пуллинг} \rightarrow \ldots \rightarrow \text{Полносвязный слой} \rightarrow \text{Выход (классы)}.
\]

---

### 3. **Преимущества сверточных сетей при работе с изображениями**

1. **Сохранение пространственной информации**:
   - Свертка позволяет учитывать локальные зависимости между пикселями.

2. **Инвариантность к сдвигам**:
   - Извлечение признаков с помощью свертки и пуллинга делает сеть устойчивой к небольшим сдвигам на изображении.

3. **Эффективность вычислений**:
   - Благодаря локальной связности (каждый нейрон подключен только к части входа), CNN требуют меньше параметров по сравнению с полносвязными сетями.

4. **Иерархия признаков**:
   - Низкие слои изучают простые признаки (края, углы), а высокие — более сложные (формы, текстуры, объекты).

5. **Переносимость**:
   - Обученные сверточные сети могут быть перенесены на новые задачи (transfer learning).

---

### 5. **Ключевые моменты**

1. **Сверточные сети (CNN)** — основной инструмент для работы с изображениями.
2. **Свертки** позволяют эффективно выделять локальные признаки и уменьшать размерность данных.
3. CNN обладают высокой эффективностью благодаря локальной связности и инвариантности к преобразованиям.
4. PyTorch предоставляет удобный набор инструментов для построения и обучения сверточных сетей.
"""

    else:
        code = """
### 4. **Пример реализации сверточной сети в PyTorch**
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # Первый сверточный слой: 3 входных канала, 16 фильтров, ядро 3x3
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
        # Второй сверточный слой: 16 входных каналов, 32 фильтра
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        # Полносвязный слой
        self.fc = nn.Linear(32 * 8 * 8, 10)  # Выходной размер: 10 классов

    def forward(self, x):
        # Свертка, активация, пуллинг
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)  # Уменьшение размерности
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        # Преобразование в плоский вектор
        x = torch.flatten(x, 1)
        # Полносвязный слой
        x = self.fc(x)
        return x

# Инициализация модели
model = SimpleCNN()

# Проверка на фейковых данных
dummy_input = torch.randn(1, 3, 32, 32)  # Мини-батч из 1 изображения 3x32x32
output = model(dummy_input)
print("Output shape:", output.shape)
"""

    pyperclip.copy(code)


def conv_pool(idx: int = 0):
    if idx == 0:
        code = """
### 16. Архитектура многослойной ИНС для распознавания изображений, сверточные и сжимающие слои в PyTorch

---

### 1. **Архитектура многослойной ИНС для распознавания изображений**

Архитектура сверточной нейронной сети (CNN) для распознавания изображений состоит из нескольких основных компонентов:

#### Основные элементы архитектуры:
1. **Сверточные слои (Convolution Layers)**:
   - Извлекают локальные признаки из изображения.
   - Фильтры (ядра свертки) обучаются для выделения краёв, текстур, форм и других признаков.

2. **Слои активации (Activation Layers)**:
   - Добавляют нелинейность, чтобы модель могла изучать сложные зависимости.
   - Обычно используются ReLU или его модификации.

3. **Сжимающие слои (Pooling Layers)**:
   - Уменьшают пространственное разрешение, сохраняя важные признаки.
   - Наиболее популярны:
     - Max Pooling (выбирает максимальное значение в каждом окне).
     - Average Pooling (вычисляет среднее значение).

4. **Полносвязные слои (Fully Connected Layers)**:
   - Преобразуют двумерные признаки в одномерный вектор для классификации.

5. **Регуляризация**:
   - Dropout используется для предотвращения переобучения.

#### Пример структуры сети:
\[
\text{Входное изображение (RGB, HxW)} \rightarrow \text{Свертка + ReLU} \rightarrow \text{Пуллинг} \rightarrow \ldots \rightarrow \text{Полносвязный слой} \rightarrow \text{Классы}
\]

---

### 2. **Сверточные слои (Convolution Layers)**

#### Функция сверточного слоя:
- Применяет фильтр (ядро) фиксированного размера к изображению, перемещая его по всей площади.
- Фильтры обучаются автоматически для извлечения признаков.

#### Параметры сверточного слоя:
1. **`in_channels`**: количество входных каналов (например, 3 для RGB-изображения).
2. **`out_channels`**: количество фильтров (каналов выхода).
3. **`kernel_size`**: размер фильтра (например, \(3 \times 3\)).
4. **`stride`**: шаг фильтра (обычно 1).
5. **`padding`**: добавление нулей вокруг границ изображения.

#### Пример создания сверточного слоя в PyTorch:
```python
import torch
import torch.nn as nn

conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
input_image = torch.randn(1, 3, 32, 32)  # 1 изображение, 3 канала, размер 32x32
output = conv(input_image)
print("Output shape:", output.shape)  # Результат: [1, 16, 32, 32]
```

---

### 3. **Сжимающие слои (Pooling Layers)**

#### Функция сжимающих слоев:
- Уменьшают размер пространственного представления, сохраняя важные признаки.
- Сглаживают шумы и улучшают устойчивость модели.

#### Виды пуллинга:
1. **Max Pooling**:
   - Выбирает максимальное значение из каждого окна.
2. **Average Pooling**:
   - Вычисляет среднее значение из каждого окна.

#### Параметры пуллинга:
1. **`kernel_size`**: размер окна.
2. **`stride`**: шаг окна.
3. **`padding`**: добавление нулей вокруг границ.

#### Пример создания MaxPooling слоя:
```python
pool = nn.MaxPool2d(kernel_size=2, stride=2)  # Размер окна 2x2, шаг 2
output = pool(output)
print("Output shape after pooling:", output.shape)  # Результат: [1, 16, 16, 16]
```

---

### 5. **Ключевые моменты**

1. **Сверточные слои (Convolution Layers)**:
   - Используются для выделения локальных признаков изображения (краёв, текстур, форм).
   - Параметры, такие как `kernel_size` и `stride`, задают, как фильтр перемещается по изображению.

2. **Сжимающие слои (Pooling Layers)**:
   - Уменьшают размерность данных, сохраняя ключевую информацию.
   - MaxPooling — наиболее распространённый выбор.

3. **Пример архитектуры CNN**:
   - Состоит из последовательности сверточных и пуллинговых слоев, за которыми следуют полносвязные слои для классификации.

4. **PyTorch предоставляет готовые слои**, которые легко использовать для построения сложных сверточных сетей.        
"""
    else:
        code = """
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # Сверточные слои
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        
        # Пуллинг
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # Полносвязные слои
        self.fc1 = nn.Linear(32 * 8 * 8, 128)  # 32 канала, 8x8 размер после двух пуллингов
        self.fc2 = nn.Linear(128, 10)  # 10 классов

    def forward(self, x):
        # Первый сверточный блок
        x = self.pool(torch.relu(self.conv1(x)))
        
        # Второй сверточный блок
        x = self.pool(torch.relu(self.conv2(x)))
        
        # Преобразование в плоский вектор
        x = torch.flatten(x, 1)
        
        # Полносвязные слои
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# Создание модели
model = SimpleCNN()

# Проверка модели на данных
dummy_input = torch.randn(1, 3, 32, 32)  # 1 изображение 3x32x32
output = model(dummy_input)
print("Output shape:", output.shape)  # Результат: [1, 10]
"""

    pyperclip.copy(code)


def cnn_ex(idx: int = 0):
    if idx == 0:
        code = """
### 17. Приемы для глубокого обучения на небольших наборах изображений

---

Обучение глубоких нейронных сетей на небольших наборах изображений — сложная задача из-за высокого риска **переобучения** и недостаточной обобщающей способности модели. Однако существуют приемы, которые позволяют улучшить качество обучения и повысить устойчивость модели.

---

### 1. **Увеличение данных (Data Augmentation)**

Увеличение данных помогает искусственно расширить обучающую выборку, создавая новые данные из существующих. Это снижает риск переобучения.

#### Популярные методы:
1. **Геометрические трансформации**:
   - Повороты, сдвиги, отражения, масштабирование.
2. **Цветовые преобразования**:
   - Изменение яркости, контрастности, насыщенности.
3. **Добавление шума**:
   - Гауссовский шум, случайное стирание (Random Erasing).
4. **Random Cropping**:
   - Случайная обрезка и изменение размера изображения.

#### Пример в PyTorch:
```python
from torchvision import transforms

transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.RandomResizedCrop(size=32, scale=(0.8, 1.0)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
    transforms.ToTensor()
])
```

---

### 2. **Transfer Learning (Трансферное обучение)**

Использование предварительно обученных моделей на больших наборах данных (например, ImageNet) помогает существенно улучшить качество обучения на небольших наборах.

#### Подходы:
1. **Фиксация весов (Fine-tuning)**:
   - Замораживаются веса предварительно обученных слоев, изменяются только верхние (классификационные) слои.
2. **Полное дообучение (Full training)**:
   - Все веса сети дообучаются на новом наборе данных.

#### Пример в PyTorch:
```python
from torchvision import models

# Загружаем предварительно обученную модель
model = models.resnet18(pretrained=True)

# Замораживаем веса всех слоев
for param in model.parameters():
    param.requires_grad = False

# Меняем последний слой под свою задачу
model.fc = nn.Linear(512, 10)  # 10 классов
```

---

### 3. **Регуляризация**

Для предотвращения переобучения в условиях малого объема данных используются методы регуляризации.

#### Основные техники:
1. **Dropout**:
   - Исключение случайных нейронов во время обучения.
   ```python
   nn.Dropout(p=0.5)
   ```
   
2. **L2-регуляризация (Weight Decay)**:
   - Добавляет штраф за большие значения весов в функцию потерь.
   ```python
   optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
   ```

3. **Раннее завершение обучения (Early Stopping)**:
   - Остановка обучения, если ошибка на валидационной выборке перестает уменьшаться.

---

### 4. **Использование небольших моделей**

Глубокие модели требуют большого количества данных. Для небольших наборов данных рекомендуется использовать компактные архитектуры.

#### Примеры:
- MobileNet, SqueezeNet, ResNet-18.
- Создание собственных небольших моделей с уменьшенным количеством параметров.

---

### 5. **Кросс-валидация**

Кросс-валидация помогает более эффективно использовать небольшой объем данных. Например:
- Разделение данных на \(k\) фолдов.
- Обучение и тестирование на каждом фолде, чередуя данные.

---

### 6. **Синтетическое увеличение данных**

Генеративные модели, такие как GAN или VAE, могут быть использованы для создания новых изображений, похожих на обучающую выборку.

---

### 7. **Использование дополнительных данных**

1. **Pretraining**:
   - Использование похожих наборов данных для предварительного обучения.
   - Например, для медицинских изображений можно использовать открытые наборы данных из той же области.

2. **Weakly Supervised Learning**:
   - Использование данных с менее точной разметкой.

---

### 8. **Нормализация данных**

Нормализация входных данных помогает улучшить сходимость и стабильность обучения:
- Приведение значений пикселей в диапазон \([0, 1]\).
- Нормализация к среднему и стандартному отклонению обучающего набора.
```python
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
```

---

### 9. **Энсамблирование моделей**

Энсамблирование улучшает устойчивость и точность предсказаний, комбинируя результаты нескольких моделей.

#### Пример:
1. Тренировка нескольких моделей с разными инициализациями.
2. Усреднение предсказаний всех моделей.

---

### Выводы

1. **Увеличение данных** и **трансферное обучение** — ключевые техники для работы с небольшими наборами изображений.
2. Регуляризация и компактные модели помогают избежать переобучения.
3. PyTorch предоставляет мощные инструменты для эффективной реализации этих методов.
"""
    else:
        code = """
### Пример полной архитектуры для небольших наборов данных в PyTorch
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torchvision.models import resnet18

# Аугментация данных
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

# Загрузка данных
train_data = datasets.CIFAR10(root='./data', train=True, transform=transform, download=True)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=32, shuffle=True)

# Модель с трансферным обучением
model = resnet18(pretrained=True)
model.fc = nn.Linear(512, 10)  # CIFAR-10: 10 классов

# Функция потерь и оптимизатор
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

# Обучение
for epoch in range(10):
    model.train()
    for images, labels in train_loader:
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
"""

    pyperclip.copy(code)


def cnn_cls(idx: int = 0):
    if idx == 0:
        code = """
### 18. Схема работы сверточной сети, операции свертки, пуллинга и общий вид сети для классификации изображений

---

### 1. **Схема работы сверточной сети**

Сверточная нейронная сеть (CNN) — это архитектура, которая предназначена для обработки данных с сетчатой структурой, таких как изображения. Она работает поэтапно, извлекая признаки различной сложности.

#### Основные этапы работы:
1. **Извлечение локальных признаков**:
   - Сверточные слои (Convolution Layers) применяют фильтры для выделения локальных особенностей изображения (краёв, текстур, форм).

2. **Уменьшение размерности**:
   - Сжимающие слои (Pooling Layers) уменьшают размерность изображения, сохраняя основные признаки и устраняя шум.

3. **Объединение признаков**:
   - Последние слои объединяют извлечённые признаки и преобразуют их в классификацию.

---

### 2. **Операции свертки**

Свертка — это основная операция в CNN, которая выделяет локальные признаки изображения.

#### Механизм:
1. Применяется фильтр (ядро) — матрица фиксированного размера (например, \(3 \times 3\) или \(5 \times 5\)).
2. Фильтр перемещается по изображению с определённым шагом (\(stride\)), вычисляя скалярное произведение его элементов и области изображения.
3. Результаты записываются в новое изображение — **карту признаков (feature map)**.

#### Формула свертки:
Для входного изображения \(I\) и фильтра \(K\):
\[
O(x, y) = \sum_{i=1}^{m} \sum_{j=1}^{n} I(x+i, y+j) \cdot K(i, j)
\]

#### Пример свертки в PyTorch:
```python
import torch
import torch.nn as nn

# Входное изображение: 1 канал, 32x32
input_image = torch.randn(1, 1, 32, 32)

# Сверточный слой: 1 входной канал, 16 выходных каналов, ядро 3x3
conv = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1)

# Применение свертки
output = conv(input_image)
print("Output shape:", output.shape)  # Результат: [1, 16, 32, 32]
```

---

### 3. **Операции пуллинга (Pooling)**

Пуллинг уменьшает размерность карты признаков, сохраняя важную информацию.

#### Виды пуллинга:
1. **Max Pooling**:
   - Выбирает максимальное значение из окна.
2. **Average Pooling**:
   - Вычисляет среднее значение в окне.

#### Пример Max Pooling:
```python
# MaxPooling: размер окна 2x2, шаг 2
pool = nn.MaxPool2d(kernel_size=2, stride=2)

# Применение пуллинга
pooled_output = pool(output)
print("Pooled Output shape:", pooled_output.shape)  # Результат: [1, 16, 16, 16]
```

---

### 4. **Общий вид сверточной сети для классификации изображений**

CNN состоит из чередующихся слоев свертки и пуллинга, за которыми следуют полносвязные слои.

#### Структура:
1. **Входное изображение** (\(H \times W \times C\)).
2. **Сверточные блоки**:
   - Слой свертки (\(Conv\)) + нелинейность (\(ReLU\)) + пуллинг (\(Pooling\)).
3. **Полносвязный блок**:
   - Преобразование карты признаков в одномерный вектор.
   - Полносвязные слои (\(Fully Connected\)).
4. **Выходной слой**:
   - Количество нейронов равно количеству классов.

---

### 6. **Преимущества CNN для классификации изображений**

1. **Локальность признаков**:
   - Свертки эффективно извлекают локальные особенности изображения.

2. **Сокращение параметров**:
   - По сравнению с полносвязными слоями, сверточные слои требуют меньше параметров.

3. **Инвариантность к трансформациям**:
   - Использование пуллинга делает CNN устойчивой к сдвигам и масштабам объектов.

4. **Иерархия признаков**:
   - Низкие слои изучают простые паттерны (края, текстуры), высокие — сложные (формы, объекты).

---

### Вывод

- CNN работают на основе чередования сверточных и пуллинговых слоев, которые извлекают и сжимают признаки.
- Полносвязные слои используют извлечённые признаки для классификации.
- PyTorch предоставляет удобные инструменты для реализации всех этапов построения CNN.        
"""

    else:
        code = """
#### Полная реализация CNN:
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # Первый сверточный блок
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # Второй сверточный блок
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # Полносвязные слои
        self.fc1 = nn.Linear(32 * 8 * 8, 128)  # Размер после двух пуллингов: 8x8
        self.fc2 = nn.Linear(128, 10)  # 10 классов для классификации

    def forward(self, x):
        # Первый сверточный блок
        x = F.relu(self.conv1(x))
        x = self.pool1(x)
        
        # Второй сверточный блок
        x = F.relu(self.conv2(x))
        x = self.pool2(x)
        
        # Преобразование в плоский вектор
        x = torch.flatten(x, 1)
        
        # Полносвязные слои
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# Создание модели
model = SimpleCNN()

# Проверка на данных
dummy_input = torch.randn(1, 3, 32, 32)  # 1 изображение (RGB), 32x32
output = model(dummy_input)
print("Output shape:", output.shape)  # Результат: [1, 10]
"""
    pyperclip.copy(code)


def kinds_rl(idx: int = 0):
    if idx == 0:
        code = """
### 19. Виды задач машинного обучения и постановка задачи обучения с подкреплением

---

### 1. **Основные виды задач машинного обучения**

Машинное обучение классифицируется на основе типа предоставленных данных и цели обучения. Основные виды:

---

#### 1.1 **Обучение с учителем (Supervised Learning)**

- **Цель**: Обучить модель на основе данных с метками (вход-выход), чтобы предсказывать метки для новых данных.
- **Примеры задач**:
  - **Классификация**: Прогнозирование категорий (например, спам/не спам).
  - **Регрессия**: Прогнозирование непрерывных значений (например, цены недвижимости).

**Постановка задачи**:
\[
\text{Дано: } \{(x_i, y_i)\}_{i=1}^n \quad \text{Найти: } f(x) \approx y
\]
где \(x_i\) — входные данные, \(y_i\) — метки.

**Пример**:
- Данные: \(x\) — площадь дома, \(y\) — цена.
- Цель: Прогнозировать цену для нового дома.

---

#### 1.2 **Обучение без учителя (Unsupervised Learning)**

- **Цель**: Обнаружить скрытые структуры или закономерности в данных без меток.
- **Примеры задач**:
  - **Кластеризация**: Разделение данных на группы (например, сегментация клиентов).
  - **Снижение размерности**: Уменьшение числа признаков (например, PCA, t-SNE).

**Постановка задачи**:
\[
\text{Дано: } \{x_i\}_{i=1}^n \quad \text{Найти: } \text{структуры или группы в данных.}
\]

**Пример**:
- Данные: Поведение пользователей на сайте.
- Цель: Разделить пользователей на сегменты.

---

#### 1.3 **Обучение с подкреплением (Reinforcement Learning)**

- **Цель**: Научить агента принимать последовательные решения, максимизируя награду.
- **Примеры задач**:
  - Игра в шахматы или го.
  - Управление роботами.
  - Оптимизация маршрутов.

**Постановка задачи**:
- Агент взаимодействует с окружающей средой, выполняя действия \(a_t\) в состоянии \(s_t\), чтобы максимизировать ожидаемую совокупную награду.

**Формализация через MDP (Markov Decision Process)**:
1. \(S\): Пространство состояний.
2. \(A\): Пространство действий.
3. \(P(s_{t+1} | s_t, a_t)\): Вероятность перехода.
4. \(R(s_t, a_t)\): Награда.
5. \(\gamma\): Коэффициент дисконтирования.

Задача:
\[
\text{Найти: } \pi^* = \arg\max_{\pi} \mathbb{E} \left[ \sum_{t=0}^\infty \gamma^t R(s_t, a_t) \right]
\]
где \(\pi\) — политика агента.

---

#### 1.4 **Полуобучение (Semi-Supervised Learning)**

- **Цель**: Использовать небольшую часть данных с метками и большую часть без меток.
- **Пример задачи**:
  - Разметка текстов, где большая часть текстов не имеет меток.

---

#### 1.5 **Обучение с самообучением (Self-Supervised Learning)**

- **Цель**: Использовать внутренние зависимости в данных для создания задач предобучения.
- **Пример задачи**:
  - Прогнозирование следующего слова в тексте (например, в GPT).

---

### 2. **Обучение с подкреплением**

---

#### 2.1 **Примеры задач**

1. **Игры**:
   - Пример: AlphaGo, обучение игре в шахматы.
2. **Управление роботами**:
   - Пример: Обучение манипуляции объектами.
3. **Оптимизация маршрутов**:
   - Пример: Выбор маршрута для беспилотника.
4. **Экономика**:
   - Пример: Оптимизация портфеля.

---

#### 2.2 **Процесс обучения с подкреплением**

1. **Инициализация**:
   - Агент начинает в состоянии \(s_0\).
2. **Цикл взаимодействия**:
   - Агент выбирает действие \(a_t\) на основе политики \(\pi(s_t)\).
   - Среда возвращает новую награду \(R(s_t, a_t)\) и новое состояние \(s_{t+1}\).
3. **Обновление**:
   - Агент обновляет стратегию \(\pi(s)\), чтобы улучшить ожидаемую награду.

---

#### 2.3 **Методы обучения**

1. **Q-Learning**:
   - Использует Q-таблицу для оценки "качества" действий:
     \[
     Q(s_t, a_t) = R(s_t, a_t) + \gamma \max_a Q(s_{t+1}, a)
     \]

2. **Deep Q-Learning**:
   - Использует нейронные сети для аппроксимации Q-функции.

3. **Policy Gradient**:
   - Находит оптимальную стратегию \(\pi(a|s)\), максимизируя награду:
     \[
     J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \sum_t R(s_t, a_t) \right]
     \]

---

### Выводы

1. **Виды задач машинного обучения**:
   - Обучение с учителем, без учителя, с подкреплением, полуобучение и самообучение.
2. **Обучение с подкреплением**:
   - Агент обучается через взаимодействие со средой, максимизируя совокупную награду.
3. **Методы обучения**:
   - Q-Learning, Policy Gradient, Deep Q-Learning.
4. PyTorch предоставляет инструменты для реализации сложных алгоритмов обучения с подкреплением.        
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

# Пример Q-обучения с простой средой

# Среда: движение вдоль линии
states = 10
actions = 2  # Влево или вправо
q_table = np.zeros((states, actions))

# Гиперпараметры
learning_rate = 0.1
gamma = 0.99
epsilon = 0.1
episodes = 1000

# Q-обучение
for episode in range(episodes):
    state = np.random.randint(0, states)
    done = False
    
    while not done:
        if np.random.rand() < epsilon:
            action = np.random.randint(0, actions)  # Исследование
        else:
            action = np.argmax(q_table[state])  # Эксплуатация
        
        # Выполнение действия
        next_state = state + (1 if action == 1 else -1)
        next_state = max(0, min(next_state, states - 1))
        reward = 1 if next_state == states - 1 else -0.1
        done = next_state == states - 1

        # Обновление Q-таблицы
        q_table[state, action] += learning_rate * (
            reward + gamma * np.max(q_table[next_state]) - q_table[state, action]
        )
        state = next_state

print("Обучение завершено. Q-таблица:")
print(q_table)
"""
    pyperclip.copy(code)


def rl_policy(idx: int = 0):
    if idx == 0:
        code = """
### 20. Подходы к определению стратегии в RL, вознаграждение, дисконтированное вознаграждение, Q- и V-функции, уравнение Беллмана

---

### 1. **Подходы к определению стратегии в RL**

Стратегия (policy, \(\pi\)) в Reinforcement Learning определяет, какое действие агент выбирает в каждом состоянии.

#### Виды стратегий:
1. **Детерминированная стратегия**:
   - Агент всегда выбирает одно и то же действие \(a\) в состоянии \(s\).
   - Формально: \(\pi(s) = a\).

2. **Стохастическая стратегия**:
   - Агент выбирает действие с определённой вероятностью.
   - Формально: \(\pi(a|s) = P(a|s)\), где \(P(a|s)\) — вероятность выбора действия \(a\) в состоянии \(s\).

---

### 2. **Определение вознаграждения и дисконтированного вознаграждения**

#### 2.1. Вознаграждение
Вознаграждение \(R(s, a)\) — численная оценка, показывающая, насколько хорош результат действия \(a\) в состоянии \(s\).

- Мгновенное вознаграждение: \(R_t = R(s_t, a_t)\).
- Пример: Если агент достигает цели, вознаграждение равно \(+1\); если ошибается, \(-1\).

#### 2.2. Дисконтированное вознаграждение
Чтобы учитывать будущие награды, используется дисконтированное вознаграждение:
\[
G_t = \sum_{k=0}^\infty \gamma^k R_{t+k+1}
\]
где:
- \(G_t\) — совокупное дисконтированное вознаграждение.
- \(\gamma \in [0, 1]\) — коэффициент дисконтирования, определяющий важность будущих наград.

**Значение \(\gamma\):**
- Если \(\gamma \approx 1\), агент учитывает долгосрочные награды.
- Если \(\gamma \approx 0\), агент фокусируется только на краткосрочных наградах.

---

### 3. **Q-функция и V-функция**

#### 3.1. Функция значения состояния (State Value Function, \(V(s)\))
Оценивает "хорошесть" состояния \(s\) при следовании стратегии \(\pi\):
\[
V^\pi(s) = \mathbb{E}_\pi \left[ G_t | s_t = s \right]
\]
где \(G_t\) — совокупное дисконтированное вознаграждение.

#### 3.2. Q-функция (State-Action Value Function, \(Q(s, a)\))
Оценивает "хорошесть" пары \(s, a\) при следовании стратегии \(\pi\):
\[
Q^\pi(s, a) = \mathbb{E}_\pi \left[ G_t | s_t = s, a_t = a \right]
\]

#### Связь между \(V(s)\) и \(Q(s, a)\):
\[
V^\pi(s) = \mathbb{E}_\pi \left[ Q^\pi(s, a) \right]
\]
или:
\[
V^\pi(s) = \sum_a \pi(a|s) Q^\pi(s, a)
\]

---

### 4. **Уравнение Беллмана**

Уравнение Беллмана описывает рекурсивную связь между значением состояния и наградами.

#### 4.1. Уравнение Беллмана для \(V(s)\):
\[
V^\pi(s) = \sum_a \pi(a|s) \left[ R(s, a) + \gamma \sum_{s'} P(s'|s, a) V^\pi(s') \right]
\]
где:
- \(P(s'|s, a)\) — вероятность перехода из \(s\) в \(s'\) при выполнении действия \(a\).

#### 4.2. Уравнение Беллмана для \(Q(s, a)\):
\[
Q^\pi(s, a) = R(s, a) + \gamma \sum_{s'} P(s'|s, a) \sum_{a'} \pi(a'|s') Q^\pi(s', a')
\]

---

### Выводы

1. **Q- и V-функции**:
   - \(V(s)\) оценивает "хорошесть" состояния, \(Q(s, a)\) — пары состояние-действие.
2. **Уравнение Беллмана**:
   - Описывает связь между текущей наградой и будущими состояниями.
3. **Q-Learning**:
   - Метод обучения политики через аппроксимацию \(Q(s, a)\).
4. PyTorch позволяет эффективно реализовать как табличные, так и глубокие методы обучения.
"""
    else:
        code = """
#### 5.1. Простая реализация Q-Learning:
import numpy as np

# Пример среды
states = 5
actions = 2
q_table = np.zeros((states, actions))  # Q-таблица

# Гиперпараметры
alpha = 0.1  # Скорость обучения
gamma = 0.9  # Коэффициент дисконтирования
epsilon = 0.1  # Вероятность случайного действия
episodes = 1000

# Обучение
for episode in range(episodes):
    state = np.random.randint(0, states)
    done = False

    while not done:
        # Выбор действия
        if np.random.rand() < epsilon:
            action = np.random.randint(0, actions)  # Исследование
        else:
            action = np.argmax(q_table[state])  # Эксплуатация

        # Выполнение действия
        next_state = (state + action) % states
        reward = 1 if next_state == states - 1 else -0.1
        done = next_state == states - 1

        # Обновление Q-таблицы
        q_table[state, action] += alpha * (
            reward + gamma * np.max(q_table[next_state]) - q_table[state, action]
        )
        state = next_state

print("Обучение завершено. Q-таблица:")
print(q_table)

#### 5.2. Deep Q-Learning в PyTorch:
import torch
import torch.nn as nn
import torch.optim as optim

# Сеть для Q-функции
class QNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(QNetwork, self).__init__()
        self.fc1 = nn.Linear(state_dim, 128)
        self.fc2 = nn.Linear(128, action_dim)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        return self.fc2(x)

# Гиперпараметры
state_dim = 4
action_dim = 2
q_network = QNetwork(state_dim, action_dim)
optimizer = optim.Adam(q_network.parameters(), lr=0.001)
loss_fn = nn.MSELoss()

# Обучение
for episode in range(1000):
    state = torch.randn(1, state_dim)
    action = torch.randint(0, action_dim, (1,))
    reward = torch.tensor([1.0])
    next_state = torch.randn(1, state_dim)

    # Q-значение
    q_value = q_network(state)[0, action]
    target = reward + gamma * q_network(next_state).max()
    
    # Оптимизация
    loss = loss_fn(q_value, target.detach())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
"""
    pyperclip.copy(code)


def policy_grad(idx: int = 0):
    if idx == 0:
        code = """
### 21. Метод Policy Gradient, улучшения и модель Actor-Critic

---

### 1. **Метод Policy Gradient**

Метод Policy Gradient (PG) относится к классу методов обучения с подкреплением, которые обучают **стратегию (policy)** напрямую, оптимизируя её параметры \(\theta\).

#### Основная идея:
- Вместо обучения Q-функции или V-функции, как в Q-Learning, PG обучает **стратегию \(\pi(a|s; \theta)\)**, которая определяет вероятность выбора действия \(a\) в состоянии \(s\).

#### Целевая функция (Expected Return):
Оптимизируем функцию вознаграждения:
\[
J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \sum_{t=0}^\infty \gamma^t R(s_t, a_t) \right]
\]

#### Обновление параметров:
Используется градиент целевой функции:
\[
\nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) G_t \right]
\]
где:
- \(\log \pi_\theta(a|s)\) — вероятность выбранного действия.
- \(G_t\) — дисконтированное суммарное вознаграждение.

---

### 2. **Улучшения метода Policy Gradient**

#### 2.1. **Использование Advantage вместо \(G_t\)**

Проблема:
- Использование полного вознаграждения \(G_t\) приводит к высокой дисперсии в обновлениях.

Решение:
- Использовать **Advantage Function \(A(s, a)\)**, которая оценивает, насколько действие лучше среднего:
\[
A(s, a) = Q(s, a) - V(s)
\]

Формула обновления:
\[
\nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) A(s, a) \right]
\]

#### 2.2. **REINFORCE with Baseline**

Проблема:
- Высокая дисперсия при оценке градиента.

Решение:
- Вычесть из \(G_t\) **базовый уровень** \(b(s)\), который не зависит от действия:
\[
\nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) (G_t - b(s)) \right]
\]

Обычно в качестве \(b(s)\) используется функция значения \(V(s)\).

---

### 3. **Модель Actor-Critic**

Модель Actor-Critic комбинирует два подхода:
1. **Actor**:
   - Реализует стратегию \(\pi(a|s; \theta_{\text{actor}})\).
   - Обновляется с использованием градиента политики:
     \[
     \nabla_{\theta_{\text{actor}}} J = \mathbb{E} \left[ \nabla_{\theta_{\text{actor}}} \log \pi(a|s) A(s, a) \right]
     \]

2. **Critic**:
   - Оценивает функцию значения \(V(s; \theta_{\text{critic}})\) или \(Q(s, a; \theta_{\text{critic}})\).
   - Обучается как задача регрессии:
     \[
     \mathcal{L}_{\text{critic}} = \left( R + \gamma V(s') - V(s) \right)^2
     \]

#### Преимущества Actor-Critic:
- **Actor** отвечает за улучшение стратегии.
- **Critic** снижает дисперсию, предоставляя стабильную оценку Advantage.

---

### 5. **Ключевые моменты**

1. **Policy Gradient**:
   - Оптимизирует стратегию напрямую, обучая параметры \(\pi(a|s; \theta)\).
   - Использует вознаграждение или Advantage для обновления.

2. **Actor-Critic**:
   - Actor обновляет стратегию.
   - Critic оценивает состояние, снижая дисперсию градиента.

3. **Преимущества Actor-Critic**:
   - Быстрая сходимость.
   - Уменьшенная дисперсия по сравнению с чистым Policy Gradient.
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

# Сеть для Actor
class Actor(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(Actor, self).__init__()
        self.fc = nn.Linear(state_dim, 128)
        self.output = nn.Linear(128, action_dim)

    def forward(self, x):
        x = torch.relu(self.fc(x))
        return torch.softmax(self.output(x), dim=-1)

# Сеть для Critic
class Critic(nn.Module):
    def __init__(self, state_dim):
        super(Critic, self).__init__()
        self.fc = nn.Linear(state_dim, 128)
        self.output = nn.Linear(128, 1)

    def forward(self, x):
        x = torch.relu(self.fc(x))
        return self.output(x)

# Гиперпараметры
state_dim = 4
action_dim = 2
gamma = 0.99
lr = 0.001

# Инициализация Actor и Critic
actor = Actor(state_dim, action_dim)
critic = Critic(state_dim)
actor_optimizer = optim.Adam(actor.parameters(), lr=lr)
critic_optimizer = optim.Adam(critic.parameters(), lr=lr)

# Пример обучения
for episode in range(1000):
    state = torch.randn(1, state_dim)  # Случайное состояние
    probs = actor(state)
    action = torch.multinomial(probs, 1).item()  # Сэмплирование действия
    reward = torch.tensor([1.0])  # Пример вознаграждения
    next_state = torch.randn(1, state_dim)  # Новое состояние
    
    # Вычисляем Advantage
    value = critic(state)
    next_value = critic(next_state)
    advantage = reward + gamma * next_value - value

    # Обновление Actor
    actor_loss = -torch.log(probs[0, action]) * advantage.detach()
    actor_optimizer.zero_grad()
    actor_loss.backward()
    actor_optimizer.step()

    # Обновление Critic
    critic_loss = advantage.pow(2)
    critic_optimizer.zero_grad()
    critic_loss.backward()
    critic_optimizer.step()

    if episode % 100 == 0:
        print(f"Episode {episode}, Actor Loss: {actor_loss.item():.4f}, Critic Loss: {critic_loss.item():.4f}")
"""
    pyperclip.copy(code)


def q_learning(idx: int = 0):
    if idx == 0:
        code = """
### 1. **Определение метода Q-Learning**

**Q-Learning** — это алгоритм обучения с подкреплением, который используется для нахождения оптимальной стратегии (\(\pi^*\)) в среде. Цель метода — аппроксимация функции \(Q(s, a)\), которая оценивает "качество" выполнения действия \(a\) в состоянии \(s\).

#### Функция \(Q(s, a)\):
- \(Q(s, a)\) — ожидаемая совокупная дисконтированная награда при выполнении действия \(a\) в состоянии \(s\) и следовании оптимальной стратегии:
\[
Q(s, a) = \mathbb{E} \left[ \sum_{t=0}^\infty \gamma^t R(s_t, a_t) \, \Big| \, s_0 = s, a_0 = a \right]
\]

#### Цель метода:
- Максимизировать совокупное дисконтированное вознаграждение:
\[
\pi^*(s) = \arg\max_a Q(s, a)
\]

---

### 2. **Обновление Q-функции**

Q-Learning использует уравнение Беллмана для обновления Q-значений. Оптимальное значение \(Q^*(s, a)\) удовлетворяет уравнению:
\[
Q^*(s, a) = R(s, a) + \gamma \max_{a'} Q^*(s', a')
\]
где:
- \(R(s, a)\) — мгновенное вознаграждение.
- \(\gamma \in [0, 1]\) — коэффициент дисконтирования, который уменьшает влияние будущих наград.
- \(s'\) — новое состояние после выполнения действия \(a\).

#### Алгоритм обновления:
1. Для текущего состояния \(s\) и действия \(a\), выполните действие, получите \(s'\) и \(R(s, a)\).
2. Обновите Q-значение:
\[
Q(s, a) \leftarrow Q(s, a) + \alpha \left[ R(s, a) + \gamma \max_{a'} Q(s', a') - Q(s, a) \right]
\]
где \(\alpha \in [0, 1]\) — скорость обучения.

---

### 3. **Алгоритм Q-Learning**

#### Псевдокод:
1. Инициализируйте Q-таблицу: \(Q(s, a) = 0 \, \forall s, a\).
2. Для каждого эпизода:
   - Инициализируйте начальное состояние \(s\).
   - Повторяйте, пока эпизод не завершится:
     1. Выберите действие \(a\) с использованием \(\epsilon\)-жадной стратегии (исследование и эксплуатация).
     2. Выполните \(a\), получите новое состояние \(s'\) и вознаграждение \(R(s, a)\).
     3. Обновите Q-значение:
        \[
        Q(s, a) \leftarrow Q(s, a) + \alpha \left[ R(s, a) + \gamma \max_{a'} Q(s', a') - Q(s, a) \right]
        \]
     4. Обновите текущее состояние: \(s \leftarrow s'\).

3. Повторяйте, пока Q-таблица не сойдётся.

---

### 4. **Пример реализации Q-Learning в Python**

#### Пример: Простая среда с движением вдоль линии
```python
import numpy as np

# Гиперпараметры
alpha = 0.1  # Скорость обучения
gamma = 0.9  # Коэффициент дисконтирования
epsilon = 0.1  # Вероятность случайного действия
episodes = 1000
states = 5
actions = 2  # Влево или вправо

# Инициализация Q-таблицы
q_table = np.zeros((states, actions))

# Q-Learning
for episode in range(episodes):
    state = np.random.randint(0, states)  # Случайное начальное состояние
    done = False

    while not done:
        # Выбор действия (\epsilon-жадная стратегия)
        if np.random.rand() < epsilon:
            action = np.random.randint(0, actions)  # Исследование
        else:
            action = np.argmax(q_table[state])  # Эксплуатация

        # Выполнение действия
        next_state = state + (1 if action == 1 else -1)
        next_state = max(0, min(next_state, states - 1))  # Границы среды
        reward = 1 if next_state == states - 1 else -0.1  # Награда
        done = next_state == states - 1

        # Обновление Q-таблицы
        q_table[state, action] += alpha * (
            reward + gamma * np.max(q_table[next_state]) - q_table[state, action]
        )
        state = next_state

# Вывод Q-таблицы
print("Q-таблица после обучения:")
print(q_table)
```

---

### 5. **Особенности метода Q-Learning**

#### Преимущества:
1. **Модель-независимость**:
   - Не требует знания модели среды (\(P(s'|s, a)\)) — только награды и состояния.
2. **Гарантированная сходимость**:
   - При правильных гиперпараметрах (\(\alpha\), \(\gamma\)) Q-Learning сходится к оптимальной стратегии.

#### Недостатки:
1. **Ограничение по размеру состояния**:
   - При большом количестве состояний Q-таблица становится слишком большой (проблема размерности).
2. **Исследование и эксплуатация**:
   - Баланс между исследованием (\(exploration\)) и эксплуатацией (\(exploitation\)) требует тонкой настройки.

---

### 6. **Deep Q-Learning (DQN)**

Для среды с большими размерами состояния используют **глубокое Q-обучение**, где Q-функция аппроксимируется нейронной сетью.

#### Основные идеи:
1. Используется нейронная сеть \(Q(s, a; \theta)\) вместо Q-таблицы.
2. Потери оптимизируются с помощью стохастического градиентного спуска:
   \[
   \mathcal{L}(\theta) = \left( R + \gamma \max_{a'} Q(s', a'; \theta^-) - Q(s, a; \theta) \right)^2
   \]
   где \(\theta^-\) — параметры замороженной целевой сети.

---

### Выводы

1. **Q-Learning** — мощный алгоритм обучения, который обучает Q-функцию, оптимизируя стратегию агента.
2. **Уравнение Беллмана** лежит в основе обновления Q-значений.
3. **Deep Q-Learning (DQN)** позволяет применять Q-Learning к сложным задачам с помощью нейронных сетей.
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

# Нейронная сеть для Q-функции
class QNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(QNetwork, self).__init__()
        self.fc1 = nn.Linear(state_dim, 128)
        self.fc2 = nn.Linear(128, action_dim)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        return self.fc2(x)

# Инициализация
state_dim = 4
action_dim = 2
q_network = QNetwork(state_dim, action_dim)
optimizer = optim.Adam(q_network.parameters(), lr=0.001)
loss_fn = nn.MSELoss()

# Пример обновления
state = torch.tensor([[0.1, 0.2, 0.3, 0.4]], dtype=torch.float32)
action = 1
reward = 1.0
next_state = torch.tensor([[0.2, 0.3, 0.4, 0.5]], dtype=torch.float32)

# Q-значение
q_value = q_network(state)[0, action]
target = reward + gamma * torch.max(q_network(next_state)).detach()

# Оптимизация
loss = loss_fn(q_value, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
"""
    pyperclip.copy(code)


def vgg16(idx: int = 0):
    if idx == 0:
        code = """
### 23. Типы задач машинного зрения, архитектура VGG16: преимущества и недостатки

---

### 1. **Типы задач машинного зрения, решаемые с помощью глубоких моделей**

Глубокие модели, особенно сверточные нейронные сети (CNN), активно применяются для решения различных задач машинного зрения.

#### Основные типы задач:

1. **Классификация изображений**:
   - Задача: определить, к какому классу принадлежит изображение.
   - Пример: распознавание объектов на изображении (кошка, собака, автомобиль).

2. **Сегментация изображений**:
   - Задача: выделить области изображения, относящиеся к разным классам.
   - Пример: выделение дорог на спутниковых снимках.

3. **Обнаружение объектов (Object Detection)**:
   - Задача: найти объекты на изображении и определить их границы.
   - Пример: нахождение пешеходов на изображении.

4. **Генерация изображений**:
   - Задача: создание новых изображений с использованием моделей, таких как GAN.
   - Пример: генерация лиц или стилизация изображений.

5. **Распознавание действий**:
   - Задача: анализировать последовательности изображений (видео) для распознавания действий.
   - Пример: обнаружение аномалий на производстве.

6. **Поиск изображений (Image Retrieval)**:
   - Задача: найти изображения, похожие на данное.
   - Пример: поиск товаров по фото.

---

### 2. **Архитектура VGG16**

VGG16 — это одна из популярных архитектур сверточных нейронных сетей, предложенная в 2014 году исследователями из Visual Geometry Group (VGG). Она была представлена в работе "Very Deep Convolutional Networks for Large-Scale Image Recognition".

#### Структура VGG16:
1. **Основная идея**:
   - Использование небольших свёрточных фильтров (\(3 \times 3\)) с шагом 1 и фиксированным padding, чтобы сохранить размер изображения.
   - Увеличение глубины сети для извлечения более сложных признаков.

2. **Архитектура**:
   - Состоит из 16 слоёв: 13 свёрточных слоёв и 3 полносвязных слоя.
   - После каждого блока свёртки применяется MaxPooling для уменьшения пространственного размера.
   - Последние слои — полносвязные, заканчивающиеся Softmax для классификации.

| Блок | Слои в блоке | Размер карты признаков |
|------|--------------|-------------------------|
| Conv1 | \(3 \times 3\), \(3 \to 64\), \(3 \times 3\), \(64 \to 64\) | \(224 \times 224 \to 112 \times 112\) |
| Conv2 | \(3 \times 3\), \(64 \to 128\), \(3 \times 3\), \(128 \to 128\) | \(112 \times 112 \to 56 \times 56\) |
| Conv3 | \(3 \times 3\), \(128 \to 256\), 3 слоя | \(56 \times 56 \to 28 \times 28\) |
| Conv4 | \(3 \times 3\), \(256 \to 512\), 3 слоя | \(28 \times 28 \to 14 \times 14\) |
| Conv5 | \(3 \times 3\), \(512 \to 512\), 3 слоя | \(14 \times 14 \to 7 \times 7\) |
| FC    | Полносвязные слои: \(4096 \to 4096 \to 1000\) | \(1 \times 1\) |

---

### 3. **Преимущества VGG16**

1. **Универсальность**:
   - VGG16 широко используется как базовая архитектура для многих задач в машинном зрении.

2. **Простота архитектуры**:
   - Использует только \(3 \times 3\) фильтры, что упрощает понимание и реализацию.

3. **Сложность признаков**:
   - Глубокая структура позволяет извлекать сложные признаки.

4. **Переносное обучение (Transfer Learning)**:
   - VGG16, предобученная на ImageNet, может быть использована для различных задач, требуя минимального дообучения.

---

### 4. **Недостатки VGG16**

1. **Высокая вычислительная сложность**:
   - Глубокая архитектура требует значительных ресурсов для обучения (время, память).

2. **Большое количество параметров**:
   - Около 138 миллионов параметров, что делает модель громоздкой и склонной к переобучению.

3. **Эффективность**:
   - Новые архитектуры, такие как ResNet, MobileNet, и EfficientNet, достигают лучших результатов с меньшими вычислительными затратами.

---

### 6. **Выводы**

1. **Типы задач машинного зрения**:
   - Классификация, сегментация, обнаружение объектов, генерация изображений.
2. **Архитектура VGG16**:
   - Глубокая сеть с последовательными \(3 \times 3\) свёртками и пуллингом.
   - Хорошо подходит для переноса обучения.
3. **Преимущества и недостатки**:
   - Простота и высокая эффективность в своём времени, но большие вычислительные требования и устаревание по сравнению с современными архитектурами.
"""
    else:
        code = """
#### Загрузка предобученной модели VGG16:
import torch
from torchvision import models

# Загрузка VGG16, предобученной на ImageNet
vgg16 = models.vgg16(pretrained=True)

# Проверка структуры модели
print(vgg16)

# Замена последнего слоя для классификации на 10 классов
vgg16.classifier[6] = torch.nn.Linear(4096, 10)

# Проверка обновлённой структуры
print(vgg16)

#### Пример обучения:
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# Датасет
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])

train_data = datasets.FakeData(transform=transform)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=32, shuffle=True)

# Оптимизатор и функция потерь
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(vgg16.parameters(), lr=0.001)

# Обучение
for epoch in range(3):
    for images, labels in train_loader:
        optimizer.zero_grad()
        outputs = vgg16(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
    print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")
"""
    pyperclip.copy(code)


def google_net(idx: int = 0):
    if idx == 0:
        code = """
### 24. Архитектура GoogLeNet, описание Inception module, свертка \(1 \times 1\), DepthConcat и сравнение с другими архитектурами

---

### 1. **Архитектура GoogLeNet**

GoogLeNet — это архитектура глубоких сверточных нейронных сетей, представленная в 2014 году командой Google Research в работе *"Going Deeper with Convolutions"*. Она завоевала первое место на соревновании ImageNet Large Scale Visual Recognition Challenge (ILSVRC-2014).

#### Ключевые характеристики:
1. **Глубокая сеть**:
   - GoogLeNet состоит из 22 уровней (глубина сети).
   - Включает 9 Inception-модулей.

2. **Inception-модуль**:
   - Ключевая инновация архитектуры, позволяющая эффективно обрабатывать признаки разного масштаба.

3. **Снижение числа параметров**:
   - Используется свертка \(1 \times 1\) для уменьшения размерности и повышения вычислительной эффективности.
   - Итого: 5 миллионов параметров (в сравнении с ~60 млн у AlexNet).

4. **Auxiliary Classifiers**:
   - Вспомогательные классификаторы добавлены в промежуточных слоях для борьбы с затуханием градиентов.

---

### 2. **Inception Module**

#### Основная идея:
Вместо того чтобы выбирать размер фильтра для свертки (\(1 \times 1\), \(3 \times 3\), \(5 \times 5\)), Inception module объединяет их и параллельно вычисляет признаки разного масштаба.

#### Структура:
Inception-модуль включает:
1. Свертки \(1 \times 1\):
   - Для сокращения размерности (и вычислительной нагрузки).
2. Свертки \(3 \times 3\) и \(5 \times 5\):
   - Для извлечения локальных признаков разного масштаба.
3. MaxPooling:
   - Для снижения размерности и выделения обобщенных признаков.
4. **DepthConcat**:
   - Результаты всех параллельных операций объединяются по глубине (каналам).

#### Пример структуры:
```
        Input
          ↓
   [1x1] [1x1 → 3x3] [1x1 → 5x5] [Pooling → 1x1]
          ↓
        Concatenate (по глубине)
```

---

### 3. **Свертка \(1 \times 1\)**

#### Основное назначение:
1. **Снижение размерности**:
   - Уменьшает количество каналов, что снижает вычислительные затраты.
   - Например, преобразование \(256 \to 64\) перед применением более сложных операций (\(3 \times 3\), \(5 \times 5\)).

2. **Добавление нелинейности**:
   - После свертки \(1 \times 1\) применяется ReLU, что позволяет сети изучать более сложные зависимости.

#### Пример:
Свертка \(1 \times 1\) с 256 входными каналами и 64 выходными:
\[
\text{Число параметров} = 1 \cdot 1 \cdot 256 \cdot 64 = 16,384
\]
Для сравнения, свертка \(3 \times 3\) без предварительного сокращения размерности потребовала бы:
\[
\text{Число параметров} = 3 \cdot 3 \cdot 256 \cdot 64 = 147,456
\]

---

### 4. **DepthConcat**

DepthConcat — это операция объединения признаков по оси каналов после выполнения всех параллельных операций в Inception-модуле.

#### Логика:
- Выходы каждого пути (например, свертки \(1 \times 1\), \(3 \times 3\), \(5 \times 5\)) добавляются как отдельные каналы.
- Например:
  - Вход: \(32 \times 32 \times 64\).
  - После \(1 \times 1\): \(32 \times 32 \times 32\).
  - После \(3 \times 3\): \(32 \times 32 \times 64\).
  - После \(5 \times 5\): \(32 \times 32 \times 32\).
  - После DepthConcat: \(32 \times 32 \times 192\).

---

### 5. **Сравнение GoogLeNet с другими архитектурами**

| **Архитектура** | **Число параметров** | **Глубина** | **Ключевые инновации** | **Недостатки** |
|------------------|-----------------------|-------------|-------------------------|-----------------|
| **AlexNet**     | 60 млн               | 8           | MaxPooling, Dropout    | Большое число параметров |
| **VGG16**       | 138 млн              | 16          | Простота, \(3 \times 3\) свертки | Огромные вычислительные затраты |
| **GoogLeNet**   | 5 млн                | 22          | Inception module, Auxiliary Classifiers | Сложность архитектуры |
| **ResNet**      | 25 млн (ResNet-50)   | 50          | Residual Connections   | Сложность для внедрения |

---

### 7. **Выводы**

1. **GoogLeNet**:
   - Впервые ввела Inception-модуль, позволив извлекать признаки разного масштаба.
   - Отличается низким числом параметров (всего 5 млн).
2. **Inception Module**:
   - Сочетает фильтры разного размера (\(1 \times 1\), \(3 \times 3\), \(5 \times 5\)) и MaxPooling.
   - Использует DepthConcat для объединения выходов.
3. **Сравнение**:
   - GoogLeNet значительно эффективнее по числу параметров, но уступает современным архитектурам (например, ResNet) по точности.
"""
    else:
        code = """
import torch
import torch.nn as nn

# Реализация Inception Module
class InceptionModule(nn.Module):
    def __init__(self, in_channels, out_1x1, red_3x3, out_3x3, red_5x5, out_5x5, pool_proj):
        super(InceptionModule, self).__init__()
        self.branch1 = nn.Conv2d(in_channels, out_1x1, kernel_size=1)

        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, red_3x3, kernel_size=1),
            nn.Conv2d(red_3x3, out_3x3, kernel_size=3, padding=1)
        )

        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, red_5x5, kernel_size=1),
            nn.Conv2d(red_5x5, out_5x5, kernel_size=5, padding=2)
        )

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, pool_proj, kernel_size=1)
        )

    def forward(self, x):
        branch1 = self.branch1(x)
        branch2 = self.branch2(x)
        branch3 = self.branch3(x)
        branch4 = self.branch4(x)
        return torch.cat([branch1, branch2, branch3, branch4], dim=1)

# Пример использования
x = torch.randn(1, 192, 32, 32)  # Пример входных данных
inception = InceptionModule(192, 64, 96, 128, 16, 32, 32)
output = inception(x)
print("Output shape:", output.shape)  # Результат: [1, 256, 32, 32]
"""
    pyperclip.copy(code)


def resnet(idx: int = 0):
    if idx == 0:
        code = """
### 25. Архитектуры с residual connections, основные принципы, нарушение симметрии, сравнение с другими архитектурами

---

### 1. **Архитектуры с residual connections**

**Residual connections** (остаточные соединения) впервые были представлены в архитектуре **ResNet** (Residual Network), предложенной в 2015 году в статье *"Deep Residual Learning for Image Recognition"*.

#### Цель:
- Решить проблемы глубоких нейронных сетей:
  - **Затухание градиентов**: в очень глубоких сетях градиенты уменьшаются до нуля.
  - **Деградация обучения**: при увеличении глубины модели её производительность может ухудшаться.

#### Ключевая идея:
- Добавить прямые пропуски (skip connections), позволяя слоям обучаться на **остаточной функции**:
  \[
  \mathcal{F}(x) = H(x) - x \quad \implies \quad H(x) = \mathcal{F}(x) + x
  \]
  где:
  - \(H(x)\) — целевая функция, которую должен изучить слой.
  - \(\mathcal{F}(x)\) — остаточная функция, которую изучает слой, начиная с \(\mathcal{F}(x) = 0\).

---

### 2. **Основные принципы residual connections**

1. **Добавление прямого пути (skip connection)**:
   - Выход из одного слоя добавляется к выходу другого слоя.
   - Например:
     \[
     y = \mathcal{F}(x, W) + x
     \]
     где \(\mathcal{F}(x, W)\) — результат преобразования (например, свертка), а \(x\) — вход.

2. **Стабилизация обучения**:
   - Прямой путь \(x\) позволяет сохранять информацию из предыдущих слоёв, снижая затухание градиентов.

3. **Ускорение сходимости**:
   - Легче обучать остаточную функцию (\(\mathcal{F}(x)\)).

4. **Гибкость**:
   - Можно пропускать данные через несколько слоёв (deep residual block).

---

### 3. **Нарушение симметрии глубоких ИНС с помощью residual connections**

#### Проблема симметрии:
В традиционных глубоких сетях без остаточных соединений может возникнуть **сильная корреляция между соседними слоями**, так как каждый слой напрямую зависит от выхода предыдущего. Это приводит к:
- Переобучению.
- Затуханию или взрыву градиентов.

#### Решение через residual connections:
- **Нарушение симметрии**:
  - Skip connections добавляют **альтернативный путь для градиентов**, позволяя передавать информацию напрямую через несколько слоёв.
- **Устойчивость обучения**:
  - Остаточные функции (\(\mathcal{F}(x)\)) обучаются быстрее, так как начинают с малых значений (ближе к нулю).

---

### 4. **Пример структуры residual block**

#### Обычный residual block:
1. Два слоя свёрток с активацией (ReLU) и BatchNorm.
2. Прямое соединение (skip connection):
   \[
   y = \text{ReLU}(\mathcal{F}(x, W) + x)
   \]

---

### 5. **Сравнение ResNet с другими архитектурами**

| **Архитектура** | **Число параметров** | **Глубина** | **Ключевые инновации**        | **Недостатки**                    |
|------------------|-----------------------|-------------|--------------------------------|------------------------------------|
| **AlexNet**     | ~60 млн              | 8           | MaxPooling, Dropout           | Огромное число параметров         |
| **VGG16**       | ~138 млн             | 16          | Простота, последовательные \(3 \times 3\) | Высокие вычислительные затраты    |
| **GoogLeNet**   | ~5 млн               | 22          | Inception-модуль              | Сложность архитектуры             |
| **ResNet**      | ~25 млн (ResNet-50)  | 50+         | Residual Connections          | Сложность в реализации            |

---

### 6. **Преимущества ResNet**

1. **Обучение глубоких сетей**:
   - Возможность тренировать очень глубокие сети (например, ResNet-152).

2. **Снижение затухания градиентов**:
   - Градиенты передаются через skip connections, что предотвращает их затухание.

3. **Гибкость**:
   - Легко масштабируется для задач различной сложности.

4. **Улучшенная сходимость**:
   - Быстрая сходимость благодаря обучению остаточной функции.

---

### 7. **Недостатки ResNet**

1. **Сложность архитектуры**:
   - Усложнение реализации и трудность настройки для кастомных задач.

2. **Избыточность**:
   - В очень глубоких сетях могут возникать проблемы с переобучением из-за лишних слоёв.

---

### 9. **Выводы**

1. **Residual connections** позволяют эффективно обучать глубокие нейронные сети, решая проблемы затухания градиентов и деградации.
2. ResNet и его производные (ResNet-50, ResNet-152) являются стандартом для многих задач машинного зрения.
3. В сравнении с другими архитектурами ResNet предлагает отличную производительность и глубину при разумном числе параметров.
"""
    else:
        code = """


### 8. **Пример ResNet-50 в PyTorch**

from torchvision.models import resnet50

# Загрузка предобученной модели ResNet-50
model = resnet50(pretrained=True)

# Изменение последнего слоя для классификации на 10 классов
model.fc = nn.Linear(2048, 10)

# Проверка структуры
print(model)

import torch
import torch.nn as nn

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # Прямое соединение (для изменения числа каналов или шага)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = torch.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        return torch.relu(out)

# Пример использования блока
x = torch.randn(1, 64, 32, 32)  # 1 изображение, 64 канала, размер 32x32
block = ResidualBlock(64, 64)
output = block(x)
print("Output shape:", output.shape)  # [1, 64, 32, 32]
"""
    pyperclip.copy(code)


def unet(idx: int = 0):
    if idx == 0:
        code = """
### 26. Архитектура U-Net: общие принципы, описание, residual connections и применение в задачах сегментации

---

### 1. **Общие принципы U-Net**

**U-Net** — это архитектура сверточной нейронной сети, предложенная в 2015 году для решения задач сегментации изображений. Она была разработана для медицинских изображений, но сейчас широко применяется для сегментации в других областях.

#### Основная цель:
- Изучение пространственной структуры изображения для предсказания класса каждого пикселя (задача сегментации).

#### Ключевые особенности:
1. **Симметричная U-образная структура**:
   - Архитектура состоит из двух частей:
     - **Контрактивная часть (Encoder)**: извлекает признаки, уменьшая размерность изображения.
     - **Экспансивная часть (Decoder)**: восстанавливает разрешение изображения, используя пропущенные признаки.
   - Название "U-Net" связано с формой архитектуры.

2. **Скип-соединения (skip connections)**:
   - Признаки из Encoder передаются напрямую в Decoder, сохраняя пространственную информацию.

3. **Полная свертка (Fully Convolutional Network)**:
   - U-Net не содержит полносвязных слоев, что делает её применимой для изображений любого размера.

---

### 2. **Описание архитектуры U-Net**

#### Общая структура:
1. **Контрактивная часть (Encoder)**:
   - Состоит из повторяющихся блоков:
     - Две последовательные свёртки \(3 \times 3\) (Conv → ReLU).
     - MaxPooling \(2 \times 2\) для уменьшения размера изображения.
   - На каждом уровне увеличивается количество каналов, чтобы захватить больше признаков.

2. **Боттлнек (Bottleneck)**:
   - Центральный узел сети, где размер изображения минимальный, а количество каналов максимальное.
   - Позволяет обобщить информацию на высоком уровне.

3. **Экспансивная часть (Decoder)**:
   - Состоит из:
     - Транспонированных свёрток (Transposed Convolution) или UpSampling для увеличения разрешения.
     - Конкатенации с соответствующими признаками из Encoder через skip connections.
     - Двух последовательных свёрток \(3 \times 3\) (Conv → ReLU).

4. **Выходной слой**:
   - Последний слой — свёртка \(1 \times 1\), которая преобразует многоканальные данные в выходной канал (например, карту сегментации).

#### Графическое представление:
```
Input → [Encoder] → Bottleneck → [Decoder + Skip Connections] → Output
```

---

### 3. **Использование residual connections в U-Net**

#### Зачем нужны skip connections?
1. **Сохранение пространственной информации**:
   - В процессе свёртки и пуллинга детали теряются. Skip connections восстанавливают эти детали, соединяя низкоуровневые признаки из Encoder с высокоуровневыми признаками в Decoder.

2. **Улучшение сходимости**:
   - Прямые пути для градиентов стабилизируют обучение.

#### Пример реализации residual connections:
В некоторых вариантах U-Net (например, ResUNet) используются **residual connections** для каждой свёртки:
\[
y = \text{ReLU}(\mathcal{F}(x) + x)
\]
где \(\mathcal{F}(x)\) — выход из сверточного блока, а \(x\) — входной сигнал.

---

### 5. **Применение U-Net в задаче сегментации**

#### Пример задачи:
Сегментация медицинских изображений, где требуется выделить органы или аномалии на снимках.

#### Процесс:
1. **Входные данные**:
   - Чёрно-белые (1 канал) или цветные изображения (3 канала).
2. **Выход**:
   - Карта сегментации, где каждый пиксель отнесён к определённому классу.
3. **Функция потерь**:
   - Для бинарной сегментации: Binary Cross-Entropy (BCE).
   - Для многоклассовой сегментации: Cross-Entropy Loss.
   - Можно использовать Dice Loss для точного учета совпадений.

---

### 6. **Сравнение U-Net с другими архитектурами**

| **Архитектура** | **Цель**                    | **Ключевые особенности**          | **Недостатки**                         |
|------------------|-----------------------------|------------------------------------|----------------------------------------|
| **U-Net**       | Сегментация изображений     | Skip connections, симметрия       | Трудности с большими изображениями     |
| **SegNet**      | Сегментация изображений     | Использует пуллинг индексы        | Высокие вычислительные затраты         |
| **DeepLab**     | Продвинутая сегментация     | Dilated convolutions              | Более сложная архитектура              |
| **PSPNet**      | Контекстная информация      | Pyramid Pooling Module            | Требует больше памяти                  |

---

### 7. **Выводы**

1. **U-Net**:
   - Отлично подходит для задач сегментации благодаря U-образной архитектуре и skip connections.
2. **Residual connections**:
   - Варианты U-Net с residual connections (например, ResUNet) улучшают стабильность обучения.
3. **Применение**:
   - Используется для медицинских, спутниковых и промышленных изображений.
4. **Сравнение**:
   - Простота U-Net делает её базовой архитектурой для задач сегментации, хотя есть более сложные модели с улучшенной производительностью.
"""
    else:
        code = """
import torch
import torch.nn as nn

# U-Net блок
class UNetBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(UNetBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.conv1(x))
        x = self.relu(self.conv2(x))
        return x

# U-Net архитектура
class UNet(nn.Module):
    def __init__(self):
        super(UNet, self).__init__()
        # Encoder
        self.enc1 = UNetBlock(1, 64)
        self.enc2 = UNetBlock(64, 128)
        self.enc3 = UNetBlock(128, 256)
        self.enc4 = UNetBlock(256, 512)

        # Bottleneck
        self.bottleneck = UNetBlock(512, 1024)

        # Decoder
        self.up4 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)
        self.dec4 = UNetBlock(1024, 512)

        self.up3 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)
        self.dec3 = UNetBlock(512, 256)

        self.up2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
        self.dec2 = UNetBlock(256, 128)

        self.up1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)
        self.dec1 = UNetBlock(128, 64)

        # Выходной слой
        self.out = nn.Conv2d(64, 1, kernel_size=1)

    def forward(self, x):
        # Encoder
        enc1 = self.enc1(x)
        enc2 = self.enc2(nn.MaxPool2d(2)(enc1))
        enc3 = self.enc3(nn.MaxPool2d(2)(enc2))
        enc4 = self.enc4(nn.MaxPool2d(2)(enc3))

        # Bottleneck
        bottleneck = self.bottleneck(nn.MaxPool2d(2)(enc4))

        # Decoder
        dec4 = self.dec4(torch.cat([self.up4(bottleneck), enc4], dim=1))
        dec3 = self.dec3(torch.cat([self.up3(dec4), enc3], dim=1))
        dec2 = self.dec2(torch.cat([self.up2(dec3), enc2], dim=1))
        dec1 = self.dec1(torch.cat([self.up1(dec2), enc1], dim=1))

        # Выход
        return self.out(dec1)

# Проверка модели
model = UNet()
x = torch.randn(1, 1, 256, 256)  # Пример входного изображения
output = model(x)
print("Output shape:", output.shape)  # Результат: [1, 1, 256, 256]
"""
    pyperclip.copy(code)


def gradcam(idx: int = 0):
    if idx == 0:
        code = """
### 27. Механизмы внимания в машинном зрении и метод Grad-CAM

---

### 1. **Подходы к использованию механизма внимания в глубоких моделях машинного зрения**

Механизм внимания (Attention Mechanism) позволяет моделям концентрироваться на наиболее значимых частях изображения или данных, улучшая производительность в задачах классификации, сегментации, детекции объектов и других.

#### Основные подходы к использованию внимания:

1. **Channel Attention**:
   - Оценивает важность каждого канала карты признаков.
   - Пример: Squeeze-and-Excitation Networks (SE-Net), где веса каналов обновляются на основе их значимости.

2. **Spatial Attention**:
   - Определяет, какие пространственные области изображения важны для текущей задачи.
   - Пример: выделение объектов на изображении для детекции.

3. **Self-Attention**:
   - Каждая позиция в изображении взаимодействует с другими позициями, чтобы выявить глобальные зависимости.
   - Пример: Vision Transformers (ViT).

4. **Combined Attention**:
   - Сочетание каналового и пространственного внимания (например, CBAM: Convolutional Block Attention Module).

5. **Cross-Attention**:
   - Используется в мультимодальных задачах (например, соотношение текстовых и визуальных данных).

---

### 2. **Механизм и логика Grad-CAM**

Grad-CAM (Gradient-weighted Class Activation Mapping) — это метод визуализации, позволяющий интерпретировать решения глубоких моделей. Он помогает понять, какие части изображения оказали наибольшее влияние на предсказание модели.

#### Основная идея:
- Grad-CAM использует градиенты выходного предсказания по активациям определённого слоя, чтобы вычислить "важность" каждой пространственной позиции.

---

#### 2.1 **Механизм Grad-CAM**

1. **Выбор слоя**:
   - Обычно выбирается последний сверточный слой, так как он содержит высокоуровневые признаки с пространственной информацией.

2. **Градиенты предсказания**:
   - Вычисляются градиенты выхода модели (\(y_c\)) по активациям \(A_k\) выбранного слоя:
     \[
     \frac{\partial y_c}{\partial A_k}
     \]
     где \(y_c\) — выходная вероятность класса \(c\).

3. **Среднее по каналам**:
   - Градиенты усредняются по пространственным измерениям, чтобы получить важность каждого канала:
     \[
     \alpha_k^c = \frac{1}{Z} \sum_i \sum_j \frac{\partial y_c}{\partial A_{k, ij}}
     \]
     где \(Z\) — общее число элементов в карте \(A_k\).

4. **Взвешивание активаций**:
   - Активации карты \(A_k\) взвешиваются по их важности:
     \[
     L^c_{\text{Grad-CAM}} = \text{ReLU} \left( \sum_k \alpha_k^c A_k \right)
     \]
     Использование ReLU гарантирует, что только положительные влияния учитываются.

5. **Интерполяция и наложение**:
   - Карта \(L^c_{\text{Grad-CAM}}\) интерполируется до размера исходного изображения и накладывается на него для визуализации.

---

#### 2.2 **Алгоритм Grad-CAM**

1. Пропустите изображение через модель и получите предсказание.
2. Вычислите градиенты выходного класса по активациям слоя.
3. Усредните градиенты по пространственным измерениям.
4. Сложите взвешенные активации и примените ReLU.
5. Масштабируйте карту важности до размера входного изображения.
6. Визуализируйте карту важности поверх оригинального изображения.

---

### 4. **Преимущества Grad-CAM**

1. **Интерпретируемость**:
   - Показывает, какие части изображения влияют на решение модели.

2. **Универсальность**:
   - Совместим с любой CNN.

3. **Простота реализации**:
   - Требует минимальных изменений в архитектуре модели.

---

### 5. **Сравнение Grad-CAM с другими подходами**

| **Метод**        | **Назначение**                          | **Особенности**                                |
|-------------------|-----------------------------------------|------------------------------------------------|
| **CAM**          | Локализация объектов                   | Требует изменения архитектуры модели           |
| **Grad-CAM**     | Универсальная визуализация внимания     | Подходит для любой CNN                         |
| **Smooth Grad-CAM** | Сглаживание карты внимания             | Снижает шум, увеличивая стабильность          |
| **Attention Maps** | Карты внимания для Transformer моделей | Ориентированы на глобальные взаимосвязи        |

---

### 6. **Выводы**

1. **Механизмы внимания**:
   - Усовершенствуют модели машинного зрения, позволяя концентрироваться на важных областях.
2. **Grad-CAM**:
   - Удобный и универсальный метод визуализации, который улучшает интерпретируемость глубоких моделей.
3. **Практическое применение**:
   - Grad-CAM используется для диагностики ошибок моделей, визуализации внимания и интерпретации сложных решений.
"""
    else:
        code = """
### 3. **Реализация Grad-CAM в PyTorch**
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# Загрузка предобученной модели
model = models.resnet50(pretrained=True)
model.eval()

# Функция Grad-CAM
class GradCAM:
    def __init__(self, model, target_layer):
        self.model = model
        self.target_layer = target_layer
        self.gradients = None

        # Хук для градиентов
        self.target_layer.register_backward_hook(self.save_gradients)

    def save_gradients(self, module, grad_in, grad_out):
        self.gradients = grad_out[0]

    def __call__(self, x, class_idx):
        # Прямой проход
        activations = None
        for name, module in self.model.named_children():
            x = module(x)
            if name == self.target_layer:
                activations = x

        # Обратное распространение
        self.model.zero_grad()
        class_score = x[:, class_idx].squeeze()
        class_score.backward()

        # Вычисление Grad-CAM
        pooled_gradients = torch.mean(self.gradients, dim=[0, 2, 3])
        activations = activations[0]  # Извлечение активаций

        for i in range(activations.shape[0]):
            activations[i, :, :] *= pooled_gradients[i]

        heatmap = torch.mean(activations, dim=0).detach().numpy()
        heatmap = np.maximum(heatmap, 0)
        heatmap /= np.max(heatmap)  # Нормализация
        return heatmap

# Пример использования Grad-CAM
image_path = 'example.jpg'
image = Image.open(image_path).convert('RGB')
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

input_tensor = transform(image).unsqueeze(0)

# Применение Grad-CAM
grad_cam = GradCAM(model, target_layer='layer4')
heatmap = grad_cam(input_tensor, class_idx=243)  # Например, класс 243 (собака)

# Наложение heatmap на изображение
heatmap = np.uint8(255 * heatmap)
heatmap = Image.fromarray(heatmap).resize(image.size, Image.BILINEAR)
heatmap = np.array(heatmap)
superimposed_img = np.array(image) * 0.5 + heatmap[:, :, np.newaxis] * 0.5

# Визуализация
plt.imshow(superimposed_img.astype('uint8'))
plt.axis('off')
plt.show()
"""
    pyperclip.copy(code)


def vit_swin(idx: int = 0):
    if idx == 0:
        code = """
### 28. Применение Transformer в компьютерном зрении и архитектура Swin Transformer

---

### 1. **Применение модуля Transformer в компьютерном зрении**

Transformer — архитектура, первоначально разработанная для обработки последовательностей (например, текста), благодаря механизму **Self-Attention**. В компьютерном зрении (CV) Transformer был адаптирован для обработки изображений, заменяя традиционные сверточные слои.

#### Основные принципы применения:
1. **Преобразование изображения в последовательность**:
   - Изображение делится на фиксированные патчи (\(16 \times 16\)).
   - Каждый патч преобразуется в одномерный вектор (например, через линейный слой).

2. **Self-Attention**:
   - Позволяет каждому патчу взаимодействовать с другими, выявляя как локальные, так и глобальные взаимосвязи.

3. **Embedding и позиционная информация**:
   - Для учета порядка патчей добавляются позиционные эмбеддинги.

4. **Классификация или сегментация**:
   - На выходе применяется классификационная или сегментационная голова (например, MLP-слой).

#### Преимущества Transformers в CV:
- **Глобальный контекст**: Self-Attention анализирует взаимосвязи между любыми частями изображения.
- **Гибкость**: Подходит для задач классификации, сегментации, детекции объектов.
- **Простая архитектура**: Избавляется от жестких локальных ограничений сверточных слоев.

#### Примеры моделей:
- **Vision Transformer (ViT)**: Первый Transformer, адаптированный для CV.
- **DeiT (Data-Efficient Transformer)**: Улучшенная версия ViT с оптимизированными данными.
- **Swin Transformer**: Применяет оконное внимание, улучшая масштабируемость и эффективность.

---

### 2. **Архитектура Swin Transformer**

Swin Transformer (Shifted Window Transformer) был предложен в работе *"Swin Transformer: Hierarchical Vision Transformer using Shifted Windows"*. Эта архитектура отличается от ViT улучшением локальности и масштабируемости.

#### Основные особенности Swin Transformer:
1. **Оконное внимание (Window Attention)**:
   - Изображение делится на фиксированные окна (\(7 \times 7\)), и Self-Attention вычисляется только внутри каждого окна.
   - Снижает вычислительную сложность с \(O(N^2)\) до \(O((M^2) \cdot N / M)\), где \(M\) — размер окна, \(N\) — общее число патчей.

2. **Сдвиг окон (Shifted Windows)**:
   - Чтобы учесть информацию за пределами окна, окна сдвигаются на фиксированное расстояние между слоями.

3. **Иерархическая структура**:
   - В отличие от ViT, Swin Transformer строит иерархию признаков (как в сверточных сетях), уменьшая разрешение на каждом уровне и увеличивая число каналов.

4. **Скалируемость**:
   - Архитектура подходит для различных задач (например, классификация, сегментация, детекция объектов).

---

#### Структура Swin Transformer:

1. **Patch Partition**:
   - Делит изображение на патчи (\(4 \times 4\)) и представляет их как векторы.
   - Выход: \(H/4 \times W/4 \times C\), где \(H\) и \(W\) — размеры изображения, \(C\) — число каналов.

2. **Patch Merging**:
   - Уменьшает пространственное разрешение, увеличивая число каналов.
   - Аналогично операциям пуллинга в CNN.

3. **Shifted Window Attention**:
   - Каждый блок состоит из двух этапов:
     - **Оконное внимание (Window Attention)**: Self-Attention внутри окон.
     - **Сдвиг окон (Shifted Window Attention)**: Обеспечивает перекрытие между соседними окнами.

4. **MLP**:
   - Используется для обработки эмбеддингов на каждом этапе.

5. **Выходная голова**:
   - Применяется для конкретной задачи (например, классификации или сегментации).

---

#### Пример архитектуры Swin Transformer для классификации:

| Этап                     | Разрешение карты признаков | Число окон | Операции                     |
|---------------------------|----------------------------|------------|------------------------------|
| Patch Partition           | \(224 \times 224 \to 56 \times 56\) | —          | Разделение на патчи          |
| Stage 1                   | \(56 \times 56\)          | \(8 \times 8\) | Window Attention             |
| Stage 2                   | \(28 \times 28\)          | \(4 \times 4\) | Shifted Window Attention     |
| Stage 3                   | \(14 \times 14\)          | \(2 \times 2\) | Shifted Window Attention     |
| Stage 4                   | \(7 \times 7\)            | \(1 \times 1\) | Global Attention (полный контекст) |

---

### 4. **Сравнение Swin Transformer с другими архитектурами**

| **Архитектура**       | **Особенности**                            | **Преимущества**                        | **Недостатки**                         |
|-----------------------|--------------------------------------------|-----------------------------------------|----------------------------------------|
| **CNN (ResNet)**      | Локальные свёртки                         | Эффективность                          | Ограниченный глобальный контекст       |
| **ViT**               | Глобальное Self-Attention                 | Мощное извлечение признаков             | Высокая сложность для больших данных   |
| **Swin Transformer**  | Локальное оконное внимание + иерархия     | Масштабируемость, эффективность         | Сложность реализации                   |

---

### 5. **Выводы**

1. **Transformers в CV**:
   - Они заменяют CNN в задачах классификации, сегментации и детекции объектов благодаря механизму внимания.
2. **Swin Transformer**:
   - Применяет локальное оконное внимание и иерархическую обработку для эффективного анализа изображений.
3. **Практическое значение**:
   - Swin Transformer подходит для широкого спектра задач компьютерного зрения, сочетая гибкость ViT и эффективность CNN.
"""
    else:
        code = """
### 3. **Реализация Swin Transformer в PyTorch**
import torch
import torch.nn as nn

class WindowAttention(nn.Module):
    def __init__(self, dim, num_heads, window_size):
        super(WindowAttention, self).__init__()
        self.dim = dim
        self.num_heads = num_heads
        self.window_size = window_size
        self.scale = (dim // num_heads) ** -0.5
        self.softmax = nn.Softmax(dim=-1)

        self.qkv = nn.Linear(dim, dim * 3, bias=True)
        self.proj = nn.Linear(dim, dim)

    def forward(self, x):
        B, N, C = x.shape
        qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]

        attn = (q @ k.transpose(-2, -1)) * self.scale
        attn = self.softmax(attn)
        out = (attn @ v).transpose(1, 2).reshape(B, N, C)
        return self.proj(out)

class SwinBlock(nn.Module):
    def __init__(self, dim, input_resolution, num_heads, window_size, shift_size):
        super(SwinBlock, self).__init__()
        self.window_size = window_size
        self.shift_size = shift_size
        self.attn = WindowAttention(dim, num_heads, window_size)

    def forward(self, x):
        # Реализация оконного внимания и сдвига
        B, H, W, C = x.shape
        x = x.view(B, H * W, C)
        x = self.attn(x)
        return x.view(B, H, W, C)

# Проверка модели
B, H, W, C = 1, 56, 56, 96
x = torch.randn(B, H, W, C)
block = SwinBlock(dim=96, input_resolution=(56, 56), num_heads=4, window_size=7, shift_size=0)
output = block(x)
print("Output shape:", output.shape)
"""
    pyperclip.copy(code)


def decode(idx: int = 0):
    if idx == 0:
        code = """
### 29. Векторная интерпретация весов скрытого слоя многослойного перцептрона и задача восстановления входного сигнала после сжатия

---

### 1. **Векторная интерпретация весов скрытого слоя многослойного перцептрона**

В многослойном перцептроне (MLP) веса скрытого слоя можно интерпретировать как **векторное пространство** в многомерном пространстве входных признаков.

#### Основные идеи:

1. **Проекции на новое пространство**:
   - Каждый нейрон скрытого слоя преобразует входной вектор в новое пространство, выполняя линейное преобразование:
     \[
     z_j = \sum_{i} w_{ij} x_i + b_j
     \]
     где \(x_i\) — входной сигнал, \(w_{ij}\) — веса, \(b_j\) — смещение.

   - Веса \(w_{ij}\) можно интерпретировать как координаты базисного вектора в новом пространстве, а линейное преобразование как проекцию входного вектора на этот базис.

2. **Выявление зависимостей**:
   - Нейроны скрытого слоя "учат" представления, отражающие внутренние зависимости и особенности данных.

3. **Измерение важности признаков**:
   - Веса нейрона показывают, какие входные признаки (компоненты \(x_i\)) имеют наибольшее влияние на его активацию.

#### Пример интерпретации:
- Для задачи классификации изображения веса первого слоя могут представлять шаблоны или "края", а последующие слои — более сложные формы.

---

### 2. **Задача восстановления входного сигнала после сжатия**

Эта задача связана с использованием нейронных сетей для восстановления (реконструкции) входных данных из их сжатого представления. Она лежит в основе **автоэнкодеров**.

#### Постановка задачи:
1. **Сжатие** (Encoding):
   - Входной сигнал \(x \in \mathbb{R}^n\) преобразуется в скрытое представление \(h \in \mathbb{R}^m\), где \(m < n\):
     \[
     h = f_{\text{encode}}(x) = \sigma(W_{\text{encode}} x + b_{\text{encode}})
     \]
     где \(W_{\text{encode}}\) — матрица весов, \(b_{\text{encode}}\) — смещение.

2. **Восстановление** (Decoding):
   - Скрытое представление \(h\) преобразуется обратно в исходное пространство:
     \[
     \hat{x} = f_{\text{decode}}(h) = \sigma(W_{\text{decode}} h + b_{\text{decode}})
     \]

3. **Цель**:
   - Минимизировать разницу между входом \(x\) и его реконструкцией \(\hat{x}\) с помощью функции потерь:
     \[
     \mathcal{L}(x, \hat{x}) = \|x - \hat{x}\|^2
     \]

---

### 3. **Автоэнкодеры (Autoencoders)**

Автоэнкодеры — это нейронные сети, обучаемые для сжатия данных (в Encoder) и их последующего восстановления (в Decoder).

#### Архитектура:
1. **Encoder**:
   - Сжимает входные данные в представление меньшей размерности.
   - Например, для изображения \(28 \times 28\) (784 признаков) представление может быть сжато до 32 признаков.

2. **Latent Space**:
   - Пространство скрытых представлений, в котором данные имеют компактное описание.

3. **Decoder**:
   - Восстанавливает данные из скрытого представления.

#### Пример:
- Вход: изображение.
- Скрытое представление: компактное описание объекта на изображении.
- Выход: восстановленное изображение.

---

### 5. **Применение автоэнкодеров**

1. **Сжатие данных**:
   - Пример: уменьшение размерности изображений или звуковых сигналов.
2. **Удаление шума (Denoising Autoencoder)**:
   - Входные данные с шумом, выход — очищенные данные.
3. **Аномалия детекции**:
   - Низкая способность восстановления для аномальных данных.
4. **Генерация данных**:
   - На основе вариационных автоэнкодеров (VAE).

---

### 6. **Выводы**

1. **Интерпретация весов скрытого слоя**:
   - Веса представляют собой базисное пространство, описывающее важные признаки данных.
2. **Реконструкция входных данных**:
   - Автоэнкодеры обучаются восстанавливать входы из их сжатого представления.
3. **Практическое применение**:
   - Автоэнкодеры используются для уменьшения размерности, удаления шума, генерации данных и детекции аномалий.
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

# Определение автоэнкодера
class Autoencoder(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super(Autoencoder, self).__init__()
        # Encoder
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU()
        )
        # Decoder
        self.decoder = nn.Sequential(
            nn.Linear(hidden_dim, input_dim),
            nn.Sigmoid()  # Для нормализованных данных (например, изображений)
        )

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded

# Размер входных данных и скрытого слоя
input_dim = 784  # Для изображений 28x28
hidden_dim = 32

# Инициализация модели
model = Autoencoder(input_dim, hidden_dim)

# Функция потерь и оптимизатор
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Пример данных (например, из MNIST)
data = torch.randn(64, input_dim)  # Мини-батч из 64 изображений 28x28

# Обучение автоэнкодера
for epoch in range(100):
    optimizer.zero_grad()
    output = model(data)
    loss = criterion(output, data)  # Сравнение выхода с входом
    loss.backward()
    optimizer.step()
    if (epoch + 1) % 10 == 0:
        print(f"Epoch [{epoch+1}/100], Loss: {loss.item():.4f}")
"""
    pyperclip.copy(code)


def autoencoder(idx: int = 0):
    if idx == 0:
        code = """
### 30. Автоэнкодеры: линейные, нелинейные, латентное пространство, интерполяция и экстраполяция

---

### 1. **Автоэнкодеры**

Автоэнкодеры (Autoencoders) — это архитектура нейронной сети, предназначенная для обучения компактного представления данных, называемого **латентным пространством (latent space)**. Они состоят из двух частей:
- **Encoder**: Сжимает входные данные \(x\) в латентное представление \(h\).
- **Decoder**: Восстанавливает входные данные \(\hat{x}\) из латентного представления \(h\).

#### Цель:
Минимизировать ошибку реконструкции между входными данными \(x\) и их восстановлением \(\hat{x}\):
\[
\mathcal{L}(x, \hat{x}) = \|x - \hat{x}\|^2
\]

---

### 2. **Линейный автоэнкодер**

#### Определение:
Линейный автоэнкодер использует только линейные преобразования для кодирования и декодирования:
\[
\text{Encoder: } h = Wx + b, \quad \text{Decoder: } \hat{x} = W'h + b'
\]
где \(W\), \(W'\) — матрицы весов, \(b\), \(b'\) — смещения.

#### Связь с PCA:
Линейный автоэнкодер эквивалентен **анализу главных компонент (PCA)**, если:
1. Используется квадратичная функция потерь (\(L_2\)-норма).
2. Количество нейронов в латентном пространстве равно числу главных компонент.

**Обоснование:**
- Латентное пространство линейного автоэнкодера совпадает с подпространством, найденным методом PCA.
- Веса \(W\) линейного автоэнкодера приближают собственные векторы матрицы ковариации данных.

#### Пример:
Если входные данные \(x \in \mathbb{R}^{n}\), а латентное пространство имеет размерность \(k\), то автоэнкодер будет находить первые \(k\) главных компонент.

---

### 3. **Нелинейные и глубокие автоэнкодеры**

#### Нелинейные автоэнкодеры:
- Используют нелинейные функции активации (например, ReLU, Sigmoid).
- Способны моделировать сложные нелинейные зависимости в данных.

#### Глубокие автоэнкодеры:
- Состоят из нескольких скрытых слоев как в Encoder, так и в Decoder.
- Каждый слой изучает более сложные уровни абстракции данных.
- Пример архитектуры:
  - Encoder: \(784 \to 512 \to 256 \to 64\).
  - Decoder: \(64 \to 256 \to 512 \to 784\).

#### Преимущества:
- Нелинейные автоэнкодеры превосходят линейные при работе с данными, лежащими на сложных многообразиях (например, изображения, текст, звук).

---

### 4. **Области значений латентного пространства**

#### Определение:
Латентное пространство — это компактное представление исходных данных в новом пространстве. Оно должно:
1. Захватывать ключевые признаки данных.
2. Обеспечивать реконструкцию входов.

#### Структура латентного пространства:
- **Локальная структура**:
  - Близкие точки в латентном пространстве соответствуют схожим данным.
- **Глобальная структура**:
  - Латентное пространство отражает сложные зависимости данных.

#### Проблемы:
1. В линейных автоэнкодерах латентное пространство имеет ограниченную выразительность.
2. Нелинейные автоэнкодеры часто создают латентное пространство без явно определённой структуры, что затрудняет интерполяцию.

---

### 5. **Интерполяция и экстраполяция в латентном пространстве**

#### Интерполяция:
- Интерполяция позволяет генерировать новые точки данных, находящиеся между известными примерами.
- Пример: Линейная интерполяция между двумя точками \(h_1\) и \(h_2\) в латентном пространстве:
  \[
  h_{\text{interp}} = \alpha h_1 + (1 - \alpha) h_2, \quad \alpha \in [0, 1]
  \]

#### Экстраполяция:
- Генерация данных за пределами области значений латентного пространства.
- Проблема: Может приводить к некорректным или нереалистичным результатам, так как автоэнкодеры не обучены на таких данных.

---

### 7. **Выводы**

1. **Линейный автоэнкодер**:
   - Эквивалентен PCA и полезен для анализа линейных зависимостей.
2. **Нелинейные и глубокие автоэнкодеры**:
   - Способны моделировать сложные нелинейные структуры данных.
3. **Латентное пространство**:
   - Представляет сжатую форму данных, сохраняя их ключевые особенности.
4. **Интерполяция и экстраполяция**:
   - Интерполяция внутри латентного пространства позволяет создавать реалистичные данные, тогда как экстраполяция часто приводит к неестественным результатам.
"""
    else:
        code = """
import torch
import torch.nn as nn
import numpy as np

# Генерация двух латентных точек
h1 = torch.tensor([0.1, 0.2, 0.3])
h2 = torch.tensor([0.5, 0.6, 0.7])

# Интерполяция
alpha = 0.5
h_interp = alpha * h1 + (1 - alpha) * h2
print("Interpolated point:", h_interp.numpy())

# Экстраполяция
alpha_ext = 1.5
h_extrap = alpha_ext * h1 + (1 - alpha_ext) * h2
print("Extrapolated point:", h_extrap.numpy())

#### MNIST (рукописные цифры):
1. Постройте автоэнкодер.
2. Преобразуйте изображения в латентное пространство.
3. Используйте T-SNE или PCA для визуализации латентных представлений.

#### Пример кода:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# Предположим, что latent_space содержит скрытые представления
latent_space = torch.randn(1000, 2).numpy()  # Примерное латентное пространство
labels = np.random.randint(0, 10, 1000)     # Метки классов (например, цифры)

# Визуализация с T-SNE
tsne = TSNE(n_components=2, random_state=42)
latent_2d = tsne.fit_transform(latent_space)

plt.scatter(latent_2d[:, 0], latent_2d[:, 1], c=labels, cmap='tab10', s=5)
plt.colorbar()
plt.title("Latent Space Visualization")
plt.show()
"""
    pyperclip.copy(code)


def discr_models(idx: int = 0):
    if idx == 0:
        code = """
        ### 31. Дискриминативные и генеративные модели: задачи, сравнение, классификация и применение

---

### 1. **Дискриминативные и генеративные модели**

#### Дискриминативные модели:
- **Цель**: Предсказывать метки \(y\) на основе входных данных \(x\).
- **Пример задачи**:
  - Классификация: \(P(y|x)\), где \(y\) — метка, а \(x\) — входные данные.
- **Подход**:
  - Изучают границу принятия решений между классами.
- **Примеры**:
  - Логистическая регрессия, SVM, нейронные сети (например, ResNet).

#### Генеративные модели:
- **Цель**: Моделировать распределение данных \(P(x)\) или совместное распределение \(P(x, y)\).
- **Пример задачи**:
  - Генерация новых данных, похожих на обучающую выборку.
- **Подход**:
  - Изучают, как данные \(x\) распределены в пространстве.
- **Примеры**:
  - Variational Autoencoders (VAE), Generative Adversarial Networks (GAN), нормализующие потоки.

---

### 2. **Сравнение дискриминативных и генеративных моделей**

| **Аспект**               | **Дискриминативные модели**           | **Генеративные модели**             |
|---------------------------|---------------------------------------|-------------------------------------|
| **Цель**                 | Предсказание меток \(y\)             | Моделирование данных \(x\)         |
| **Распределение данных** | Не моделируют \(P(x)\)               | Изучают \(P(x)\) или \(P(x, y)\)   |
| **Применение**           | Классификация, регрессия             | Генерация данных, обучение без учителя |
| **Примеры**              | SVM, логистическая регрессия, ResNet | VAE, GAN, Normalizing Flows        |
| **Особенность**          | Высокая точность классификации       | Умеют генерировать новые данные    |

---

### 3. **Классификация генеративных моделей**

Генеративные модели делятся на несколько категорий по принципу работы:

#### 3.1. **Латентно-пространственные модели**
- Моделируют распределение данных \(P(x)\) через скрытое (латентное) пространство \(z\).
- Примеры:
  - **Variational Autoencoders (VAE)**:
    - Используют аппроксимацию байесовского вывода для латентного пространства.
    - Позволяют контролировать генерацию через \(z\).
  - **Generative Adversarial Networks (GAN)**:
    - Используют состязательный процесс между генератором и дискриминатором.
    - Генератор создает новые данные, дискриминатор оценивает их достоверность.

#### 3.2. **Нормализующие модели**
- Моделируют плотность \(P(x)\) напрямую, обеспечивая возможность вычисления \(P(x)\) точно.
- Примеры:
  - **Normalizing Flows**:
    - Преобразуют данные в латентное пространство через обратимые преобразования.
    - Пример: RealNVP, Glow.

#### 3.3. **Энергетические модели**
- Моделируют распределение \(P(x)\) через энергетическую функцию \(E(x)\):
  \[
  P(x) \propto \exp(-E(x))
  \]
- Примеры:
  - Restricted Boltzmann Machines (RBM).
  - Energy-Based Models (EBM).

#### 3.4. **Авторегрессионные модели**
- Разбивают распределение \(P(x)\) на последовательность условных вероятностей:
  \[
  P(x) = \prod_{i=1}^n P(x_i | x_1, x_2, \ldots, x_{i-1})
  \]
- Примеры:
  - PixelCNN, PixelRNN.

---

### 4. **Прикладные задачи генеративных моделей в компьютерном зрении**

Генеративные модели используются в широком спектре прикладных задач:

#### 4.1. **Генерация изображений**
- Создание новых изображений, похожих на данные из обучающей выборки.
- Пример: GAN генерируют реалистичные фотографии.

#### 4.2. **Стилизация изображений**
- Преобразование изображений в определённый художественный стиль.
- Пример: CycleGAN, DeepArt.

#### 4.3. **Суперразрешение (Super-Resolution)**
- Улучшение разрешения изображений.
- Пример: SRGAN.

#### 4.4. **Удаление шума (Denoising)**
- Удаление шума из изображений.
- Пример: Variational Autoencoders (VAE).

#### 4.5. **Восстановление отсутствующих данных (Inpainting)**
- Заполнение отсутствующих частей изображения.
- Пример: Contextual Attention GAN.

#### 4.6. **Обнаружение аномалий**
- Выявление необычных объектов, не принадлежащих обучающей выборке.
- Пример: Использование автоэнкодеров для оценки вероятности данных.

#### 4.7. **Создание видео**
- Генерация реалистичных видео.
- Пример: VideoGAN.

#### 4.8. **Мультимодальная генерация**
- Создание изображений на основе текста.
- Пример: DALL-E, CLIP-GAN.

---

### 6. **Выводы**

1. **Дискриминативные модели**:
   - Предсказывают метки, но не могут генерировать данные.
2. **Генеративные модели**:
   - Моделируют распределение данных, предоставляя возможности для генерации, восстановления и аномалий.
3. **Применение**:
   - Генерация изображений, суперразрешение, обнаружение аномалий, стилизация и мультимодальная генерация.
4. **Современные достижения**:
   - GAN, VAE и нормализующие потоки активно развиваются и применяются в широком спектре задач.
"""
    else:
        code = """
#### Реализация простого GAN в PyTorch:
import torch
import torch.nn as nn
import torch.optim as optim

# Генератор
class Generator(nn.Module):
    def __init__(self, noise_dim, img_dim):
        super(Generator, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(noise_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, img_dim),
            nn.Tanh()
        )

    def forward(self, x):
        return self.net(x)

# Дискриминатор
class Discriminator(nn.Module):
    def __init__(self, img_dim):
        super(Discriminator, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(img_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# Гиперпараметры
noise_dim = 100
img_dim = 28 * 28  # Для изображений 28x28 (например, MNIST)
lr = 0.0002

# Инициализация моделей
generator = Generator(noise_dim, img_dim)
discriminator = Discriminator(img_dim)

# Оптимизаторы
optimizer_G = optim.Adam(generator.parameters(), lr=lr)
optimizer_D = optim.Adam(discriminator.parameters(), lr=lr)

# Функция потерь
criterion = nn.BCELoss()

# Тренировка GAN
for epoch in range(10000):
    # Генерация случайного шума
    noise = torch.randn(64, noise_dim)
    fake_images = generator(noise)
    real_labels = torch.ones(64, 1)
    fake_labels = torch.zeros(64, 1)

    # Обновление дискриминатора
    real_images = torch.randn(64, img_dim)  # Пример реальных данных
    outputs_real = discriminator(real_images)
    outputs_fake = discriminator(fake_images.detach())
    loss_D = criterion(outputs_real, real_labels) + criterion(outputs_fake, fake_labels)
    optimizer_D.zero_grad()
    loss_D.backward()
    optimizer_D.step()

    # Обновление генератора
    outputs_fake = discriminator(fake_images)
    loss_G = criterion(outputs_fake, real_labels)
    optimizer_G.zero_grad()
    loss_G.backward()
    optimizer_G.step()

    if (epoch + 1) % 1000 == 0:
        print(f"Epoch {epoch+1}, Loss_D: {loss_D.item():.4f}, Loss_G: {loss_G.item():.4f}")
"""

    pyperclip.copy(code)


def gan(idx: int = 0):
    if idx == 0:
        code = """
### 32. Архитектура GAN: общая архитектура, обучение и пример с одномерной функцией распределения

---

### 1. **Общая архитектура GAN**

**Generative Adversarial Networks (GAN)** — это класс генеративных моделей, основанных на состязательном подходе между двумя нейронными сетями:
- **Генератор (Generator, \(G\))**:
  - Создаёт новые данные \(x\), похожие на реальные данные.
  - Принимает на вход случайный шум \(z\) из латентного пространства.
- **Дискриминатор (Discriminator, \(D\))**:
  - Определяет, является ли входной сигнал реальным (из обучающего набора) или сгенерированным.

#### Архитектура:
1. **Генератор**:
   - Вход: случайный шум \(z \sim p_z(z)\).
   - Выход: сгенерированные данные \(\hat{x} = G(z)\).

2. **Дискриминатор**:
   - Вход: либо реальные данные \(x\), либо сгенерированные данные \(\hat{x}\).
   - Выход: вероятность того, что данные реальны \(D(x) \in [0, 1]\).

---

### 2. **Обучение GAN**

Обучение GAN — это игра с нулевой суммой, где:
1. Генератор пытается обмануть дискриминатор, создавая данные, которые он считает реальными.
2. Дискриминатор обучается различать реальные и сгенерированные данные.

#### Целевая функция:
Обучение основано на минимаксной функции:
\[
\min_G \max_D \, \mathbb{E}_{x \sim p_{\text{data}}(x)} [\log D(x)] + \mathbb{E}_{z \sim p_z(z)} [\log (1 - D(G(z)))]
\]
где:
- \(p_{\text{data}}(x)\) — распределение реальных данных.
- \(p_z(z)\) — распределение шума.

#### Разделение задачи на этапы:
1. **Обновление дискриминатора**:
   - Максимизация вероятности правильной классификации реальных и сгенерированных данных:
     \[
     \mathcal{L}_D = - \left( \mathbb{E}_{x \sim p_{\text{data}}(x)} [\log D(x)] + \mathbb{E}_{z \sim p_z(z)} [\log (1 - D(G(z)))] \right)
     \]

2. **Обновление генератора**:
   - Минимизация вероятности того, что дискриминатор правильно классифицирует сгенерированные данные как "фейк":
     \[
     \mathcal{L}_G = - \mathbb{E}_{z \sim p_z(z)} [\log D(G(z))]
     \]

#### Итеративный процесс:
- На каждом шаге дискриминатор обновляется несколько раз (обычно 1-2), чтобы лучше различать данные.
- Затем обновляется генератор.

---

### 3. **Архитектуры глубоких моделей в GAN**

1. **Стандартные GAN**:
   - Используют полносвязные слои для генератора и дискриминатора.

2. **Deep Convolutional GAN (DCGAN)**:
   - Применяют сверточные слои, особенно для изображений.
   - Генератор использует транспонированные свёртки (\(ConvTranspose\)).
   - Дискриминатор использует стандартные свёртки (\(Conv\)).

3. **Conditional GAN (cGAN)**:
   - Генератор и дискриминатор условно зависят от дополнительных данных \(y\) (например, класса изображения):
     \[
     G(z, y), \quad D(x, y)
     \]

4. **Wasserstein GAN (WGAN)**:
   - Вводят новую функцию потерь на основе расстояния Васерштейна, чтобы улучшить стабильность обучения.

5. **StyleGAN**:
   - Генератор разделяет стиль и контент, позволяя контролировать определённые аспекты сгенерированных данных.

---

### 4. **Принцип обучения GAN на примере одномерной функции распределения**

#### Задача:
- Сгенерировать распределение данных, которое приближается к реальному одномерному распределению (например, \(p_{\text{data}}(x) \sim \mathcal{N}(0, 1)\)).

#### Шаги:

1. **Реальные данные**:
   - \(p_{\text{data}}(x)\): Истинное распределение данных (например, нормальное распределение).

2. **Шум**:
   - \(z \sim \mathcal{U}(-1, 1)\): Случайный шум из равномерного распределения.

3. **Генератор**:
   - Создаёт сэмплы \(G(z)\), которые должны соответствовать \(p_{\text{data}}(x)\).

4. **Дискриминатор**:
   - Определяет, является ли точка реальной (из \(p_{\text{data}}\)) или сгенерированной (\(G(z)\)).

---

### 5. **Выводы**

1. **GAN**:
   - Состязательная архитектура, в которой генератор создаёт данные, а дискриминатор различает их от реальных.
2. **Обучение**:
   - Минимаксная задача, где генератор стремится обмануть дискриминатор.
3. **Пример**:
   - GAN успешно приближает одномерное распределение с помощью итеративного обучения.
4. **Применение**:
   - GAN находят применение в генерации изображений, видео, восстановлении данных и других задачах.
"""
    else:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt

# Генератор
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(1, 16),
            nn.ReLU(),
            nn.Linear(16, 1)
        )

    def forward(self, z):
        return self.net(z)

# Дискриминатор
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(1, 16),
            nn.ReLU(),
            nn.Linear(16, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# Инициализация моделей
generator = Generator()
discriminator = Discriminator()

# Оптимизаторы
optimizer_G = optim.Adam(generator.parameters(), lr=0.001)
optimizer_D = optim.Adam(discriminator.parameters(), lr=0.001)

# Функция потерь
criterion = nn.BCELoss()

# Обучение GAN
num_epochs = 1000
real_data = lambda n: torch.randn(n, 1)  # Истинное распределение N(0, 1)
batch_size = 64

for epoch in range(num_epochs):
    # 1. Обновление дискриминатора
    z = torch.rand(batch_size, 1) * 2 - 1  # Шум из U(-1, 1)
    fake_data = generator(z)
    real_samples = real_data(batch_size)

    real_labels = torch.ones(batch_size, 1)
    fake_labels = torch.zeros(batch_size, 1)

    loss_D = criterion(discriminator(real_samples), real_labels) + \
             criterion(discriminator(fake_data.detach()), fake_labels)
    optimizer_D.zero_grad()
    loss_D.backward()
    optimizer_D.step()

    # 2. Обновление генератора
    loss_G = criterion(discriminator(fake_data), real_labels)
    optimizer_G.zero_grad()
    loss_G.backward()
    optimizer_G.step()

    # Логирование
    if (epoch + 1) % 100 == 0:
        print(f"Epoch {epoch+1}, Loss_D: {loss_D.item():.4f}, Loss_G: {loss_G.item():.4f}")

# Визуализация результатов
z = torch.rand(1000, 1) * 2 - 1
generated_data = generator(z).detach().numpy()

plt.hist(generated_data, bins=50, alpha=0.5, label='Generated')
plt.hist(real_data(1000).numpy(), bins=50, alpha=0.5, label='Real')
plt.legend()
plt.title("Distribution Comparison")
plt.show()
"""
    pyperclip.copy(code)


def vae(idx: int = 0):
    if idx == 0:
        code = """
### 33. Вариационный автоэнкодер (VAE): цели, архитектура, байесовское моделирование и функция потерь

---

### 1. **Общие и специфические цели модели VAE**

#### Общие цели:
- Вариационный автоэнкодер (VAE) — это генеративная модель, способная:
  1. Сжимать данные в компактное представление (латентное пространство).
  2. Генерировать новые данные, которые соответствуют распределению обучающих данных.

#### Специфические цели:
1. **Контролируемое латентное пространство**:
   - Латентное пространство имеет осмысленную структуру, что позволяет интерполировать, экстраполировать и управлять свойствами сгенерированных данных.
2. **Байесовское моделирование**:
   - VAE аппроксимирует истинное апостериорное распределение данных \(P(z|x)\) через приближенное распределение \(q(z|x)\).
3. **Регуляризация**:
   - VAE регулирует латентное пространство так, чтобы оно следовало заранее заданному распределению (обычно стандартному нормальному \(\mathcal{N}(0, 1)\)).

---

### 2. **Общая архитектура VAE**

Архитектура VAE состоит из двух частей, схожих с классическим автоэнкодером:

1. **Encoder**:
   - Преобразует данные \(x\) в параметры распределения латентного пространства \(z\), такие как среднее \(\mu(x)\) и стандартное отклонение \(\sigma(x)\).
   - Выход: параметры \(\mu, \sigma\), которые описывают распределение \(q(z|x)\).

2. **Decoder**:
   - Принимает сэмпл \(z\), полученный из \(q(z|x)\), и реконструирует данные \(\hat{x}\).
   - Выход: реконструкция данных \(\hat{x} \sim P(x|z)\).

---

### 3. **Байесовское моделирование в VAE**

#### Основная идея:
VAE основывается на байесовском подходе для моделирования распределения данных. Цель — максимизировать логарифмическое правдоподобие данных:
\[
\log P(x) = \int P(x|z) P(z) dz
\]
где:
- \(P(z)\) — априорное распределение латентных переменных (обычно \(\mathcal{N}(0, 1)\)).
- \(P(x|z)\) — вероятность наблюдать \(x\) при данном \(z\).

#### Проблема:
Интеграл вычислять напрямую сложно из-за высокой размерности.

#### Решение:
Используется **вариационная аппроксимация**:
- Заменяется истинное апостериорное распределение \(P(z|x)\) приближённым \(q(z|x)\), параметризованным нейросетью.
- Это приводит к **вариационному нижнему пределу (ELBO)**:
  \[
  \log P(x) \geq \mathbb{E}_{z \sim q(z|x)} [\log P(x|z)] - D_{\text{KL}}(q(z|x) \| P(z))
  \]
  где:
  - Первый член — логарифм правдоподобия реконструкции (ошибка восстановления).
  - Второй член — дивергенция Кульбака-Лейблера между \(q(z|x)\) и \(P(z)\) (регуляризация латентного пространства).

---

### 4. **Функция потерь VAE**

Функция потерь VAE основана на ELBO:

1. **Ошибка восстановления (\(\log P(x|z)\))**:
   - Оценивает, насколько хорошо декодер восстанавливает данные.
   - Используется, например, MSE или кросс-энтропия.

2. **Регуляризация (\(D_{\text{KL}}\))**:
   - Заставляет распределение \(q(z|x)\) быть близким к \(P(z)\) (обычно \(\mathcal{N}(0, 1)\)).
   - Формула KL-дивергенции для нормальных распределений:
     \[
     D_{\text{KL}}(\mathcal{N}(\mu, \sigma^2) \| \mathcal{N}(0, 1)) = -\frac{1}{2} \sum (1 + \log \sigma^2 - \mu^2 - \sigma^2)
     \]

---

### 5. **Специфика получаемых скрытых представлений**

#### Особенности:
1. **Смысловая структура**:
   - Латентное пространство имеет регулярную и непрерывную структуру, что позволяет интерполяцию между точками.

2. **Контролируемое генеративное пространство**:
   - Каждое измерение в \(z\) отвечает за определённую характеристику данных (например, яркость, угол поворота).

3. **Обобщение**:
   - Благодаря регуляризации, VAE не переобучается на тренировочные данные и может создавать новые примеры.

---

### 6. **Пример интерполяции в латентном пространстве**

#### Интерполяция:
Пусть \(z_1, z_2 \in \mathbb{R}^k\) — два латентных вектора. Интерполяция между ними:
\[
z_\text{interp} = \alpha z_1 + (1 - \alpha) z_2, \quad \alpha \in [0, 1]
\]

#### Экстраполяция:
Аналогично, но коэффициенты выходят за пределы \([0, 1]\).

---

### 8. **Выводы**

1. **Цели VAE**:
   - Обучение компактного латентного представления.
   - Генерация новых данных.
2. **Байесовское моделирование**:
   - Используется для аппроксимации распределения данных.
3. **Функция потерь**:
   - Состоит из реконструкции данных и регуляризации латентного пространства.
4. **Применение**:
   - Генерация изображений, обработка шума, исследование латентного пространства.
"""
    else:
        code = """
### 7. **Реализация VAE в PyTorch**
import torch
import torch.nn as nn
import torch.optim as optim

# Encoder
class Encoder(nn.Module):
    def __init__(self, input_dim, latent_dim):
        super(Encoder, self).__init__()
        self.fc1 = nn.Linear(input_dim, 128)
        self.fc_mu = nn.Linear(128, latent_dim)
        self.fc_logvar = nn.Linear(128, latent_dim)

    def forward(self, x):
        h = torch.relu(self.fc1(x))
        mu = self.fc_mu(h)
        logvar = self.fc_logvar(h)
        return mu, logvar

# Decoder
class Decoder(nn.Module):
    def __init__(self, latent_dim, output_dim):
        super(Decoder, self).__init__()
        self.fc1 = nn.Linear(latent_dim, 128)
        self.fc_out = nn.Linear(128, output_dim)

    def forward(self, z):
        h = torch.relu(self.fc1(z))
        return torch.sigmoid(self.fc_out(h))

# VAE
class VAE(nn.Module):
    def __init__(self, input_dim, latent_dim):
        super(VAE, self).__init__()
        self.encoder = Encoder(input_dim, latent_dim)
        self.decoder = Decoder(latent_dim, input_dim)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def forward(self, x):
        mu, logvar = self.encoder(x)
        z = self.reparameterize(mu, logvar)
        return self.decoder(z), mu, logvar

# Гиперпараметры
input_dim = 784  # Например, для MNIST
latent_dim = 2
vae = VAE(input_dim, latent_dim)
optimizer = optim.Adam(vae.parameters(), lr=0.001)
criterion = nn.MSELoss()

# Обучение
for epoch in range(10):
    for x in train_loader:  # Данные из DataLoader
        x = x.view(-1, input_dim)
        optimizer.zero_grad()
        x_reconstructed, mu, logvar = vae(x)
        reconstruction_loss = criterion(x_reconstructed, x)
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        loss = reconstruction_loss + kl_loss
        loss.backward()
        optimizer.step()
"""
    pyperclip.copy(code)


def diffusion(idx: int = 0):
    if idx == 0:
        code = """
### 35. Denoising Diffusion Models: принцип работы, прямой и обратный процессы

---

### 1. **Общий принцип работы Denoising Diffusion Models**

Denoising Diffusion Models (DDMs) — это класс генеративных моделей, которые создают новые данные путем итеративного "обратного процесса" из шума, используя принципы стохастической диффузии.

#### Основная идея:
1. **Прямой процесс**:
   - Входные данные постепенно подвергаются шуму, пока они не станут похожими на случайный шум.
2. **Обратный процесс**:
   - Обучение модели для последовательного удаления шума, восстанавливая данные от полного шума к оригинальным.

#### Задачи:
- Выучить распределение данных \(P(x)\) с использованием двух процессов:
  1. Прямого процесса: \(q(x_t|x_{t-1})\).
  2. Обратного процесса: \(p_\theta(x_{t-1}|x_t)\).

---

### 2. **Прямой процесс**

#### Определение:
Прямой процесс (\(q(x_t|x_{t-1})\)) добавляет гауссовский шум к данным \(x\) за \(T\) шагов. В результате данные постепенно превращаются в шум.

1. **Итеративное добавление шума**:
   - Преобразование данных описывается как:
     \[
     q(x_t|x_{t-1}) = \mathcal{N}(x_t; \sqrt{1 - \beta_t} x_{t-1}, \beta_t I)
     \]
     где:
     - \(\beta_t \in (0, 1)\) — параметры уровня шума для шага \(t\).
     - \(\mathcal{N}\) — гауссовское распределение.

2. **Сведение к шуму**:
   - После \(T\) шагов распределение \(x_T\) приближается к стандартному нормальному распределению:
     \[
     q(x_T|x_0) \approx \mathcal{N}(0, I)
     \]

3. **Прямой процесс за один шаг**:
   - Уравнение для любого \(t\):
     \[
     q(x_t|x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t} x_0, (1 - \bar{\alpha}_t) I)
     \]
     где \(\bar{\alpha}_t = \prod_{s=1}^t (1 - \beta_s)\).

---

### 3. **Обратный процесс**

#### Определение:
Обратный процесс (\(p_\theta(x_{t-1}|x_t)\)) учит постепенно убирать шум, восстановив данные \(x_0\).

1. **Модель обратного процесса**:
   - Аппроксимируется нейронной сетью:
     \[
     p_\theta(x_{t-1}|x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \Sigma_\theta(x_t, t))
     \]
   - Где \(\mu_\theta\) и \(\Sigma_\theta\) — параметры, предсказываемые моделью.

2. **Цель обучения**:
   - Оптимизировать параметры \(\theta\), чтобы восстановить данные \(x_0\) через последовательность шагов:
     \[
     p_\theta(x_0) = \int \prod_{t=1}^T p_\theta(x_{t-1}|x_t) dx_t
     \]

3. **Упрощения**:
   - Обычно \(\Sigma_\theta\) фиксируется, и задача сводится к предсказанию \(\mu_\theta\).

---

### 4. **Функция потерь**

#### Основная задача:
- Использовать вариационный подход для максимизации правдоподобия данных:
  \[
  \mathcal{L}_{\text{VLB}} = \mathbb{E}_q \left[ \sum_{t=1}^T D_{\text{KL}}(q(x_{t-1}|x_t, x_0) \| p_\theta(x_{t-1}|x_t)) \right]
  \]

#### Упрощение функции потерь:
- В результате приближения обучают сеть \(\epsilon_\theta(x_t, t)\), которая предсказывает добавленный шум:
  \[
  \mathcal{L} = \mathbb{E}_{t, x_0, \epsilon} \left[ \| \epsilon - \epsilon_\theta(x_t, t) \|^2 \right]
  \]
  где:
  - \(\epsilon\) — шум, добавленный на шаге \(t\).
  - \(\epsilon_\theta(x_t, t)\) — шум, предсказанный моделью.

---

### 5. **Интуиция работы Denoising Diffusion Models**

#### Прямой процесс:
1. Изображение \(x_0\) постепенно становится более шумным.
2. Шаги \(x_1, x_2, \ldots, x_T\) описывают промежуточные состояния.

#### Обратный процесс:
1. Нейронная сеть учит убирать шум с \(x_T\), шаг за шагом приближаясь к \(x_0\).
2. Итеративное восстановление проходит через \(T\) шагов.


---

### 7. **Выводы**

1. **Прямой процесс**:
   - Добавляет шум к данным, приближая их к нормальному распределению.
2. **Обратный процесс**:
   - Учит пошагово убирать шум, чтобы восстановить данные.
3. **Функция потерь**:
   - Основывается на предсказании шума, добавленного на каждом шаге.
4. **Применение**:
   - Генерация изображений, текстур, редактирование и удаление шума.
"""
    else:
        code = """
### 6. **Пример кода Denoising Diffusion Model в PyTorch**
import torch
import torch.nn as nn
import torch.optim as optim

# Параметры шума
T = 1000  # Число шагов
betas = torch.linspace(1e-4, 0.02, T)  # Коэффициенты шума
alphas = 1.0 - betas
alpha_bar = torch.cumprod(alphas, dim=0)  # Кумулятивные произведения

# Прямая диффузия
def forward_diffusion(x0, t):
    noise = torch.randn_like(x0)
    mean = torch.sqrt(alpha_bar[t]) * x0
    std = torch.sqrt(1 - alpha_bar[t])
    xt = mean + std * noise
    return xt, noise

# Модель
class SimpleDiffusionModel(nn.Module):
    def __init__(self, input_dim):
        super(SimpleDiffusionModel, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Linear(128, input_dim)
        )

    def forward(self, x, t):
        return self.net(x)

# Функция потерь
def loss_fn(model, x0, t):
    xt, noise = forward_diffusion(x0, t)
    noise_pred = model(xt, t)
    return nn.MSELoss()(noise_pred, noise)

# Инициализация
model = SimpleDiffusionModel(input_dim=784)
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# Обучение
for epoch in range(10):
    x0 = torch.randn(64, 784)  # Пример данных
    t = torch.randint(0, T, (64,))  # Случайные шаги
    loss = loss_fn(model, x0, t)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print(f"Epoch {epoch + 1}, Loss: {loss.item():.4f}")
"""
    pyperclip.copy(code)


def mlp_reg(idx: int = 0):
    if idx == 0:
        code = """
import pandas as pd
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import matplotlib.pyplot as plt
"""
    elif idx == 1:
        code = """
# 1. загрузка даннных
data = pd.read_csv('international-airline-passengers.csv')
data.columns = ['Month', 'Passengers']

# конвертация даты
data['Month'] = pd.to_datetime(data['Month'])
data['Month_num'] = data['Month'].dt.year * 12 + data['Month'].dt.month

# нормализация(МинМакс)
scaler = MinMaxScaler()
data['Passengers'] = scaler.fit_transform(data[['Passengers']])
data['Month_num'] = scaler.fit_transform(data[['Month_num']])
"""
    elif idx == 2:
        code = """
# Create input-output pairs (using the previous month's passengers as input)
def create_sequences(data, seq_length=1):
    x, y = [], []
    for i in range(len(data) - seq_length):
        x.append(data[i:i+seq_length, 0])  # Passengers as input
        y.append(data[i+seq_length, 0])    # Next month's passengers as output
    return np.array(x), np.array(y)

seq_length = 1
x_data, y_data = create_sequences(data[['Passengers']].values, seq_length=seq_length)

# разделение данных 
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=42)

x_train = torch.tensor(x_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
x_test = torch.tensor(x_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32).unsqueeze(1)
"""
    elif idx == 3:
        code = """
# 2. определяем модель
class RegressionModel(nn.Module):
    def __init__(self):
        super(RegressionModel, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(seq_length, 64),
            nn.ReLU(),
            nn.Linear(64, 64),
            nn.ReLU(),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.network(x)

# 3. функция обучения
def train_model(optimizer_name, model, criterion, optimizer, epochs=100):
    train_losses = []
    val_losses = []

    for epoch in range(epochs):

        model.train()
        optimizer.zero_grad()
        y_pred = model(x_train)
        train_loss = criterion(y_pred, y_train)
        train_loss.backward()
        optimizer.step()

        model.eval()
        with torch.no_grad():
            y_val_pred = model(x_test)
            val_loss = criterion(y_val_pred, y_test)

        train_losses.append(train_loss.item())
        val_losses.append(val_loss.item())

    return train_losses, val_losses
"""
    elif idx == 4:
        code = """
# инициализация
optimizers = {
    'SGD': torch.optim.SGD,
    'Adam': torch.optim.Adam,
    'AdamW': torch.optim.AdamW
}

results = {}
for opt_name, opt_class in optimizers.items():
    model = RegressionModel()
    criterion = nn.MSELoss()
    optimizer = opt_class(model.parameters(), lr=0.01)

    train_losses, val_losses = train_model(opt_name, model, criterion, optimizer)


    model.eval()
    with torch.no_grad():
        y_test_pred = model(x_test)
        mse = mean_squared_error(y_test.numpy(), y_test_pred.numpy())
        mae = mean_absolute_error(y_test.numpy(), y_test_pred.numpy())
        r2 = r2_score(y_test.numpy(), y_test_pred.numpy())

    results[opt_name] = {
        'train_losses': train_losses,
        'val_losses': val_losses,
        'mse': mse,
        'mae': mae,
        'r2': r2
    }


    plt.plot(train_losses, label=f'{opt_name} - Train')
    plt.plot(val_losses, label=f'{opt_name} - Val')

plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Curves for Different Optimizers')
plt.legend()
plt.show()


for opt_name, metrics in results.items():
    print(f"Optimizer: {opt_name}")
    print(f"MSE: {metrics['mse']:.4f}, MAE: {metrics['mae']:.4f}, R2: {metrics['r2']:.4f}\n")
"""
    pyperclip.copy(code)


def mlp_reg_2(idx: int = 0):
    if idx == 0:
        code = """
import pandas as pd
import numpy as np

import torch
from torch import nn, optim
from torch.utils.data import DataLoader, TensorDataset

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

import matplotlib.pyplot as plt

# Загрузка данных (замените на путь к вашим данным)
data = pd.read_csv('data.csv')

# Целевая переменная и признаки
X = data.drop(columns=['Gold_T+22'])
y = data['Gold_T+22']

# Выделение числовых признаков
numerical_cols = X.columns.tolist()

# Препроцессинг
scaler = StandardScaler()
X_processed = scaler.fit_transform(X)

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

# Преобразование данных в тензоры
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train.values, dtype=torch.float32).view(-1, 1)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test.values, dtype=torch.float32).view(-1, 1)

# Создание датасетов и DataLoader
train_dataset = TensorDataset(X_train, y_train)
test_dataset = TensorDataset(X_test, y_test)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

define_model = lambda input_dim: nn.Sequential(
    nn.Linear(input_dim, 64),
    nn.ReLU(),
    nn.Linear(64, 64),
    nn.ReLU(),
    nn.Linear(64, 1)
)

# Инициализация модели, функции потерь и оптимизатора
input_dim = X_train.shape[1]
model = define_model(input_dim)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Обучение модели
num_epochs = 100
print_every = 10
train_losses = []
test_losses = []

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0
    for batch_X, batch_y in train_loader:
        optimizer.zero_grad()
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    train_losses.append(epoch_loss / len(train_loader))

    # Оценка на тестовой выборке
    model.eval()
    with torch.no_grad():
        test_predictions = model(X_test)
        test_loss = criterion(test_predictions, y_test).item()
        test_losses.append(test_loss)

    if (epoch + 1) % print_every == 0:
        print(f"Epoch {epoch+1}/{num_epochs}, Train Loss: {train_losses[-1]:.4f}, Test Loss: {test_loss:.4f}")

# Визуализация loss
plt.figure(figsize=(10, 6))
plt.plot(range(1, num_epochs + 1), train_losses, label='Train Loss')
plt.plot(range(1, num_epochs + 1), test_losses, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.title('Training and Test Loss')
plt.show()       
"""
    pyperclip.copy(code)


def mlp_reg_3(idx: int = 0):
    if idx == 0:
        code = """
import kagglehub

# Download latest version
path = kagglehub.dataset_download("balakrishcodes/others")

print("Path to dataset files:", path)

import pandas as pd
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import matplotlib.pyplot as plt
"""
    elif idx == 1:
        code = """
# загрузка и преобразования не числовых данных
data = pd.read_csv('Movie_regression.csv')


categorical_columns = data.select_dtypes(include=['object']).columns

# One-hot encode 
if len(categorical_columns) > 0:
    encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
    encoded = encoder.fit_transform(data[categorical_columns])
    encoded_df = pd.DataFrame(encoded, columns=encoder.get_feature_names_out(categorical_columns))
    data = pd.concat([data.drop(columns=categorical_columns), encoded_df], axis=1)
"""
    elif idx == 2:
        code = """
print("Есть ли NaN значения?")
print(data.isnull().sum())  

print("Есть ли бесконечные значения?")
print(np.isinf(data).any())

print("Максимальное значение в данных:", data.max(numeric_only=True))

#замена пропущенных значений на среднее

data.fillna(data.mean(), inplace=True)

# нормализация
numerical_columns = data.drop(columns=['Marketing expense']).columns
scaler = StandardScaler()
data[numerical_columns] = scaler.fit_transform(data[numerical_columns])

# разделение
X = data.drop(columns=['Marketing expense']).values
y = data['Marketing expense'].values

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32).unsqueeze(1)
"""
    elif idx == 3:
        code = """
# Определяем разные архитектуры
class SimpleModel(nn.Module):
    def __init__(self, input_dim):
        super(SimpleModel, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_dim, 32),
            nn.ReLU(),
            nn.Linear(32, 1)
        )

    def forward(self, x):
        return self.network(x)

class ComplexModel(nn.Module):
    def __init__(self, input_dim):
        super(ComplexModel, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_dim, 64),
            nn.ReLU(),
            nn.Linear(64, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.network(x)


def train_model(model, criterion, optimizer, epochs=100):
    train_losses = []
    val_losses = []

    for epoch in range(epochs):
        # Training phase
        model.train()
        optimizer.zero_grad()
        y_pred = model(X_train)
        train_loss = criterion(y_pred, y_train)
        train_loss.backward()
        optimizer.step()

        # Validation phase
        model.eval()
        with torch.no_grad():
            y_val_pred = model(X_test)
            val_loss = criterion(y_val_pred, y_test)

        train_losses.append(train_loss.item())
        val_losses.append(val_loss.item())

    return train_losses, val_losses    
"""
    elif idx == 4:
        code = """
# инициализация и обучение
input_dim = X_train.shape[1]
models = {
    'Simple': SimpleModel(input_dim),
    'Complex': ComplexModel(input_dim)
}

results = {}

for model_name, model in models.items():
    print(f"Training {model_name} model...")
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

    train_losses, val_losses = train_model(model, criterion, optimizer)

    model.eval()
    with torch.no_grad():
        y_test_pred = model(X_test)
        mse = mean_squared_error(y_test.numpy(), y_test_pred.numpy())
        mae = mean_absolute_error(y_test.numpy(), y_test_pred.numpy())
        r2 = r2_score(y_test.numpy(), y_test_pred.numpy())

    results[model_name] = {
        'train_losses': train_losses,
        'val_losses': val_losses,
        'mse': mse,
        'mae': mae,
        'r2': r2
    }

    # график функции потерь
    plt.plot(train_losses, label=f'{model_name} - Train')
    plt.plot(val_losses, label=f'{model_name} - Val')

plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Loss Curves for Different Architectures')
plt.legend()
plt.show()


for model_name, metrics in results.items():
    print(f"Model: {model_name}")
    print(f"MSE: {metrics['mse']:.4f}, MAE: {metrics['mae']:.4f}, R2: {metrics['r2']:.4f}\n")
"""
    pyperclip.copy(code)


def mlp_class(idx: int = 0):
    if idx == 0:
        code = """
   import pandas as pd
import numpy as np

import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader, TensorDataset

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc
import matplotlib.pyplot as plt
"""
    elif idx == 1:
        code = """
data = pd.read_csv("bank.csv")

X = data.drop(columns=["default"])
y = data["default"].map({"yes": 1, "no": 0})  # Преобразуем целевую переменную в 0 и 1

# Определяем категориальные и числовые столбцы
categorical_cols = ["job", "marital", "education", "housing", "loan", "contact", "month", "poutcome"]
numerical_cols = ["age", "balance", "day", "duration", "campaign", "pdays", "previous"]

# Препроцессинг данных
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), numerical_cols),
        ('cat', OneHotEncoder(), categorical_cols)
    ])

X_processed = preprocessor.fit_transform(X)

X_train, X_test, y_train, y_test = train_test_split(X_processed, y, test_size=0.2, random_state=42)
"""

    elif idx == 2:
        code = """
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train.values, dtype=torch.float32).unsqueeze(1)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test.values, dtype=torch.float32).unsqueeze(1)

train_dataset = TensorDataset(X_train, y_train)
test_dataset = TensorDataset(X_test, y_test)

batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)    
"""

    elif idx == 3:
        code = """
class Model(nn.Module):
    def __init__(self, input_dim):
        super(Model, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

model = Model(X_train.shape[1])
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

epochs = 100
print_every = 10
loss_history = []
"""

    elif idx == 4:
        code = """
for epoch in range(epochs):
    epoch_loss = 0
    model.train()
    for batch_X, batch_y in train_loader:
        optimizer.zero_grad()
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    loss_history.append(epoch_loss / len(train_loader))

    if (epoch + 1) % print_every == 0:
        print(f'Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss / len(train_loader):.4f}')

model.eval()
with torch.no_grad():
    y_pred = model(X_test)
    y_pred_class = (y_pred >= 0.5).float()
"""

    elif idx == 5:
        code = """
cm = confusion_matrix(y_test, y_pred_class)
print("Confusion Matrix:")
print(cm)

print("Classification Report:")
print(classification_report(y_test, y_pred_class))

fpr, tpr, _ = roc_curve(y_test, y_pred)
roc_auc = auc(fpr, tpr)

plt.figure()
plt.plot(fpr, tpr, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend(loc='lower right')
plt.show()

plt.figure()
plt.plot(range(1, epochs + 1), loss_history, label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()
plt.show()
"""

    pyperclip.copy(code)


def cnn_class(idx: int = 0):
    if idx == 0:
        code = """
import torch
import torch.nn as nn
import torch.optim as optim

from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt
"""
    elif idx == 1:
        code = """
data_dir = "chars"

def calculate_mean_std(dataset):
    loader = DataLoader(dataset, batch_size=64, shuffle=False, num_workers=2)
    mean = 0.0
    std = 0.0
    total_images = 0
    for images, _ in loader:
        batch_samples = images.size(0)
        images = images.view(batch_samples, images.size(1), -1)
        mean += images.mean(2).sum(0)
        std += images.std(2).sum(0)
        total_images += batch_samples

    mean /= total_images
    std /= total_images
    return mean, std

transform_for_stats = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])      
"""
    elif idx == 2:
        code = """
stats_dataset = ImageFolder(data_dir, transform=transform_for_stats)
mean, std = calculate_mean_std(stats_dataset)

transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
    transforms.Normalize(mean=mean, std=std)
])

full_dataset = ImageFolder(data_dir, transform=transform)

train_size = int(0.7 * len(full_dataset))
test_size = len(full_dataset) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [train_size, test_size])
"""

    elif idx == 3:
        code = """
class CNN(nn.Module):
    def __init__(self, num_classes):
        super(CNN, self).__init__()
        self.conv_layers = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.fc_layers = nn.Sequential(
            nn.Linear(128 * 16 * 16, 256),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(256, num_classes)
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = x.view(x.size(0), -1)
        x = self.fc_layers(x)
        return x      
"""

    elif idx == 4:
        code = """
batch_size = 64
num_epochs = 100
print_every = 10
num_classes = len(full_dataset.classes)

model = CNN(num_classes)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size)

loss_history = []

for epoch in range(num_epochs):
    epoch_loss = 0
    model.train()
    for batch_X, batch_y in train_loader:
        optimizer.zero_grad()
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    loss_history.append(epoch_loss / len(train_loader))

    if (epoch + 1) % print_every == 0:
        print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {epoch_loss / len(train_loader):.4f}')      
"""

    elif idx == 5:
        code = """
model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for batch_X, batch_y in test_loader:
        outputs = model(batch_X)
        _, predicted = torch.max(outputs, 1)
        total += batch_y.size(0)
        correct += (predicted == batch_y).sum().item()

    print(f'Accuracy: {100 * correct / total:.2f}%')

import random

dataiter = iter(test_loader)
images, labels = next(dataiter)

model.eval()
with torch.no_grad():
    outputs = model(images)
    _, predictions = torch.max(outputs, 1)

fig, axes = plt.subplots(2, 2, figsize=(10, 10))
for i, ax in enumerate(axes.flat):
    img = images[i].permute(1, 2, 0) * torch.tensor(std).view(1, 1, -1) + torch.tensor(mean).view(1, 1, -1)
    img = img.clamp(0, 1)
    ax.imshow(img)
    ax.set_title(f'True: {full_dataset.classes[labels[i]]}\nPred: {full_dataset.classes[predictions[i]]}')
    ax.axis('off')
plt.tight_layout()
plt.show()

plt.figure()
plt.plot(range(1, num_epochs + 1), loss_history, label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()
plt.show()
"""

    pyperclip.copy(code)


def cnn_class_2(idx: int = 0):
    if idx == 0:
        code = """
import torch
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
from torchvision.datasets import ImageFolder
from torch.functional import F
import torch.nn as nn

dataset_path = 'sign_language'

transform = transforms.Compose([
    transforms.Resize((128, 128)), 
    transforms.ToTensor(),         
])

dataset = ImageFolder(root=dataset_path, transform=transform)

train_dataset, val_dataset = random_split(dataset, [0.8, 0.2])
"""
    elif idx == 1:
        code = """
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)


print(f"Total images: {len(dataset)}")
print(f"Training images: {len(train_dataset)}")
print(f"Validation images: {len(val_dataset)}")

# Iterate through the training DataLoader as an example
for images, labels in train_loader:
    print(f"Batch image shape: {images.shape}, Batch label shape: {labels.shape}")
    break
"""
    elif idx == 2:
        code = """
class CNN(nn.Module):
    def __init__(self, num_classes=10):
        super(CNN, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)  # Output: 16x128x128
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1) # Output: 32x128x128
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)  # Halves the dimensions
        
#         чтобы понять сколько здесь нейронов
#         запустим модель без линейных слоев (только сверточные и пуллинги)
#         flatten результата и будет количество нейронов
#         для первого линейного слоя 
        
        self.fc1 = nn.Linear(32 * 32 * 32, 128)  
        self.fc2 = nn.Linear(128, num_classes)   

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))  
        x = self.pool(F.relu(self.conv2(x)))  
        
        x = x.view(x.size(0), -1)  
        
        x = F.relu(self.fc1(x))
        x = self.fc2(x)  
        
        return x
    
# model = CNN()
# img = torch.unsqueeze(dataset[0][0], 0)
# output = model(img).flatten().shape

# print(output, 32**3)   
"""
    elif idx == 3:
        code = """
lr = 1e-3
epochs = 10

model = CNN()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

for epoch in range(epochs):
    model.train()
    train_loss = 0.0
    correct = 0
    total = 0

    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)

        # Backward pass
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item() * images.size(0)
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()

    train_accuracy = 100.0 * correct / total
    train_loss /= total
"""
    elif idx == 4:
        code = """
    # Validation loop
    model.eval()
    val_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for images, labels in val_loader:
            images, labels = images.to(device), labels.to(device)

            outputs = model(images)
            loss = criterion(outputs, labels)

            val_loss += loss.item() * images.size(0)
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

    val_accuracy = 100.0 * correct / total
    val_loss /= total

    print(f"Epoch {epoch+1}/{epochs}, "
          f"Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}%, "
          f"Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.2f}%")
"""
    pyperclip.copy(code)
