Добрый день, добрые друзья! Я решил научить нейронную сеть различать рукописные русские буквы, как говорится - на коленке.
Первым делом, стоит найти или сделать(сделать - громко сказано, с такими техническими возможностями). Я не нашел дата сет с русскими буквами, но я решил его сделать.
Первым делом, мы должны понять как именно будем создавать дата сет, очевидно рисуя буквы.
Но где? Я решил это делать на телефоне в приложении "рисовалке" - PaperDraw(не реклама). Другие приложения показались мне слишком сложными.

Так я делаю изображения:

Так я сделал по 9 картинок для каждой буквы.

Получив по 9 изображений для каждой буквы, мы приходим к выводу, что этого маловато будет, не приемлемо мало. Тогда, используя данный код, мы просто создадим несколько вариаций для каждой картинки.
# rotate_image функция что-бы брать изображение и переворацивть его + закрашивать область определённым цветом from scipy.ndimage import rotate as rotate_image # Что-бы открывать и сохранять изображения. import cv2 # открываем изображение image = cv2.imread('a.jpg', cv2.IMREAD_GRAYSCALE) # переменная итерации для цикла i = 0 # переменная итерации градуса наклона изображения g = 0 # и сам цикл, будет while(i < 19): # берём изображени image, поворациваем изображение на -45 + g(из изначального наклона в левую сторуну плавно переходим в правую сторону), cval - это цвет от 0 до 255 rotated_img1 = rotate_image(image,-45 + g, cval=243) #и сохраняем изображение cv2.imwrite('page/'+str(i)+'.jpg',rotated_img1) i = i + 1 g = g + 5
Выглядит не плохо, но стоило бы привести их к какому-то общему виду и первое, что мы сделаем - это уменьшим изображения по границам букв.
import time start_time = time.time() import os import cv2 files = os.listdir('writeimg') def isTrue(l): i = 0 while (i < len(l)): if (l[i] != 243): return True i = i + 1 return False print(len(files)) i = 0 while(i < 0): img = cv2.imread('pullImg/'+files[i], cv2.IMREAD_GRAYSCALE) height, width = img.shape w = 0 h = 0 WR = 0 WL = 0 HU = 0 HD = 0 while(w < width): lineW = img[0:height, w:w+1] if(isTrue(lineW)): WL=w break w = w + 1 w = 0 while(w < width): lineW = img[0:height, width-w-1:width-w] if(isTrue(lineW)): WR = (width-w-1) break w = w + 1 while(h < height): lineH = img[h:h+1, 0:width] if(isTrue(lineH[0])): HU = h break h = h + 1 h = 0 while(h < height): lineH = img[height-h-1:height-h, 0:width] if(isTrue(lineH[0])): HD = height-h-1 break h = h + 1 cv2.imwrite('writeimg/'+files[i], img[HU:HD, WL:WR]) i = i + 1
Вот результат:

А вот сейчас, мы видим, что все фото разного размера, необходимо размеры изображений уровнять, это тоже будет просто но не факт, что правильно.
Вот код:
import cv2, os p = os.listdir('./writeimg') import cv2 import numpy as np i = 0 while(i < len(p)): image = np.zeros((1100, 1100, 3), dtype=np.uint8) image.fill(243) image_height, image_width, _ = image.shape # Загрузка изображения, которое нужно вставить insert_image = cv2.imread('./writeimg/'+p[i]) # print(insert_image) # Получение размеров изображения, которое нужно вставить insert_height, insert_width, _ = insert_image.shape # Определение координат для вставки изображения посередине start_x = int((image_width - insert_width) / 2) start_y = int((image_height - insert_height) / 2) end_x = start_x + insert_width end_y = start_y + insert_height # Вставка изображения посередине image[start_y:end_y, start_x:end_x] = insert_image cv2.imwrite(p[i], image) i = i + 1
Так же, мы вставили картинки в середину большого "холста".

Теперь, мы просто их делаем черно-белыми, код:
import cv2, os l = './q/' p = os.listdir(l) p = os.listdir('./q/') o = 0 while(o < len(p)): i = 0 l = cv2.cvtColor(cv2.imread('./q/'+p[o]), cv2.COLOR_BGR2GRAY).reshape(1100*1100) while(i < len(l)): if(l[i] == 243): l[i] = 255 else: l[i] = 0 i = i + 1 cv2.imwrite('./grey/'+p[o],l.reshape(1100,1100)) o = o + 1

И самое простое, если не вдаваться в подробности - создание нейронной сети. Так-как мы просто можем взять пример из примера руководства keras из классификации цифр - мы это и сделаем. Так же, разделим наш на логические блоки.
# --------код блока 1 --------- # вот тут просто ипортируем билиотеки import os import keras import numpy as np import cv2 from keras.layers import Dense, Dropout , Conv2D , Flatten, MaxPooling2D # --------код блока 1 --------- # --------код блока 2 --------- # переменная с формой входных данных input_shape = (275, 275, 1) # это будет x_tain, именно входящие изображения. data = [] # это метки классов data1 = [] # --------код блока 2 --------- # --------код блока 3 --------- j = 0 # Дальше запускаем цикл добавления в массив изображений, которые мы получили. И к этому просто добавляею значение(число буквы по счету) в массив. while(j <= 160): o = 1 h = 0 while(o<=33): data.append( np.array(cv2.imread('./t/'+str(o)+'_'+str(j)+'.jpg', cv2.IMREAD_GRAYSCALE)) ) data1.append(h) o = o + 1 h = h + 1 j = j + 1 # --------код блока 3 --------- # --------код блока 4 --------- # колличество классов num_classes = 33 # меняем форму массива, из обычного в формат np. data = np.array(data) data1 = np.array(data1) # Важный нюанс, нам нужно изменить размерность массива. data = np.expand_dims(data, -1) # Вот тут просто меняем вид ответа в нейронке, [ [3] ] -> [ [0,0,0,1] ] y_test = keras.utils.to_categorical(data1, num_classes) # --------код блока 4 --------- # --------код блока 5 --------- # нейронная сеть из примера классификации цифр. model = keras.Sequential( [ keras.Input(shape=input_shape), Conv2D(32, kernel_size=(3, 3), activation="relu"), MaxPooling2D(pool_size=(2, 2)), Conv2D(64, kernel_size=(3, 3), activation="relu"), MaxPooling2D(pool_size=(2, 2)), Flatten(), Dropout(0.5), Dense(num_classes, activation="softmax") ] ) # --------код блока 5 --------- # --------код блока 6 --------- model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"]) model.fit(data, y_test, epochs=10,) # проверяем будет ли правильный ответ. m = model.predict(np.array([cv2.imread('./t/1_161.jpg',cv2.IMREAD_GRAYSCALE)])) # да, ответ верный. print( np.argmax(m) + 1 ) # --------код блока 6 ---------
Объяснения:
Как видно из комментария, тут мы просто импортируем библиотеки. Первый импорт, библиотека os - предоставляет функции для взаимодействия с операционной системой(файлы, пути, процессы и т.д). Второй импорт, это сам keras. Третий импорт, это библиотека numpy, она нужна математических операций, далее есть приписка 'as np', то есть мы переименовываем название библиотеки, что бы было проще к ней обращаться. Четвертый импорт, это библиотека cv2, нужна для работы с изображением. Пятый импорт, мы из keras пакета layers - импортируем слои которые требуются для работы нейронной сети, а именно: Dense - полносвязный слой, Dropout - слой, который предотвращает переобучение, грубо говоря, он в процессе обучения отключает некоторые нейроны(рандом), Conv2D - слой, который создаёт свертку, выделяет признаки в изображении, Flatten - изменяет форму входных данных в одномерный вектор, MaxPooling2D - используется для уменьшения размерности входных данных, применяя операцию максимальной пулинга, т.е из окна пикселей(квадрата 2 на 2) выбирает пиксель с максимальным числовым значением.
Мы создаем переменную, в которой мы указываем размерность входных данных, т.е указываем какого размера изображение будет входить в нейронную сеть, а именно: ширина, высота, размерность пикселя(цифра 1 указывает на то, что каждый пиксель имеет значение от 0 до 255, то есть черно-белое изображение). Дальше создаем массив data, который представляет входные данные, и массив data1, который представляет выходные данные.
Здесь, как мне кажется, раскрывается весь смысл названия этого поста. Смысл этого блока в том, что мы заполняем сам "датасет", используя цикл в цикле, во вложенном цикле мы добавляем в массив изображение буквы - "а" а в другой массив её индекс по счету, индекс начинается с 0. Соответственно: наш массив data выглядит так, [изображение буквы а, изображение буквы б, изображение буквы в, ... изображение буквы я, изображение буквы а, изображение буквы б, изображение буквы в, ... изображение буквы я], т.е в массиве у нас просто буквы русского алфавита повторяются по порядку. Массив data1 имеет следующий вид, [ 0, 1, 2, 3, ... 32, 0, 1, 2, 3, ... 32, ] т.е в массивы у нас просто индексы букв алфавита, индекс начинается с 0, т.е 0 индекс - это буква "а", 32 индекс - это буква "я".
Мы создаем переменную num_classes, где указываем количество классов для распознавания, т.к букв 33, то и классов 33. Дальше меняем тип данных, из обычных массивов, в массив numpy, это нужно для того, что бы можно было применять математические функции к массиву. Фактически мы изменили тип массива для того чтобы применить функцию expand_dims, которая есть в numpy, мы даже могли свою функцию написать, которая работает, как expand_dims, но keras не принимает обычные массивы для работы. И в самом конце мы используем функцию to_categorical, что бы изменить форму data1, у нас был массив такого вида - [ 0, 1, 2, 3, ... 32, 0, 1, 2, 3, ... 32, ], теперь у нас массив такого вида - [ [1,0,0, ... 0,0,0,], [0,1,0, ... 0,0,0,] ]. Если коротко сказать, что мы цифры индексов - 0,1,2, и т.д, указали в векторном представлении, это значит, что в место цифры допустим 2, у нас есть вектор(массив состоящих из цифр) с размерностью - 33, тоже и количество классов, и мы образно говоря, договорились между собой, если нам нужно значение чисел от 0 до 32, представить в виде вектора, то мы создаем массив(вектор) состоящий из 33 элементов(количество классов для распознавания), где каждый элемент массива(вектора) равен 0, кроме того элемента, индекс которого равен числу, который мы представляем в виде вектора. То есть если у нас количество классов для классификации 4, то обозначается это так, - [0, 1, 2, 3], но для классификации, нужно представить цифровое значение каждого класса в вектор, то получится [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ].
Сама нейронная сеть. Как мы видим, мы создаем переменную model, - это и есть модель нашей нейронной сети. Мы из keras вызываем функцию Sequential, которая принимает в себя аргумент, - массив, где каждый элемент массива, это отдельный слой нейронной сети. Единственное уточнение: первый элемент массива, это не отдельный слой нейронной сети, а функция в которой указывается размерность входных данных.
Первой строчкой, мы компилируем модель, а именно, указываем какая будет loss(потеря), то есть функции оценки точности нейронной сети, на основе этого оптимизатор будет обновлять веса используя разницу в оценке. Дальше мы указываем оптимизатор, у нас будет оптимизатор - Adam, мы используем его потому, что у него лучшая сходимость. И последним, мы указываем какую метрику будем отслеживать, это не обязательный аргумент, просто, я хочу видеть, какая точность на каждой эпохе, по этому я указываю метрику accuracy, что с английского переводится, как точность. На следующей строчке, мы вызываем функцию под названием fit, которая инициирует начало обучения, аргументы которые передаются, - это массив изображений, массив классов, и количество эпох. После этого, создав переменную m - передаем значение ответа/предсказания(predict), функция возвращает вектор, того же типа, что мы передавали в массив data1. Вектор с ответом выглядит так, - [[1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 4.161553e-37 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 3.723864e-38 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00]], как мы видим, везде значения нулей или чисел, которые очень близки к нулю, например - 4.161553e-37, число предельно маленькое, что оно приблизительно равно 4.16 в минус 37 степени(для тех кто не знает: если число представить в виде обыкновенной дроби например 5/1(пять первых) и возвести эту дробь в отрицательную степень, то число переноситься в знаменатель и уже там возводиться в степень, т.е (5/1)^-3 = 1/5^3 = 1/125, и если перевести эту дробь в формат десятичной, по получиться 0.008, т.е число приближенное к нулю, если взять число 4.16 и возвести допустим не в -37 степень, а к примеру в -5, то получится число 0.0008026631901946794, вот настолько маленькое, а теперь можно представить, насколько маленьким будет число в -37 степени. Так же видно, что первый элемент массива равен ровно единице. Мы весь этот массив передаём в функцию np.argmax и это функция возвращает индекс элемента с максимальным значением, в моём примере , он вернет 0, по этому, мы просто прибавляем единицу к ответу, вот правильный ответ.

Точность 97%.В целом, то что нас получилось, называется переобучение и это считается плохим, но, по моему скромному мнению, когда у нас есть задача определять что за буква указана, и самое ключевое, то, что буква печатная а не от руки написанная, и соответственно нейронная сеть, должна запомнить эту букву, а не определять вероятность, т.к она должна запомнить шрифт, т.е будет указанно, что это буква, например - "а", с вероятностью 100%.
Надеюсь вам было интересно, все доброго.
