Простота и cложность примитивов или как определить ненужный препроцессинг для нейронной сети

что сложнее для искусственного интеллекта, треугольник или четырехугольник?

https://habr.com/ru/post/439122/

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



      Попробуем сравнить, и для сравнения у нас есть отличная, проверенная поколениями студентов, идея — чем короче шпаргалка, тем легче экзамен.

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

      Итак, попробуем выяснить, какая геометрическая фигура сложнее или проще для сегментации, какой курс лекций для ИИ понятней и лучше усваивается.

      Геометрических фигур много разных, но мы будем сравнивать только треугольники, четырехугольники и пятиконечные звезды. Мы применим простой метод построения трейн последовательности — мы разделим 128х128 одноцветной картинки на четыре части и случайным образом будем помещать в эти четверти эллипс и, например, треугольник. Будем детектить треугольник того же цвета, что и эллипс. Т.е. задача состоит в том, что бы обучить сеть отличать, например четырехугольный полигон от эллипса, окрашенного в тот же цвет. Вот примеры картинок, которые будем изучать







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

      Возьмем для исследования классическую U-net и три вида обучающих последовательностей с треугольниками, четырехугольниками и звездами.

      Итак, дано:

      • три обучающие последовательности пар картинка/маска;
      • сеть. Обыкновенная U-net, которая широко используются для сегментации.

      Идея для проверки:

      • определим, какая из обучающих последовательностей «сложнее» для обучения;
      • как влияют на обучение некоторые приемы предобработки.

      Начнем, выберем 10 000 пар картинок четырехугольников с эллипсами и масок и рассмотрим их внимательно. Нам интересно, насколько короткой получится шпаргалка и от чего её длина зависит.

      Загружаем библиотеки, определяем размеры массива картинок
      import numpy as np
      import matplotlib.pyplot as plt
      %matplotlib inline
      import math
      from tqdm import tqdm
      
      from skimage.draw import ellipse, polygon
      
      from keras import Model
      from keras.optimizers import Adam
      from keras.layers import Input,Conv2D,Conv2DTranspose,MaxPooling2D,concatenate
      from keras.layers import BatchNormalization,Activation,Add,Dropout
      from keras.losses import binary_crossentropy
      from keras import backend as K
      
      import tensorflow as tf
      import keras as keras
      
      w_size = 128
      train_num = 10000
      
      radius_min = 10
      radius_max = 20

      определяем функции потерь и точности
      def dice_coef(y_true, y_pred):
          y_true_f = K.flatten(y_true)
          y_pred = K.cast(y_pred, 'float32')
          y_pred_f = K.cast(K.greater(K.flatten(y_pred), 0.5), 'float32')
          intersection = y_true_f * y_pred_f
          score = 2. * K.sum(intersection) / (K.sum(y_true_f) + K.sum(y_pred_f))
          return score
      
      def dice_loss(y_true, y_pred):
          smooth = 1.
          y_true_f = K.flatten(y_true)
          y_pred_f = K.flatten(y_pred)
          intersection = y_true_f * y_pred_f
          score = (2. * K.sum(intersection) + smooth) / (K.sum(y_true_f) +
                       K.sum(y_pred_f) + smooth)
          return 1. - score
      
      def bce_dice_loss(y_true, y_pred):
          return binary_crossentropy(y_true, y_pred) + dice_loss(y_true, y_pred)
      
      def get_iou_vector(A, B):
          # Numpy version
          
          batch_size = A.shape[0]
          metric = 0.0
          for batch in range(batch_size):
              t, p = A[batch], B[batch]
              true = np.sum(t)
              pred = np.sum(p)
              
              # deal with empty mask first
              if true == 0:
                  metric += (pred == 0)
                  continue
              
              # non empty mask case.  Union is never empty 
              # hence it is safe to divide by its number of pixels
              intersection = np.sum(t * p)
              union = true + pred - intersection
              iou = intersection / union
              
              # iou metrric is a stepwise approximation of the real iou over 0.5
              iou = np.floor(max(0, (iou - 0.45)*20)) / 10
              
              metric += iou
              
          # teake the average over all images in batch
          metric /= batch_size
          return metric
      
      def my_iou_metric(label, pred):
          # Tensorflow version
          return tf.py_func(get_iou_vector, [label, pred > 0.5], tf.float64)
      
      from keras.utils.generic_utils import get_custom_objects
      
      get_custom_objects().update({'bce_dice_loss': bce_dice_loss })
      get_custom_objects().update({'dice_loss': dice_loss })
      get_custom_objects().update({'dice_coef': dice_coef })
      get_custom_objects().update({'my_iou_metric': my_iou_metric })
      

      Мы будем использовать метрику из первой статьи. Напомню читателям, что будем предсказывать маску пикселя — это «фон» или «четырехугольник» и оценивать истинность или ложность предсказания. Т.е. возможны следующие четыре варианта — мы правильно предсказали, что пиксель это фон, правильно предсказали, что пиксель это четырехугольник или ошиблись в предсказании «фон» или «четырехугольник». И так по всем картинкам и всем пикселям оцениваем количество всех четырех вариантов и подсчитываем результат — это и будет результат работы сети. И чем меньше ошибочных предсказаний и больше истинных, то тем точнее полученный результат и лучше работа сети.

      Мы исследуем сеть как «черный ящик», мы не станем смотреть, что происходит с сетью внутри, как меняются веса и как выбираются градиенты — заглянем в недра сети попозже, когда будем сравнивать сети.

      простая U-net
      def build_model(input_layer, start_neurons):
          # 128 -> 64
          conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(input_layer)
          conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(conv1)
          pool1 = MaxPooling2D((2, 2))(conv1)
          pool1 = Dropout(0.25)(pool1)
      
          # 64 -> 32
          conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(pool1)
          conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(conv2)
          pool2 = MaxPooling2D((2, 2))(conv2)
          pool2 = Dropout(0.5)(pool2)
      
          # 32 -> 16
          conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(pool2)
          conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(conv3)
          pool3 = MaxPooling2D((2, 2))(conv3)
          pool3 = Dropout(0.5)(pool3)
      
          # 16 -> 8
          conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(pool3)
          conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(conv4)
          pool4 = MaxPooling2D((2, 2))(conv4)
          pool4 = Dropout(0.5)(pool4)
      
          # Middle
          convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(pool4)
          convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(convm)
      
          # 8 -> 16
          deconv4 = Conv2DTranspose(start_neurons * 8, (3, 3), strides=(2, 2), padding="same")(convm)
          uconv4 = concatenate([deconv4, conv4])
          uconv4 = Dropout(0.5)(uconv4)
          uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4)
          uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4)
      
          # 16 -> 32
          deconv3 = Conv2DTranspose(start_neurons * 4, (3, 3), strides=(2, 2), padding="same")(uconv4)
          uconv3 = concatenate([deconv3, conv3])
          uconv3 = Dropout(0.5)(uconv3)
          uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3)
          uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3)
      
          # 32 -> 64
          deconv2 = Conv2DTranspose(start_neurons * 2, (3, 3), strides=(2, 2), padding="same")(uconv3)
          uconv2 = concatenate([deconv2, conv2])
          uconv2 = Dropout(0.5)(uconv2)
          uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2)
          uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2)
      
          # 64 -> 128
          deconv1 = Conv2DTranspose(start_neurons * 1, (3, 3), strides=(2, 2), padding="same")(uconv2)
          uconv1 = concatenate([deconv1, conv1])
          uconv1 = Dropout(0.5)(uconv1)
          uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1)
          uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1)
      
          uncov1 = Dropout(0.5)(uconv1)
          output_layer = Conv2D(1, (1,1), padding="same", activation="sigmoid")(uconv1)
          
          return output_layer
      # model
      
      input_layer = Input((w_size, w_size, 1))
      output_layer = build_model(input_layer, 26)
      model = Model(input_layer, output_layer)
      model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric])
      
      model.summary()
      

      Функция генерации пар картинка/маска. На черно-белой картинке 128х128 заполненной случайным шумом со случайно выбранным из двух диапазонов, или 0.0...0.75 или 0.25..1.0. Случайным образом выбираем четверть на картинке и размещаем случайно ориентированный эллипс и в другой четверти размещаем четырехугольник и одинаково раскрашиваем случайным шумом.

      def next_pair():
          img_l = (np.random.sample((w_size, w_size, 1))*
                   0.75).astype('float32')
          img_h = (np.random.sample((w_size, w_size, 1))*
                   0.75 + 0.25).astype('float32')
          img = np.zeros((w_size, w_size, 2), dtype='float')
          
          i0_qua = math.trunc(np.random.sample()*4.)
          i1_qua = math.trunc(np.random.sample()*4.)
          while i0_qua == i1_qua:
              i1_qua = math.trunc(np.random.sample()*4.)
          _qua = np.int(w_size/4)
          qua = np.array([[_qua,_qua],[_qua,_qua*3],[_qua*3,_qua*3],[_qua*3,_qua]])
          
          p = np.random.sample() - 0.5
          r = qua[i0_qua,0]
          c = qua[i0_qua,1]
          
          r_radius = np.random.sample()*(radius_max-radius_min) + radius_min
          c_radius = np.random.sample()*(radius_max-radius_min) + radius_min
          rot = np.random.sample()*360
          rr, cc = ellipse(
              r, c, 
              r_radius, c_radius, 
              rotation=np.deg2rad(rot), 
              shape=img_l.shape
          )
      
          p0 = np.rint(np.random.sample()*(radius_max-radius_min) + radius_min)
          p1 = qua[i1_qua,0] - (radius_max-radius_min)
          p2 = qua[i1_qua,1] - (radius_max-radius_min)
          
          p3 = np.rint(np.random.sample()*radius_min)
          p4 = np.rint(np.random.sample()*radius_min)
          p5 = np.rint(np.random.sample()*radius_min)
          p6 = np.rint(np.random.sample()*radius_min)
          p7 = np.rint(np.random.sample()*radius_min)
          p8 = np.rint(np.random.sample()*radius_min)
      
          poly = np.array((
              (p1, p2),
              (p1+p3, p2+p4+p0),
              (p1+p5+p0, p2+p6+p0),
              (p1+p7+p0, p2+p8),
              (p1, p2),
          ))
          rr_p, cc_p = polygon(poly[:, 0], poly[:, 1], img_l.shape)
      
          if p > 0:
              img[:,:,:1] = img_l.copy()
              img[rr, cc,:1] = img_h[rr, cc]
              img[rr_p, cc_p,:1] = img_h[rr_p, cc_p]
          else:
              img[:,:,:1] = img_h.copy()
              img[rr, cc,:1] = img_l[rr, cc]
              img[rr_p, cc_p,:1] = img_l[rr_p, cc_p]
              
          img[:,:,1] = 0.
          img[rr_p, cc_p,1] = 1.
      
          return img
      
      

      Создадим обучающую последовательность пар, посмотрим случайные 10. Напомню, что картинки монохромные, градации серого.

      _txy = [next_pair() for idx in range(train_num)]
      f_imgs = np.array(_txy)[:,:,:,:1].reshape(-1,w_size ,w_size ,1)
      f_msks = np.array(_txy)[:,:,:,1:].reshape(-1,w_size ,w_size ,1)
      del(_txy)
      # смотрим на случайные 10 с масками    
      fig, axes = plt.subplots(2, 10, figsize=(20, 5))
      for k in range(10):
          kk = np.random.randint(train_num)
          axes[0,k].set_axis_off()
          axes[0,k].imshow(f_imgs[kk])
          axes[1,k].set_axis_off()
          axes[1,k].imshow(f_msks[kk].squeeze())
      

    Первый шаг. Обучаем на минимальном стартовом множестве


    • Первый шаг нашего эксперимента простой, мы пробуем обучить сеть предсказывать всего 11 первых картинок.

      batch_size = 10
      val_len = 11
      precision = 0.85
      
      m0_select = np.zeros((f_imgs.shape[0]), dtype='int')
      
      for k in range(val_len):
          m0_select[k] = 1
      
      t = tqdm()
      while True:
          fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0],
                          batch_size=batch_size, 
                          epochs=1, 
                          verbose=0
                         )
          
          current_accu = fit.history['my_iou_metric'][0]
          current_loss = fit.history['loss'][0]
          t.set_description("accuracy {0:6.4f} loss {1:6.4f} ".\
                            format(current_accu, current_loss))
          t.update(1)
          if current_accu > precision:
              break
      t.close()

      accuracy 0.8545 loss 0.0674 lenght 11 : : 793it [00:58, 14.79it/s]

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

    Теперь начнем главный эксперимент


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

      batch_size = 50
      t_batch_size = 1024
      raw_len = val_len
      
      t = tqdm(-1)
      id_train = 0
      #id_select = 1
      
      while True:
          t.set_description("Accuracy {0:6.4f} loss {1:6.4f}\
           selected img {2:5d} tested img {3:5d} ".
                            format(current_accu, current_loss, val_len, raw_len))
          t.update(1)
      
          if id_train == 1:
              fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0],
                              batch_size=batch_size,
                              epochs=1,
                              verbose=0
                             )
          
              current_accu = fit.history['my_iou_metric'][0]
              current_loss = fit.history['loss'][0]
              if current_accu > precision:
                  id_train = 0
                  
          else:
              t_pred = model.predict(
                  f_imgs[raw_len: min(raw_len+t_batch_size,f_imgs.shape[0])],
                  batch_size=batch_size
                                    )
              for kk in range(t_pred.shape[0]):
                  val_iou = get_iou_vector(
                      f_msks[raw_len+kk].reshape(1,w_size,w_size,1),
                      t_pred[kk].reshape(1,w_size,w_size,1) > 0.5)
                  if val_iou < precision*0.95:
                      new_img_test = 1
                      m0_select[raw_len+kk] = 1                
                      val_len += 1
                      break
              raw_len += (kk+1)
              id_train = 1
          
          if raw_len >= train_num:
              break
      
      t.close()
      

      Accuracy 0.9338 loss 0.0266 selected img  1007 tested img  9985 : : 4291it [49:52,  1.73s/it]

      Здесь accuracy используется в смысле «точность», а не как стандартная метрика keras и для вычисления точности используется подпрограмма «my_iou_metric».

      А теперь сравним работу той же самой сети с теми же самыми параметрами на другой последовательности, на треугольниках



      И получим совсем другой результат

      Accuracy 0.9823 loss 0.0108 selected img  1913 tested img  9995 : : 6343it [2:11:36,  3.03s/it]

      Сеть выбрала 1913 картинок с «новой» информацией, т.е. содержательность картинок с треугольниками получается в два раза ниже, чем с четырехугольниками!

      Проверим то же самое на звездах и запустим сеть на третьей последовательности



      получим

      Accuracy 0.8985 loss 0.0478 selected img   476 tested img  9985 : : 2188it [16:13,  1.16it/s]

      Как видим, звезды оказались наиболее информативными, всего 476 картинок в шпаргалке.

      У нас появились основания судить о сложности геометрических фигур для восприятия их нейронной сетью. Самая простая это звезда, всего 476 картинок в шпаргалке, далее четырехугольник с его 1007 и самым сложным оказался треугольник — для обучения нужно 1913 картинок.

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

    Теперь о серьезном


    • На первый взгляд все эти эллипсы и треугольники кажутся баловством, куличи из песка и лего. Но вот конкретный и серьезный вопрос: если к исходной последовательности применить какую нибудь предобработку, фильтр, то как изменится сложность последовательности? Например, возьмем всё те же эллипсы и четырехугольники и применим к ним вот такую предобработку

      from scipy.ndimage import gaussian_filter
      _tmp = [gaussian_filter(idx, sigma = 1) for idx in f_imgs]
      f1_imgs = np.array(_tmp)[:,:,:,:1].reshape(-1,w_size ,w_size ,1)
      del(_tmp)
      fig, axes = plt.subplots(2, 5, figsize=(20, 7))
      for k in range(5):
          kk = np.random.randint(train_num)
          axes[0,k].set_axis_off()
          axes[0,k].imshow(f1_imgs[kk].squeeze(), cmap="gray")
          axes[1,k].set_axis_off()
          axes[1,k].imshow(f_msks[kk].squeeze(), cmap="gray")
      



      На первый взгляд всё то же самое, такие же эллипсы, такие же полигоны, но сеть стала работать совсем по-другому:

      Accuracy 1.0575 loss 0.0011    selected img  7963 tested img  9999 : : 17765it [29:02:00, 12.40s/it]

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

      Но, как видно из результата работы, простой gaussian_filter создал сети много проблем, породил много новой, и наверно лишней, информации.

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



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

      Accuracy 0.9004 loss 0.0315 selected img   251 tested img  9832 : : 1000it [06:46,  1.33it/s]
      

      Сеть вполне обошлась информацией, извлеченной из 251 картинки, почти в четыре раза меньше, чем из множества картинок раскрашенных шумом.

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

      Возможность применения этого лего примера в реальных делах очевидна, но реальные трейны и сети читателей дело самих читателей.

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

    Комментарии

    Популярные сообщения из этого блога

    Удивительное рядом

    Искусственный интеллект против лжи и коварства