Tensorflow2.0 语法

0 / 1210

行列轴

以列表为例(抽象举例,摞起来的面包片。。。。)

[               # 最外层,无意义不用记 
    [1,2,3],    # 面包片1  (第一个样本)
    [4,5,6],    # 面包片2  (第二个样本)
]
  1. 每个 次内层列表 代表一个样本, 比如 [1,2,3] 整体代表 第一个样本
  2. 最内层元素代表属性值。 eg: 1,2,3 单个拿出来都是属性值。
  3. 例子: 元素5 单独拿出来,它就被看做 "第二个样本的,属性值5" (当然横纵索引依然都是从0取的)

以刚才的数据为例:

t = tf.constant(
    [
        [1., 2., 3.], 
        [4., 5., 6.]
    ]
)

print(tf.reduce_sum(t, axis=0))  # 求和操作,上下压扁, 聚合样本
>> tf.Tensor([5. 7. 9.], shape=(3,), dtype=float32) 

print(tf.reduce_sum(t, axis=1))  # 求和操作,左右压扁, 聚合属性
>> tf.Tensor([ 6. 15.], shape=(2,), dtype=float32)

行列轴自己的记忆方式(axis=0, axis=1):

  1. 0轴通常代表,样本(上下压扁)
  2. 1轴通常代表,属性(左右压扁)
    常需要用 axis参数 的相关聚合函数:
    tf.reduce_sum() # 求和
    tf.reduce_mean() # 平均值
    tf.reduce_max() # 最大值
    tf.reduce_min() # 最小值
    tf.square() # 平方
    tf.concat() # 拼接
    注: 如果 axis参数 "不传", 那么"所有维度"都会被操作。

常用导入

# 基本会用到的
import numpy as np
import tensorflow as tf
from tensorflow import keras

# 可选导入
import os, sys, pickle

import scipy
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.preprocessing import StandardScaler      # 标准化
from sklearn.model_selection import train_test_split  # 训测分离

转换数据类型

tf.cast(tf.equal(1,2), tf.int32)

张量tf.Tensor (加减乘除,矩阵乘也可以用@, 但必须是tensor)

print(tf.add(1,2))
print(tf.subtract(1,2))
print(tf.multiply(1,2))
# tf20 除法有些问题,结果是标量 0.5 ,需手动转Tensor,TF24改过来了divide直接就是tensor
print(tf.cast(tf.divide(1,2),tf.int32))

print(tf.matmul([[1,2]], [[5],[6]]))  # 矩阵乘 也可用  @,但必须是tensor

常量(普通的张量)

定义:

c = tf.constant( [[1., 2., 3.], [4., 5., 6.]] )    # 数字后面加个点代表 转float32 类型
print(c)  
>> tf.Tensor([[1. 2. 3.] [4. 5. 6.]], shape=(2, 3), dtype=float32)

矩阵乘法

语法格式:  a @ b
条件要求:  a的列数 === b的行数     (必须相等)
eg:       (5行2列 @ 2行10列 = 5行10列)

特例: (第0维度,必须相等)
    t1 = tf.ones([2, 20, 30])
    t2 = tf.ones([2, 30, 50])
    print( (t1@t2).shape )
    >> (2, 20, 50)   # 第0维没变, 后2维照常按照矩阵乘法运算

矩阵转置:

tf.transpose(t)
# 不仅可以普通转置,还可以交换维度
t2 = tf.transpose(t,[1,0])    # 行变列,列变行。 和基本的转置差不多(逆序索引,轴变逆序)

# 或假如以 (2,100,200,3)形状 为例
t = tf.ones([2, 100, 200, 3])                 
print(tf.transpose(t, [1, 3, 0, 2]).shape) # 轴交换位置
>> (100, 3, 2, 200)
# 原1轴 -> 放在现在0轴
# 原3轴 -> 放在现在1轴
# 原0轴 -> 放在现在2轴
# 原2轴 -> 放在现在3轴

加减乘除都具有"广播机制" :我尝试用白话解释下:

  1. 我形状和你不一样, 但我和你运算的时候,我会尽力扩张成 你的形状 来和你运算。
  2. 扩张后如果出现空缺, 那么把自己复制一份,填补上 (如果补不全,就说明不能运算)
eg:
    t = tf.constant(
        [
            [1, 2, 3],
            [4, 5, 6],
        ]
    )
    t + [1,2,1] 
过程分析:
    [1,2,1] 显然是 小形状, 它会自动尝试变化成大形状 ->
        第一步变形(最外层大框架满足, 里面还有空缺):
            [
                [1,2,1],        
            ]
        第二步变形 (把自己复制,然后填补空缺):
            [
                [1,2,1],
                [1,2,1],          # 这就是复制的自己
            ]
        第三步运算(逐位相加)
            [             +     [              =  [
                [1,2,3],            [1,2,1],          [2,4,4],
                [4,5,6],            [1,2,1],          [5,7,7],
            ]                   ]                 [

抽象(广播机制)演示:

注意:我以下的数据演示,全是表示 Tensor的形状,形状,形状!
    [5,200,1,50]     # 很明显,开始这2行数据 维度没匹配, 形状也没对齐
           [5,1]
------------------------            
    [5,200,1,50]    
          [5,50]     # 这行对齐补50
------------------------            
    [5,200,5,50]     # 这行对齐补5
          [5,50]
------------------------            
    [5,200,5,50]           
    [1, 1, 5,50]     # 这行扩张了2个, 默认填1
------------------------            
    [5,200,5,50]           
    [1,200, 5,50]    # 这行对齐补200
------------------------            
    [5,200,5,50]           
    [5,200,5,50]     # 这行对齐补5

注意:
    1. 每个维度形状:二者必须有一个是1, 才能对齐。 (不然ERROR,下例ERROR->)
        [5,200,1,50]
              [5,20]    # 同理开始向右对齐,但是  50与20都不是1,所以都不能对齐,所以ERROR
    2. 若维度缺失:
        依然是全部贴右对齐
        然后先从右面开始,补每个维度的形状 
        然后扩展维度,并默认设形状为1
        然后补扩展后维度的形状(因为默认设为1了,所以是一定可以补齐的)

当然上面说的都是运算时的自动广播机制
你也可以手动广播:

t1 = tf.ones([2, 20, 1])                # 原始形状  【2,20,1】
print(tf.broadcast_to(t1, [5,2,20,30]).shape) # 目标形状【5,2,20,30】

[5,2,20,30]
  [2,20, 1]
-----------
[5,2,20,30]
  [2,20,30]
-----------
[5,2,20,30] 
[1,2,20,30]
-----------
[5,2,20,30]
[5,2,20,30]

注:因为是手动广播,所以只能 原始形状 自己向 目标形状 ”补充维度,或者补充形状“
    而目标形状是一点也不能动的。

扩充维度(f.expand_dims)+ 复制(tile) 代替 => 广播(tf.broadcasting)
同样是上面的例子,我想把形状 [2,20,1] ,变成 [5,2,20,30]

t1 = tf.ones([2, 20, 1])
a = tf.expand_dims(t1,axis=0)  # 0轴索引处插入一个轴, 结果[1,2,20,1]
print(tf.tile(a,[5,1,1,30]).shape)    # 结果 [5, 2, 20, 30]
流程:
    [5,2,20,30]
      [2,20,1]
    -----------
    [5,2,20,30]    # tf.expand_dims(t1,axis=0)
    [1,2,20,1]     # 0号索引插入一个新轴(增维)
    -----------
    [5,2,20,30]    # tf.tile(5,1,1,30)   (形状对齐,tile每个参数代表对应轴的形状扩充几倍)
    [5,2,30,30]         1*5 2*1 20*1 1*30

tile 与 broadcasting的区别:

  1. tile是物理复制,物理空间增加
  2. 而broadcasting是虚拟复制,(为了计算,隐式实现的复制,并没有物理空间增加)
  3. tile可以对任意(整数倍复制n*m, mn同为整数)
  4. 而broadcasting(原始数据形状只能存在1的情况下才能扩张。 1*n , n为整数)
    压缩维度(tf.squeeze):
    就是把每个维度为1的维度都删除掉 (就像数学 a * 1 = a)
    print(tf.squeeze(tf.ones([2,1,3,1])).shape)

    (2, 3)

当然你也可以指定维度压缩(默认不指定,所有维度为1的全部压缩):
print(tf.squeeze(tf.ones([2,1,3,1]), axis=-1).shape)

(2, 1, 3)

索引&切片

灵魂说明:无论索引还是切片, (行列 是使用 逗号 分隔的), 并且无论行列,索引都是从0开始的。
索引:取一个值

print(t[1,2])  # 逗号前面代表行的索引, 逗号后面是列的索引
>> tf.Tensor(6.0, shape=(), dtype=float32)

切片:取子结构 (有两种方式)
方式1(冒号切片):

print(t[:, 1:])  # 逗号前面是行。只写: 代表取所有行。逗号后面是列。 1: 代表第二列到最后
>> tf.Tensor([[2. 3.] [5. 6.]], shape=(2, 2), dtype=float32)

方式2(省略号切片): (我相信不了解Numpy的人都没听说过 python的 Ellipsis , 就是省略号类)
先自己去运行玩玩这行代码:

print(... is Ellipsis)
>>> True

回到正题:(省略号 ... 切片,是针对多维度的, 如果是二维直接用:即可)

(我们以三维为例,这个就不适合称作行列了)
# shape 是 (2, 2, 2)
t = tf.constant(
    [   # 一维
        [   # 二维
            [1, 2], # 三维
            [3, 4],
        ],
        [
            [5, 6],
            [7, 8],
        ],
    ]
)
伪码:t[1维切片, 二维切片, 三维切片]
代码:t[:, :, 0:1]     # 1维不动, 2维不动, 3维 取一条数据
结果: shape为 (2,2,1)
    [   # 一维
        [   # 二维
            [1], # 三维
            [3],
        ],
        [
            [5],
            [7],
        ],
    ]

即使我不对 1维,和 2维切片,我也被迫要写 2个: 来占位
那假如有100个维度,我只想对最后一个维度切片。 前99个都不用动, 那难道我要写 99个 : 占位??
不,如下代码即可解决:

print(t[..., 0:1])       # 这就是 ... 的作用 (注意,只在 numpy 和 tensorflow中有用)

tensor 转 numpy 类型

t.numpy()    # tensor 转为 numpy 类型

变量

定义:

v = tf.Variable(   # 注意: V是大写
    [
        [1, 2, 3], 
        [4, 5, 6]
    ]
)

变量赋值(具有自身赋值的性质):

注意: 变量一旦被定义,形状就定下来了。 赋值(只能赋给同形状的值)

v.assign(
    [
        [1,1,1], 
        [1,1,1],
    ]
)
print(v)
>> <tf.Variable 'Variable:0' shape=(2, 3) dtype=int32, numpy=array([[1, 1, 1],[1, 1, 1]])>

变量取值(相当于转换为Tensor):

特别: 变量本身就是 Variable类型, 取值取出得是 Tensor (包括切片取值,索引取值等)
print( v.value() )
>> tf.Tensor([[1 2 3] [4 5 6]], shape=(2, 3), dtype=int32)

变量 索引&切片 赋值:

常量:是不可变的。所以只有取值,没有赋值。
变量:取值、赋值都可以
v.assign(xx)  类似于 python的 v=xx

v[0, 1].assign(100)          # 索引赋值,  v.assign 等价于
v[0, :].assign([10, 20, 30]) # 注意,切片赋值传递的需要是容器类型

特别注意: 前面说过,变量 结构形状 是 不可变的,赋值的赋给的是数据。
           但是你赋值的时候要时刻注意,不能改变变量原有形状
拿切片赋值为例:
    你切多少个,你就得赋多少个。 并且赋的值结构要一致。
    举个栗子: 你从正方体里面挖出来一个小正方体。那么你必须填补一块一模一样形状的小正方体)
    
还有两种扩展API:
    v.assign_add()  # 类似python 的 +=
    v.assign_sub()  # 类似python 的 -=

Variable 转 Numpy

print(v.numpy())

不规则张量(RaggedTensor)

定义:

rag_tensor = tf.ragged.constant(
    [ 
        [1,2], 
        [2,3,4,5],
    ]
)    # 允许每个维度的数据长度参差不齐

拼接:假如需要"拼接 不规则张量" (可使用 tf.concat(axis=) )

0轴:竖着拼接(样本竖着摞起来)可随意拼接。 拼接后的依然是"不规则张量"
1轴:横着拼接(属性水平拼起来)这时候需要你样本个数必须相等, 否则对不上,报错
总结: 样本竖着随便拼, 属性横着(必须样本个数相等) 才能拼

RaggedTensor 普通 Tensor:

说明:普通Tensor是必须要求, 长度对齐的。入 对不齐的 末尾补0
tensor = rag_tensor.to_tensor()

稀疏张量 (Sparse Tensor)

特点(可理解为 记录索引):

  1. 只记录非0的坐标位置, indices参数:每个 子列表 表示 一个坐标
  2. 虽然只记录坐标,但是转为普通Tensor后,只有坐标位置 有值, 其他位置的值全是0
  3. 填充范围,取决于 dense_shape的设定

定义:

s = tf.SparseTensor(
    indices=[[0, 1], [1, 0], [2, 3]],  # 注意,这个索引设置需要是(从左到右,从上到下)的顺序设置
    values=[1, 2, 3],   # 将上面3个坐标值分别设值为 1,2,3
    dense_shape=[3, 4]  # Tensor总范围
)
print(s)
>> SparseTensor(indices=tf.Tensor([[0 1], [1 0],[2 3]], shape=(3, 2), dtype=int64)。。。

转为普通 Tensor (转为普通Tensor后,看见的才是存储真正的值)

tensor = tf.sparse.to_dense(s) 
print(tensor)
>> tf.Tensor([ [0 1 0 0],[2 0 0 0],[0 0 0 3] ], shape=(3, 4), dtype=int32)

如果上面使用 to_dense() 可能会遇到错误:

error: is out of range

这个错误的原因是创建 tf.SparseTensor(indices=) ,前面也说了indices,要按(从左到右,从上到下)顺序写
当然你也可以用排序API,先排序,然后再转:
eg:
    _ = tf.sparse.reorder(s)        # 先将索引排序
    tensor = tf.sparse.to_dense(_)  # 再转

tf.function

这个API作为一个装饰器使用, 用来将 Python 语法转换 尽可能有效的转换为 TF语法、图结构

import tensorflow as tf
import numpy as np

@tf.function
def f():
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])
    return a + b
print( f() )
>>> tf.Tensor([5 7 9], shape=(3,), dtype=int32)

你应该发现了一个特点,我们定义的 f()函数内部,一个tf语法都没写。 只装饰了一行 @tf.function
而调用结果返回值居然是个 tensor 。
这就是 @tf.function 装饰器的作用了!

当然函数里面,也可以写 tf的操作,也是没问题的。
但注意一点, 函数里面不允许定义 变量, 需要定义的变量 应 拿到函数外面定义
a = tf.Variabl