Нейросеть для классификации фруктов на Python (Fruit Neural Network)

Автор: | 22.08.2019

Введение
Гистограммы изображений
Создание Pickle-файлов
Структура нейросети
Полезные ссылки

Введение

В статье Идентификация образов по цвету, текстуре и форме  рассмотрен алгоритмический подход к решению задачи распознавания фруктов.

В статье Artificial Neural Network Implementation using NumPy and Classification of the Fruits360 Image Dataset автора Ahmed Gad рассматривается альтернативный подход к решению подобной задачи  — с помощью нейросети (Artificial Neural Network — ANN).

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

Почему не используется модель цветового пространства RGB? Во-первых она не изолирует информацию о цвете от освещения. Во-вторых, если для представления изображений используется RGB, в расчетах будут задействованы 3 канала. Поэтому лучше использовать цветовое пространство HSV, которое в определенной степени изолирует информацию о цвете в один канал — это канал оттенка H. Подробнее см. Уменьшение влияния освещенности на признаки цвета.

Исходные коды на языке Python, и данные для обработки (изображения фруктов) предоставлены автором статьи здесь: https://github.com/ahmedfgad/NumPyANN.

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

Гистограммы изображений

Код, который вычисляет гистограммы канала оттенка изображений — the hue channel (H) в модели цвета HSV:

import numpy
import skimage.io, skimage.color
import matplotlib.pyplot
 
raspberry = skimage.io.imread(fname="raspberry.jpg", as_grey=False)
apple = skimage.io.imread(fname="apple.jpg", as_grey=False)
mango = skimage.io.imread(fname="mango.jpg", as_grey=False)
lemon = skimage.io.imread(fname="lemon.jpg", as_grey=False)
 
apple_hsv = skimage.color.rgb2hsv(rgb=apple)
mango_hsv = skimage.color.rgb2hsv(rgb=mango)
raspberry_hsv = skimage.color.rgb2hsv(rgb=raspberry)
lemon_hsv = skimage.color.rgb2hsv(rgb=lemon)
 
fruits = ["apple", "raspberry", "mango", "lemon"]
hsv_fruits_data = [apple_hsv, raspberry_hsv, mango_hsv, lemon_hsv]
idx = 0
for hsv_fruit_data in hsv_fruits_data:
    fruit = fruits[idx]
    hist = numpy.histogram(a=hsv_fruit_data[:, :, 0], bins=360)
    matplotlib.pyplot.bar(numpy.arange(360), hist[0])
    matplotlib.pyplot.savefig(fruit+"-hue-histogram.jpg", bbox_inches="tight")
    matplotlib.pyplot.close("all")
    idx = idx + 1

Модуль skimage инсталлировать не удается: Failed to install ‘skimage’. Рекомендуется вместо skimage подключить scikit-image. Инсталляция модуля scikit-image прошла успешно. Но при запуске приложения появляется сообщение: No module named ‘scipy’. Провел инсталляцию модуля scipy.  После этого приложение запустилось.

Файлы изображений фруктов (raspberry.jpg, apple.jpg, mango.jpg, lemon.jpg) должны быть помещены перед запуском приложения в ту же папку, где и исходный файл (с расширением  .py):

 

 

 

После запуска программы в папке, где и исходный файл, появятся файлы гистограмм  (raspberry-hue-histogram.jpg, apple-hue-histogram.jpg, mango-hue-histogram.jpg, lemon-hue-histogram.jpg).

Создание Pickle-файлов

Модуль pickle предоставляет функции и классы для сериализации и десериализации объектов. В программировании под сериализацией понимают преобразование каких-либо данных в набор байтов, который потом обычно сохраняют в файл или передают по сети. Десериализация — это восстановление объектов из их байтовых представлений.

Код для создания Pickle-файлов — dataset_features.pkl и outputs.pkl:

import numpy
import skimage.io, skimage.color, skimage.feature
import os
import pickle

fruits = ["apple", "raspberry", "mango", "lemon"]
#492+490+490+490=1,962
dataset_features = numpy.zeros(shape=(1962, 360))
outputs = numpy.zeros(shape=(1962))

idx = 0
class_label = 0
for fruit_dir in fruits:
    curr_dir = os.path.join(os.path.sep, fruit_dir)
    all_imgs = os.listdir(os.getcwd()+curr_dir)
    for img_file in all_imgs:
        if img_file.endswith(".jpg"): # Ensures reading only JPG files.
            fruit_data = skimage.io.imread(fname=os.getcwd()+curr_dir+'\\'+img_file, as_grey=False)
            fruit_data_hsv = skimage.color.rgb2hsv(rgb=fruit_data)
            hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360)
            dataset_features[idx, :] = hist[0]
            outputs[idx] = class_label
            idx = idx + 1
    class_label = class_label + 1

with open("dataset_features.pkl", "wb") as f:
    pickle.dump(dataset_features, f)

with open("outputs.pkl", "wb") as f:
    pickle.dump(outputs, f)

Перед запуском программы папки с файлами изображений фруктов (raspberry, apple, mango, lemon) должны быть помещены в ту же папку, где и исходный файл приложения.

После запуска программы в папке, где и исходный файл, появятся файлы outputs.pkl и dataset_features.pkl.

В настоящее время каждое изображение представлено с использованием векторного элемента из 360 элементов. Такие элементы фильтруются для того, чтобы просто сохранить наиболее релевантные элементы для дифференциации 4 классов. Уменьшенная длина вектора признаков равна 102, а не 360. Использование меньшего количества элементов помогает выполнять обучение быстрее, чем раньше. Форма переменной dataset_features будет 1962×102.

Структура нейросети

Следующий рисунок отображает структуру ANN — входной слой с 102 входами, 2 скрытых слоя с 150 и 60 нейронами и выходной слой с 4 выходами (по одному для каждого класса фруктов).

Входной вектор на любом слое умножается (умножение матрицы) на матрицу весов, соединяющую его со следующим слоем для получения выходного вектора. Такой выходной вектор снова умножается на матрицу весов, соединяющую его слой со следующим слоем. Процесс продолжается до достижения выходного слоя. Краткое описание матрицы умножения приведено на следующем рисунке.

Входной вектор размером 1х102 должен быть умножен на матрицу весов первого скрытого слоя размером 102х150. Помните, что это умножение матриц. Таким образом, форма выходного массива равна 1×150. Такой вывод затем используется как вход для второго скрытого слоя, где он умножается на матрицу весов размером 150×60. Размер результата 1×60. Наконец, такой вывод умножается на весовые коэффициенты между вторым скрытым слоем и выходным слоем размером 60×4. Результат, наконец, имеет размер 1×4. Каждый элемент в таком результирующем векторе относится к выходному классу. Входной образец помечен в соответствии с классом с наибольшим количеством баллов.

Код для реализации таких умножений приведен ниже.

import numpy
import pickle

def sigmoid(inpt):
    return 1.0 / (1 + numpy.exp(-1 * inpt))

f = open("dataset_features.pkl", "rb")
data_inputs2 = pickle.load(f)
f.close()

features_STDs = numpy.std(a=data_inputs2, axis=0)
data_inputs = data_inputs2[:, features_STDs > 50]

f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()

HL1_neurons = 150
input_HL1_weights = numpy.random.uniform(low=-0.1, high=0.1, size=(data_inputs.shape[1], HL1_neurons))
HL2_neurons = 60
HL1_HL2_weights = numpy.random.uniform(low=-0.1, high=0.1, size=(HL1_neurons, HL2_neurons))
output_neurons = 4
HL2_output_weights = numpy.random.uniform(low=-0.1, high=0.1, size=(HL2_neurons, output_neurons))
H1_outputs = numpy.matmul(data_inputs[0, :], input_HL1_weights)
H1_outputs = sigmoid(H1_outputs)
H2_outputs = numpy.matmul(H1_outputs, HL1_HL2_weights)
H2_outputs = sigmoid(H2_outputs)
out_otuputs = numpy.matmul(H2_outputs, HL2_output_weights)

predicted_label = numpy.where(out_otuputs == numpy.max(out_otuputs))[0][0]
print("Predicted class : ", predicted_label)

Запускаем программу, получаем результат:

После считывания ранее сохраненных объектов c их выходными метками и фильтрации объектов определяются матрицы весов слоев. Им случайным образом дают значения от -0,1 до 0,1. Например, переменная «input_HL1_weights» содержит матрицу весов между входным слоем и первым скрытым слоем. Размер такой матрицы определяется в соответствии с количеством элементов признака и количеством нейронов в скрытом слое.

После создания матриц весов следует применить умножение матриц. Например, переменная «H1_outputs» содержит выходные данные умножения вектора признаков данного образца на матрицу весов между входным слоем и первым скрытым слоем.

Обычно функция активации применяется к выходам каждого скрытого слоя для создания нелинейных отношений между входами и выходами. Например, выходные данные умножения матриц применяются к функции активации сигмоида.

После генерации выходных выходных слоев происходит прогнозирование. Метка прогнозируемого класса сохраняется в переменной «предикат». Такие шаги повторяются для каждой входной выборки.

Полный код, который работает во всех примерах, приведен ниже.

import numpy
import pickle

def sigmoid(inpt):
    return 1.0 / (1 + numpy.exp(-1 * inpt))

def relu(inpt):
    result = inpt
    result[inpt < 0] = 0
    return result

def update_weights(weights, learning_rate):
    new_weights = weights - learning_rate * weights
    return new_weights

def train_network(num_iterations, weights, data_inputs, data_outputs, learning_rate, activation="relu"):
    for iteration in range(num_iterations):
        print("Itreation ", iteration)
        for sample_idx in range(data_inputs.shape[0]):
            r1 = data_inputs[sample_idx, :]
            for idx in range(len(weights) - 1):
                curr_weights = weights[idx]
                r1 = numpy.matmul(r1, curr_weights)
                if activation == "relu":
                    r1 = relu(r1)
                elif activation == "sigmoid":
                    r1 = sigmoid(r1)
            curr_weights = weights[-1]
            r1 = numpy.matmul(r1, curr_weights)
            predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
            desired_label = data_outputs[sample_idx]
            if predicted_label != desired_label:
                weights = update_weights(weights, learning_rate=0.001)
    return weights

def predict_outputs(weights, data_inputs, activation="relu"):
    predictions = numpy.zeros(shape=(data_inputs.shape[0]))
    for sample_idx in range(data_inputs.shape[0]):
        r1 = data_inputs[sample_idx, :]
        for curr_weights in weights:
            r1 = numpy.matmul(r1, curr_weights)
            if activation == "relu":
                r1 = relu(r1)
            elif activation == "sigmoid":
                r1 = sigmoid(r1)
        predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
        predictions[sample_idx] = predicted_label
    return predictions

f = open("dataset_features.pkl", "rb")
data_inputs2 = pickle.load(f)
f.close()

features_STDs = numpy.std(a=data_inputs2, axis=0)
data_inputs = data_inputs2[:, features_STDs > 50]

f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()

HL1_neurons = 150
input_HL1_weights = numpy.random.uniform(low=-0.1, high=0.1,
                                         size=(data_inputs.shape[1], HL1_neurons))
HL2_neurons = 60
HL1_HL2_weights = numpy.random.uniform(low=-0.1, high=0.1,
                                       size=(HL1_neurons, HL2_neurons))
output_neurons = 4
HL2_output_weights = numpy.random.uniform(low=-0.1, high=0.1,
                                          size=(HL2_neurons, output_neurons))

weights = numpy.array([input_HL1_weights, 
                       HL1_HL2_weights,
                       HL2_output_weights])

weights = train_network(num_iterations=10,
                        weights=weights,
                        data_inputs=data_inputs,
                        data_outputs=data_outputs,
                        learning_rate=0.01,
                        activation="relu")

predictions = predict_outputs(weights, data_inputs)
num_flase = numpy.where(predictions != data_outputs)[0]
print("num_flase ", num_flase.size)

Запускаем программу, получаем результат:

Переменная «weights» содержит весовые коэффициенты всей сети. На основе размера каждой весовой матрицы структура сети задается динамически. Например, если размер переменной «input_HL1_weights» равен 102×80, то мы можем сделать вывод, что первый скрытый слой имеет 80 нейронов.

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

Если сеть сделала ложный прогноз для данной выборки, то веса обновляются с помощью функции «update_weights». Алгоритм оптимизации не используется для обновления весов. Веса просто обновляются в зависимости от скорости обучения. Точность не превышает 45%. Для достижения большей точности используется алгоритм оптимизации для обновления весов. Например, вы можете найти технику градиентного спуска в реализации ANN библиотеки scikit-learn.

Полезные ссылки:

 

Автор: Николай Свирневский

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *