\documentclass[a4paper,fontsize=14bp]{article}

\input{../common-preamble}
\input{../bmstu-preamble}
\input{../fancy-listings-preamble}
\numerationTop

\begin{document}
\thispagestyle{empty}
\makeBMSTUHeader

% ... работе, номер, тема, предмет, ?а, кто
\makeReportTitle{лабораторной}{4}{Разработка программы для классификации изображений с использованием свёрточной нейронной сети}{Цифровая обработка изображений}{}{Большаков В.Э.}
\newpage
\thispagestyle{empty}
\tableofcontents
\newpage
\pagestyle{fancy}
\sloppy
\section{Цель}
Основной целью лабораторной работы является изучение методов распознания объекта с использованием свёрточной нейронной сети (CNN) и определение лучшей архитектуры сети для такого распознавания.
\section{Задание}
\begin{itemize}
\item Подключить библиотеку scikit-learn, tensorflow или pytorch;
\item Дополнить наборы картинок по варианту. Минимум 20 картинок для каждого класса (при неудовлетворительной классификации добавить ещё). Картинки намеренно должны быть схожими;
\item Выбрать реализацию и обучить свёрточную нейронную сеть (CNN) для классификации в соответствии с вариантом;
\item Провести эксперимент по распознаванию изображений с визуализацией  результатов;
\item Провести информационный поиск в Интернете. Попробовать несколько вариантов архитектур CNN. Выбрать лучшую архитектуру как минимум из двух. Построить график ошибок первого и второго рода по результатам распознавания;
\item Прислать программу и подготовленные изображения преподавателю;
\item Подготовить и прислать отчет.
\end{itemize}

Вариант (по номеру в списке группы) 9, то есть «плантан и утёнок».
\section{Теоретическая часть}
\subsection{Свёрточная нейронная сеть}
Сверточная нейронная сеть основана на универсальных математических операциях с матрицами\cite[c. 665]{dsa:cormen}. В общем случае, это последовательное применение операции сложения и/или умножения подматрицы основной матрицы для получения значения свёртки (рис. \hrf{pic:convolution-idea}). Свёртка - это линейное преобразование.

\begin{figure}[H]
  \centering
  \def\svgwidth{100mm}
  \input{pics/02-dip-04-lab-cnn-idea.pdf_tex}
  \caption{Схема преобразвоания матрицы 3х5 в свёртку 1х3}
  \label{pic:convolution-idea}
\end{figure}

Двумерная свертка — это операция для которой формируется ядро, представляющее из себя матрицу весов (weight matrix). Ядро проходит над двумерным изображением, поэлементно выполняя операцию умножения с той частью входных данных, над которой оно сейчас находится, и суммирует все полученные значения в одно, выходное.

\begin{figure}[H]
  \centering
  \def\svgwidth{130mm}
  \input{pics/02-dip-04-lab-cnn-weight.pdf_tex}
  \caption{Добавление к свёртке весов}
  \label{pic:convolution-weight}
\end{figure}

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

Например, имея $5*5=25$ признаков на входе и $3*3=9$ признаков на выходе для стандартного слоя (standard fully connected layer) мы бы имели весовую матрицу $25*9=225$ параметров, а каждый выходной признак являлся бы взвешенной суммой всех признаков на входе. Свертка позволяет произвести такую операцию с всего 9-ю параметрами, ведь каждый признак на выходе получается анализом не каждого признака на входе, а только одного входного, находящегося в «примерно том же месте».

Часто используемые техники: Padding и Striding.
\paragraph{Padding.} При проходе весовой матрицы по краю изображения, самые крайние пиксели, фактически, обрезаются, преобразуя матрицу признаков размером $5*5$ в матрицу $3*3$. Крайние пиксели никогда не оказываются в центре ядра, потому что тогда ядру не над чем будет проходить за краем. Padding добавляет к краям поддельные пиксели (обычно нулевые).

\paragraph{Striding.} Часто бывает, что при работе со сверточным слоем, нужно получить выходные данные меньшего размера, чем входные. Идея stride заключается в том, чтобы пропустить некоторые области, над которыми проходит ядро. Шаг 1 означает, что берутся области «через пиксель», то есть, по факту, стандартная свертка. Шаг 2 означает, что берутся области через каждые два пикселя, пропуская все другие области в процессе и уменьшая их количество примерно в два раза, шаг 3 означает пропуск трёх пикселей, сокращая количество в 3 раза и т.д.

\subsection{Архитектура нейронной сети и обучение}
\label{subsect:arch}
В работе не применяются поддельные пиксели ни в одной из архитектур. Пропускание шагов свёртки также не используется (области анализа следуют без разрывов). Для увеличения количества обучающих примеров была применена \textit{аугментация данных} (добавление шумов, деформация изображения, приближение в случайную точку изображения и произвольный поворот изображений). Пример аугментации приведён на рисунке \hrf{pic:augment}, а код, который деформирует изображения в листинге \hrf{lst:augment}.

\begin{figure}[H]
  \centering
  \begin{subfigure}[b]{0.45\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-04-lab-pic-src.png}
    \caption{Исходное изображение}
  \end{subfigure}
  \hfill
  \begin{subfigure}[b]{0.45\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-04-lab-pic-aug.png}
    \caption{Изменённое изображение}
  \end{subfigure}
  \caption{Аугментация изображений}
  \label{pic:augment}
\end{figure}

\begin{lstlisting}[language=Python,style=PyCodeStyle,label={lst:augment}]
data_augmentation = keras.Sequential(
        [
            layers.RandomFlip("horizontal_and_vertical",
                              input_shape=(img_height,
                                           img_width,
                                           3)),
            layers.RandomRotation(0.3),
            layers.RandomZoom(-0.3),
            layers.Rescaling(0.3),
        ]
    )
\end{lstlisting}

В обучении всех архитектур, кроме одной, использовались пакеты по 8 изображений, а в единственном случае, 32. Пример изображений внутри пакета из 32 изображений приведён на рисунке \hrf{pic:pack-sample}. 

\begin{figure}[H]
  \centering
  \includegraphics[width=11cm]{02-dip-04-lab-pack-sample.png}
  \caption{Пример пакета для обучения нейросети}
  \label{pic:pack-sample}
\end{figure}

В работе были применены разные архитектуры, обучавшиеся каждая по 1000 эпох. Каждая архитектура включает в себя специальные свёрточные слои и один примитивный слой (обычный персептрон). Структуры нейронных сетей приведены в таблице \hrf{table:arch-diff}. Построенные автоматизированно диаграммы нейронных сетей в приложении \hrf{appendix:architectures}.

\begin{table}[h!]
  \centering
  \begin{tabular}{|p{15mm}|p{30mm}|p{40mm}|p{30mm}|p{15mm}|p{15mm}|}
    \hline
    Номер архитектуры & Метод обучения & Количество нейронов свёрточного слоя & Количество нейронов примитивного слоя & Размер пакета & Эпох \\ [0.5ex]
    \hline
    1(\hrf{pic:arch-1}) & ADAM & 16/32/64           & 256 & 8  & 1000  \\
    2(\hrf{pic:arch-1}) & SGD  & 16/32/64           & 256 & 8  & 1000  \\
    3(\hrf{pic:arch-1}) & ADAM & 16/32/64           & 256 & 32 & 1000  \\
    4(\hrf{pic:arch-1}) & ADAM & 16/32/64           & 256 & 8  & 10000 \\
    5(\hrf{pic:arch-5}) & ADAM & 16/32/64/128/64/32 & 256 & 8  & 1000  \\
    6(\hrf{pic:arch-6}) & ADAM & 32/64/128          & 512 & 8  & 1000  \\
    \hline
  \end{tabular}
  \caption{Свойства архитектур нейронных сетей, применённых в работе}
  \label{table:arch-diff}
\end{table}

\textbf{SGD (Stochastic Gradient Descent)} - тренировка нейронной сети заключается в подборе параметров (весов модели) таким образом, чтобы минимизировать ошибку нейронной сети на тренировочном наборе. Если значение ошибки нейросети не минимальное, но и незначительно изменяется между эпохами обучения, то алгоритм метода принимает решение об изменении поправок к весам нейронов.

\textbf{ADAM (ADAptive Moment estimation)} - это алгоритм оптимизации, который можно использовать вместо классической процедуры SGD для итеративного обновления весов сети на основе обучающих данных. SGD поддерживает единую скорость обучения (называемую альфа) для всех обновлений веса, и скорость обучения не изменяется во время тренировки. В то время как в методе Adam скорость обучения поддерживается для каждого веса сети (параметра) и отдельно адаптируется по мере развития обучения. Метод вычисляет индивидуальные адаптивные скорости обучения для различных параметров из оценок первого и второго моментов градиентов.

Каждая архитектура содержит специальный слой, препятствующий переобучению сети. Такой слой в терминах TensorFlow называется DropOut. Это виртуальный слой, не строгое понятие, некоторая абстракция, между двумя смежными слоями, этому слою передается число от 0 до 1 - вероятность срабатывания. Дропаут представляет собой "выключение" нейрона из смежного слоя (рис. \hrf{pic:dropout}).

\begin{figure}[H]
  \centering
  \def\svgwidth{100mm}
  \input{pics/02-dip-04-lab-dropout.pdf_tex}
  \caption{Принцип работы dropout}
  \label{pic:dropout}
\end{figure}

\section{Практическая часть}
Приложение состоит из набора пресетов свёрточных нейронных сетей, функций создания обучающей и валидационной выборки, терминального интерфейса пользователя и функции тестирования нейросети на наборе изображений из исходного задания.

\subsection{Пользовательский интерфейс}
Пользовательский интерфейс предполагает выбор количества обучающих эпох и выбор одной из шести архитектур нейронной сети. 
\begin{lstlisting}[language=Python,style=PyCodeStyle,caption={Запрос аргументов и настройка нейросети},label={src:learn}]
def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('-e', '--epoch')
    parser.add_argument('-b', '--batch')
    parser.add_argument('-a', '--arch')
    parser.add_argument('-m', '--method')
    return parser

namespace = create_parser().parse_args(sys.argv[1:])

if __name__ == '__main__':
    #Создание и обучение сети
    mainFunction(int(namespace.epoch), int(namespace.batch), int(namespace.arch), namespace.method)

\end{lstlisting}

\subsection{Архитектуры нейросетей}
В листинге \hrf{src:archpick} приведён пример создания архитектуры свёрточной нейросети и функция выбора архитектуры для дальнейшего применения в приложении. Полностью, функции создания всех архитектур приведены в приложении \hrf{appendix:main}.

\begin{lstlisting}[language=Python,style=PyCodeStyle,caption={Создание архитектуры по умолчанию},label={src:archpick}]
def DefaultArchitecture():
    return Sequential([
        data_augmentation,
        layers.Rescaling(1. / 255, input_shape=(img_height, img_width, 3)),
        layers.Conv2D(32, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(64, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(128, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Flatten(),
        layers.Dropout(0.2),
        layers.Dense(256, activation='relu'),
        layers.Dense(num_classes)
    ])

architectureType = {
    1 : DefaultArchitecture,
    2 : DefaultArchitecture,
    3 : DefaultArchitecture,
    4 : DefaultArchitecture,
    5 : FiveArchitecture,
    6 : SixArchitecture
}

def GetModelArchitecture(type):
    num_classes = len(class_names)
    return architectureType[type]
\end{lstlisting}

\subsection{Обучение}
Для обучения была сформирована выборка из похожих изображений. Формирование более широкой выборки для обучения нейросетей подробно описано в подразделе «\hRf{subsect:arch}». Код, осуществляющий формирование обучающей и валидационной выборки представлен в листинге \hrf{src:learn}.

\begin{lstlisting}[language=Python,style=PyCodeStyle,caption={Функция создания выборок и функция обучения},label={src:learn}]
def CreateTrainAndValidDataSet(batch_size):
    train_ds = tf.keras.utils.image_dataset_from_directory(
        data_dir,
        validation_split=0.2,
        subset="training",
        seed=123,
        image_size=(img_height, img_width),
        batch_size=batch_size)
    
    val_ds = tf.keras.utils.image_dataset_from_directory(
      data_dir,
      validation_split=0.2,
      subset="validation",
      seed=123,
      image_size=(img_height, img_width),
      batch_size=batch_size)
    return [train_ds, val_ds]

def TrainNetwork(model, train_ds, val_ds, epochs):
    return model.fit(
        train_ds,
        validation_data=val_ds,
        epochs=epochs
    )
\end{lstlisting}

\subsection{Выборка для тестирования}
Для тестирования была использована выборка из задания, показанная на рисунке \hrf{pic:testing-set}. Тестирование нейросети и демонстрация результатов производилось скриптом, представленным в листинге \hrf{src:testing}. Скрипт разбивает исходное изображение на 16 изображений объектов и последовательно классифицирует каждое с последующим выводом результатов на экран. Результаты работы скрипта для каждой из архитектур представлены в разделе \hrf{sec:results}.

\begin{figure}[H]
  \centering
  \includegraphics[width=16cm]{01-dip-lab04-testing.jpg}
  \caption{Исходная выборка из задания}
  \label{pic:testing-set}
\end{figure}

\begin{lstlisting}[language=Python,style=PyCodeStyle,caption={Разбиение тестовой выборки и классификация},label={src:testing}]
valid_image = PIL.Image.open('plantain_vs_duckling.jpg')
matrix_valid_image = np.array(valid_image)
plt.figure(figsize=(10, 10))
dx = int(matrix_valid_image.shape[0] / 4)
dy = int(matrix_valid_image.shape[1] / 4)
count = 1
for j in range(0, matrix_valid_image.shape[1] - dy, dy):
    for i in range(0, matrix_valid_image.shape[0] - dx, dx):
        ax = plt.subplot(4, 4, count)
        plt.imshow(matrix_valid_image[j:j + dy,i:i + dx].astype("uint8"))
        plt.axis("off")
        count += 1

plt.figure(figsize=(10, 10))
dx = int(matrix_valid_image.shape[0] / 4)
dy = int(matrix_valid_image.shape[1] / 4)
count = 1
for j in range(0, matrix_valid_image.shape[1] - dy, dy):
    for i in range(0, matrix_valid_image.shape[0] - dx, dx):
        ax = plt.subplot(4, 4, count)
        plt.imshow(matrix_valid_image[j:j + dy, i:i + dx].astype("uint8"))
        img = (PIL.Image.fromarray(np.uint8(matrix_valid_image[j:j + dy, i:i + dx]))).resize((img_height, img_width))
        img_array = tf.keras.utils.img_to_array(img)
        img_array = tf.expand_dims(img_array, 0)
        predictions = model.predict(img_array)
        score = tf.nn.softmax(predictions[0])
        plt.title(class_names[np.argmax(score)] + "\nТочность = " + str(int(100 * np.max(score))) + " %")
        plt.axis("off")
        count += 1
plt.subplots_adjust(wspace=0.1, hspace=1)
plt.tight_layout()
plt.show()
\end{lstlisting}

\section{Результат работы}
\label{sec:results}
Нейросети определили на изображениях объекты с некоторой вероятностью. Пример результата работы нейросети для первой архитектуры  представлен в таблице \hrf{table:results}

\begin{table}[H]
  \centering
  \begin{tabular}{|c|c|c|c|}
    \hline
    Номер & Изображено & Попадание & Точность \\ [0.5ex] 
    \hline
    1     & Плантан    & да  & 100 \\ 
    2     & Утёнок     & да  & 99  \\
    3     & Плантан    & да  & 100 \\
    4     & Утёнок     & да  & 100 \\
    5     & Плантан    & да  & 100 \\ 
    6     & Плантан    & нет & 88  \\
    7     & Утёнок     & да  & 100 \\
    8     & Плантан    & да  & 100 \\
    9     & Утёнок     & да  & 100 \\ 
    10    & Утёнок     & да  & 99  \\
    11    & Плантан    & да  & 99  \\ 
    12    & Плантан    & нет & 99  \\
    13    & Плантан    & да  & 100 \\
    14    & Утёнок     & да  & 100 \\
    15    & Плантан    & нет & 100 \\ 
    16    & Утёнок     & да  & 99  \\
    \hline
  \end{tabular}
  \caption{Результат работы нейросети №1}
  \label{table:results}
\end{table}

В результате классификации (после обучения и тестирования) были получены данные представленные на рисунке \hrf{pic:net-test}. Графики процессов обучения (наличие ошибок и точность классификации представлены в приложении \hrf{appendix:graphics})

\begin{figure}[H]
  \centering
  \includegraphics[width=12cm]{02-dip-04-lab-1network-valid.png}
  \caption{Результаты классификации изображений нейронной сетью}
  \label{pic:net-test}
\end{figure}

\subsection{Ошибки}
В результате работы нейросетями были допущены ошибки первого и второго рода (ложноположительные и ложноотрицательные заключения). Ошибка первого рода ($\alpha$-ошибка) - это ситуация, когда отвергнута верная гипотеза, а ошибка второго рода ($\beta$-ошибка) - это ситуация, когда принята неверная гипотеза. Можно предположить, что неуверенность (ниже 98\%) сети в верном результате классификации - это ошибка первого рода, а некорректная классификация с любым уровнем точности - ошибка второго рода. Такие допущения введены из-за малого размера обучающих наборов. Сводная таблица \hrf{table:mistakes} содержит результаты классификации изображений, а результаты работы каждой сети представлены в приложении \hrf{appendix:result-pics}.

\begin{table}[H]
  \centering
  \begin{tabular}{|c|c|c|} 
    \hline
    Архитектура & $\alpha$-ошибки & $\beta$-ошибка \\
    \hline
    1 & 0/16 & 3/16 \\ 
    2 & 2/16 & 1/16 \\
    3 & 8/16 & 2/16 \\
    4 & 1/16 & 1/16 \\
    5 & 4/16 & 2/16 \\
    6 & 1/16 & 2/16 \\
    \hline
  \end{tabular}
  \caption{Сводная таблица ошибок}
  \label{table:mistakes}
\end{table}
\section{Заключение}
В ходе выполнения задания были изучены принципы работы CNN и их основные архитектуры. Исходя из результатов тестирования можно сделать вывод, что использование небольших датасетов для обучения является основной проблемой для распознавания изображений, то есть при достаточном расширении обучающей выборки или увеличении количества эпох нейросеть можно обучить для достаточно точного определения объекта на изображении.

\newpage
\printbibliography[heading=bibintoc, title={Список литературы}, resetnumbers=1]

\newpage
\appendix
\section*{Приложения}
\addcontentsline{toc}{section}{Приложения}
\renewcommand{\thesubsection}{\Asbuk{subsection}}

\subsection{Полный текст программы}
\label{appendix:main}
\begin{lstlisting}[language=Python,style=PyCodeStyle]
import argparse
import codecs
import pathlib
import sys

import PIL
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

img_height = 180
img_width = 180
data_dir = pathlib.Path("dataset")

data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal_and_vertical",
                          input_shape=(img_height,
                                       img_width,
                                       3)),
        layers.RandomRotation(0.1),
        layers.RandomZoom(-0.1),
        layers.Rescaling(0.1),
    ]
)
def TrainNetwork(model, train_ds, val_ds, epochs):
    return model.fit(
        train_ds,
        validation_data=val_ds,
        epochs=epochs
    )

def CreateTrainAndValidDataSet(batch_size):
    train_ds = tf.keras.utils.image_dataset_from_directory(
        data_dir,
        validation_split=0.2,
        subset="training",
        seed=123,
        image_size=(img_height, img_width),
        batch_size=batch_size)

    val_ds = tf.keras.utils.image_dataset_from_directory(
      data_dir,
      validation_split=0.2,
      subset="validation",
      seed=123,
      image_size=(img_height, img_width),
      batch_size=batch_size)
    global class_names
    class_names = train_ds.class_names
    return [train_ds, val_ds]

def CompileMoment(model, optimaizer):
    model.compile(optimizer=optimaizer,
                  loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
                  metrics=['accuracy'])

def GetModelArchitecture(type):
    global num_classes
    num_classes = len(class_names)
    return architectureType[type]()

def DefaultArchitecture():

    return Sequential([
        data_augmentation,
        layers.Rescaling(1. / 255, input_shape=(img_height, img_width, 3)),
        layers.Conv2D(32, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(64, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(128, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Flatten(),
        layers.Dropout(0.2),
        layers.Dense(256, activation='relu'),
        layers.Dense(num_classes)
    ])

def FiveArchitecture():
    return Sequential([
        data_augmentation,
        layers.Rescaling(1. / 255, input_shape=(img_height, img_width, 3)),
        layers.Conv2D(64, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(128, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(256, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(128, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(64, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Flatten(),
        layers.Dropout(0.2),
        layers.Dense(256, activation='relu'),
        layers.Dense(num_classes)
    ])

def SixArchitecture():
    return Sequential([
        data_augmentation,
        layers.Rescaling(1. / 255, input_shape=(img_height, img_width, 3)),
        layers.Conv2D(128, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(256, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Conv2D(512, 3, padding='same', activation='relu'),
        layers.MaxPooling2D(),
        layers.Flatten(),
        layers.Dropout(0.2),
        layers.Dense(512, activation='relu'),
        layers.Dense(num_classes)
    ])

architectureType = {
    1 : DefaultArchitecture,
    2 : DefaultArchitecture,
    3 : DefaultArchitecture,
    4 : DefaultArchitecture,
    5 : FiveArchitecture,
    6 : SixArchitecture
}

def create_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('-e', '--epoch')
    parser.add_argument('-b', '--batch')
    parser.add_argument('-a', '--arch')
    parser.add_argument('-m', '--method')
    return parser

namespace = create_parser().parse_args(sys.argv[1:])

def mainFunction(epoch, batch_size, modelType, optimaizeMethod):
    image_count = len(list(data_dir.glob('*/*.jpg')))
    print("Всего изображений = " + str(image_count))

    #Передаем размер пакета
    train_ds, val_ds = CreateTrainAndValidDataSet(batch_size)

    AUTOTUNE = tf.data.AUTOTUNE

    train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
    val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

    #Модель из предкомпелированных
    model = GetModelArchitecture(modelType)

    CompileMoment(model, optimaizeMethod)

    history = TrainNetwork(model, train_ds, val_ds, epoch)

    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']

    loss = history.history['loss']
    val_loss = history.history['val_loss']

    epochs_range = range(epoch)

    plt.figure(figsize=(8, 8))
    plt.subplot(1, 2, 1)
    plt.plot(epochs_range, acc, label='Точность на тренировочной выборке')
    plt.plot(epochs_range, val_acc, label='Точность на контрольной выборке')
    plt.legend(loc='lower right')
    plt.title('Точности на тренировочной и контрольной выборке')

    plt.subplot(1, 2, 2)
    plt.plot(epochs_range, loss, label='Ошибка на тренировочной выборке')
    plt.plot(epochs_range, val_loss, label='Ошибка на контрольной выборке')
    plt.legend(loc='upper right')
    plt.title('Ошибки на обучающей и контрольной выборках')
    plt.show()

    valid_image = PIL.Image.open('plantain_vs_duckling.jpg')

    matrix_valid_image = np.array(valid_image)
    plt.figure(figsize=(10, 10))
    dx = int(matrix_valid_image.shape[0] / 4)
    dy = int(matrix_valid_image.shape[1] / 4)
    count = 1
    for j in range(0, matrix_valid_image.shape[1] - dy, dy):
        for i in range(0, matrix_valid_image.shape[0] - dx, dx):
            ax = plt.subplot(4, 4, count)
            plt.imshow(matrix_valid_image[j:j + dy, i:i + dx].astype("uint8"))
            img = (PIL.Image.fromarray(np.uint8(matrix_valid_image[j:j + dy, i:i + dx]))).resize((img_height, img_width))
            img_array = tf.keras.utils.img_to_array(img)
            img_array = tf.expand_dims(img_array, 0)
            predictions = model.predict(img_array)
            score = tf.nn.softmax(predictions[0])
            plt.title(class_names[np.argmax(score)] + "\nТочность = " + str(int(100 * np.max(score))) + " %")
            plt.axis("off")
            count += 1
    plt.subplots_adjust(wspace=0.1, hspace=1)
    plt.tight_layout()
    plt.show()


if __name__ == '__main__':
    #Создание и обучение сети
    mainFunction(int(namespace.epoch), int(namespace.batch), int(namespace.arch), namespace.method)
\end{lstlisting}

\subsection{Архитектуры применённых нейросетей}
\label{appendix:architectures}

\begin{figure}[H]
  \centering
  \begin{subfigure}[b]{0.3\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-04-lab-1network-arch.png}
    \caption{Нейросеть для 1, 2, 3 и 4 случая}
    \label{pic:arch-1}
  \end{subfigure}
  \hfill
  \begin{subfigure}[b]{0.3\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-04-lab-5network-arch.png}
    \caption{Нейросеть 5}
    \label{pic:arch-5}
  \end{subfigure}
  \hfill
  \begin{subfigure}[b]{0.3\textwidth}
    \centering
    \includegraphics[width=\textwidth]{02-dip-04-lab-6network-arch.png}
    \caption{Нейросеть 6}
    \label{pic:arch-6}
  \end{subfigure}
  \caption{Стандартная визуализация архитектур пакета TensorFlow}
  \label{pic:architectures}
\end{figure}

\subsection{Графики обучения нейросетей}
\label{appendix:graphics}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-1network.png}
  \caption{Графики обучения сети №1}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-2network.png}
  \caption{Графики обучения сети №2}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-3network.png}
  \caption{Графики обучения сети №3}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-4network.png}
  \caption{Графики обучения сети №4}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-5network.png}
  \caption{Графики обучения сети №5}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-6network.png}
  \caption{Графики обучения сети №6}
\end{figure}

\subsection{Результаты классификации изображений}
\label{appendix:result-pics}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-1network-valid.png}
  \caption{Классификация сетью №1}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-2network-valid.png}
  \caption{Классификация сетью №2}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-3network-valid.png}
  \caption{Классификация сетью №3}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-4network-valid.png}
  \caption{Классификация сетью №4}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-5network-valid.png}
  \caption{Классификация сетью №5}
\end{figure}

\begin{figure}[H]
  \includegraphics[width=17cm]{02-dip-04-lab-6network-valid.png}
  \caption{Классификация сетью №6}
\end{figure}

\end{document}
