Видео проигрыватель загружается.Воспроизвести видеоВоспроизвестиБез звукаТекущее время 0:00/Продолжительность 8:46Загрузка: 0.00%0:00Тип потока ОНЛАЙНSeek to live, currently behind liveОНЛАЙНОставшееся время -8:46 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%Стиль края текстаНичегоПоднятыйПониженныйОдинаковыйТеньШрифтПропорциональный без засечекМоноширинный без засечекПропорциональный с засечкамиМоноширинный с засечкамиСлучайныйПисьменныйМалые прописныеСбросить сбросить все найстройки по умолчаниюГотовоЗакрыть модальное окноКонец диалогового окна.
Давайте копнём чуть чуть глубже — посмотрим, как же можно реализовать свёртки своими руками. Для этого давайте опишем класс — все pytorch-модули наследуются от базового класса "nn.module", давайте реализуем не весь, но основной функционал стандартного модуля "nn.conv1d". При этом, давайте будем, по возможности, делать так, чтобы наш модуль можно было использовать как замену — один в один. Итак, входные параметры — это количество исходных каналов, количество результирующих каналов, размер ядра и размер паддинга. В конструкторе а мы создаём два тензора — это наши веса. "self.weight" — это ядро свёртки. В отличие от стандартного модуля, мы будем использовать здесь прямоугольную матрицу, в этой матрице количество строк равно количеству входных каналов умноженному на размер свёртки (на размер ядра), а количество столбцов этой матрицы равно количеству выходных каналов. Инициализируем этот тензор мы шумом из нормального распределения с маленькой дисперсией. Также у нас есть второй тензор параметров — это смещение. Это просто вектор размерности, равной количеству выходных каналов, его мы инициализируем нулями. Теперь давайте посмотрим на метод "forward". На вход одномерные свёртки принимают трёхмерные тензоры, первая размерность которых соответствует размеру батча, вторая — количеству входных каналов, и третья — длине последовательности. А в качестве результата возвращают также трёхмерный тензор размерности ["количество элементов в батче", "количество выходных каналов" на "новую длину последовательности"]. Она может либо остаться прежней, либо уменьшится, либо увеличиться — это зависит от размеров ядра и от паддинга. Сначала мы делаем паддинг (вот этот кусок кода отвечает за паддинг). Здесь мы реализовали паддинг только нулями. В случае одномерных свёрток, паддинг заключается в том, что мы увеличиваем третье измерение (удлиняем тензор по третьему измерению — то есть, по длине последовательности) на указанное количество элементов, как спереди, так и сзади. Для этого мы создаём тензор нулей, а затем — конкатенируем его вместе с исходным тензором признаков, так, чтобы паддинги были и в начале тензора, и в конце. И нам нужно вычислить заново длину последовательности, переприсвоить переменной. Далее мы подготавливаем матрицу признаков. Как мы это делаем? Вот этот кусок кода отвечает за подготовку признаков. Предположим, что на вход нам пришла вот такая матрица. В ней каждый столбец соответствует какому-то элементу последовательности. Давайте их условно перенумеруем "1, 2, 3, 4, 5". И у нас ядро свёртки, допустим, равно трём. Тогда мы из исходной матрицы сформируем новую матрицу, в которой количество столбцов — новое, оно вычисляется как длина исходной последовательности минус размер ядра плюс 1 (длина исходной последовательности, конечно, берётся уже с учётом паддинга). Для примера — допустим, что у нас нету паддинга. Итак — как мы будем готовить матрицу признаков? Мы будем перебирать все смещения от 0 до размера ядра - 1 и для каждого смещения мы будем брать фрагмент исходной матрицы, начиная с этого смещения, длины равной результирующей длине. Давайте на примере — для исходной последовательности длины 5 и ядра свёртки 3, длина выходной последовательности будет также равна 3 (то есть, chunk size будет равен 3). Тогда — перебирать все смещения от 0 до 2 и выбирать фрагмент исходной матрицы (сначала мы берём такую под-матрицу, затем берём начиная со второго элемента, затем — начиная с третьего элемента). Потом, когда мы перебрали все возможные смещения, мы объединяем все эти кусочки в одну матрицу, причём объединяем её по второму измерению, то есть по измерению признаков — мы получаем, в итоге, вот такую новую матрицу, в ней три столбца, а количество строк равно количеству исходных каналов умноженное на размер ядра свёртки (здесь у нас, по сути, значения будут такие). Таким образом, мы реализовали скользящее окно, которым мы идём по данным. То есть, каждый столбец этой матрицы содержит признаки всех данных, которые попадают в скользящее окно, которым мы идём по исходной последовательности и преобразовываем исходную последовательность в каждом окне. После объединения кусочков в одну матрицу признаков мы её транспонируем и применяем ядро свёртки. Делать мы это будем с помощью функции "torch.bmm" ("BMM" расшифровывается как "batch matrix multiplication", то есть это пакетное матричное перемножение). эта функция принимает на вход два трёхмерных тензора и, для каждой пары матриц в этих тензорах, делает матричное умножение. Первый тензор — это признаки, которые мы только что составили из кусочков, и второй тензор — это наше ядро свёртки, но наше ядро свёртки — это матрица, а нам нужно получить трёхмерный тензор. Для этого мы добавляем фиктивное нулевое измерение, соответствующее размеру батча и вызываем функцию "expand" ("expand" изменяет размер тензора, но, при этом, она делает это без выделения дополнительной памяти, то есть снаружи выглядит, что тензор большой, а на самом деле это — просто плоская матрица). Итак, ядро свёртки применили, теперь добавляем смещение и транспонируем полученную матрицу так, чтобы у нас смысл измерений в результирующем тензоре соответствовал смыслу измерений во входном тензоре, то есть — сначала размер батча, потом количество каналов, а потом — пространственные измерения, то есть длина последовательности. Всё, это дело мы передаём дальше. Давайте попробуем обучить модель для определения частей речи токенов, но свёрточный модуль заменим на наш свёрточный модуль, возьмём за основу модель, которая учитывает контекст токенов, и создадим экземпляр этой модели, и передадим туда все те же самые параметры, которые передавали и раньше, но добавим ещё один — а именно, скажем ей "используй, пожалуйста, наш модуль — не стандартный nn.conv1d из pytorch, а наш модуль, который мы только что описали". Как мы видим, количество параметров в результате мы получили ровно такое же. Обучаем нашу модель, используя ту же самую функцию. Любопытно, что модель, которая использует наш свёрточный модуль, на одну эпоху требует 52 секунды. А модель, которая использовала стандартный свёрточный модуль требовала примерно 120 секунд на одну эпоху, то есть получается, что наш модуль чуть-чуть побыстрее. Эта разница имеет значение только для той версии pytorch, которую используем мы сейчас, в другой версии pytorch разница может быть совершенно другая. Наиболее вероятно, что это ускорение вызвано тем, что наша реализация свёрток узкоспециализирована, в ней нету кучи разных вариантов паддинга, в ней нет механизма прореживания ядра, а также нет страйдов — в ней нельзя задавать шаг, с которым нужно идти скользящим окном по исходной последовательности. Другими словами, она проще гораздо, чем стандартная реализация свёрток. Таким образом, иногда имеет смысл что-то реализовать самому, но надо помнить, что в машинном обучении, часто, гораздо важнее быстро проверять разные гипотезы, а для этого лучше использовать стандартные модули, которые проверены, работают надёжно в разных ситуациях, они универсальны и вам не нужно тратить время на написание своих модулей. Свои модули имеет смысл писать только тогда, когда вы точно знаете ту архитектуру, которая вам нужна для решения вашей прикладной задачи, и вы хотите её ускорить. Например — для того, чтобы уметь запускать её на мобильном телефоне.
К сожалению, у нас пока нет статистики ответов на данный вопрос,
но мы работаем над этим.