Как стать автором
Обновить

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

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



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

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

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

Геометрических фигур много разных, но мы будем сравнивать только треугольники, четырехугольники и пятиконечные звезды. Мы применим простой метод построения трейн последовательности — мы разделим 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 картинки, почти в четыре раза меньше, чем из множества картинок раскрашенных шумом.

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

Возможность применения этого лего примера в реальных делах очевидна, но реальные трейны и сети читателей дело самих читателей.
Теги:
Хабы:
Всего голосов 18: ↑15 и ↓3+12
Комментарии0

Публикации