В Части 1 этого сообщения в блоге я объяснил, как работает адаптивный softmax и как он может ускорить вашу языковую модель до 1000%. В части 2 я шаг за шагом проведу вас через реализацию Pytorch (с прилагаемой записной книжкой Jupyter), в которой используется встроенная в Pytorch функция AdaptiveLogSoftmaxWithLoss.

Для предварительной обработки вам понадобится fastai (см. Https://docs.fast.ai/), библиотека глубокого обучения, которая работает поверх Pytorch и упрощает обучение нейронных сетей. [Тем, кто хочет изучить современные методы глубокого обучения, я настоятельно рекомендую курс Джереми Ховарда fast.ai, который доступен в Интернете бесплатно: https://course.fast.ai/]. Я решил использовать набор данных Wikitext-2, который представляет собой относительно небольшой набор данных, содержащий ~ 2 миллиона токенов и ~ 33 тысячи слов в словаре. После того, как данные были загружены и правильно отформатированы в файлах csv, fastai упрощает быструю разметку, числовую оценку и создание загрузчика данных, который будет использоваться для обучения. Я также загрузил Предварительно обученные векторы слов в GloVe, которые используются для встраивания слов в модели. Наконец, я создал класс разработчика моделей, который занимается обучением.

Для этой модели я создал простую версию квази-рекуррентной нейронной сети (QRNN), которая имеет лучшую точность прогнозирования, чем традиционные рекуррентные нейронные сети (RNN), и работает до 16 раз быстрее (см. Bradbury et al., 2016) . Сеть, которую я построил, относительно небольшая, всего с 4 слоями и 300 скрытыми модулями на слой ... Моя цель состояла в том, чтобы создать сеть, которая может относительно быстро достичь приличного (если не современного) результата. Использование градиентного отсечения позволяет использовать большую скорость обучения (lr = 1), что вместе с импульсом Нестерова помогает тренировкам быстро сходиться.

Часть 1 описывает, как данные перемещаются по сети. Выходные данные x последнего слоя QRNN имеют размер [seq_length = 70, bs = 50, nh = 300], где seq_length - это длина последовательности, bs - размер пакета, а nh - размерность последнего слоя.

x, new_h = self.rnn(x, self.hidden)

Обычный Softmax

Для обычного softmax следующим шагом после последнего слоя QRNN является полностью связанный линейный слой (здесь называется fc1), который преобразует выходные данные, чтобы иметь количество выходных функций, равное размеру словаря ( поэтому переход от размера [seq_length, bs, nh] к размеру [seq_length, bs, vs], где vs - размер вашего словаря ). Для большинства языковых моделей эта операция доминирует во времени вычислений.

#self.fc1 = nn.Linear(nh, vs)
x= self.fc1(x)

Затем модель выводит log-softmax этих значений, и отрицательное логарифмическое правдоподобие используется для вычисления потерь.

return F.log_softmax(x.view(-1,vs),dim=1)
...
loss = self.criterion(output, target)

Адаптивный Softmax

Подход адаптивного softmax несколько иной. Выходные данные x последнего слоя QRNN по-прежнему имеют размер [seq_length = 70, bs = 50, nh = 300]. Затем размер вывода изменяется до [seq_length * bs, nh] и передается в функцию AdaptiveLogSoftmaxWithLoss, которая вычисляет убытки за вас.

x,new_h = self.rnn(x, self.hidden)
x=x.view(-1,x.size()[2])
#self.out=nn.AdaptiveLogSoftmaxWithLoss(nh, vs, cutoffs=[round(vs/15), 3*round(vs/15)], div_value=4)
return self.out(x,target)
...
loss= output.loss

Давайте посмотрим, что делает эта функция AdaptiveLogSoftmaxWithLoss. Вы можете видеть, что я указал отсечки = [раунд (vs / 15), 3 * раунд (vs / 15)]. Это означает, что я разбиваю свой словарь из 25520 слов на 3 группы. Головной кластер содержит 1701 наиболее распространенное слово (т.е. 1/15 словарного запаса) плюс два «слова», чтобы указать вероятность наличия двух хвостовых кластеров, всего 1703 слова. Первый хвостовой кластер содержит следующие 3402 наиболее употребительных слова (т. Е. 2/15 словарного запаса), а второй хвостовой кластер содержит оставшиеся 20417 наименее употребляемых слов (т. Е. 12/15 словарного запаса). Чтобы это сработало, мне нужно было убедиться, что мой словарный запас (здесь называется itos) был организован в порядке от наиболее употребительных до наименее распространенных слов.

Как я упоминал в Части 1, каждому из этих кластеров дается разная емкость, относительные размеры которой указываются в div_value, передаваемом функции AdaptiveLogSoftmaxWithLoss. Значение div_value, равное 4, означает, что емкость каждого дополнительного кластера будет в 4 раза меньше, чем у предыдущего кластера. В этом случае головной кластер имеет емкость 300, первый хвостовой кластер имеет емкость 75, а второй хвостовой кластер имеет емкость только 18. Это означает, что для наиболее распространенных слов в головном кластере он будет напрямую преобразовать размерность с 300 до 1703 выходных характеристик (поскольку 1703 - это количество словарных слов в заголовке). Однако для первого хвостового кластера исходный линейный слой сначала снизит размерность до 75 объектов, прежде чем второй линейный слой преобразует размерность до 3402 выходных объектов. Первоначальное уменьшение размерности ускоряет время обработки (по сравнению с прямым линейным преобразованием с 300 входных функций до 3402 выходных функций). Аналогичный процесс происходит для наименее распространенных слов в последнем хвостовом кластере.

(out): AdaptiveLogSoftmaxWithLoss(
    (head): Linear(in_features=300, out_features=1703, bias=False)
    (tail): ModuleList(
      (0): Sequential(
        (0): Linear(in_features=300, out_features=75, bias=False)
        (1): Linear(in_features=75, out_features=3402, bias=False)
      )
      (1): Sequential(
        (0): Linear(in_features=300, out_features=18, bias=False)
        (1): Linear(in_features=18, out_features=20417, bias=False)
      )
    )

Полученные результаты

Традиционная сеть softmax имеет окончательные потери 4,84 (недоумение = 127), тогда как адаптивная сеть softmax имеет окончательные потери 4,88 (недоумение = 132). Это составляет менее 1% разницы в окончательном проигрыше (и 4% разницы в затруднении). С другой стороны, адаптивная сеть softmax завершила вычисления за 7,80 минуты (7 минут 48 секунд), тогда как традиционная сеть softmax завершила расчет за 24,55 минуты. Таким образом, результаты двух подходов очень похожи с точки зрения точности, но адаптивная сеть softmax работает в 3 раза быстрее! Эти результаты совпадают с экспериментом Grave et al. С набором данных Text8 (который имеет такой же размер, что и набор данных Wikitext-2, который я использую)… почти идентичные окончательные потери с почти трехкратным ускорением по сравнению с полным softmax.

Так насколько же хорош адаптивный softmax по сравнению с традиционным softmax? Если бы я решил провести тренировки на большее количество эпох, вполне возможно, что окончательная потеря разошлась бы еще больше. Однако Grave et al. (2017) и другие исследователи НЛП (например, Dauphin et al., 2016) показывают, что они действительно могут использовать адаптивный softmax для достижения современных результатов языкового моделирования на различных наборах данных. Поэтому вам обязательно стоит подумать о добавлении адаптивного softmax в ваш набор инструментов языкового моделирования, поскольку он обеспечивает значительное ускорение по сравнению с полным softmax с минимальными затратами на точность. См. Часть 1 для полного объяснения того, как работает адаптивный softmax.

Использованная литература:

Брэдбери, Джеймс и др. «Квазипериодические нейронные сети. Препринт arXiv arXiv: 1611.01576 (2016). »

Дофин, Янн Н. и др. «Языковое моделирование с помощью закрытых сверточных сетей. Препринт arXiv arXiv: 1612.08083 (2016). »

Могила, Эдуард и др. «Эффективное приближение softmax для графических процессоров. Препринт arXiv arXiv: 1609.04309v3 (2017). »