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