Видео проигрыватель загружается.Воспроизвести видеоВоспроизвестиБез звукаТекущее время 0:00/Продолжительность 9:20Загрузка: 0.00%0:00Тип потока ОНЛАЙНSeek to live, currently behind liveОНЛАЙНОставшееся время -9:20 1xСкорость воспроизведения2x1.75x1.5x1.25x1x, выбрано0.75x0.5xГлавыГлавыОписанияОтключить описания, выбраноСубтитрынастройки субтитров, откроется диалог настройки субтитровСубтитры выкл., выбраноЗвуковая дорожкаPicture-in-PictureПолноэкранный режимThis is a modal window.Начало диалоговго окна. Кнопка Escape закроет или отменит окноТекстColorБелыйЧерныйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачностьПолупрозрачныйФонColorЧерныйБелыйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачностьПолупрозрачныйПрозрачныйОкноColorЧерныйБелыйКрасныйЗеленыйСинийЖелтыйПурпурныйГолубойTransparencyПрозрачныйПолупрозрачныйПрозрачностьРазмер шрифта50%75%100%125%150%175%200%300%400%Стиль края текстаНичегоПоднятыйПониженныйОдинаковыйТеньШрифтПропорциональный без засечекМоноширинный без засечекПропорциональный с засечкамиМоноширинный с засечкамиСлучайныйПисьменныйМалые прописныеСбросить сбросить все найстройки по умолчаниюГотовоЗакрыть модальное окноКонец диалогового окна.
Более практичный способ декодирования текста (он даёт более хорошие результаты, более качественные тексты) — это использование лучевого поиска. Упрощённый вариант лучевого поиска мы реализовали в классе "BeamGenerator". У него точно такой же интерфейс, как и у "GreedyGenerator". Давайте посмотрим, как он работает. На экране вы видите код лучевого поиска. На вход функция принимает исходный текст, а также параметры лучевого поиска. Наибольшее количество шагов — это, по сути, наибольшее количество токенов, которые мы можем добавить к исходной последовательности, это количество лучших гипотез, лучших вариантов декодирования, которое нам нужно вернуть из этой функции. Ширина луча — это количество наилучших промежуточных вариантов, которое мы будем хранить в процессе декодирования. Как и в прошлый раз, начинаем мы с того, что преобразовываем текст в последовательность токенов. Две самые важные переменные, которые мы будем обновлять в процессе генерации: первая — это список промежуточных гипотез (или частичных гипотез). Этот список будет содержать пары (то есть кортежи из двух элементов). На первом месте кортежа будет стоять вес, то есть какая-то оценка правдоподобности этой гипотезы, а на втором месте — собственно, сама гипотеза (в виде списка токенов). На самом деле, эта переменная — это не просто список. Мы будем поддерживать эту переменную в виде очереди с приоритетами, то есть мы будем её пересортировывать после добавления нового элемента каждый раз, чтобы на первом месте стояла гипотеза с наилучшим score (с наилучшей оценкой правдоподобности). Второй важный список — это список готовых гипотез, то есть, когда мы уже либо сделали наибольшее количество шагов, либо мы дошли до конца последовательности, то мы прекращаем генерировать из данной гипотезы и мы перемещаем её в список готовых гипотез. Это список, из которого будет формироваться результат работы этой функции. Для того, чтобы было удобно реализовывать очереди с приоритетами, в python есть пакет "heap queue", то есть это "очередь куча". В нём есть базовые операции для работы с кучей и, соответственно, с очередью с приоритетами. Если вы не знаете, что такое "куча" и "очередь с приоритетами", то мы оставим ссылку на материалы, которые вы можете почитать и освежить свои знания в этой области. Функция "heap pop" возвращает нам элемент с головы кучи, то есть это элемент с наименьшим текущем скором. То есть библиотека "heap queue" реализовывает кучу на минимум. То есть, на вершине кучи лежит наименьший элемент. Эта функция возвращает нам элемент списка "partial hypothesis". Как мы помним, этот список содержит кортежи. На первом месте кортежа стоит score, на втором месте кортежа стоит гипотеза в виде списка токенов. Следующим шагом мы кладём нашу текущую гипотезу в модель и получаем новое предсказание для последнего токена. Здесь мы не можем работать с исходными логитами, нам нужно как-то нормировать. Но сами вероятности от нуля до единицы нам не очень удобны, потому что правдоподобность целой гипотезы будет считаться как произведение вероятностей. P(A)*P(B)*P(C)... и, таким образом, если у нас хотя бы одна из этих вероятностей достаточно маленькая, то всё произведение устремится к нулю. Это очень неудобно — мы очень быстро выйдем за пределы точности вычислений и эта оценка правдоподобия станет бесполезной. Вместо этого мы будем использовать log-вероятность. Напомню, что, если мы работаем с логарифмированными вероятностями, то у нас произведение заменяется на сумму. Да, эти логарифмы — это большие по модулю отрицательные числа, но, с помощью операции сложения, нам гораздо сложнее выйти за пределы точности, и поэтому, на практике, используют часто именно логарифмированные вероятности. Чтобы получить логарифмированные вероятности мы применяем не "softmax", а "log softmax". А затем выбираем k токенов с наибольшей log-вероятностью из этого списка. Далее мы итерируемся по списку k лучших вариантов и добавляем новые гипотезы в нашу очередь. Как мы это делаем? Во-первых, мы преобразовываем тензоры в числа — так, чтобы не тащить за собой объекты pytorch. Это нам экономит память, если бы мы этого не делали, то у нас бы утекала память. Затем нам нужно посчитать новую оценку правдоподобности гипотезы. Оценку правдоподобности гипотезы мы считаем как log-вероятность этой гипотезы, делённую на корень из длины этой гипотезы в токенах. Такую дополнительную нормализацию, то есть деление на корень из длины, обычно используют для того, чтобы в процессе поиска мы не предпочитали слишком короткие гипотезы, потому что понятно — когда мы перемножаем много вероятностей или складываем много отрицательных чисел — score, в принципе, сильно падает, и поэтому более короткие гипотезы априорно оказываются более вероятными. Но мы этого не хотим, поэтому мы делим score на корень из длины. Далее мы обновляем нашу гипотезу, то есть дописываем в неё токены, и кладём эту гипотезу либо в список финальных гипотез (если эта гипотеза уже достаточно длинная или мы на этом шаге вы выбрали токен конца последовательности), либо мы кладём её в нашу очередь, когда мы говорим, что — "ага, начиная с этой гипотезы мы можем продолжить наш поиск". Далее мы обрезаем нашу очередь. То есть мы оставляем в нашей очереди только заданное количество наилучших гипотез. Если бы мы этого не делали, то наш поиск, в принципе, бы выродился в полный перебор и нам бы не хватило никакой памяти для этого. Если "beam_size" равен единице, то, по сути, "beam search" откатывается, деградируя до "полностью жадного"[1] алгоритма. Поэтому мы должны регулировать значением параметра "beam_size" то, насколько мы хотим перебирать разные варианты. Чем больше "beam_size", тем больше времени мы потратим, тем больше памяти мы потратим. Но, скорее всего, мы получим более правдоподобный текст (хотя — не факт, если наша модель переобучилась, то необязательно мы получим более правдоподобный текст). Такие итерации мы повторяем до тех пор, пока наша очередь не пуста. А именно — она пополняется тогда, когда мы кладём в неё частичные гипотезы, не очень длинные — не законченные фрагменты текста. Она перестаёт пополняться тогда, когда у нас уже накапливаются очень длинные гипотезы или когда мы постепенно выбираем в качестве продолжения токен конца последовательности. Итак, пара операций, которые нам нужно сделать уже после цикла. Мы накопили какой-то список финальных гипотез, нам нужно декорировать их в тексты и выбрать заданное количество наилучших гипотез — всё это возвращается назад. То есть, эта функция возвращает нам список пар (вот она возвращает нам список пар). Первый элемент пары — это score, то есть оценка правдоподобности текста, и второй элемент — это, собственно, сам текст. Необходимо обратить внимание на вот это место — у нас token_score — это отрицательное число, это логарифм вероятности, и чем это число меньше (то есть, чем оно больше по модулю), тем менее вероятен этот токен. А мы помним, что модуль "heap queue" в python реализует кучу на минимум. Поэтому нам нужно, как бы, накапливать score со знаком "минус" — так, чтобы минимальный score был у самой правдоподобной гипотезы. Поэтому мы здесь используем минус — мы вычитаем из накопленного score, score текущего токена.
[1] Тема жадного декодирования и лучевого поиска также будет рассматриваться позднее в лекции про seq2seq
К сожалению, у нас пока нет статистики ответов на данный вопрос,
но мы работаем над этим.