Long story short

Создают ли повороты ложные зависимости в датасете?

Небольшое исследование свойств rotate.


Представим себе, в существенно упрощенном виде, процесс скоринга.

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

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

Если банк использует только предсказание сети, то он прогорит, но об этом другая статья.

И представим для простоты, что человеческое тело это шар соискатель предоставляет просто матрицу W_SIZE x W_SIZE, ну или 128х128, например.
Почему бы и нет!

И мы не станем пытаться объять необъятное и искать все те способы обработки, что могли быть применены.

Мы возьмем матрицу w_size х w_size, заполненную случайно, мы же не знаем, что там, в реальности, и нам годятся все матрицы такого размера, и проверим, не поворачивал ли некто эту картинку(матрицу).

Т.е. будем решать максимально упрощенную задачу - в последовательности матриц/картинок будем искать те, что искусственно были повернуты. Ну и на такой же последовательности матриц будем и учить.

Использовать будем простую сеть на keras и обычные пакеты обработки, в которых есть функция "поворот"

Обычным способом грузим

import numpy as np
import cv2

from scipy import ndimage, misc, stats
from skimage import exposure

from matplotlib import pyplot as plt, colors
# work in interactive moode
%matplotlib inline 

from tensorflow import keras
from tensorflow.keras import layers

from math import sqrt, sin, cos, radians

import tqdm

Теперь создадим обучающую и тестовую последовательности. Создадим одну и поделим её пополам. Картинки/матрицы у нас будут такие - круг на черном фоне заполненный случайными значениями с заранее определенным распределением. При поворотах границы вносят существенные искажения, поэтому и берем круг в квадрате.

w_size = 128
w2_size = w_size // 2
R = w2_size//2
center = (w2_size, w2_size)
scale = 1

circle = np.zeros((w_size, w_size), dtype='uint8')
for i in range(w_size):
    for j in range(w_size):
        ii = float(i - w2_size)
        jj = float(j - w2_size)
        r = sqrt(ii*ii + jj*jj)
        if r < R:
            circle[i,j] = 1

def shift(x,y,w2_size):
    ii = float(x - w2_size)
    jj = float(y - w2_size)
    return ii,jj 

def rotate(ii,jj,teta):
    i1 =  ii*cos(teta) + jj*sin(teta)
    j1 =  ii*sin(teta) + jj*cos(teta)
    return i1,j1


circle = np.zeros((w_size, w_size), dtype='int')
for i in range(w_size):
    for j in range(w_size):
        ii,jj = shift(i,j,w2_size)
        r = sqrt(ii*ii + jj*jj)
        if r < R:
            circle[i,j] = 1

fig, ax = plt.subplots(1, 1,figsize=(5, 5))
ax.set_axis_off()
ax.set_title("circle")
ax.imshow(circle.squeeze(), cmap='gray', norm=None)

Первый эксперимент

Первый эксперимент проведем умозрительно, на бумаге.
Заполним квадрат с помощью numpy.random.uniform, после повернем с помощью ndimage.rotate на случайно выбранный угол.
Или не повернем. И из таких квадратов составим наши последовательности.
И признак для классификации оставим такой - 0, если не повернут и 1 если квадрат повернут.
И в каждом квадрате вырезаем центральный круг. Т.е. артефакты, вызванные граничными точками, убираем.
Нам нужен только круг в центре.

Очевидно, что сеть их отличит очень уверенно, на графиках гистограмм можем увидеть подтверждение ЦПТ, исходные картинки с ровной (почти) гистограммой, повернутые картинки дают гистограмму в виде горба ( очень похож на Гауссиан )

Так что дальше будем проводить эксперименты только с numpy.random.normal распределением.

Для тех, кто не верит на слово, пожалуйста, код.

num_classes = 2
train_len = 5000
test_len = 1000
input_shape = (w_size, w_size, 1)

X = np.zeros((train_len+test_len, w_size, w_size, 1), dtype='float32')
Y = np.zeros(train_len+test_len, dtype='float32')

mu = 0.25
sigma = 0.05

for iii in tqdm.tqdm(range(train_len + test_len)):
    
    angle = np.random.randint(-5,5)+ 45

    n = np.random.uniform(0.25, 0.75, size=(w_size, w_size))
    t = np.zeros((w_size, w_size), dtype='float')
    t[circle>0] = n[circle>0]
    
    r = ndimage.rotate(n, angle, reshape=False, mode='nearest')
    tt = np.zeros((w_size, w_size), dtype='float')
    tt[circle>0] = r[circle>0]

    choise = np.random.randint(0, high=65535, dtype=int) % 2
    if choise == 1:
        X[iii,:,:,0] = t[:,:]
        Y[iii] = 0
    else:
        X[iii,:,:,0] = tt[:,:]
        Y[iii] =1

yy_train = np.array(keras.utils.to_categorical(Y[:train_len], num_classes))
yy_test = np.array(keras.utils.to_categorical(Y[train_len:], num_classes))

xx_train = np.array(X[:train_len])
xx_test = np.array(X[train_len:])

print(xx_train.shape, yy_train.shape)
print(xx_test.shape, yy_test.shape)

nrows=2
ncols=4
fig, ax = plt.subplots(nrows*2, ncols,figsize=(16, 20))

for ii in range(nrows):
    for jj in range(ncols):
        random_characters = int(np.random.uniform(0,train_len))
        ax[2*ii,jj].set_axis_off()
        ax[2*ii,jj].set_title(str(int(Y[random_characters])))
        ax[2*ii,jj].imshow(X[random_characters].squeeze(), cmap='gray', norm=None)
        tx = X[random_characters]
        ax[2*ii+1,jj].hist(255.*X[random_characters].flatten(), np.arange(1,256), facecolor='blue',histtype='step')

plt.show(block=True)

Второй эксперимент

Второй эксперимент проведем, заполняя исходные квадраты случайными значениями уже с numpy.random.normal распределением.

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

Оказывается сеть спокойно их различает.

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

X = np.zeros((train_len+test_len, w_size, w_size, 1), dtype='float32')
Y = np.zeros(train_len+test_len, dtype='float32')

mu = 0.45
sigma = 0.1

for iii in tqdm.tqdm(range(train_len + test_len)):
    
    angle = np.random.randint(-5,5)+ 45

    n = np.random.normal(mu, sigma, size=(w_size, w_size))
    t = np.zeros((w_size, w_size), dtype='float')
    t[circle>0] = n[circle>0]
    
    r = ndimage.rotate(n, angle, reshape=False, mode='nearest')
    tt = np.zeros((w_size, w_size), dtype='float')
    tt[circle>0] = r[circle>0]

    choise = np.random.randint(0, high=65535, dtype=int) % 2
    if choise == 1:
        X[iii,:,:,0] = t[:,:]
        Y[iii] = 0
    else:
        X[iii,:,:,0] = tt[:,:]
        Y[iii] =1

yy_train = np.array(keras.utils.to_categorical(Y[:train_len], num_classes))
yy_test = np.array(keras.utils.to_categorical(Y[train_len:], num_classes))

xx_train = np.array(X[:train_len])
xx_test = np.array(X[train_len:])

print(xx_train.shape, yy_train.shape)
print(xx_test.shape, yy_test.shape)
nrows=2
ncols=4
fig, ax = plt.subplots(nrows*2, ncols,figsize=(16, 20))

for ii in range(nrows):
    for jj in range(ncols):
        random_characters = int(np.random.uniform(0,train_len))
        ax[2*ii,jj].set_axis_off()
        ax[2*ii,jj].set_title(str(int(Y[random_characters])))
        ax[2*ii,jj].imshow(X[random_characters].squeeze(), cmap='gray', norm=None)
        ax[2*ii+1,jj].hist(255.*X[random_characters].flatten(), np.arange(1,256),\
                           facecolor='blue',histtype='step')

plt.show(block=True)
model = keras.Sequential(
    [
        keras.Input(shape=input_shape),
        layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
        layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
        layers.MaxPooling2D(pool_size=(2, 2)),
        layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
        layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
#        layers.MaxPooling2D(pool_size=(2, 2)),
        layers.Flatten(),
        layers.Dropout(0.5),
        layers.Dense(128, activation="relu"),
        layers.Dense(num_classes, activation="softmax"),
    ]
)

batch_size = 25
epochs = 5
num_classes = 2
n_bins = 256

model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
#model.summary()

model.fit(xx_train, yy_train, batch_size=batch_size, epochs=epochs, 
          validation_data=(xx_test, yy_test), verbose = 2)

Epoch 1/5

200/200 - 6s - loss: 0.2437 - accuracy: 0.8638 - val_loss: 4.7537e-05 - val_accuracy: 1.0000

Epoch 2/5

200/200 - 4s - loss: 3.7514e-05 - accuracy: 1.0000 - val_loss: 2.2031e-05 - val_accuracy: 1.0000

Epoch 3/5

200/200 - 4s - loss: 1.8480e-05 - accuracy: 1.0000 - val_loss: 1.1520e-05 - val_accuracy: 1.0000

Epoch 4/5

200/200 - 4s - loss: 1.0300e-05 - accuracy: 1.0000 - val_loss: 6.6436e-06 - val_accuracy: 1.0000

Epoch 5/5

200/200 - 4s - loss: 5.9791e-06 - accuracy: 1.0000 - val_loss: 4.1065e-06 - val_accuracy: 1.0000

Третий эксперимент

Третий эксперимент проведем, заполняя исходные квадраты случайными значениями с numpy.random.normal распределением.

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

И часть картинок повернем.

Но теперь повернутые картинки немного исказим так, что бы гистограмма повернутой совпадала с гистограммой оригинальной картинки.

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

Не так уж он оказался умен, этот ваш искусственный интеллект

X = np.zeros((train_len+test_len, w_size, w_size, 1), dtype='float32')
Y = np.zeros(train_len+test_len, dtype='float32')

mu = 0.45
sigma = 0.1

view_test = np.random.randint(0, train_len+test_len, (3))

for iii in tqdm.tqdm(range(train_len + test_len)):
    
    angle = np.random.randint(-5,5)+ 45

    n = np.random.normal(mu, sigma, size=(w_size, w_size))
    t = np.zeros((w_size, w_size), dtype='float')
    t[circle>0] = n[circle>0]
    tf = t.flatten()
    
    r = ndimage.rotate(n, angle, reshape=False, mode='nearest')
    tt = np.zeros((w_size, w_size), dtype='float')
    tt[circle>0] = r[circle>0]
    tts = tt.copy()
    ttf = tt.flatten()

    step = np.arange(0., 1., 1./255.)
    
    for ii in range(1):
        for i in range(1,253):
            T = np.sum(np.bitwise_and(tf>step[i], tf<step[i+1]))
            TT = np.sum(np.bitwise_and(ttf>step[i], ttf<step[i+1]))
            T_TT = abs(T-TT)
            if T_TT == 0:
                continue
            if T>TT:
                jj = 0
                while True:
                    tt_array = np.nonzero(np.bitwise_and(ttf>step[i+1],ttf<step[i+2]))[0]
                    if len(tt_array)>=T_TT or i+jj > w_size-2:
                        break
                    ttf[ttf>step[i+2]] -=step[1]
                    jj += 1
                indices = np.arange(len(tt_array))
                np.random.shuffle(indices)
                for j in range(min(len(tt_array), T_TT)):
                    ttf[tt_array[indices[j]]] -= step[1]
            else:
                tt_array = np.nonzero(np.bitwise_and(ttf>step[i], ttf<step[i+1]))[0]
                if len(tt_array) != 0:
                    indices = np.arange(len(tt_array))
                    np.random.shuffle(indices)
                    for j in range(min(len(tt_array), T_TT)):
                        ttf[tt_array[indices[j]]] +=step[1]
                        
        tt = ttf.reshape(w_size,w_size)

    choise = np.random.randint(0, high=65535, dtype=int) % 2
    if choise == 1:
        X[iii,:,:,0] = t[:,:]
        Y[iii] = 0
    else:
        tt = ttf.reshape(w_size,w_size)
        X[iii,:,:,0] = tt
        Y[iii] =1
    
    if iii in view_test:
        random_idx = int(np.random.uniform(0,train_len))
        alpha = 0.05
        fig, axs = plt.subplots(1, 3, figsize=(30, 10))
    
        stat, p = stats.normaltest(t[circle>0].flatten())
        if p > alpha:
            axs[0].set_title("Оригинал Принять "+ str(p))
        else:
            axs[0].set_title("Оригинал Отклонить ?"+ str(p))
        axs[0].set_axis_off()
        axs[0].imshow(t, cmap="gray")
    
        stat, pp = stats.normaltest(tt[circle>0].flatten())
        if pp > alpha:
            axs[1].set_title("выровненный поворот Принять "+ str(pp))
        else:
            axs[1].set_title("выровненный поворот Отклонить "+ str(pp))
        axs[1].set_axis_off()
        axs[1].imshow(tt, cmap="gray")
    
        stat, p = stats.normaltest(tts[circle>0].flatten())
        if p > alpha:
            axs[2].set_title("просто поворот Принять "+ str(p))
        else:
            axs[2].set_title("просто поворот Отклонить "+ str(p))
        axs[2].set_axis_off()
        axs[2].imshow(tts, cmap="gray")
        
    
        tt_hist,b = np.histogram(256*tt.flatten(), np.arange(1,257))
        tts_hist,b = np.histogram(256*tts.flatten(), np.arange(1,257))
        t_hist,_ = np.histogram(256*t.flatten(), np.arange(1,257))
        fig, axs = plt.subplots(1, 1, figsize=(30, 30))
        bins = np.arange(1,256)
        axs.plot(bins, tts_hist, 'y')
        axs.plot(bins, tt_hist, 'b')
        axs.plot(bins, t_hist, 'g')
        plt.show(block=True)

yy_train = np.array(keras.utils.to_categorical(Y[:train_len], num_classes))
yy_test = np.array(keras.utils.to_categorical(Y[train_len:], num_classes))

xx_train = np.array(X[:train_len])
xx_test = np.array(X[train_len:])

print(xx_train.shape, yy_train.shape)
print(xx_test.shape, yy_test.shape)

Сеть та же самая.

batch_size = 25
epochs = 5
num_classes = 2
n_bins = 256

model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
#model.summary()

model.fit(xx_train, yy_train, batch_size=batch_size, epochs=epochs, 
          validation_data=(xx_test, yy_test), verbose = 2)
  • Epoch 1/5

  • 200/200 - 4s - loss: 0.6990 - accuracy: 0.4966 - val_loss: 0.6932 - val_accuracy: 0.4970

  • Epoch 2/5

  • 200/200 - 4s - loss: 0.6932 - accuracy: 0.4992 - val_loss: 0.6932 - val_accuracy: 0.4970

  • Epoch 3/5

  • 200/200 - 4s - loss: 0.6932 - accuracy: 0.4988 - val_loss: 0.6931 - val_accuracy: 0.5030

  • Epoch 4/5

  • 200/200 - 4s - loss: 0.6932 - accuracy: 0.4930 - val_loss: 0.6931 - val_accuracy: 0.4970

  • Epoch 5/5

  • 200/200 - 4s - loss: 0.6932 - accuracy: 0.4990 - val_loss: 0.6932 - val_accuracy: 0.4970

Главный эксперимент

Ну и главный эксперимент проведем на эллипсах. Очень хорошая и удобная геометрическая фигура. Картинки для испытаний создадим так - тот же квадрат, заполненный случайными данными нормального распределения и внутри квадрата эллипс, заполненный случайными значениями тоже нормального распределения, но с другими ожиданием и дисперсией. Так же после поворота выровняем гистограмму, так же как и в предыдущем эксперименте. И вот такие повороты нейронная простая сеть отлично находит.

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

w_size = 128
w2_size = w_size // 2
R = w2_size//2
center = (w2_size, w2_size)
scale = 1

RR = R //2
A = RR + 4
B = RR - 4
c = sqrt(float(A*A - B*B))

num_classes = 2
train_len = 5000
test_len = 1000
input_shape = (w_size, w_size, 1)

circle = np.zeros((w_size, w_size), dtype='uint8')
for i in range(w_size):
    for j in range(w_size):
        ii = float(i - w2_size)
        jj = float(j - w2_size)
        r = sqrt(ii*ii + jj*jj)
        if r < R:
            circle[i,j] = 1

def shift(x,y,w2_size):
    ii = float(x - w2_size)
    jj = float(y - w2_size)
    return ii,jj 

def rotate(ii,jj,teta):
    i1 =  ii*cos(teta) + jj*sin(teta)
    j1 =  ii*sin(teta) + jj*cos(teta)
    return i1,j1


circle = np.zeros((w_size, w_size), dtype='int')
for i in range(w_size):
    for j in range(w_size):
        ii,jj = shift(i,j,w2_size)
        r = sqrt(ii*ii + jj*jj)
        if r < R:
            circle[i,j] = 1

def ellipse_p(teta_grad):
    ellipse = np.zeros((w_size, w_size), dtype='float32')
    teta = radians(teta_grad)
    ci1, cj1 = rotate( c,0,teta)
    ci2, cj2 = rotate(-c,0,teta)
    for i in range(w_size):
        for j in range(w_size):
            ii,jj = shift(i,j,w2_size)
            r1 = sqrt((ii-ci1)*(ii-ci1) + (jj-cj1)*(jj-cj1))
            r2 = sqrt((ii-ci2)*(ii-ci2) + (jj-cj2)*(jj-cj2))
            
            if r1+r2 < 2*A:
                ellipse[i,j] = 255
    return (ellipse)

teta_grad = 45.0
ellipse = ellipse_p(teta_grad)

fig, ax = plt.subplots(1, 2,figsize=(10, 20))
ax[0].set_axis_off()
ax[0].set_title("circle")
ax[0].imshow(circle.squeeze(), cmap='gray', norm=None)
ax[1].set_axis_off()
ax[1].set_title("ellipse")
ax[1].imshow(ellipse.squeeze(), cmap='gray', norm=None)
X = np.zeros((train_len+test_len, w_size, w_size, 1), dtype='float32')
Y = np.zeros(train_len+test_len, dtype='float32')

mu = 0.35
sigma = 0.1
el_mu = 0.65
el_sigma = 0.1
view_test = np.random.randint(0, train_len+test_len, (3))

for iii in tqdm.tqdm(range(train_len + test_len)):
    
    angle = np.random.randint(-5,5)+ 45

    n = np.random.normal(mu, sigma, size=(w_size, w_size))
    el = np.random.normal(el_mu, el_sigma, size=(w_size, w_size))
    t = np.zeros((w_size, w_size), dtype='float')
    t[circle>0] = n[circle>0]
    ellipse = ellipse_p(angle)
    t[ellipse>0] = el[ellipse>0]
    tf = t.flatten()
    
    ellipse = ellipse_p(0)
    n[ellipse>0] = el[ellipse>0]
    r = ndimage.rotate(n, angle, reshape=False, mode='nearest')
    tts = np.zeros((w_size, w_size), dtype='float')
    tts[circle>0] = r[circle>0]
    ttf = tts.flatten()

    
    step = np.arange(0., 1., 1./255.)
    
    for ii in range(1):
        for i in range(1,253):
            T = np.sum(np.bitwise_and(tf>step[i], tf<step[i+1]))
            TT = np.sum(np.bitwise_and(ttf>step[i], ttf<step[i+1]))
            T_TT = T-TT
            if abs(T_TT) == 0:
                continue
            if T>TT:
                jj = 0
                while True:
                    tt_array = np.nonzero(np.bitwise_and(ttf>step[i+1],ttf<step[i+2]))[0]
                    if len(tt_array)>=T_TT or i+jj > w_size-2:
                        break
                    ttf[ttf>step[i+2]] -=step[1]
                    jj += 1
                indices = np.arange(len(tt_array))
                np.random.shuffle(indices)
                for j in range(min(len(tt_array), T_TT)):
                    ttf[tt_array[indices[j]]] -= step[1]
            else:
                tt_array = np.nonzero(np.bitwise_and(ttf>step[i], ttf<step[i+1]))[0]
                if len(tt_array) != 0:
                    indices = np.arange(len(tt_array))
                    np.random.shuffle(indices)
                    for j in range(min(len(tt_array), abs(T_TT))):
                        ttf[tt_array[indices[j]]] +=step[1]
                        
        tt = ttf.reshape(w_size,w_size)

    choise = np.random.randint(0, high=65535, dtype=int) % 2
    if choise == 1:
        X[iii,:,:,0] = t[:,:]
        Y[iii] = 0
    else:
        tt = ttf.reshape(w_size,w_size)
        X[iii,:,:,0] = tt
        Y[iii] =1
        
    if iii in view_test:
        random_idx = int(np.random.uniform(0,train_len))
        alpha = 0.05
        fig, axs = plt.subplots(1, 3, figsize=(30, 10))
        axs[0].set_axis_off()
        axs[1].set_axis_off()
        axs[2].set_axis_off()
        axs[0].imshow(t, cmap="gray")
        axs[1].imshow(tt, cmap="gray")
        axs[2].imshow(tts, cmap="gray")
            
        tt_hist,b = np.histogram(256*tt.flatten(), np.arange(1,257))
        tts_hist,b = np.histogram(256*tts.flatten(), np.arange(1,257))
        t_hist,_ = np.histogram(256*t.flatten(), np.arange(1,257))
        fig, axs = plt.subplots(1, 1, figsize=(30, 30))
        bins = np.arange(1,256)
        axs.plot(bins, tts_hist, 'y')
        axs.plot(bins, tt_hist, 'b')
        axs.plot(bins, t_hist, 'g')
        plt.show(block=True)

yy_train = np.array(keras.utils.to_categorical(Y[:train_len], num_classes))
yy_test = np.array(keras.utils.to_categorical(Y[train_len:], num_classes))

xx_train = np.array(X[:train_len])
xx_test = np.array(X[train_len:])

print(xx_train.shape, yy_train.shape)
print(xx_test.shape, yy_test.shape)
print(np.min(X), np.max(X))
batch_size = 25
epochs = 5
num_classes = 2
n_bins = 256

model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
#model.summary()

model.fit(xx_train, yy_train, batch_size=batch_size, epochs=epochs, 
          validation_data=(xx_test, yy_test), verbose = 2)

Epoch 1/5
200/200 - 4s - loss: 0.3143 - accuracy: 0.8330 - val_loss: 0.0030 - val_accuracy: 0.9990
Epoch 2/5
200/200 - 4s - loss: 0.0077 - accuracy: 0.9978 - val_loss: 2.2245e-04 - val_accuracy: 1.0000
Epoch 3/5
200/200 - 4s - loss: 0.0019 - accuracy: 0.9994 - val_loss: 4.2272e-04 - val_accuracy: 1.0000
Epoch 4/5
200/200 - 4s - loss: 0.0091 - accuracy: 0.9966 - val_loss: 9.0115e-04 - val_accuracy: 0.9990
Epoch 5/5
200/200 - 4s - loss: 0.0105 - accuracy: 0.9964 - val_loss: 0.0067 - val_accuracy: 0.9980

Краткое резюме.

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

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