Как отличать птиц от цветов. Или цветы от птиц

    В качестве программы выходного дня мне захотелось поиграться с как бы «нейронной» сетью (спойлер — в ней нет нейронов). А чтобы потом не было мучительно больно за бесцельно прожитые годы часы, я подумал, что зря мы его кормим, пусть пользу приносит — пусть заодно эта сетка разберет домашний фотоархив и хотя бы разложит фотографии цветов в отдельную папку.

    Самая простая сеть


    Самая простая сеть нашлась в статье "Нейросеть в 11 строчек на Python" (это перевод от SLY_G статьи "A Neural Network in 11 lines of Python (Part 1)", вообще у автора есть еще продолжение "A Neural Network in 13 lines of Python (Part 2 — Gradient Descent)", но здесь достаточно первой статьи).

    Краткое описание сетки — в этой сети есть ровно одна зависимость — NumPy.

    Множество входов рассматривается как матрица $X$, множество выходов — как вектор $y$. В оригинальной статье сеть умножает входную матрицу, размерностью (4 x 3), на матрицу весов входов $syn0$ (3 x 4), к произведению применяет передаточную функцию, и получает матрицу слоя $l1$ (4 x 4).

    $ X = \begin{bmatrix} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 9 & 10 & 11 & 12 \end{bmatrix} \\ y = \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} $


    $ f(X \times syn0) \implies l1 $


    Далее слой $l1$ умножается на матрицу весов выходов $syn1$ (4 x 1), также пропускается через функцию, и получается слой $l2$ (4 x 1), который и есть результат работы сети.

    $ f(l1 \times syn1) \implies l2 \implies y $

    Итого, опуская скалярную передаточную функцию, сеть реализует два матричных умножения:

    $ X \times syn0 \times syn1 \implies y $

    Следствием этого, согласно правилам матричного умножения, получилось, что одна из размерностей в ходе работы сети не изменяется $(4 * 3) \times (3 * 1) = (4 * 1)$ и получить на выходе единственное число невозможно.

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

    Например, если нужно, чтобы было на входе матрица (3 x 4), а выход — единственное число, то добавляем две матрицы синапсов (4 x 1) и (3 x 1):

    $((\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \end{bmatrix} \times \begin{bmatrix} 0 \\ 0 \\1 \\ 1 \end{bmatrix})^T \times \begin{bmatrix} 0 \\ 1 \\ 1 \end{bmatrix})^T = [1] \\ (((3 * 4) \times (4 * 1))^T \times (3*1))^T = (1 * 1) \\ $

    Или, скажем, можно преобразовать входную матрицу (10 x 8) на выход (4 x 5):

    $(((10 * 8) \times (8 * 5))^T \times (10*4))^T = (4 * 5)$


    Получившийся код:

    nnmat.py
    import numpy as np
    
    def nonlin(x,deriv=False):
        if(deriv==True):
            return (x)*(1-(x))
        return 1/(1+np.exp(-x))
    
    def fmax(x,deriv=False):
        if(deriv==True):
            return 0.33
        return np.maximum(x,0)/3
    
    class NN:
        def __init__(self, shapes, func=nonlin):
            self.func = func
            self.shapes = shapes
            self.syns = [ 2*np.random.random((shapes[i-1][1],shapes[i][0])) - 1
                          for i in range(1, len(shapes)) ]
            self.layers = [ np.zeros(shapes[i])
                            for i in range(1, len(shapes)) ]
        
        def learn(self, X, y, cycles):
            for j in range(cycles):
                res = self.calc(X)
                prev = y - res
                for i in range(len(self.layers)-1,-1,-1):
                    l_delta = (prev*self.func(self.layers[i], True)).T
                    if i == 0:
                        self.syns[i] += X.T.dot(l_delta)
                    else:
                        prev = l_delta.dot(self.syns[i].T)
                        self.syns[i] += self.layers[i-1].T.dot(l_delta)
            return self.layers[-1]
    
        def calc(self,X):
            for i in range(len(self.syns)):
                if i == 0:
                    self.layers[i] = self.func(np.dot(X,self.syns[i])).T
                else:
                    self.layers[i] = self.func(np.dot(self.layers[i-1],self.syns[i])).T
            return self.layers[-1]
    
    if __name__ == '__main__':
        X = np.array([ [0,0,1],[0,1,1],[1,0,1],[1,1,1] ])
        y = np.array([[0,1,1,0]])
        print('X =',X)
        print('y =',y)
        nn = NN((X.shape, (y.shape[1], X.shape[0]), y.shape))
        nn.learn(X,y,1000)
        print('Result =',nn.calc(X).round(2))
     
    

    Результат работы:

    X = [[0 0 1]
     [0 1 1]
     [1 0 1]
     [1 1 1]]
    y = [[0 1 1 0]]
    Result = [[ 0.02  0.99  0.98  0.02]]

    Загрузка фотографий


    Так, сетка есть, теперь надо разобраться с загрузкой фоток. Фотографии лежат на диске, в основном в JPG, но встречаются и другие форматы. Размеры у них тоже разные, смотря чем снимали и как обрабатывали, от 3 Mpx до 16 Mpx.

    Сначала я попробовал загружать фотографии через Qt, класс QImage, он умеет работать с разными форматами, обеспечивает конверсию и дает прямой доступ к данным картинки. Наверняка в Python существует способ проще, но зато с QImage мне не надо было разбираться. Чтобы сеть могла работать с картинкой, следует перевести в монохромное изображение и уменьшить до стандартного размера.

    def readImage(file, imageSize):
            img = QImage(file)
            if img.isNull():
                return 0
            img = img.convertToFormat(QImage.Format_Grayscale8)
            img = img.scaled(imageSize[0],imageSize[1],Qt.IgnoreAspectRatio)
            return img
    

    Для передачи в сетку нужно преобразовать изображение в матрицу numpy.ndarray. QImage.bits() дает указатель на данные изображения, где каждый байт соответствует пикселу. В NumPy нашлась функция recarray, способная сделать массив записей из буфера, а у него есть метод view, который нам сделает матрицу numpy.ndarray без копирования данных.

            srcBi = img.bits()
            srcBi.setsize(img.width() * img.height())
            srcBy = bytes(srcBi)
            srcW, srcH = img.width(), img.height()
            srcArr = np.recarray((srcH, srcW), dtype=np.int8, buf=srcBy).view(dtype=np.byte,type=np.ndarray)
    

    Сеть для изображений


    Картинку, хоть и уменьшенную, непосредственно подавать на вход сети будет слишком накладно — я уже говорил, что сеть делает матричное умножение, поэтому даже один цикл обучения будет приводить к 400x400x400 = 64 млн. умножений. Знатоки рекомендуют использовать свертку. В Википедии есть замечательная иллюстрация ее работы:


    На этой анимации видно, что размерность результата равна размерности исходной матрицы. Но я немного упрощу себе жизнь, буду двигаться не по пикселам, а разобью изображение на кусочки размером равным матрице входов, и применю сетку к ним поочередно. В матрицах вырезание кусочка делается достаточно просто:

    srcArr[x:x+dw, y:y+dw]

    Результат обработки кусочков сетью складывается в матрицу меньшего размера, эта матрица передается на вход общей сети. То есть будет две сети — первая работает с кусочками изображения, вторая — с результатом работы первой сети над кусочками.

    Создание первичной сети:

    class ImgNN:
        def __init__(self, shape, resultShape = (16, 16), imageSize = (400,400)):
            self.resultShape = resultShape
            self.w = imageSize[0] // shape[0]
            self.h = imageSize[1] // shape[1]
            self.net = NN([shape, (1,shape[0]), (1,1)])
            self.shape = shape
            self.imageSize = imageSize
    

    Внутри создается self.net — собственно сеть, с заданным размером матрицы входов shape и c выходом в виде элементарной матрицы 1х1. Да, можно было наследоваться от класса сети NN, но был выходной, хотелось побыстрее получить результат, а архитектура еще не устоялась. Time to market бьется в наших сердцах!

    Обсчет изображения первой сетью:

        def calc(self, srcArr):
            w = srcArr.shape[0] // self.shape[0]
            h = srcArr.shape[1] // self.shape[1]
            resArr = np.zeros(self.resultShape)
            for x in range(w):
                for y in range(h):
                    a = srcArr[x:x+self.shape[0], y:y+self.shape[1]]
                    if a.shape != (self.shape[0], self.shape[1]):
                        continue
                    if x >= self.resultShape[0] or y >= self.resultShape[1]:
                        continue
                    res = self.nn.calc(a)
                    resArr[x,y] = res[0,0]
            return resArr
    

    На выходе имеем матрицу resArr, с размерностью, равной количеству кусочков, на которые было разбито изображение. Эту матрицу передаем на вход второй сети, которая даcт конечный результат.

        y = np.array([[1,0,1,0]])
        firstShape = (40, 40)
        middleShape = (5, 5)
        imageSize = firstShape[0]*middleShape[0], firstShape[1]*middleShape[1]
    ...
                nn = ImgNN(firstShape, resultShape=middleShape, imageSize=imageSize)
                nn2 = NN([middleShape, (y.shape[1], middleShape[0]), y.shape])
    ...
                    i = readImage(f, imageSize)
                    mid = nn.calc(i)
                    res = nn2.calc(mid)
    

    Тут вы должны меня спросить, откуда я взял первую строчку, и что она значит:

    y = np.array([[1,0,1,0]])

    Это — ожидаемый результат сети в случае положительного ответа, т.е. если сеть считает, что на входе изображение цветка. Размерность выбрал из принципа «ни мало, ни много» — если брать размерность 1х1, то из одного получившегося числа трудно судить, насколько сеть «сомневается» в результате. Большую размерность задавать тоже смысла нет — она не даст больше информации. Равное количество нулей и единиц дает четкий ориентир — чем ближе к нему, тем больше совпадение. Если же взять все единицы или все нули, то у сети появится стимул к переобучению — увеличить все сомножители или, соответственно, обнулить их, чтобы получать нужный результат независимо от входных данных.

    Как обучать сверточную сеть?


    Обучающую выборку я сделал из своих же фотографий, попросту разложив их в два каталога:
    flowers



    и noflowers



    Пути к картинкам соберу в два массива

                import os
                fl = [e.path for e in os.scandir('flowers')]
                nofl = [e.path for e in os.scandir('noflowers')]
                all = fl+nofl
    

    Обучать простые сети обычно, в том числе в оригинальной статье, предлагается традиционным методом — обратным распространением ошибки. Но чтобы этот метод применить к сверточной сети, состоящей из двух элементарных, нужно обеспечить сквозную передачу накопленной ошибки из второй сети в первую. Вообще для сверточных сетей есть и другие методы. Переделывать работающую сеть мне было лень, по крайней мере пока, поэтому решил обучить вторую сеть, а первую вообще не обучать, оставить забитой при создании случайными значениями, рассудив, что раз глазные нервы у человека не обучаются, то и мне нечего обучать первичную сеть, «смотрящую» на изображение.

    
            for epoch in range(100):
                print('Epoch =', epoch)
                nn = ImgNN(firstShape, resultShape=middleShape, imageSize=imageSize)
                nn2 = NN([middleShape, (y.shape[1], middleShape[0]), y.shape])
                for f in fl:
                    i = readImage(f, imageSize)
                    # nn.learn(i, yy, 1)
                    mid = nn.calc(i)
                    nn2.learn(mid, y, 1000)
    

    В каждой эпохе сразу после обучения прогоняю через сеть всю выборку и смотрю, что получилось.

                for f in all:
                    i = readImage(f, imageSize)
                    mid = nn.calc(i)
                    res = nn2.calc(mid)
                    delta = abs(y-res)
                    v = round(np.std(delta),3)
    

    Если сеть обучилась правильно, то на ее выходе должно быть значение, близкое к заданному [[1,0,1,0]], если на входе цветок, и как можно более отличающееся от заданного, например [[0,1,0,1]], если на входе не цветок. Результат оценивается, эмпирически я принял отклонение от успешного результат не более 0,2 — это тоже успешный результат, и считается число ошибок. Из всех прогонов выбираем такую, где делается меньше всего ошибок, и сохраняем веса синапсов обоих сеток в файлы. Дальше эти файлы можно использовать для загрузки сеток.

                    if v > 0.2 and f in fl:
                        fails += 1
                        failFiles.append(f)
                    elif v<0.2 and f in nofl:
                        fails +=1
                        failFiles.append(f)
                if minFails == None or fails < minFails:
                    minFails = fails
                    lastSyns = nn.net.syns
                    lastSyns2 = nn2.syns
                print('fails =',fails, failFiles)
                print('min =',minFails)
                if minFails <= 1:
                    print('found!')
                    break
            for i in range(len(lastSyns)):
                np.savetxt('syns_save%s.txt'%i, lastSyns[i])
            for i in range(len(lastSyns2)):
                np.savetxt('syns2_save%s.txt'%i, lastSyns2[i])
    

    Хоть розой назови её, хоть нет


    С надеждой запускаю и… подождав..., потом еще подождав..., и еще… получаю полный бред — сетка не обучается:

    Ничего не вышло
    flowers\178.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\179.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\180.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\182.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\186-2.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\186.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\187.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\190 (2).jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\190.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\191.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\195.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\199.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    flowers\2.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    flowers\200.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\032.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\085.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    noflowers\088.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\122.JPG res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\123.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\173.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    noflowers\202.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\205.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\cutxml.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
    noflowers\Getaway.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    noflowers\IMGP1800.JPG res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
    noflowers\trq-4.png res = [[ 0.97 0.51 0.97 0.51]] v = 0.239
    fails = 14


    Будучи носителем настоящих живых, а не искусственных нейронов, до меня дошло, что главным отличием цветов является цвет (да, кэп, спасибо, что ты всегда рядом, хотя зачастую опаздываешь со своими советами). Поэтому надо бы перевести его в какую-то цветовую модель, где цветовая составляющая будет выделена (HSV или HSL), и обучать сеть на цвете.

    Но оказалось, что класс QImage не знает такие цветовые пространства. Пришлось отказаться от него и загружать фотки с помощью OpenCV, где такая возможность есть.

    import cv2
    
    def readImageCV(file, imageSize):
        img = cv2.imread(file)
        small = cv2.resize(img, imageSize)
        hsv = cv2.cvtColor(small, cv2.COLOR_BGR2HSV)
        return hsv[:,:,0]/255
    

    Правда, OpenCV наотрез отказался работать с русскими буквами в именах файлов, пришлось их переименовать.

    Запустил — результат не порадовал, практически тот же.

    Еще подумал, решил, что проблема в сильно случайных значениях в первой сетке, зря я понадеялся, что звезды сойдутся без моей помощи, поэтому добавил ей небольшое предобучение, всего 2 цикла на файл. Для образца положительного результата взял единичную матрицу.

            yy = np.zeros(middleShape)
            np.fill_diagonal(yy,1)
    ...
                for f in fl:
                    i = readImage(f, imageSize)
                    nn.learn(i, yy, 2) # чуть-чуть обучаем первую сетку
                    mid = nn.calc(i)
                    nn2.learn(mid, y, 1000)
    

    Снова запустил — стало куда интереснее, цифры стали меняться, хотя идеала не достиг.

    Лучший результат
    Epoch = 34
    flowers\178.jpg res = [[ 0.86 0.47 0.88 0.47]] v = 0.171
    flowers\179.jpg res = [[ 0.87 0.51 0.89 0.5 ]] v = 0.194
    flowers\180.jpg res = [[ 0.79 0.69 0.79 0.67]] v = 0.233
    flowers\182.jpg res = [[ 0.87 0.53 0.88 0.48]] v = 0.189
    flowers\186-2.jpg res = [[ 0.89 0.41 0.89 0.39]] v = 0.144
    flowers\186.jpg res = [[ 0.85 0.54 0.83 0.55]] v = 0.194
    flowers\187.jpg res = [[ 0.86 0.54 0.86 0.54]] v = 0.199
    flowers\190 (2).jpg res = [[ 0.96 0.25 0.97 0.15]] v = 0.089
    flowers\190.jpg res = [[ 0.95 0.13 0.97 0.14]] v = 0.048
    flowers\191.jpg res = [[ 0.81 0.57 0.82 0.57]] v = 0.195
    flowers\195.jpg res = [[ 0.81 0.55 0.79 0.56]] v = 0.177
    flowers\199.jpg res = [[ 0.89 0.45 0.89 0.45]] v = 0.171
    flowers\2.jpg res = [[ 0.83 0.56 0.83 0.55]] v = 0.195
    flowers\200.jpg res = [[ 0.91 0.42 0.89 0.43]] v = 0.163
    noflowers\032.jpg res = [[ 0.7 0.79 0.69 0.8 ]] v = 0.246
    noflowers\085.jpg res = [[ 0.86 0.53 0.86 0.53]] v = 0.192
    noflowers\088.jpg res = [[ 0.86 0.56 0.87 0.53]] v = 0.207
    noflowers\122.JPG res = [[ 0.81 0.63 0.81 0.62]] v = 0.218
    noflowers\123.jpg res = [[ 0.83 0.59 0.84 0.55]] v = 0.204
    noflowers\173.jpg res = [[ 0.83 0.6 0.83 0.58]] v = 0.209
    noflowers\202.jpg res = [[ 0.78 0.7 0.8 0.65]] v = 0.234
    noflowers\205.jpg res = [[ 0.84 0.77 0.79 0.75]] v = 0.287
    noflowers\cutxml.jpg res = [[ 0.81 0.61 0.81 0.63]] v = 0.213
    noflowers\Getaway.jpg res = [[ 0.85 0.56 0.85 0.55]] v = 0.202
    noflowers\IMGP1800.JPG res = [[ 0.85 0.55 0.86 0.54]] v = 0.199
    noflowers\trq-4.png res = [[ 0.7 0.72 0.7 0.71]] v = 0.208
    fails = 3 ['flowers\\180.jpg', 'noflowers\\085.jpg', 'noflowers\\IMGP1800.JPG']
    min = 3


    Дальше… А дальше выходной закончился, и мне пора было заниматься хозработами.

    Что делать дальше?


    Конечно, и эта сеть, и то, как я ее учил, и тестовый dataset очень мало соотносятся с реальными сетями и тем, чем занимаются data scientists. Это лишь игрушка для гимнастики ума, не возлагайте на нее больших надежд.

    Можно наметить дальнейшие шаги, как добиться нужного результата (если он вам нужен):

    1. Добавить еще один промежуточный слой или несколько во вторую сеть — так у нее появится больше свободы в обучении. Все-таки сеть на матричном умножении не совсем классическая, так как в ней меньше связей-синапсов между слоями, да и сами синапсы не уникальны.
    2. Использовать приближения к успешным результатам как заготовки для последующих обучений — т.е. запоминать веса синапсов самого успешного результата, а не затиратьвсе случайными значениями.
    3. Попробовать генетические алгоритмы — смешивать и делить, размножать успешное и отбраковывать неудачное.
    4. Пробовать другие способы обучения, коих уже вагон и маленькая тележка.
    5. Использовать больше информации из исходного изображения, например, одновременно цвет и монохром подавать на различные сети, результаты обрабатывать в общей сети.

    Исходный код
    import numpy as np
    from nnmat import *
    import os
    
    import sys
    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    import meshandler
     
    import random
    import cv2
    
    class ImgNN:
        def __init__(self, shape, resultShape = (16, 16), imageSize = (400,400)):
            self.resultShape = resultShape
            self.w = imageSize[0] // shape[0]
            self.h = imageSize[1] // shape[1]
            self.net = NN([shape, (1,shape[0]), (1,1)])
            self.shape = shape
            self.imageSize = imageSize
    
        def learn(self, srcArr, result, cycles):
            for c in range(cycles):
                for x in range(self.w):
                    for y in range(self.h):
                        a = srcArr[x:x+self.shape[0], y:y+self.shape[1]]
                        if a.shape != (self.shape[0], self.shape[1]):
                            print(a.shape)
                            continue
                        self.net.learn(a, result[x,y], 1)
    
        def calc(self, srcArr):
            resArr = np.zeros(self.resultShape)
            for x in range(self.w):
                for y in range(self.h):
                    a = srcArr[x:x+self.shape[0], y:y+self.shape[1]]
                    if a.shape != (self.shape[0], self.shape[1]):
                        continue
                    if x >= self.resultShape[0] or y >= self.resultShape[1]:
                        continue
                    res = self.net.calc(a)
                    resArr[x,y] = res[0,0]
            return resArr
            
        def learnFile(self, file, result, cycles):
            return self.learn(readImage(file, self.imageSize), result, cycles)
    
        def calcFile(self, file):
            return self.calc(readImage(file, self.imageSize))
    
    def readImageCV(file, imageSize):
        img = cv2.imread(file)
        small = cv2.resize(img, imageSize)
        hsv = cv2.cvtColor(small, cv2.COLOR_BGR2HSV)
        return hsv[:,:,0]/255
    
    def readImageQ(file, imageSize):
        img = QImage(file)
        if img.isNull():
            return 0
        img = img.convertToFormat(QImage.Format_Grayscale8)
        img = img.scaled(imageSize[0],imageSize[1],Qt.IgnoreAspectRatio)
        srcBi = img.bits()
        srcBi.setsize(img.width() * img.height())
        srcBy = bytes(srcBi)
    
        srcW, srcH = img.width(), img.height()
        srcArr = np.recarray((srcH, srcW), dtype=np.uint8, buf=srcBy).view(dtype=np.uint8,type=np.ndarray)
        return srcArr/255
    
    if __name__ == '__main__':
        readImage = readImageCV
        
        y = np.array([[1,0,1,0]])
        firstShape = (40, 40)
        middleShape = (10, 10)
        imageSize = firstShape[0]*middleShape[0], firstShape[1]*middleShape[1]
        
        StartLearn = True
    
        if not StartLearn:
            pictDir = '2014-05'
            nn = ImgNN(firstShape, resultShape=middleShape, imageSize=imageSize)
            nn.net.syns[0] = np.loadtxt('syns_save0.txt')
            nn.net.syns[1] = np.loadtxt('syns_save1.txt')
            nn2 = NN([middleShape, (y.shape[1], middleShape[0]), y.shape])
            nn2.syns[0] = np.loadtxt('syns2_save0.txt')
            nn2.syns[1] = np.loadtxt('syns2_save1.txt')
            files = [e.path for e in os.scandir(pictDir)]
            for f in files:
                i = readImage(f, imageSize)
                res = nn2.calc(i)
                delta = y-res
                v = round(np.std(delta),3)
                if v < 0.2:
                    print('Flower',f)
                else:
                    print('No flower',f)
        else:    
            fl = [e.path for e in os.scandir('flowers')]
            nofl = [e.path for e in os.scandir('noflowers')]
            all = fl+nofl
            yy = np.zeros(middleShape)
            np.fill_diagonal(yy,1)
            minFails = None
            for epoch in range(100):
                print('Epoch =', epoch)
                nn = ImgNN(firstShape, resultShape=middleShape, imageSize=imageSize)
                nn2 = NN([middleShape, (y.shape[1], middleShape[0]), y.shape])
                for f in fl:
                    i = readImage(f, imageSize)
                    nn.learn(i, yy, 2)
                    mid = nn.calc(i)
                    nn2.learn(mid, y, 1000)
                fails = 0
                failFiles = []
                for f in all:
                    i = readImage(f, imageSize)
                    mid = nn.calc(i)
                    res = nn2.calc(mid)
                    delta = abs(y-res)
                    v = round(np.std(delta),3)
                    #v = round(delta.sum(),3)
                    print(f, 'res = ', res.round(2),'v =',v)
                    if v > 0.2 and f in fl:
                        fails += 1
                        failFiles.append(f)
                    elif v<0.2 and f in nofl:
                        fails +=1
                        failFiles.append(f)
                if minFails == None or fails < minFails:
                    minFails = fails
                    lastSyns = nn.net.syns
                    lastSyns2 = nn2.syns
                print('fails =',fails, failFiles)
                print('min =',minFails)
                if minFails <= 1:
                    print('found!')
                    break
            for i in range(len(lastSyns)):
                np.savetxt('syns_save%s.txt'%i, lastSyns[i])
            for i in range(len(lastSyns2)):
                np.savetxt('syns2_save%s.txt'%i, lastSyns2[i])
    




    Продолжение
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 15
    • 0
      Я буду с нетерпением ждать следующих выходных :)
      • 0
        А что-нибудь коробочное принципиально не использовали? Написать CNN на том же TensorFlow тоже довольно интересно, полная гибкость и контроль, Python дает полный доступ к возможностям библиотеки — сказка!
        • 0
          Отчасти принципиально не использовал. С коробочными решениями есть одна проблема — прежде чем их использовать, мне нужно сначала понимать, что я хочу с ними сделать. Не являясь ни специалистом в машинном обучении, ни математиком, давно хотелось взять что-то совсем элементарное и покрутить в руках, что выявить все недостатки моего наивного подхода. А тут как раз наткнулся на хабре на перевод SLY_G.

          Если бы машинное обучение было бы моей работой, то я бы так не делал. Наверное.

          Кстати, здесь есть хорошая статья по TensorFlow, оставлю ссылку здесь.
          Еще PyTorch.
          Да что там, можно ничего другого не подключать, даже в OpenCV есть реализация нейронных сетей и глубокого обучения.
        • +1

          Дополнительные слои редко дают выигрыш. Каждый лишний слой это лишняя стадия в распространении ошибки при обратном проходе(back propagation).
          Гораздо лучших результатов можно добиться нормализацией входных значений. Но тут нужно постараться не потерять важное при нормализации.


          Но изображения лучше всего обрабатывать сверточными сетями с предварительной тренировкой (энкодер) входных слоёв.

          • +1
            >Дополнительные слои редко дают выигрыш.
            Ну если не ошибаюсь, то больше слоев — больше «выразительность» сети, т.е. больше возможностей уменьшить ошибку на train set. Так что дают, но этим выигрышем надо еще воспользоваться.
            > с предварительной тренировкой (энкодер) входных слоёв.
            Вроде как совсем необязательно. А вот предобученную на ImageNet сеть и правда есть смысл использовать.
            • +1
              Более того, году так в 85-90 была доказана теорема что для любой многослойной сети существует функционально аналогичная сеть с одним внутренним слоем.
              • 0
                Эта теорема Хехт-Нильсена? По моему, она работает только для полносвязных сетей, где каждый нейрон скрытого слоя связан с каждым нейроном входного слоя, и также каждый с каждым между скрытым и выходным слоями. В рассматриваемой в статье сети это не так, потому что она использует матричное умножение, а в нем нейрон скрытого слоя связан лишь с одной строкой входной матрицы.
                Схема умножения матриц
                image
            • +1

              И да, размерность выхода вашей сети должна быт 1. Порог ,5 (но можно с ним поиграться). Оценка точности предсказания — просто по максимальным значениям. Ваш вариант с размерностью 4 просто размазывает результат на 4 значения, оценивать которые гораздо сложнее.

              • 0
                Спасибо.
                Размерность 1 я пробовал в одном из вариантов, но понял, что я не понимаю, что происходит — учится она или нет, поэтому для наглядности сделал 4.

                Насчет доп.слоев и нормализации я подумаю. Вообще используя цветовую составляющую уже происходит нормализация по яркости и насыщенности.
              • 0
                А если основное различие происходит на границах нарезанных кусочков матрицы, то получим неправильный результат?
                • 0

                  В общем случае необязательно, поскольку суммирующая сеть может быть готова к такой ситуации. Но конечно, разбивка на кусочки — это осознанное загрубление результата и риск потери деталей.

                • 0
                  Ну т.е. в итоге неуспех. А смысл тогда статью писать, какое value?
                  • 0
                    Я понимаю — включаешь так фильм по ТВ, смотришь, думаешь боевик, а нет, опять сериал.
                    А вам было не интересно?
                  • 0
                    Спасибо за статью. Подскажите как исправить ошибку
                    ModuleNotFoundError: No module named 'meshandler'

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.