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