Поиск по сайту:

Пакетная нормализация в сверточных нейронных сетях


Пакетная нормализация — это термин, обычно упоминаемый в контексте сверточных нейронных сетей. В этой статье мы собираемся изучить, что это на самом деле влечет за собой, и его влияние, если таковое имеется, на производительность или общее поведение сверточных нейронных сетей.

Предварительные условия

  • Python: для запуска кода на вашем компьютере потребуется установленный Python. Прежде чем продолжить, читатели должны иметь базовый опыт программирования на Python.
  • Основы глубокого обучения. В этой статье рассматриваются концепции, необходимые для применения теории глубокого обучения, и ожидается, что читатели будут иметь некоторый опыт работы с соответствующими терминами и базовой теорией.

Термин «нормализация»

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import torchvision
    import torchvision.transforms as transforms
    import torchvision.datasets as Datasets
    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    import matplotlib.pyplot as plt
    import cv2
    from tqdm.notebook import tqdm
    import seaborn as sns
    from torchvision.utils import make_grid

    if torch.cuda.is_available():
      device = torch.device('cuda:0')
      print('Running on the GPU')
    else:
      device = torch.device('cpu')
      print('Running on the CPU')

Нормализация в статистике относится к процессу ограничения данных или набора значений в диапазоне от 0 до 1. Довольно неудобно, в некоторых областях нормализация также относится к процессу установки среднего значения распределения данных на ноль и его стандартного отклонения. до 1.

В действительности этот процесс установки среднего значения распределения равным 0, а его стандартного отклонения — 1, называется стандартизацией. Однако из-за определенных свобод его также называют нормализацией или нормализацией z-показателя. Важно усвоить это различие и помнить о нем.

Предварительная обработка данных

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

Нормализация мин-макс.

Нормализация min-max — один из наиболее распространенных методов нормализации данных. Как следует из его названия, он ограничивает точки данных диапазоном от 0 до 1, устанавливая минимальное значение в наборе данных равным 0, максимальное — 1, а все, что находится между ними, масштабируется соответствующим образом. Приведенное ниже уравнение дает математическое описание процесса нормализации min-max. По сути, он включает в себя вычитание минимального значения в наборе данных из каждой точки данных, а затем деление на диапазон (максимум – минимум).

Используя приведенную ниже функцию, мы можем воспроизвести процесс нормализации min-max. Используя эту функцию, мы можем интуитивно понять, что на самом деле происходит за кулисами.

    def min_max_normalize(data_points: np.array):
      """
      This function normalizes data by constraining
      data points between the range of 0 & 1  
      """
      #  convert list to numpy array
      if type(data_points) == list:
        data_points = np.array(data_points)
      else:
        pass

      #  create a list to hold normalized data  
      normalized = []

      #  derive minimum and maximum values
      minimum = data_points.min()
      maximum = data_points.max()

      #  convert to list for iteration
      data_points = list(data_points)
      #  normalizing data
      for value in data_points:
        normalize = (value-minimum)/(maximum-minimum)
        normalized.append(round(normalize, 2))

      return np.array(normalized)

Давайте создадим массив случайных значений, используя NumPy, а затем попытаемся нормализовать их, используя функцию нормализации min-max, определенную выше.

    #  creating a random set of data points
    data = np.random.rand(50)*20

    #  normalizing data points
    normalized = min_max_normalize(data)

Из графиков ниже видно, что до нормализации значения находились в диапазоне от 0 до 20, при этом подавляющее большинство точек данных имели значения от 5 до 10. Однако после нормализации видно, что значения теперь варьируются от 0 до 1 с подавляющее большинство точек данных имеют значения от 0,25 до 0,5. Примечание: если/когда вы запустите этот код, распределение данных будет отличаться от того, которое используется в этой статье, поскольку оно генерируется случайным образом.

    #  visualising distribution
    figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
    sns.histplot(data, ax=axes[0])
    axes[0].set_title('unnormalized')
    sns.histplot(normalized, ax=axes[1])
    axes[1].set_title('min-max normalized')

Нормализация Z-оценки

Нормализация Z-показателя, также называемая стандартизацией, представляет собой процесс установки среднего и стандартного отклонения распределения данных на 0 и 1 соответственно. Приведенное ниже уравнение представляет собой математическое уравнение, которое управляет нормализацией z-показателя. Оно включает в себя вычитание среднего значения распределения из значения, подлежащего нормализации, перед делением на стандартное отклонение распределения.

Функция, определенная ниже, повторяет процесс нормализации z-показателя. С помощью этой функции мы можем поближе взглянуть на то, что на самом деле влечет за собой.

    def z_score_normalize(data_points: np.array):
      """
      This function normalizes data by computing
      their z-scores  
      """
      #  convert list to numpy array
      if type(data_points) == list:
        data_points = np.array(data_points)
      else:
        pass

      #  create a list to hold normalized data
      normalized = []

      #  derive mean and and standard deviation
      mean = data_points.mean()
      std = data_points.std()

      #  convert to list for iteration
      data_points = list(data_points)
      #  normalizing data
      for value in data_points:
        normalize = (value-mean)/std
        normalized.append(round(normalize, 2))

      return np.array(normalized)

Используя распределение данных, созданное в предыдущем разделе, давайте попытаемся нормализовать точки данных с помощью функции z-оценки.

    #  normalizing data points
    z_normalized = z_score_normalize(data)

    #  check the mean value
    z_normalized.mean()
    >>>> -0.0006

    #  check the standard deviation
    z_normalized.std()
    >>>> 1.0000

Опять же, из визуализации мы видим, что исходное распределение имеет значения в диапазоне от 0 до 20, в то время как нормализованные значения z-показателя теперь сосредоточены вокруг 0 (среднее значение нуля) и в диапазоне примерно от -1,5 до 1,5, что соответствует более управляемый диапазон.

    #  visualizing distributions
    figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
    sns.histplot(data, ax=axes[0])
    axes[0].set_title('unnormalized')
    sns.histplot(z_normalized, ax=axes[1])
    axes[1].set_title('z-score normalized')

Причины предварительной обработки

Что касается данных в машинном обучении, мы рассматриваем отдельные точки данных как функции. Все эти функции обычно находятся в разных масштабах. Например, рассмотрим дом с 3 спальнями и гостиной площадью 400 квадратных футов. Эти две функции находятся настолько далеко друг от друга, что если их включить в алгоритм машинного обучения, который планируется оптимизировать с помощью градиентного спуска. Оптимизация будет довольно утомительной, поскольку функция большего масштаба будет иметь приоритет над всеми остальными. Чтобы облегчить процесс оптимизации, рекомендуется иметь все точки данных в одном масштабе.

Нормализация в слоях свертки

Точками данных на изображении являются его пиксели. Значения пикселей обычно находятся в диапазоне от 0 до 255; Вот почему, прежде чем подавать изображения в сверточную нейронную сеть, рекомендуется каким-то образом нормализовать их, чтобы поместить все пиксели в управляемый диапазон.

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

Чтобы предотвратить эту проблему, на каждом уровне монастыря вводится нормализация. Такая нормализация называется Пакетной нормализацией.

Процесс пакетной нормализации

Пакетная нормализация по существу устанавливает для пикселей во всех картах объектов в сверточном слое новое среднее значение и новое стандартное отклонение. Обычно все начинается с нормализации всех пикселей по z-показателю, а затем нормализованные значения умножаются на произвольный параметр альфа (масштаб) перед добавлением еще одного произвольного параметра бета (смещение).

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

Пакетная нормализация в действии

Чтобы действительно оценить эффекты пакетной нормализации в слоях свертки, нам нужно сравнить две сети: одну без пакетной нормализации, а другую с пакетной нормализацией. Для этого мы будем использовать архитектуру LeNet-5 и набор данных MNIST.

Набор данных и класс сверточной нейронной сети

В этой статье набор данных MNIST будет использоваться для целей сравнительного анализа, как упоминалось ранее. Этот набор данных состоит из изображений рукописных цифр размером от 0 до 9 размером 28 x 28 пикселей, помеченных соответствующим образом.

Примеры изображений из набора данных MNIST.

Его можно загрузить в PyTorch, используя приведенный ниже блок кода. Обучающий набор состоит из 60 000 изображений, а проверочный набор — из 10 000 изображений. Поскольку мы будем использовать этот набор данных с LeNet-5, размер изображений необходимо изменить до 32 x 32 пикселей, как определено в параметре Transforms.

    #  loading training data
    training_set = Datasets.MNIST(root='./', download=True,
                                  transform=transforms.Compose([transforms.ToTensor(),
                                                                transforms.Resize((32, 32))]))

    #  loading validation data
    validation_set = Datasets.MNIST(root='./', download=True, train=False,
                                    transform=transforms.Compose([transforms.ToTensor(),
                                                                  transforms.Resize((32, 32))]))

Для обучения и использования наших сетей мы будем использовать приведенный ниже класс с метким названием «ConvolutionalNeuralNet()». Этот класс содержит методы, которые помогут обучать и классифицировать экземпляры с помощью обученной сети. Метод train() также содержит внутренние вспомогательные функции, такие как init_weights() и точность.

    class ConvolutionalNeuralNet():
      def __init__(self, network):
        self.network = network.to(device)
        self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)

      def train(self, loss_function, epochs, batch_size, 
                training_set, validation_set):

        #  creating log
        log_dict = {
            'training_loss_per_batch': [],
            'validation_loss_per_batch': [],
            'training_accuracy_per_epoch': [],
            'validation_accuracy_per_epoch': []
        } 

        #  defining weight initialization function
        def init_weights(module):
          if isinstance(module, nn.Conv2d):
            torch.nn.init.xavier_uniform_(module.weight)
            module.bias.data.fill_(0.01)
          elif isinstance(module, nn.Linear):
            torch.nn.init.xavier_uniform_(module.weight)
            module.bias.data.fill_(0.01)

        #  defining accuracy function
        def accuracy(network, dataloader):
          network.eval()
          total_correct = 0
          total_instances = 0
          for images, labels in tqdm(dataloader):
            images, labels = images.to(device), labels.to(device)
            predictions = torch.argmax(network(images), dim=1)
            correct_predictions = sum(predictions==labels).item()
            total_correct+=correct_predictions
            total_instances+=len(images)
          return round(total_correct/total_instances, 3)

        #  initializing network weights
        self.network.apply(init_weights)

        #  creating dataloaders
        train_loader = DataLoader(training_set, batch_size)
        val_loader = DataLoader(validation_set, batch_size)

        #  setting convnet to training mode
        self.network.train()

        for epoch in range(epochs):
          print(f'Epoch {epoch+1}/{epochs}')
          train_losses = []

          #  training
          print('training...')
          for images, labels in tqdm(train_loader):
            #  sending data to device
            images, labels = images.to(device), labels.to(device)
            #  resetting gradients
            self.optimizer.zero_grad()
            #  making predictions
            predictions = self.network(images)
            #  computing loss
            loss = loss_function(predictions, labels)
            log_dict['training_loss_per_batch'].append(loss.item())
            train_losses.append(loss.item())
            #  computing gradients
            loss.backward()
            #  updating weights
            self.optimizer.step()
          with torch.no_grad():
            print('deriving training accuracy...')
            #  computing training accuracy
            train_accuracy = accuracy(self.network, train_loader)
            log_dict['training_accuracy_per_epoch'].append(train_accuracy)

          #  validation
          print('validating...')
          val_losses = []

          #  setting convnet to evaluation mode
          self.network.eval()

          with torch.no_grad():
            for images, labels in tqdm(val_loader):
              #  sending data to device
              images, labels = images.to(device), labels.to(device)
              #  making predictions
              predictions = self.network(images)
              #  computing loss
              val_loss = loss_function(predictions, labels)
              log_dict['validation_loss_per_batch'].append(val_loss.item())
              val_losses.append(val_loss.item())
            #  computing accuracy
            print('deriving validation accuracy...')
            val_accuracy = accuracy(self.network, val_loader)
            log_dict['validation_accuracy_per_epoch'].append(val_accuracy)

          train_losses = np.array(train_losses).mean()
          val_losses = np.array(val_losses).mean()

          print(f'training_loss: {round(train_losses, 4)}  training_accuracy: '+
          f'{train_accuracy}  validation_loss: {round(val_losses, 4)} '+  
          f'validation_accuracy: {val_accuracy}\n')

        return log_dict

      def predict(self, x):
        return self.network(x)

Ленет-5

LeNet-5 (Y. Lecun et al) — одна из первых сверточных нейронных сетей, специально разработанная для распознавания/классификации изображений рукописных цифр. Его архитектура изображена на изображении выше, а ее реализация в PyTorch представлена в следующем блоке кода.

    class LeNet5(nn.Module):
      def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.pool1 = nn.AvgPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.pool2 = nn.AvgPool2d(2)
        self.linear1 = nn.Linear(5*5*16, 120)
        self.linear2 = nn.Linear(120, 84)
        self.linear3 = nn. Linear(84, 10)

      def forward(self, x):
        x = x.view(-1, 1, 32, 32)

        #----------
        # LAYER 1
        #----------
        output_1 = self.conv1(x)
        output_1 = torch.tanh(output_1)
        output_1 = self.pool1(output_1)

        #----------
        # LAYER 2
        #----------
        output_2 = self.conv2(output_1)
        output_2 = torch.tanh(output_2)
        output_2 = self.pool2(output_2)

        #----------
        # FLATTEN
        #----------
        output_2 = output_2.view(-1, 5*5*16)

        #----------
        # LAYER 3
        #----------
        output_3 = self.linear1(output_2)
        output_3 = torch.tanh(output_3)

        #----------
        # LAYER 4
        #----------
        output_4 = self.linear2(output_3)
        output_4 = torch.tanh(output_4)

        #-------------
        # OUTPUT LAYER
        #-------------
        output_5 = self.linear3(output_4)
        return(F.softmax(output_5, dim=1))

Используя определенную выше архитектуру LeNet-5, мы создадим экземпляр model_1, члена класса ConvolutionalNeuralNet, с параметрами, как показано в блоке кода. Эта модель будет служить нашей базовой линией для целей сравнительного анализа.

    #  training model 1
    model_1 = ConvolutionalNeuralNet(LeNet5())

    log_dict_1 = model_1.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64, 
                           training_set=training_set, validation_set=validation_set)

После обучения в течение 10 эпох и визуализации точности из журнала показателей, который мы получаем в ответ, мы видим, что точность как обучения, так и проверки увеличивалась в ходе обучения. В нашем эксперименте точность проверки начиналась примерно с 93% после первой эпохи, а затем продолжала неуклонно увеличиваться в течение следующих 9 итераций, в конечном итоге достигнув чуть более 98% к 10-й эпохе.

    sns.lineplot(y=log_dict_1['training_accuracy_per_epoch'], x=range(len(log_dict_1['training_accuracy_per_epoch'])), label='training')

    sns.lineplot(y=log_dict_1['validation_accuracy_per_epoch'], x=range(len(log_dict_1['validation_accuracy_per_epoch'])), label='validation')

    plt.xlabel('epoch')
    plt.ylabel('accuracy')

Пакетная нормализация LeNet-5

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

    class LeNet5_BatchNorm(nn.Module):
      def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.batchnorm1 = nn.BatchNorm2d(6)
        self.pool1 = nn.AvgPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.batchnorm2 = nn.BatchNorm2d(16)
        self.pool2 = nn.AvgPool2d(2)
        self.linear1 = nn.Linear(5*5*16, 120)
        self.linear2 = nn.Linear(120, 84)
        self.linear3 = nn. Linear(84, 10)

      def forward(self, x):
        x = x.view(-1, 1, 32, 32)

        #----------
        # LAYER 1
        #----------
        output_1 = self.conv1(x)
        output_1 = torch.tanh(output_1)
        output_1 = self.batchnorm1(output_1)
        output_1 = self.pool1(output_1)

        #----------
        # LAYER 2
        #----------
        output_2 = self.conv2(output_1)
        output_2 = torch.tanh(output_2)
        output_2 = self.batchnorm2(output_2)
        output_2 = self.pool2(output_2)

        #----------
        # FLATTEN
        #----------
        output_2 = output_2.view(-1, 5*5*16)

        #----------
        # LAYER 3
        #----------
        output_3 = self.linear1(output_2)
        output_3 = torch.tanh(output_3)

        #----------
        # LAYER 4
        #----------
        output_4 = self.linear2(output_3)
        output_4 = torch.tanh(output_4)

        #-------------
        # OUTPUT LAYER
        #-------------
        output_5 = self.linear3(output_4)
        return(F.softmax(output_5, dim=1))

Используя приведенный ниже сегмент кода, мы можем создать model_2 с включенной пакетной нормализацией и начать обучение с теми же параметрами, что и model_1. Затем мы получаем оценки точности…

    #  training model 2
    model_2 = ConvolutionalNeuralNet(LeNet5_BatchNorm())

    log_dict_2 = model_2.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64, 
                           training_set=training_set, validation_set=validation_set)

Глядя на график, становится ясно, что точность обучения и проверки увеличивалась в ходе обучения, как и в модели без пакетной нормализации. Точность проверки после первой эпохи составляла чуть выше 95 %, что на 3 процентных пункта выше, чем model_1 в в той же точке, а затем постепенно увеличивалась и достигала кульминации примерно на 98,5 %, что на 0,5 % выше, чем у model_1.

    sns.lineplot(y=log_dict_2['training_accuracy_per_epoch'], x=range(len(log_dict_2['training_accuracy_per_epoch'])), label='training')

    sns.lineplot(y=log_dict_2['validation_accuracy_per_epoch'], x=range(len(log_dict_2['validation_accuracy_per_epoch'])), label='validation')

    plt.xlabel('epoch')
    plt.ylabel('accuracy')

Сравнение моделей

Сравнивая обе модели, становится ясно, что модель LeNet-5 с пакетно-нормализованными слоями свертки превзошла обычную модель без пакетно-нормализованных слоев свертки. Поэтому можно с уверенностью сказать, что пакетная нормализация в данном случае помогла повысить производительность.

Сравнение потерь при обучении и проверке между обычной и пакетной нормализованной моделями LeNet-5 также показывает, что пакетная нормализованная модель достигает более низких значений потерь быстрее, чем обычная модель. Это указатель на пакетную нормализацию, увеличивающую скорость, с которой модель оптимизирует свои веса в правильном направлении, или, другими словами, пакетная нормализация увеличивает скорость обучения консети.

Потери на обучение и проверку.

Заключительные замечания

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

После этого мы изучили сам процесс пакетной нормализации, прежде чем оценить его влияние, сравнив два варианта коннетов LeNet-5 (один без пакетной нормы, другой с пакетной нормой) в наборе данных MNIST. По результатам мы пришли к выводу, что нормализация партии способствовала повышению производительности и скорости оптимизации веса. Также были некоторые предположения, что это предотвращает внутренний сдвиг ковариат, однако консенсус по этому поводу мог бы и не быть достигнут.

Статьи по данной тематике: