{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"%matplotlib inline","metadata":{"id":"3z6RDc0_9zry","execution":{"iopub.status.busy":"2026-06-24T11:50:16.809350Z","iopub.execute_input":"2026-06-24T11:50:16.809715Z","iopub.status.idle":"2026-06-24T11:50:16.815337Z","shell.execute_reply.started":"2026-06-24T11:50:16.809687Z","shell.execute_reply":"2026-06-24T11:50:16.814453Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Введение в Машинное обучение с Keras и TensorFlow\n\n**Основано на примерах Daniel Moser (UT Southwestern Medical Center)**\n\n**Resources: [Xavier Snelgrove](https://github.com/wxs/keras-mnist-tutorial), [Yash Katariya](https://github.com/yashk2810/MNIST-Keras)**","metadata":{"id":"hDCzMcn99zrz"}},{"cell_type":"markdown","source":"Чтобы помочь вам понять основы машинного обучения, в этой демонстрации будут рассмотрены основные этапы создания игрушечной моделей для классификации рукописных чисел с точностью, превышающей 95%.","metadata":{"id":"KrWjgkuU9zr1"}},{"cell_type":"markdown","source":"## Задача для ИИ\n\nНаша цель - построить и обучить нейронную сеть на тысячах изображений рукописных цифр, чтобы она могла успешно идентифицировать любые цифры. Данные, которые будут использованы, - это база данных MNIST, которая содержит 60 000 изображений для обучения и 10 000 тестовых изображений. Мы будем использовать Keras Python API с TensorFlow.","metadata":{"id":"ZecwVd4D9zr2"}},{"cell_type":"markdown","source":"<img src=\"https://github.com/AviatorMoser/keras-mnist-tutorial/blob/master/mnist.png?raw=1\" >","metadata":{"id":"K75jxFpP9zr3"}},{"cell_type":"markdown","source":"## Импорт нужных библиотек\n\nВо-первых, в среду Python необходимо загрузить некоторые библиотеки.","metadata":{"id":"RQIweS2U9zr4"}},{"cell_type":"code","source":"import numpy as np                   # advanced math library\nimport pandas as pd\nimport matplotlib.pyplot as plt      # MATLAB like plotting routines\nimport random                        # for generating random numbers\n\n#from keras.datasets import mnist     # MNIST dataset is included in Keras\nimport keras\nfrom keras.models import Sequential  # Model type to be used\n\nfrom keras.layers.core import Dense, Dropout, Activation # Types of layers to be used in our model\nfrom keras.utils import np_utils                         # NumPy related tools","metadata":{"id":"uKVcL1zM9zr5","execution":{"iopub.status.busy":"2026-06-24T11:50:16.816577Z","iopub.execute_input":"2026-06-24T11:50:16.816813Z","iopub.status.idle":"2026-06-24T11:50:16.834362Z","shell.execute_reply.started":"2026-06-24T11:50:16.816783Z","shell.execute_reply":"2026-06-24T11:50:16.833524Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Загружаем тренировочную выборку\n\nНабор данных MNIST удобно связан с Keras, и мы можем легко проанализировать некоторые из его функций в Python.","metadata":{"id":"oihpD_pq9zr6"}},{"cell_type":"code","source":"train_data = pd.read_csv('/kaggle/input/mnist-in-csv/mnist_train.csv')\ntest_data = pd.read_csv('/kaggle/input/mnist-in-csv/mnist_test.csv')\ntarin_df = pd.DataFrame(train_data)\ntest_df = pd.DataFrame(test_data)\n\n\nX_train = np.asarray(train_data.iloc[:, 1:]).astype(\"float32\")\nprint('X_train: ', X_train.shape)\ny_train = np.asarray(train_data['label'])\ny_train = keras.utils.np_utils.to_categorical(y_train, num_classes=10, dtype=\"float32\")\nprint('y_train: ', y_train.shape)\nX_test = np.asarray(test_data.iloc[:, 1:]).astype(\"float32\")\nprint('X_test: ', X_test.shape)\ny_test = np.asarray(test_data['label'])\ny_test = keras.utils.np_utils.to_categorical(y_test, num_classes=10, dtype=\"float32\")\nprint('y_test: ', y_test.shape)\n\nprint(y_train)","metadata":{"execution":{"iopub.status.busy":"2026-06-24T11:50:16.835903Z","iopub.execute_input":"2026-06-24T11:50:16.836171Z","iopub.status.idle":"2026-06-24T11:50:18.750596Z","shell.execute_reply.started":"2026-06-24T11:50:16.836149Z","shell.execute_reply":"2026-06-24T11:50:18.749776Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Используя matplotlib, мы можем вывести некоторые образцы изображений из обучающего набора прямо на экран.","metadata":{"id":"szsmgK-x9zr8"}},{"cell_type":"code","source":"x_train = X_train.reshape(-1, 28, 28)\nx_test = X_test.reshape(-1, 28, 28)\n\nplt.rcParams['figure.figsize'] = (9,9) # Make the figures a bit bigger\n\nfor i in range(9):\n    plt.subplot(3,3,i+1)\n    num = random.randint(0, len(x_train))\n    plt.imshow(x_train[num], cmap='gray', interpolation='none')\n    plt.title(\"Class {}\".format(y_train[num]))\n    \nplt.tight_layout()","metadata":{"id":"VdBtgogD9zr8","outputId":"9a4f1f6b-171b-4981-dc29-9227f1d802cd","execution":{"iopub.status.busy":"2026-06-24T11:50:18.751462Z","iopub.execute_input":"2026-06-24T11:50:18.751689Z","iopub.status.idle":"2026-06-24T11:50:19.528323Z","shell.execute_reply.started":"2026-06-24T11:50:18.751669Z","shell.execute_reply":"2026-06-24T11:50:19.527516Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Давайте рассмотрим одну цифру поближе и распечатаем массив, представляющий последнюю цифру.","metadata":{"id":"qLpksWF19zr9"}},{"cell_type":"code","source":"# just a little function for pretty printing a matrix\ndef matprint(mat, fmt=\"g\"):\n    col_maxes = [max([len((\"{:\"+fmt+\"}\").format(x)) for x in col]) for col in mat.T]\n    for x in mat:\n        for i, y in enumerate(x):\n            print((\"{:\"+str(col_maxes[i])+fmt+\"}\").format(y), end=\"  \")\n        print(\"\")\n\n# now print!        \nmatprint(x_train[num])","metadata":{"id":"Yp8XmgeM9zr-","outputId":"334ad525-b7fb-414c-8f97-bab1211a5f93","execution":{"iopub.status.busy":"2026-06-24T11:50:19.530079Z","iopub.execute_input":"2026-06-24T11:50:19.530296Z","iopub.status.idle":"2026-06-24T11:50:19.539509Z","shell.execute_reply.started":"2026-06-24T11:50:19.530276Z","shell.execute_reply":"2026-06-24T11:50:19.538766Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Каждый пиксель представляет собой 8-битное целое число от 0 до 255. 0 - полностью черный, а 255 - полностью белый. Это то, что мы называем одноканальным пикселем. Это называется монохромный.\n\n* Забавный факт! На экране вашего компьютера есть три канала для каждого пикселя: красный, зеленый, синий. Каждый из этих каналов также, вероятно, принимает 8-битное целое число. 3 канала - всего 24 бита - 16 777 216 возможных цветов! *","metadata":{"id":"0PQ80PAx9zr_"}},{"cell_type":"markdown","source":"## Форматирование слоя входных данных\n\nВместо матрицы 28 x 28 мы строим нашу сеть так, чтобы она принимала вектор длиной 784.\n\nЗатем каждое изображение необходимо преобразовать (или сгладить) в вектор. Мы также нормируем входные данные, чтобы они находились в диапазоне [0-1], а не [0-255].","metadata":{"id":"cZw_XLfh9zr_"}},{"cell_type":"markdown","source":"<img src='https://github.com/AviatorMoser/keras-mnist-tutorial/blob/master/flatten.png?raw=1' >","metadata":{"id":"ZikCUaDy9zr_"}},{"cell_type":"markdown","source":"# Создание 3-х слойной полносвязной нейросети\n\n<img src=\"https://github.com/AviatorMoser/keras-mnist-tutorial/blob/master/figure.png?raw=1\" />","metadata":{"id":"xD2tBZb69zsB"}},{"cell_type":"code","source":"# The Sequential model is a linear stack of layers and is very common.\n\nmodel = Sequential()","metadata":{"id":"-NlbJCUK9zsB","execution":{"iopub.status.busy":"2026-06-24T11:50:19.540354Z","iopub.execute_input":"2026-06-24T11:50:19.540571Z","iopub.status.idle":"2026-06-24T11:50:19.551718Z","shell.execute_reply.started":"2026-06-24T11:50:19.540552Z","shell.execute_reply":"2026-06-24T11:50:19.550932Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Первый скрытый слой","metadata":{"id":"mx4RweEP9zsC"}},{"cell_type":"code","source":"# Первый скрытый слой - это набор из 512 узлов (нейронов).\n# Каждый узел получит элемент из каждого входного вектора и применит к нему некоторый вес и смещение.\n\nmodel.add(Dense(512, input_shape=(784,))) #(784,) is not a typo -- that represents a 784 length vector!","metadata":{"id":"9HqCR-j69zsC","execution":{"iopub.status.busy":"2026-06-24T11:50:19.553005Z","iopub.execute_input":"2026-06-24T11:50:19.553216Z","iopub.status.idle":"2026-06-24T11:50:19.572340Z","shell.execute_reply.started":"2026-06-24T11:50:19.553197Z","shell.execute_reply":"2026-06-24T11:50:19.571570Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# «Активация» - это нелинейная функция, применяемая к выходным данным уровня выше.\n# Он проверяет новое значение узла и решает, сработал ли этот искусственный нейрон.\n# Выпрямленный линейный блок (ReLU) преобразует все отрицательные входные данные в узлы следующего уровня, чтобы они были равны нулю.\n\n\nmodel.add(Activation('relu'))","metadata":{"id":"jEkyjT6y9zsC","execution":{"iopub.status.busy":"2026-06-24T11:50:19.573227Z","iopub.execute_input":"2026-06-24T11:50:19.573460Z","iopub.status.idle":"2026-06-24T11:50:19.579895Z","shell.execute_reply.started":"2026-06-24T11:50:19.573435Z","shell.execute_reply":"2026-06-24T11:50:19.579293Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dropout обнуляет выбор случайных выходов (т.е. отключает их активацию)\n# Dropout помогает защитить модель от запоминания или \"переобучения\" обучающих данных.\nmodel.add(Dropout(0.2))","metadata":{"id":"9rP7vrmy9zsD","execution":{"iopub.status.busy":"2026-06-24T11:50:19.580723Z","iopub.execute_input":"2026-06-24T11:50:19.580979Z","iopub.status.idle":"2026-06-24T11:50:19.591888Z","shell.execute_reply.started":"2026-06-24T11:50:19.580959Z","shell.execute_reply":"2026-06-24T11:50:19.590914Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Добавляем второй скрытый слой","metadata":{"id":"e_u3Fgn89zsE"}},{"cell_type":"code","source":"# Второй скрытый слой выглядит идентичным нашему первому слою.\n# Однако вместо того, чтобы каждый из 512 узлов получал 784 входа из входных данных изображения,\n# они получают 512 входов с выхода первого 512-узлового уровня.\n\nmodel.add(Dense(512))\nmodel.add(Activation('relu'))\nmodel.add(Dropout(0.2))","metadata":{"id":"vNMg1de89zsE","execution":{"iopub.status.busy":"2026-06-24T11:50:19.593176Z","iopub.execute_input":"2026-06-24T11:50:19.593469Z","iopub.status.idle":"2026-06-24T11:50:19.612027Z","shell.execute_reply.started":"2026-06-24T11:50:19.593413Z","shell.execute_reply":"2026-06-24T11:50:19.611197Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Финальный слой","metadata":{"id":"S0Tq6efS9zsE"}},{"cell_type":"code","source":"# Последний слой из 10 нейронов полностью связан с предыдущим 512-узловым слоем.\n# Последний уровень FCN должен быть равен количеству желаемых классов (в данном случае 10).\nmodel.add(Dense(10))","metadata":{"id":"znrzquuT9zsE","execution":{"iopub.status.busy":"2026-06-24T11:50:19.614126Z","iopub.execute_input":"2026-06-24T11:50:19.614353Z","iopub.status.idle":"2026-06-24T11:50:19.625286Z","shell.execute_reply.started":"2026-06-24T11:50:19.614333Z","shell.execute_reply":"2026-06-24T11:50:19.624424Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Активация softmax представляет собой распределение вероятностей по K различным возможным исходам.\n# Все его значения неотрицательны и в сумме равны 1.\n\nmodel.add(Activation('softmax'))","metadata":{"id":"dqWRmPWQ9zsF","execution":{"iopub.status.busy":"2026-06-24T11:50:19.626248Z","iopub.execute_input":"2026-06-24T11:50:19.626530Z","iopub.status.idle":"2026-06-24T11:50:19.637798Z","shell.execute_reply.started":"2026-06-24T11:50:19.626501Z","shell.execute_reply":"2026-06-24T11:50:19.636565Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Summarize the built model\n\nmodel.summary()","metadata":{"id":"3Yc95-CN9zsF","outputId":"b0578cbf-b300-4681-b303-d18cc107cfa4","execution":{"iopub.status.busy":"2026-06-24T11:50:19.638966Z","iopub.execute_input":"2026-06-24T11:50:19.639255Z","iopub.status.idle":"2026-06-24T11:50:19.646213Z","shell.execute_reply.started":"2026-06-24T11:50:19.639223Z","shell.execute_reply":"2026-06-24T11:50:19.645422Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Составление модели\n\nKeras построен на основе Theano и TensorFlow. Оба пакета позволяют вам определять *граф вычислений* в Python, который затем компилируется и эффективно запускается на CPU или GPU без дополнительных затрат на интерпретатор Python.\n\nПри компиляции модели Keras просит вас указать **функцию потерь** и **оптимизатор**. Функция потерь, которую мы здесь будем использовать, называется *категориальной кросс-энтропией* и является функцией потерь, хорошо подходящей для сравнения двух распределений вероятностей.\n\nНаши прогнозы представляют собой распределения вероятностей по десяти различным цифрам (например, «мы на 80% уверены, что это изображение - 3, 10% уверены, что это 8, 5% - это 2, и т. Д.»), А цель - это распределение вероятностей. со 100% для правильной категории и 0 для всего остального. Перекрестная энтропия - это мера того, насколько ваше предсказанное распределение отличается от целевого распределения. [Подробнее в Википедии] (https://en.wikipedia.org/wiki/Cross_entropy)\n\nОптимизатор помогает определить, насколько быстро модель обучается с помощью **градиентного спуска**. Скорость спуска по градиенту называется **скоростью обучения**.","metadata":{"id":"vCqGNGyK9zsF"}},{"cell_type":"code","source":"# Let's use the Adam optimizer for learning\nmodel.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])","metadata":{"id":"0gGXRyZ69zsH","execution":{"iopub.status.busy":"2026-06-24T11:50:19.647473Z","iopub.execute_input":"2026-06-24T11:50:19.647799Z","iopub.status.idle":"2026-06-24T11:50:19.660619Z","shell.execute_reply.started":"2026-06-24T11:50:19.647776Z","shell.execute_reply":"2026-06-24T11:50:19.659617Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Обучите модель!\nЭто самое интересное!","metadata":{"id":"KjxP3iGF9zsH"}},{"cell_type":"markdown","source":"Размер т.н. батча определяет, сколько данных на шаг используется для вычисления функции потерь, градиентов и обратного распространения. Большие размеры батчей позволяют сети быстрее завершить обучение; Однако есть и другие факторы, помимо скорости тренировки, которые следует учитывать.\n\nСлишком большой размер батча сглаживает локальные минимумы функции потерь, заставляя оптимизатор выбирать один, потому что он считает, что нашел глобальный минимум.\n\nСлишком маленький размер батча создает очень шумную функцию потерь, и оптимизатор может никогда не найти глобальный минимум.\n\nТаким образом, чтобы найти подходящий размер батча, может потребоваться метод проб и ошибок!","metadata":{"id":"v716QuP49zsH"}},{"cell_type":"code","source":"EPOCHS = 10\nmodel.fit(X_train, y_train,\n          batch_size=128, epochs=EPOCHS,\n          verbose=1)","metadata":{"id":"qHttTerH9zsI","outputId":"74cb6048-94c3-4e0b-c1b9-0772599bfc01","execution":{"iopub.status.busy":"2026-06-24T11:50:19.661619Z","iopub.execute_input":"2026-06-24T11:50:19.661851Z","iopub.status.idle":"2026-06-24T11:50:51.431969Z","shell.execute_reply.started":"2026-06-24T11:50:19.661831Z","shell.execute_reply":"2026-06-24T11:50:51.431382Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Два числа, расположенные по порядку, представляют значение функции потерь сети на обучающем наборе и общую точность сети на обучающих данных. Но как он работает с данными, на которых он не тренировался?","metadata":{"id":"UqI8nQlZ9zsI"}},{"cell_type":"markdown","source":"## Evaluate Model's Accuracy on Test Data","metadata":{"id":"YkoYiAee9zsI"}},{"cell_type":"code","source":"score = model.evaluate(X_test, y_test)\nprint('Test score:', score[0])\nprint('Test accuracy:', score[1])","metadata":{"id":"NbdieKFv9zsI","outputId":"b5d074b3-b2b1-447e-b7fb-e6e54fe8c495","execution":{"iopub.status.busy":"2026-06-24T11:50:51.433048Z","iopub.execute_input":"2026-06-24T11:50:51.433260Z","iopub.status.idle":"2026-06-24T11:50:52.009706Z","shell.execute_reply.started":"2026-06-24T11:50:51.433240Z","shell.execute_reply":"2026-06-24T11:50:52.008888Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Проверка вывода\n\nВсегда полезно проверить результат и убедиться, что все выглядит нормально. Здесь мы рассмотрим несколько примеров, когда всё правильно, и несколько примеров, когда он ошибается.","metadata":{"id":"VAEPltu49zsJ"}},{"cell_type":"code","source":"# The predict_classes function outputs the highest probability class\n# according to the trained classifier for each input example.\n# predicted_classes = model.predict_classes(X_test)\npredict_x = model.predict(X_test) \npredicted_classes = np.argmax(predict_x,axis=1)\n\n# Check which items we got right / wrong\ncorrect_indices = np.nonzero(predicted_classes == np.argmax(y_test, axis=1))[0]\n\nincorrect_indices = np.nonzero(predicted_classes != np.argmax(y_test, axis=1))[0]","metadata":{"id":"AuwmiF3N9zsK","execution":{"iopub.status.busy":"2026-06-24T11:50:52.010548Z","iopub.execute_input":"2026-06-24T11:50:52.010738Z","iopub.status.idle":"2026-06-24T11:50:52.399803Z","shell.execute_reply.started":"2026-06-24T11:50:52.010720Z","shell.execute_reply":"2026-06-24T11:50:52.399051Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure()\nfor i, correct in enumerate(correct_indices[:9]):\n    plt.subplot(3,3,i+1)\n    plt.imshow(X_test[correct].reshape(28,28), cmap='gray', interpolation='none')\n    plt.title(\"Predicted {}, Class {}\".format(predicted_classes[correct], np.argmax(y_test[correct])))\n    \nplt.tight_layout()\n    \nplt.figure()\nfor i, incorrect in enumerate(incorrect_indices[:9]):\n    plt.subplot(3,3,i+1)\n    plt.imshow(X_test[incorrect].reshape(28,28), cmap='gray', interpolation='none')\n    plt.title(\"Predicted {}, Class {}\".format(predicted_classes[incorrect], np.argmax(y_test[incorrect])))\n    \nplt.tight_layout()","metadata":{"id":"1DrBCYRt9zsK","outputId":"bd0b05d9-8e45-4c82-9a1a-0f6e6bec9655","execution":{"iopub.status.busy":"2026-06-24T11:50:52.400740Z","iopub.execute_input":"2026-06-24T11:50:52.400976Z","iopub.status.idle":"2026-06-24T11:50:53.786692Z","shell.execute_reply.started":"2026-06-24T11:50:52.400955Z","shell.execute_reply":"2026-06-24T11:50:53.785819Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Ответы на задание\n\n**1. Какую точность показала модель при исходном числе эпох?**\n\nПри исходном значении `epochs = 5` модель показала точность на тестовой выборке около **96.5%** (`accuracy = 0.965`).\n\n**2. Как изменилась точность при меньшем и большем числе эпох? Что заметили?**\n\n- При уменьшении числа эпох до 1–2 точность становится ниже, поскольку модель не успевает достаточно хорошо обучиться. (При `epochs = 1` `accuracy = 0.93`)\n- При увеличении числа эпох до 10 точность обычно немного возрастает по сравнению с 5 эпохами, однако рост уже не такой заметный. (`accuracy = 0.967`)\n- Чем больше эпох, тем лучше сеть изучает данные, но после некоторого момента улучшения становятся небольшими и может начаться переобучение.\n\n**3. Пример неправильного предсказания**\n\nСреди ошибочных примеров встречаются цифры, написанные неразборчиво или похожие сразу на несколько классов. Например, цифра 5 может быть ошибочно распознана как 3, если её верхняя и нижняя части имеют необычную форму. Причина ошибки связана с тем, что изображение похоже на образцы другого класса из обучающей выборки.\n","metadata":{}},{"cell_type":"markdown","source":"# Бонусная часть","metadata":{}},{"cell_type":"code","source":"import cv2\nimport os\n\n# === предобработка ===\ndef preprocess_custom_image(image_path):\n    \"\"\"\n    Преобразует фото рукописной цифры в формат MNIST (28×28, 0-255, НЕ нормализовано)\n    \"\"\"\n    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n    \n    if img is None:\n        raise ValueError(f\"Не удалось загрузить изображение: {image_path}\")\n    \n    # Инверсия: в MNIST фон чёрный (0), цифра белая (255)\n    img = cv2.bitwise_not(img)\n    \n    # Бинаризация\n    _, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)\n    \n    # Находим контур цифры\n    contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    \n    if not contours:\n        raise ValueError(f\"Не удалось найти цифру на изображении: {image_path}\")\n    \n    # Берём самый большой контур\n    cnt = max(contours, key=cv2.contourArea)\n    x, y, w, h = cv2.boundingRect(cnt)\n    \n    # Добавляем отступы\n    padding = 4\n    x = max(0, x - padding)\n    y = max(0, y - padding)\n    w = min(img.shape[1] - x, w + 2 * padding)\n    h = min(img.shape[0] - y, h + 2 * padding)\n    \n    # Вырезаем цифру\n    digit = img[y:y+h, x:x+w]\n    \n    # Масштабируем: цифра 20×20 внутри 28×28 (как в оригинальном MNIST)\n    scale = 20.0 / max(h, w)\n    new_h = int(h * scale)\n    new_w = int(w * scale)\n    digit_resized = cv2.resize(digit, (new_w, new_h))\n    \n    # Центрируем на 28×28\n    result = np.zeros((28, 28), dtype=np.uint8)\n    y_offset = (28 - new_h) // 2\n    x_offset = (28 - new_w) // 2\n    result[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = digit_resized\n    \n    # оставляем в диапазоне 0-255\n    return result.astype('float32')\n\n\ndef predict_digit(model, image_path):\n    \"\"\"\n    Предсказывает цифру. Вход: (1, 784) в диапазоне 0-255\n    \"\"\"\n    img = preprocess_custom_image(image_path)\n    \n    # Преобразуем (28, 28) → (1, 784)\n    img_flat = img.reshape(1, 784)\n    \n    # Предсказание\n    probs = model.predict(img_flat, verbose=0)[0]\n    predicted = np.argmax(probs)\n    \n    return predicted, probs, img\n\n\n# === Обработка всех 10 изображений ===\n\nfolder_path = '/kaggle/input/datasets/altaww/digits/'\n\nresults = []\n\nfor i in range(10):\n    filename = f'{i}.png'\n    filepath = os.path.join(folder_path, filename)\n    \n    if not os.path.exists(filepath):\n        print(f\"Файл не найден: {filepath}\")\n        continue\n    \n    # Предсказание\n    predicted, probabilities, processed_img = predict_digit(model, filepath)\n    \n    true_label = i\n    correct = (predicted == true_label)\n    \n    results.append({\n        'true': true_label,\n        'predicted': predicted,\n        'correct': correct,\n        'confidence': probabilities[predicted] * 100,\n        'probs': probabilities,\n        'img': processed_img\n    })\n    \n    status = \"✅\" if correct else \"❌\"\n    print(f\"{status} {filename}: истинная={true_label}, предсказана={predicted}, \"\n          f\"уверенность={probabilities[predicted]*100:.1f}%\")\n\n\n# === Визуализация ===\n\nfig, axes = plt.subplots(2, 5, figsize=(15, 7))\nfig.suptitle('Распознавание собственных рукописных цифр', fontsize=14)\n\nfor idx, res in enumerate(results):\n    ax = axes[idx // 5, idx % 5]\n    ax.imshow(res['img'], cmap='gray', vmin=0, vmax=255)\n    \n    color = 'green' if res['correct'] else 'red'\n    for spine in ax.spines.values():\n        spine.set_color(color)\n        spine.set_linewidth(3)\n    \n    title = f\"Истина: {res['true']}\\nПредсказано: {res['predicted']}\\n\"\n    title += f\"Уверенность: {res['confidence']:.1f}%\"\n    ax.set_title(title, color=color, fontsize=10)\n    ax.axis('off')\n\nplt.tight_layout()\nplt.savefig('./custom_digits_results.png', dpi=150, bbox_inches='tight')\nplt.show()\n\n\n# === Статистика ===\n\ncorrect_count = sum(1 for r in results if r['correct'])\ntotal = len(results)\naccuracy = 100 * correct_count / total\n\nprint(f\"\\n{'='*50}\")\nprint(f\"ИТОГО: угадано {correct_count} из {total} ({accuracy:.1f}%)\")\nprint(f\"{'='*50}\")\n\nprint(\"\\nОшибки модели:\")\nfor r in results:\n    if not r['correct']:\n        print(f\"  Цифра {r['true']} → предсказано {r['predicted']} \"\n              f\"(уверенность {r['confidence']:.1f}%)\")\n        top3 = np.argsort(r['probs'])[-3:][::-1]\n        print(f\"    Топ-3: \", end=\"\")\n        for digit in top3:\n            print(f\"{digit}:{r['probs'][digit]*100:.1f}% \", end=\"\")\n        print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T12:12:54.253004Z","iopub.execute_input":"2026-06-24T12:12:54.253330Z","iopub.status.idle":"2026-06-24T12:12:55.400472Z","shell.execute_reply.started":"2026-06-24T12:12:54.253306Z","shell.execute_reply":"2026-06-24T12:12:55.399431Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Результаты\n\n| Цифра | Предсказание | Результат | Уверенность |\n|-------|-------------|-----------|-------------|\n| 0 | 0 | ✅ Угадало | высокая |\n| 1 | 5 | ❌ Ошибка | 33.8% |\n| 2 | 2 | ✅ Угадало | высокая |\n| 3 | 5 | ❌ Ошибка | 44.8% |\n| 4 | 9 | ❌ Ошибка | 56.2% |\n| 5 | 5 | ✅ Угадало | высокая |\n| 6 | 6 | ✅ Угадало | высокая |\n| 7 | 3 | ❌ Ошибка | 43.4% |\n| 8 | 8 | ✅ Угадало | высокая |\n| 9 | 9 | ✅ Угадало | высокая |\n\n**Итог: угадано 6 из 10 (60.0%)**\n\n---\n\n### Комментарий\n\n**Где угадало:** Модель хорошо распознала «круглые» цифры с чёткой геометрией — **0, 2, 5, 6, 8, 9**. Эти цифры имеют выраженные замкнутые контуры и характерные петли, которые сеть легко выучила на MNIST.\n\n**Где ошиблось и почему:**\n\n- **1 → 5:** Рукописная единица с наклоном или хвостиком визуально похожа на верхнюю часть пятёрки. Модель фокусируется на локальных паттернах, а вертикальная палка с небольшим изгибом воспринимается как начало пятёрки.\n\n- **3 → 5:** Три и пять имеют схожую верхнюю дугу. Если нижняя петля тройки была незамкнутой или размытой, сеть приняла её за пятёрку — у обеих цифр есть верхняя горизонтальная «полка».\n\n- **4 → 9:** Наиболее частая ошибка в MNIST. Закрытая четвёрка с закруглённым верхом визуально почти идентична девятке. Модель не увидела открытого угла внизу слева, который отличает 4 от 9.\n\n- **7 → 3:** Семёрка с перечёркиванием или с изогнутым верхом похожа на верхнюю дугу тройки. Если горизонтальный штрих семёрки был коротким, а вертикальный — с изгибом, сеть приняла это за две дуги тройки.\n\n**Общий вывод:** Модель, обученная на MNIST, хорошо справляется с цифрами, стиль написания которых близок к стандартному датасету. Проблемы возникают с **цифрами, имеющими схожие локальные паттерны** (1/5, 3/5, 4/9, 7/3), а также если рукописный стиль сильно отличается от «канонического» MNIST (наклон, толщина линий, пропорции). Для улучшения можно добавить аугментацию (повороты, сдвиги, масштабирование) при обучении.","metadata":{}}]}