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