Навчальні нейронні мережі виявляють надзвичайно малі значення NaN


329

Я намагаюся реалізувати архітектуру нейронної мережі в Haskell і використовувати її на MNIST.

Я використовую hmatrixпакет для лінійної алгебри. Моя навчальна база побудована за допомогою pipesпакету.

Мій код компілюється і не дає збою. Але проблема полягає в тому, що певні комбінації розміру шару (скажімо, 1000), розміру міні-партії та швидкості навчання породжують NaNзначення в обчисленнях. Після деякої перевірки я бачу, що 1e-100в активаціях з часом з’являються надзвичайно малі значення (порядок ). Але, навіть коли цього не відбувається, навчання все одно не працює. Немає покращення щодо його втрати або точності.

Я перевірив і перевірив свій код, і я не знаю, що може бути коренем проблеми.

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

backward lf n (out,tar) das = do
    let δout = tr (derivate lf (tar, out)) -- dE/dy
        deltas = scanr (\(l, a') δ ->
                         let w = weights l
                         in (tr a') * (w <> δ)) δout (zip (tail $ toList n) das)
    return (deltas)

lfце функція втрат, nє мережа ( weightматриця і biasвектор для кожного шару), outі tarє фактичний вихід мережі і target( по бажанню) виходу, і dasє похідними активації кожного шару.

У пакетному режимі out, tarє матриці (рядки є вихідними векторами), і dasявляють собою список матриць.

Ось фактичний розрахунок градієнта:

  grad lf (n, (i,t)) = do
    -- Forward propagation: compute layers outputs and activation derivatives
    let (as, as') = unzip $ runLayers n i
        (out) = last as
    (ds) <- backward lf n (out, t) (init as') -- Compute deltas with backpropagation
    let r  = fromIntegral $ rows i -- Size of minibatch
    let gs = zipWith (\δ a -> tr (δ <> a)) ds (i:init as) -- Gradients for weights
    return $ GradBatch ((recip r .*) <$> gs, (recip r .*) <$> squeeze <$> ds)

Тут, lfі nтакі самі, як вище, iє вхідними даними і tє цільовим результатом (як у пакетній формі, так і в матрицях).

squeezeперетворює матрицю у вектор шляхом підсумовування по кожному рядку. Тобто це dsсписок матриць дельт, де кожен стовпець відповідає дельтам для рядка міні-партії. Отже, градієнти для ухилів є середнім значенням дельт за всю міні-партію. Те саме gs, що відповідає градієнтам ваг.

Ось фактичний код оновлення:

move lr (n, (i,t)) (GradBatch (gs, ds)) = do
    -- Update function
    let update = (\(FC w b af) g δ -> FC (w + (lr).*g) (b + (lr).*δ) af)
        n' = Network.fromList $ zipWith3 update (Network.toList n) gs ds
    return (n', (i,t))

lr- це швидкість навчання. FCє конструктором шару і afє функцією активації для цього шару.

Алгоритм градієнтного спуску гарантує передачу негативного значення швидкості навчання. Фактичний код для градієнтного спуску - це просто петля навколо композиції gradта move, з параметризованою умовою зупинки.

Нарешті, ось код для функції середньоквадратичної втрати помилок:

mse :: (Floating a) => LossFunction a a
mse = let f (y,y') = let gamma = y'-y in gamma**2 / 2
          f' (y,y') = (y'-y)
      in  Evaluator f f'

Evaluator просто поєднує функцію втрат та її похідну (для обчислення дельти вихідного шару).

Решта коду розміщена на GitHub: NeuralNetwork .

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


17
Дякую, я розгляну це. Але я не думаю, що це нормальна поведінка. Наскільки я знаю, інші реалізації того, що я намагаюся зробити (просте перенаправлення повністю підключеної нейронної мережі), як Haskell, так і іншими мовами, схоже, не роблять цього.
Чарльз Ланглуа

17
@Charles: Ви насправді пробували власні мережі та набори даних із зазначеними іншими реалізаціями? З мого власного досвіду, ВР легко піде нанівець, коли НН не підходить для вирішення проблеми. Якщо у вас є сумніви щодо вашої реалізації BP, ви можете порівняти його результати з результатами наївного розрахунку градієнта (звичайно, для NN розміром із іграшку) - що важче помилитися, ніж BP.
shinobi

5
Хіба MNIST, як правило, не є проблемою класифікації? Чому ви використовуєте MES? Ви повинні використовувати кросссентропію softmax (розраховується з логітів) ні?
mdaoust

6
@CharlesLanglois, Можливо, це не ваша проблема (я не можу прочитати код), але "середньоквадратична помилка" не є опуклою для проблеми класифікації, що може пояснити застрягнення. «Логит - аналіз» це просто химерний спосіб сказати лог фору: Використовуйте ce = x_j - log(sum_i(exp(x)))розрахунок тут , так що ви не берете журнал експонента (який часто генерує нехтує малий)
mdaoust

6
Вітаємо з тим, що Ви ставили питання з найбільшим голосуванням (станом на 20 січня 20 року), не маючи жодної схваленої чи прийнятої відповіді!
hongsy

Відповіді:


2

Чи знаєте ви про "зникнення" та "вибух" градієнтів при зворотному розмноженні? Я не надто знайомий з Haskell, тому я не можу легко зрозуміти, що саме робить ваш backprop, але схоже, ви використовуєте логістичну криву як свою функцію активації.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.