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

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

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

Итак, начнём!

Как и ранее, мы будем изучать последовательности пар картинка/маска. На картинке в разных четвертях выбранных случайно будем размещать эллипс случайного размера и четырёхугольник тоже произвольного размера и оба они окрашены в один цвет, тоже случайно выбранный их двух. Во второй оставшийся цвет окрасим фон. Размеры и эллипса и четырёхугольника конечно же ограничены.

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

Примеры случайных 10



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



для сегментации возьмём те же самые программы расчёта метрики и потерь и ту же простую U-net, только не будем использовать Dropout.

Библиотеки
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import NoNorm
%matplotlib inline

import math
from tqdm import tqdm
#from joblib import Parallel, delayed

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
from keras.models import load_model

import tensorflow as tf
import keras as keras

w_size = 128
train_num = 10000


radius_min = 10
radius_max = 30


Функции метрики и потерь
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 = Conv2D(start_neurons * 1, (2, 2),
                   strides=(2, 2), activation="relu", padding="same")(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 = Conv2D(start_neurons * 1, (2, 2),
                   strides=(2, 2), activation="relu", padding="same")(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 = Conv2D(start_neurons * 1, (2, 2),
                   strides=(2, 2), activation="relu", padding="same")(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 = Conv2D(start_neurons * 1, (2, 2),
                   strides=(2, 2), activation="relu", padding="same")(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

input_layer = Input((w_size, w_size, 1))
output_layer = build_model(input_layer, 27)
model = Model(input_layer, output_layer)
model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric])

model.summary()


Программа генерации картинок и масок — истинной и фальшивой. В массив помещается первым слоем картинка, вторым истинная маска и третьим слоем ложная маска.

def next_pair_f(idx):
    img_l = np.ones((w_size, w_size, 1), dtype='float')*0.45
    img_h = np.ones((w_size, w_size, 1), dtype='float')*0.55
    img = np.zeros((w_size, w_size, 3), 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[:,:,1] = 0.
    img[rr_p, cc_p,1] = 1.

    img[:,:,2] = 0.
    p_f = np.random.sample()*1000.
    if p_f > 10:
        img[rr_p, cc_p,2] = 1.
    else:
        img[rr, cc,2] = 1.
        i_false[idx] = 1

    return img

Программа расчета шпаргалки
def make_sh(f_imgs, f_msks, val_len):
    precision = 0.85
    batch_size = 50
    
    t = tqdm()
    t_batch_size = 50
    raw_len = val_len
    
    id_train = 1
    #id_select = 1
    v_false = np.zeros((train_num), dtype='float')
    
    while True:
        if id_train == 1:
            fit = model.fit(f_imgs[m2_select>0], f_msks[m2_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)
                v_false[raw_len+kk] = val_iou
                if val_iou < precision*0.95:
                    new_img_test = 1
                    m2_select[raw_len+kk] = 1                
                    val_len += 1
                    break
            raw_len += (kk+1)
            id_train = 1

        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 raw_len >= train_num:
            break
    
    t.close()
    return v_false


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

i_false = np.zeros((train_num), dtype='int')

Тут находится индикатор ложности маски. Если 1, то маска из F_msks не совпадает с маской из f_msks. Это индикатор того, что мы собственно и будем искать — ложные маски.

m2_select = np.zeros((train_num), dtype='int')

Индикатор того, что эта картинка отобрана в шпаргалку.

batch_size = 50
val_len = batch_size + 1

# i_false - false mask marked as 1
i_false = np.zeros((train_num), dtype='int')

# t_imgs, t_msks -test images and masks
_txy = [next_pair_f(idx) for idx in range(train_num)]
t_imgs = np.array(_txy)[:,:,:,:1].reshape(-1,w_size ,w_size ,1)
t_msks = np.array(_txy)[:,:,:,1].reshape(-1,w_size ,w_size ,1)

# m2_select - initial 51 pair
m2_select = np.zeros((train_num), dtype='int')
for k in range(val_len):
    m2_select[k] = 1

# i_false - false mask marked as 1
i_false = np.zeros((train_num), dtype='int')
_txy = [next_pair_f(idx) 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)
# F_msks - mask array with ~1% false mask
F_msks = np.array(_txy)[:,:,:,2].reshape(-1,w_size ,w_size ,1)

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].squeeze(), cmap="gray", norm=NoNorm())
    axes[1,k].set_axis_off()
    axes[1,k].imshow(f_msks[kk].squeeze(), cmap="gray", norm=NoNorm())
plt.show(block=True)

false_num = np.arange(train_num)[i_false>0]
fig, axes = plt.subplots(3, 10, figsize=(20, 7))
for k in range(10):
    kk = np.random.randint(false_num.shape[0])
    axes[0,k].set_axis_off()
    axes[0,k].set_title(false_num[kk])
    axes[0,k].imshow(f_imgs[false_num[kk]].squeeze(), cmap="gray", norm=NoNorm())
    axes[1,k].set_axis_off()
    axes[1,k].imshow(f_msks[false_num[kk]].squeeze(), cmap="gray", norm=NoNorm())
    axes[2,k].set_axis_off()
    axes[2,k].imshow(F_msks[false_num[kk]].squeeze(), cmap="gray", norm=NoNorm())

plt.show(block=True)

Строим последовательности пар картинка/маска для тренировки и ещё одну последовательность для проверки. Т.е. проверять будем на новой, независимой последовательности в 10000 пар. Выводим на экран и визуально выборочно проверяем случайные картинки с истинной и ложной масками. Сами картинки выше показаны.

В данном конкретном случае получились 93 фальшивые маски, на которых в качестве маски отмечен эллипс, а не четырёхугольник.

Запускаем тренировку на правильном множестве, в качестве маски используем f_msks

input_layer = Input((w_size, w_size, 1))
output_layer = build_model(input_layer, 25)
model = Model(input_layer, output_layer)
model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric])
v_false = make_sh(f_imgs, f_msks, val_len)

t_pred = model.predict(t_imgs,batch_size=batch_size)
print (get_iou_vector(t_msks,t_pred.reshape(-1,w_size ,w_size ,1)))

Accuracy 0.9807 loss 0.0092 selected img   404 tested img 10000 : : 1801it [08:13,  3.65it/s]
0.9895299999999841

Шпаргалка получилась всего в 404 картинки и на независимой тестовой последовательности получили приемлемую точность.

Теперь заново компилируем сеть и тренируем на той же самой обучающей последовательности, но в качестве масок подаем на вход F_msks с 1% ложных масок

input_layer = Input((w_size, w_size, 1))
output_layer = build_model(input_layer, 25)
model = Model(input_layer, output_layer)
model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric])
v_false = make_sh(f_imgs, F_msks, val_len)

t_pred = model.predict(t_imgs,batch_size=batch_size)
print (get_iou_vector(t_msks,t_pred.reshape(-1,w_size ,w_size ,1)))

Accuracy 0.9821 loss 0.0324 selected img   727 tested img 10000 : : 1679it [25:44,  1.09it/s]
0.9524099999999959

Получили шпаргалку в 727 картинок, что существенно больше и точность предсказаний тестовой, той же самой, что в предыдущем тесте последовательности, снизилась с 0.98953 до 0.9525. Мы добавили лжи в тренировочную последовательность меньше чем на 1%, всего 93 маски из 10000 были ложью, но результат ухудшился на 3.7%. И это уже не просто ложь, это самое настоящее коварство! И шпаргалка увеличилась с всего 404 до уже 727 картинок.

Успокаивает и радует только одно

print (len(set(np.arange(train_num)[m2_select>0]).intersection(set(np.arange(train_num)[i_false>0]))))

93

Поясню эту длинную формулу, мы берём пересечение множества выбранных в шпаргалку картинок с множеством ложных картинок и видим, что все 93 ложные картинки алгоритм выбрал в шпаргалку.

Задача упростилась существенно, это не 10000 картинок просматривать вручную, это всего 727 и вся ложь концентрировано сосредоточена тут.

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

print (len(set(np.arange(train_num)[v_false<0.01]).intersection(set(np.arange(train_num)[i_false>0]))))
89


Как видим основная часть из ложных масок 89 из 93 попала в эти маски
np.arange(train_num)[v_false<0.01].shape
(382,)

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

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

Подведём итоги


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

P.S.: Умение детектить овалы, треугольники, простые полигоны является необходимым условием создания любого ИИ, управляющего автомобилем. Не умеете искать овалы и треугольники — не найдете все дорожные знаки и уедет ваш ИИ на автомобиле не туда.

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


  1. vassabi
    13.02.2019 10:29

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

    PS: А для людей — отсеивать такие новости, которые как можно сильнее привлекают их внимание, после чего достаточно будет показать 140 таких, которые отвлекут на 10 минут каждая, — и все, день потрачен.


    1. Hardcoin
      13.02.2019 12:42

      Если снаряд есть — то да. А если нужно отсекать человеческую невнимательность и ошибки, то это не снаряд, специально делать больше ошибок при разметке назло сети не будут.


  1. roryorangepants
    13.02.2019 11:54
    +1

    1. Берем игрушечный датасет (почему-то потом обобщаем это на реальные задачи)
    2. Генерируем ошибки разметки так, чтобы они были максимально не похожи на бОльшую часть разметки
    3. Показываем, что сеть ошибается на этих картинках чаще

    Ты чёртов гений, рядовой Гамп!

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

    Поливаем водой типа этой:

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

    Готово, мы получили мусорную статью.


  1. kuza2000
    14.02.2019 08:25

    Очень интересное исследование!
    Меня тоже никогда не покидала мысль, что огромный датасет для обучение НС — это следствие его не оптимальности. Шум, ошибочные примеры, «дублирование» информации.