From 63ea0b47396b95930465bca90814ac1b7125b444 Mon Sep 17 00:00:00 2001 From: bogdan Date: Sun, 19 Jan 2025 14:20:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B9=D1=80=D0=BE=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=B7=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=86=D0=B8=D1=84=D1=80=203x5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + cpu_simple.py | 78 +++++++++ main.py | 4 + networks.py | 451 ++++++++++++++++++++++++++++++++++++++++++++++++++ readme.ipynb | 391 +++++++++++++++++++++++++++++++++++++++++++ utils.py | 68 ++++++++ 6 files changed, 994 insertions(+) create mode 100644 cpu_simple.py create mode 100644 main.py create mode 100644 networks.py create mode 100644 readme.ipynb create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index ef81b1e..48ee6b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /.venv/ +/.idea/ +/__pycache__/ \ No newline at end of file diff --git a/cpu_simple.py b/cpu_simple.py new file mode 100644 index 0000000..4ac644c --- /dev/null +++ b/cpu_simple.py @@ -0,0 +1,78 @@ +# is a library for the Python programming language, adding support for large, +# multi-dimensional arrays and matrices, along with a large collection of high-level mathematical functions to operate on these arrays. +import numpy as np +# Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. Matplotlib makes easy things easy and hard things possible. +import matplotlib.pyplot as plt +import networks +from utils import create_digit_image, add_noise + +# Создание датасета +def create_dataset(num_samples=1000, noise_level=0.1): + images = [] + labels = [] + for _ in range(num_samples): + digit = np.random.randint(0, 10) + image = create_digit_image(digit) + noisy_image = add_noise(image, noise_level) + images.append(noisy_image.flatten()) + labels.append(digit) + return np.array(images), np.array(labels) + +# Создание тренировочного и тестового наборов данных +train_images, train_labels = create_dataset(num_samples=1000, noise_level=0.01) +test_images, test_labels = create_dataset(num_samples=200, noise_level=0.01) + +# # Визуализация нескольких примеров (цифры) +# fig, axes = plt.subplots(1, 4, figsize=(10, 3)) +# for i, ax in enumerate(axes): +# digit = np.random.randint(0, 10) +# image = create_digit_image(digit) +# ax.imshow(image, cmap='gray') +# ax.set_title(f'Label: {digit}') +# ax.axis('off') +# plt.show() + +# # Визуализация нескольких примеров (данные обучения) +# fig, axes = plt.subplots(1, 5, figsize=(10, 3)) +# for i, ax in enumerate(axes): +# ax.imshow(train_images[i].reshape(5, 3), cmap='gray') +# ax.set_title(f'Label: {train_labels[i]}') +# ax.axis('off') +# plt.show() +# +# # Визуализация нескольких примеров (данные проверки) +# fig, axes = plt.subplots(1, 5, figsize=(10, 3)) +# for i, ax in enumerate(axes): +# ax.imshow(test_images[i].reshape(5, 3), cmap='gray') +# ax.set_title(f'Label: {test_labels[i]}') +# ax.axis('off') +# plt.show() + +def run(): + # Инициализация модели + input_size = 5 * 3 + hidden_size = 64 + output_size = 10 + model = networks.SimpleNeuralNetwork(input_size, hidden_size, output_size) # SimpleNeuralNetwork + + # Обучение модели + model.train(train_images, train_labels, learning_rate=0.01, epochs=5000) + + # Оценка модели + model.evaluate(train_images[0], train_labels[0]) + + # Построение графиков + model.plot_metrics() + + # Предсказание на тестовых данных + predictions = model.predict(test_images) + accuracy = np.mean(predictions == test_labels) + print(f'Test accuracy: {accuracy}') + + # Визуализация нескольких примеров + fig, axes = plt.subplots(1, 5, figsize=(10, 3)) + for i, ax in enumerate(axes): + ax.imshow(test_images[i].reshape(5, 3), cmap='gray') + ax.set_title(f'True: {test_labels[i]}, Pred: {predictions[i]}') + ax.axis('off') + plt.show() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..8537325 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +from cpu_simple import run + +if __name__ == '__main__': + run() diff --git a/networks.py b/networks.py new file mode 100644 index 0000000..c30e91b --- /dev/null +++ b/networks.py @@ -0,0 +1,451 @@ +import numpy as np +import matplotlib.pyplot as plt + +# input_size = 5 * 3 +# hidden_size = 64 +# output_size = 10 +# model = networks.ImprovedNeuralNetwork(input_size, hidden_size1, hidden_size2, output_size) +class SimpleNeuralNetwork: + def __init__(self, input_size, hidden_size, output_size): + self.input_size = input_size + self.hidden_size = hidden_size + self.output_size = output_size + + # # Инициализация весов + # self.W1 = np.random.randn(input_size, hidden_size) * 0.01 + # self.b1 = np.zeros((1, hidden_size)) + # self.W2 = np.random.randn(hidden_size, output_size) * 0.01 + # self.b2 = np.zeros((1, output_size)) + + # Инициализация весов (Xavier initialization) + self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) + self.b1 = np.zeros((1, hidden_size)) + self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) + self.b2 = np.zeros((1, output_size)) + + def relu(self, Z): + return np.maximum(0, Z) + + def relu_derivative(self, Z): + return Z > 0 + + def forward(self, X): + # Входной слой к скрытому слою + # Входные данные умножаются на матрицу весов (м/у входными и скрытым) и добавляется вектор смещения скрытого слоя + # \[ + # \mathbf{Z}_1 = \mathbf{X} \mathbf{W}_1 + \mathbf{b}_1 + # \] + # numpy.dot(a, b, out=None) # Скалярное произведение + # a: Первый входной массив. + # b: Второй входной массив. + # out: Выходной массив, в который будет записан результат. Если не указан, результат будет возвращен как новый массив. + self.Z1 = np.dot(X, self.W1) + self.b1 + # применяется функция активации ReLU + # \[ + # \mathbf{A}_1 = \text{ReLU}(\mathbf{Z}_1) = \max(0, \mathbf{Z}_1) + # \] + self.A1 = self.relu(self.Z1) # self.A1 = np.tanh(self.Z1) + # Скрытый слой к выходному слою + # активация (предыдущий этап) умножается на матрицу весов (м/у скрытым и выходным) и добавляется вектор смещения выходного слоя + self.Z2 = np.dot(self.A1, self.W2) + self.b2 + # применяется функция активации softmax + # \[ + # \mathbf{A}_2 = \text{softmax}(\mathbf{Z}_2) = \frac{\exp(\mathbf{Z}_2)}{\sum \exp(\mathbf{Z}_2)} + # \] + # numpy.sum(a, axis=None, dtype=None, out=None, keepdims=, initial=, where=) + # a: Входной массив или объект, который может быть преобразован в массив. + # axis: Ось или оси по которым вычисляется сумма. Если axis равно None (по умолчанию), сумма вычисляется по всем элементам массива. + # dtype: Тип данных результата. Если не указан, тип данных результата будет таким же, как и тип данных входного массива. + # out: Выходной массив, в который будет записан результат. Если не указан, результат будет возвращен как новый массив. + # keepdims: Если True, размерность результата будет такой же, как и размерность входного массива, но с размером 1 по указанным осям. По умолчанию False. + # initial: Начальное значение для суммирования. Если не указано, начальное значение будет 0. + # where: Маска, определяющая, какие элементы массива будут включены в сумму. Если не указано, все элементы массива будут включены. + + self.A2 = np.exp(self.Z2) / np.sum(np.exp(self.Z2), axis=1, keepdims=True) + return self.A2 + + # Кросс-энтропийная функция потерь для задачи классификации определяется следующим образом: + # + # \[ 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 \) — количество примеров в наборе данных. + # - \( C \) — количество классов. + # - \( \mathbf{Y} \) — матрица истинных меток размером \( N \times C \), где \( y_{i,c} \) равно 1, если пример \( i \) принадлежит классу \( c \), и 0 в противном случае. + # - \( \mathbf{\hat{Y}} \) — матрица предсказанных вероятностей размером \( N \times C \), где \( \hat{y}_{i,c} \) — предсказанная вероятность того, что пример \( i \) принадлежит классу \( c \). + # + # Кросс-энтропийная функция потерь основана на теории информации и измеряет количество информации, необходимой для передачи сообщения. В контексте классификации, она измеряет разницу между предсказанными вероятностями и истинными метками. + # + # ### Преимущества + # + # 1. **Интерпретируемость**: Кросс-энтропийная функция потерь имеет четкую интерпретацию в терминах теории информации. + # 2. **Дифференцируемость**: Она является дифференцируемой функцией, что позволяет использовать градиентный спуск для оптимизации. + # 3. **Эффективность**: Она эффективно работает с вероятностными предсказаниями, что делает её подходящей для задач классификации. + # + # ### Недостатки + # + # 1. **Чувствительность к плохим предсказаниям**: Кросс-энтропийная функция потерь может быть чувствительна к плохим предсказаниям, особенно если предсказанная вероятность близка к 0 или 1. + # 2. **Необходимость нормализации**: Предсказанные вероятности должны быть нормализованы, чтобы их сумма была равна 1. Это обычно достигается с помощью функции активации softmax. + def compute_loss(self, Y, Y_hat): + # Используется кросс-энтропийная функция потерь + # \[ + # L(\mathbf{Y}, \mathbf{Y}_{\text{hat}}) = -\frac{1}{m} \sum_{i=1}^{m} \log(\mathbf{Y}_{\text{hat}}[i, \mathbf{Y}[i]]) + # \] + m = Y.shape[0] + logprobs = np.log(Y_hat[range(m), Y]) + loss = -np.sum(logprobs) / m + return loss + + def backward(self, X, Y, Y_hat): + m = X.shape[0] + # Выходной слой + # Градиенты функции потерь + # \[ + # \mathbf{dZ}_2 = \mathbf{Y}_{\text{hat}} - \mathbf{Y} + # \] + dZ2 = Y_hat - np.eye(self.output_size)[Y] + # Градиенты весов + # \[ + # \mathbf{dW}_2 = \frac{1}{m} \mathbf{A}_1^T \mathbf{dZ}_2 + # \] + dW2 = (1 / m) * np.dot(self.A1.T, dZ2) + # Градиенты смещений + # \[ + # \mathbf{db}_2 = \frac{1}{m} \sum \mathbf{dZ}_2 + # \] + db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True) + + # Скрытый слой + # Градиенты функции потерь + # \[ + # \mathbf{dA}_1 = \mathbf{dZ}_2 \mathbf{W}_2^T + # \] + dA1 = np.dot(dZ2, self.W2.T) + # Градиенты функции потерь + # \[ + # \mathbf{dZ}_1 = \mathbf{dA}_1 \cdot \text{ReLU}'(\mathbf{Z}_1) + # \] + dZ1 = dA1 * self.relu_derivative(self.Z1) # dZ1 = dA1 * (1 - np.power(self.A1, 2)) + # Градиенты весов + # \[ + # \mathbf{dW}_1 = \frac{1}{m} \mathbf{X}^T \mathbf{dZ}_1 + # \] + dW1 = (1 / m) * np.dot(X.T, dZ1) + # Градиенты смещений + # \[ + # \mathbf{db}_1 = \frac{1}{m} \sum \mathbf{dZ}_1 + # \] + db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True) + + return dW1, db1, dW2, db2 + + def update_parameters(self, dW1, db1, dW2, db2, learning_rate): + # \[ + # \mathbf{W}_1 := \mathbf{W}_1 - \alpha \mathbf{dW}_1 + # \] + self.W1 -= learning_rate * dW1 + # \[ + # \mathbf{b}_1 := \mathbf{b}_1 - \alpha \mathbf{db}_1 + # \] + self.b1 -= learning_rate * db1 + # \[ + # \mathbf{W}_2 := \mathbf{W}_2 - \alpha \mathbf{dW}_2 + # \] + self.W2 -= learning_rate * dW2 + # \[ + # \mathbf{b}_2 := \mathbf{b}_2 - \alpha \mathbf{db}_2 + # \] + self.b2 -= learning_rate * db2 + + def train(self, X, Y, learning_rate=0.01, epochs=1000): + self.losses = [] + self.accuracies = [] + for epoch in range(epochs): + Y_hat = self.forward(X) + loss = self.compute_loss(Y, Y_hat) + dW1, db1, dW2, db2 = self.backward(X, Y, Y_hat) + self.update_parameters(dW1, db1, dW2, db2, learning_rate) + self.losses.append(loss) + self.accuracies.append(self.accuracy(Y, np.argmax(Y_hat, axis=1))) + if epoch % 100 == 0: + print(f'Epoch {epoch}, Loss: {loss}, Accuracy: {self.accuracies[-1]}') + + def predict(self, X): + Y_hat = self.forward(X) + return np.argmax(Y_hat, axis=1) + + def accuracy(self, Y_true, Y_pred): + return np.mean(Y_true == Y_pred) + + def plot_metrics(self): + plt.figure(figsize=(12, 5)) + + plt.subplot(1, 2, 1) + plt.plot(self.losses, label='Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.title('Loss over Epochs') + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(self.accuracies, label='Accuracy') + plt.xlabel('Epoch') + plt.ylabel('Accuracy') + plt.title('Accuracy over Epochs') + plt.legend() + + plt.tight_layout() + plt.show() + + def evaluate(self, X_test, Y_test): + Y_pred = self.predict(X_test) + accuracy = self.accuracy(Y_test, Y_pred) + print("Accuracy:", accuracy) + +# input_size = 5 * 3 +# hidden_size1 = 128 +# hidden_size2 = 64 +# output_size = 10 +# ТОДО: Многослойная нейронная сеть +class ImprovedNeuralNetwork: + def __init__(self, input_size, hidden_size1, hidden_size2, output_size): + self.input_size = input_size + self.hidden_size1 = hidden_size1 + self.hidden_size2 = hidden_size2 + self.output_size = output_size + + # Инициализация весов + self.W1 = np.random.randn(input_size, hidden_size1) * 0.01 + self.b1 = np.zeros((1, hidden_size1)) + self.W2 = np.random.randn(hidden_size1, hidden_size2) * 0.01 + self.b2 = np.zeros((1, hidden_size2)) + self.W3 = np.random.randn(hidden_size2, output_size) * 0.01 + self.b3 = np.zeros((1, output_size)) + + def relu(self, Z): + return np.maximum(0, Z) + + def relu_derivative(self, Z): + return Z > 0 + + def forward(self, X): + self.Z1 = np.dot(X, self.W1) + self.b1 + self.A1 = np.tanh(self.Z1) # self.relu(self.Z1) + self.Z2 = np.dot(self.A1, self.W2) + self.b2 + self.A2 = np.tanh(self.Z2) # self.relu(self.Z2) + self.Z3 = np.dot(self.A2, self.W3) + self.b3 + self.A3 = np.exp(self.Z3) / np.sum(np.exp(self.Z3), axis=1, keepdims=True) + return self.A3 + + def compute_loss(self, Y, Y_hat): + m = Y.shape[0] + logprobs = np.log(Y_hat[range(m), Y]) + loss = -np.sum(logprobs) / m + return loss + + def backward(self, X, Y, Y_hat): + m = X.shape[0] + + dZ3 = Y_hat - np.eye(self.output_size)[Y] + dW3 = (1 / m) * np.dot(self.A2.T, dZ3) + db3 = (1 / m) * np.sum(dZ3, axis=0, keepdims=True) + + dA2 = np.dot(dZ3, self.W3.T) + dZ2 = dA2 * (1 - np.power(self.A2, 2)) # dA2 * self.relu_derivative(self.Z2) + dW2 = (1 / m) * np.dot(self.A1.T, dZ2) + db2 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True) + + dA1 = np.dot(dZ2, self.W2.T) + dZ1 = dA1 * (1 - np.power(self.A1, 2)) # dA1 * self.relu_derivative(self.Z1) + dW1 = (1 / m) * np.dot(X.T, dZ1) + db1 = (1 / m) * np.sum(dZ1, axis=0, keepdims=True) + + return dW1, db1, dW2, db2, dW3, db3 + + def update_parameters(self, dW1, db1, dW2, db2, dW3, db3, learning_rate): + self.W1 -= learning_rate * dW1 + self.b1 -= learning_rate * db1 + self.W2 -= learning_rate * dW2 + self.b2 -= learning_rate * db2 + self.W3 -= learning_rate * dW3 + self.b3 -= learning_rate * db3 + + def train(self, X, Y, learning_rate=0.01, epochs=1000): + for epoch in range(epochs): + Y_hat = self.forward(X) + loss = self.compute_loss(Y, Y_hat) + dW1, db1, dW2, db2, dW3, db3 = self.backward(X, Y, Y_hat) + self.update_parameters(dW1, db1, dW2, db2, dW3, db3, learning_rate) + if epoch % 100 == 0: + print(f'Epoch {epoch}, Loss: {loss}') + + def predict(self, X): + Y_hat = self.forward(X) + return np.argmax(Y_hat, axis=1) + +# ТОДО: CNN +class SimpleCNeuralNetwork: + def __init__(self, input_size, hidden_size, output_size, filter_size=3, num_filters=32): + self.input_size = input_size + self.hidden_size = hidden_size + self.output_size = output_size + self.filter_size = filter_size + self.num_filters = num_filters + + # Инициализация весов для сверточного слоя + self.filters = np.random.randn(num_filters, filter_size, filter_size) * np.sqrt(2.0 / (filter_size * filter_size)) + self.conv_bias = np.zeros((num_filters,)) + + # Инициализация весов для полносвязного слоя + self.W1 = np.random.randn(input_size * input_size * num_filters, hidden_size) * np.sqrt(2.0 / (input_size * input_size * num_filters)) + self.b1 = np.zeros((1, hidden_size)) + self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) + self.b2 = np.zeros((1, output_size)) + + def relu(self, Z): + return np.maximum(0, Z) + + def relu_derivative(self, Z): + return Z > 0 + + def conv2d(self, X, filters, bias, stride=1): + batch_size, height, width = X.shape # 1000, 5, 3 + filter_size = filters.shape[1] # 3 + num_filters = filters.shape[0] # 32 + + # Размер выходного изображения + output_height = (height - filter_size) // stride + 1 # 3 + output_width = (width - filter_size) // stride + 1 # 1 + + # Инициализация выходного массива + output = np.zeros((batch_size, output_height, output_width, num_filters)) # (1000, 3, 1, 32) + + # Применение свертки + for i in range(output_height): + for j in range(output_width): + for f in range(num_filters): + region = X[:, i * stride:i * stride + filter_size, j * stride:j * stride + filter_size] + output[:, i, j, f] = np.sum(region * filters[f], axis=(1, 2)) + bias[f] + + return output + + def forward(self, X): + # Сверточный слой + self.Z1 = self.conv2d(X, self.filters, self.conv_bias) + self.A1 = self.relu(self.Z1) + + # Сплющивание + self.A1_flat = self.A1.reshape(self.A1.shape[0], -1) + + # Полносвязный слой + # ТОДО: Разобраться с размерностями матриц + self.Z2 = np.dot(self.A1_flat, self.W1) + self.b1 + self.A2 = self.relu(self.Z2) + + # Выходной слой + self.Z3 = np.dot(self.A2, self.W2) + self.b2 + self.A3 = np.exp(self.Z3) / np.sum(np.exp(self.Z3), axis=1, keepdims=True) + return self.A3 + + def compute_loss(self, Y, Y_hat): + m = Y.shape[0] + logprobs = np.log(Y_hat[range(m), Y]) + loss = -np.sum(logprobs) / m + return loss + + def backward(self, X, Y, Y_hat): + m = X.shape[0] + + # Градиенты выходного слоя + dZ3 = Y_hat - np.eye(self.output_size)[Y] + dW2 = (1 / m) * np.dot(self.A2.T, dZ3) + db2 = (1 / m) * np.sum(dZ3, axis=0, keepdims=True) + + # Градиенты полносвязного слоя + dA2 = np.dot(dZ3, self.W2.T) + dZ2 = dA2 * self.relu_derivative(self.Z2) + dW1 = (1 / m) * np.dot(self.A1_flat.T, dZ2) + db1 = (1 / m) * np.sum(dZ2, axis=0, keepdims=True) + + # Градиенты сверточного слоя + dA1 = dZ2.reshape(self.A1.shape) * self.relu_derivative(self.Z1) + dZ1 = np.zeros_like(self.Z1) + for i in range(m): + for f in range(self.num_filters): + for h in range(self.Z1.shape[1]): + for w in range(self.Z1.shape[2]): + dZ1[i, h, w, f] = np.sum(dA1[i, h, w, f] * self.filters[f]) + + dfilters = np.zeros_like(self.filters) + for i in range(m): + for f in range(self.num_filters): + for h in range(self.Z1.shape[1]): + for w in range(self.Z1.shape[2]): + region = X[i, h:h+self.filter_size, w:w+self.filter_size, :] + dfilters[f] += dA1[i, h, w, f] * region + + dconv_bias = np.sum(dA1, axis=(0, 1, 2)) + + return dW1, db1, dW2, db2, dfilters, dconv_bias + + def update_parameters(self, dW1, db1, dW2, db2, dfilters, dconv_bias, learning_rate): + self.W1 -= learning_rate * dW1 + self.b1 -= learning_rate * db1 + self.W2 -= learning_rate * dW2 + self.b2 -= learning_rate * db2 + self.filters -= learning_rate * dfilters + self.conv_bias -= learning_rate * dconv_bias + + def train(self, X, Y, learning_rate=0.01, epochs=1000): + self.losses = [] + self.accuracies = [] + for epoch in range(epochs): + Y_hat = self.forward(X) + loss = self.compute_loss(Y, Y_hat) + dW1, db1, dW2, db2, dfilters, dconv_bias = self.backward(X, Y, Y_hat) + self.update_parameters(dW1, db1, dW2, db2, dfilters, dconv_bias, learning_rate) + self.losses.append(loss) + self.accuracies.append(self.accuracy(Y, np.argmax(Y_hat, axis=1))) + if epoch % 100 == 0: + print(f'Epoch {epoch}, Loss: {loss}, Accuracy: {self.accuracies[-1]}') + + def predict(self, X): + Y_hat = self.forward(X) + return np.argmax(Y_hat, axis=1) + + def accuracy(self, Y_true, Y_pred): + return np.mean(Y_true == Y_pred) + + def confusion_matrix(self, Y_true, Y_pred, num_classes): + cm = np.zeros((num_classes, num_classes), dtype=int) + for true, pred in zip(Y_true, Y_pred): + cm[true, pred] += 1 + return cm + + def plot_metrics(self): + plt.figure(figsize=(12, 5)) + + plt.subplot(1, 2, 1) + plt.plot(self.losses, label='Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.title('Loss over Epochs') + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(self.accuracies, label='Accuracy') + plt.xlabel('Epoch') + plt.ylabel('Accuracy') + plt.title('Accuracy over Epochs') + plt.legend() + + plt.tight_layout() + plt.show() + + def evaluate(self, X_test, Y_test): + Y_pred = self.predict(X_test) + accuracy = self.accuracy(Y_test, Y_pred) + cm = self.confusion_matrix(Y_test, Y_pred, self.output_size) + print("Accuracy:", accuracy) + print("Confusion Matrix:\n", cm) + diff --git a/readme.ipynb b/readme.ipynb new file mode 100644 index 0000000..39cb184 --- /dev/null +++ b/readme.ipynb @@ -0,0 +1,391 @@ +{ + "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 +} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..8ddfdd6 --- /dev/null +++ b/utils.py @@ -0,0 +1,68 @@ +import numpy as np + +# Функция для создания изображения цифры +def create_digit_image(digit): + image = np.zeros((5, 3)) + image[0:5, 0:3] = 1 + if digit == 0: + image[0:5, 0] = 0 + image[0:5, 2] = 0 + image[0, 0:3] = 0 + image[4, 0:3] = 0 + elif digit == 1: + image[0:5, 2] = 0 + image[1, 1] = 0 + elif digit == 2: + image[0, 0:3] = 0 + image[1, 2] = 0 + image[2, 1] = 0 + image[3, 0] = 0 + image[4, 0:3] = 0 + elif digit == 3: + image[0, 0:3] = 0 + image[0:5, 2] = 0 + image[2, 1] = 0 + image[4, 0:3] = 0 + elif digit == 4: + image[0:3, 0] = 0 + image[0:5, 2] = 0 + image[2, 0:3] = 0 + elif digit == 5: + image[0, 0:3] = 0 + image[2, 0:3] = 0 + image[4, 0:3] = 0 + image[1, 0] = 0 + image[3, 2] = 0 + elif digit == 6: + image[0, 0:3] = 0 + image[2, 0:3] = 0 + image[4, 0:3] = 0 + image[0:5, 0] = 0 + image[3, 2] = 0 + elif digit == 7: + image[0, 0:3] = 0 + image[1, 2] = 0 + image[2, 1] = 0 + image[3, 0] = 0 + image[4, 0] = 0 + elif digit == 8: + image[0:5, 0] = 0 + image[0:5, 2] = 0 + image[0, 1] = 0 + image[2, 1] = 0 + image[4, 1] = 0 + elif digit == 9: + image[0, 0:3] = 0 + image[2, 0:3] = 0 + image[4, 0:3] = 0 + image[0:5, 2] = 0 + image[1, 0] = 0 + return image + +# Функция для добавления повреждений +def add_noise(image, noise_level=0.1): + noisy_image = image.copy() + x = np.random.randint(0, 5) + y = np.random.randint(0, 3) + noisy_image[x, y] = 1 + return noisy_image \ No newline at end of file