欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TensorFlow2.x 使用之快速入门

程序员文章站 2024-03-23 13:15:58
...

1. 数据类型

TensorFlow 中的基本数据类型,包含数值类型、字符串类型和布尔类型。

1.1 数值类型

  • 标量(Scalar):单个实数,如1,2,3等,维度(Dimension)数为0,shape为[]
  • 向量(Vector):n个实数的有序集合,通过中括号包裹,如[1, 2],[5.20, 13.14]等,维度数为1,长度不定,shape为[n]
  • 矩阵(Matrix):n行m列实数的有序集合,如[[1, 2],[3, 4]],也可以写成
    [1234] \left[ \begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix} \right]
    维度数为2,每个维度上的长度不定,shape为[n, m]
  • 张量(Tensor):所有维度数dim > 2的数组统称为张量。张量的每个维度也叫作轴(Axis),一般维度代表了具体的物理含义,比如Shape为[2, 32, 32, 3]的张量共有4维,如果表示图片数据的话,每个维度/轴代表的含义分别是图片数量、图片高度、图片宽度、图片通道数,其中2代表了2张图片,32代表了高、宽均为32,3代表了RGB共3个通道。张量的维度数以及每个维度所代表的具体物理含义需要由用户自行定义。
    编程实现如下:
import tensorflow as tf

a = 1.2 # python 语言方式创建标量
aa = tf.constant(1.2)  # TF方式创建标量
print(type(a))  # <class 'float'>
print(type(aa))  # <class 'tensorflow.python.framework.ops.EagerTensor'>
print(tf.is_tensor(aa))  # True

# 打印张量aa的相关信息,shape 表示张量的形状,dtype 表示张量的数值精度
bb = tf.constant([1, 2. ,3.3])
print(bb)  # tf.Tensor([1.  2.  3.3], shape=(3,), dtype=float32)

# 张量 numpy()方法可以返回 Numpy.array 类型的数据
cc = bb.numpy()  # 将TF张量的数据导出为 numpy 数组格式
print(cc)  # [1.  2.  3.3]

# 向量的定义须通过 List 容器传给 tf.constant()函数
dd = tf.constant([5.20])  # 创建一个元素的向量
print(dd)  # tf.Tensor([5.2], shape=(1,), dtype=float32)
print(dd.shape)  # (1,)

# 定义矩阵的实现
ee = tf.constant([[1,2],[3,4]])  # 创建2行2列的矩阵
print(ee)  # [[1 2]
           #  [3 4]], shape=(2, 2), dtype=int32)
print(ee.shape)  # (2, 2)

# 三维张量
ff = tf.constant([[[1,2],[3,4]],
                  [[5,6],[7,8]]])  # 创建3维张量
print(ff)  # tf.Tensor(
           # [[[1 2]
           #   [3 4]]
           #  [[5 6]
           #   [7 8]]], shape=(2, 2, 2), dtype=int32)

1.2 字符串类型

import tensorflow as tf

a = tf.constant('Hello, Deep Learning.')  # 创建字符串
print(a)  # tf.Tensor(b'Hello, Deep Learning.', shape=(), dtype=string)

# 在 tf.strings 模块中,提供了常见的字符串类型的工具函数,
# 如小写化 lower()、拼接join()、长度 length()、切分 split()等。
b = tf.strings.lower(a)  # 小写化字符串
print(b)  # tf.Tensor(b'hello, deep learning.', shape=(), dtype=string)

# 深度学习算法主要还是以数值类型张量运算为主,字符串类型的数据使用频率较低

1.3 布尔类型

import tensorflow as tf

a = tf.constant(True)   # 创建布尔类型标量
print(a)  # tf.Tensor(True, shape=(), dtype=bool)

b = tf.constant([True, False])  # 创建布尔类型向量
print(b)  # tf.Tensor([ True False], shape=(2,), dtype=bool)

# TensorFlow 的布尔类型和 Python 语言的布尔类型并不等价,不能通用
c = tf.constant(True)  # 创建TF布尔张量
print(c is True)  # TF布尔类型张量与 python 布尔类型比较, False,对象不等价

print(c == True)  # 仅数值比较, tf.Tensor(True, shape=(), dtype=bool)

2. 数值精度

常用的精度类型有 tf.int16、tf.int32、tf.int64、tf.float16、tf.float32、tf.float64 等,其中 tf.float64 即为 tf.double。

import tensorflow as tf
import numpy as np
# 创建指定精度的张量
a = tf.constant(123456789, dtype=tf.int16)
b = tf.constant(123456789, dtype=tf.int32)

# 保存精度过低时,数据 123456789 发生了溢出,得到了错误的
# 结果,一般使用tf.int32、tf.int64。对于浮点数,高精度的张量可以表示更精准的数据
print(a)  # tf.Tensor(-13035, shape=(), dtype=int16)
print(b)  # tf.Tensor(123456789, shape=(), dtype=int32)

# np.pi, 从 numpy 中导入 pi 常量
c = tf.constant(np.pi, dtype=tf.float32) # 32 位
print(c)  # tf.Tensor(3.1415927, shape=(), dtype=float32)

# 如果采用 tf.float64 精度保存π,则能获得更高的精度
d = tf.constant(np.pi, dtype=tf.float64) # 64 位
print(d)  # tf.Tensor(3.141592653589793, shape=(), dtype=float64)

# 对于大部分深度学习算法,一般使用 tf.int32 和 tf.float32 可满足大部分场合的运算精
# 度要求,部分对精度要求较高的算法,如强化学习某些算法,可以选择使用 tf.int64 和
# tf.float64 精度保存张量。

2.1 读取精度

通过访问张量的 dtype 成员属性可以判断张量的保存精度。

import tensorflow as tf

a = tf.constant(1.2)
print('before:', a.dtype)  # 读取原有张量的数值精度, before: <dtype: 'float32'>
if a.dtype != tf.float32:  # 如果精度不符合要求,则进行转换
    a = tf.cast(a, tf.float32)  # tf.cast 函数可以完成精度转换
print('after :', a.dtype)  # 打印转换后的精度, after : <dtype: 'float32'>

对于某些只能处理指定精度类型的运算操作,需要提前检验输入张量的精度类型,并将不符合要求的张量进行类型转换。

2.2 类型转换

系统的每个模块使用的数据类型、数值精度可能各不相同,对于不符合要求的张量的类型及精度,需要通过tf.cast函数进行转换。

import tensorflow as tf
import numpy as np

a = tf.constant(np.pi, dtype=tf.float16) # 创建 tf.float16 低精度张量
print(a)  # tf.Tensor(3.14, shape=(), dtype=float16)
b = tf.cast(a, tf.double) # 转换为高精度张量
print(b)  # tf.Tensor(3.140625, shape=(), dtype=float64)

# 进行类型转换时,需要保证转换操作的合法性,例如将高精度的张量转换为低精度的张量
# 时,可能发生数据溢出隐患
c = tf.constant(123456789, dtype=tf.int32)
print(c)  # tf.Tensor(123456789, shape=(), dtype=int32)
d = tf.cast(c, tf.int16) # 转换为低精度整型
print(d)  # tf.Tensor(-13035, shape=(), dtype=int16)

# 布尔类型与整型之间相互转换也是合法的,
# 一般默认 0 表示 False,1 表示 True,在 TensorFlow 中,将非 0 数字都视为 True
e = tf.constant([True, False])
print(e)  # tf.Tensor([ True False], shape=(2,), dtype=bool)
f = tf.cast(e, tf.int32) # 布尔类型转整型
print(f)  # tf.Tensor([1 0], shape=(2,), dtype=int32)

3. 待优化张量

为了区分需要计算梯度信息的张量与不需要计算梯度信息的张量,TensorFlow 增加了一种专门的数据类型来支持梯度信息的记录:tf.Variable。tf.Variable 类型在普通的张量类
型基础上添加了 name,trainable 等属性来支持计算图的构建。由于梯度运算会消耗大量的
计算资源,而且会自动更新相关参数,对于不需要的优化的张量,如神经网络的输入????,不需要通过 tf.Variable 封装;相反,对于需要计算梯度并优化的张量,如神经网络层的???? 和????,需要通过 tf.Variable 包裹以便 TensorFlow 跟踪相关梯度信息。
通过 tf.Variable()函数可以将普通张量转换为待优化张量,例如:

import tensorflow as tf

a = tf.constant([-1, 0, 1, 2])  # 创建 TF 张量
aa = tf.Variable(a)  # 转换为 Variable 类型
# Variable 类型张量的属性
print(aa.name, aa.trainable)  # Variable:0 True
'''
其中张量的 name 和 trainable 属性是 Variable 特有的属性,name 属性用于命名计算图中的
变量,这套命名体系是 TensorFlow 内部维护的,一般不需要用户关注 name 属性;trainable
属性表征当前张量是否需要被优化,创建 Variable 对象时是默认启用优化标志,可以设置
trainable=False 来设置张量不需要优化。
'''

b = tf.Variable([[1,2],[3,4]]) # 直接创建 Variable 张量
print(b)
# <tf.Variable 'Variable:0' shape=(2, 2) dtype=int32, numpy=
# array([[1, 2],
#        [3, 4]])>

# 待优化张量可视为普通张量的特殊类型,普通张量其实也可以通过 GradientTape.watch()方
# 法临时加入跟踪梯度信息的列表,从而支持自动求导功能。

4. 创建张量

在 TensorFlow 中,可以通过多种方式创建张量,如从 Python 列表对象创建,从
Numpy 数组创建,或者创建采样自某种已知分布的张量等。

4.1 从数组、列表对象创建

通过 tf.convert_to_tensor 函数可以创建新 Tensor,并将保存在 Python List 对象或者
Numpy Array 对象中的数据导入到新 Tensor 中。

import tensorflow as tf
import numpy as np

a = tf.convert_to_tensor([1,2.]) # 从列表创建张量
print(a)  # tf.Tensor([1. 2.], shape=(2,), dtype=float32)

b = tf.convert_to_tensor(np.array([[1,2.],[3,4]]))  # 从数组中创建张量
print(b)  # tf.Tensor(
          # [[1. 2.]
          #  [3. 4.]], shape=(2, 2), dtype=float64)
# 需要注意的是,Numpy 浮点数数组默认使用 64 位精度保存数据,转换到 Tensor 类型时精
# 度为 tf.float64,可以在需要的时候将其转换为 tf.float32 类型。
# 实际上,tf.constant()和 tf.convert_to_tensor()都能够自动的把 Numpy 数组或者 Python 
# 列表数据类型转化为 Tensor 类型,这两个 API 命名来自 TensorFlow 1.x 的命名习惯,在
# TensorFlow 2 中函数的名字并不是很贴切,使用其一即可。

4.2 创建全0或全1张量

将张量创建为全 0 或者全 1 数据是非常常见的张量初始化手段。考虑线性变换???? = ???????? + ????,将权值矩阵????初始化为全 1 矩阵,偏置 b 初始化为全 0 向量,此时线性变化层输出???? = ????,因此是一种比较好的层初始化状态。通过 tf.zeros()和 tf.ones()即可创建任意形状,且内容全 0 或全 1 的张量。

import tensorflow as tf

a = tf.zeros([])  # 创建全0的标量
b = tf.ones([])  # 创建全1的标量
print(a)  # tf.Tensor(0.0, shape=(), dtype=float32)
print(b)  # tf.Tensor(1.0, shape=(), dtype=float32)

c = tf.zeros([1])  # 创建全0的向量
d = tf.ones([1])  # 创建全1的向量
print(c)  # tf.Tensor([0.], shape=(1,), dtype=float32)
print(d)  # tf.Tensor([1.], shape=(1,), dtype=float32)

e = tf.zeros([2, 2])  # 创建全 0 矩阵,指定 shape 为 2 行 2 列
print(e)  # tf.Tensor(
          # [[0. 0.]
          #  [0. 0.]], shape=(2, 2), dtype=float32)
f = tf.ones([3,2])  # 创建全 1 矩阵,指定 shape 为 3 行 2 列
print(f)  # tf.Tensor(
          # [[1. 1.]
          #  [1. 1.]
          #  [1. 1.]], shape=(3, 2), dtype=float32)

# 通过 tf.zeros_like, tf.ones_like 可以方便地新建与某个张量 shape 一致,
# 且内容为全 0 或 全 1 的张量
g = tf.ones([2,3])  # 创建一个矩阵
gg = tf.zeros_like(g)  # 创建一个与 g 形状相同,但是全 0 的新矩阵
print(gg)  # tf.Tensor(
           # [[0. 0. 0.]
           #  [0. 0. 0.]], shape=(2, 3), dtype=float32)

h = tf.zeros([3,2]) # 创建一个矩阵
hh = tf.ones_like(h) # 创建一个与 h 形状相同,但是全 1 的新矩阵
print(hh)  # tf.Tensor(
           # [[1. 1.]
           #  [1. 1.]
           #  [1. 1.]], shape=(3, 2), dtype=float32)

4.3 创建自定义数值张量

除了初始化为全 0,或全 1 的张量之外,有时也需要全部初始化为某个自定义数值的张量,比如将张量的数值全部初始化为−1等。
通过 tf.fill(shape, value)可以创建全为自定义数值 value 的张量,形状由 shape 参数指
定。

import tensorflow as tf

a = tf.fill([], -1) # 创建-1 的标量
print(a)  # tf.Tensor(-1, shape=(), dtype=int32)

b = tf.fill([1], -1) # 创建-1 的向量
print(b)  # tf.Tensor([-1], shape=(1,), dtype=int32)

c = tf.fill([2,2], 99) # 创建 2 行 2 列,元素全为 99 的矩阵
print(c)  # tf.Tensor(
          # [[99 99]
          #  [99 99]], shape=(2, 2), dtype=int32)

4.4 创建已知分布的张量

正态分布(Normal Distribution,或 Gaussian Distribution)和均匀分布(Uniform Distribution)是最常见的分布之一。
通过 tf.random.normal(shape, mean=0.0, stddev=1.0)可以创建形状为 shape,均值为
mean,标准差为 stddev 的正态分布????(mean, stddev2)。
通过 tf.random.uniform(shape, minval=0, maxval=None, dtype=tf.float32)可以创建采样自[minval, maxval)区间的均匀分布的张量。

import tensorflow as tf

# 创建标准正态分布的张量,均值为 0,标准差为 1
a = tf.random.normal([2,2])
print(a)  # tf.Tensor(
          # [[ 0.32461685 -0.13123313]
          #  [-0.77334005 -0.99685717]], shape=(2, 2), dtype=float32)

# 创建均值为 1,标准差为 2 的正态分布
b = tf.random.normal([2,2], mean=1,stddev=2)
print(b)  # tf.Tensor(
          # [[ 6.4615455 -4.231422 ]
          #  [ 1.0218422  0.5758535]], shape=(2, 2), dtype=float32)

c = tf.random.uniform([2,2])  # 创建采样自[0,1)均匀分布的矩阵
print(c)  # tf.Tensor(
          # [[0.7776183  0.8894594 ]
          #  [0.7019726  0.59900653]], shape=(2, 2), dtype=float32)

d = tf.random.uniform([2,2],maxval=10)  # 创建采样自[0,10)均匀分布的矩阵
print(d)  # tf.Tensor(
          # [[8.159562   6.2609873 ]
          #  [9.326186   0.50424457]], shape=(2, 2), dtype=float32)

# 如果需要均匀采样整形类型的数据,必须指定采样区间的最大值 maxval 参数,同时指 定数据类型为 tf.int*型
# 创建采样自[0,100)均匀分布的整型矩阵
e = tf.random.uniform([2,2],maxval=100,dtype=tf.int32)
print(e)  # tf.Tensor(
          # [[25 50]
          #  [47 30]], shape=(2, 2), dtype=int32)

4.5 创建序列

在循环计算或者对张量进行索引时,经常需要创建一段连续的整型序列,可以通过
tf.range()函数实现。tf.range(limit, delta=1)可以创建[0, limit)之间,步长为 delta 的整型序
列,不包含 limit 本身。
通过 tf.range(start, limit, delta=1)可以创建[start,limit),步长为 delta 的序列,不包含
limit 本身。

import tensorflow as tf

a = tf.range(10)  # 创建 0~10,步长为 1 的整型序列,不包含 10
print(a)  # tf.Tensor([0 1 2 3 4 5 6 7 8 9], shape=(10,), dtype=int32)

b = tf.range(10,delta=2)  # 创建 0~10,步长为 2 的整形序列
print(b)  # tf.Tensor([0 2 4 6 8], shape=(5,), dtype=int32)

c = tf.range(1,10,delta=2) # 1~10
print(c)  # tf.Tensor([1 3 5 7 9], shape=(5,), dtype=int32)

5. 索引与切片

通过索引与切片操作可以提取张量的部分数据,它们的使用频率非常高。

5.1 索引

import tensorflow as tf

# 取第 1 张图片的数据
x = tf.random.normal([4,32,32,3]) # 创建 4D 张量
print(x[0])  # tf.Tensor(
             # [[[ 0.01931079 -0.15679789 -0.7452232 ]
             #   [ 0.8018192   0.68360156  2.347867  ]
             #   ...
             #   [ 0.48492217 -0.40642446 -0.24011232]
             #   [-0.14609094  0.893129   -0.8661169 ]]], shape=(32, 32, 3), dtype=float32)

# 取第 1 张图片的第 2 行
print(x[0][1])    # tf.Tensor(
                  # [[ 1.1219755  -0.08472323  0.28014842]
                  #  [ 0.36257166  1.7051443   0.38203102]
                  #  ...
                  #  [-0.9957301  -1.0245532   0.18782209]], shape=(32, 3), dtype=float32)

# 取第 1 张图片,第 2 行,第 3 列的数据
print(x[0][1][2])  # tf.Tensor([-0.6911552  -0.20871519  0.37134022], shape=(3,), dtype=float32)

# 取第 3 张图片,第 2 行,第 1 列的像素,B 通道(第 2 个通道)颜色强度值
print(x[2][1][0][1])  # tf.Tensor(-0.3229141, shape=(), dtype=float32)

# 当张量的维度数较高时,使用[????][????]. . .[????]的方式书写不方便,可以采用[????,????, … , ????]的方
# 式索引,它们是等价的。

# 取第 2 张图片,第 10 行,第 3 列的数据
print(x[1,9,2])  # tf.Tensor([-0.3269816  -1.2077118   0.81598526], shape=(3,), dtype=float32)

5.2 切片

通过start: end: step切片方式可以方便地提取一段数据,其中 start 为开始读取位置的索
引,end 为结束读取位置的索引(不包含 end 位),step 为采样步长。
以 shape 为[4,32,32,3]的图片张量为例。

import tensorflow as tf

x = tf.random.normal([4,32,32,3]) # 创建 4D 张量
# 读取第 2,3 张图片
print(x[1:3])  # tf.Tensor(
               # [[[[ 1.94379961e+00  1.40747717e-02  1.51915818e-01]
               #    [-1.54877591e+00 -5.99838257e-01 -3.56691658e-01]
               #    [-7.14290798e-01 -1.81166217e-01  1.30167231e-01]
               #    ...
               #    shape=(2, 32, 32, 3), dtype=float32)

# start: end: step切片方式有很多简写方式,其中 start、end、step 3 个参数可以根据需要
# 选择性地省略,全部省略时即为::,表示从最开始读取到最末尾,步长为 1,即不跳过任何
# 元素。如 x[0,::]表示读取第 1 张图片的所有行,其中::表示在行维度上读取所有行,它等价
# 于 x[0]的写法,::可以简写为单个冒号:

# # 读取第一张图片
print(x[0,::])  # tf.Tensor(
                # [[[ 0.85544187 -0.1265464  -0.6480988 ]
                #   [ 0.88735324 -0.4283463  -0.5642525 ]
                #   [ 0.8177189  -2.3131967   0.21035941]
                #   ...
                #   , shape=(32, 32, 3), dtype=float32)
# 表示读取所有图片、隔行采样、隔列采样,、读取所有通道数据,相当于在图片的高宽上各
# 缩放至原来的 50%
print(x[:,0:28:2,0:28:2,:])  #tf.Tensor(
                             # [[[[ 8.55441868e-01 -1.26546398e-01 -6.48098826e-01]
                             #    [ 8.17718923e-01 -2.31319666e+00  2.10359409e-01]
                             #    [ 1.39555752e+00  1.81259942e+00  4.25395920e-05]
                             #    ...
                             #    , shape=(4, 14, 14, 3), dtype=float32)

# step 可以为负数,考虑最特殊的一种例子,当step = −1时,start: end: −1表
# 示从 start 开始,逆序读取至 end 结束(不包含 end),索引号???????????? ≤ ????????????????????。

x = tf.range(9)  # 创建 0~9 向量
# 从 8 取到 0,逆序,不包含 0
print(x[8:0:-1])  # tf.Tensor([8 7 6 5 4 3 2 1], shape=(8,), dtype=int32)

# 逆序全部元素
print(x[::-1])  # tf.Tensor([8 7 6 5 4 3 2 1 0], shape=(9,), dtype=int32)

# 逆序间隔采样
print(x[::-2])  # tf.Tensor([8 6 4 2 0], shape=(5,), dtype=int32)

# 读取每张图片的所有通道,其中行按着逆序隔行采样,列按着逆序隔行采样
x = tf.random.normal([4,32,32,3])
print(x[0,::-2,::-2])  # 行、列逆序间隔采样
# tf.Tensor(
# [[[ 7.38768399e-01  1.15021610e+00  1.41060674e+00]
#   [ 3.73294783e+00 -8.90625238e-01  1.15817475e+00]
#   ...
#   , shape=(16, 16, 3), dtype=float32)

# 当张量的维度数量较多时,不需要采样的维度一般用单冒号:表示采样所有元素,此时
# 有可能出现大量的:出现。
# 为了避免出现像 [: , : , : ,1]这样过多冒号的情况,可以使用⋯符号表示取多个维度上所
# 有的数据,其中维度的数量需根据规则自动推断:当切片方式出现⋯符号时,⋯符号左边
# 的维度将自动对齐到最左边,⋯符号右边的维度将自动对齐到最右边,此时系统再自动推
# 断⋯符号代表的维度数量

# 读取第 1~2 张图片的 G/B 通道数据
x[0:2,...,1:] # 高宽维度全部采集

# 读取最后 2 张图片
x[2:,...] # 高、宽、通道维度全部采集,等价于 x[2:]

# 读取 R/G 通道数据
x[...,:2] # 所有样本,所有高、宽的前 2 个通道

6. 维度变换

在神经网络运算过程中,维度变换是最核心的张量操作,通过维度变换可以将数据任意地切换形式,满足不同场合的运算需求。
那么为什么需要维度变换呢?考虑线性层的批量形式:
???? = ????@???? + ????
其中,假设????包含了 2 个样本,每个样本的特征长度为 4,????的 shape 为[2,4]。线性层的输
出为 3 个节点,即????的 shape 定义为[4,3],偏置????的 shape 定义为[3]。那么aaa@qq.com的运算结 果张量 shape 为[2,3],需要叠加上 shape 为[3]的偏置????。不同 shape 的 2 个张量怎么直接相加呢?
回顾设计偏置的初衷,我们给每个层的每个输出节点添加一个偏置,这个偏置数据是
对所有的样本都是共享的,换言之,每个样本都应该累加上同样的偏置向量????,如图所示:
TensorFlow2.x 使用之快速入门
因此,对于 2 个样本的输入????,我们需要将 shape 为[3]的偏置
TensorFlow2.x 使用之快速入门
按样本数量复制 1 份,变成如下矩阵形式????′:
TensorFlow2.x 使用之快速入门
通过与????′ = ????@????
TensorFlow2.x 使用之快速入门
相加,此时X′与????′
shape 相同,满足矩阵相加的数学条件:
TensorFlow2.x 使用之快速入门
通过这种方式,既满足了数学上矩阵相加需要 shape 一致的条件,又达到了给每个输入样本的输出节点共享偏置向量的逻辑。为了实现这种运算方式,我们将偏置向量????插入一个新的维度,并把它定义为 Batch 维度,然后在 Batch 维度将数据复制 1 份,得到变换后的????′,新的 shape 为[2,3]。这一系列的操作就是维度变换操作。
算法的每个模块对于数据张量的格式有不同的逻辑要求,当现有的数据格式不满足算法要求时,需要通过维度变换将数据调整为正确的格式。这就是维度变换的功能。
基本的维度变换操作函数包含了改变视图 reshape、插入新维度 expand_dims,删除维度squeeze、交换维度transpose、复制数据tile 等函数。

6.1 改变视图

张量的视图就是我们理解张量的方式,比如 shape 为[2,4,4,3]的张量????,我们从逻辑上可以理解为2张图片,每张图片4行4列,每个位置有 RGB 3 个通道的数据;张量的存储体现在张量在内存上保存为一段连续的内存区域,对于同样的存储,我们可以有不同的理解方式,比如上述张量????,我们可以在不改变张量的存储下,将张量????理解为2个样本,每个样本的特征为长度 48 的向量。同一个存储,从不同的角度观察数据,可以产生不同的视图,这就是存储与视图的关系。视图的产生是非常灵活的,但需要保证是合理。

import tensorflow as tf

x = tf.range(96) # 生成向量
print(x)  # tf.Tensor([ 0  1  2  3  4  5 ...], shape=(96,), dtype=int32)

x = tf.reshape(x,[2,4,4,3]) # 改变x的视图,获得4D张量,存储并未改变
# 可以观察到数据仍然是 0~95 的顺序,可见数据并未改变,改变的是数据的结构
print(x)  # tf.Tensor(
          # [[[[ 0  1  2]
          #    [ 3  4  5]
          #    [ 6  7  8]
          #    [ 9 10 11]]..., shape=(2, 4, 4, 3), dtype=int32)

print(x.ndim)  # 获取张量的维度数 4
print(x.shape)  # 获取张量形状列表 (2, 4, 4, 3)

# 通过tf.reshape(x, new_shape),可以将张量的视图任意地合法改变
x = tf.reshape(x, [2, -1])  # 参数−1表示当前轴上长度需要根据张量总元素不变的法则自动推导
print(x)  # tf.Tensor([[ 0  1  2  3  ...]], shape=(2, 48), dtype=int32)

6.2 增删维度

增加维度 增加一个长度为 1 的维度相当于给原有的数据添加一个新维度的概念,维度
长度为 1,故数据并不需要改变,仅仅是改变数据的理解方式,因此它其实可以理解为改
变视图的一种特殊方式。
删除维度 是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为 1 的维
度,也不会改变张量的存储。继续考虑增加维度后 shape 为[1,28,28,1]的例子,如果希望将图片数量维度删除,可以通过tf.squeeze(x, axis)函数,axis 参数为待删除的维度的索引号。

import tensorflow as tf

# 一张28 × 28大小的灰度图片的数据保存为 shape 为[28,28]的张
# 量,在末尾给张量增加一新维度,定义为通道数维度,此时张量的 shape 变为[28,28,1]
x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32)

# 通过 tf.expand_dims(x, axis)可在指定的 axis 轴前可以插入一个新的维度
x = tf.expand_dims(x,axis=2) # axis=2 表示宽维度后面的一个维度
# 插入一个新维度后,数据的存储顺序并没有改变,仅仅是在插入一个新的维度后,改变了数据的视图。

x = tf.expand_dims(x,axis=0) # 高维度之前插入新维度
# 需要注意的是,tf.expand_dims 的 axis 为正时,表示在当前维度之前插入一个新维度;为
# 负时,表示当前维度之后插入一个新的维度。

x = tf.squeeze(x, axis=0)  # 删除图片数量维度
x = tf.squeeze(x, axis=2)  # 删除图片通道数维度
# 如果不指定维度参数 axis,即 tf.squeeze(x),那么它会默认删除所有长度为 1 的维度

6.3 交换维度

改变视图、增删维度都不会影响张量的存储。在实现算法逻辑时,在保持维度顺序不变的条件下,仅仅改变张量的理解方式是不够的,有时需要直接调整的存储顺序,即交换维度(Transpose)。通过交换维度操作,改变了张量的存储顺序,同时也改变了张量的视图。
交换维度操作是非常常见的,比如在TensorFlow 中,图片张量的默认存储格式是通道后行格式:[????, ℎ, , ????],但是部分库的图片格式是通道先行格式:[????, ????, ℎ, ],因此需要完成[????, ℎ, , ????]到[????, ????, ℎ, ]维度交换运算,此时若简单的使用改变视图函数 reshape,则新视图
的存储方式需要改变,因此使用改变视图函数是不合法的。我们以[????, ℎ, , ????]转换到[????, ????, ℎ, ]为例,介绍如何使用 tf.transpose(x, perm)函数完成维度交换操作,其中参数 perm表示新维度的顺序 List。考虑图片张量 shape 为[2,32,32,3],“图片数量、行、列、通道数”的维度索引分别为 0、1、2、3,如果需要交换为[????, ????, ℎ, ]格式,则新维度的排序为“图片数量、通道数、行、列”,对应的索引号为[0,3,1,2],因此参数 perm 需设置为[0,3,1,2].

import tensorflow as tf

x = tf.random.normal([2,32,32,3])
tf.transpose(x,perm=[0,3,1,2]) # 交换维度

通过 tf.transpose 完成维度交换后,张量的存储顺序已经改变,视图也随之改变,后续的所有操作必须基于新的存续顺序和视图进行。相对于改变视图操作,维度交换操作的计算代价更高。

6.4 复制数据

当通过增加维度操作插入新维度后,可能希望在新的维度上面复制若干份数据,满足后续算法的格式要求。考虑???? = ????@???? + ????的例子,偏置 ????插入样本数的新维度后,需要在新维度上复制Batch Size份数据,将shape变为与 ????@???? 一致后,才能完成张量相加运算。
可以通过tf.tile(x, multiples)函数完成数据在指定维度上的复制操作,multiples分别指定每个维度上面的复制数,对应位置为1表示不复制,为2表明新长度为原来长度的2倍,即数据复制一份,以此类推。
以输入为[2,4],输出为 3 个节点线性变换层为例,偏置????定义为:
TensorFlow2.x 使用之快速入门
通过 tf.expand_dims(b, axis=0)插入新维度,变成矩阵:
TensorFlow2.x 使用之快速入门
此时????的 shape 变为[1,3],我们需要在axis=0 图片数量维度上根据输入样本的数量复制若干次,这里的 Batch Size 为 2,即复制一份,变成
TensorFlow2.x 使用之快速入门
通过 tf.tile(b, multiples=[2,1])即可在 axis=0 维度复制 1 次,在 axis=1 维度不复制。

import tensorflow as tf

b = tf.constant([1,2])  # 创建向量 b
print(b)  # tf.Tensor([1 2], shape=(2,), dtype=int32)
b = tf.expand_dims(b, axis=0)  # 插入新维度,变成矩阵
print(b)  # tf.Tensor([[1 2]], shape=(1, 2), dtype=int32)

b = tf.tile(b, multiples=[2,1]) # 样本维度上复制一份
print(b)  # tf.Tensor(
          # [[1 2]
          #  [1 2]], shape=(2, 2), dtype=int32)

tf.tile 会创建一个新的张量来保存复制后的张量,由于复制操作涉及大量数据的读写 IO 运算,计算代价相对较高。神经网络中不同 shape 之间的张量运算操作十分频繁,那么有没有轻量级的复制操作呢?这就是接下来要介绍的 Broadcasting 操作。

7. Broadcasting

Broadcasting 称为广播机制(或自动扩展机制),它是一种轻量级的张量复制手段,在逻
辑上扩展张量数据的形状,但是只会在需要时才会执行实际存储复制操作。对于大部分场
景,Broadcasting 机制都能通过优化手段避免实际复制数据而完成逻辑运算,从而相对于
tf.tile 函数,减少了大量计算代价。
对于所有长度为 1 的维度,Broadcasting 的效果和 tf.tile 一样,都能在此维度上逻辑复
制数据若干份,区别在于 tf.tile 会创建一个新的张量,执行复制 IO 操作,并保存复制后的
张量数据,而 Broadcasting 并不会立即复制数据,它会在逻辑上改变张量的形状,使得视
图上变成了复制后的形状。Broadcasting 会通过深度学习框架的优化手段避免实际复制数据而完成逻辑运算,至于怎么实现的用户不必关心,对于用户来说,Broadcasting 和 tf.tile 复制的最终效果是一样的,操作对用户透明,但是 Broadcasting 机制节省了大量计算资源,建议在运算过程中尽可能地利用Broadcasting 机制提高计算效率。

import tensorflow as tf

x = tf.random.normal([2,4])
w = tf.random.normal([4,3])
b = tf.random.normal([3])
y = aaa@qq.com+b  # 不同 shape 的张量直接相加

# 上述加法并没有发生逻辑错误,那么它是怎么实现的呢?这是因为它自动调用 Broadcasting
# 函数 tf.broadcast_to(x, new_shape),将两者 shape 扩张为相同的[2,3],即上式可以等效为:
y = aaa@qq.com + tf.broadcast_to(b,[2,3]) # 手动扩展,并相加

Broadcasting 机制的核心思想是普适性,即同一份数据能普遍适合于其他位置。在验证
普适性之前,需要先将张量 shape 靠右对齐,然后进行普适性判断:对于长度为 1 的维度,默认这个数据普遍适合于当前维度的其他位置;对于不存在的维度,则在增加新维度
后默认当前数据也是普适于新维度的,从而可以扩展为更多维度数、任意长度的张量形
状。
考虑 shape 为[w, 1]的张量????,需要扩展为 shape:[????, ℎ, w, ????],如图所示,第一行为欲扩展的 shape,第二行为现有 shspe:
TensorFlow2.x 使用之快速入门
首先将 2 个 shape 靠右对齐,对于通道维度????,张量的现长度为 1,则默认此数据同样适合当前维度的其他位置,将数据在逻辑上复制???? − 1份,长度变为 c;对于不存在的????和ℎ维
度,则自动插入新维度,新维度长度为 1,同时默认当前的数据普适于新维度的其他位
置,即对于其它的图片、其它的行来说,与当前的这一行的数据完全一致。这样将数据???? 和ℎ维度的长度自动扩展为????和ℎ,Broadcasting示意图如下:
TensorFlow2.x 使用之快速入门

# 通过 tf.broadcast_to(x, new_shape)函数可以显式地执行自动扩展功能,将现有 shape 扩
# 张为 new_shape
A = tf.random.normal([32,1]) # 创建矩阵
tf.broadcast_to(A, [2,32,32,3]) # 扩展为 4D 张量

8. 数学运算

8.1 加减乘除运算

加、减、乘、除是最基本的数学运算,分别通过 tf.add, tf.subtract, tf.multiply, tf.divide
函数实现,TensorFlow 已经重载了+、 − 、 ∗ 、/运算符,一般推荐直接使用运算符来完成
加、减、乘、除运算。
整除和余除也是常见的运算之一,分别通过//和%运算符实现。我们来演示整除运算。

8.2 乘方运算

通过 tf.pow(x, a)可以方便地完成???? = X????的乘方运算,也可以通过运算符**实现 ∗∗ ????运算。
对于常见的平方和平方根运算,可以使用 tf.square(x)和 tf.sqrt(x)实现。

8.3 指数和对数运算

通过 tf.pow(a, x)或者**运算符也可以方便地实现指数运算????x
特别地,对于自然指数e????,可以通过 tf.exp(x)实现。
在 TensorFlow 中,自然对数logex 可以通过 tf.math.log(x)实现

8.4 矩阵相乘运算

通过@运算符实现矩阵相乘,还可以通过tf.matmul(a, b)函数实现。
Tensorflow中的矩阵相乘可以使用批量方式,也就是张量A和B的维度数可以大于2。当张量A和B维度数大于2时,Tensorflow会选择A和B的最后两个维度进行矩阵相乘,前面所有的维度都视为Batch维度。
根据矩阵相乘的定义,A和B能够矩阵相乘的条件是,A的倒数第一个维度长度(列)和B的倒数第二个维度长度(行)必须相等。比如张量a shape: [4, 3, 28, 32]可以与张量b shape: [4, 3, 32, 28]进行矩阵相乘