В
Tensorflow это слово можно воспринимать как поток тензоров, и действительно,
здесь это, фактически, означает поток вычислений с многомерными матрицами. Да, тензоры
в Tensorflow представляются именно как многомерные матрицы, а не математические
тензоры. Поэтому те, кто знаком с математикой тензоров следует быть аккуратными
с их восприятием при работе в этом пакете. Подробную документацию по ним можно
посмотреть по ссылке:
https://www.tensorflow.org/guide/tensor
На этом занятии мы с вами познакомимся со
способом определения тензоров и некоторыми базовыми операциями с ними.
Для
тех, кто ранее работал с версией Tensorflow 1.x хочу отметить,
что в Tensorflow 2.x по умолчанию включен режим
активного выполнения (executingeagerly). В этом можно убедиться, выполнив
команду:
print(tf.executing_eagerly() )
В консоли увидим значение True. Этот режим очень
удобен для небольших проектов, так как позволяет вычислять значения переменных
сразу после выполнения соответствующих команд. Это упрощает отладку и
обеспечивает наглядность работы с Tensorflow.
Определение тензоров
Но
вернемся к тензорам. В самом простом случае их можно определить как
константные, неизменяемые объекты, используя функцию
tf.constant(value,
dtype=None, shape=None, name="Const")
-
value
– значения
тензора;
-
dtype
– тип
данных тензора;
-
shape
– размерность
тензора;
-
name
– имя
тензора.
Например,
через эту функцию можно задать тензор, содержащий одно число:
a = tf.constant(1)
print(a)
В
консоли увидим, что это объект типа Tensor со значением 1,
не имеющий осей и тип данных int32 – целочисленный:
tf.Tensor(1, shape=(), dtype=int32)
Отсутствие
осей говорит, что ранг тензора равен нулю. Однако, если явно указать параметр shape с размерностями
1x1:
a = tf.constant(1, shape=(1,1))
то
получим уже матрицу с этим размером 1x1:
tf.Tensor([[1]], shape=(1, 1), dtype=int32)
Если
же создать тензор на основе одномерного списка:
b = tf.constant([1, 2, 3, 4])
print(b)
То
у него автоматически появится одна ось, размером в 4 элемента:
tf.Tensor([1 2 3 4], shape=(4,), dtype=int32)
Соответственно,
для двумерного списка (или матрицы):
с = tf.constant([[1, 2],
[3, 4],
[5, 6]], dtype=tf.float16)
print(с)
получим
тензор ранга два (две оси):
tf.Tensor(
[[1. 2.]
[3. 4.]
[5.
6.]], shape=(3, 2), dtype=float16)
И
так далее, мы можем создавать тензоры произвольной размерности. Причем,
обратите внимание, в последнем случае мы явно указали тип данных float16
(вещественный 16 бит) и при создании тензора этот тип был применен для всех его
элементов. Если же тип не указывается, то он определяется автоматически, исходя
из переданных данных. Отсюда следует, что все элементы в тензоре имеют единый
тип данных, то есть, здесь нельзя одновременно хранить, например, числа,
строки, булевы значения. Только что то одно – один тип. Также следует иметь в
виду, что тензоры формируются на основе прямоугольных списков. То есть, число
элементов в каждой строке матрицы должно быть одинаковым и это правило должно
соблюдаться и в многомерном случае. Хотя, в Tensorflow есть возможность
определять неравномерные тензоры (RaggedTensor).
При
необходимости мы можем поменять тип данных тензора. Для этого используется
функция cast, которая возвращает новый тензор, содержащий те же значения с
указанным типом данных:
a2 = tf.cast(a, dtype=tf.float32)
Вообще,
тензоры построены по аналогии с многомерными матрицами пакета NumPy и многие
операции над ними аналогичны операциям в NumPy. Поэтому, я
советую вам ознакомиться с курсом по пакету NumPy на этом канале:
Курс по NumPy
Мало
того, любой тензор можно преобразовать в массив NumPy, либо с помощью
функции array, либо методом numpy():
import numpy as np
np_b1 = np.array(b)
np_b2 = b.numpy()
print(np_b1, np_b2, sep="\n")
Переменные tf.Variable
Если
же требуется объявить тензор с изменяемыми значениями, то для этого
используется класс tf.Variable. Он имеет похожий набор параметров, что и
функция tf.constant() и применяется
подобным образом:
b = tf.constant([1, 2, 3, 4])
v1 = tf.Variable(-1.2) # переменная из одного числа
v2 = tf.Variable([4, 5, 6, 7], dtype=tf.float32) # переменная через список
v3 = tf.Variable(b) # переменная через тензор
print(v1, v2, v3, sep="\n\n")
Чтобы
изменить значение переменной, вызывается метод assign() с указанием
других величин, например, так:
v1.assign(0)
v2.assign([0, 1, 6, 7])
Обратите
внимание, что размерность обновляемых данных должна совпадать с размерностью
переменной, иначе произойдет исключение (ошибка). При этом, новые данные, как
правило, записываются в ту же область памяти, где хранились прежние значения.
По
аналогии можно использовать методы:
v3.assign_add([1, 1, 1, 1]) # добавление значений
v1.assign_sub(5) # вычитание значений
Некоторые
из вас могут заметить, что того же результата в нашей программе можно добиться
просто создав новый тензор:
v3 = tf.Variable([-1, -2, -3, -4])
Мы
также получим изменение данных, хранящихся в новом объекте, а старый будет
уничтожен сборщиком мусора.
Но
именно из-за этого возникает одна существенная проблема. Когда какой-либо
тензор передается функции, например:
opt = tf.optimizers.SGD(learning_rate=0.1)
opt.minimize(y, [x])
то
измененное таким образом значение не будет доступно за пределами этой функции
(в основной программе), так как ссылки y и x будет вести к
исходным объектам, а не новым. Именно поэтому, при оптимизации каких-либо
изменяемых параметров, например, весов в НС, их следует определять как
переменные через объект tf.Variable.
Также
следует помнить, что создание новой переменной, на основе существующей:
создает
ее копию, то есть, ссылки v4 и v1 будут вести на
разные объекты. Эту конструкцию можно использовать для клонирования переменных
тензоров.
Опять
же, если нам нужно преобразовать переменную в массив NumPy, то достаточно
вызвать метод numpy():
Чтобы
посмотреть размерность тензора можно воспользоваться его свойством shape:
Возвращает
кортеж с размерностями по каждой из осей:
(4,)
Индексирование и срезы
Следующий
важный момент – это индексирование элементов тензоров и извлечение срезов. Эти
операции реализованы аналогично массивам пакета NumPy. Поэтому за
подробной информацией обращайтесь к учебному курсу на этом канале. Я не буду здесь
повторяться и лишь сделаю краткий обзор по индексированию и срезам в Tensorflow.
Следует
иметь в виду, что по индексам можно только читать данные из тензоров, например,
так:
val_0 = v3[0] # первый элемент
val_12 = v3[1:3] # элементы со 2-го по 3-й [-2, -3]
Причем,
переменные val_0 и val_12 будут
ссылаться на те же данные, что и тензор v3, то есть,
копирования информации здесь не происходит. Это очень удобно с точки зрения
производительности и использования памяти, но при этом, нужно понимать, что
изменение данных в val_0 и val_12 приведет к изменению и тензора v3:
val_0.assign(10)
print(v3)
Увидим
значения:
<tf.Variable
'Variable:0' shape=(4,) dtype=int32, numpy=array([10, -2, -3, -4])>
то
есть, через val_0 был изменен
первый элемент тензора v3.
Далее,
Tensorflowподдерживает
списочное индексирование с помощью функции gather(), следующим
образом:
x = tf.constant(range(10)) + 5
x_indx = tf.gather(x, [0, 4])
print(x, x_indx, sep="\n")
На
выходе тензор x_indx будет состоять
из двух элементов с индексами 0 и 4, взятыми из тензора x.
Если
же мы попытаемся обратиться к вектору через список:
то
это будет восприниматься как получение элемента из второй строки (индекс 1) и
третьего столбца (индекс 2). Поэтому для одномерного тензора данная запись
приведет к ошибке. Но если задать двумерный тензор:
v3 = tf.constant([[1, 2, 7], [3, 4, 8], [5, 6, 9]])
val_indx = v3[(1, 2)]
print(val_indx)
то
в консоли увидим значение 8 (элемент из 2-й строки и 3-го столбца).
Того
же самого результата можно добиться и так:
но,
обычно, используют первый вариант через кортеж, т.к. он удобнее и нагляднее:
Если
же мы в качестве одного из параметров укажем только один индекс:
val_indx = v3[0]
print(val_indx)
то
будет возвращена соответствующая строка (в данном случае первая). При этом,
размерность тензора val_indx будет shape=(3,):
tf.Tensor([1 2 7], shape=(3,), dtype=int32)
то
есть, он представляется уже одномерным массивом.
Для
считывания определенного столбца целиком, следует использовать механизм срезов,
например, так:
получим
второй столбец в виде вектора [2, 4, 6].
Напомню,
что срезы определяются синтаксисом:
start:stop:step
поэтому
для извлечения, например, только первых двух элементов из последнего 3-го
столбца можно записать:
или
так:
Здесь
отрицательные индексы определяют нумерацию с конца соответствующей оси.
В
целом, вот так выполняется индексирование в Tensorflow и по аналогии
применяется к матрицам любых размерностей. Опять же, подробную информацию об
индексировании смотрите в курсе по NumPy.
Изменение формы тензоров
У
любого тензора можно поменять форму (число элементов по осям). Например, пусть
задан одномерный тензор длиной в 30 элементов:
a = tf.constant(range(30))
Его
можно привести к другой форме (создать новое представление) в виде матрицы 5x6 элементов. Это
делается с помощью функции tf.reshape(), следующим
образом:
b = tf.reshape(a, [5, 6])
print(b.numpy())
Теперь
переменная bссылается на те
же самые данные, что и переменная a, но форма тензора стала другой –
матрица 5x6. Так как здесь
происходит лишь изменение формы тензора, то это достаточно быстрая операция и
ее можно безопасно выполнять и для больших данных.
Обратите
внимание, что мы выбрали форму матрицы именно 5x6, которая
содержит также 30 элементов, что и начальный вектор. Если указать
несогласованные размеры, например:
b = tf.reshape(a, [5, 5])
то
это приведет к ошибке, так как 5x5 = 25, что меньше, чем 30 – число
элементов в векторе a.
Наконец,
мы можем по одной из осей не указывать размер (записывая -1), тогда он будет
вычислен автоматически, исходя из общего числа элементов:
b = tf.reshape(a, [6, -1])
Получим
матрицу 6x5.
Функция
reshape() позволяет
менять форму тензора, но не порядок следования элементов. Это значит, что она
не подходит, например, для операции транспонирования, когда строки и столбцы
меняются между собой. Для этого следует использовать специальную функцию:
b_T = tf.transpose(b, perm=[1, 0])
Здесь
параметр permуказывает по
каким осям проводить операцию транспонирования.
Я,
надеюсь, из этого занятия вы узнали как задавать константные и переменные
тензоры, как выполняется их индексирование и операции срезов, а также как
менять их форму. На следующем занятии продолжим эту тему и рассмотрим базовые
математические операции над тензорами.