Главная страница
Навигация по странице:

  • 2D-свертка: кодирование прямого прохода

  • 2D-свертка: написание кода для обратного прохода

  • Последний элемент: добавление «каналов»

  • Использование операции для обучения CNN

  • Пара слов о скорости и альтернативной реализации

  • Рекуррентные нейронные сети

  • Курсовая работа. Глубокое обучение


    Скачать 4.97 Mb.
    НазваниеГлубокое обучение
    АнкорКурсовая работа
    Дата26.06.2022
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаVeydman_S_Glubokoe_obuchenie_Legkaya_razrabotka_proektov_na_Pyth.pdf
    ТипДокументы
    #615357
    страница15 из 22
    1   ...   11   12   13   14   15   16   17   18   ...   22
    Двумерная свертка
    2D-свертка — это просто расширение одномерного случая, так как соеди- нение входа и выхода через фильтры в каждой отдельной размерности будет таким, как в одномерном случае. Действия будут такими же:
    1. На прямом проходе мы:
    x дополняем входные данные;
    x используем новый вход с отступами и параметры для вычисления результата.

    178
    Глава 5. Сверточная нейронная сеть
    2. На обратном проходе для вычисления входного градиента мы:
    x соответствующим образом заполняем выходной градиент;
    x используем этот выходной градиент с дополнением, а также вход- ные данные и параметры, чтобы вычислить как входной градиент, так и градиент параметров.
    3. На обратном проходе для вычисления градиента параметра мы:
    x правильно дополняем ввод;
    x проходим по элементам дополненного ввода и соответствующим образом увеличиваем градиент параметра.
    2D-свертка: кодирование прямого прохода
    В одномерном случае код с учетом входных данных и параметров прямого прохода выглядел следующим образом:
    # input_pad: вход с добавленными отступами out = np.zeros_like(inp)
    for o in range(out.shape[0]):
    for p in range(param_len):
    out[o] += param[p] * input_pad[o+p]
    Для двумерных сверток получаем:
    # input_pad: вход с добавленными отступами out = np.zeros_like(inp)
    for o_w in range(img_size): # проход по высоте изображения for o_h in range(img_size): # проход по ширине изображения for p_w in range(param_size): # проход по высоте параметра for p_h in range(param_size): # проход по ширине параметра out[o_w][o_h] += param[p_w][p_h] * input_pad[o_
    w+p_w][o_h+p_h]
    Каждый цикл разбивался на два вложенных цикла.

    Свертка: обратный проход
    179
    Расширение до двух измерений при наличии пакета изображений тоже похоже на одномерный случай: мы просто добавляем цикл for снаружи циклов, показанных здесь.
    2D-свертка: написание кода для обратного прохода
    Конечно же, как и в прямом проходе, мы можем использовать для об- ратного прохода те же индексы, что и в одномерном случае. Напомню, что в одномерном случае код имел вид:
    input_grad = np.zeros_like(inp)
    for o in range(inp.shape[0]):
    for p in range(param_len):
    input_grad[o] += output_pad[o+param_len-p-1] * param[p]
    В 2D-случае код выглядит так:
    # output_pad: вывод с отступами input_grad = np.zeros_like(inp)
    for i_w in range(img_width):
    for i_h in range(img_height):
    for p_w in range(param_size):
    for p_h in range(param_size):
    input_grad[i_w][i_h] +=
    output_pad[i_w+param_size-p_w-1][i_h+param_size-p_h-1] \
    * param[p_w][p_h]
    Обратите внимание, что индексирование на выходе будет таким же, как в одномерном случае, однако происходит в двух измерениях; в одномер- ном случае было:
    output_pad[i+param_size-p-1] * param[p]
    в 2D-случае есть:
    output_pad[i_w+param_size-p_w-1][i_h+param_size-p_h-1] * param[p_w][p_h]
    Другие факты одномерного случая тоже остались актуальны:
    y для пакета входных изображений выполняется предыдущая операция для каждого наблюдения, а затем результаты суммируются;

    180
    Глава 5. Сверточная нейронная сеть y
    для градиента параметра производится проход через все изображения в пакете и добавляются компоненты от каждого к соответствующим местам в градиенте параметра
    1
    :
    # input_pad: входные данные с отступами param_grad = np.zeros_like(param)
    for i in range(batch_size): # equal to inp.shape[0]
    for o_w in range(img_size):
    for o_h in range(img_size):
    for p_w in range(param_size):
    for p_h in range(param_size):
    param_grad[p_w][p_h] += input_pad[i][o_w+p_w]
    [o_h+p_h] \
    * output_grad[i][o_w][o_h]
    Код для полноценной многоканальной свертки почти готов. В данный момент код сворачивает фильтры по двумерному вводу и производит двумерный вывод. Как уже говорилось, на каждом сверточном слое есть не только нейроны, расположенные вдоль этих двух измерений, но и некоторое количество «каналов», равное количеству карт признаков, которые создает слой. Рассмотрим этот момент подробнее.
    Последний элемент: добавление «каналов»
    Как учесть случаи, когда и ввод, и вывод являются многоканальными?
    Как и в случае добавления пакетов, ответ прост: добавляются два внешних цикла for в уже знакомый код — один цикл для входных каналов и другой для выходных каналов. Зацикливая все комбинации входного и выходного каналов, мы делаем каждую карту выходных объектов комбинацией всех карт входных объектов.
    Чтобы это работало, следует всегда представлять изображения как трех- мерные ndarrays
    . Черно-белые изображения будут иметь один канал, а цветные изображения — три (красный, синий и зеленый). Затем, неза- висимо от количества каналов, работа продолжается, как описано ранее, с использованием ряда карт признаков, созданных из изображения, каж-
    1
    Полную реализацию см. на веб-сайте книги (
    https://oreil.ly/2H99xkJ
    ).

    Свертка: обратный проход
    181
    дый из которых представляет собой комбинацию сверток, полученных от всех каналов в изображении (или из каналов на предыдущем слое, если речь идет о слоях в сети).
    Прямой проход
    А теперь напишем код для вычисления вывода сверточного слоя с учетом четырехмерных ndarrays на входе и параметров:
    def _compute_output_obs(obs: ndarray, param: ndarray) -> ndarray:
    '''
    obs: [channels, img_width, img_height]
    param: [in_channels, out_channels, param_width, param_height]
    '''
    assert_dim(obs, 3)
    assert_dim(param, 4)
    param_size = param.shape[2]
    param_mid = param_size // 2
    obs_pad = _pad_2d_channel(obs, param_mid)
    in_channels = fil.shape[0]
    out_channels = fil.shape[1]
    img_size = obs.shape[1]
    out = np.zeros((out_channels,) + obs.shape[1:])
    for c_in in range(in_channels):
    for c_out in range(out_channels):
    for o_w in range(img_size):
    for o_h in range(img_size):
    for p_w in range(param_size):
    for p_h in range(param_size):
    out[c_out][o_w][o_h] += \
    param[c_in][c_out][p_w][p_h]
    * obs_pad[c_in][o_w+p_w][o_h+p_h]
    return out def _output(inp: ndarray, param: ndarray) -> ndarray:
    '''

    182
    Глава 5. Сверточная нейронная сеть obs: [batch_size, channels, img_width, img_height]
    param: [in_channels, out_channels, param_width, param_height]
    '''
    outs = [_compute_output_obs(obs, param) for obs in inp]
    return np.stack(outs)
    Обратите внимание, что функция
    _pad_2d_channel добавляет отступы.
    Код вычислений аналогичен коду в более простом 2D-случае (без ка- налов), показанном ранее, за исключением того, что теперь имеется fil[c_out][c_in][p_w][p_h]
    вместо обычного fil[p_w][p_h]
    , поскольку в массиве фильтров есть еще два измерения и лишние c_out
    × c_in элементов.
    Обратный проход
    Обратный проход аналогичен и выполняется так же, как и в простом
    2D-случае:
    1) для входных градиентов вычисляются градиенты каждого наблю- дения по отдельности (для этого добавляем выходной градиент), а затем градиенты складываются;
    2) используется выходной градиент с отступами для градиента параме- тра, выполняется проход в цикле по наблюдениям и применяются соответствующие значения для каждого из них, чтобы обновить градиент параметра.
    Ниже приведен код вычисления выходного градиента:
    def _compute_grads_obs(input_obs: ndarray, output_grad_obs: ndarray,
    param: ndarray) -> ndarray:
    '''
    input_obs: [in_channels, img_width, img_height]
    output_grad_obs: [out_channels, img_width, img_height]
    param: [in_channels, out_channels, img_width, img_height]
    '''
    input_grad = np.zeros_like(input_obs)
    param_size = param.shape[2]
    param_mid = param_size // 2
    img_size = input_obs.shape[1]

    Свертка: обратный проход
    183
    in_channels = input_obs.shape[0]
    out_channels = param.shape[1]
    output_obs_pad = _pad_2d_channel(output_grad_obs, param_mid)
    for c_in in range(in_channels):
    for c_out in range(out_channels):
    for i_w in range(input_obs.shape[1]):
    for i_h in range(input_obs.shape[2]):
    for p_w in range(param_size):
    for p_h in range(param_size):
    input_grad[c_in][i_w][i_h] += \
    output_obs_pad[c_out][i_w+param_size-p_w-1]
    [i_h+param_size-p_h-1] \
    * param[c_in][c_out][p_w][p_h]
    return input_grad def _input_grad(inp: ndarray, output_grad: ndarray, param: ndarray)
    -> ndarray:
    grads = [_compute_grads_obs(inp[i], output_grad[i], param) for i in range( output_grad.shape[0])]
    return np.stack(grads)
    А вот и градиент параметра:
    def _param_grad(inp: ndarray, output_grad: ndarray, param: ndarray)
    -> ndarray:
    '''
    inp: [in_channels, img_width, img_height]
    output_grad_obs: [out_channels, img_width, img_height]
    param: [in_channels, out_channels, img_width, img_height]
    '''
    param_grad = np.zeros_like(param)
    param_size = param.shape[2]
    param_mid = param_size // 2
    img_size = inp.shape[2]
    in_channels = inp.shape[1]
    out_channels = output_grad.shape[1]

    184
    Глава 5. Сверточная нейронная сеть inp_pad = _pad_conv_input(inp, param_mid)
    img_shape = output_grad.shape[2:]
    for i in range(inp.shape[0]):
    for c_in in range(in_channels):
    for c_out in range(out_channels):
    for o_w in range(img_shape[0]):
    for o_h in range(img_shape[1]):
    for p_w in range(param_size):
    for p_h in range(param_size):
    param_grad[c_in][c_out][p_w][p_h] += \
    inp_pad[i][c_in][o_w+p_w][o_h+p_h] \
    * output_grad[i][c_out][o_w][o_h]
    return param_grad
    Эти три функции —
    _output
    ,
    _input_grad и
    _param_grad
    — именно то, что нужно для создания класса
    Conv2DOperation
    , формирующего ядро
    Conv2DLayers
    , которое мы будем использовать в наших CNN! Осталось проработать всего пару деталей, а потом можно применять эту операцию в сверточной сети.
    Использование операции для обучения CNN
    Чтобы получить работающую модель CNN, нужно реализовать еще не- много:
    1) реализовать операцию flatten, рассмотренную ранее в этой главе, чтобы модель могла делать прогнозы;
    2) включить этот класс
    Operation
    , а также
    Conv2DOpOperation в слой
    Conv2D
    ;
    3) написать более быструю версию
    Conv2DOperation
    . Напишем ее здесь, а подробности рассмотрим в разделе «Цепное правило» приложения А.
    Операция flatten
    Для завершения сверточного слоя понадобится еще одна операция: опера- ция flatten. Результатом операции свертки будет трехмерный массив для каждого наблюдения измерения (число каналов, img_height
    , img_width
    ).
    Но если мы не передадим эти данные в другой сверточный слой, сначала

    Использование операции для обучения CNN
    185
    нужно будет преобразовать их в вектор для каждого наблюдения. По- скольку каждый нейрон кодирует присутствие шаблона в данном месте на изображении, мы легко можем «сплющить» этот трехмерный ndarray в одномерный вектор и передать его вперед. Операция flatten, показанная здесь, делает именно это, учитывая тот факт, что в сверточных слоях, как и в любом другом слое, первым измерением нашего ndarray всегда является размер пакета:
    class Flatten(Operation):
    def __init__(self):
    super().__init__()
    def _output(self) -> ndarray:
    return self.input.reshape(self.input.shape[0], -1)
    def _input_grad(self, output_grad: ndarray) -> ndarray:
    return output_grad.reshape(self.input.shape)
    Это последняя требуемая операция. Теперь обернем наши
    Operation в
    Layer
    Готовый слой Conv2D
    Таким образом, весь сверточный слой будет выглядеть примерно так:
    class Conv2D(Layer):
    def __init__(self, out_channels: int, param_size: int, activation: Operation = Sigmoid(), flatten: bool = False) -> None:
    super().__init__()
    self.out_channels = out_channels self.param_size = param_size self.activation = activation self.flatten = flatten def _setup_layer(self, input_: ndarray) -> ndarray:
    self.params = []

    186
    Глава 5. Сверточная нейронная сеть conv_param = np.random.randn(self.out_channels,
    input_.shape[1], # входные каналы self.param_size,
    self.param_size)
    self.params.append(conv_param)
    self.operations = []
    self.operations.append(Conv2D(conv_param))
    self.operations.append(self.activation)
    if self.flatten:
    self.operations.append(Flatten())
    return None
    В зависимости от того, хотим ли мы передавать выходные данные этого слоя в другой сверточный слой или в полносвязный связанный слой для предсказаний, применятся (или нет) операция flatten.
    Пара слов о скорости и альтернативной реализации
    Читатели, знакомые с понятием вычислительной сложности, могут сказать, что такой код катастрофически медленный: для вычисления градиента параметра пришлось написать семь вложенных циклов! В этом нет ничего плохого, поскольку нам нужно было прочувствовать и понять принцип работы CNN, написав все с нуля. Но можно написать по-другому:
    1) из входных данных извлекаются участки image_height
    × image_
    width
    × num_channels размера filter_height
    × filter_width из набора тестов;
    2) для каждого участка выполняется скалярное произведение на соот- ветствующий фильтр, соединяющий входные каналы с выходными каналами;
    3) складываем результаты скалярных произведений, чтобы сформи- ровать результат.
    Проявив смекалку, можно выразить почти все описанные ранее операции через пакетное умножение матриц, реализованное с помощью функции
    NumPy mul
    . В приложении A и на веб-сайте книги показано, как это сде- лать, а пока достаточно сказать, что такая реализация позволяет писать

    Использование операции для обучения CNN
    187
    относительно небольшие сверточные нейронные сети, которые будут об- учаться за разумное количество времени. А это, в свою очередь, открывает простор для экпериментов!
    Эксперименты
    Даже если мы используем функцию matmul и изменение формы, обуче- ние модели на одну эпоху с одним слоем свертки все равно займет около
    10 минут, поэтому ограничимся демонстрацией модели только с одним сверточным слоем с 32 каналами (количество выбрано условно):
    model = NeuralNetwork(
    layers=[Conv2D(out_channels=32,
    param_size=5,
    dropout=0.8,
    weight_init="glorot",
    flatten=True,
    activation=Tanh()),
    Dense(neurons=10,
    activation=Linear())],
    loss = SoftmaxCrossEntropy(),
    seed=20190402)
    Обратите внимание, что у этой модели 32
    × 5 × 5 = 800 параметров в пер- вом слое, но эти параметры используются для создания 32
    × 28 × 28 =
    = 25 088 нейронов, или «изученных признаков». В полносвязном слое со скрытым размером 32 получится 784
    × 32 = 25 088 параметров и всего
    32 нейрона.
    Несколько простых проб и ошибок — обучение этой модели на нескольких сотнях пакетов с разными скоростями обучения и наблюдение за полу- ченными в результате потерями — показывают, что скорость обучения 0.01 работает лучше, чем скорость обучения 0.1, когда первый слой сверточ- ный, а не полносвязный. Обучение сети за одну эпоху с оптимизатором
    SGDMomentum
    (lr
    =
    0.01,
    momentum
    =
    0.9)
    дает:
    Validation accuracy after 100 batches is 79.65%
    Validation accuracy after 200 batches is 86.25%
    Validation accuracy after 300 batches is 85.47%
    Validation accuracy after 400 batches is 87.27%
    Validation accuracy after 500 batches is 88.93%

    188
    Глава 5. Сверточная нейронная сеть
    Validation accuracy after 600 batches is 88.25%
    Validation accuracy after 700 batches is 89.91%
    Validation accuracy after 800 batches is 89.59%
    Validation accuracy after 900 batches is 89.96%
    Validation loss after 1 epochs is 3.453
    Model validation accuracy after 1 epoch is 90.50%
    Из результата видно, что мы можем обучить сверточную нейронную сеть с нуля, что в итоге дает MNIST более 90% точности всего за один проход по обучающему набору
    1
    !
    Заключение
    В этой главе мы поговорили о сверточных нейронных сетях. Мы начали с общих понятий о сверточных сетях и о том, в чем они схожи и чем от- личаются от полносвязных нейронных сетей, а затем описали их работу на низком уровне, реализовав базовую операцию многоканальной свертки с нуля в Python.
    Начиная с высокого уровня сверточные слои создают примерно на по- рядок больше нейронов, чем полносвязные слои, которые мы видели до этого, причем каждый нейрон представляет собой комбинацию всего лишь нескольких признаков из предыдущего слоя, а не всех элементов предыдущего уровня, как в полносвязных слоях. На уровне ниже мы увидели, что нейроны фактически сгруппированы в «карты признаков», каждая из которых показывает, присутствует ли определенный шаблон или их комбинация в данном месте на изображении. Такие карты при- знаков называются «каналами» сверточного слоя.
    Несмотря на все отличия от классов
    Operation
    , которые мы использовали в плотных слоях, операция свертки вписывается в тот же шаблон, что и другие операции
    ParamOperation
    , которые мы видели:
    y у него есть метод
    _output
    , который вычисляет вывод с учетом его входных данных и параметров;
    y у него есть методы
    _input_grad и
    _param_grad
    , которые при заданном output_grad той же формы, что и выходные данные операции, вычис-
    1
    Полный код можно найти в разделе главы в репозитории книги на GitHub.

    Заключение
    189
    ляют градиенты той же формы, что и входные данные и параметры соответственно.
    Разница тут лишь в том, что
    _input
    , output и params теперь являются четы- рехмерными объектами ndarray
    , тогда как в случае полностью связанных слоев они были двумерными.
    Полученные знания сформируют прочный фундамент для изучения или применения сверточных нейронных сетей в будущем. Далее рассмотрим еще один распространенный вид архитектуры нейронных сетей: рекур- рентные нейронные сети, предназначенные для работы с данными, появ- ляющимися в последовательностях, а не просто с несвязанными пакетами, с которыми мы имели дело в случаях с домами и изображениями. Вперед!

    ГЛАВА 6
    Рекуррентные нейронные сети
    В этой главе рассмотрим рекуррентные нейронные сети (recurrent neural
    networks, RNN), класс нейросетей, предназначенных для обработки по- следовательных данных. В нейронных сетях, с которыми мы работали до этого, каждая полученная партия данных рассматривалась как набор независимых наблюдений. Сеть не знала ничего о предыдущих или будущих данных цифр из MNIST, будь то полносвязная нейронная сеть из главы 4 или сверточная нейронная сеть из главы 5. Однако многие виды данных являются по своей природе упорядоченными. Это могут быть зависящие от времени последовательности финансовых данных или языковые данные, в которых символы, слова и предложения упорядоче- ны, и т. д. Цель рекуррентных нейронных сетей — научиться принимать
    последовательности таких данных и возвращать правильный прогноз, например цену товара на следующий день или следующее слово в пред- ложении.
    Для работы с упорядоченными данными с помощью полносвязных ней- ронных сетей, которые мы рассматривали в первых нескольких главах, потребуются три модификации. Во-первых, потребуется добавить «новое измерение» в данные, которые мы подаем на вход нейронной сети. Ра- нее данные, которые мы подавали нейронным сетям, были, по существу, двумерными — у каждого ndarray было одно измерение, отвечающее за число наблюдений, и еще одно, обозначающее число признаков
    1
    . Можно представить это иначе — каждое наблюдение представляет собой одно- мерный вектор. В рекуррентных нейронных сетях у данных тоже будет измерение, представляющее количество наблюдений, но каждое наблю- дение будет представлено в виде двумерного массива: одно измерение обозначает длину последовательности данных, а второе — сами признаки
    1
    Мы обнаружили, что наблюдения удобно располагать по строкам, а элементы — по столбцам, но это не обязательно. В любом случае данные должны быть двумерными.

    Ключевое ограничение: работа с ветвлениями
    1   ...   11   12   13   14   15   16   17   18   ...   22


    написать администратору сайта