Или необыкновенные свойства обычной U-net.

Одной из самых жутких проблем для любого любителя, как и для профессионала в data science является качество разметки. Качество разметки способно погубить самую толковую и красивую идею.

Но не всё оказалось так плохо и вашему вниманию предлагается, как и всегда в моих постах, красивая идея с кодами и примером.


Будем учить сеть находить круг в квадратной картинке.

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

И так у нас есть картинка с кругом, маска для обучения, явно не совпадающая с картинкой, и точная маска. Вот пример картинок.

Для экспериментов возьмем ту же самую, очень хорошо изученную U-net.

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import math
from tqdm import tqdm_notebook, tqdm

import tensorflow as tf
import keras as keras
from keras import Model
from keras.models import load_model
from keras.optimizers import Adam
from keras.layers import Input, Conv2D, Conv2DTranspose, MaxPooling2D, concatenate, Dropout
from keras.losses import binary_crossentropy
from keras.layers.core import Activation
from keras import backend as K
from keras.utils.generic_utils import get_custom_objects

from tqdm import tqdm_notebook

from math import sqrt
w_size = 128
w2_size = w_size // 2
RR = int(w2_size * 0.5)

def next_pair(k):
    
    delta = np.random.uniform(-10,10)
    circle = np.zeros((w_size, w_size,1), dtype='int')
    circle_mask = np.zeros((w_size, w_size), dtype='int')
    R = RR - (np.random.random_sample()*10)
    r_x = np.random.random_sample()*(RR//2) # - R//4
    r_y = np.random.random_sample()*(RR//2) # - R//4
    for i in range(w_size):
        for j in range(w_size):
            r = sqrt(float((i - w2_size - r_x)*(i - w2_size - r_x) +
                           (j - w2_size - r_y)*(j - w2_size - r_y)))
            # if r < (R + np.random.uniform(-20,20)):
            if r < R + delta:
                circle_mask[i,j] = 1
            if r < R:
                circle[i,j,0] = 1
    img_l = np.random.sample((w_size, w_size, 1))*0.5
    img_h = np.random.sample((w_size, w_size, 1))*0.5 + 0.5

    img = img_h.copy()
    img[circle>0] = img_l[circle > 0]
    
    msk = np.zeros((w_size, w_size, 1), dtype='float32')
    msk[circle_mask>0] = 1. # красим пиксели маски эллипса

    return img, msk, circle

Создаем картинки, маски и истинные, точные маски:

train_num = 2048
from joblib import Parallel, delayed
train = np.array(Parallel(n_jobs=4)(delayed(next_pair)(k) for k in range(train_num)))
train_x = train[:,0,:,:,:]
train_y = train[:,1,:,:,:]
train_r = train[:,2,:,:,:] # true mask

Немного модифицируем сеть, DICE точнее указывает совпадение масок:

def dice_coef(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 score

get_custom_objects().update({'dice_coef': dice_coef })

def build_model(input_layer, start_neurons):
    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)

    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)

    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)

    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)

    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)

    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)

    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)

    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)
    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, 16)
model = Model(input_layer, output_layer)
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
              loss=tf.keras.losses.BinaryCrossentropy(),
              metrics=['dice_coef'])

history = model.fit(train_x, train_y
                    ,batch_size=16
                    ,epochs=10
                    ,verbose=2
                    ,validation_split=0.2
                    ,use_multiprocessing=True
                   )

Результат не очень. Сеть отлично справляется с распознаванием областей при точной разметке. Но если разметка плохая и велика ошибка от реальной разметки, то и точность нашей сети будет не идеальной, всего 0.75. Кого-то наверно это устраивает, кому-то мало.

Epoch 1/10
103/103 - 3s - loss: 0.2875 - dice_coef: 0.4463 - val_loss: 0.1805 - val_dice_coef: 0.6473
Epoch 2/10
103/103 - 3s - loss: 0.1296 - dice_coef: 0.7414 - val_loss: 0.1174 - val_dice_coef: 0.7337
Epoch 3/10
103/103 - 3s - loss: 0.1162 - dice_coef: 0.7539 - val_loss: 0.1132 - val_dice_coef: 0.7482
Epoch 4/10
103/103 - 3s - loss: 0.1112 - dice_coef: 0.7603 - val_loss: 0.1091 - val_dice_coef: 0.7657
Epoch 5/10
103/103 - 3s - loss: 0.1085 - dice_coef: 0.7617 - val_loss: 0.1331 - val_dice_coef: 0.7584
Epoch 6/10
103/103 - 3s - loss: 0.1088 - dice_coef: 0.7605 - val_loss: 0.1080 - val_dice_coef: 0.7599
Epoch 7/10
103/103 - 3s - loss: 0.1064 - dice_coef: 0.7635 - val_loss: 0.1067 - val_dice_coef: 0.7573
Epoch 8/10
103/103 - 3s - loss: 0.1062 - dice_coef: 0.7638 - val_loss: 0.1069 - val_dice_coef: 0.7587
Epoch 9/10
103/103 - 3s - loss: 0.1054 - dice_coef: 0.7649 - val_loss: 0.1068 - val_dice_coef: 0.7617
Epoch 10/10
103/103 - 3s - loss: 0.1058 - dice_coef: 0.7644 - val_loss: 0.1087 - val_dice_coef: 0.7616
1

Но нас интересует другое, гораздо более интересное явление. Сравним нашу разметку и полученное предсказание.

pred = model.predict(train_x)
dice_coef(train_y.astype('float32'), pred).numpy()
  0.7675821
dice_coef(train_r.astype('float32'), pred).numpy()
  0.819669

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

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

model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
              loss=tf.keras.losses.BinaryCrossentropy(),
              metrics=['dice_coef'])
history = model.fit(train_x, pred>0.5
                    ,batch_size=16
                    ,epochs=10
                    ,verbose=2
                    ,validation_split=0.2
                    ,use_multiprocessing=True
                   )
Epoch 1/10
103/103 - 3s - loss: 0.0045 - dice_coef: 0.9899 - val_loss: 0.0021 - val_dice_coef: 0.9944
Epoch 2/10
103/103 - 3s - loss: 0.0030 - dice_coef: 0.9933 - val_loss: 0.0017 - val_dice_coef: 0.9953
Epoch 3/10
103/103 - 3s - loss: 0.0027 - dice_coef: 0.9941 - val_loss: 0.0016 - val_dice_coef: 0.9958
Epoch 4/10
103/103 - 3s - loss: 0.0024 - dice_coef: 0.9946 - val_loss: 0.0016 - val_dice_coef: 0.9960
Epoch 5/10
103/103 - 3s - loss: 0.0022 - dice_coef: 0.9951 - val_loss: 0.0014 - val_dice_coef: 0.9963
Epoch 6/10
103/103 - 3s - loss: 0.0021 - dice_coef: 0.9953 - val_loss: 0.0014 - val_dice_coef: 0.9963
Epoch 7/10
103/103 - 3s - loss: 0.0021 - dice_coef: 0.9955 - val_loss: 0.0014 - val_dice_coef: 0.9965
Epoch 8/10
103/103 - 3s - loss: 0.0020 - dice_coef: 0.9957 - val_loss: 0.0014 - val_dice_coef: 0.9965
Epoch 9/10
103/103 - 3s - loss: 0.0019 - dice_coef: 0.9958 - val_loss: 0.0013 - val_dice_coef: 0.9967
Epoch 10/10
103/103 - 3s - loss: 0.0019 - dice_coef: 0.9959 - val_loss: 0.0014 - val_dice_coef: 0.9965

И тут получаем просто фантастический результат - точность больше 0.99!

А конкретно достигаем dice_coeff 0.9919946, имея на руках дрянную разметку в начале.

pred_1 = model.predict(train_x)
dice_coef(train_r.astype('float32'), pred_1).numpy()
  0.9919946

Итак мы получили следующее: у нас есть картинки для сегментации и некачественная разметка.

Проявив реальный интеллект, смекалку и немного искусственного интеллекта, мы восстановили реальную, точную разметку, из данной нам некачественной. Т.е. наше предсказание сегментации всё-таки достигло нужной и приемлемой точности.

Конечно же, у нас игрушечный датасет, простая U-net и мы не пытаемся решать глобальные задачи и быть там, где не хотим быть.

Но идея полезная, нужная и пригодится может всем.

Главное, это поверить, что и на вашем датасете, с вашей сетью этот эффект сохранится ))

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


  1. vassabi
    07.10.2022 15:29

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