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
Краткое резюме.
Если в вашем датасете с картинками не выровненные классы, и вы решили добавить картинок для выравнивания с помощью аугментации, то результат будет удивительным и коварным.
