Как стать автором
Обновить

Учимся распознавать прописные русские буквы на коленке. python/keras

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров2.3K

Добрый день, добрые друзья! Я решил научить нейронную сеть различать рукописные русские буквы, как говорится - на коленке.

Первым делом, стоит найти или сделать(сделать - громко сказано, с такими техническими возможностями). Я не нашел дата сет с русскими буквами, но я решил его сделать.

Первым делом, мы должны понять как именно будем создавать дата сет, очевидно рисуя буквы.

Но где? Я решил это делать на телефоне в приложении "рисовалке" - 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 ---------

Объяснения:

  1. Как видно из комментария, тут мы просто импортируем библиотеки. Первый импорт, библиотека os - предоставляет функции для взаимодействия с операционной системой(файлы, пути, процессы и т.д). Второй импорт, это сам keras. Третий импорт, это библиотека numpy, она нужна математических операций, далее есть приписка 'as np', то есть мы переименовываем название библиотеки, что бы было проще к ней обращаться. Четвертый импорт, это библиотека cv2, нужна для работы с изображением. Пятый импорт, мы из keras пакета layers - импортируем слои которые требуются для работы нейронной сети, а именно: Dense - полносвязный слой, Dropout - слой, который предотвращает переобучение, грубо говоря, он в процессе обучения отключает некоторые нейроны(рандом), Conv2D - слой, который создаёт свертку, выделяет признаки в изображении, Flatten - изменяет форму входных данных в одномерный вектор, MaxPooling2D - используется для уменьшения размерности входных данных, применяя операцию максимальной пулинга, т.е из окна пикселей(квадрата 2 на 2) выбирает пиксель с максимальным числовым значением.

  2. Мы создаем переменную, в которой мы указываем размерность входных данных, т.е указываем какого размера изображение будет входить в нейронную сеть, а именно: ширина, высота, размерность пикселя(цифра 1 указывает на то, что каждый пиксель имеет значение от 0 до 255, то есть черно-белое изображение). Дальше создаем массив data, который представляет входные данные, и массив data1, который представляет выходные данные.

  3. Здесь, как мне кажется, раскрывается весь смысл названия этого поста. Смысл этого блока в том, что мы заполняем сам "датасет", используя цикл в цикле, во вложенном цикле мы добавляем в массив изображение буквы - "а" а в другой массив её индекс по счету, индекс начинается с 0. Соответственно: наш массив data выглядит так, [изображение буквы а, изображение буквы б, изображение буквы в, ... изображение буквы я, изображение буквы а, изображение буквы б, изображение буквы в, ... изображение буквы я], т.е в массиве у нас просто буквы русского алфавита повторяются по порядку. Массив data1 имеет следующий вид, [ 0, 1, 2, 3, ... 32, 0, 1, 2, 3, ... 32, ] т.е в массивы у нас просто индексы букв алфавита, индекс начинается с 0, т.е 0 индекс - это буква "а", 32 индекс - это буква "я".

  4. Мы создаем переменную 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] ].

  5. Сама нейронная сеть. Как мы видим, мы создаем переменную model, - это и есть модель нашей нейронной сети. Мы из keras вызываем функцию Sequential, которая принимает в себя аргумент, - массив, где каждый элемент массива, это отдельный слой нейронной сети. Единственное уточнение: первый элемент массива, это не отдельный слой нейронной сети, а функция в которой указывается размерность входных данных.

  6. Первой строчкой, мы компилируем модель, а именно, указываем какая будет 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%.

Надеюсь вам было интересно, все доброго.

Теги:
Хабы:
Всего голосов 6: ↑0 и ↓6-6
Комментарии2

Публикации

Работа

Data Scientist
42 вакансии

Ближайшие события