Pull to refresh

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

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

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

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

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

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

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

Примеры случайных 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 фальшивые маски, на которых в качестве true positive отмечен эллипс, а не четырёхугольник.

Запускаем тренировку на правильном множестве, в качестве маски используем 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. Умение детектить овалы, треугольники, простые полигоны является необходимым условием создания любого ИИ, управляющего автомобилем. Не умеете искать овалы и треугольники — не найдете все дорожные знаки и уедет ваш ИИ на автомобиле не туда.
Tags:
Hubs:
Total votes 19: ↑12 and ↓7+5
Comments4

Articles