Шпаргалка для искусственного интеллекта — выбрось лишнее, учи главному. Техника обработки обучающих последовательностей

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



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

Недавно закончились соревнования на kaggle по поиску судов на море. Компания Airbus предлагала провести анализ космических снимков моря как с судами так и без. Всего 192555 картинок 768х768х3 — это 340 720 680 960 байт если uint8 и это громадный объем информации и возникло смутное подозрение, что не все картинки нужны для обучения сети и в таком количестве информации очевидны повторы и избыточность. При обучении сети принято некоторую часть данных отделять и не использовать в обучении, а использовать для проверки качества обучения. И если один и тот же участок моря попал на два разных снимка и при этом один снимок попал в тренировочную последовательность, а другой в проверочную, то проверка смысл потеряет и сеть переобучится, мы не проверим свойство сети обобщать информацию, ведь данные те же самые. Борьба с эти явлением отняла много сил и времени GPU участников. Как обычно, победители и призеры не торопятся показать своим поклонникам секреты мастерства и выложить код и нет возможности его изучить и поучиться, поэтому займемся теорией.

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









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

В первой статье мы рассматривали картинки с эллипсами и помехами, так и продолжим их изучать. Преимущество такого подхода в том, что если найти какую либо привлекательную особенность сети, обученной на произвольном множестве картинок, то не понятно, это свойство сети или свойство обучающего множества. Статистические параметры последовательностей, взятых из реального мира, неизвестны. Недавно грандмастер Плесков Павел paske57 рассказывал как иногда легко выиграть соревнование по сегментации/классификации картинок, если хорошо покопаться в данных самому, например посмотреть метаданные фотографий. И нет никаких гарантий, что в реальных данных таких, непроизвольно оставленных, зависимостей нет. Поэтому берем для изучения свойств сети картинки с эллипсами и прямоугольниками, и место и цвет и другие параметры определяем с помощью генератора случайных чисел компьютера (у кого это псевдослучайный генератор, у кого это генератор, основанный на других, не цифровых алгоритмах и физических свойствах вещества, Но это не будем обсуждать в этой статье).

Итак, море возьмем np.random.sample()*0.75, нам не нужны волны, ветер, берега и иные скрытые закономерности и лики. Суда/эллипсы раскрасим тоже в тот же цвет и чтобы отличать море от кораблика и помехи прибавим к морю или кораблику/помехе 0.25, и все они будут одинаковой формы — эллипсы разного размера и ориентации. Помехи тоже будут только прямоугольниками того же цвета, что и эллипс — это важно, информация и помеха одинакового цвета на фоне шума. Внесем только небольшое изменение в раскраску и будем запускать np.random.sample() для каждой картинки и для каждого эллипса/прямоугольника, т.е. не повторяются ни фон, ни цвет эллипса/прямоугольника. Дальше в тексте есть код программы создания картинок/масок и пример десяти случайно выбранных пар.

Возьмем очень распространенный вариант сети (вы можете взять свою любимую сеть) и попробуем выявить и показать избыточность большой обучающей последовательности, получить хоть какие то качественные и количественные характеристики избыточности. Т.е. автор считает, что многие гигабайты обучающих последовательностей избыточны существенно, там много ненужных картинок, нет никакой необходимости загружать десятки GPU и делать ненужные вычисления. Избыточность данных проявляется не только и не столько в том, что одинаковые части отображены на разных картинках, но и в избыточности информации в этих данных. Данные могут быть избыточны, даже если они не повторяются точно. Прошу учесть, что это не строгое определение информации и её достаточности или избыточности. Мы просто хотим выяснить, на сколько можно сократить трейн, какие картинки можно выбросить из обучающей последовательности и сколько картинок достаточно для приемлемого (точность зададим сами в программе) обучения. Это конкретная программа, конкретный датасет и возможно, что на эллипсах с треугольниками, в качестве помехи, ничего не будет работать так, как и на эллипсах с прямоугольниками (моя гипотеза: что будет всё то же и так же. Но мы сейчас это не проверяем, анализ не проводим и теорем не доказываем).

Итак, дано:

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

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

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

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

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

Для практического применения прошу учесть, что у нас и море и кораблики с помехами выбраны искусственно, это np.random.sample().

Загружаем библиотеки, определяем размеры массива картинок
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, это отличная сеть для сегментации картинок. Выбран не совсем классический вариант U-net, но идея та же, сеть производит с картинками очень простую операцию — пошагово уменьшает размерность картинки с некоторыми преобразованиями и после пытается восстановить маску из сжатого изображения. Т.е. размерность картинки в нашем случае доводится до 16x16 и далее пытаемся восстановить маску, используя данные со всех предыдущих слоев сжатия.

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

U-net с блоками
def convolution_block(x, 
                      filters, 
                      size, 
                      strides=(1,1), 
                      padding='same', 
                      activation=True):
    x = Conv2D(filters, 
               size, 
               strides=strides, 
               padding=padding)(x)
    x = BatchNormalization()(x)
    if activation == True:
        x = Activation('relu')(x)
    return x

def residual_block(blockInput, num_filters=16):
    x = Activation('relu')(blockInput)
    x = BatchNormalization()(x)
    x = convolution_block(x, num_filters, (3,3) )
    x = convolution_block(x, num_filters, (3,3), activation=False)
    x = Add()([x, blockInput])
    return x

# Build model
def build_model(input_layer, start_neurons, DropoutRatio = 0.5):

    conv1 = Conv2D(start_neurons * 1, (3, 3), 
                   activation=None,
                   padding="same"
                  )(input_layer)
    conv1 = residual_block(conv1,start_neurons * 1)
    conv1 = residual_block(conv1,start_neurons * 1)
    conv1 = Activation('relu')(conv1)
    pool1 = MaxPooling2D((2, 2))(conv1)
    pool1 = Dropout(DropoutRatio/2)(pool1)


    conv2 = Conv2D(start_neurons * 2, (3, 3), 
                   activation=None,
                   padding="same"
                  )(pool1)
    conv2 = residual_block(conv2,start_neurons * 2)
    conv2 = residual_block(conv2,start_neurons * 2)
    conv2 = Activation('relu')(conv2)
    pool2 = MaxPooling2D((2, 2))(conv2)
    pool2 = Dropout(DropoutRatio)(pool2)


    conv3 = Conv2D(start_neurons * 4, (3, 3), 
                   activation=None,
                   padding="same")(pool2)
    conv3 = residual_block(conv3,start_neurons * 4)
    conv3 = residual_block(conv3,start_neurons * 4)
    conv3 = Activation('relu')(conv3)
    pool3 = MaxPooling2D((2, 2))(conv3)
    pool3 = Dropout(DropoutRatio)(pool3)


    conv4 = Conv2D(start_neurons * 8, (3, 3),
                   activation=None,
                   padding="same")(pool3)
    conv4 = residual_block(conv4,start_neurons * 8)
    conv4 = residual_block(conv4,start_neurons * 8)
    conv4 = Activation('relu')(conv4)
    pool4 = MaxPooling2D((2, 2))(conv4)
    pool4 = Dropout(DropoutRatio)(pool4)

    # Middle
    convm = Conv2D(start_neurons * 16, (3, 3),
                   activation=None,
                   padding="same")(pool4)
    convm = residual_block(convm,start_neurons * 16)
    convm = residual_block(convm,start_neurons * 16)
    convm = Activation('relu')(convm)
    

    deconv4 = Conv2DTranspose(start_neurons * 8, (3, 3),
                              strides=(2, 2),
                              padding="same")(convm)
    uconv4 = concatenate([deconv4, conv4])
    uconv4 = Dropout(DropoutRatio)(uconv4)
    
    uconv4 = Conv2D(start_neurons * 8, (3, 3),
                    activation=None,
                    padding="same")(uconv4)
    uconv4 = residual_block(uconv4,start_neurons * 8)
    uconv4 = residual_block(uconv4,start_neurons * 8)
    uconv4 = Activation('relu')(uconv4)
    

    deconv3 = Conv2DTranspose(start_neurons * 4, (3, 3),
                              strides=(2, 2),
                              padding="same")(uconv4)
    uconv3 = concatenate([deconv3, conv3])    
    uconv3 = Dropout(DropoutRatio)(uconv3)
    
    uconv3 = Conv2D(start_neurons * 4, (3, 3),
                    activation=None, 
                    padding="same")(uconv3)
    uconv3 = residual_block(uconv3,start_neurons * 4)
    uconv3 = residual_block(uconv3,start_neurons * 4)
    uconv3 = Activation('relu')(uconv3)


    deconv2 = Conv2DTranspose(start_neurons * 2, (3, 3),
                              strides=(2, 2),
                              padding="same")(uconv3)
    uconv2 = concatenate([deconv2, conv2])
        
    uconv2 = Dropout(DropoutRatio)(uconv2)
    uconv2 = Conv2D(start_neurons * 2, (3, 3),
                    activation=None,
                    padding="same")(uconv2)
    uconv2 = residual_block(uconv2,start_neurons * 2)
    uconv2 = residual_block(uconv2,start_neurons * 2)
    uconv2 = Activation('relu')(uconv2)
    
 
    deconv1 = Conv2DTranspose(start_neurons * 1, (3, 3),
                              strides=(2, 2),
                              padding="same")(uconv2)
    uconv1 = concatenate([deconv1, conv1])
    
    uconv1 = Dropout(DropoutRatio)(uconv1)
    uconv1 = Conv2D(start_neurons * 1, (3, 3),
                    activation=None,
                    padding="same")(uconv1)
    uconv1 = residual_block(uconv1,start_neurons * 1)
    uconv1 = residual_block(uconv1,start_neurons * 1)
    uconv1 = Activation('relu')(uconv1)
    
    uconv1 = Dropout(DropoutRatio/2)(uconv1)
    output_layer = Conv2D(1, (1,1),
                          padding="same",
                          activation="sigmoid")(uconv1)
    
    return output_layer

# model
input_layer = Input((w_size, w_size, 3))
output_layer = build_model(input_layer, 16)
model = Model(input_layer, output_layer)
model.compile(loss=bce_dice_loss, optimizer="adam", metrics=[my_iou_metric])

model.summary()


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

def next_pair():
    img_l = (np.random.sample((w_size, w_size, 3))*
             0.75).astype('float32')
    img_h = (np.random.sample((w_size, w_size, 3))*
             0.75 + 0.25).astype('float32')
    img = np.zeros((w_size, w_size, 4), dtype='float')

    p = np.random.sample() - 0.5
    r = np.random.sample()*(w_size-2*radius_max) + radius_max
    c = np.random.sample()*(w_size-2*radius_max) + radius_max
    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
    )

    p1 = np.rint(np.random.sample()*
                 (w_size-2*radius_max) + radius_max)
    p2 = np.rint(np.random.sample()*
                 (w_size-2*radius_max) + radius_max)
    p3 = np.rint(np.random.sample()*
                 (2*radius_max - radius_min) + radius_min)
    p4 = np.rint(np.random.sample()*
                 (2*radius_max - radius_min) + radius_min)


    poly = np.array((
        (p1, p2),
        (p1, p2+p4),
        (p1+p3, p2+p4),
        (p1+p3, p2),
        (p1, p2),
    ))
    rr_p, cc_p = polygon(poly[:, 0], poly[:, 1], img_l.shape)

    in_sc_rr = list(set(rr) & set(rr_p))
    in_sc_cc = list(set(cc) & set(cc_p))

    if len(in_sc_rr) > 0 and len(in_sc_cc) > 0:
        if len(in_sc_rr) > 0:
            _delta_rr = np.max(in_sc_rr) - np.min(in_sc_rr) + 1
            if np.mean(rr_p) > np.mean(in_sc_rr):
                poly[:,0] += _delta_rr
            else:
                poly[:,0] -= _delta_rr
        
        if len(in_sc_cc) > 0:
            _delta_cc = np.max(in_sc_cc) - np.min(in_sc_cc) + 1
            if np.mean(cc_p) > np.mean(in_sc_cc):
                poly[:,1] += _delta_cc
            else:
                poly[:,1] -= _delta_cc
   
    rr_p, cc_p = polygon(poly[:, 0], poly[:, 1], img_l.shape)

    if p > 0:
        img[:,:,:3] = img_l.copy()
        img[rr, cc,:3] = img_h[rr, cc]
        img[rr_p, cc_p,:3] = img_h[rr_p, cc_p]
    else:
        img[:,:,:3] = img_h.copy()
        img[rr, cc,:3] = img_l[rr, cc]
        img[rr_p, cc_p,:3] = img_l[rr_p, cc_p]
        
    img[:,:,3] = 0.
    img[rr, cc,3] = 1.

    return img

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

_txy = [next_pair() for idx in range(train_num)]
f_imgs = np.array(_txy)[:,:,:,:3].reshape(-1,w_size ,w_size ,3)
f_msks = np.array(_txy)[:,:,:,3:].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.8636 loss 0.0666 : : 47it [00:29, 5.82it/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.9830 loss 0.0287 selected img   271 tested img  9949 : : 1563it [14:16,  1.01it/s]

Здесь accuracy используется в смысле «точность», а не как стандартная метрика keras и для вычисления точности используется подпрограмма «my_iou_metric». Очень интересно наблюдать точность и количество исследованных и добавленных картинок. В начале почти все пары картинка/маска сеть добавляет и где то около 70 начинает уже выбрасывать. Ближе к 8000 выбрасывает почти все пары.

Проверим визуально случайные пары выбранные сетью:

fig, axes = plt.subplots(2, 10, figsize=(20, 5))
t_imgs = f_imgs[m0_select>0]
t_msks = f_msks[m0_select>0]
for k in range(10):
    kk = np.random.randint(t_msks.shape[0])
    axes[0,k].set_axis_off()
    axes[0,k].imshow(t_imgs[kk])
    axes[1,k].set_axis_off()
    axes[1,k].imshow(t_msks[kk].squeeze())

Ничего особенного или сверхъестественного:



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

Проверка результата и точности


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

sh = np.arange(train_num)
np.random.shuffle(sh)
f0_imgs = f_imgs[sh]
f0_msks = f_msks[sh]

model.compile(loss=bce_dice_loss, optimizer="adam", metrics=[my_iou_metric])
model.summary()

Код тренировки
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(f0_imgs[m0_select>0], f0_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.8636 loss 0.0710 : : 249it [01:03,  5.90it/s]

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(f0_imgs[m0_select>0], f0_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.9890 loss 0.0224 selected img   408 tested img  9456 : : 1061it [21:13,  2.16s/it]


Результат не зависит существенно от порядка пар исходной последовательности. В предыдущем случае сеть выбрала 271, сейчас 408, если еще перемешать, сеть может выбрать другое количество. Не станем проверять, автор считает, что всегда будет существенно меньше, чем 10000.

Проверим точность предсказания сети на новой независимой последовательности

_txy = [next_pair() for idx in range(train_num)]
test_imgs = np.array(_txy)[:,:,:,:3].reshape(-1,w_size ,w_size ,3)
test_msks = np.array(_txy)[:,:,:,3:].reshape(-1,w_size ,w_size ,1)
del(_txy)
test_pred_0 = model.predict(test_imgs)
t_val_0 = get_iou_vector(test_msks,test_pred_0)
t_val_0

0.9927799999999938


Итоги и выводы


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

Всего в 271 паре картинка/маска содержится информация для предсказания 10000 пар с точностью не меньше 0.8075 на каждой паре, т.е суммарная точность по всей последовательности выше, но и на каждой картинке она не меньше чем 0.8075, у нас нет картинок которые мы не можем предсказать и мы знаем нижнюю границу этого предсказания. ( тут конечно автор прихвастнул, как же без этого, в статье нет ни проверки этого утверждения, про 0.8075, ни доказательства, но скорее всего это верно)

Для обучения сети нет необходимости нагружать GPU всем, что попало под руку, можно вытащить такое вот ядро трейна и на нем обучить сеть, как начало обучения. По мере получения новых картинок можно вручную разметить те, что сеть не смогла предсказать и их добавить в ядро трейна, заново переобучив сеть, что бы выжать из новых картинок всю информацию. И не нужно выделять валидационную последовательность, можно считать, что всё остальное, кроме выбранной и есть валидационная последовательность.

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

Ну и небольшое замечание про фолды, многие специалисты и kaggler-ы делят тренировочную последовательности на фолды и тренируют их отдельно, объединяя в дальнейшем хитрыми способами полученные результаты. В нашем случае можно точно также разделять на фолды, если из 10000 изъять 271 пару, то на оставшихся можно точно также создать новую корневую последовательность, которая очевидно предоставит другой, но сопоставимый результат. Можно и просто перемешать и взять другие начальные 11, как показано выше.

В статье предоставлен код и показан способ тренировки U-net для сегментации картинок. Это конкретный пример и в статье намеренно нет никаких обобщений на другие сети, на другие последовательности, нет суровой математики, всё рассказано и показано «на пальцах». Просто пример того, как можно учить сеть и при этом достичь приемлемой точности.
  • +11
  • 4,9k
  • 4
Поделиться публикацией

Комментарии 4

    +1

    Вы эту реализацию U-net сами написали или взяли откуда-то? Есть некоторые вопросы:


    x = Conv2D(filters, 
               size, 
               strides=strides, 
               padding=padding)(x)
    x = BatchNormalization()(x)

    Почему в свёртке не указано use_bias = False? По-идее он бесполезным будет, если сразу после неё используется нормализация. Или оно вообще не влияет на результат и производительность, и проще не писать лишнюю строчку?


    Зачем после макс-пулинга используется dropout? Разве батч-нормализации недостаточно? От него в данном случае есть какой-нибудь ощутимый эффект? Зачем у dropout разный rate на разных мастштабах? Из каких соображений это получается?


    И ещё — в каждом residual блоке выход после последней свёртки нормализуется. Из-за этого получается, что каждый блок делает примерно одинаковый вклад, даже если, например, предыдущие слои уже всё распознали и вклад от текущего блока не особо нужен. Не получится ли, что без нормализации в конце каждого блока сеть обучится лучше?


    deconv4 = Conv2DTranspose(start_neurons * 8, (3, 3),
                              strides=(2, 2),
                              padding="same")(convm)

    Почему для увеличения используется транспонированная свёртка с ядром 3*3? Будет же проблема с шахматным рисунком.


    Просто в оригинальной статье нет ни слова про dropout, батч-нормализацию, residual-блоки, а для увеличения используется "up-convolution 2x2".


    Общей идеей архитектура вроде бы похожа на U-net, но, как мне кажется, стоило хотябы упомянуть отличия — а лучше объяснить, почему сделанно именно таким образом, хотя бы на уровне "батч-нормализация точно нужна" или "dropout просто потому что так захотелось".

      –1
      сеть выбрана из кернелов каггла и на тот момент была весьма популярна.
      0
      Интересно, а на реальных данных эта гипотеза работает?

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

      По-моему, в этом соревновании была проблема с первоначальными данными: в тестовых данных можно было напрямую угадать участок из тренировочных и некоторые участники предсказывали с точностью 100%) Не знаю, чем закончилось, вроде обновили данные в ходе соревнования…
        –1
        Данные там обновляли.
        На реальных данных очень долго считать, на тех же корабликах 10 суток заняла тренировка.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое