前言:
本专栏在保证内容完整性的基础上,力求简洁,旨在让初学者能够更快地、高效地入门TensorFlow2 深度学习框架。如果觉得本专栏对您有帮助的话,可以给一个小小的三连,各位的支持将是我创作的最大动力!
系列文章汇总:TensorFlow2 入门指南
Github项目地址:https://github.com/Keyird/TensorFlow2-for-beginner
文章目录
- 一、数据类型
- (1) 字符串型
- (2) 布尔类型
- (3) 整型、浮点型
- (4) 张量转换
- (5) 数值类型
- (6) 待训练张量
- 二、创建张量Tensor
- (1) 创建全0、全1张量
- (2) 创建自定义数值张量
- (3) 创建已知分布的张量
- (4) 创建序列
- (5) 从 Numpy、List 对象创建张量
- 三、索引与切片
- (1) 索引
- (2) 切片
- 四、维度变换
- 五、广播机制
- 六、数学运算
- 七、前向传播实战
- (1) 导入相关库:
- (2) 数据集准备:
- (3) 初始化变量
- (4) 迭代训练
TensorFlow2 是一个面向于深度学习算法的科学计算库,内部数据均以张量形式保存,所有的运算操作也都是基于张量进行的。复杂的神经网络算法本质上就是各种张量相乘、相加等基本运算操作的组合,所以在学习神经网络算法之前,我们先来了解一下 TensorFlow2 张量的基础操作方法~
一、数据类型
(1) 字符串型
通过tf.constant()即可创建字符串类型的张量:
s = tf.constant('Hello, TensoeFlow2!')
在 tf.strings 模块中,提供了常见的字符串型的工具函数,如拼接 join(),长度 length(),切分 split(),转换小写lower() 等等:
tf.strings.lower(s)
深度学习算法主要还是以数值类型张量运算为主,字符串类型的数据使用频率较低,我们不做过多阐述。
(2) 布尔类型
布尔类型张量的创建方式如下:
a = tf.constant(True)
传入布尔类型的向量:
a = tf.constant([True, False])
需要注意的是,TensorFlow 的布尔类型和 Python 语言的布尔类型并不对等,不能通用。
(3) 整型、浮点型
创建一个数值类型为整型的张量:
a = tf.constant(12, dtype=tf.int16)
创建一个数值类型为浮点型的张量:
b = tf.constant(12.5698, dtype=tf.float32)
(4) 张量转换
当然,我们也能对张量中数据的精度以及类型进行转换:
对精度进行转换:
a = tf.constant(12.5698, dtype=tf.float32)
a = tf.cast(a, tf.float64)
对类型进行转换:
b = tf.constant(12, dtype=tf.int16)
tf.cast(b, tf.double)
(5) 数值类型
数值类型的张量是 TensorFlow 的主要数据载体,主要分为:标量、向量、矩阵、张量(维度大于2)
创建标量,注意必须通过 TensorFlow 规定的方式去创建张量,而不能使用 Python 语言的标准变量创建方式。
a = tf.constant(1.2)
创建两个元素的向量:
b = tf.constant([1,2, 3.])
同样的方法来定义矩阵:
c = tf.constant([[1,2],[3,4]])
三维张量可以定义为:
d = tf.constant([[[1,2],[3,4]],[[5,6],[7,8]]])
(6) 待训练张量
在深度学习网络训练过程中,需要通过TensorFlow来建立梯度更新环境,用来对参数进行更新。为了区分需要计算梯度信息的张量与不需要计算梯度信息的张量,TensorFlow 增加了一种专门的数据类型来支持梯度信息的记录:tf.Variable。
tf.Variable 类型在普通的张量类型基础上添加了 name,trainable 等属性来支持计算图的构建。由于梯度运算会消耗大量的计算资源,而且会自动更新相关参数,对于不需要的优化的张量,如神经网络的输入 X,不需要通过 tf.Variable 封装;相反,对于需要计算梯度并优化的张量,如神经网络层的W和𝒃,需要通过 tf.Variable 包裹以便 TensorFlow 跟踪相关梯度信息。
下面,通过 tf.Variable()函数可以将普通张量转换为待优化张量:
a = tf.constant([-1, 0, 1, 2])
a = tf.Variable(a)
当然,也可以直接创建待训练张量:
a = tf.Variable([[1,2],[3,4]])
待优化张量可看做普通张量的特殊类型,普通张量也可以通过 GradientTape.watch()方法临时加入跟踪梯度信息的列表。
二、创建张量Tensor
(1) 创建全0、全1张量
创建全 0 和全 1 的向量:
tf.zeros([1]), tf.ones([1])
创建全 0 和全 1 的矩阵:
tf.zeros([2,2]), tf.ones([3,2])
通过 tf.zeros_like, tf.ones_like 可以方便地新建与某个张量 shape 一致,内容全 0 或全 1 的张量。例如,创建与张量 a 形状一样的全 0 张量:
a = tf.ones([4,5])
tf.zeros_like(a)
创建与张量 a 形状一样的全 1 张量:
a = tf.ones([4,5])
tf.ones_like(a)
(2) 创建自定义数值张量
除了初始化为全 0,或全 1 的张量之外,有时也需要全部初始化为某个自定义数值的张量,比如将张量的数值全部初始化为-1 等。通过 tf.fill(shape, value)可以创建全为自定义数值 value 的张量。
创建所有元素为-1 的向量:
tf.fill([1], -1)
创建所有元素为 9 的矩阵:
tf.fill([2,2], 9)
(3) 创建已知分布的张量
正态分布(Normal Distribution)和均匀分布(Uniform Distribution)是最常见的分布之一,创建采样自这 2 种分布的张量非常有用,比如在卷积神经网络中,卷积核张量 W 初始化为正态分布有利于网络的训练;在对抗生成网络中,隐藏变量 z 一般采样自均匀分布。
通过 tf.random.normal(shape, mean=0.0, stddev=1.0)可以创建形状为 shape,均值为mean,标准差为 stddev 的正态分布。例如,创建均值为 0,标准差为 1的正太分布:
tf.random.normal([2,2])
创建均值为 1,标准差为 2 的正太分布:
tf.random.normal([2,2], mean=1,stddev=2)
通过 tf.random.uniform(shape, minval=0, maxval=None, dtype=tf.float32)可以创建采样自 [𝑚𝑖𝑛𝑣𝑎𝑙, 𝑚𝑎𝑥𝑣𝑎𝑙] 区间的均匀分布的张量。
例如创建采样自区间[0,1],shape 为[2,2]的矩阵:
tf.random.uniform([2,2])
创建采样自区间[0,10],shape 为[2,2]的矩阵:
tf.random.uniform([2,2],maxval=10)
如果需要均匀采样整形类型的数据,必须指定采样区间的最大值 maxval 参数,同时制定数据类型为 tf.int* 型:
tf.random.uniform([2,2],maxval=100,dtype=tf.int32)
(4) 创建序列
在循环计算或者对张量进行索引时,经常需要创建一段连续的整形序列,可以通过tf.range()函数实现。tf.range(limit, delta=1)可以创建[0,𝑙𝑖𝑚𝑖𝑡)之间,步长为 delta 的整形序列,不包含 limit 本身。
例如,创建 0~99,步长为 1 的整形序列:
tf.range(100)
创建 0~99,步长为 2 的整形序列:
tf.range(100, 2)
通过 tf.range(start, limit, delta=1)可以创建[𝑠𝑡𝑎𝑟𝑡, 𝑙𝑖𝑚𝑖𝑡),步长为 delta 的序列,不包含 limit 本身。例如,创建5~99,步长为2的序列:
tf.range(5,100,delta=2)
(5) 从 Numpy、List 对象创建张量
通过 tf.convert_to_tensor 可以创建新 Tensor,并将保存在 Python List 对象或者 Numpy Array 对象中的数据导入到新 Tensor 中。
从List对象创建张量:
tf.convert_to_tensor([1,2.])
从Numpy对象创建张量:
tf.convert_to_tensor(np.array([[1,2.],[3,4]]))
三、索引与切片
(1) 索引
在 TensorFlow 中,支持基本的[𝑖][𝑗]…标准索引方式,也支持通过逗号分隔索引号的索引方式。
考虑输入张量 x 为 8 张 32x32 大小的彩色3通道图片,即 shape 为 [4,32,32,3] 的张量:
# x使用随即分布模拟产生
x = tf.random.normal([4,32,32,3])
接下来我们使用索引方式读取张量的部分数据。
取第 1 张图片的数据:
x[0] # 索引是从0开始的
取第 1 张图片的第 10 行:
x[0][10]
取第 1 张图片,第 10 行,第 8 列的像素:
x[0][10][8]
取第 3 张图片,第 10 行,第 8 列的像素,B 通道(第 2 个通道)颜色强度值:
x[0][10][8][2]
当然,也可以采用它的等价写法:
x[0, 10, 8, 2]
(2) 切片
通过𝑠𝑡𝑎𝑟𝑡: 𝑒𝑛𝑑: 𝑠𝑡𝑒𝑝切片方式可以方便地提取一段数据,其中 start 为开始读取位置的索引,end 为结束读取位置的索引(不包含 end 位),step 为读取步长。
我们还是以上面 shape 为 [4,32,32,3] 的图像为例,首先创建一个张量:
# x使用随即分布模拟产生
x = tf.random.normal([4,32,32,3])
读取第 2,3 张图片:
x[1:3]
start: end: step切片方式有很多简写方式,其中 start、end、step 3 个参数可以根据需要选择性地省略,全部省略时即::,表示从最开始读取到最末尾,步长为 1,即不跳过任何元素。
如 x[0,::]表示读取第 1 张图片的所有行,其中::表示在行维度上读取所有行,它等于x[0]的写法:
x[0,::]
为了更加简洁,::可以简写为单个冒号:,如取所有图片,隔行采样,隔列采样,所有通道信息,相当于在图片的高宽各缩放至原来的 50%:
x[:, 0:28:2, 0:28:2, :]
特别地,step 可以为负数,考虑最特殊的一种例子,step = −1时,表示逆序读取元素。
当张量的维度数量较多时,不需要采样的维度一般用单冒号:表示采样所有元素。比如读取第一张图片:
x[0,:,:,:]
在TensorFlow中,为了避免出现像 𝑥[0, : , : , : ] 这样出现过多冒号的情况,可以使用...
符号表示取多个维度上所有的数据,其中维度的数量需根据规则自动推断。例如上面的例子就可以写成:
x[0,...]
四、维度变换
TensorFlow中对张量维度进行变换的操作函数有以下几种:
- tf.reshape(x, [ b, w, h, c ])。按照 [b, w, h, c]的维度对输入张量x进行变换。
- tf.transpose(x, perm=[0, 1, 3 , 2])。按照perm对维度进行转置,比如原来x的维度信息是[4,3,2,1],经过转置后,维度信息变为[4,3,1,2]。如果没有设置perm,那么x按照默认转置方式,变换后的维度为[1,2,3,4]。
- tf.expand_dims(x, axis=0)。在第0轴增加一个维度,比如原来x的维度是[28,28,3],增加第0维度后,x的维度变为[1,28,28,3].。
- tf.squeeze(x)。仅仅减去所有通道数为1的维度,比如原来x的维度是[1,28,28,1],处理后维度变为[28,28]。
- tf.squeeze(x, axis=0)。指定减去某个通道数为1的维度。比如原来x的维度是[1,28,28,1],处理后维度变为[28,28,1]。
五、广播机制
广播机制,即 Broadcasting,它能很方便的解决不同维度张量间的运算问题。广播操作类似tf.tile()对数据进行复制扩张,不同的是,tf.tile()是真的复制。而 Broadcasting 并不会立即复制数据,它只是在逻辑上改变张量的形状,使得视图上变成了复制后的形状。
Broadcasting 会通过深度学习框架的优化手段避免实际复制数据而完成逻辑运算,相对比于 tf.tile 函数而言,减少了大量计算代价,而作用却和 tf.tile 一样。所以建议在运算过程中,尽量使用 Broadcasting 来提高计算效率。
下图展示了几种不同情形下,不同维度张量间执行加法元素时的广播操作,如下图所示:
以神经网络中前向传播的过程为例,网络中某一层的计算公式是:Y=X@W+b,假设该层X@W 的 shape 是 [100, 5],偏置 b 的 shape 是 [5]。很明显 X@W 和 b 的 shape 是不一样的,所以不能直接相加。这个时候就需要先对 b 进行 Broadcasting,具体操作如下:
y = x@w + tf.broadcast_to(b,[100,5])
注意:之所以采用广播操作,是为了优化计算,并且考虑到复制会占用内存。
六、数学运算
Tensorflow中张量a、b间的数学运算主要分为3大类:
- 张量中逐元素计算,比如:+、-、*、/、//、%。
- 对张量中某一个维度上的计算,比如:reduce_mean、max、min、sum。
- 张量整体计算,类似矩阵间的乘法一样。比如@、matmul。
Tensorflow中用于数学运算的元素符以及函数:
- 对于逐元素计算:常用的运算符有:+、-、*、**、/、//、%;常用的函数有:tf.math.log()、tf.exp()、tf.pow()、tf.sqrt()、
- 整体上计算:a@b、tf.matmul(a,b)
七、前向传播实战
下面,我们将利用已经学到的知识来搭建三层神经网络,实现 MNIST 手写数字识别。MNIST数据集在前面的文章中已经介绍过,这里就不再赘述。
(1) 导入相关库:
import tensorflow as tf
from tensorflow.keras import datasets
(2) 数据集准备:
# 加载数据集
(x, y), (x_test, y_test) = datasets.mnist.load_data()
# 转为张量
x = tf.convert_to_tensor(x, dtype=tf.float32) / 255.
y = tf.convert_to_tensor(y, dtype=tf.float32)
# 构建每一个batch数据
train_db = tf.data.Dataset.from_tensor_slices((x, y)).batch(128)
train_iter = iter(train_db)
sample = next(train_iter)
print('batch:', sample[0].shape, sample[1].shape)
(3) 初始化变量
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]))
(4) 迭代训练
for epoch in range(10):
# x:[128,28,28], y:[128]
for step, (x, y) in enumerate(train_db): # step = nums/batch
x = tf.reshape(x, [-1, 28*28])
# 构建梯度环境
with tf.GradientTape() as tape:
# 第一层: [b,784]*[784,256]+[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
# 将输出转换成热独码
y_onehot = tf.one_hot(y, depth=10)
# 建立mse损失函数
loss = tf.square(y_onehot - out)
loss = tf.reduce_mean(loss)
# 计算梯度
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
# 参数更新
lr = 1e-3
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])
if step % 100 == 0:
print(epoch, step, 'loss: ', float(loss))
本教程所有代码会逐渐上传github仓库:https://github.com/Keyird/TensorFlow2-for-beginner
如果对你有帮助的话,欢迎star收藏~
最好的关系是互相成就,各位的「三连」就是【AI 菌】创作的最大动力,我们下期见!