Прелюдия

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

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

Фон

Нейронная сеть, как можно понять из названия, - это метод, позволяющий заставить компьютеры учиться на данных, созданный на основе того, как, по нашему мнению, мозг может учиться на данных. Классический вариант использования нейронной сети - научить компьютер распознавать нарисованные от руки цифры. Хотя это может показаться нам ослепляюще очевидным, с самого начала совсем не ясно, как мы можем научить компьютер распознавать какой-то паттерн как 3, а какой-то другой как 4. Для правильного объяснения математической интуиции я бы порекомендовал 3Blue1Browns отличный цикл из 4 частей по этой теме (https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3p). Как и большинство методов машинного обучения, нейронные сети используют множество обучающих данных, чтобы попытаться научиться. В классическом примере мы можем скормить компьютеру много-много нарисованных от руки цифр. В нашем случае нам нужно найти много фотографий мотоциклов и тигров и дать нашей модели, чтобы она творила чудеса.

Парсинг веб-страниц

Перво-наперво нам нужны некоторые данные. Мы будем использовать программу для автоматического извлечения и загрузки около 170 изображений велосипедов и тигров из изображений Google. Код для этого извлечения любезно предоставлен



from selenium import webdriver
import requests
import os
import io
import hashlib
from bs4 import BeautifulSoup
from PIL import Image
import time


###Image scraping with python code goes here####

search_and_download(search_term="tiger",driver_path=DRIVER_PATH,number_images=170)
#gets 170 images of tigers from google images, saves to folder called tiger, in folder called image, in our working directory

search_and_download(search_term="bike",driver_path=DRIVER_PATH,number_images=170)
#Same for bikes

Чтобы получить наши данные, мы вводим поисковый запрос и количество изображений в функцию загрузки. После его запуска у нас должна быть папка с именем tiger, содержащая 170 файлов tiger jpg, и папка с именем bike, содержащая 170 файлов bike jpgs, со случайным именем и форматированием. Обратите внимание: сколько значений мы можем очистить с помощью этого алгоритма, прежде чем он застрянет, зависит от поискового запроса. Для этих терминов мы смогли извлечь около 170. Игра с аргументом «сон между взаимодействиями» в коде ссылки может помочь увеличить это число. Google не любит, когда мы извлекаем jpeg-файлы слишком быстро, поэтому намеренное увеличение времени обработки может помочь нам получить больше данных, не создавая никаких предупреждений. Теперь, когда у нас есть необработанные данные изображения, наш следующий шаг - выполнить некоторую обработку этих изображений с помощью модуля PIL, чтобы привести их в пригодную для использования форму.

Обработка изображений PIL

Наша цель здесь - преобразовать наши изображения в пригодные для использования пакеты данных. Для каждого изображения наша модель нейронной сети требует в качестве входных данных массив функций и массив меток. Массив функций - это, по сути, массив чисел, соответствующих каждому пикселю наших данных. Для данных цвета это означает 3 числа, соответствующие значению RGB для каждого пикселя. Для оттенков серого, как мы будем использовать, требуется только 1 число, яркость. Массив меток - это всего лишь одномерный массив с каждой меткой изображения, числовой уровень, который соответствует этой категории изображений. В нашем случае изображения тигров получают уровень 1, а изображения велосипедов - уровень 0. Прежде чем мы сможем получить эти массивы, мы должны преобразовать все наши изображения в согласованную обрабатываемую форму. Начнем с того, что сделаем их все черно-белыми. Чтобы преобразовать изображение jpg в оттенки серого, мы запускаем:

import PIL
img= Image.open(jpg)
img = img.convert('L')

Затем мы преобразуем все изображения в некоторую общую форму и разрешение, поскольку модель ожидает, что от каждого изображения будет поступать один и тот же объем данных, то есть одно и то же количество пикселей. Здесь мы также можем настроить на основе большого количества данных, которые может обработать наш компьютер. Изображение размером 1000x1000 содержит 1 миллион пикселей, то есть 1 миллион фрагментов данных, которые наша модель должна преобразовать и обработать на 1 изображение. Мы настраиваем разрешение в соответствии с нашими вычислительными возможностями, игра с числами, как правило, здесь самый простой подход. Мы можем использовать функции «кадрирования», «размера» и «изменения размера» PIL, как показано ниже. Во-первых, чтобы обрезать изображение до квадрата.

size = img.size
dim = min(size)
img = img.crop((0, 0, dim, dim))

Это возвращает квадратное обрезанное изображение размера dimxdim, где dim - это минимальное значение высоты и ширины изображения. Теперь настройте разрешение

img=img.resize((130, 130))

Это возвращает наше изображение в разрешении 130x130.

Наконец, чтобы превратить наш img в массив, мы просто запускаем:

img=np.asarray(img)

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

def image_processor(jpg):
    img= Image.open(jpg)
    img = img.convert('L').    #converts to grayscale
    size = img.size
    dim = min(size)
    img = img.crop((0, 0, dim, dim))  #crops to square
    img=img.resize((130, 130))   #changes res to 130x130
    name=str(jpg)
    img.save(name)  #saves image to folder
    return(img)

Эта функция возвращает полностью обработанное изображение и сохраняет это изображение в папке.

Преобразование данных

Теперь, чтобы превратить наши изображения в наши массивы изображений и массивы меток. Следующие фрагменты кода обрабатывают каждое изображение с помощью image_processor, конвертируют эти обработанные изображения в image_arrays и label_arrays и разбивают данные на наборы для обучения и тестирования.

from os import walk
import shutil
# first we need file path to tiger photos, bike photos, and images
#the folder containing both
tiger_path='<insert _tiger file path>'
bicycle_path='<insert bike file path>'
image_path='<insert image file path>'


tiger_img=(_, _, filenames) = list(next(walk(tiger_path)))
tiger_img=tiger_img[2]

bicycle_img=(_, _, filenames) = list(next(walk(bicycle_path)))
bicycle_img=bicycle_img[2]
#gets list of all files names in tiger and bike folder respectively
dir_path = image_path + '/processed_images'

if os.path.exists(dir_path):
    shutil.rmtree(dir_path)

os.chdir(image_path)
os.makedirs("processed_images")
#first deletes "processed_images" folder if present, then adds new #folder of that name
os.chdir(tiger_path)

A=0
for jpg in tiger_img:
    A=A+1
    label=str(A)+"_image_tiger"
    print(label)
    img=image_processor(jpg)
    jpg_name = ("%s" % (label)) + ".jpg"
    file_path = os.path.join( dir_path,jpg_name)
    print(file_path)
    img.save( file_path )

#sets tiger photos folder as directory, processes all photos inside #and saves them to processed images folder under name 
#str(A)+"_image_tiger" where A is just index of photo 
os.chdir(bicycle_path)

B=0
for jpg in bicycle_img:
    B = B + 1
    label = str(B)+"_image_bicycle"
    img=image_processor(jpg)
    jpg_name = ("%s" % (label)) + ".jpg"
    file_path = os.path.join( dir_path,jpg_name)
    img.save( file_path )
#Same for bikes

os.chdir(dir_path)
train_images=[]
train_labels=[]
test_images=[]
test_labels=[]

Bt=int(B*.7)
At=int(A*.7)
# sets processed images folder as new directory, sets limiting index #for training and testing set as .7*total index for bike and tiger #respectively
for i in range(1,Bt):
    bike_img=str(i)+"_image_bicycle" +'.jpg'
    bike_img = Image.open(bike_img)
    bike_array=np.asarray(bike_img)
    train_labels.append(0)
    train_images.append(bike_array)
#converts processed training bike images to array, appends array 
#to train_images, appends 0 to train labels since bikes have label 0
for i in range(1, At):
    tiger_img = str(i)+"_image_tiger" + '.jpg'
    tiger_img = Image.open(tiger_img)
    tiger_array = np.asarray(tiger_img)
    train_labels.append(1)
    train_images.append(tiger_array)
#converts processed training tiger images to array, appends array 
#to train_images, appends 1 to train labels since tigers have label #0

for i in range(Bt,B):
    bike_img=str(i)+"_image_bicycle" +'.jpg'
    bike_img = Image.open(bike_img)
    bike_array=np.asarray(bike_img)
    test_labels.append(0)
    test_images.append(bike_array)

for i in range(At, A):
    tiger_img =  str(i)+"_image_tiger" + '.jpg'
    tiger_img = Image.open(tiger_img)
    tiger_array = np.asarray(tiger_img)
    test_labels.append(1)
    test_images.append(tiger_array)
# same for testing
train_images=np.asarray(train_images)
train_labels=np.asarray(train_labels)
test_images=np.asarray(test_images)
test_labels=np.asarray(test_labels)
#creates array of the array lists
train_images = train_images / 255.0

test_images = test_images / 255.0
#scales value down to [0,1], to produce our final data

Теперь данные полностью обработаны и готовы к использованию для обучения нейронной сети.

Настройка базовой нейронной сети

Код ниже вводит наши обработанные данные в нейронную сеть. Мы вводим соответствующую форму данных, желаемое количество классов и желаемые гиперпараметры. Здесь гиперпараметры: relu, softmax, adam, sparse_categorical_crossentropy, epoch. Эпоха - это просто количество раз, когда модель видит наши данные. Сложность моделей такова, что они могут видеть данные в разное время и узнавать разные вещи из этих данных (в отличие от регрессии, которая дает одинаковый результат при постоянной загрузке одних и тех же данных). Мы хотим настроить эпоху так, чтобы модель училась всему, что можно, на основе данных, но не настолько, чтобы ее переобучать. Мы увидим это через секунду.

from tensorflow import keras

model = keras.Sequential([
    keras.layers.Flatten(input_shape=(130, 130)),  # input layer (1)
#flatten changes shape from 130x130 tensor to 16900 vector
    keras.layers.Dense(2500, activation='relu'),  # hidden layer (2)
#Dense means all connected, 2500 is number of neurons, should be #fraction size of input layer here ~1/8
 
    keras.layers.Dense(2, activation='softmax') # output layer (3)
    #should have as many neurons as classes to predict
])


model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
#Compiles model using given hyperparameters
model.fit(train_images, train_labels, epochs=5)
#trains model on our data

Эта модель выводит:

Epoch 1/5
8/8 [==============================] — 1s 129ms/step — loss: 0.6762 — accuracy: 0.4828
Epoch 2/5
8/8 [==============================] — 1s 133ms/step — loss: 0.6070 — accuracy: 0.6681
Epoch 3/5
8/8 [==============================] — 1s 131ms/step — loss: 0.5521 — accuracy: 0.7500
Epoch 4/5
8/8 [==============================] — 1s 132ms/step — loss: 0.5142 — accuracy: 0.7543
Epoch 5/5
8/8 [==============================] — 1s 131ms/step — loss: 0.4701 — accuracy: 0.8190
4/4 [==============================] — 0s 12ms/step — loss: 0.5468 — accuracy: 0.6765
test_acc= 0.6764705777168274

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

import matplotlib.pyplot as plt
accuracy_test=[]
accuracy_train=[]

for i in range(25):
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    model.fit(train_images, train_labels, epochs=i)
    test_loss, test_acc = model.evaluate(test_images, test_labels,        .   verbose=1)
    accuracy_test.append(test_acc)
    train_loss,train_acc=model.evaluate(train_images,
    train_labels,verbose=1)
    accuracy_train.append(train_acc)
#runs model with 1 to 25 epochs, gets test and train accuracy for each and appends to 2 lists
import matplotlib.pyplot as plt
plt.plot(accuracy_test)
plt.plot( accuracy_train)
plt.title('accuracy v epoch')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['Validation','Train'], loc='upper left')
plt.show()
#plots training and testing accuracy per epoch

Выполнение кода возвращает следующий график:

Точность на тестовом наборе достигает пика около 0,85 в течение 12 периодов, затем падает до 16, после чего все становится статичным, поскольку модель полностью запомнила данные.

Можем ли мы сделать лучше? Наивный подход может заключаться только в увеличении разрешения. Более высокая детализация = больше информации = лучший прогноз, верно? К сожалению, это не так просто.

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

Источники: