Первая модель нейронной сети для распознования печатных цифр
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
Bogdan Zuy 63ea0b4739 Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
.gitignore Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
cpu_simple.py Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
main.py Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
networks.py Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
readme.ipynb Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago
utils.py Реализация простой нейронной сети для распознования печатных цифр 3x5 11 months ago

readme.ipynb

{
"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=<no value>, initial=<no value>, where=<no value>)\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
}