В качестве программы выходного дня мне захотелось поиграться с как бы «нейронной» сетью (спойлер — в ней нет нейронов). А чтобы потом не было мучительно больно за бесцельно прожитые годы часы, я подумал, что зря мы его кормим, пусть пользу приносит — пусть заодно эта сетка разберет домашний фотоархив и хотя бы разложит фотографии цветов в отдельную папку.
Самая простая сеть
Самая простая сеть нашлась в статье "Нейросеть в 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.
Множество входов рассматривается как матрица , множество выходов — как вектор . В оригинальной статье сеть умножает входную матрицу, размерностью (4 x 3), на матрицу весов входов (3 x 4), к произведению применяет передаточную функцию, и получает матрицу слоя (4 x 4).
Далее слой умножается на матрицу весов выходов (4 x 1), также пропускается через функцию, и получается слой (4 x 1), который и есть результат работы сети.
Итого, опуская скалярную передаточную функцию, сеть реализует два матричных умножения:
Следствием этого, согласно правилам матричного умножения, получилось, что одна из размерностей в ходе работы сети не изменяется и получить на выходе единственное число невозможно.
Поэтому я немного доработал код из статьи, добавил транспонирование после умножения и работу с произвольным числом слоев в сетке. Это дало мне возможность получать любое сочетание размерностей входов и выходов.
Например, если нужно, чтобы было на входе матрица (3 x 4), а выход — единственное число, то добавляем две матрицы синапсов (4 x 1) и (3 x 1):
Или, скажем, можно преобразовать входную матрицу (10 x 8) на выход (4 x 5):
Получившийся код:
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])
Хоть розой назови её, хоть нет
С надеждой запускаю и… подождав..., потом еще подождав..., и еще… получаю полный бред — сетка не обучается:
flowers178.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers179.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers180.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers182.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers186-2.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers186.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers187.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers190 (2).jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers190.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers191.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers195.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers199.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
flowers2.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
flowers200.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers32.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers85.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
noflowers88.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers122.JPG res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers123.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers173.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
noflowers202.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowers205.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowerscutxml.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.241
noflowersGetaway.jpg res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
noflowersIMGP1800.JPG res = [[ 0.98 0.5 0.98 0.5 ]] v = 0.24
noflowerstrq-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
flowers178.jpg res = [[ 0.86 0.47 0.88 0.47]] v = 0.171
flowers179.jpg res = [[ 0.87 0.51 0.89 0.5 ]] v = 0.194
flowers180.jpg res = [[ 0.79 0.69 0.79 0.67]] v = 0.233
flowers182.jpg res = [[ 0.87 0.53 0.88 0.48]] v = 0.189
flowers186-2.jpg res = [[ 0.89 0.41 0.89 0.39]] v = 0.144
flowers186.jpg res = [[ 0.85 0.54 0.83 0.55]] v = 0.194
flowers187.jpg res = [[ 0.86 0.54 0.86 0.54]] v = 0.199
flowers190 (2).jpg res = [[ 0.96 0.25 0.97 0.15]] v = 0.089
flowers190.jpg res = [[ 0.95 0.13 0.97 0.14]] v = 0.048
flowers191.jpg res = [[ 0.81 0.57 0.82 0.57]] v = 0.195
flowers195.jpg res = [[ 0.81 0.55 0.79 0.56]] v = 0.177
flowers199.jpg res = [[ 0.89 0.45 0.89 0.45]] v = 0.171
flowers2.jpg res = [[ 0.83 0.56 0.83 0.55]] v = 0.195
flowers200.jpg res = [[ 0.91 0.42 0.89 0.43]] v = 0.163
noflowers32.jpg res = [[ 0.7 0.79 0.69 0.8 ]] v = 0.246
noflowers85.jpg res = [[ 0.86 0.53 0.86 0.53]] v = 0.192
noflowers88.jpg res = [[ 0.86 0.56 0.87 0.53]] v = 0.207
noflowers122.JPG res = [[ 0.81 0.63 0.81 0.62]] v = 0.218
noflowers123.jpg res = [[ 0.83 0.59 0.84 0.55]] v = 0.204
noflowers173.jpg res = [[ 0.83 0.6 0.83 0.58]] v = 0.209
noflowers202.jpg res = [[ 0.78 0.7 0.8 0.65]] v = 0.234
noflowers205.jpg res = [[ 0.84 0.77 0.79 0.75]] v = 0.287
noflowerscutxml.jpg res = [[ 0.81 0.61 0.81 0.63]] v = 0.213
noflowersGetaway.jpg res = [[ 0.85 0.56 0.85 0.55]] v = 0.202
noflowersIMGP1800.JPG res = [[ 0.85 0.55 0.86 0.54]] v = 0.199
noflowerstrq-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
Дальше… А дальше выходной закончился, и мне пора было заниматься хозработами.
Что делать дальше?
Можно наметить дальнейшие шаги, как добиться нужного результата:
1. Добавить еще один промежуточный слой или несколько во вторую сеть — так у нее появится больше свободы в обучении. Все-таки сеть на матричном умножении не совсем классическая, так как в ней меньше связей-синапсов между слоями, да и сами синапсы не уникальны.
2. Использовать приближения к успешным результатам как заготовки для последующих обучений — т.е. запоминать веса синапсов самого успешного результата, а не затиратьвсе случайными значениями.
3. Попробовать генетические алгоритмы — смешивать и делить, размножать успешное и отбраковывать неудачное.
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])
Автор: sshmakov