ML: Рекурентні мережі на PyTorch
Вступ
Іноді навчальні дані являють собою набір впорядкованих послідовностей. Прикладами є часові ряди (котирування акцій, показання сенсорів) або текст природною мовою. У цих випадках підходящою архітектурою є рекурентні нейронні мережі RNN (recurrent neural network).
Ми будемо використовувати фреймворк PyTorch з основами якого можна ознайомитися тут і тут. Імпорт його бібліотек виглядає таким чином:
import torch import torch.nn as nnУсі приклади знаходяться у файлі ML_RNN_Torch.ipynb, а обговорення рекурентних мереж на бібліотеці Keras наведено тут.
Проста RNN
Рекурентний шар складається з L комірок з однаковими параметрами.
На його вхід подають впорядковану послідовність $\mathbf{x}^{(0)},...,\mathbf{x}^{(\mathrm{L}-1)}$ довжини L.
Елементи послідовності - це E-вимірні вектори ознак
$\mathbf{x}^{(t)}=\{x^{(t)}_0,...,x^{(t)}_{\mathrm{E}-1}\}$
(E від ембедингу слів).
Перший вектор надходить на вхід першої комірки, другий - на вхід другої і т.д.
Кожна комірка характеризується H-вимірним вектором прихованого стану
$\mathbf{h}=\{h_0,...,h_{\mathrm{H}-1}\}$.
Цей вектор є виходом комірки (стрілка вгору),
і він же відправляється в наступну комірку. Усередині простої RNN-комірки
проводиться таке обчислення (для $t=0,...,\text{L}-1$):
Матриці $\mathbf{W}:$(E, H), $~\mathbf{H}:$(H, H) і вектор $~\mathbf{b}:$(H,) є параметрами комірки (і всього шару). Оскільки комірки однакові, число входів L на число параметрів не впливає. Загальне число параметрів дорівнює (E + H + 1) * H. Початковий вектор прихованого стану $\mathbf{h}^{(-1)}$ (що входить у першу комірку) або дорівнює нулю $\mathbf{0}$ або задається руками.
Нелінійна функція гіперболічного тангенса $\tanh(x)=(e^x-e^{-x})/(e^x+e^{-x})$ , як зазвичай, необхідна, щоб послідовність матричних множень не "схлопнулася" в одне. Знакозмінність tanh бореться з неконтрольованим позитивним зростанням компонент вектора $\mathbf{h}^{(t)}$ при послідовному (рекурентному) множенні матриць.
Приклади RNN-архітектур
Вектор прихованого стану поступово "накопичує" усереднену інформацію про попередні входи.
Фінальний вектор $\mathbf{h}^{(\mathrm{L}-1)}$ характеризує всю послідовність і називається
контекстним вектором (context vector).
Його можна, наприклад, направити в стопку лінійних шарів,
на виході яких відбувається класифікація тексту (sentiment analysis - позитивний чи негативний відгук на продукт)
або передбачення чергового члена послідовності. Праворуч наведено приклад простої архітектури,
яка передбачає наступну букву в тексті. При цьому передбачається наявність додаткових шарів.
Номер букви в алфавіті подається на вхід шару векторизації Embedding.
На його виході виходить E-вимірний вектор, компоненти якого є параметрами навчання.
Після проходження через RNN-комірку цей вектор змінює свою розмірність,
перетворюючись на H-вимірний вектор нових ознак на виході комірки.
Потім він відправляється у звичайний повнозв'язний шар (fully connected) з "C" нейронами,
де "C" дорівнює числу букв в алфавіті.
Проходження цього C-вимірного вектора через softmax-функцію
дає "ймовірності" чергової букви в послідовності. Задача навчання полягає в підборі параметрів
RNN-комірки і векторів ембедингу таким чином, щоб ймовірність передбачення
правильної букви була максимальною.
Можна використовувати не тільки останній прихований стан, а й приховані стани всіх комірок.
Наприклад, архітектура мережі, що здійснює розмітку тексту, при якій кожному слову ставиться у відповідність його частина мови (займенник,
дієслово, артикль, прикметник, іменник) має вигляд, наведений праворуч.
Як і в попередньому прикладі, на всіх входах повинні бути шари ембедингу слів.
На виході - повнозв'язний свій з числом нейронів, рівним числу частин мови і софтмакс-функція, що дає їхні ймовірності.
У прикладах вище початковий прихований стан, що надходить на першу комірку,
був нульовим вектором.
Як цей вектор, можна також використовувати похідні ознаки деякого
об'єкта. Наприклад, праворуч схематично представлена задача текстового опису зображення.
Пікселі вихідної картинки пропускаються через послідовність згорткових шарів. Ваги цих шарів навчаються
на деякій іншій задачі (наприклад, розпізнавання класів об'єктів). Фінальний шар згорткової мережі
у вигляді початкового прихованого стану направляється в RNN-шар.
Входом його першої комірки є ембединг службового токена (слова) <BOS> (begin of sentence).
Рекурентну мережу вчать на виходах видавати релевантний картинці текст. При цьому перше згенероване слово надходить на вхід другої
комірки і т.д., поки на деякій ітерації шар не видасть службовий токен <EOS> (end of sentence).
Останній приклад пов'язаний з машинним перекладом і архітектурою уваги (attention).
Нехай є два рекурентних шари з різними параметрами.
Перший називається енкодер (encoder), а другий - декодер (decoder).
На входи комірок енкодера надходять ембединги слів речення
"I know a cool girl"
на вихідній (source) мові.
На виходах декодера очікується переклад речення на цільовій (target) мові:
"Я знаю крутую девчонку".
Приховані стани RNN-енкодера, як зазвичай, накопичують інформацію про вихідне речення. Їхня сума з деякими вагами $w_i$ надходить як додаткові входи на комірки рекурентного шару декодера (на малюнку показаний тільки один такий вхід). Ваги обчислюються навчуваною функцією від прихованих станів енкодера і прихованого стану декодера з попередньої комірки (у прикладі це "знаю"). Поточний прихований стан декодера "припускає", що у вихідному реченні важливо для перекладу і "фокусує увагу" на потрібній інформації, що відображається в значенні ваг $w_i$.
Матричні множення
Розглянемо як перемножуються матриці $\mathbf{x}^{(t)}\cdot \mathbf{W} + \mathbf{h}^{(t-1)}\cdot \mathbf{H} + \mathbf{b}$
при обчисленні прихованого стану.
У загальному випадку в RNN-комірку поточний вхід $\mathbf{x}^{(t)}$ надходить пачками (batches) по
B штук у кожній пачці.
Тому $t$-й вектор ознак $\mathbf{x}^{(t)}$ є матрицею форми (B,E)
(у рядках знаходяться приклади).
Ця матриця множиться на матрицю $\mathbf{W}:$ (E,H) зліва
і потім додається добуток матриць $\mathbf{h}^{(t-1)}:$ (B,H)
і $\mathbf{H}:$ (H,H). До результату додається вектор зміщення $\mathbf{b}$:
$$
\underbrace{\text{(B,E)} \cdot \text{(E,H)}}_{\mathbf{x}^{(t)}\,\cdot\,\mathbf{W}} ~~~+~~~
\underbrace{\text{(B,H)}\cdot\text{(H,H)}}_{\mathbf{h}^{(t-1)}\,\cdot\,\mathbf{H}} ~~~+~~~
\underbrace{\text{(1,H)}}_{\mathbf{b}} ~~~=~~~ \text{(B,H)}
$$
Нехай розмірності входу і прихованого стану дорівнюють E=2 і H=3,
а прикладів у пачці B=4. Тоді в одній комірці відбувається таке обчислення,
результат якого потім пропускається через tanh:
Те саме вийде компактніше, якщо зробити конкатенацію векторів $\mathbf{x}^{(t)}$, $\mathbf{h}^{(t-1)}$ праворуч: torch.cat([x,h], dim=1) і приєднати матрицю $\mathbf{H}$ до матриці $\mathbf{W}$ знизу: torch.cat([W,H], dim=0):
$$ \begin{array}{|c|c|c|c|c|} \hline x^{(t)\phantom{-1}}_{00} & x^{(t)\phantom{-1}}_{01} & h^{(t-1)}_{00} & h^{(t-1)}_{01}& h^{(t-1)}_{02}\\ \hline x^{(t)\phantom{-1}}_{10} & x^{(t)\phantom{-1}}_{11} & h^{(t-1)}_{10} & h^{(t-1)}_{11}& h^{(t-1)}_{12}\\ \hline x^{(t)\phantom{-1}}_{20} & x^{(t)\phantom{-1}}_{21} & h^{(t-1)}_{20} & h^{(t-1)}_{21}& h^{(t-1)}_{22}\\ \hline x^{(t)\phantom{-1}}_{30} & x^{(t)\phantom{-1}}_{31} & h^{(t-1)}_{30} & h^{(t-1)}_{31}& h^{(t-1)}_{32}\\ \hline \end{array} \cdot \begin{array}{|c|c|c|} \hline W_{00}^{\phantom{(0)}} & W_{01} & W_{02} \\ \hline W_{10}^{\phantom{(0)}} & W_{11} & W_{12} \\ \hline H_{00}^{\phantom{(0)}} & H_{01} & H_{02} \\ \hline H_{10}^{\phantom{(0)}} & H_{11} & H_{12} \\ \hline H_{20}^{\phantom{(0)}} & H_{21}& H_{22} \\ \hline \end{array} ~~+~~ \begin{array}{|c|c|c|} \hline b_{0} & b_{1} & b_{2}\\ \hline \end{array} ~~=~~ \begin{array}{|c|c|c|} \hline r_{00} & r_{01} & r_{02}\\ \hline r_{10} & r_{11} & r_{12}\\ \hline r_{20} & r_{21} & r_{22}\\ \hline r_{30} & r_{31} & r_{32}\\ \hline \hline \end{array} $$
Саме тому в комірці малюються злиті в одну стрілки векторів $\mathbf{x}^{(t)},\mathbf{h}^{(t-1)}$
на вході $\tanh$.
Такі обчислення проводяться в кожній комірці. Якщо вхідний тензор X
має форму (L,B,E),
то X[t] є матрицями (B,E), розглянутими вище:
Формою прихованого стану останньої комірки Hn для одноманітності прийнято вважати (1,B,H), а не (B,H).
Рекурентні мережі в PyTorch
У PyTorch простий рекурентний шар називається nn.RNN. Його обов'язковими параметрами є розмірність входів E і прихованих станів H:
E, H = 2, 3 # розмірності входів і прихованих станів B, L = 4, 5 # число прикладів, довжина прикладу (число векторів) rnn = nn.RNN(E, H) # екземпляр класу nn.RNNВиведемо імена параметрів RNN-шару та їхні форми:
for k, v in rnn.state_dict().items(): # weight_ih_l0 : (3, 2) (H,E)
print(f'{k:10s} : {tuple(v.shape)}') # weight_hh_l0 : (3, 3) (H,H)
# bias_ih_l0 : (3,) (H,)
# bias_hh_l0 : (3,) (H,)
Як і в лінійному шарі, матриці ваг зберігаються в транспонованому вигляді, для порядкового множення матриць. Звернемо увагу, що в списку параметрів знаходиться два зміщення. Насправді значущим параметром є сумарний вектор bias_ih_l0 + bias_hh_l0. У вихідниках пояснюється, що: "Second bias vector included for CuDNN compatibility" (так очевидно повинно працювати швидше на графічних картах).
Створимо випадковий набір даних X і відправимо його в рекурентний шар. На виході вийде кортеж: тензор усіх прихованих станів форми (L,B,E) і тензор прихованого стану останньої комірки (1,B,E):
X = torch.rand(L, B, E)
Y, Hn = rnn(X) # усі виходи і останній прихований стан
# (L, B, H) (1, B, H)
print(tuple(Y.shape), tuple(Hn.shape)) # (5, 4, 3) (1, 4, 3) Y[-1] == Hn[0]
Відтворимо обчислення, що відбуваються всередині простого рекурентного шару. Для цього отримаємо від об'єкта rnn значення параметрів комірки (від'єднавши їх від графа методом detach):
W_ih, W_hh = rnn.weight_ih_l0.detach(), rnn.weight_hh_l0.detach() B_ih, B_hh = rnn.bias_ih_l0.detach(), rnn.bias_hh_l0.detach()Прихований стан $\mathbf{h}^{(-1)}$, що входить у першу комірку заповнимо нулями, як це робить за замовчуванням nn.RNN.
У циклі "пробіжимося по всіх комірках" (метод addmm(v,M1,M2) обчислює v + M1 @ M2):
Hn = torch.zeros(B,H) # початковий прихований стан - нулі
for x in X: # по комірках for x:(B,E) in X:(L,B,E)
Hn =torch.tanh( torch.addmm(B_ih, x, W_ih.t())
+ torch.addmm(B_hh, Hn, W_hh.t()) )
print(Hn)
Початковий прихований стан можна передавати об'єкту мережі другим параметром (тоді він буде ненульовим). Повторимо попередні обчислення, подаючи в rnn по одному входу послідовності:
Hn = torch.zeros(1,B,H) # початковий прихований стан - нулі
for x in X:
_, Hn = rnn( x.view(1,B,E), Hn ) # Hn від попередньої комірки
print(Hn)
Шар Bidirectional і стопка шарів
Іноді на теперішнє впливає не тільки минуле, а й майбутнє.
Наприклад, сенс слова в реченні визначається всім реченням,
а не тільки попередніми йому словами.
У цьому випадку доречно спільно використовувати два рекурентних шари з різними параметрами. У першому шарі приховані стани поширюються зліва направо, а в другому - справа наліво. Вхідні вектори подаються незалежним чином на кожен шар, а виходи шарів конкатенуються. Тому вихідний тензор має розмірність (L,B,2*H). Фінальні приховані стани в Hn: (2,B,E) надходять від кожного шару. При цьому Hn[0] - це прихований стан останньої (найправішої) комірки першого шару, а Hn[1] - першої (найлівішої) комірки другого шару. У PyTorch двонапрямний шар створюється так:
rnn = nn.RNN(E, H, bidirectional=True)
Рекурентний шар (одиночний або двонапрямний) можна перетворити на стопку шарів:
rnn = nn.RNN(E, H, num_layers=3)
Покладемо Dir = 2 if bidirectional==True else 1.
Якщо число шарів дорівнює Num, тоді
вхідні і вихідні тензори у загальному випадку мають такі розмірності:
X : (L, B, E) => Y : (L, B, Dir*H) H0: (Num*Dir, B, H) => Hn: (Num*Dir, B, H )
Упаковка послідовностей
Навчальні послідовності часто мають різну довжину. Наприклад, число слів помітно змінюється від речення до речення. Можливі різні стратегії роботи з RNN у таких ситуаціях.
Можна подавати на вхід RNN по одній послідовності (B=1). Оскільки PyTorch будує динамічні графи, необхідне число комірок буде "створюватися на льоту". Однак пропускання через мережу по одному прикладу сповільнює обчислення, особливо при використанні графічних карт. Якщо розкид довжин невеликий, можна сортувати послідовності за довжиною і формувати батчі з прикладів однакової довжини.
Нарешті, при формуванні батчів можна скористатися функцією pack_padded_sequence, яка сама відсортує приклади в батчі за спаданням довжини і для кожної комірки сформує максимально довгий батч.
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequenceПроілюструємо дію цієї функції на прикладі. Нехай у батчі B=4 навчальних прикладів. Перший має довжину 5, другий - 3, третій - 4 і четвертий - 2. Приклади об'єднані в тензор, коротші послідовності "добиваються" до максимальної довжини L=5 нульовими вхідними векторами (розмірності E=2). Таким чином вхідний тензор X має форму (L,B,E)=(5,4,2):
X = torch.tensor([[1,1],[1,2],[1,3],[1,4],[1,5],
[2,1],[2,2],[2,3],[0,0],[0,0],
[3,1],[3,2],[3,3],[3,4],[0,0],
[4,1],[4,2],[0,0],[0,0],[0,0]],
dtype=torch.float)
X = X.view(B,L,E) # (B*L,E)->(B,L,E)
X = X.transpose(0,1) # -> (L,B,E)
Задамо довжини прикладів X_len (довжини рядків, якщо відкинути нулі) і викличемо функцію упаковки:
X_len = torch.tensor([5,3,4,2])# довжини прикладів
Xp = pack_padded_sequence(X, # пакуємо
X_len,
enforce_sorted=False)
PackedSequence(
data=tensor(
[[1., 1.],
[3., 1.],
[2., 1.],
[4., 1.], <- 1-а комірка B=4
[1., 2.],
[3., 2.],
[2., 2.],
[4., 2.], <- 2-а комірка B=4
[1., 3.],
[3., 3.],
[2., 3.], <- 3-а комірка B=3
[1., 4.],
[3., 4.], <- 4-а комірка B=2
[1., 5.]]),<- 5-а комірка B=1
batch_sizes = tensor([4,4,3,2,1]),
sorted_indices = tensor([0,2,1,3]))
Тепер можна відправити упаковку в мережу: Yp, Hn = rnn(Xp). Останній прихований стан Hn буде звичайного розміру (1,B,H)=(1,4,3). Виходи всіх комірок Yp, як і входи Xp, будуть упаковані. Щоб їх розпакувати, необхідно викликати другу функцію:
Y, Y_len = pad_packed_sequence(Yp)У результаті вийде щось типу (тепер Y[-1] != Hn[0], оскільки Hn містить останні ненульові рядки з усіх виходів Y[i]):
Y[0] = [[-0.4083, 0.2363, 0.8988], | Y[-1]= [[-0.8162, 0.9937, 0.9942],
[-0.8875, -0.6924, 0.9849], | [ 0.0000, 0.0000, 0.0000],
[-0.7268, -0.2967, 0.9605], | [ 0.0000, 0.0000, 0.0000],
[-0.9561, -0.8852, 0.9943]] | [ 0.0000, 0.0000, 0.0000]]
Y_len = tensor([5, 3, 4, 2]) # те саме, що і X_len
Алгоритм роботи RNN з упакованими даними відтворюється таким чином:
Hn = torch.zeros(Xp.batch_sizes[0], H) # нулі в початковому стані
Yp = torch.empty(len(Xp.data), H) # упакований тензор виходів комірок
beg = 0
for bs in Xp.batch_sizes: # за розмірами батчів
XX = Xp.data[beg: beg + bs] # батч поточної комірки
HH = Hn[ : bs] # прихований стан, що входить у неї
HH = torch.tanh( torch.addmm(B_ih, XX, W_ih.t()) # власне обчислення
+ torch.addmm(B_hh, HH, W_hh.t()) )
Yp[beg: beg + bs].copy_(HH) # пакуємо батч виходу
Hn[ : bs].copy_(HH) # накопичуємо його в прих.стані
beg += bs
Hn = Hn[Xp.sorted_indices] # вихідний порядок рядків
print(Yp, Hn) # зійдеться з Yp, Hn = rnn(Xp)
Крім input=X і length у функцій упаковки є ще два параметри. Якщо enforce_sorted = True, то батч повинен бути відсортований за спаданням довжини послідовностей. Параметр batch_first = True передбачає, що вхідний тензор має форму (B,L,E). Цей самий параметр тоді необхідно використовувати в конструкторі RNN при створенні екземпляра шару.
Усі параметри класу RNN
Наведемо список усіх параметрів класу RNN у фреймворку PyTorch:
✒ nn.RNN
… (input_size,
hidden_size, num_layers=1, nonlinearity='tanh', bias=True,
… batch_first=False, dropout=0, bidirectional=False)
[doc]
Зазначимо не згаданий раніше параметр dropout. За замовчуванням він дорівнює нулю. При ненульовому значенні, після кожного шару (num_layers > 1), крім останнього, вставляється шар dropout, який з ймовірністю dropout випадково "вимикає" (робить нульовими) частину елементів тензорів на виходах кожної комірки.
Встановлення параметра bias у значення False ліквідує вектор зміщення після перемноження матриць.
Комірка LSTM
У LSTM шарі (long short-term memory), крім прихованого стану $\mathbf{h}$ між комірками передається "стан пам'яті" $\mathbf{c}$. Розмірність H цього вектора збігається з розмірністю прихованого стану $\mathbf{h}$. Вектори $\mathbf{c}$ регулюють які ознаки треба запам'ятати або забути при передачі до наступної комірки, що покращує довгострокову пам'ять.
Усередині LSTM-комірки присутні чотири лінійних шари з H нейронами. Три з них мають сигмоїдну активацію $\sigma$ (на виході вектор зі значеннями $[0...1]$) і один шар з гіперболічним тангенсом $\tanh$: $[-1...1]$.
Перед цим обчисленням відбувається зміна значення вхідного в комірку вектора пам'яті $\mathbf{c}^{(t-1)}$. Спочатку $\mathbf{x}^{(t)}, \mathbf{h}^{(t-1)}$ потрапляють у повнозв'язний шар Forget з сигмоїдою (гейт забування). Розмірність виходу цього шару дорівнює H (як у $\mathbf{h}$ і $\mathbf{c}$). Цей вектор без згортки множиться на компоненти попереднього вектора пам'яті $\mathbf{c}^{(t-1)}$. Передбачається, що при множенні якісь ознаки в $\mathbf{c}^{(t-1)}$ забуваються (якщо їх помножили на 0), а якісь рухаються далі (якщо їх помножили на 1). Приклад забування: "Він взяв джин, а вона взяла мартіні" ("взяла" після "вона" може забути про "він"). Аналогічно крапка, як ознака кінця речення, повинна занулити істотну частину компонент вектора пам'яті $\mathbf{c}^{(t)}$.
Подібним чином працює наступний гейт, що реалізує запам'ятовування. Ті фічі, які необхідно запам'ятати, додаються у вектор $\mathbf{c}$. Шар з $\tanh$ [-1...1] формує "фічі-кандидати", а шар із сигмоїдом [0...1] посилює або послаблює роль фічі, що запам'ятовується. Аналітично обчислення в LSTM комірці виглядають таким чином: $$ \left\{ \begin{array}{lclclcl} \mathbf{F} &=& ~~~~~~\sigma(\mathbf{x}^{(t)}\, \mathbf{W}_{f} &+& \mathbf{h}^{(t-1)}\, \mathbf{H}_{f} &+& \mathbf{b}_f),\\ \mathbf{I} &=& ~~~~~~\sigma(\mathbf{x}^{(t)}\, \mathbf{W}_{i} &+& \mathbf{h}^{(t-1)}\, \mathbf{H}_{i} &+& \mathbf{b}_i),\\ \mathbf{R} &=& \text{tanh}(\mathbf{x}^{(t)}\, \mathbf{W}_{r} &+& \mathbf{h}^{(t-1)}\, \mathbf{H}_{r} &+& \mathbf{b}_r),\\ \mathbf{O} &=& ~~~~~~\sigma(\mathbf{x}^{(t)}\, \mathbf{W}_{o} &+& \mathbf{h}^{(t-1)}\, \mathbf{H}_{o} &+& \mathbf{b}_o), \end{array} \right. ~~~~~~~~~~~~~~~~~~ \left\{ \begin{array}{lcl} \mathbf{c}^{(t)} &=& \mathbf{F} \odot \mathbf{c}^{(t-1)} + \mathbf{R}\odot \mathbf{I},\\[2mm] \mathbf{h}^{(t)} &=& \tanh\bigr(\mathbf{c}^{(t)}\bigr) \odot \mathbf{O}. \end{array} \right. $$
Розмірності матриць для E$=\dim(\mathbf{x}),~~$ H$=\dim(\mathbf{h}),~\dim(\mathbf{c})$ дорівнюють: $$ \mathbf{W}_{i}, ~\mathbf{W}_{f}, ~\mathbf{W}_{r}, ~\mathbf{W}_{o}:~~~\mathrm{(E, H)};~~~~~~~~~~ \mathbf{H}_{i}, ~\mathbf{H}_{f}, ~\mathbf{H}_{r}, ~\mathbf{H}_{o}:~~~\mathrm{(H, H)};~~~~~~~~~~ \mathbf{b}_i, ~\mathbf{b}_f, ~\mathbf{b}_r, ~\mathbf{b}_o: ~~~~\mathrm{(1, H)}. $$ У PyTorch LSTM-мережу реалізує клас torch.nn.LSTM. На відміну torch.nn.RNN при прямому проході повертається три тензори: Y, Hd, Cn (прихований стан і вектор пам'яті мають однакову розмірність).Комірка GRU
Керований рекурентний блок (Gated Recurrent Units, GRU) є спрощеною версією LSTM при порівнянній обчислювальній потужності. Він виконує такі обчислення:
Поширення градієнтів
Розглянемо особливості поширення градієнтів по рекурентному шару під час його навчання. Потік градієнтів суттєво залежить від того, як обчислюється помилка $L$, яка ініціює його запуск.
Будемо для визначеності прогнозувати чергову букву в тексті.
Нехай букви векторизовані (ембединг)
і надходять на вхід шару у вигляді послідовності L=5
векторів: $\{\mathbf{x}^{(0)},\mathbf{x}^{(1)},\mathbf{x}^{(2)},\mathbf{x}^{(3)},\mathbf{x}^{(4)}\}$.
Вихід останньої комірки можна направити в повнозв'язний шар (з числом нейронів рівним числу букв) і потім пропустити через функцію
softmax, що дає "ймовірності" чергової букви $\mathbf{x}^{(5)}$.
У більш загальному випадку можна навчати мережу передбачати букви на декількох останніх комірках (нижче - це останні три виходи $\{\mathbf{x}^{(3)},\mathbf{x}^{(4)},\mathbf{x}^{(5)}\}$). На виході останньої комірки ми, як і раніше, очікуємо отримати $\mathbf{x}^{(5)}$, а на попередніх двох виходах - останні "вхідні" букви, але зміщені назад. На перших двох комірках шар накопичує у прихованому стані історію, а потім починає передбачення, "продовжуючи" накопичення історії:
Помилки дорівнюють логарифмам ймовірностей (зі зворотним знаком) "правильних" букв. Від кожної з трьох букв ці помилки сумуються, що дає сумарну помилку $L$. При зворотному поширенні, градієнт $g=1$ з $L$, розщепившись, спускається вниз, проходячи через софтмакс-функцію і лінійний шар, потрапляють на виходи останніх трьох комірок. Потім, пройшовши через комірки, градієнти рухаються ліворуч по шару.
Набір параметрів RNN-комірки на правому малюнку позначений вектором $\mathbf{w} = \{\mathbf{W},\mathbf{H},\mathbf{b},...\}$. Оскільки для всіх комірок ці параметри одні й ті самі, градієнти з комірок потрапляють у $\mathbf{w}$ і там сумуються.
Звернемо увагу, що в останню (5-ту) комірку заходить сумарний градієнт менший ніж у 4-ту і 3-ю, оскільки в нього не потрапляє градієнт у горизонтальному напрямку.
Нижче представлено реальний приклад зміни вздовж RNN-шару довжини градієнта усередненого по батчу. У шару L=25 комірок і помилка обчислюється за останніми 5 виходами (E=10, H=50, один шар, GRU-комірки). Жирна синя лінія - це сумарний градієнт, що входить у комірку (зверху і справа); тонка лінія - градієнт, що входить тільки зверху і пунктирна зелена - середня довжина вектора прихованого стану. Графік відповідає початку навчання:
Якщо дивитися на графік справа-наліво, видно, що градієнт спочатку починає підростати (до "вертикальних" градієнтів додаються "горизонтальні"). Потім, коли "впорскування" вертикальних градієнтів від помилок припиняється, градієнт монотонно затухає до початку послідовності (vanishing gradient problem).
Така ситуація типова на початку навчання (після першої епохи loss: trn=3.07 val=2.78). Коли мережа навчена після 30 епохи loss: trn=1.12 val=1.10, затухання градієнта може зникати (середнє значення за модулем елементів матриць $H$, що згортаються з прихованим станом збільшується з 0.12 до 0.42).
Градієнти в комірках
Нехай у просту комірку nn.RNN праворуч, по лінії прихованого стану входить градієнт $\mathbf{g}$.
Похідна від гіперболічного тангенса $\tanh (x)$ дорівнює $1-\tanh^2(x)$.
На виході вузла тангенса на етапі прямого проходження вийшов вектор $\mathbf{h}$.
Тому пройшовши вузол $\tanh$, компоненти градієнта помножаться на
$1-\mathbf{h}\odot \mathbf{h}$, де символ $\odot$ - це множення без згортки:
$(\mathbf{u}\odot\mathbf{v})_i = u_i v_i$.
Враховуючи правила перетворення градієнтів на вузлах елементарних операцій, отримуємо, що на виході комірки градієнт дорівнює $[\mathbf{g}\odot (1-\mathbf{h}\odot \mathbf{h})]\cdot \mathbf{H}^\top$. Якщо $\mathbf{h}$ близький до $\pm 1$, то градієнт зменшуватиметься. Аналогічно зменшують його малі компоненти матриці $\mathbf{H}$.
Після проходження n комірок градієнт, що потрапляє всередину першої комірки буде помножений на фактор типу: $$ \mathbf{g}' = \bigr[\bigr[...\bigr[\mathbf{g}^{(n)} \odot (1-\mathbf{h}^{(n)}\odot \mathbf{h}^{(n)})\cdot \mathbf{H}^\top\bigr] \odot (1-\mathbf{h}^{(n-1)}\odot \mathbf{h}^{(n-1)})\cdot \mathbf{H}^\top\bigr]\,\odot ... \odot\, (1-\mathbf{h}^{(1)}\odot \mathbf{h}^{(1)})\bigr] \cdot \mathbf{H}^\top $$ Оскільки параметри всіх комірок однакові, градієнти, що входять у них, необхідно скласти. Найбільш значущими будуть градієнти від останніх комірок якщо помилка обчислюється тільки до останнього прихованого стану (тобто градієнт від помилки входить тільки в останню комірку).
Одна з можливих стратегій боротьби з затуханням полягає в тому, що навчання починається з урахування помилок виходів усіх комірок. Потім, у міру навченості мережі, враховуються тільки помилки останніх комірок. Можна також збільшувати дисперсію початкових випадкових значень матриці $\mathbf{H}$.
Нагадаємо, що в стопці повнозв'язних шарів затухання градієнта приводить до повної зупинки навчання. У рекурентному шарі проблема затухання не настільки серйозна. Оскільки у всіх комірок параметри одні й ті самі, вони будуть у будь-якому випадку змінюватися за рахунок градієнта в останніх комірках
Додаткове читання :
- The Unreasonable Effectiveness of Recurrent Neural Networks
- Understanding LSTM Networks
- Building a LSTM by hand on PyTorch
- Як навчити свою нейромережу генерувати вірші