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

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


Самая простая сеть нашлась в статье "Нейросеть в 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)


  1. capjdcoder
    07.10.2017 21:08

    Я буду с нетерпением ждать следующих выходных :)


  1. QtRoS
    08.10.2017 00:38

    А что-нибудь коробочное принципиально не использовали? Написать CNN на том же TensorFlow тоже довольно интересно, полная гибкость и контроль, Python дает полный доступ к возможностям библиотеки — сказка!


    1. sshmakov Автор
      08.10.2017 10:27

      Отчасти принципиально не использовал. С коробочными решениями есть одна проблема — прежде чем их использовать, мне нужно сначала понимать, что я хочу с ними сделать. Не являясь ни специалистом в машинном обучении, ни математиком, давно хотелось взять что-то совсем элементарное и покрутить в руках, что выявить все недостатки моего наивного подхода. А тут как раз наткнулся на хабре на перевод SLY_G.

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

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


  1. Sly_tom_cat
    08.10.2017 15:01
    +1

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


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


    1. Dark_Daiver
      08.10.2017 20:29
      +1

      >Дополнительные слои редко дают выигрыш.
      Ну если не ошибаюсь, то больше слоев — больше «выразительность» сети, т.е. больше возможностей уменьшить ошибку на train set. Так что дают, но этим выигрышем надо еще воспользоваться.
      > с предварительной тренировкой (энкодер) входных слоёв.
      Вроде как совсем необязательно. А вот предобученную на ImageNet сеть и правда есть смысл использовать.


    1. skiedr
      08.10.2017 22:07
      +1

      Более того, году так в 85-90 была доказана теорема что для любой многослойной сети существует функционально аналогичная сеть с одним внутренним слоем.


      1. sshmakov Автор
        08.10.2017 23:57

        Эта теорема Хехт-Нильсена? По моему, она работает только для полносвязных сетей, где каждый нейрон скрытого слоя связан с каждым нейроном входного слоя, и также каждый с каждым между скрытым и выходным слоями. В рассматриваемой в статье сети это не так, потому что она использует матричное умножение, а в нем нейрон скрытого слоя связан лишь с одной строкой входной матрицы.

        Схема умножения матриц
        image


  1. Sly_tom_cat
    08.10.2017 15:15
    +1

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


    1. sshmakov Автор
      08.10.2017 17:54

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

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


  1. romy4
    08.10.2017 18:54

    А если основное различие происходит на границах нарезанных кусочков матрицы, то получим неправильный результат?


    1. sshmakov Автор
      08.10.2017 19:40

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


  1. andd3dfx
    08.10.2017 22:18

    Ну т.е. в итоге неуспех. А смысл тогда статью писать, какое value?


    1. sshmakov Автор
      09.10.2017 10:08

      Я понимаю — включаешь так фильм по ТВ, смотришь, думаешь боевик, а нет, опять сериал.
      А вам было не интересно?


  1. mouze1976
    10.10.2017 23:30

    Спасибо за статью. Подскажите как исправить ошибку

    ModuleNotFoundError: No module named 'meshandler'


    1. sshmakov Автор
      10.10.2017 23:39

      meshandler — это модуль для вывода отладочных сообщений Qt. Его можно скачать, например, отсюда github.com/sshmakov/conopy/blob/master/conopy/meshandler.py

      Или можно убрать строчку

      import meshandler