Что ж, не так давно меня пригласили на собеседование. Я решил приложить все усилия, и то, что начиналось как простой код на Pycharm, а затем как усовершенствованный блокнот Jupyter, теперь станет небольшой статьей.

По сути, задача заключалась в создании простой нейронной сети для обнаружения объектов (в нашем случае - эллипсов). Не было никаких ограничений ни относительно рабочего фрейма (Tensorflow, Keras, Pytorch…), ни используемых модулей (Opencv, pandas, numpy).

Первая часть заключалась в создании следующего набора данных:

  1. Все изображения должны иметь один эллипс или вообще не иметь эллипса.
  2. У каждого эллипса будет случайный центр, случайная малая и большая оси, случайная угловая ориентация и случайный цвет.
  3. Фон белый.
  4. Изображения будут 64 x 64.
  5. Создайте функцию next_batch, возвращающую пакет изображений и вектор метки, содержащий для каждого изображения наличие elipse (1 или 0) и параметры, упомянутые в разделе 2 (помимо цвета).

Вторая часть заключалась в создании нейронной сети, которая будет определять:

  1. Есть ли эллипс или нет.
  2. Центр эллипса.
  3. Оси, ориентация - здесь не решаются.

Итак, давайте сначала представим импортированные пакеты:

import numpy as np
import tensorflow as tf
import cv2
from matplotlib import pyplot as plt
import pandas as pd
from PIL import Image 

from skimage import color

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

img = np.full((64,64,3), 255, dtype=np.uint8)
center_x = np.random.randint(10,54)
center_y = np.random.randint(10,54)
major_axis = np.random.randint(1,9)
minor_axis= np.random.randint(1,9)
angle = np.random.randint(0,360)
B = np.random.randint(0,254)
G = np.random.randint(0,254)
R = np.random.randint(0,254)
a = cv2.ellipse(img, (center_x, center_y), (major_axis, minor_axis), angle, 0, 360, (B, G, R), -1)
plt.imshow(a)
a.shape

Результат этого скрипта должен выглядеть примерно так:

Давайте теперь создадим функцию с учетом рекомендаций в первой части, вход функции будет целым числом batch_size. и вывод будет:

  1. Массив, половина из которых (batch_size) изображений содержит эллипсы, а другая половина (batch_size) содержит белые (пустые) изображения.
  2. Массив с метками для каждого изображения в предыдущем массиве (2 координаты для центра эллипса и метка «0 или 1», если эллипс есть или его нет)

Код :

def next_batch(batch_size):
    elipse = np.ndarray(shape=(batch_size,64,64,3), dtype=np.uint8)
    labels = np.ndarray(shape=(batch_size,2), dtype=np.uint8)
    for j in range(batch_size):
        img = np.full((64,64,3), 255, dtype=np.uint8)
        center_x = np.random.randint(10,54)
        center_y = np.random.randint(10,54)
        center = np.array([center_x, center_y])
        major_axis = np.random.randint(1,9)
        minor_axis=np.random.randint(1,9)
        angle = np.random.randint(0,360)
        B = np.random.randint(0,255)
        G = np.random.randint(0,255)
        R = np.random.randint(0,255)
        my_elipse = cv2.ellipse(img, (center_x, center_y), (major_axis, minor_axis), angle, 0, 360, (B, G, R), -1)
        elipse[j,:,:,:] = my_elipse
        labels[j,:] = center
    no_elipse = np.full((batch_size ,64,64,3), 255, dtype=np.uint8) # image of 64x64 with white background
    no_elipse_labels = np.zeros(shape=(batch_size,2), dtype=np.uint8)
    # classification labels
    class_elipse_labels = np.ones(shape=(batch_size,1), dtype=np.uint8)
    class_no_elipse_labels = np.zeros(shape=(batch_size,1), dtype=np.uint8)
    class_labels = np.concatenate([class_elipse_labels, class_no_elipse_labels])
    labels = np.concatenate([labels,no_elipse_labels])
    labels = np.concatenate([labels, class_labels], axis=1)
    return np.concatenate([elipse,no_elipse]), labels
batch_size = 64
images, labels = next_batch(batch_size)

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

plt.imshow(images[62,:,:,:])
labels[62,2], images.shape, labels.shape

Отлично, у нас есть пакетная функция. на выходе получается пакет эллипсов с 3-мя цветами каналов!
Он также дает нам центр эллипса (сейчас я сосредоточусь на этих параметрах, позже мы добавим оси, ориентацию ...)
Итак наши данные представлены таким образом, что CNN может их получить.

А теперь перейдем ко второй части, модели:

num_hidden = 64 # hidden nodes
image_size = 64 #
num_channels = 3
alpha = 0.95
num_labels = 3

graph = tf.Graph()

with graph.as_default():

  # Input data.
  tf_train_dataset = tf.placeholder(tf.float32, shape=(batch_size*2, image_size, image_size, num_channels))
  tf_train_labels = tf.placeholder(tf.float32, shape=(batch_size*2, num_labels))
  tf_valid_dataset = tf.placeholder(tf.float32, shape=(batch_size*2, image_size, image_size, num_channels))
  tf_valid_labels = tf.placeholder(tf.float32, shape=(batch_size*2, num_labels))
  tf_test_dataset = tf.placeholder(tf.float32, shape=(batch_size*2, image_size, image_size, num_channels))
  tf_test_labels = tf.placeholder(tf.float32, shape=(batch_size*2, num_labels))


  #tf_test_labels= tf.Variable(test_labels)

  def model(data):    
      conv11 = tf.contrib.layers.conv2d(data, num_outputs=1, kernel_size=(1,1),activation_fn=None)
      conv11_2 = tf.contrib.layers.conv2d(tf_train_dataset, num_outputs=1, kernel_size=(1,1),activation_fn=tf.sigmoid)
      drop = tf.nn.dropout(conv11_2, 0.5)
      conv11_2flat = tf.layers.flatten(drop)
      dense1 = tf.layers.dense(conv11_2flat, 128, activation = tf.nn.relu)
      dense2 = tf.layers.dense(dense1, 2)
      return dense2
  def model_class(data):
      data = tf.layers.flatten(data)
      dense1 = tf.layers.dense(data, 128)
      dense2 = tf.layers.dense(dense1, 1, use_bias=True, activation = tf.sigmoid)
      dense2 = tf.transpose(dense2)
      return dense2


  #Loss definition
  logits = model(tf_train_dataset)
  logits_class = model_class(tf_train_dataset)
    
  loss1 = tf.reduce_mean(tf.reduce_sum((tf_train_labels[:,:2]-logits[:,:2])**2,axis=1)) 
  loss2 = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=tf_train_labels[:,2], logits=logits_class))
  loss = (alpha * loss1) + ((1-alpha) * loss2) 
    

   

  # Optimizer
  optimizer = tf.train.AdamOptimizer(0.001).minimize(loss)

Хорошо, я использовал covnet, так как с ядрами легче находить формы \ геометрические объекты.
Чтобы упростить вычисления, я использовал ядра 1x1 (установка медленная).
В какой-то момент я вызвал выпадение, так как я заметил, что когда я пытаюсь запустить алгоритм со случайными партиями, потери застревают, однако, когда я дважды пытался запустить только одну партию, потери были чрезвычайно низкими (менее 3), поэтому отсев мог решить проблему переобучения.
MSE было разумным выбором для измерения расстояния до прогнозируемых центров эллипсов. Функция потерь - это комбинированная функция для линейной регрессии (центры) и классификации (наличие эллипса)
Вдобавок к этому была произведена простая предварительная обработка вычитания 255 из изображений, чтобы простота вычислений (представлена ​​в следующем фрагменте кода).

num_steps = 20001
    
with tf.Session(graph=graph) as session:
  tf.global_variables_initializer().run()
  print('Initialized weights ,biases and other variables')
  for step in range(num_steps):
    batch_data, batch_labels = next_batch(batch_size)
#preprocessing for image
    batch_data = 255-batch_data
    feed_dict = {tf_train_dataset : batch_data, tf_train_labels : batch_labels}
    _, l, predictions, elipse = session.run(  [optimizer, loss, logits, logits_class], feed_dict=feed_dict)
    elipse.shape
    if (step % 200 == 0):
      print('Training batch loss at step %d: %f' % (step, l))
      print('ground truth of elipse center is', batch_labels[2, :2])
      print('Prediction of elipse centers is:', predictions[2,:])
      print('ground truth if elipse is:' ,batch_labels[2, 2])
      print('prediction if elipse or not', elipse[:, 2], elipse[:, 100])  
  test_data, test_label = next_batch(batch_size)
  #preprocessing for image
  test_data = 255-test_data
  feed_dict={tf_train_dataset:test_data, tf_train_labels : test_label}
  predictions, elipse = session.run(  [logits, logits_class], feed_dict=feed_dict)

Результат был повторен, вот его фрагмент во время обучения:

Training batch loss at step 6400: 66.561447
ground truth of elipse center is [22 31]
Prediction of elipse centers is: [19.771732 30.636047]
ground truth if elipse is: 1
prediction if elipse or not [1.] [0.0003532]

У нас потеря 19, я успел достичь потери 14 раньше, но не смог ее воспроизвести.
Сходимость, кажется, происходит около шага 10 000.
Здесь нет эпох, так как это случайная партия (бесконечный обучающий набор).
Размер пакета составляет 128 (64 эллипса и 64 пустых кадра), я выбрал произвольный размер
Давайте посмотрим на результаты для случайного эллипса.

plt.imshow(test_data[62,:,:,:]+255)
predictions[62, :],elipse[:,62]

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

Я забыл упомянуть, что для пустых картинок «центры» в векторе меток равны 0, хотя нас это не особо заботит.

Что касается пустой картинки:

elipse[elipse<0.5] = 0 # thresholding the sigmoid
plt.imshow(test_data[70,:,:,:]+255)
predictions[70, :],elipse[:,70]

Следующим шагом будет поиск главной и второстепенной осей, я этого не реализовал, но моим решением было бы добавить ядра большего размера, чтобы захватить весь размер осей и реализовать так же, как и центры.
Что касается поворота, то эта часть немного сложна, поскольку поворот на 180 и поворот на 360 одинаковы.
Возможным решением было бы получить cos и sin поворота и угла и получить 2 особенности для одного угла.
Таким образом, каждый угол будет отличаться, и мы сможем найти его более легким способом ...

Это все для этой записи в блоге. Надеюсь, вам понравилось.

Для комментариев: [email protected]