Tensorflow2.0基础函数学习笔记(2)

维度变换

算法的每个模块对于数据张量的格式有不同的逻辑要求,当现有的数据格式不满足算法要求时,需要通过维度变换将数据调整为正确的格式。这就是维度变换的功能。
基本的维度变换包含了改变视图 reshape,插入新维度 expand_dims,删除维度squeeze,交换维度 transpose,复制数据 tile 等。

Reshape

在介绍改变视图操作之前,我们先来认识一下张量的存储和视图(View)的概念。张量的视图就是我们理解张量的方式,比如 shape 为[2,4,4,3]的张量 A,我们从逻辑上可以理解为 2 张图片,每张图片 4 行 4 列,每个位置有 RGB 3 个通道的数据;张量的存储体现在张量在内存上保存为一段连续的内存区域,对于同样的存储,我们可以有不同的理解方式,
比如上述 A,我们可以在不改变张量的存储下,将张量 A 理解为 2 个样本,每个样本的特征为长度 48 的向量。这就是存储与视图的关系。
我们通过 tf.range()模拟生成 x 的数据:

In [67]: x=tf.range(96)
x=tf.reshape(x,[2,4,4,3])
Out[67]:
<tf.Tensor: id=11, shape=(2, 4, 4, 3), dtype=int32, numpy=
array([[[[ 0, 1, 2],
 [ 3, 4, 5],
 [ 6, 7, 8],
 [ 9, 10, 11]],

从语法上来说,视图变换只需要满足新视图的元素总量与内存区域大小相等即可,即新视图的元素数量等于𝑏 ∗ ℎ ∗w∗ 𝑐。正是由于视图的设计约束很少,完全由用户定义,使得在改变视图时容易出现逻辑隐患。
举个例子,对于 shape 为[4,32,32,3]的图片数据,通过 Reshape 操作将 shape 调整为[4,1024,3],此时视图
的维度顺序为𝑏 − 𝑝𝑖𝑥𝑒𝑙 − 𝑐,张量的存储顺序为[𝑏, ℎ, , 𝑐]。可以将[4,1024,3]恢复为

  • [𝑏, ℎ, , 𝑐] = [4,32,32,3]时,新视图的维度顺序与存储顺序无冲突,可以恢复出无逻辑问题的数据
  • [𝑏, , ℎ, 𝑐] = [4,32,32,3]时,新视图的维度顺序与存储顺序冲突
  • [ℎ ∗ ∗ 𝑐, 𝑏] = [3072,4]时,新视图的维度顺序与存储顺序冲突

在 TensorFlow 中,可以通过张量的 ndimshape 成员属性获得张量的维度数和形状:

In [68]: x.ndim,x.shape
Out[68]:(4, TensorShape([2, 4, 4, 3]))

通过 tf.reshape(x, new_shape),可以将张量的视图任意的合法改变

In [69]: tf.reshape(x,[2,-1])
Out[69]:<tf.Tensor: id=520, shape=(2, 48), dtype=int32, numpy=
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95]])>

增删维度

增加维度 增加一个长度为 1 的维度相当于给原有的数据增加一个新维度的概念,维度长度为 1,故数据并不需要改变,仅仅是改变数据的理解方式,因此它其实可以理解为改变视图的一种特殊方式。
考虑一个具体例子,一张 28x28 灰度图片的数据保存为 shape 为[28,28]的张量,在末尾给张量增加一新维度,定义为为通道数维度,此时张量的 shape 变为[28,28,1]:

In [72]:
x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32)
Out[72]:
<tf.Tensor: id=552, shape=(28, 28), dtype=int32, numpy=
array([[4, 5, 7, 6, 3, 0, 3, 1, 1, 9, 7, 7, 3, 1, 2, 4, 1, 1, 9, 8, 6, 6,
 4, 9, 9, 4, 6, 0],

通过tf.expand_dims(x, axis)可在指定的 axis 轴前可以插入一个新的维度:

In [73]: x = tf.expand_dims(x,axis=2)
Out[73]:
<tf.Tensor: id=555, shape=(28, 28, 1), dtype=int32, numpy=
array([[[4],
 [5],
 [7],
 [6],
 [3],

可以看到,插入一个新维度后,数据的存储顺序并没有改变,依然按着 4,5,7,6,3,0,…的顺序保存,仅仅是在插入一个新的维度后,改变了数据的视图。
需要注意的是,tf.expand_dims 的 axis 为正时,表示在当前维度之前插入一个新维度;为负时,表示当前维度之后插入一个新的维度。以[𝑏, ℎ, , 𝑐]张量为例,不同 axis 参数的实际插入位置如下图 4.6 所示:
在这里插入图片描述
删除维度 是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为 1 的维度,也不会改变张量的存储。继续考虑增加维度后 shape 为[1,28,28,1]的例子,如果希望将图片数量维度删除,可以通过 tf.squeeze(x, axis)函数,axis 参数为待删除的维度的索引号,图片数量的维度轴 axis=0:

In [75]: x = tf.squeeze(x, axis=0)
Out[75]:
<tf.Tensor: id=586, shape=(28, 28, 1), dtype=int32, numpy=
array([[[8],
 [2],
 [2],
 [0],

如果不指定维度参数 axis,即 tf.squeeze(x),那么他会默认删除所有长度为 1 的维度:

In [77]:
x = tf.random.uniform([1,28,28,1],maxval=10,dtype=tf.int32)
tf.squeeze(x)
Out[77]:
<tf.Tensor: id=594, shape=(28, 28), dtype=int32, numpy=
array([[9, 1, 4, 6, 4, 9, 0, 0, 1, 4, 0, 8, 5, 2, 5, 0, 0, 8, 9, 4, 5, 0,
 1, 1, 4, 3, 9, 9],

交换维度

改变视图、增删维度都不会影响张量的存储。在实现算法逻辑时,在保持维度顺序不变的条件下,仅仅改变张量的理解方式是不够的,有时需要直接调整的存储顺序,即交换维度(Transpose)。通过交换维度,改变了张量的存储顺序,同时也改变了张量的视图。

交换维度操作是非常常见的,比如在 TensorFlow 中,图片张量的默认存储格式是通道后行格式:[𝑏, ℎ, , 𝑐],但是部分库的图片格式是通道先行:[𝑏, 𝑐, ℎ, ],因此需要完成[𝑏, ℎ, , 𝑐]到[𝑏, 𝑐, ℎ, ]维度交换运算。我们以[𝑏, ℎ, , 𝑐]转换到[𝑏, 𝑐, ℎ, ]为例,介绍如何使用 tf.transpose(x, perm)函数完成维度交换操作,其中 perm 表示新维度的顺序 List。考虑图片张量 shape 为[2,32,32,3],图片数量、行、列、通道数的维度索引分别为 0,1,2,3,如果需要交换为[𝑏, 𝑐, ℎ, ]格式,则新维度的排序为图片数量、通道数、行、列,对应的索引号为[0,3,1,2],实现如下:

In [78]: x = tf.random.normal([2,32,32,3])
tf.transpose(x,perm=[0,3,1,2])
Out[78]:
<tf.Tensor: id=603, shape=(2, 3, 32, 32), dtype=float32, numpy=
array([[[[-1.93072677e+00, -4.80163872e-01, -8.85614634e-01, ...,
 1.49124235e-01, 1.16427064e+00, -1.47740364e+00],
 [-1.94761145e+00, 7.26879001e-01, -4.41877693e-01, ...

需要注意的是,通过 tf.transpose 完成维度交换后,张量的存储顺序已经改变,视图也随之改变,后续的所有操作必须基于新的存续顺序进行。

数据复制

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

In [80]:
b = tf.constant([1,2])
b = tf.expand_dims(b, axis=0)
b
Out[80]:
<tf.Tensor: id=645, shape=(1, 2), dtype=int32, numpy=array([[1, 2]])>

In [81]: b = tf.tile(b, multiples=[2,1])
Out[81]:
<tf.Tensor: id=648, shape=(2, 2), dtype=int32, numpy=
array([[1, 2],
 [1, 2]])>

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

Broadcasting

Broadcasting 也叫广播机制(自动扩展也许更合适),它是一种轻量级张量复制的手段,在逻辑上扩展张量数据的形状,但是只要在需要时才会执行实际存储复制操作。对于大部分场景,Broadcasting 机制都能通过优化手段避免实际复制数据而完成逻辑运算,从而相对于 tf.tile 函数,减少了大量计算代价。
继续考虑上述的Y = X@W + 𝒃的例子,X@W的 shape 为[2,3],𝒃的 shape 为[3],我们可以通过结合 tf.expand_dims 和 tf.tile 完成实际复制数据运算,将𝒃变换为[2,3],然后与X@W完成相加。但实际上,我们直接将 shape 为[2,3]与[3]的𝒃相加:

x = tf.random.normal([2,4])
w = tf.random.normal([4,3])
b = tf.random.normal([3])
y = x@w+b

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

y = x@w + tf.broadcast_to(b,[2,3])

Broadcasting 机制的核心思想是普适性,即同一份数据能普遍适合于其他位置。在验证普适性之前,需要将张量 shape 靠右对齐,然后进行普适性判断:对于长度为 1 的维度,默认这个数据普遍适合于当前维度的其他位置;对于不存在的维度,则在增加新维度后默认当前数据也是普适性于新维度的,从而可以扩展为更多维度数、其他长度的张量形状。
考虑 shape 为[ , 1]的张量 A,需要扩展为 shape:[𝑏, ℎ, , 𝑐],如图
在这里插入图片描述
首先将 2 个 shape 靠右对齐,对于通道维度 c,张量的现长度为 1,则默认此数据同样适合当前维度的其他位置,将数据逻辑上复制𝑐 − 1份,长度变为 c;对于不存在的 b 和 h 维度,则自动插入新维度,新维度长度为 1,同时默认当前的数据普适于新维度的其他位置,即对于其它的图片、其他的行来说,与当前的这一行的数据完全一致。这样将数据b,h 维度的长度自动扩展为 b,h,如图
在这里插入图片描述
我们来考虑不满足普适性原则的例子,如下图
在这里插入图片描述
在 c 维度上,张量已经有 2 个特征数据,新 shape 对应维度长度为 c(𝑐 ≠ 2,比如 c=3),那么当前维度上的这 2 个特征无法普适到其他长度,故不满足普适性原则,无法应用Broadcasting 机制。
在进行张量运算时,有些运算可以在处理不同 shape 的张量时,会隐式自动调用Broadcasting 机制,如+,-,*,/等运算等,将参与运算的张量Broadcasting 成一个公共shape,再进行相应的计算,如图 所示,演示了 3 种不同 shape 下的张量 A,B 相加的例子:
在这里插入图片描述

数学运算

前面的章节我们已经使用了基本的加减乘除等数学运算函数,本节我们将系统地介绍
TensorFlow 中常见的数学运算函数。

加减乘除

整除和余除也是常见的运算之一,分别通过//和%运算符实现。我们来演示整除运
算:


```python
In [89]:
a = tf.range(5)
b = tf.constant(2)
a//b
Out[89]:
<tf.Tensor: id=115, shape=(5,), dtype=int32, numpy=array([0, 0, 1, 1, 2])>

In [90]: a%b
Out[90]:
<tf.Tensor: id=117, shape=(5,), dtype=int32, numpy=array([0, 1, 0, 1, 0])>

乘方

通过 tf.pow(x, a)可以方便地完成$𝑦 = 𝑥 ^ 𝑎乘方运算,也可以通过运算符**实现𝑥 ∗∗ 𝑎运
算,实现如下:

In [91]:
x = tf.range(4)
tf.pow(x,3)
Out[91]:
<tf.Tensor: id=124, shape=(4,), dtype=int32, numpy=array([ 0, 1, 8, 27])>

In [92]: x**2
Out[92]:
<tf.Tensor: id=127, shape=(4,), dtype=int32, numpy=array([0, 1, 4, 9])>

特别地,对于常见的平方和平方根运算,可以使用 tf.square(x)和 tf.sqrt(x)实现。平方运算
实现如下:

In [94]:x = tf.range(5)
x = tf.cast(x, dtype=tf.float32)
x = tf.square(x)
Out[94]:
<tf.Tensor: id=159, shape=(5,), dtype=float32, numpy=array([ 0., 1., 4.,
9., 16.], dtype=float32)>

In [95]:tf.sqrt(x)
Out[95]:
<tf.Tensor: id=161, shape=(5,), dtype=float32, numpy=array([0., 1., 2., 3.,
4.], dtype=float32)>

指数、对数

通过 tf.pow(a, x)或者**运算符可以方便实现指数运算𝑎𝑥,特别地,对于自然指数𝑒𝑥,可以通过 tf.exp(x)实现:

In [97]: tf.exp(1.)
Out[97]:
<tf.Tensor: id=182, shape=(), dtype=float32, numpy=2.7182817>

在 TensorFlow 中,自然对数l g𝑒 𝑥可以通过 tf.math.log(x)实现:

In [98]: x=tf.exp(3.)
tf.math.log(x)
Out[98]:
<tf.Tensor: id=186, shape=(), dtype=float32, numpy=3.0>

如果希望计算其他底数的对数,可以根据对数的换底公式:
在这里插入图片描述
间接的通过 tf.math.log(x)实现。如计算log10𝑥可以通过 𝑙𝑜𝑔𝑒 𝑥/𝑙𝑜𝑔𝑒 10
实现如下

In [99]: x = tf.constant([1.,2.])
x = 10**x
tf.math.log(x)/tf.math.log(10.)
Out[99]:
<tf.Tensor: id=222, shape=(2,), dtype=float32, numpy=array([0. ,
2.3025851], dtype=float32)>

矩阵相乘

神经网络中间包含了大量的矩阵相乘运算,前面我们已经介绍了通过@运算符可以方便的实现矩阵相乘,还可以通过 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]可以与张量 bshape:[4,3,32,2]进行矩阵相乘:

In [100]:
a = tf.random.normal([4,3,23,32])
b = tf.random.normal([4,3,32,2])
a@b
Out[100]:
<tf.Tensor: id=236, shape=(4, 3, 23, 2), dtype=float32, numpy=
array([[[[-1.66706240e+00, -8.32602978e+00],
 [ 9.83304405e+00, 8.15909767e+00],
 [ 6.31014729e+00, 9.26124632e-01],

矩阵相乘函数支持自动 Broadcasting 机制:

In [101]:
a = tf.random.normal([4,28,32])
b = tf.random.normal([32,16])
tf.matmul(a,b)
Out[101]:
<tf.Tensor: id=264, shape=(4, 28, 16), dtype=float32, numpy=
array([[[-1.11323869e+00, -9.48194981e+00, 6.48123884e+00, ...,
 6.53280640e+00, -3.10894990e+00, 1.53050375e+00],
 [ 4.35898495e+00, -1.03704405e+01, 8.90656471e+00, ...,

前向传播实战

到现在为止,我们已经介绍了如何创建张量,对张量进行索引切片,维度变换和常见的数学运算等操作。本节我们将利用我们已经学到的知识去完成三层神经网络的实现:
在这里插入图片描述
我们采用的数据集是 MNIST 手写数字图片集,输入节点数为 784,第一层的输出节点数是256,第二层的输出节点数是 128,第三层的输出节点是 10,也就是当前样本属于 10 类别的概率。
首先创建每个非线性函数的 w,b 参数张量:

w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))

在前向计算时,首先将 shape 为[𝑏, 28,28]的输入数据 Reshape 为[𝑏, 784]:

# [b, 28, 28] => [b, 28*28]
 x = tf.reshape(x, [-1, 28*28])

完成第一个非线性函数的计算,我们这里显示地进行 Broadcasting:

# [b, 784]@[784, 256] + [256] => [b, 256] + [256] => [b, 256] +
[b, 256]
 h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256])
 h1 = tf.nn.relu(h1)

同样的方法完成第二个和第三个非线性函数的前向计算,输出层可以不使用 ReLU 激活函数:

# [b, 256] => [b, 128]
 h2 = h1@w2 + b2
 h2 = tf.nn.relu(h2)
 # [b, 128] => [b, 10]
 out = h2@w3 + b3y 

将真实的标注张量 y 转变为 one-hot 编码,并计算与 out 的均方差:

#y_onehot=(tf.one_hot(y, depth=10) 
#mse = mean(sum(y_onehot-out)^2)
 # [b, 10]
 loss = tf.square(y_onehot- out)
 # mean: scalar
 loss = tf.reduce_mean(loss)

上述的前向计算过程都需要包裹在 with tf.GradientTape() as tape 上下文中,使得前向计算时能够保存计算图信息,方便反向求导运算。

        with tf.GradientTape() as tape:
            h1 = x @ w1 + b1
            h1 = tf.nn.relu(h1)

            h2 = h1 @ w2 + b2
            h2 = tf.nn.relu(h2)

            out = h2 @ w3 + b3

            loss = tf.square(y-out)
            loss = tf.reduce_mean(loss)
			#通过 tape.gradient()函数求得网络参数到梯度信息
            grads = tape.gradient(loss,[w1,b1,w2,b2,w3,b3])

并按照
在这里插入图片描述
来更新网络参数:

# w1 = w1 - lr * w1_grad
 w1.assign_sub(lr * grads[0])
 b1.assign_sub(lr * grads[1])
 w2.assign_sub(lr * grads[2])
 b2.assign_sub(lr * grads[3])
 w3.assign_sub(lr * grads[4])
 b3.assign_sub(lr * grads[5])

其中 assign_sub()将原地(In-place)减去给定的参数值,实现参数的自我更新操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值