Как реализовать простую сверточную нейронную сеть с нуля, используя чистый код NumPy

искусственный интеллект глубокое обучение Нейронные сети NumPy


Из KDnuggets Ахмеда Гада, составленного Heart of the Machine.

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

В некоторых случаях может быть удобно использовать модель, которая уже существует в библиотеке ML/DL. Но для лучшего контроля и понимания моделей вам следует реализовать их самостоятельно. В этой статье показано, как реализовать CNN, используя только библиотеку NumPy.

Сверточные нейронные сети (CNN) — это современный современный метод анализа многомерных сигналов, таких как изображения. Уже существует множество библиотек, реализующих CNN, таких как TensorFlow и Keras. Такая библиотека предоставляет только абстрактный API, что значительно снижает сложность разработки и позволяет избежать сложности реализации, но разработчики, использующие такую ​​библиотеку, не имеют доступа к деталям, которые могут быть важны на практике.

Иногда специалистам по данным приходится тщательно изучать эти детали, чтобы повысить производительность. В этом случае лучше всего самостоятельно построить такие модели, которые помогут вам получить максимальный контроль над сетью. Итак, в этой статье мы попытаемся создать CNN, используя только NumPy. Мы создадим три слоя, а именно сверточный слой (сокращенно conv), слой ReLU и слой максимального объединения. Основные этапы заключаются в следующем:

  1. Прочитайте входное изображение.
  2. Подготовьте фильтр.
  3. Сверточный слой: выполняет операцию свертки входного изображения с использованием фильтра.
  4. Слой ReLU: примените функцию активации ReLU к карте объектов (выход сверточный слой).
  5. Max Pooling Layer: примените операцию объединения к выходным данным слоя ReLU.
  6. Стекируйте сверточные слои, слои ReLU и слои с максимальным объединением.


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)

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

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

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

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

Создайте массив на основе количества фильтров и размера каждого фильтра. У нас есть 2 фильтра размером 3*3, поэтому размер массива равен (2=число_фильтров, 3=число_рядов_фильтра, 3=число_столбцов_фильтра). Размер фильтра выбран как двумерный массив без глубины, поскольку входное изображение имеет оттенки серого и имеет глубину 1. Если изображение RGB с 3 каналами, размер фильтра должен быть (3, 3, 3 = глубина).

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

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 используется для выполнения операции свертки над изображением:

1.  l1_feature_map = conv(img, l1_filter)   

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

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 проверяет, имеют ли канал и фильтр глубину. Если они есть, внутренний оператор 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()  

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

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]))

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

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-else:

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_. Это просто для того, чтобы облегчить исследование кода. Вот реализация функции 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. На рисунке ниже показана карта объектов, возвращаемая таким сверточным слоем.

Вывод сверточного слоя будет применен к слою ReLU.

4. Слой ReLU

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

l1_feature_map_relu = relu(l1_feature_map)

Функция 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. Вывод слоя ReLU показан на рисунке ниже.

Выходные данные слоя ReLU будут переданы на уровень максимального пула.

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

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

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

Слой максимального пула реализуется с помощью функции пула следующим образом:

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  

Функция принимает три входных данных: выходные данные слоя 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]))  

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

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

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

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

На данный момент завершена архитектура CNN со слоями свертки, ReLU и максимального объединения. В дополнение к ранее упомянутым слоям, другие слои могут быть объединены в стопку для углубления сети.

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")  

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

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")  

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

Но имейте в виду, что выходные данные каждого предыдущего уровня являются входными данными следующего слоя, например, следующий код принимает предыдущий вывод в качестве своих входных данных.

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.com/Ahmed F Cafe/N…

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

Оригинальная ссылка:Ууху. См. nuggets.com/2018/04/no i…