транслирование
(broadcasting), то
есть работу с двумя входными тензорами различной формы с применением элемента
входного тензора меньшей формы к нескольким элементам второго входного тензора
в соответствии с определенным правилом. См. подробное обсуждение в инфобоксе 2.4.
Б.2.3 Конкатенация и срезы тензоров
Унарные и бинарные операции относятся к типу «тензор на входе, тензор на выходе»
(tensorintensorout, TITO) в том смысле, что получают один или несколько тензо
ров в качестве входных данных и возвращают тензор в качестве выходных данных.
Некоторые часто используемые операции в TensorFlow.js не относятся к этому типу,
поскольку получают на входе тензор, наряду с другим (и) нетензорным (и) аргумен
том (ами) в качестве входных данных. Вероятно, чаще всего используемая функция
из этой категории —
tf.concat()
. С ее помощью можно «склеивать» несколько тен
зоров совместимой формы в один. Например, можно склеить тензор формы
[5,
3]
с тензором формы
[4,
3]
по первой оси и получить тензор
[9,
3]
, но невозможно
объединить тензоры форм
[5,
3]
и
[4,
2]
! Функцию
tf.concat()
можно использовать
для конкатенации тензоров при условии совместимости форм. Например, следующий
код склеивает состоящий из одних нулей тензор формы
[2,
2]
с состоящим из одних
единиц тензором формы
[2,
2]
, в результате чего получается тензор формы
[4,
2]
,
в котором «верхняя» половина заполнена нулями, а «нижняя» — единицами:
> const x = tf.zeros([2, 2]);
> const y = tf.ones([2, 2]);
> tf.concat([x, y]).print();
Tensor
[[0, 0],
[0, 0],
[1, 1],
[1, 1]]
Поскольку формы обоих входных тензоров идентичны, можно склеить их иначе:
по второй оси. Ось можно указать с помощью второго входного аргумента метода
tf.concat()
. В результате получится тензор формы
[2,
4]
, в котором левая половина
заполнена нулями, а правая — единицами:
> tf.concat([x, y], 1).print();
Tensor
[[0, 0, 1, 1],
[0, 0, 1, 1]]
566
Приложения
Помимо конкатенации нескольких тензоров в один, иногда бывает нужно произ
вести и «обратную» операцию — извлечь часть тензора. Например, пусть мы создали
двумерный тензор (матрицу) формы
[3,
2]
:
> const x = tf.randomNormal([3, 2]);
> x.print();
Tensor
[[1.2366893 , 0.6011682 ],
[-1.0172369, -0.5025602],
[-0.6265425, -0.0009868]]
и хотим извлечь ее вторую строку. Для этого можно воспользоваться цепочечной
версией метода
tf.slice()
:
> x.slice([1, 0], [1, 2]).print();
Tensor
[[-1.0172369, -0.5025602],]
Первый аргумент метода
tf.slice()
указывает, что нужная нам часть входного
тензора начинается с индекса 1 по первому измерению и индекса 0 по второму из
мерению. Другими словами, она должна начинаться со второй строки и первого
столбца, поскольку наш тензор представляет собой матрицу. Второй аргумент за
дает форму желаемых выходных данных:
[1,
2]
, или, на языке матриц, одна строка
и два столбца.
Как вы можете проверить по выводимым значениям, мы успешно извлекли
вторую строку матрицы 3
×
2. Ранг выходных данных — такой же, как и у входных
(2), но размер первого измерения — 1. В данном случае мы полностью извлекли все
второе измерение (все столбцы) и подмножество первого измерения (подмножество
строк). Это особый случай, позволяющий достичь того же результата с помощью
более простого синтаксиса:
> x.slice(1, 1).print();
Tensor
[[-1.0172369, -0.5025602],]
При таком более простом синтаксисе необходимо только указать начальный
индекс и размер нужного фрагмента по первому измерению. Если передать 2, а не 1
в качестве второго входного аргумента, выходные данные будут включать вторую
и третью строки матрицы:
> x.slice(1, 2).print();
Tensor
[[-1.0172369, -0.5025602],
[-0.6265425, -0.0009868]]
Как вы можете предположить, этот более простой синтаксис связан с соглаше
нием о батчах. Он упрощает извлечение данных для отдельных примеров данных
из тензора батча.
Но что, если нам нужно извлечь
столбцы
матрицы, а не строки? В этом случае нам
пришлось бы использовать более сложный синтаксис. Например, пусть нам нужен
второй столбец матрицы. Это можно сделать с помощью:
Приложение Б. Краткое руководство по тензорам и операциям над ними
567
> x.slice([0, 1], [-1, 1]).print();
Tensor
[[0.6011682 ],
[-0.5025602],
[-0.0009868]]
Здесь первый аргумент (
[0,
1]
) представляет собой массив, соответствующий
начальным индексам нужного среза. Это первый индекс по первому измерению
и второй индекс по второму. Проще говоря, мы хотим, чтобы срез начинался с пер
вой строки и второго столбца. Второй аргумент (
[-1,
1]
) задает размер нужного нам
среза. Первое число указывает, что мы хотим получить данные для всех индексов
по первому измерению (все строки), а второе число (1) означает, что мы хотим
получить данные только для одного индекса по второму измерению (только один
столбец). Результат представляет собой второй столбец матрицы.
Из синтаксиса метода
slice()
вы, наверное, поняли, что он подходит для из
влечения не только строк и столбцов. На самом деле он достаточно гибок для
извлечения произвольной «подматрицы» входного двумерного тензора (любой
последовательной прямоугольной области в матрице), если начальные индексы
и размер массива указаны должным образом. В общем случае для тензоров произ
вольного ранга > 0 метод
slice()
позволяет извлечь произвольный непрерывный
подтензор того же ранга из входного тензора. Мы оставим это в качестве упражне
ния для читателей.
Помимо методов
tf.slice()
и
tf.concat()
, существуют еще две часто исполь
зуемые операции для разбиения тензора на части или объединения нескольких
тензоров в один:
tf.unstack()
и
tf.stack()
. Метод
tf.unstack()
разбивает тензор
на несколько частей по первому измерению, каждая — размером 1 (по первому изме
рению). Например, можно воспользоваться цепочечным API
tf.unstack()
, вот так:
> const x = tf.tensor2d([[1, 2], [3, 4], [5, 6]]);
> x.print();
Tensor
[[1, 2],
[3, 4],
[5, 6]]
> const pieces = x.unstack();
> console.log(pieces.length);
3
> pieces[0].print();
Tensor
[1, 2]
> pieces[1].print();
Tensor
[3, 4]
> pieces[2].print();
Tensor
[5, 6]
Как вы могли заметить, ранг возвращаемых методом
unstack()
частей на единицу
меньше, чем у входного тензора.
568
Приложения
Метод
tf.stack()
— противоположность метода
tf.unstack()
. Как ясно из его
названия, он «складирует» несколько тензоров одинаковой формы в новый тензор.
Продолжая предыдущий фрагмент кода, мы можем вот так совместить части тензора
обратно:
> tf.stack(pieces).print();
Tensor
[[1, 2],
[3, 4],
[5, 6]]
Метод
tf.unstack()
удобен для извлечения из тензора батча данных, соответ
ствующих отдельным примерам данных; метод
tf.stack()
удобен для объединения
отдельных примеров данных в тензор батча.
Б.3. Управление памятью в TensorFlow.js:
tf.dispose() и tf.tidy()
При работе непосредственно с тензорными объектами в TensorFlow.js необходимо
брать на себя управление выделяемой для них памятью. В частности, тензоры не
обходимо удалять после создания и использования, иначе они продолжат занимать
выделенную для них память. В случае слишком большого числа неудаленных тен
зоров или слишком большого их суммарного размера в конце концов закончится
либо память WebGL, доступная вкладке браузера, либо системная память/память
GPU, доступная процессу Node.js (в зависимости от того, какая версия tfjsnode ис
пользуется: CPU или GPU). TensorFlow.js не производит автоматической сборки
мусора для создаваемых пользователями тензоров
1
. Причина в том, что JavaScript
не поддерживает финализации объектов. TensorFlow.js предоставляет две функции
для управления памятью:
tf.dispose()
и
tf.tidy()
.
Например, рассмотрим сценарий с повторяемым многократно с помощью цикла
for
выводом на основе модели TensorFlow.js:
1
Впрочем, управление памятью тензоров, создаваемых внутри функций и методов объек
тов TensorFlow.js, осуществляет сама библиотека, так что не нужно заботиться об обер
тывании вызовов подобных функций/методов в tf.tidy(). Примеры подобных функций:
tf.confusionMatrix(), tf.Model.predict() и tf.Model.fit().
Приложение Б. Краткое руководство по тензорам и операциям над ними
569
Результаты выполнения этого кода выглядят следующим образом:
Tensor
[[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 14
Tensor
[[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 15
Tensor
[[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 16
Как вы можете видеть из выведенного в консоль,
model.predict()
создает при
каждом вызове дополнительный тензор, не удаляемый после завершения итера
ции. Если этот цикл
for
будет выполняться в течение достаточно большого числа
итераций, то в конце концов приведет в ошибке нехватки памяти. А все потому, что
выходной тензор
y
не удаляется должным образом, что приводит к утечке памяти.
Исправить ситуацию можно двумя способами.
Первый вариант — вызывать метод
tf.dispose()
выходного тензора, когда он
больше не нужен:
Второй вариант — обернуть тело цикла
for
в вызов
tf.tidy()
:
При любом из этих подходов вы увидите, что число выделенных тензоров
не меняется по мере итераций, а значит, утечки памяти нигде нет. Какой подход
предпочтительнее? В принципе, лучше использовать метод
tf.tidy()
(второй под
ход), поскольку при этом не нужно отслеживать требующие удаления тензоры.
tf.tidy()
— «умная» функция, удаляющая все тензоры, созданные внутри переда
ваемой в него анонимной функции (за исключением тензоров, возвращаемых этой
функцией, — подробнее об этом далее), даже тензоры, не связанные ни с какими
объектами JavaScript. Например, слегка изменим предыдущий код вывода, чтобы
получать индекс наилучшего класса с помощью
argMax()
:
const model = await tf.loadLayersModel(
'https://storage.googleapis.com/tfjs-models/tfjs/iris_v1/model.json');
const x = tf.randomUniform([1, 4]);
for (let i = 0; i < 3; ++i) {
570
Приложения
const winningIndex =
model.predict(x).argMax().dataSync()[0];
console.log(`winning index: ${winningIndex}`);
console.log(`# of tensors: ${tf.memory().numTensors}` );
}
При работе этого кода вы увидите, что происходит утечка памяти, выделенной
не для одного тензора, а для двух:
winning index: 0
# of tensors: 15
winning index: 0
# of tensors: 17
winning index: 0
# of tensors: 19
Почему происходит утечка памяти, выделенной для двух тензоров на каждой
итерации? Дело в том, что строка:
const winningIndex =
model.predict(x).argMax().dataSync()[0];
приводит к созданию двух новых тензоров. Первый из них — результат работы
метода
model.predict()
, а второй — возвращаемое
argMax()
значение. Ни один из
них не связан ни с какими объектами JavaScript. Они используются сразу же после
создания. Эти два тензора «теряются» в том смысле, что не существует JavaScript
объектов, с помощью которых можно было бы на них сослаться. А потому нельзя
воспользоваться для их удаления методом
tf.dispose()
. А вот с помощью
tf.tidy()
попрежнему можно устранить эту утечку памяти, поскольку она ведет учет новых
тензоров, вне зависимости от того, связаны ли они с какимито объектами JavaScript:
В этом примере использования
tf.tidy()
функция не возвращает никаких тензо
ров. Если же функция возвращает тензоры, удалять их нежелательно, ведь они пона
добятся позднее. Эта ситуация часто встречается при написании пользовательских
операций с тензорами на основе базовых операций TensorFlow.js. Например, пусть
нам нужно написать функцию вычисления нормализованного значения входного
тензора — то есть тензора с вычтенным средним значением и приведенным к 1
среднеквадратичным отклонением:
function normalize(x) {
const mean = x.mean();
Приложение Б. Краткое руководство по тензорам и операциям над ними
571
const sd = x.norm(2);
return x.sub(mean).div(sd);
}
В чем основная проблема этой реализации?
1
С точки зрения управления памятью
здесь происходит утечка памяти, выделенной для трех тензоров: 1) тензора среднего
значения; 2) тензора среднеквадратичного отклонения; 3) (более тонкий нюанс)
тензора возвращаемого значения вызова
sub()
. Для устранения этой утечки памяти
обернем тело функции в
tf.tidy()
:
function normalize(x) {
return tf.tidy(() => {
const mean = x.mean();
const sd = x.norm(2);
return x.sub(mean).div(sd);
});
}
В этом коде
tf.tidy()
производит три действия.
z
z
Автоматически удаляет тензоры, созданные в анонимной функции, но не воз
вращаемые ею, включая все три упомянутых источника утечек. Мы видели это
в предыдущих примерах.
z
z
Определяет, что анонимная функция возвращает результат вызова
div()
, а по
тому переносит его в свое собственное возвращаемое значение.
z
z
Вместе с тем удалять этот конкретный тензор она не станет, так что его можно
будет использовать снаружи вызова
tf.tidy()
.
Как можно видеть,
tf.tidy()
— «умная» и обладающая большими возможно
стями функция управления памятью. Она широко используется в базе кода самого
TensorFlow.js. Также вы не раз встретитесь с ней в примерах в этой книге. Впрочем,
у нее есть одно важное ограничение: передаваемая в
tf.tidy()
анонимная функция
не
может быть асинхронной. Для требующего управления памятью асинхронного кода
вам придется воспользоваться
tf.dispose()
и самостоятельно отслеживать тензоры,
требующие удаления. В подобных случаях можно воспользоваться
tf.memory().
num Tensor
для проверки количества тензоров, вызывающих утечки. Рекомендуется
также писать модульные тесты для контроля отсутствия утечек памяти.
Б.4. Вычисление градиентов
Этот раздел предназначен для тех читателей, кого интересуют возможности вычис
ления производных и градиентов в TensorFlow.js. В большинстве моделей глубокого
обучения в этой книге вычисление производных и градиентов производится «под
1
У этой реализации есть и другие проблемы. Например, в ней не производится проверка
допустимости входного тензора: необходимо проверить, что он содержит хотя бы два эле
мента, иначе среднеквадратичное отклонение будет равно нулю, что приведет к делению на
ноль и к ошибке. Но эти проблемы с обсуждаемым здесь вопросом напрямую не связаны.
572
Приложения
капотом»
model.fit()
и
model.fitDataset()
, так что заботиться об этом не надо.
Впрочем, в определенных задачах, например поиска максимально активирующих
нейроны изображений для сверточных фильтров в главе 7 и обучения с подкрепле
нием в главе 11, приходится вычислять производные и градиенты явным образом.
TensorFlow.js предоставляет API для подобных сценариев использования. Начнем
с простейшего сценария — функции, принимающей на входе один тензор и возвра
щающей на выходе также один тензор:
const f = x => tf.atan(x);
Для вычисления производной функции
(f)
по входной переменной
(x)
мы вос
пользуемся функцией
tf.grad()
:
const df = tf.grad(f);
Обратите внимание, что функция
tf.grad()
не возвращает непосредственно
значение производной, а возвращает
функцию
, которая является производной от
исходной функции
(f)
. А уже эту функцию
(df)
можно вызвать с конкретным зна
чением
x
и получить значение
df/dx
. Например, результат выполнения:
const x = tf.tensor([-4, -2, 0, 2, 4]);
df(x).print();
дает нам правильное значение производной функции
atan()
в точках –4, –2, 0, 2,
и 4 по
x
(рис. Б.5):
Tensor
[0.0588235, 0.2, 1, 0.2, 0.0588235]
tf.grad()
можно использовать только для функций с одним тензором на входе.
А что делать с функцией с несколькими входными тензорами? Рассмотрим пример
функции
h(x,
y)
, равной просто произведению двух тензоров:
const h = (x, y) => x.mul(y);
Сгенерировать функцию, возвращающую частные производные входной функ
ции по всем аргументам, можно с помощью метода
tf.grads()
(c s в конце):
const dh = tf.grads(h);
const dhValues = dh([tf.tensor1d([1, 2]), tf.tensor1d([-1, -2])]);
dhValues[0].print();
dhValues[1].print();
который возвращает следующие результаты:
Tensor
[-1, -2]
Tensor
[1, 2]
Эти результаты правильны, поскольку частная производная
Do'stlaringiz bilan baham: |