Завершите построение сверточной нейронной сети CNN, используя только NumPy (с кодом Python)

искусственный интеллект Python Нейронные сети NumPy

В настоящее время в Интернете есть много скомпилированных наборов инструментов для машинного обучения и глубокого обучения.В некоторых случаях может быть очень удобно и эффективно напрямую вызывать уже построенные модели, такие как наборы инструментов Caffe и TensorFlow, но для этих наборов инструментов требуется много инструментов. аппаратные ресурсы, что не способствует практике и пониманию новичков. Поэтому, чтобы лучше понять и освоить соответствующие знания, лучше всего попрактиковаться в программировании самостоятельно. В этой статье будет показано, как использовать NumPy для построения сверточной нейронной сети (CNN).
  CNN — это разновидность нейронной сети, предложенная ранее, и она стала популярной только в последние годы, можно сказать, что это наиболее используемая сеть в области компьютерного зрения. Модель CNN была хорошо реализована в некоторых наборах инструментов, а соответствующие библиотечные функции были полностью скомпилированы. Разработчикам нужно только вызвать существующие модули для завершения построения модели, избегая сложности реализации. Но на самом деле это заставит разработчиков не знать конкретных деталей реализации. Бывают случаи, когда специалист по данным должен улучшить производительность модели с помощью деталей, которых нет в наборе инструментов. В этом случае единственным решением является самостоятельное программирование аналогичной модели, чтобы иметь наивысший уровень контроля над реализуемой моделью, а также лучшее понимание того, как модель обрабатывается на каждом шаге.
  В этой статье NumPy будет использоваться только для реализации сети CNN и создания трехуровневых модулей, а именно сверточный слой (Conv), функция активации ReLu и максимальный пул (max pooling).

1. Прочитайте входное изображение

Следующий код будет считывать уже существующее изображение из библиотеки skimage Python и преобразовывать его в изображение в градациях серого:

1.  import skimage.data  
2.  # Reading the image  
3.  img = skimage.data.chelsea()  
4.  # Converting the image into gray.  
5.  img = skimage.color.rgb2gray(img)js

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

1

2. Подготовьте фильтр

Следующий код подготавливает банк фильтров для первого сверточного слоя Conv (уровень 1, сокращенно l1, то же самое ниже):

1.  l1_filter = numpy.zeros((2,3,3))

Создайте массив нулей на основе количества фильтров и размера каждого фильтра. Приведенный выше код создает 2 фильтра размером 3x3, номера элементов в (2, 3, 3) представляют 2: количество фильтров (num_filters), 3: количество столбцов фильтра, 3: количество строк фильтров. Поскольку входное изображение представляет собой изображение в градациях серого, после считывания оно становится матрицей двумерного изображения, поэтому размер фильтра выбирается как двумерный массив, а глубина отбрасывается. Если изображение является цветным (с 3 каналами, RGB), размер фильтра должен быть (3,3,3), последние 3 — это глубина, а приведенный выше код также изменяется на (2,3, 3). ,3).
  Размер банка фильтров указывается сам по себе, но в данном фильтре нет конкретного значения, и обычно используется случайная инициализация. Для проверки вертикальных и горизонтальных краев можно использовать следующий набор значений:

1.  l1_filter[0, :, :] = numpy.array([[[-1, 0, 1],   
2.                                     [-1, 0, 1],   
3.                                     [-1, 0, 1]]])  
4.  l1_filter[1, :, :] = numpy.array([[[1,   1,  1],   
5.                                     [0,   0,  0],   
6.                                     [-1, -1, -1]]]) 

3. Слой свертки (Conv Layer)

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

1.  l1_feature_map = conv(img, l1_filter) 

Функция conv принимает только два параметра: входное изображение и банк фильтров:

1.  def conv(img, conv_filter):  
2.      if len(img.shape) > 2 or len(conv_filter.shape) > 3: # Check if number of image channels matches the filter depth.  
3.          if img.shape[-1] != conv_filter.shape[-1]:  
4.              print("Error: Number of channels in both image and filter must match.")  
5.              sys.exit()  
6.      if conv_filter.shape[1] != conv_filter.shape[2]: # Check if filter dimensions are equal.  
7.          print('Error: Filter must be a square matrix. I.e. number of rows and columns must match.')  
8.          sys.exit()  
9.      if conv_filter.shape[1]%2==0: # Check if filter diemnsions are odd.  
10.         print('Error: Filter must have an odd size. I.e. number of rows and columns must be odd.')  
11.         sys.exit()  
12.   
13.     # An empty feature map to hold the output of convolving the filter(s) with the image.  
14.     feature_maps = numpy.zeros((img.shape[0]-conv_filter.shape[1]+1,   
15.                                 img.shape[1]-conv_filter.shape[1]+1,   
16.                                 conv_filter.shape[0]))  
17.   
18.     # Convolving the image by the filter(s).  
19.     for filter_num in range(conv_filter.shape[0]):  
20.         print("Filter ", filter_num + 1)  
21.         curr_filter = conv_filter[filter_num, :] # getting a filter from the bank.  
22.         """  
23.         Checking if there are mutliple channels for the single filter. 
24.         If so, then each channel will convolve the image. 
25.         The result of all convolutions are summed to return a single feature map. 
26.         """  
27.         if len(curr_filter.shape) > 2:  
28.             conv_map = conv_(img[:, :, 0], curr_filter[:, :, 0]) # Array holding the sum of all feature maps.  
29.             for ch_num in range(1, curr_filter.shape[-1]): # Convolving each channel with the image and summing the results.  
30.                 conv_map = conv_map + conv_(img[:, :, ch_num],   
31.                                   curr_filter[:, :, ch_num])  
32.         else: # There is just a single channel in the filter.  
33.             conv_map = conv_(img, curr_filter)  
34.         feature_maps[:, :, filter_num] = conv_map # Holding feature map with the current filter.
35.      return feature_maps # Returning all feature maps. 

Функция сначала гарантирует, что глубина каждого фильтра равна количеству каналов изображения, код выглядит следующим образом. Оператор if сначала проверяет, есть ли у изображения и фильтра канал глубины. Если он существует, он проверяет, равно ли количество каналов. Если совпадение не удалось, сообщается об ошибке.

1.  if len(img.shape) > 2 or len(conv_filter.shape) > 3: # Check if number of image channels matches the filter depth.  
2.          if img.shape[-1] != conv_filter.shape[-1]:  
3.              print("Error: Number of channels in both image and filter must match.")  

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

1.  if conv_filter.shape[1] != conv_filter.shape[2]: # Check if filter dimensions are equal.  
2.      print('Error: Filter must be a square matrix. I.e. number of rows and columns must match.')  
3.      sys.exit()  
4.  if conv_filter.shape[1]%2==0: # Check if filter diemnsions are odd.  
5.      print('Error: Filter must have an odd size. I.e. number of rows and columns must be odd.')  
6.      sys.exit()  

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

1.  # An empty feature map to hold the output of convolving the filter(s) with the image.  
2.  feature_maps = numpy.zeros((img.shape[0]-conv_filter.shape[1]+1,   
3.                              img.shape[1]-conv_filter.shape[1]+1,   
4.                              conv_filter.shape[0])) 

Поскольку шаг или отступы не заданы, шаг по умолчанию равен 1, а отступы отсутствуют. Тогда размер карты признаков, полученной после операции свертки, равен (img_rows-filter_rows+1, image_columns-filter_columns+1, num_filters), то есть размер входного изображения минус размер фильтра плюс 1. Обратите внимание, что каждый фильтр выводит карту объектов.

1.   # Convolving the image by the filter(s).  
2.      for filter_num in range(conv_filter.shape[0]):  
3.          print("Filter ", filter_num + 1)  
4.          curr_filter = conv_filter[filter_num, :] # getting a filter from the bank.  
5.          """  
6.          Checking if there are mutliple channels for the single filter. 
7.          If so, then each channel will convolve the image. 
8.          The result of all convolutions are summed to return a single feature map. 
9.          """  
10.         if len(curr_filter.shape) > 2:  
11.             conv_map = conv_(img[:, :, 0], curr_filter[:, :, 0]) # Array holding the sum of all feature maps.  
12.             for ch_num in range(1, curr_filter.shape[-1]): # Convolving each channel with the image and summing the results.  
13.                 conv_map = conv_map + conv_(img[:, :, ch_num],   
14.                                   curr_filter[:, :, ch_num])  
15.         else: # There is just a single channel in the filter.  
16.             conv_map = conv_(img, curr_filter)  
17.         feature_maps[:, :, filter_num] = conv_map # Holding feature map with the current filter.  

После прохода по каждому фильтру в банке фильтров обновите состояние фильтра с помощью следующего кода:

1.  curr_filter = conv_filter[filter_num, :] # getting a filter from the bank.  

Если входное изображение имеет более одного канала, фильтр должен иметь такое же количество каналов. Только тогда процесс свертки может протекать нормально. Наконец, выходные данные каждого фильтра суммируются в виде выходной карты объектов. Следующий код определяет количество каналов во входном изображении.Если изображение имеет только один канал, то одна свертка может завершить весь процесс:

1.  if len(curr_filter.shape) > 2:  
2.       conv_map = conv_(img[:, :, 0], curr_filter[:, :, 0]) # Array holding the sum of all feature map 
3.       for ch_num in range(1, curr_filter.shape[-1]): # Convolving each channel with the image and summing the results.  
4.          conv_map = conv_map + conv_(img[:, :, ch_num],   
5.                                    curr_filter[:, :, ch_num])  
6.  else: # There is just a single channel in the filter.  
7.      conv_map = conv_(img, curr_filter) 

Функция conv_ в приведенном выше коде отличается от предыдущей функции conv. Функция conv принимает только два параметра: входное изображение и банк фильтров. Сама операция свертки не выполняется. Набор входных фильтров. Ниже приведен код реализации функции conv_:

1.  def conv_(img, conv_filter):  
2.      filter_size = conv_filter.shape[0]  
3.      result = numpy.zeros((img.shape))  
4.      #Looping through the image to apply the convolution operation.  
5.      for r in numpy.uint16(numpy.arange(filter_size/2,   
6.                            img.shape[0]-filter_size/2-2)):  
7.          for c in numpy.uint16(numpy.arange(filter_size/2, img.shape[1]-filter_size/2-2)):  
8.              #Getting the current region to get multiplied with the filter.  
9.              curr_region = img[r:r+filter_size, c:c+filter_size]  
10.             #Element-wise multipliplication between the current region and the filter.  
11.             curr_result = curr_region * conv_filter  
12.             conv_sum = numpy.sum(curr_result) #Summing the result of multiplication.  
13.             result[r, c] = conv_sum #Saving the summation in the convolution layer feature map.  
14.               
15.     #Clipping the outliers of the result matrix.  
16.     final_result = result[numpy.uint16(filter_size/2):result.shape[0]-numpy.uint16(filter_size/2),   
17.                           numpy.uint16(filter_size/2):result.shape[1]-numpy.uint16(filter_size/2)]  
18.     return final_result  

Каждый фильтр итеративно сворачивает изображение с одинаковым размером, реализуемым следующим кодом:

1.  curr_region = img[r:r+filter_size, c:c+filter_size]  

После этого побитовое умножение между матрицей области изображения и фильтром и суммирование результатов для получения однозначного результата:

1.  #Element-wise multipliplication between the current region and the filter.  
2.  curr_result = curr_region * conv_filter  
3.  conv_sum = numpy.sum(curr_result) #Summing the result of multiplication.  
4.  result[r, c] = conv_sum #Saving the summation in the convolution layer feature map.  

После свертки входного изображения с каждым фильтром карта объектов возвращается функцией conv. На следующем рисунке показана карта объектов, возвращаемая свёрточным слоем (поскольку параметры фильтра свёрточного слоя l1 равны (2, 3, 3), то есть 2 ядра свертки размером 3x3, окончательный результат 2 карт объектов):

2
Изображение после свертки


За сверточным слоем обычно следует слой функции активации, и в этой статье используется функция активации ReLU.

4. Уровень функции активации ReLU

Слой ReLU применяет функцию активации ReLU к каждой карте объектов, выводимой слоем conv, и вызывает функцию активации ReLU в соответствии со следующими строками кода:

l1_feature_map_relu = relu(l1_feature_map)

Конкретный код реализации функции активации ReLU (ReLU) выглядит следующим образом:

1.  def relu(feature_map):  
2.      #Preparing the output of the ReLU activation function.  
3.      relu_out = numpy.zeros(feature_map.shape)  
4.      for map_num in range(feature_map.shape[-1]):  
5.          for r in numpy.arange(0,feature_map.shape[0]):  
6.              for c in numpy.arange(0, feature_map.shape[1]):  
7.                  relu_out[r, c, map_num] = numpy.max(feature_map[r, c, map_num], 0)  

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

3
Выходное изображение слоя ReLU


За уровнем функции активации обычно следует уровень пула, и в этой статье используется максимальный пул.

5. Максимальный слой объединения

Выходные данные слоя ReLU используются в качестве входных данных слоя максимального пула, а операция максимального пула вызывается в соответствии со следующей строкой кода:

1.  l1_feature_map_relu_pool = pooling(l1_feature_map_relu, 2, 2)  

Конкретный код реализации функции максимального объединения (max pooling) выглядит следующим образом:

1.  def pooling(feature_map, size=2, stride=2):  
2.      #Preparing the output of the pooling operation.  
3.      pool_out = numpy.zeros((numpy.uint16((feature_map.shape[0]-size+1)/stride),  
4.                              numpy.uint16((feature_map.shape[1]-size+1)/stride),  
5.                              feature_map.shape[-1]))  
6.      for map_num in range(feature_map.shape[-1]):  
7.          r2 = 0  
8.          for r in numpy.arange(0,feature_map.shape[0]-size-1, stride):  
9.              c2 = 0  
10.             for c in numpy.arange(0, feature_map.shape[1]-size-1, stride):  
11.                 pool_out[r2, c2, map_num] = numpy.max(feature_map[r:r+size,  c:c+size])  
12.                 c2 = c2 + 1  
13.             r2 = r2 +1  

Эта функция принимает 3 параметра: выходные данные слоя ReLU, размер и шаг маски объединения. Первый — создать пустой массив для хранения вывода функции. Размер массива определяется в соответствии с размером карты входных объектов, размером маски и шагом.

1.  pool_out = numpy.zeros((numpy.uint16((feature_map.shape[0]-size+1)/stride),  
2.                          numpy.uint16((feature_map.shape[1]-size+1)/stride),  
3.                          feature_map.shape[-1]))  

Выполните операцию максимального объединения для каждого входного канала карты объектов и верните наибольшее значение в области. Код выглядит следующим образом:

pool_out[r2, c2, map_num] = numpy.max(feature_map[r:r+size,  c:c+size])

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

4
Выходное изображение слоя пула

6. Наложение слоев

Вышеприведенный контент реализовал базовые слои структуры CNN — conv, ReLU и max pooling, и теперь они сложены и используются, код выглядит следующим образом:

1.  # Second conv layer  
2.  l2_filter = numpy.random.rand(3, 5, 5, l1_feature_map_relu_pool.shape[-1])  
3.  print("\n**Working with conv layer 2**")  
4.  l2_feature_map = conv(l1_feature_map_relu_pool, l2_filter)  
5.  print("\n**ReLU**")  
6.  l2_feature_map_relu = relu(l2_feature_map)  
7.  print("\n**Pooling**")  
8.  l2_feature_map_relu_pool = pooling(l2_feature_map_relu, 2, 2)  
9.  print("**End of conv layer 2**\n") 

Как видно из кода, l2 представляет собой второй слой свертки, а ядро ​​свертки, используемое в этом слое свертки, равно (3, 5, 5), то есть три ядра свертки (фильтры) 5x5 и первое. слой подвергается операции свертки для получения 3 карт объектов. Затем следует функция активации ReLU и операция максимального объединения. Визуализируйте результаты каждой операции, как показано ниже:

5
Изображение визуализации процесса обработки уровня L2

1.  # Third conv layer  
2.  l3_filter = numpy.random.rand(1, 7, 7, l2_feature_map_relu_pool.shape[-1])  
3.  print("\n**Working with conv layer 3**")  
4.  l3_feature_map = conv(l2_feature_map_relu_pool, l3_filter)  
5.  print("\n**ReLU**")  
6.  l3_feature_map_relu = relu(l3_feature_map)  
7.  print("\n**Pooling**")  
8.  l3_feature_map_relu_pool = pooling(l3_feature_map_relu, 2, 2)  
9.  print("**End of conv layer 3**\n"

Как видно из кода, l3 представляет третий слой свертки, а ядро ​​свертки, используемое в этом слое свертки, равно (1, 7, 7), то есть ядро ​​свертки 7x7 (фильтр) и второе ядро ​​свертки. слоя подвергается операции свертки для получения карты объектов. Затем следует функция активации ReLU и операция максимального объединения. Визуализируйте результаты каждой операции, как показано ниже:

6
Изображение визуализации процесса обработки уровня L3


Базовая структура нейронной сети заключается в том, что выходные данные предыдущего слоя используются в качестве входных данных следующего слоя, например, слой l2 получает выходные данные слоя l1, а слой l3 получает выходные данные слоя l2. Код выглядит следующим образом:
1.  l2_feature_map = conv(l1_feature_map_relu_pool, l2_filter)  
2.  l3_feature_map = conv(l2_feature_map_relu_pool, l3_filter)

7. Полный код

Весь код загружен наGithubВыше визуализация каждого слоя реализована с помощью библиотеки Matplotlib.

Ахмед Гад, исследовательские интересы которого связаны с глубоким обучением, искусственным интеллектом и компьютерным зрением.
Домашняя страница:Ву Ву LinkedIn.com/in/Ahmed F Cafe…
Эта статья переведена общественной организацией Alibaba Cloud Yunqi.
Оригинальное название статьи «Построение сверточной нейронной сети с использованием NumPy с нуля», переводчик: Haitang, рецензент: Uncle_LLD.
В статье упрощенный перевод, более подробное содержание,Пожалуйста, просмотрите исходный текст.