05_2_TensorFlow2高阶_填充与复制&张量限幅&高阶操作
填充与复制
Outline
- pad 数据的填充
- tile 数据的复制
- broadcast_to
Pad(填充)
补充0,-1或者其他,默认填充0
举例子:shape为[3]
A表示左边填充1个单元,B表示右边填充2个单元。
=> [6]
例子:[2,2]
[ [0,1], [1,1] ] 上面补充0行,下面补充1行;左边补充1列,右边补充1列
具体的实例:
tf.pad(a,[[1,0],[0,0]])
Image padding
行和列,上下和左右padding2个单位
一开始 B略<A,我们为了使B=A,首先将A ->(padding) A’,A’会略>A
A’->B,可能等于A。选择合适的padding数量。
先使A放大一点点,A’通过convolution后,会得到B,B的size略<A’,这样的话A和B是有可能相等的。
这是image之间做convolution非常常见的操作。
tile(复制)
按不同维度复制会有不同的结果。就是绕着某个维度进行重复的复制的工作。
Inner dim first
tf.tile(a,[1,2])
1表示第1个维度不复制,2表示第2个维度复制1次。
1可以理解为保持不变,2表示当前的维度复制1次,新的Tensor是之前的2倍。
tile VS broadcast_to
最终的效果可能是一样的,但是从性能上来讲broadcast更强,它没有占用另外的内存,是运行时候完成的优化的操作。
并且broadcast有时不需要主动转换,有些操作会自动转换broadcast。
张量限幅
Outline
- clip_by_value 通过具体数值裁减掉
- relu 是通过clipping实现的
- clip_by_norm 根据总体的范数来裁剪张量
- gradient clipping 根据一群norm的总体的值来裁剪每一个张量的norm:gradient clipping非常常见
clip_by_value
根据值的裁剪,是通常意义上的限幅。
maximum(0,x),如果x<0, result=0;else x
minimum(0,x),如果x>0,result=0;else x
如果要将数据限制在某个区间内,比如2~8之间:min(8,max(x,2))
以下的方式更加简便:tf.clip_by_value(a,2,8)
给定最小值和最大值,底层还是通过maximum和minimum实现的,
上面代码的下限幅是2,上限幅是8。
relu
利用限幅实现rulu函数
tf.maximum(a,0) == tf.nn.relu(a)
clip_by_norm
根据范数来进行裁剪。
对一个Vector,它本身是有方向的,gradient代表函数的值增加的方向,负gradient代表函数的值减小的方向。如果我们在gradient clipping的时候,就是当Tensor的值变大变小的时候,如果我们是根据值来进行裁剪的话,这个Vector(x,y),如果x成分裁减了,y成分没有裁剪的话,就意味着改变了Vector的方向。对于gradient来说,我们是不希望改变gradient的方向的,因为gradient方向是我们已经计算出来的,如果做clipping的时候改变gradient的方向的话,这样是不利于找到最优解的,所以我们既需要把值给限制到比较小的范围之内,但是又不希望改变方向的话,就有等比例的放缩,首先把整体的模求出来,
(
x
,
y
)
∣
∣
(
x
,
y
)
∣
∣
(
二
范
数
)
\frac{(x,y)}{||(x,y)||(二范数)}
∣∣(x,y)∣∣(二范数)(x,y),这样归一化到0~1的范围之内,然后再乘以一个值,比如希望整体的模最大值是15,那么
(
x
,
y
)
∣
∣
(
x
,
y
)
∣
∣
∗
15
\frac{(x,y)}{||(x,y)||} * 15
∣∣(x,y)∣∣(x,y)∗15,这样的范围就约束在15之内了,这样就实现了等比例的放缩。既没有改变Tensor的方向,然后又使Tensor整体的数值减少了,这就是clip_by_norm的功能。
tf.norm(a)
默认为二范数,
∑
x
2
\sqrt{\sum x^2}
∑x2
tf.clip_by_norm(a,15)
对a根据norm进行放缩,a的新的二范数为15.000001
限幅就是限制新的norm,a->aa保持方向不变,只改变向量的模
Gradient clipping(梯度裁剪)
所有的这些都是创造良好的环境,使得我们的gradient能很好的引导我们的网络走向比较优良的方向,也就是loss下降的方向。
- 其中有两大障碍,Gradient Exploding(梯度爆炸)和vanishing(梯度消失)
- Gradient Exploding就是gradient的值太大了,一次前进的步长太长了,这样会导致不停地来回震荡
- Gradient Vanishing就是值太少了,长时间不动,loss一直不动
- 为了使MNIST上模拟复现这些问题,在案例中set lr = 1
- 有Gradient-w1,Gradient-w2,首先w1是一个方向,w2是一个方向,他们都是一个向量,方向是多维的,它不再是1维、2维的,有几百维几千维。我们可以通过clip_by_norm来使得w1的方向不变,w1的norm减少,但是w1和w2本身都是代表了整体的梯度下降的方向,也就是这个网络的下降方向不只有w1的参数决定,还有很多其他的参数w1,w2,w3。我们希望w1,w2,w3在做缩放的时候,里面的所有方向都不做改变。
- 可以使用**
new_grads, total_norm = tf.clip_by_global_norm(grads,25)
** - 实现对整体的所有的norm等比例缩放,而不会出现w1缩放10倍,w2缩放1倍,w3缩放0.1倍。 [ w 1 , w 2 , w 3 ] ∣ ∣ g _ w 1 ∣ ∣ + ∣ ∣ g _ w 2 ∣ ∣ \frac{[w1,w2,w3]}{||g\_w1||+||g\_w2||} ∣∣g_w1∣∣+∣∣g_w2∣∣[w1,w2,w3]
- 传入grads,[g_w1,g_w2, …]
- 给定一个25,是指所有gradient整体的norm,所以比一般的clip_by_norm要稍微大一点, ∣ ∣ g _ w 1 ∣ ∣ + ∣ ∣ g _ w 2 ∣ ∣ ||g\_w1||+||g\_w2|| ∣∣g_w1∣∣+∣∣g_w2∣∣。
- total_norm为之前没有做clipping时候的norm,可能有100
- new_grads对每一个w1进行缩放, w 1 2 \frac{w1}{2} 2w1
具体的实例:
一般来说,范数在0~20之间是比较合适的。更新1次后,范数越来越大,意味着gradient exploding;如果范数接近0.0001的时候就是gradient vanish。可以看到这里已经出现了gradient exploding,这个时候就需要进行裁剪(clipping)。
因此我们需要对w1,w2,w3,b1,b2,b3的模做一个裁剪,裁剪的时候保持一个同比例,比如每个都裁剪4倍,利用clip_by_global_norm保持等比例的裁剪,只需要把所有的grads [g_w1,g_b1, …]保持整体norm是15。分别打印裁剪前和裁剪后的梯度值。
保证裁剪以后的范数是比较小的范围,这样更新是比较稳定的。
这个能不能收敛呢?看一个实例。
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, layers, optimizers
print(tf.__version__)
(x, y), _ = datasets.mnist.load_data()
x = tf.convert_to_tensor(x, dtype=tf.float32) / 50.
y = tf.convert_to_tensor(y)
y = tf.one_hot(y, depth=10)
print('x:', x.shape, 'y:', y.shape)
train_db = tf.data.Dataset.from_tensor_slices((x,y)).batch(128).repeat(30)
x,y = next(iter(train_db))
print('sample:', x.shape, y.shape)
# print(x[0], y[0])
def main():
# 784 => 512
w1, b1 = tf.Variable(tf.random.truncated_normal([784, 512], stddev=0.1)), tf.Variable(tf.zeros([512]))
# 512 => 256
w2, b2 = tf.Variable(tf.random.truncated_normal([512, 256], stddev=0.1)), tf.Variable(tf.zeros([256]))
# 256 => 10
w3, b3 = tf.Variable(tf.random.truncated_normal([256, 10], stddev=0.1)), tf.Variable(tf.zeros([10]))
optimizer = optimizers.SGD(lr=0.01)
for step, (x,y) in enumerate(train_db):
# [b, 28, 28] => [b, 784]
x = tf.reshape(x, (-1, 784))
with tf.GradientTape() as tape:
# layer1.
h1 = x @ w1 + b1
h1 = tf.nn.relu(h1)
# layer2
h2 = h1 @ w2 + b2
h2 = tf.nn.relu(h2)
# output
out = h2 @ w3 + b3
# out = tf.nn.relu(out)
# compute loss
# [b, 10] - [b, 10]
loss = tf.square(y-out)
#下面两个合在一起和loss = tf.reduce_mean(loss)等价
#下面第1个先计算每个batch上的均值,下面第2个计算每个instance上每一个点的均值
# [b, 10] => [b]
loss = tf.reduce_mean(loss, axis=1)
# [b] => scalar
loss = tf.reduce_mean(loss)
# compute gradient
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
print('==before==')
for g in grads:
print(tf.norm(g))
grads, _ = tf.clip_by_global_norm(grads, 15)
print('==after==')
for g in grads:
print(tf.norm(g))
# update w' = w - lr*grad
optimizer.apply_gradients(zip(grads, [w1, b1, w2, b2, w3, b3]))
if step % 100 == 0:
print(step, 'loss:', float(loss))
if __name__ == '__main__':
main()
高阶操作技巧
Outline
- where 根据坐标有目的性的选择
- scatter_nd 根据坐标有目的性的更新
- meshgrid 生成坐标系
Where(tensor)
Tensor是boolean的类型,where对这个Tensor操作时会返回一系列的坐标,就是当前True所在的坐标。比如对于一个shape为[3,3]的Tensor,一共有3个True元素,因此第一个维度会返回3;每个坐标是2维的Tensor。->[3,2],3行2列的Tensor,这个是shape。实际会返回:
[[0,0]
[1,1]
[2,2]]
返回三个这样的坐标,是一个[3,2]的Tensor。拿到后可以和之前的tf.gather_nd一起,收集到对应元素为True的位置的值。
a是3行3列的随机分布,与0比较得到mask,返回一个类型为Boolean型的Tensor,shape不变。
通过使用tf.boolean_mask(a,mask)
取得元素位置上为True的对应元素。
tf.where(mask)
查询到所有元素为True的值的坐标。
结合tf.gather_nd(a,indices)
,取回的都是True上的值。
where返回的Tensor的shape为[N,n维],N为True的数量,n为原来的Tensor是几维。
如果where接收到3个参数tf.where(cond,A,B)
,它会根据中间的True or False,从A、B中间有目的性的选择值。True的元素会从A上面选,False的元素会从B上面选。
scatter_nd
根据坐标来有目的性的更新,首先给出原始状态(底板),底板是根据shape给定的,比如[2,2] [8],给出全是0的底板,给出具体的数和坐标,将该坐标下更新这个值到底板上去。
首先看一个1维的例子。
它可以实现两个功能:
- 更新:对指定位置清零,再把新的值A’‘加到A-A’
- 对指定位置的值的相加/相减
在看2维的例子:
白色表示0,将updatas的值更新到shape中,获得output。
实现方法:
meshgrid
如果要画一个3D的图片,先要生成3D的坐标轴,下图最下的面为坐标轴,根据点来存元素的值,叫做z。这样可以得到每一个点上的函数的值,就能把函数很好的画出来。
Points
x:-2~2
y:-2~2
再给出采样的间隔,在-2~2之间采样5个点,这样x上有5个点,y上有5个点,一共生成25个点。[25,2],一共有25个点,每个点的坐标用两个数字来表示,就会生成这样的Tensor。
怎么得到这样坐标的Tensor?
Numpy
两层嵌套,-2~2间隔5个点,生成点保存到list去,在转换为np的array。[25,2]
但是这个方法是没有GPU加速的;而且是numpy实现的,无法和TensorFlow深度结合在一起。
GPU acceleration
目的:给出x: [-2~2], y: [-2,2],生成Points: [N,2]
实现:
y = tf.linspace(-2.,2,5)
x = tf.linspace(-2.,2,5)
points_x,points_y = tf.meshgrid(x,y)
point_x和point_ya拼在一起组合成[5,5,2]的Tensor,前面的一部分存储在points_x,后面的一部分存储在points_y,其实是把一个坐标拆分开来,保存在两个Tensor中去。前半部分是x的坐标,后半部分是y的坐标
目前是[5,5,2],还原成[25,2]的格式:利用tf.stack
,再利用reshape。
axis=2返回的结果为[5,5,2],因为函数使得points_x的x和points_y的y维度合并。
画出函数的曲面 z = s i n ( x ) + s i n ( y ) z = sin(x) + sin(y) z=sin(x)+sin(y)
实现:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
import tensorflow as tf
from matplotlib import pyplot as plt
def func(x):
'''
:param x: [b,2]
:return:
'''
z = tf.math.sin(x[...,0]) + tf.math.sin(x[...,1])
return z
x = tf.linspace(0.,2*3.14,500)
y = tf.linspace(0.,2*3.14,500)
#[500,500]
point_x,point_y = tf.meshgrid(x,y)
#[500,500,2]
points = tf.stack([point_x,point_y], axis=2)
# points = tf.reshape(points,[-1,2])
print('points:',points.shape)
z = func(points)
print('z:',z.shape)
plt.figure('plot 2d func value')
plt.imshow(z,origin='lower',interpolation='none')
plt.colorbar()
plt.figure('plot 2d func contour')
plt.contour(point_x,point_y,z)
plt.colorbar()
plt.show()