Фальшивомонетники проти банкірів: стравливаем adversarial networks у Theano

image
Ви б ніколи не подумали, але це прогулянка по простору нейромережі-фальшивомонетника. Зроблено крутейшими людьми Anders Boesen Lindbo Larsen і зауважив sоren Kaae Sønderby

Припустимо, у нас є завдання — зрозуміти навколишній світ.
Давайте для простоти уявімо, що світ — це гроші.

Метафора, можливо, з деякою моральної двозначності, але в цілому приклад не гірше інших — грошей (банкнот) визначено властива якась складна структура, тут у них цифра, тут буква, а там хитрі водяні знаки. Припустимо, нам потрібно зрозуміти, як вони зроблені, і дізнатися правило, за яким їх друкують. Який план?

Напрошується крок — це піти в офіс центрального банку і попросити їх видати специфікацію, але по-перше, вам її не дадуть, а по-друге, якщо витримувати метафору, то у всесвіті немає центрального банку (хоча на цей рахунок є релігійні розбіжності).

Ну, раз так, давайте спробуємо їх підробити.

Disriminative vs generative
Взагалі на тему розуміння світу є один досить відомий підхід, який полягає в тому, що зрозуміти — значить розпізнати. Тобто коли ми з вами займаємося якоюсь діяльністю в навколишньому світі, ми вчимося відрізняти одні об'єкти від інших і використовувати їх потім відповідним чином. Ось це — стілець, на ньому сидять, а ось це — яблуко, і його їдять. Шляхом послідовного спостереження потрібної кількості стільців та яблук ми вчимося відрізняти їх один від одного за вказівками вчителя, і таким чином виявляємо в світі якусь неоднорідність і структуру.
Так працює досить велика кількість моделей, які ми називаємо дискриминативными. Якщо ви трохи орієнтуєтеся в машинному навчанні, то в дискриминативную модель можна потрапити, тицьнувши пальцем навмання куди завгодно — багатошарові перцептрони, вирішальні дерева і лісу, SVM, you name it. Їх завдання — присвоювати спостережуваними даними правильну мітку, і всі вони відповідають на запитання «на що схоже на те, що я бачу?»

Інший підхід, злегка абсолютно відрізняється, полягає в тому, що зрозуміти — значить повторити. Тобто якщо ви поспостерігали за світом якийсь час, а потім опинилися в стані реконструювати його частина (скласти паперовий літачок, наприклад), то ви щось зрозуміли про те, як він влаштований. Моделі, які так роблять, зазвичай не потребують вказівок учителя, і ми називаємо їх породжують — це всілякі приховані Марківські, наївні (і не наївні) Байесовские, а модний світ глибоких нейромереж з недавніх пір додав туди restricted Boltzmann machines і автоэнкодеры.

Фундаментальна різниця в цих двох речах: для того, щоб відтворити річ, ти повинен знати про неї більш-менш все, а для того, щоб навчитися відрізняти одне від іншого — зовсім необов'язково. Ілюструється найкраще ось цією картинкою:

image

Зліва — те, що бачить породжує модель (у синього розподілу складна бимодальная структура, а червоне сильніше звужене), а праворуч — те, що бачить дискримінативна (тільки межа, на якій проходить вододіл між двома розподілами, і вона поняття не має про іншу структурі). Дискримінативна модель бачить світ гірше — вона цілком може укласти, що яблука відрізняються від стільців тільки тим, що вони червоні, і для її цілей цього цілком вистачить.

У обох моделей є свої переваги і недоліки, але давайте поки що для нашої мети погодимося, що породжує модель просто крутіше. Нам подобається ідея розуміти речі цілком, а не попарно-порівняно.

How do we generate
Ну гаразд, а як нам налаштувати цю породжує модель? Для якихось обмежених випадків ми можемо скористатися просто теорією ймовірності і теоремою Байєса — скажімо, ми намагаємося моделювати кидки монетки, і припускаємо, що вони відповідають биномиальному розподілу, тоді і так далі. Для дійсно цікавих об'єктів начебто картинок або музики це не дуже працює — прокляття розмірності дає про себе знати, ознак стає занадто багато (вони залежать один від одного, тому доводиться будувати спільне розподіл...).

Більш абстрактна (і цікава) альтернатива — припустити, що річ, яку ми моделюємо, можна представити у вигляді невеликої кількості прихованих змінних або «факторів». Для нашого гіпотетичного випадку грошей це припущення виглядає ніби цілком інтуїтивно правильним — малюнок на купюрі можна розкласти на компоненти у вигляді номіналу, серійного номера, красивої картинки, написи. Тоді можна сказати, що наші вихідні дані можна стиснути до розміру цих факторів і відновивши назад, не втративши смислового вмісту. На цьому принципі працюють всілякі автоэнкодеры, намагаються стиснути дані і потім побудувати їх максимально точну реконструкцію — дуже цікава штука, про яку ми, правда, говорити не будемо.

Ще один спосіб запропонував Іен Гудфеллоу з Google, і полягає він ось у чому:

1) Ми створюємо дві моделі, одну — породжує (назвемо її фальшивомонетником), і другу — дискриминативную (ця нехай буде банкіром)
2) Фальшивомонетник намагається побудувати на виході підробку на справжні гроші, а банкір — відрізнити підробку від оригіналу (обидві моделі починають з рандомних умов, і спочатку видають в якості результатів шум і сміття).
3) Мета фальшивомонетника — зробити такий продукт, який банкір не міг би відрізнити від справжнього. Мета банкіра — максимально ефективно відрізняти підробки від оригіналів. Обидві моделі починають гру один проти одного, де залишиться тільки один.

Обидві моделі будуть нейронними мережами — звідси назва adversarial networks. Стаття стверджує, що гра з часом сходиться до перемоги фальшивомонетника і, відповідно, ураження банкіра. Хороші новини для злочинного світу породжують моделей.

Трохи формалізму
Назвемо нашого фальшивомонетника (або generator), а банкіра — (або discriminator). У нас є якась кількість оригінальних грошей для банкіра, і нехай на виході в нього буде число діапазоном від нуля до одиниці, щоб воно виражало впевненість банкіра у тому, що видані йому на розгляд гроші справжні. Ще — оскільки фальшивомонетник у нас нейронна мережа, їй потрібні якісь вхідні дані, назвемо їх . Насправді це просто випадковий шум, що модель буде намагатися перетворити в гроші.

Тоді, очевидно, мета фальшивомонетника — це максимізувати , тобто зробити так, щоб банкір був упевнений, що підробки — справжні.

Мета банкіра складніше — йому потрібно одночасно позитивно впізнавати оригінали, і негативно — підробки. Запишемо це як максимізацію . Множення можна перетворити на додавання, якщо взяти логарифм, тому отримуємо:

Для банкіра: максимізувати
Для фальшивомонетника: максимізувати

Це трохи менше ніж вся математика, яка нам тут знадобиться.

Одновимірний приклад
(майже цілком узятий з цього чудового поста, але там — на TensorFlow)

Давайте спробуємо вирішити простий одновимірний приклад: нехай наші моделі має справу зі звичайними числами, які зібралися навколо точки з невеликим розмахом . Ймовірність кожного зустрінутого числа опинитися десь на числовій прямій можна уявити нормальним розподілом. Ось таким:



Відповідно числа, які відповідають цим розподілом (живуть в околиці від -2) будуть вважатися правильними» і «оригінальними», а інші — ні.

Візьмемо Theano і Lasagne, і поставимо наші моделі — простенькі нейронні мережі з двома шарами за десять нейронів кожен. При цьому із-за механізму роботи Theano (він будує символьний граф обчислень і дозволяє в якості входу дискримінатора задати одну конкретну змінну, а нам треба два — оригінали та фальшивки) зробимо дві копії дискримінатора: одна буде пропускати через себе «правильні» числа, а друга — підробки генератора.

Код
import theano
import theano.tensor as T
from lasagne.nonlinearities import rectify, sigmoid, linear, tanh

G_input = T. matrix('Gx')
G_l1 = lasagne.layers.InputLayer((None, 1), G_input)
G_l2 = lasagne.layers.DenseLayer(G_l1, 10, nonlinearity=rectify)
G_l3 = lasagne.layers.DenseLayer(G_l2, 10, nonlinearity=rectify)
G_l4 = lasagne.layers.DenseLayer(G_l3, 1, nonlinearity=linear)
G = G_l4

G_out = lasagne.layers.get_output(G)

# discriminators
D1_input = T. matrix('D1x')
D1_l1 = lasagne.layers.InputLayer((None, 1), D1_input)
D1_l2 = lasagne.layers.DenseLayer(D1_l1, 10, nonlinearity=tanh)
D1_l3 = lasagne.layers.DenseLayer(D1_l2, 10, nonlinearity=tanh)
D1_l4 = lasagne.layers.DenseLayer(D1_l3, 1, nonlinearity=sigmoid)
D1 = D1_l4

D2_l1 = lasagne.layers.InputLayer((None, 1), G_out)
D2_l2 = lasagne.layers.DenseLayer(D2_l1, 10, nonlinearity=tanh, W=D1_l2.W, b=D1_l2.b)
D2_l3 = lasagne.layers.DenseLayer(D2_l2, 10, nonlinearity=tanh, W=D1_l3.W, b=D1_l3.b)
D2_l4 = lasagne.layers.DenseLayer(D2_l3, 1, nonlinearity=sigmoid, W=D1_l4.W, b=D1_l4.b)
D2 = D2_l4

D1_out = lasagne.layers.get_output(D1)
D2_out = lasagne.layers.get_output(D2)



Давайте намалюємо, як веде себе дискримінатор — тобто для кожного числа, яке є на прямий (ми обмежимося діапазоном від -5 до 5) зазначимо впевненість дискримінатора в тому, що це число — правильне. Отримаємо зелену криву на графіку нижче — як бачите, оскільки дискримінатор у нас не навчений, він видає повну околорандомную єресь. І заодно попросимо генератор виплюнути якусь кількість чисел і намалюємо їх розподіл з допомогою червоної гістограми:



І ще трохи Theano-коду, щоб зробити функції ціни та почати навчання:

код
# objectives
G_obj = (T. log(D2_out)).mean()
D_obj = (T. log(D1_out) + T. log(1 - D2_out)).mean()

# parameters update and training
G_params = lasagne.layers.get_all_params(G, trainable=True)
G_lr = theano.shared(np.array(0.01, dtype=theano.config.floatX))
G_updates = lasagne.updates.nesterov_momentum(1 - G_obj, G_params, learning_rate=G_lr, momentum=0.6)
G_train = theano.function([G_input], G_obj, updates=G_updates)

D_params = lasagne.layers.get_all_params(D1, trainable=True)
D_lr = theano.shared(np.array(0.1, dtype=theano.config.floatX))
D_updates = lasagne.updates.nesterov_momentum(1 - D_obj, D_params, learning_rate=D_lr, momentum=0.6)
D_train = theano.function([G_input, D1_input], D_obj, updates=D_updates)

# training loop

epochs = 400
k = 20
M = 200 # mini-batch size

for i in range(epochs):
for j in range(k):
x = np.float32(np.random.normal(mu, sigma, M)) # sampled orginal batch
z = sample_noise(M)
D_train(z.reshape(M, 1), x.reshape(M, 1))
z = sample_noise(M)
G_train(z.reshape(M, 1))
if i % 10 == 0: # lr decay
G_lr *= 0.999
D_lr *= 0.999



Використовуємо тут learning rate decay і не дуже великий momentum. Крім того, на один крок генератора дискримінатор тренується кілька кроків (20 в даному випадку) — кілька мутний пункт, про який є в статті, але не дуже зрозуміло, навіщо. Моє припущення — це дозволяє генератора не реагувати на випадкові метання дискримінатора (тобто, фальшивомонетник спочатку чекає, поки банкір не затвердить для себе стратегію поведінки, а потім намагається обдурити її).

Процес навчання виглядає приблизно ось так:



Що відбувається в цій гифке? Зелена лінія намагається загнати червону гістограму у синій контур. Коли межа дискримінатора в певному місці опускається нижче 0.5, це означає, що для відповідних місць числової прямої дискримінатор підозрює більше підробок, ніж оригіналів, і швидше буде видавати негативні висновки (тим самим він практично заштовхує викиди червоних чисел нижче — подивіться, наприклад, на викид праворуч від центру розподілу через перші кілька секунд гифки).

Там, де гістограма збігається з контуром, крива дискримінатора тримається на рівні 0.5 — це означає, що дискримінатор більше не може відрізнити підробку від оригіналів і робить найкраще, що може в такій ситуації — просто рандомно випльовує здогадки з однаковою ймовірністю.

За межами нас цікавить ділянки дискримінатор веде себе не дуже адекватно — наприклад, позитивно з імовірністю ~1 класифікує всі великі числа -1 — але це не страшно, тому що в цій грі ми вболіваємо за фальшивомонетника, і саме його успіхи нас цікавлять.

За цією парочкою цікаво спостерігати, граючись з різними параметрами. Наприклад, якщо поставити momentum занадто великим, обидва учасника гри почнуть надто різко відгукуватися на навчаються сигнали і виправляють свою поведінку так сильно, що роблять ще гірше. Якось так:



Цілком код можна подивитися тут.

Приклад цікавіше
Гаразд, це було не так уже й погано, але ми здатні на більше, як натякає велика картинка. Спробуємо новообретенним скіли фальшивомонетника на старих добрих рукописних цифрах.

Тут для генератора доведеться використовувати невеликий трюк, який описаний у статті під кодовою назвою DCGAN (ним користуються Ларс і Сорен в заголовному пості). Він полягає в тому, що ми робимо таку свого роду сверточную мережа навпаки (разверточную мережу?), де замінюємо subsampling-шари, зменшують картинку в N разів, на upsampling-шари, які її збільшують, а згорткові шари робимо в режимі «full» — коли фільтр вискакує за кордону картинки і на виході дає результат більше, ніж вихідна картинка. Якщо для вас це мутно і неочевидно, ось ця сторінка за п'ять хвилин все прояснить, а поки можна запам'ятати просте правило — у випадку звичайної згортки картинки фільтром результат буде мати бік , а full convolution — . Це дозволить нам почати з маленького квадратика шуму і «повернути» його в повноцінне зображення.

Деякі технічні незрозумілостіВзагалі кажучи, у статті сказано, що вони рекомендують прибрати subsampling/upsampling шари взагалі. Можете спробувати зробити так, хоча по-моєму, виходить дещо громіздко… Ще одна незрозумілість пов'язана з тим, що згортки, які в них використовуються — strided, тобто робляться з кроком, більшим 1. Для звичайної згортки, взагалі кажучи, результат повинен виходити якраз такий же, як при subsampling + звичайна згортка (якщо я нічого не плутаю), і відповідно, я вирішив, що upsampling + full convolution дадуть аналогічний результат для нашої разверточной мережі. При цьому ще й відбувається жахлива плутанина з термінами — одна і та ж річ в гуглі називається «full convolution», «fractionaly-strided convolution» і навіть «deconvolution». Автори DCGAN кажуть, що deconvolution — неправильна назва, але в їх же коді використано саме таку… загалом, в якийсь момент я махнув рукою і вирішив, що працює — і гаразд.


Код генератора для цифр MNIST виглядає приблизно як під спойлером. Дискримінатором буде якась найпростіша сверточная мережу. Якщо чесно, я один-в-один здер її з readme до Lasagne, і навіть не буду тут наводити.

код
G_input = T. tensor4('Gx')
G = lasagne.layers.InputLayer((None, 1, NOISE_HEIGHT, NOISE_WIDTH), G_input)
G = batch_norm(lasagne.layers.DenseLayer(G, NOISE_HEIGHT * NOISE_WIDTH * 256, nonlinearity=rectify))
G = lasagne.layers.ReshapeLayer(G, ([0], 256, NOISE_HEIGHT, NOISE_WIDTH))
G = lasagne.layers.Upscale2DLayer(G, 2) # 4 * 2 = 8
G = batch_norm(lasagne.layers.Conv2DLayer(G, 128, (3, 3), nonlinearity=rectify, pad='full')) # 8 + 3 - 1 = 10
G = lasagne.layers.Upscale2DLayer(G, 2) # 10 * 2 = 20
G = batch_norm(lasagne.layers.Conv2DLayer(G, 64, (3, 3), nonlinearity=rectify, pad='full')) # 20 + 3 - 1 = 22
G = batch_norm(lasagne.layers.Conv2DLayer(G, 64, (3, 3), nonlinearity=rectify, pad='full')) # 22 + 3 - 1 = 24
G = batch_norm(lasagne.layers.Conv2DLayer(G, 32, (3, 3), nonlinearity=rectify, pad='full')) # 24 + 3 - 1 = 26
G = batch_norm(lasagne.layers.Conv2DLayer(G, 1, (3, 3), nonlinearity=sigmoid, pad='full')) # 26 + 3 - 1 = 28
G_out = lasagne.layers.get_output(G)



Результат виявляється приблизно такий.



Не те щоб ідеально, але щось виразно схоже. Забавно спостерігати, як генератор вчиться малювати схожими штрихами, а потім пристойний час бореться з тим, щоб скласти з них правильну форму. Наприклад, ось тут виділені символи не схожі на цифри, але при цьому явно являють собою якісь символи, тобто генератор пішов правильною дорогою:



Їдемо далі — беремо базу осіб LFW Crop, для початку зменшуємо особи до розміру MNIST (28x28 пікселів) і намагаємося повторити експеримент, тільки в кольорі. І відразу помічаємо кілька закономірностей:

1) Особи тренуються гірше. Кількість нейронів і шарів, що я тут наводив MNIST, взято трохи з запасом — його можна переполовинити, і буде виходити непогано. Особи при цьому перетворюються у незрозуміле рандомное місиво.
2) Кольорові особи тренуються ще гірше — що зрозуміло, звичайно, інформації стає в три рази більше.
3) іноді Доводиться коригувати взаємну поведінку генератора і дискримінатора: трапляються ситуації, коли один заганяє іншого в глухий кут, і гра зупиняється. Зазвичай це відбувається в ситуації, коли, скажімо, дискримінатор виявляється могутніше в репрезентативному сенсі — він може розуміти такі тонкі деталі, які генератор ще (або взагалі) не в стані розрізнити. В знову-таки заголовному пості описано декілька евристик (сповільнювати дискримінатор, коли він отримує занадто гарні оцінки і т. д.), але у мене все досить непогано працювало і без них вистачало один раз на початку навчання підправити налаштування і число нейронів.

І результат:



Непогано, непогано, але я все ще хочу що-небудь такої ж якості, як на КДПВ!

Повнорозмірний приклад і блукання в просторі рецептів
Я навантажив опечаленную GT 650M повнорозмірним (64x64) LFW Crop і залишив на ніч. До ранку пройшло приблизно 30 епох (на 10000 осіб), і кінцевий результат виглядав ось так:



Принадність яка. Якщо кому потрібні портрети персонажів для зомбі-апокаліпсису, дайте знати! У мене їх багато.
Якість вийшло не дуже, але сталося щось схоже на ту ситуацію з MNIST — наш генератор навчився справлятися з візуальними шумами і розпливчастістю, нормально малювати очі, носи і роти, і тепер у нього проблема під назвою «як їх правильно поєднати».

Давайте тепер подумаємо ще раз, що у нас намалювалося. Всі ці особи випльовує одна і та ж мережа, а нейронна мережа — це наскрізь детермінована штука, просто послідовність множень і додавань (і нелінійностей, ну ладно). Я не додавав в генератор ніякої рандомізації, dropout'а і т. д. Так яким чином особи виходять різні? Очевидно, єдине джерело рандома, яким мережа може керуватися — це той самий вхідний шум, який ми раніше обізвали «безглуздим» параметром. Тепер з'ясовується, що він виявляється досить важливим — у цьому шумі закодовані всі параметри вихідного особи, а все, що робить інша мережа — це просто читає вхідний «рецепт» і відповідно з ним наносить фарбу.

І рецепти не є дискретними (я про це не встиг сказати, але в якості шуму бралося просте рівномірний розподіл чисел від 0 до 1). Правда, ми все ще не знаємо, яке число в рецепті що саме кодує, хм — і навіть «кодує що-небудь осмислене якесь одне число?». Ну гаразд, і навіщо нам тоді це все?

По-перше, знаючи тепер, що вхідний шум — це рецепт, ми можемо керувати ним. Рівномірний шум, скажемо прямо, був не дуже хорошою ідеєю — тепер кожен шматочок шуму може кодувати що завгодно з однаковою ймовірністю. Ми можемо подати на вхід, скажімо, нормальний шум (розподілений по Гаусу) — тоді значення шуму, близькі до центру, будуть зустрічатися часто і кодувати що-небудь загальне (типу кольору шкіри), а рідкісні викиди — що-небудь особливе й рідкісне (наприклад, окуляри на обличчі). Люди, які писали статтю про DCGAN, знайшли кілька семантичних шматків рецепту, і використовували їх для того, щоб надягати особам окуляри або змушувати їх хмуритися за бажанням.

По-друге… бачили мем на тему «image enhancing» з серіалу CSI? Річ над якою довго сміялися всі люди, скільки-небудь знайомих з комп'ютерами: могутній алгоритм ФБР чарівним чином підвищує здатність картинки. Так ось, можливо, нам ще доведеться взяти свої слова назад, тому що: а чому б нам трохи не полегшити життя нашого генератора, і замість того, щоб подавати на вхід шум, подати, скажімо, зменшений варіант нашого оригіналу? Тоді замість того, щоб малювати особи з голови, генератору всього лише треба буде їх «домалювати» — а ця задача явно виглядає простіше, ніж перша.

У мене, на жаль, не дійшли до цього руки, але ось тут є відмінний пост, демонструє, як це робиться з вражаючими ілюстраціями.

Ну і по-третє, можна трохи розважитися. Раз ми знаємо, що рецепти не дискретні, то існують перехідні стани між будь-якими двома рецептами. Беремо два вектора шуму, интерполируем їх і згодовуємо генератора. Повторюємо, поки не набридне, або поки люди в кімнаті серйозно не почнуть турбуватися, навіщо ви так пильно розглядає агонізуючих зомбі.



Висновок
1) Adversarial networks — це весело.
2) Якщо ви вирішите спробувати тренувати їх самі — простіше підглянути готові рецепти, ніж гратися з параметрами.
3) Йошуа Бенджио назвал DCGAN і LAPGAN (це який image enhancing) в числі найбільш вражаючих штук в машинному навчанні 2015 року і саме після цього я поліз про них читати, само собою
4) У ще одному блозі був на днях хороший посада на тему «deep learning is easy, try something harder» — якраз для людей зразок мене, які зачепилися за популярну тему і не дуже розуміють, куди рухатися далі. Adversarial networks — хороший варіант (і теж згадується в пості як одне з нових перспективних напрямів), тому що тут купа всього цікавого, і контроль над рецептами тільки одна з самих напрошуються речей.
5) Як перекласти «adversarial networks» на російську мову? «Ворогуючі мережі»? «Змагаються мережі»?

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.