Перевод с RNN

Часть 3: Принуждение учителей

В полном руководстве по НЛП с фастаем

Перейдите по ссылке на всю серию, нажав здесь: Полное руководство по НЛП с фастай

Это будет длинный пост, в котором мы построим Rnn и объясним закодированный Rnn. Далее, используя наш Rnn, мы познакомимся с новой концепцией принуждения учителей и объясним, почему ее можно использовать.

Итак, приступим….

Прогнозирование английской словесной версии чисел с использованием RNN

Мы использовали RNN как часть нашей языковой модели на предыдущем уроке. Сегодня мы более подробно рассмотрим, что такое RNN и как они работают. Мы сделаем это, используя задачу о попытке предсказать английскую словесную версию чисел.

Давайте предскажем, что должно быть дальше в этой последовательности:

восемь тысяч один, восемь тысяч два, восемь тысяч три, восемь тысяч четыре, восемь тысяч пять, восемь тысяч шесть, восемь тысяч семь, восемь тысяч восемь, восемь тысяч девять, восемь тысяч десять, восемь тысяч одиннадцать, восемь тысяч двенадцать …

Это синтетический набор данных, чтобы иметь лучший способ проверить, работает ли что-то, отладить и понять, что происходит. При экспериментировании с новыми идеями было бы неплохо иметь для этого меньший набор данных, чтобы быстро понять, являются ли ваши идеи многообещающими (другие примеры см. в Imagenette и Imagewoof). хороший набор данных для изучения RNN. Наша задача сегодня будет заключаться в том, чтобы предсказать, какое слово будет следующим при счете.

В глубоком обучении есть 2 типа чисел

Параметры — это заученные числа. Активации — это числа, которые рассчитываются (с помощью аффинных функций и поэлементной нелинейности).

Когда вы узнаете о какой-либо новой концепции глубокого обучения, спросите себя: это параметр или активация?

Примечание для себя: укажите скрытое состояние, переходя от версии без цикла for к циклу for. Это шаг, на котором люди путаются.

Данные

In [2]:from fastai.text import *
In [3]:bs=64
In [4]:
path = untar_data(URLs.HUMAN_NUMBERS)
path.ls()
Out[4]:[PosixPath('/home/racheltho/.fastai/data/human_numbers/models'),
 PosixPath('/home/racheltho/.fastai/data/human_numbers/valid.txt'),
 PosixPath('/home/racheltho/.fastai/data/human_numbers/train.txt')]
In [5]:
def readnums(d): return [', '.join(o.strip() for o in open(path/d).readlines())]

train.txt дает нам последовательность чисел, записанных в виде английских слов:

In [6]:train_txt = readnums('train.txt'); train_txt[0][:80]
Out[6]:
'one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve, thirt'
In [7]:
valid_txt = readnums('valid.txt'); valid_txt[0][-80:]
Out[7]:
' nine thousand nine hundred ninety eight, nine thousand nine hundred ninety nine'
In [8]:
train = TextList(train_txt, path=path)
valid = TextList(valid_txt, path=path)
src = ItemLists(path=path, train=train, valid=valid).label_for_lm()
data = src.databunch(bs=bs)
In [9]:
train[0].text[:80]
Out[9]:'xxbos one , two , three , four , five , six , seven , eight , nine , ten , eleve'
In [10]:len(data.valid_ds[0][0].data)
Out[10]:13017

bptt означает обратное распространение во времени. Это говорит нам о том, сколько шагов истории мы рассматриваем.

In [11]:data.bptt, len(data.valid_dl)
Out[11]:(70, 3)

У нас есть 3 партии в нашем наборе проверки:

13017 токенов, примерно 70 токенов примерно в строке текста и 64 строки текста на пакет.

In [12]:13017/70/bs
Out[12]:2.905580357142857

Мы будем хранить каждую партию в отдельной переменной, чтобы мы могли пройтись по ней, чтобы лучше понять, что делает RNN на каждом этапе:

In [13]:
it = iter(data.valid_dl)
x1,y1 = next(it)
x2,y2 = next(it)
x3,y3 = next(it)
it.close()
In [14]:x1
Out[14]:tensor([[ 2, 19, 11,  ..., 36,  9, 19],
        [ 9, 19, 11,  ..., 24, 20,  9],
        [11, 27, 18,  ...,  9, 19, 11],
        ...,
        [20, 11, 20,  ..., 11, 20, 10],
        [20, 11, 20,  ..., 24,  9, 20],
        [20, 10, 26,  ..., 20, 11, 20]], device='cuda:0')

numel() — это метод PyTorch для возврата количества элементов в тензоре:

In [15]:x1.numel()+x2.numel()+x3.numel()
Out[15]:13440
In [16]:x1.shape, y1.shape
Out[16]:(torch.Size([64, 70]), torch.Size([64, 70]))
In [17]:x2.shape, y2.shape
Out[17]:(torch.Size([64, 70]), torch.Size([64, 70]))
In [18]:x3.shape, y3.shape
Out[18]:(torch.Size([64, 70]), torch.Size([64, 70]))
In [19]:v = data.valid_ds.vocab
In [20]:v.itos
Out[20]:
['xxunk',
 'xxpad',
 ...
'nineteen']
In [21]:x1[:,0]
Out[21]:
tensor([ 2,  9, 11, 12, 13, 11, 10,  9, 10, 14, 19, 25, 19, 15, 16, 11, 19,  9,
        10,  9, 19, 25, 19, 11, 19, 11, 10,  9, 19, 20, 11, 26, 20, 23, 20, 20,
        24, 20, 11, 14, 11, 11,  9, 14,  9, 20, 10, 20, 35, 17, 11, 10,  9, 17,
         9, 20, 10, 20, 11, 20, 11, 20, 20, 20], device='cuda:0')
In [22]:y1[:,0]
Out[22]:
tensor([19, 19, 27, 10,  9, 12, 32, 19, 26, 10, 11, 15, 11, 10,  9, 15, 11, 19,
        26, 19, 11, 18, 11, 18,  9, 18, 21, 19, 10, 10, 20,  9, 11, 16, 11, 11,
        13, 11, 13,  9, 13, 14, 20, 10, 20, 11, 24, 11,  9,  9, 16, 17, 20, 10,
        20, 11, 24, 11, 19,  9, 19, 11, 11, 10], device='cuda:0')
In [23]:v.itos[9], v.itos[11], v.itos[12], v.itos[13], v.itos[10]
Out[23]:(',', 'thousand', 'one', 'two', 'hundred')
In [24]:v.textify(x1[0])
Out[24]:'xxbos eight thousand ... seventeen , eight'
In [25]:v.textify(x1[1])
Out[25]:', eight thousand forty ... nine ,'
In [26]:v.textify(x2[1])
Out[26]:'eight thousand s... eight thousand'
In [21]:v.textify(y1[0])
Out[21]:'eight thousand ...seventeen , eight thousand'
In [54]:v.textify(x2[0])
Out[54]:'thousand eighteen ... eight thousand thirty two ,'
In [55]:v.textify(x3[0])
Out[55]:'eight thousand thirty ... eight'
In [56]:v.textify(x1[1])
Out[56]:', eight thousand forty ...thousand fifty nine ,'
In [57]:v.textify(x2[1])
Out[57]:'eight thousand sixty , eight ...eight thousand'
In [58]:v.textify(x3[1])
Out[58]:'seventy four , eight ... thousand eighty'
In [59]:v.textify(x3[-1])
Out[59]:'ninety , nine thousand ... , eight'
In [60]:data.show_batch(ds_type=DatasetType.Valid)

Мы будем последовательно рассматривать несколько различных моделей, создавая более традиционную RNN.

Одиночная полносвязная модель

In [61]: data = src.databunch(bs=bs, bptt=3)
In [62]: x,y = data.one_batch()
         x.shape,y.shape
Out[62]: (torch.Size([64, 3]), torch.Size([64, 3]))
In [63]: nv = len(v.itos); nv
Out[63]: 39
In [64]: nh=64
In [65]:
def loss4(input,target): return F.cross_entropy(input, target[:,-1])
def acc4 (input,target): return accuracy(input, target[:,-1])
In [68]: x[:,0]
Out[68]:
tensor([13, 13, 10,  9, 18,  9, 11, 11, 13, 19, 16, 23, 24,  9, 12,  9, 13, 14,
        15, 11, 10, 22, 15,  9, 10, 14, 11, 16, 10, 28, 11,  9, 20,  9, 15, 15,
        11, 18, 10, 28, 23, 24,  9, 16, 10, 16, 19, 20, 12, 10, 22, 16, 17, 17,
        17, 11, 24, 10,  9, 15, 16,  9, 18, 11])

Имена слоев:

  • i_h: вход скрыт
  • h_h: от скрытого к скрытому
  • h_o: скрыто для вывода
  • bn: норма партии
In [67]:
class Model0(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)  # green arrow
        self.h_h = nn.Linear(nh,nh)     # brown arrow
        self.h_o = nn.Linear(nh,nv)     # blue arrow
        self.bn = nn.BatchNorm1d(nh)
        
    def forward(self, x):
        h = self.bn(F.relu(self.i_h(x[:,0])))
        if x.shape[1]>1:
            h = h + self.i_h(x[:,1])
            h = self.bn(F.relu(self.h_h(h)))
        if x.shape[1]>2:
            h = h + self.i_h(x[:,2])
            h = self.bn(F.relu(self.h_h(h)))
        return self.h_o(h)
In [69]:learn = Learner(data, Model0(), loss_func=loss4, metrics=acc4)
In [70]:learn.fit_one_cycle(6, 1e-4)

То же самое с петлей

Давайте рефакторим это, чтобы использовать цикл for. Это делает то же самое, что и раньше:

In [72]:
class Model1(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)  # green arrow
        self.h_h = nn.Linear(nh,nh)     # brown arrow
        self.h_o = nn.Linear(nh,nv)     # blue arrow
        self.bn = nn.BatchNorm1d(nh)
        
    def forward(self, x):
        h = torch.zeros(x.shape[0], nh).to(device=x.device)
        for i in range(x.shape[1]):
            h = h + self.i_h(x[:,i])
            h = self.bn(F.relu(self.h_h(h)))
        return self.h_o(h)

В этом разница между развернутой (то, что было раньше) и свернутой (то, что есть сейчас) диаграммами RNN:

In [73]: learn = Learner(data, Model1(), loss_func=loss4,   metrics=acc4)
In [74]:learn.fit_one_cycle(6, 1e-4)

Наша точность примерно такая же, поскольку мы делаем то же самое, что и раньше.

Многосвязная модель

Раньше мы просто предсказывали последнее слово в строке текста. Учитывая 70 токенов, что такое токен 71? Такой подход выбрасывал много данных. Почему бы не предсказать токен 2 из токена 1, затем предсказать токен 3, затем предсказать токен 4 и так далее? Мы изменим нашу модель, чтобы сделать это.

In [75]: data = src.databunch(bs=bs, bptt=20)
In [76]: x,y = data.one_batch()
         x.shape,y.shape
Out[76]: (torch.Size([64, 20]), torch.Size([64, 20]))
In [77]:
class Model2(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)
        self.h_h = nn.Linear(nh,nh)
        self.h_o = nn.Linear(nh,nv)
        self.bn = nn.BatchNorm1d(nh)
        
    def forward(self, x):
        h = torch.zeros(x.shape[0], nh).to(device=x.device)
        res = []
        for i in range(x.shape[1]):
            h = h + self.i_h(x[:,i])
            h = F.relu(self.h_h(h))
            res.append(self.h_o(self.bn(h)))
        return torch.stack(res, dim=1)
In [78]: learn = Learner(data, Model2(), metrics=accuracy)
In [79]: learn.fit_one_cycle(10, 1e-4, pct_start=0.1)

Обратите внимание, что наша точность сейчас хуже, потому что мы выполняем более сложную задачу. Когда мы предсказываем слово k (k‹70), у нас меньше истории, чем когда мы предсказывали только слово 71.

Поддерживать состояние

Чтобы решить эту проблему, давайте сохраним скрытое состояние предыдущей строки текста, чтобы не начинать заново с каждой новой строкой текста.

In [80]:
class Model3(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)
        self.h_h = nn.Linear(nh,nh)
        self.h_o = nn.Linear(nh,nv)
        self.bn = nn.BatchNorm1d(nh)
        self.h = torch.zeros(bs, nh).cuda()
        
    def forward(self, x):
        res = []
        h = self.h
        for i in range(x.shape[1]):
            h = h + self.i_h(x[:,i])
            h = F.relu(self.h_h(h))
            res.append(self.bn(h))
        self.h = h.detach()
        res = torch.stack(res, dim=1)
        res = self.h_o(res)
        return res
In [81]: learn = Learner(data, Model3(), metrics=accuracy)
In [82]: learn.fit_one_cycle(20, 3e-3)

Теперь мы получаем большую точность, чем раньше!

nn.RNN

Давайте реорганизуем приведенное выше, чтобы использовать RNN PyTorch. Это то, что вы использовали бы на практике, но теперь вы знаете подробности!

class Model4(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)
        self.rnn = nn.RNN(nh,nh, batch_first=True)
        self.h_o = nn.Linear(nh,nv)
        self.bn = BatchNorm1dFlat(nh)
        self.h = torch.zeros(1, bs, nh).cuda()
        
    def forward(self, x):
        res,h = self.rnn(self.i_h(x), self.h)
        self.h = h.detach()
        return self.h_o(self.bn(res))
learn = Learner(data, Model4(), metrics=accuracy)
learn.fit_one_cycle(20, 3e-3)
Total time: 00:04

2-х слойный ГРУ

Когда у вас большие временные масштабы и более глубокие сети, их невозможно обучить. Один из способов решить эту проблему — добавить мини-NN, чтобы решить, какую часть зеленой стрелки оставить, а какую — оранжевую. Эти мини-NN могут быть GRU или LSTM. Подробнее об этом мы расскажем в следующем уроке.

In [83]:
class Model5(nn.Module):
    def __init__(self):
        super().__init__()
        self.i_h = nn.Embedding(nv,nh)
        self.rnn = nn.GRU(nh, nh, 2, batch_first=True)
        self.h_o = nn.Linear(nh,nv)
        self.bn = BatchNorm1dFlat(nh)
        self.h = torch.zeros(2, bs, nh).cuda()
        
    def forward(self, x):
        res,h = self.rnn(self.i_h(x), self.h)
        self.h = h.detach()
        return self.h_o(self.bn(res))
learn = Learner(data, Model5(), metrics=accuracy)
learn.fit_one_cycle(10, 1e-2)
Total time: 00:02

Подключение к ULMFit

В предыдущем блоге мы фактически заменили self.h_o классификатором, чтобы выполнить классификацию текста.
RNN — это просто реорганизованная, полностью подключенная нейронная сеть.
Вы можете использовать тот же подход для любая задача маркировки последовательности (часть речи, классифицирующая, является ли материал чувствительным,..)

Давайте перейдем к объяснению вышеизложенного визуально

Модели с возвратом от вывода к скрытому

  • Менее мощная, чем при рекуррентных связях между скрытыми и скрытыми
    • Она не может имитировать универсальную ТМ
    • Для прогнозирования будущего требуется, чтобы выходные данные собирали всю информацию о прошлом.
  • Преимущество
    • При сравнении функции потерь с выходными данными все временные шаги разделены
    • Каждый шаг может быть обучен изолированно
    • Обучение может быть распараллелено
    • Градиент для каждого шага t вычисляется в изоляция
    • Нет необходимости сначала вычислять выходные данные для предыдущего шага, поскольку обучающая выборка обеспечивает идеальное значение выходных данных.
  • Можно тренировать с помощью Учителя Форсинга: представлено далее.

учитель принуждение

Используется для моделей обучения с рекуррентными подключениями к их выходам.
Принуждение учителя во время обучения означает
• Вместо суммирования активаций от входящих единиц (возможно, ошибочных)
• Каждая единица суммирует правильные активации учителя в качестве входных данных для следующая итерация.

Процедура принуждения учителя

  • Принуждение учителя — это процедура обучения RNN со скрытым повторением на выходе
    • Оно возникает из критерия максимального правдоподобия
    • Во время обучения модель получает исходные выходные данные y(t) в качестве входных данных в момент времени t+1 . Мы можем увидеть это, исследуя последовательность с двумя временными шагами (далее)

Максимальное правдоподобие в принуждении учителя
• Рассмотрим последовательность с двумя временными шагами
• Условный критерий максимального правдоподобия:

  • В момент времени t = 2 модель обучается максимизировать условную вероятность y (2) с учетом как последовательности x на данный момент, так и предыдущего значения y из обучающего набора.
    • Мы используем y(1) в качестве принуждения учителя, а не только x(i) .
    • Максимальная вероятность указывает, что во время обучения вместо того, чтобы возвращать собственные выходные данные модели обратно к себе, целевые значения должны указывать, какими должны быть правильные выходные данные.

Иллюстрация принуждения учителей

  • Принуждение учителя - это метод обучения, применимый к RNN, у которых есть связи между выходными данными и скрытыми состояниями на следующем временном шаге.

RNN без и с принуждением учителя

Визуализация принуждения учителя

• Представьте, что сеть учится следовать по траектории
• Она сбивается с пути (из-за неправильных весов), но учитель принудительно возвращает сеть на ее траекторию. Установив состояние всех блоков на состояние учителя.

Учитель принуждение к скрытому за скрытым

  • Первоначально мы мотивировали форсирование учителя тем, что оно позволяет нам избежать обратного распространения во времени в моделях, в которых отсутствуют связи между скрытыми
    • Форсирование учителем по-прежнему может применяться к моделям, в которых есть связи между скрытыми
    — до тех пор, пока у них есть связи от вывода на одном временном шаге к значениям, вычисленным на следующем временном шаге
    — Как только скрытые единицы станут функциями более ранних временных шагов, необходим алгоритм BPTT

Тренировки с использованием Teacher Forcing и BPTT

Некоторые модели могут быть обучены как с помощью Учителя, так и с обратным распространением во времени (BPTT). Когда есть повторения как «скрытые в скрытые», так и повторения «вывод в скрытые».

Недостаток учительского принуждения

  • Если сеть будет использоваться в режиме без обратной связи с сетевыми выходами (или выборками из выходного распределения), возвращаемыми в качестве входных данных. В этом случае тип входных данных, который он увидит во время обучения, может сильно он увидит во время теста
  • Решения для смягчения этой проблемы:
    1. Тренируйтесь как с принудительными входными данными учителя, так и с произвольными входными данными. Например, прогнозирование правильного целевого количества шагов в будущем с помощью развернутых повторяющихся путей от вывода к вводу. Таким образом, сеть может научиться принимать во внимание входные условия, которые не наблюдались во время обучения.
    2. Уменьшить разрыв между входными данными, наблюдаемыми во время обучения, и во время тестирования, генерируя значения в качестве входных данных. Этот подход использует стратегию обучения по учебной программе, чтобы постепенно использовать больше сгенерированные значения в качестве входных данных

Профессор Форсинг

• Учитель Форсинг обучает RNN, предоставляя наблюдаемые значения последовательности в качестве входных данных во время обучения и используя собственные прогнозы сети на один шаг вперед для выполнения многоэтапной выборки
• Алгоритм профессора Форсинга: использует адаптацию состязательной области для поощрения динамики RNN должен быть одинаковым при обучении сети и при выборке из сети в течение нескольких временных шагов.
Применимо к
— языковому моделированию
— голосовому синтезу на необработанных сигналах
— генерации рукописного ввода
— генерации изображений
• Профессор Форсинг — состязательный метод обучения генеративные модели. Он тесно связан с генеративно-состязательными сетями.

Конец

Кредиты:

https://www.fast.ai/