{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Первая нейронная сеть\n", "\n", "## Введение\n", "Научиться основам построения нейронных сетей.\n", "\n", "## Подготовка данных\n", "В качестве входных данных будет выступать чернобелые изображения 3x5 пиксей." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# Функция для создания изображения цифры\n", "def create_digit_image(digit):\n", " image = np.zeros((5, 3))\n", " image[0:5, 0:3] = 1\n", " if digit == 0:\n", " image[0:5, 0] = 0\n", " image[0:5, 2] = 0\n", " image[0, 0:3] = 0\n", " image[4, 0:3] = 0\n", " elif digit == 1:\n", " image[0:5, 2] = 0\n", " image[1, 1] = 0\n", " elif digit == 2:\n", " image[0, 0:3] = 0\n", " image[1, 2] = 0\n", " image[2, 1] = 0\n", " image[3, 0] = 0\n", " image[4, 0:3] = 0\n", " elif digit == 3:\n", " image[0, 0:3] = 0\n", " image[0:5, 2] = 0\n", " image[2, 1] = 0\n", " image[4, 0:3] = 0\n", " elif digit == 4:\n", " image[0:3, 0] = 0\n", " image[0:5, 2] = 0\n", " image[2, 0:3] = 0\n", " elif digit == 5:\n", " image[0, 0:3] = 0\n", " image[2, 0:3] = 0\n", " image[4, 0:3] = 0\n", " image[1, 0] = 0\n", " image[3, 2] = 0\n", " elif digit == 6:\n", " image[0, 0:3] = 0\n", " image[2, 0:3] = 0\n", " image[4, 0:3] = 0\n", " image[0:5, 0] = 0\n", " image[3, 2] = 0\n", " elif digit == 7:\n", " image[0, 0:3] = 0\n", " image[1, 2] = 0\n", " image[2, 1] = 0\n", " image[3, 0] = 0\n", " image[4, 0] = 0\n", " elif digit == 8:\n", " image[0:5, 0] = 0\n", " image[0:5, 2] = 0\n", " image[0, 1] = 0\n", " image[2, 1] = 0\n", " image[4, 1] = 0\n", " elif digit == 9:\n", " image[0, 0:3] = 0\n", " image[2, 0:3] = 0\n", " image[4, 0:3] = 0\n", " image[0:5, 2] = 0\n", " image[1, 0] = 0\n", " return image\n", "\n", "# Функция для добавления повреждений\n", "def add_noise(image, noise_level=0.1):\n", " noisy_image = image.copy()\n", " x = np.random.randint(0, 5)\n", " y = np.random.randint(0, 3)\n", " noisy_image[x, y] = 1\n", " return noisy_image" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Создание датасета" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def create_dataset(num_samples=1000, noise_level=0.1):\n", " images = []\n", " labels = []\n", " for _ in range(num_samples):\n", " digit = np.random.randint(0, 10)\n", " image = create_digit_image(digit)\n", " noisy_image = add_noise(image, noise_level)\n", " images.append(noisy_image.flatten())\n", " labels.append(digit)\n", " return np.array(images), np.array(labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Создание тренировочного и тестового наборов данных" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_images, train_labels = create_dataset(num_samples=1000, noise_level=0.01)\n", "test_images, test_labels = create_dataset(num_samples=200, noise_level=0.01)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Архитектура модели\n", "Модель будет состоять из трех слоев (входной, скрытый, выходной). В качестве функции активации будет использоваться ReLU (Rectified linear unit). Для задачи классификации будем использовать кросс-энтропийную функцию потерь." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class FirstNeuralNetwork:\n", " def __init__(self, input_size, hidden_size, output_size):\n", " self.input_size = input_size\n", " self.hidden_size = hidden_size\n", " self.output_size = output_size\n", "\n", " self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)\n", " self.b1 = np.zeros((1, hidden_size))\n", " self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)\n", " self.b2 = np.zeros((1, output_size))\n", "\n", " def relu(self, Z):\n", " return np.maximum(0, Z)\n", "\n", " def relu_derivative(self, Z):\n", " return Z > 0\n", "\n", " def forward(self, X):\n", " # Входной слой к скрытому слою\n", " # Входные данные умножаются на матрицу весов (м/у входными и скрытым) и добавляется вектор смещения скрытого слоя\n", " # \\[\n", " # \\mathbf{Z}_1 = \\mathbf{X} \\mathbf{W}_1 + \\mathbf{b}_1\n", " # \\]\n", " # numpy.dot(a, b, out=None) # Скалярное произведение\n", " # a: Первый входной массив.\n", " # b: Второй входной массив.\n", " # out: Выходной массив, в который будет записан результат. Если не указан, результат будет возвращен как новый массив.\n", " self.Z1 = np.dot(X, self.W1) + self.b1\n", " # применяется функция активации ReLU\n", " # \\[\n", " # \\mathbf{A}_1 = \\text{ReLU}(\\mathbf{Z}_1) = \\max(0, \\mathbf{Z}_1)\n", " # \\]\n", " self.A1 = self.relu(self.Z1) # self.A1 = np.tanh(self.Z1)\n", " # Скрытый слой к выходному слою\n", " # активация (предыдущий этап) умножается на матрицу весов (м/у скрытым и выходным) и добавляется вектор смещения выходного слоя\n", " self.Z2 = np.dot(self.A1, self.W2) + self.b2\n", " # применяется функция активации softmax\n", " # \\[\n", " # \\mathbf{A}_2 = \\text{softmax}(\\mathbf{Z}_2) = \\frac{\\exp(\\mathbf{Z}_2)}{\\sum \\exp(\\mathbf{Z}_2)}\n", " # \\]\n", " # numpy.sum(a, axis=None, dtype=None, out=None, keepdims=, initial=, where=)\n", " # a: Входной массив или объект, который может быть преобразован в массив.\n", " # axis: Ось или оси по которым вычисляется сумма. Если axis равно None (по умолчанию), сумма вычисляется по всем элементам массива.\n", " # dtype: Тип данных результата. Если не указан, тип данных результата будет таким же, как и тип данных входного массива.\n", " # out: Выходной массив, в который будет записан результат. Если не указан, результат будет возвращен как новый массив.\n", " # keepdims: Если True, размерность результата будет такой же, как и размерность входного массива, но с размером 1 по указанным осям. По умолчанию False.\n", " # initial: Начальное значение для суммирования. Если не указано, начальное значение будет 0.\n", " # where: Маска, определяющая, какие элементы массива будут включены в сумму. Если не указано, все элементы массива будут включены.\n", "\n", " self.A2 = np.exp(self.Z2) / np.sum(np.exp(self.Z2), axis=1, keepdims=True)\n", " return self.A2\n", "\n", " # Кросс-энтропийная функция потерь для задачи классификации определяется следующим образом:\n", " #\n", " # \\[ L(\\mathbf{Y}, \\mathbf{\\hat{Y}}) = -\\frac{1}{N} \\sum_{i=1}^{N} \\sum_{c=1}^{C} y_{i,c} \\log(\\hat{y}_{i,c}) \\]\n", " #\n", " # где:\n", " # - \\( N \\) — количество примеров в наборе данных.\n", " # - \\( C \\) — количество классов.\n", " # - \\( \\mathbf{Y} \\) — матрица истинных меток размером \\( N \\times C \\), где \\( y_{i,c} \\) равно 1, если пример \\( i \\) принадлежит классу \\( c \\), и 0 в противном случае.\n", " # - \\( \\mathbf{\\hat{Y}} \\) — матрица предсказанных вероятностей размером \\( N \\times C \\), где \\( \\hat{y}_{i,c} \\) — предсказанная вероятность того, что пример \\( i \\) принадлежит классу \\( c \\).\n", " #\n", " # Кросс-энтропийная функция потерь основана на теории информации и измеряет количество информации, необходимой для передачи сообщения. В контексте классификации, она измеряет разницу между предсказанными вероятностями и истинными метками.\n", " #\n", " # ### Преимущества\n", " #\n", " # 1. **Интерпретируемость**: Кросс-энтропийная функция потерь имеет четкую интерпретацию в терминах теории информации.\n", " # 2. **Дифференцируемость**: Она является дифференцируемой функцией, что позволяет использовать градиентный спуск для оптимизации.\n", " # 3. **Эффективность**: Она эффективно работает с вероятностными предсказаниями, что делает её подходящей для задач классификации.\n", " #\n", " # ### Недостатки\n", " #\n", " # 1. **Чувствительность к плохим предсказаниям**: Кросс-энтропийная функция потерь может быть чувствительна к плохим предсказаниям, особенно если предсказанная вероятность близка к 0 или 1.\n", " # 2. **Необходимость нормализации**: Предсказанные вероятности должны быть нормализованы, чтобы их сумма была равна 1. Это обычно достигается с помощью функции активации softmax.\n", " def compute_loss(self, Y, Y_hat):\n", " # Используется кросс-энтропийная функция потерь\n", " # \\[\n", " # L(\\mathbf{Y}, \\mathbf{Y}_{\\text{hat}}) = -\\frac{1}{m} \\sum_{i=1}^{m} \\log(\\mathbf{Y}_{\\text{hat}}[i, \\mathbf{Y}[i]])\n", " # \\]\n", " m = Y.shape[0]\n", " logprobs = np.log(Y_hat[range(m), Y])\n", " loss = -np.sum(logprobs) / m\n", " return loss\n", "\n", " def backward(self, X, Y, Y_hat):\n", " m = X.shape[0]\n", " # Выходной слой\n", " # Градиенты функции потерь\n", " # \\[\n", " # \\mathbf{dZ}_2 = \\mathbf{Y}_{\\text{hat}} - \\mathbf{Y}\n", " # \\]\n", " dZ2 = Y_hat - np.eye(self.output_size)[Y]\n", " # Градиенты весов\n", " # \\[\n", " # \\mathbf{dW}_2 = \\frac{1}{m} \\mathbf{A}_1^T \\mathbf{dZ}_2\n", " # \\]\n", " dW2 = (1 / m) * np.dot(self.A1.T, dZ2)\n", " # Градиенты смещений\n", " # \\[\n", " # \\mathbf{db}_2 = \\frac{1}{m} \\sum \\mathbf{dZ}_2\n", " # \\]\n", " db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True)\n", "\n", " # Скрытый слой\n", " # Градиенты функции потерь\n", " # \\[\n", " # \\mathbf{dA}_1 = \\mathbf{dZ}_2 \\mathbf{W}_2^T\n", " # \\]\n", " dA1 = np.dot(dZ2, self.W2.T)\n", " # Градиенты функции потерь\n", " # \\[\n", " # \\mathbf{dZ}_1 = \\mathbf{dA}_1 \\cdot \\text{ReLU}'(\\mathbf{Z}_1)\n", " # \\]\n", " dZ1 = dA1 * self.relu_derivative(self.Z1) # dZ1 = dA1 * (1 - np.power(self.A1, 2))\n", " # Градиенты весов\n", " # \\[\n", " # \\mathbf{dW}_1 = \\frac{1}{m} \\mathbf{X}^T \\mathbf{dZ}_1\n", " # \\]\n", " dW1 = (1 / m) * np.dot(X.T, dZ1)\n", " # Градиенты смещений\n", " # \\[\n", " # \\mathbf{db}_1 = \\frac{1}{m} \\sum \\mathbf{dZ}_1\n", " # \\]\n", " db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True)\n", "\n", " return dW1, db1, dW2, db2\n", "\n", " def update_parameters(self, dW1, db1, dW2, db2, learning_rate):\n", " # \\[\n", " # \\mathbf{W}_1 := \\mathbf{W}_1 - \\alpha \\mathbf{dW}_1\n", " # \\]\n", " self.W1 -= learning_rate * dW1\n", " # \\[\n", " # \\mathbf{b}_1 := \\mathbf{b}_1 - \\alpha \\mathbf{db}_1\n", " # \\]\n", " self.b1 -= learning_rate * db1\n", " # \\[\n", " # \\mathbf{W}_2 := \\mathbf{W}_2 - \\alpha \\mathbf{dW}_2\n", " # \\]\n", " self.W2 -= learning_rate * dW2\n", " # \\[\n", " # \\mathbf{b}_2 := \\mathbf{b}_2 - \\alpha \\mathbf{db}_2\n", " # \\]\n", " self.b2 -= learning_rate * db2\n", "\n", " def train(self, X, Y, learning_rate=0.01, epochs=1000):\n", " for epoch in range(epochs):\n", " Y_hat = self.forward(X)\n", " loss = self.compute_loss(Y, Y_hat)\n", " dW1, db1, dW2, db2 = self.backward(X, Y, Y_hat)\n", " self.update_parameters(dW1, db1, dW2, db2, learning_rate)\n", " if epoch % 100 == 0:\n", " print(f'Epoch {epoch}, Loss: {loss}')\n", "\n", " def predict(self, X):\n", " Y_hat = self.forward(X)\n", " return np.argmax(Y_hat, axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Обучение модели\n", "Код для обучения модели, включая функцию потерь и оптимизатор.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Инициализация модели\n", "input_size = 5 * 3\n", "hidden_size = 64\n", "output_size = 10\n", "model = networks.SimpleCNeuralNetwork(input_size, hidden_size, output_size) # SimpleNeuralNetwork\n", "\n", "# Обучение модели\n", "model.train(train_images, train_labels, learning_rate=0.01, epochs=10000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Оценка модели\n", "Код для оценки точности модели на тестовой части датасета.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Предсказание на тестовых данных\n", "predictions = model.predict(test_images)\n", "accuracy = np.mean(predictions == test_labels)\n", "print(f'Test accuracy: {accuracy}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Визуализация результатов\n", "Графики обучения (точность и функция потерь).\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Визуализация нескольких примеров\n", "fig, axes = plt.subplots(1, 5, figsize=(10, 3))\n", "for i, ax in enumerate(axes):\n", " ax.imshow(test_images[i].reshape(5, 3), cmap='gray')\n", " ax.set_title(f'True: {test_labels[i]}, Pred: {predictions[i]}')\n", " ax.axis('off')\n", "plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.9.5" } }, "nbformat": 4, "nbformat_minor": 2 }