第2章
数据类型与张量详解
2.1 数据类型
TensorFlow本质上是一个深度学习的科学计算库,这个库的主要数据类型为张量,所有的运算都是基于张量数据进行的操作,更复杂的网络模型也只是一些基础运算的组合拼接,只有深入理解基本的张量运算,才能在各种深度学习的网络模型中游刃有余,开发出有价值、有创意的算法模型。
TensorFlow中基本的数据类型有数值类型、字符串类型和布尔类型。下面简单举例介绍。
(1)数值类型:var x=tf.Variable(10,name:"x")。
(2)字符串类型:var mammal1=tf.Variable("Elephant",name:"var1",dtype:tf.@string)。
(3)布尔类型:var bo=tf.Variable(true)。
具体数据类型如下。
2.2 张量详解
类似于NumPy中的N维数组对象NDArray,TensorFlow中数据的基本单位为张量(Tensor)。二者都是多维数组的概念。我们可以使用张量表示标量(0维数组)、向量(1维数组)、矩阵(2维数组)。
张量的主要特性为形状、类型和值,可以通过张量的属性shape、dtype和方法numpy()来获取特性。举例如下。
1.形状获取
上述代码运行后返回结果true,张量的属性shape通过返回整型1维数组的方式来显示张量的形状。
我们也可以直接通过TensorFlow.Binding封装的print()方法输出形状。
输出如下。
2.类型获取
输出如下。
张量的属性dtype返回TF_DataType类型的枚举,可以很方便地通过print()方法进行输出。
3.值获取
输出如下。
张量的numpy()方法返回NumSharp.NDArray类型的值,内容为张量保存的值内容,可以很方便地通过print()方法进行输出。
4.类型转换
在C#中,可以快速对0维张量进行类型转换,通过在变量前加(type)进行强制类型转换,代码参考如下。
结果输出如下。
通过上述结果可以看到,原来的张量类型被快速转换成普通的数值类型。
2.3 常量与变量
从行为特性来看,有常量constant和变量Variable两种类型的张量。
常量在计算图中不可以被重新赋值;变量在计算图中可以用assign等算子重新赋值。
1.常量
一般的常量类型如下。
输出如下。
我们来写一个4维的常量,并通过tf.rank函数返回张量的秩。
一般来说,标量为0维张量,向量为1维张量,矩阵为2维张量。
彩色图像有r、g、b三个通道,可以表示为3维张量。
视频增加一个时间维,可表示为4维张量。
代码如下。
代码运行后输出结果如下。
2.变量
在深度学习模型中,一般被训练的参数需要定义为变量,变量的值可以在模型训练过程中被修改。
我们简单测试一个2维数组的变量,代码如下。
代码输出如下。
接下来我们一起看下常量和变量的差别:常量的值可以参与运算,也可以被重新赋值,但是重新赋值或运算后的结果会开辟新的内存空间;变量的值可以通过assign、assign_add等算子重新赋值。代码如下。
输出结果如下。
2.4 字符串常见操作
TensorFlow.NET中有专门的创建、转换和截取等常见的字符串操作。
1.通过byte数组创建字符串
传入Hex十六进制的byte数组,打印输出对应的转换后的字符串,代码如下。
输出结果如下。
也可以通过ToString()方法将张量整体转换为字符串进行输出,代码如下。
输出结果如下。
2.通过numpy()方法转换字符串的值
numpy()方法可以转换字符串的值,代码如下。
输出结果如下。
3.通过tf.strings.substr()方法截取字符串
tf.strings.substr()方法可以截取字符串。下述代码演示字符串的截取,并对截取后的字符串进行比较,最后输出张量的布尔标量值。
输出结果如下。
主要参数说明如下。
方法名:tf.strings.substr(Tensor input,int pos,int len,string name=null,string@uint="BYTE")。
参数1:input,类型为Tensor,待截取的输入字符串。
参数2:pos,类型为int,起始位置。
参数3:len,类型为int,截取长度。
返回值:类型为Tensor,返回截取后输出的字符串。
4.字符串数组张量的创建和转换
下述代码演示了字符串数组张量的创建,并可以打印出张量的形状和张量的内容。ToString()方法可以将张量的完整内容转换为string;StringData()方法可以提取出张量的值并转换为string[]数组类型。
输出结果如下。
5.本地文件(图像)读取示例
我们可以通过tf.io.read_file()方法从本地读取文件(图像),并打印和测试该文件(图像)的前3个byte数据,代码如下。
输出结果如下。
2.5 基本张量操作
张量是TensorFlow.NET中常用的数据结构,TensorFlow.NET中内置了大量的基础张量操作方法,可以进行张量的创建、索引和修改等。
1.tf.cast改变张量的数据类型
下述例子演示的是将int32类型的值转换为float32类型的值。
通过tf.cast将int32类型的值转换为float32类型的值,输出结果如下。
2.tf.range创建区间张量值
常用参数说明如下。
参数1:start,区间初始值。
参数2:limit,区间限定值,取值<limit(不等于limit)。
参数3:delta,区间值递增的差量。
输出结果如下。
3.tf.zeros/tf.ones创建0值和1值的张量
下述例子创建了一个3×4的0值张量和4×5的1值张量,一般可用于张量的初始化。
输出结果如下。
4.tf.random生成随机分布张量
tf.random.normal用于随机生成正态分布的张量;tf.random.truncated_normal用于随机生成正态分布的张量,并剔除2倍方差以外的数据。
常用参数说明如下。
参数1:shape,生成的正态分布张量的形状。
参数2:mean,正态分布的中心值。
参数3:stddev,正态分布的标准差。
输出结果如下。
5.索引切片
可以通过张量的索引来读取元素;对于变量,可以通过索引对部分元素进行修改。
下述为张量的索引功能的演示。
输出结果如下。
下述为张量的切片读取功能的演示。
输出结果如下。
下述为张量的切片赋值功能的演示,通过assign算子实现。
程序运行后,通过assign算子对[":2",":2"]的张量切片进行赋值,结果如下。
6.张量比较
tf.equal可以比较两个张量是否相同;ToScalar可以获取布尔标量值。代码如下。
输出结果如下。
2.6 维度变换
张量的维度变换操作主要是指改变张量的形状,主要方法有tf.reshape、tf.squeeze、tf.expand_dims和tf.transpose。
1.tf.reshape改变张量的形状
tf.reshape主要改变张量的形状,该操作不会改变张量在内存中的存储顺序,因此速度非常快,并且操作可逆。
输出结果如下。
2.tf.squeeze维度压缩简化
tf.squeeze可以消除张量中的单个元素的维度。和tf.reshape一样,该操作不会改变张量在内存中的存储顺序。
输出结果如下。
3.tf.expand dims增加维度
tf.squeeze的逆向操作为tf.expand_dims,即往指定的维度中插入长度为1的维度。
输出结果如下。
4.tf.transpose维度交换
tf.transpose可以交换张量的维度,与tf.reshape不同,它会改变张量在内存中的存储顺序。
输出结果如下。
tf.transpose维度交换过程示意图如图2-1所示。
图2-1 tf.transpose维度交换过程示意图
2.7 合并分割
张量的合并分割和NumPy类似,其中合并有两种不同的实现方式:tf.concat可以连接不同的张量,在同一设定的维度进行,不会增加维度;tf.stack采用维度堆叠的方式,会增加维度。
1.tf.concat
我们来测试一下使用tf.concat连接3个形状为[2,2]的张量。concatValue1通过在axis:0维度中的张量连接操作,将3个张量合并为1个形状为[6,2]的新张量;concatValue2通过在axis:-1维度中的张量连接操作,将3个张量合并为1个形状为[2,6]的新张量。
输出如下,正确地实现了张量的连接合并功能。
2.tf.stack
同样是上面的例子,我们将tf.concat替换为tf.stack。可以看到,tf.stack在指定的维度上创建了新的维度,并将输入张量在新维度上进行堆叠操作。通过代码的运行,我们可以看到两种方式的内部机制的差异。
输出结果如下。
上面两个例子演示了张量的合并,接下来我们来测试张量的分割。张量的分割方法tf.split是tf.concat方法的逆操作,可以将张量平均分割或按照指定的形状分割。
3.tf.split
我们利用下述代码首先将a、b、c合并为shape:[3,2,2]的concatValue,然后通过tf.split将concatValue的0维分割,还原为3个shape:[2,2]的张量数组splitValue。
输出结果如下。
2.8 广播机制
本节我们聊聊在NumPy和张量中都很常用并很重要的一个特性:Broadcasting,即广播机制,又称作自动扩展机制。广播是一种十分轻量的张量复制操作,只会在逻辑上扩展张量的形状,而不会直接执行实际存储I/O的复制操作。经过广播后的张量在视图上会体现出复制后的形状。
在进行实际数据运算的时候,广播机制会通过深度学习框架的优化技术,避免实际复制数据而完成逻辑运算。对于用户来说,广播机制和tf.tile复制数据的最终实现效果是相同的,但是广播机制节省了大量的计算资源并自动优化了运算速度。
但是,广播机制并不是任何场合都适用的,下面我们来介绍广播机制的使用规则和实现效果。
(1)如果张量的维度不同,则对维度较小的张量左侧补齐进行扩展,直到两个张量的维度相同。
(2)如果两个张量在某个维度上的长度是相同的,或者其中一个张量在该维度上的长度为1,则我们说这两个张量在该维度上是相容的。
(3)如果两个张量在所有维度上或通过上述(1)的过程扩展后都是相容的,则它们能使用广播机制。这是广播机制的核心思想——普适性。
(4)广播之后,每个维度的长度取两个张量在该维度长度上的较大值。
(5)在任何一个维度上,如果一个张量的长度为1,另一个张量的长度大于1,那么在该维度上,就好像对第一个张量进行了复制。
我们通过图解的方式进一步举例说明。
首先来看可广播的情形:张量B的形状为[w,1],张量A的形状为[b,h,w,c],不同维度的张量相加运算A+B是可以正常运行的,这就是广播机制的作用,张量B通过广播机制扩展为和A相同的形状[b,h,w,c]。正常的广播扩展过程如图2-2所示,分为3步。
图2-2 正常的广播扩展过程
然后来看不可广播的情形:同样是上面这个例子,如果张量B的形状为[w,2],同时张量A的形状为[b,h,w,c],其中c≠2,则这两个张量不符合普适性原则,无法应用广播机制,运行张量相加操作A+B会触发报错机制。无法应用广播机制的内部原理如图2-3所示。
广播机制的实现有两种方式。
1.隐式自动调用
在进行不同形状的张量运算时,隐式地自动调用广播机制,如用“+、-、*、/”等运算,先将参与运算的张量广播成统一的形状,再进行相应的运算。
图2-3 无法应用广播机制的内部原理
运行结果如下。
2.显式广播方法
使用tf.broadcast_to显式地调用广播方法,将指定的张量广播至指定的形状。
运行结果如下。