04_3_TensorFlow2基础_Broadcasting&数学运算&前向传播
Broadcasting
本质上是一个张量维度扩张的手段
对某个维度上面重复n多次,但是没有真正的复制数据,和tf.tile不同。tile会重复n多次并且会真实的在数据上体现出来(复制数据)。
是一种优化的手段,呈现出数据已经被扩张了。
在X @ W+b上,[10]变成[b,10]的步骤就是broadingcast的步骤
Key idea
- 小维度先对齐,没有的插入维度
- 扩张成为相同的大小
例子:
[1,3]->[4,3]没有真正复制成数据,只是有利于计算
如果是[1,4]和[1,3]进行相加,是不能broadencast的。
How to understand?
例子:
[8] 对所有班级所有学生(默认高维度是通用的)的物理和数学加分(偏置)
[35,1] 给每个班级的前x个学生的每个科目加x0分
Why broadcasting?
-
for real demanding
expand in (axis=0)
tf.tile 对1的维度进行复制,复制b次。变成[b,10]
–
一般来讲,大维度是更加高层的概念;小维度的是底层的概念,这样的话如果没有写高维度的配置,高维度是默认的,所有维度都适用的。
在这里,默认班级和学生的概念更高一些,让科目的概念更低一些,这样的话如果只给了一个低维度的概念,要给所有人加五分,默认每个班级和学生都是适合于这个配置的。
[5.0]是data,dimension为1,shape为1的Tensor,扩张成[4,32,8]
-
memory consumption
如果显式的通过tf.tile这种复制方式,就会扩张内存消耗,而通过broadcast是运行的时候自动优化的手段,这样就节省了大量的内存占用空间。
–
真正复制要消耗1024*4字节,利用broadcast只消耗8*4字节。
Broadcastable?
- Match from Last dim!
- if current dim=1,expand to same
- if either has no dim, insert one dim and expand to same
- otherwise, NOT broadcastable
例子:[4] 和 [1,3] 是不能broadcast的。
从右边对齐,如果没有维度,则插入1个维度,它的shape是1;如果有维度的话,比较他们维度的数量是不是相等的,如果不等的话就是不能broadcast。
例子:[4,32,14,14] 和 [1,32,1,1] 可以
[4,32,14,14] 和 [14,14] 默认先从右边(小维度)对齐,可以
[4,32,14,14] 和 [2,32,14,14] 2不能扩张成4,不能
It’s efficient and intuitive!(好处)
节省内存空间;编程的时候更简洁
Broadcasting(编程,自动broadcast)
只要对应操作支持broadcast,程序会自动判断如果shape不一致的时候,broadcast为相同的shape
tf.broadcast_to(编程,显式的)
Broadcast VS Tile
Tile占用的内存空间更大
Broadcast的shape已经扩张成[2,3,4],但是它的实际上的内存区域没有完成扩张的复制,所以更加高效
数学运算
Outline
- 加、减、乘、除
- 多少次方、平方
- 平方根
- 整除、取余
- l o g e log_e loge、 e n e^n en
- 张量,矩阵运算
- 矩阵形式的转换
Operation type
-
element-wise
对应元素的加减乘除
-
matrix-wise
矩阵运算的多个并行计算的过程
-
dim-wise
求某个维度的均值、最大值、最小值、求和
±*/%//
两个相同维度,可以加减乘除
还可以整除、余除
tf.math.log & tf.exp
这个log函数是以e为底的
要实现log2,log10
因为有公式 l o g a b / l o g a c = l o g c b log_a b / log_a c = log_c b logab/logac=logcb,因此要实现c是2即可
pow,sqrt
@或tf.matmul(a,b)
多维度的矩阵乘法
With broadcasting
Recap(复习)
其实矩阵的相乘规则是满足标量的相乘规则。矩阵相乘的设计就是使 y = ( y 0 , y 1 , y 2 ) y=(y^0,y^1,y^2) y=(y0,y1,y2)满足 y = w ∗ x + b y=w*x+b y=w∗x+b
具体的实例: Y = X @ W + b Y = X@W+b Y=X@W+b
加上非线性因子relu后,把负数的去掉,所以下图会全部保留下来
前向传播(张量)
基于我们学过的知识:创建Tensor、索引与切片、Broadcasting、数学运算。完成一个简单的前向传播。
Recap
之前讲过分类问题,通过一个简单的非线性层( r e l u [ X @ W + b 1 ] relu[X@W+b_1] relu[X@W+b1] ),通过串联3个非线性层,来增加它的复杂度,这样我们得到输出。然后构建一个loss函数,out和label的误差。通过最小化误差函数,来得到更新的过程,做之后[ W 1 ′ , b 1 ′ , W 2 ′ , b 2 ′ , W 3 , b 3 ′ W_1',b_1', W_2',b_2',W_3,b_3' W1′,b1′,W2′,b2′,W3,b3′]的参数。
本次是用代码实现这样的一个过程。
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets
# 加载数据集,自动从网上下载
# x:[60k,28,28] y:[60k]
(x, y), _ = datasets.mnist.load_data()
# 转化成Tensor
# 原来x的范围为[0,255],y的范围为[0,9],习惯把数据转换为0~1的数值
# x: [0,255] => [0,1.]
# y: [0,9]
x = tf.convert_to_tensor(x, dtype=tf.float32) / 255.
y = tf.convert_to_tensor(y, dtype=tf.int32)
print(x.shape, y.shape, x.dtype, y.dtype)
# 看x和y的最小值和最大值
print(tf.reduce_min(x), tf.reduce_max(x))
print(tf.reduce_min(y), tf.reduce_max(y))
# 设置batch,希望一次能取128张照片
train_db = tf.data.Dataset.from_tensor_slices((x, y)).batch(128)
# train_iter可以对train_db做next方法
train_iter = iter(train_db)
# 一直取,直到得到一个最终的元素。这里是一个sample例子
# x:(128, 28, 28) y:(128,)
sample = next(train_iter)
print('batch: ', sample[0].shape, sample[1].shape)
# 创建权值
# 层是降维的过程,一开始是[b,784] => 降维[b,256] => 降维[b,128] => 降维[b,10]
# w的shape要满足运算规则,因此为[dim_in,dim_out],初始化为截断的正态分布
# b的shape为[dim_out],初始化为0
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]))
lr = 1e-3 # 10^-3
for epoch in range(10): # 对整个数据集迭代10次,iterate db for 10,表示对数据集迭代多少次
# 对一次数据集的所有图片做一个循环
# for (x, y) in train_db:
for step, (x, y) in enumerate(train_db):
# for every batch,表示对当前的数据集,你迭代的数据集,迭代哪个进度。比如说batch为128,一共有60k图片,一共取多少次,step表示当前进度的表示条
# x:[128,28,28]
# y:[128]
# 维度变换 [b,28,28] => [b,28*28]
x = tf.reshape(x, [-1, 28 * 28])
# 使得梯度信息被记录下来
with tf.GradientTape() as tape: # 默认只会跟踪tf.Variable类型
# 在这里我们希望x:[b,28*28]
# h1 = x@w1 + b1
# [b,784]@[784,256] + [256] => [b,256]+[256] 加号会自动做broadcast/自己做broadcast
# b是batch,在这里b为128,也就是x.shape[0]
# => [b,256] + [b,256] => [b,256]
h1 = x @ w1 + tf.broadcast_to(b1, [x.shape[0], 256])
# 非线性转化
h1 = tf.nn.relu(h1)
# [b,256] => [b,128]
h2 = h1 @ w2 + b2
h2 = tf.nn.relu(h2)
# [b,128] => [b,10]
out = h2 @ w3 + b3
# compute loss
# 输出out:[b,10]
# 真实结果y:[b],因此要将y进行one-hot encoding
# =>[b,10]
y_onehot = tf.one_hot(y, depth=10)
# mse(均方误差) = mean(sum(y-out)^2) y-out的平方和,之后平均
# y-out 还是shape为[b,10]
loss = tf.square(y_onehot - out)
# 均值mean:得到scalar
# 相当于 loss / b / 10。/b是求出每一个batch上的均值;/10是求每个instance上每一个点的均值
loss = tf.reduce_mean(loss)
# 需要自动求导,需要把前向计算的过程,也就是需要求梯度的过程,把它包在GradientTape
# compute gradients
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
# print(grads)
# 本来optimizer可以一次w1 = w1 - lr * w1_grad,在这里进行手写
# 在这里相减后,w1又从tf.Variable变回了tf.Tensor,这里使用assign_sub进行原地更新,保持数据类型不变
w1.assign_sub(lr * grads[0])
# w1 = w1 - 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])
# print(isinstance(b3,tf.Variable))
# print(isinstance(b3,tf.Tensor))
if step % 100 == 0:
print(epoch, step, 'loss:', float(loss))
# 100 loss: nan 梯度爆炸,解决方案:给6个参数一个范围值,stddev=0.1