原文https://github.com/lyhue1991/eat_tensorflow2_in_30_days
1、张量
张量的操作主要包括张量的结构操作和张量的数学运算。
张量结构操作诸如:张量创建,索引切片,维度变换,合并分割。
张量数学运算主要有:标量运算,向量运算,矩阵运算。另外我们会介绍张量运算的广播机制。
本篇我们介绍张量的结构操作。
创建张量:张量创建的许多方法和numpy中创建array的方法很像。
import tensorflow as tf
import numpy as np
a = tf.constant([1,2,3],dtype = tf.float32)
tf.print(a)
b = tf.range(1,10,delta = 2)
c = tf.linspace(0.0,2*3.14,100)
d = tf.zeros([3,3])
a = tf.ones([3,3])
b = tf.zeros_like(a,dtype= tf.float32)
b = tf.fill([3,2],5)
#均匀分布随机
tf.random.set_seed(1.0)
a = tf.random.uniform([5],minval=0,maxval=10)
#正态分布随机
b = tf.random.normal([3,3],mean=0.0,stddev=1.0)
#正态分布随机,剔除2倍方差以外数据重新生成
c = tf.random.truncated_normal((5,5), mean=0.0, stddev=1.0, dtype=tf.float32)
# 特殊矩阵
I = tf.eye(3,3) #单位矩阵
t = tf.linalg.diag([1,2,3]) #对角阵
索引切片:张量的索引切片方式和numpy几乎是一样的。切片时支持缺省参数和省略号。
对于tf.Variable,可以通过索引和切片对部分元素进行修改。x[1,:].assign(tf.constant([0.0,0.0]))
对于提取张量的连续子区域,也可以使用tf.slice。tf.slice(t,[1,0],[3,5])
此外,对于不规则的切片提取,可以使用tf.gather,tf.gather_nd,tf.boolean_mask。
tf.boolean_mask功能最为强大,它可以实现tf.gather,tf.gather_nd的功能,并且tf.boolean_mask还可以实现布尔索引。
如果要通过修改张量的某些元素得到新的张量,可以使用tf.where,tf.scatter_nd。
# 对于不规则的切片提取,可以使用tf.gather,tf.gather_nd,tf.boolean_mask。
# 考虑班级成绩册的例子,有4个班级,每个班级10个学生,每个学生7门科目成绩。可以用一个4107的张量来表示。
scores = tf.random.uniform((4,10,7),minval=0,maxval=100,dtype=tf.int32)
#抽取每个班级第0个学生,第5个学生,第9个学生的全部成绩
p = tf.gather(scores,[0,5,9],axis=1)
#抽取每个班级第0个学生,第5个学生,第9个学生的第1门课程,第3门课程,第6门课程成绩
q = tf.gather(tf.gather(scores,[0,5,9],axis=1),[1,3,6],axis=2)
# 抽取第0个班级第0个学生,第2个班级的第4个学生,第3个班级的第6个学生的全部成绩
#indices的长度为采样样本的个数,每个元素为采样位置的坐标
s = tf.gather_nd(scores,indices = [(0,0),(2,4),(3,6)])
# 以上tf.gather和tf.gather_nd的功能也可以用tf.boolean_mask来实现。
#抽取每个班级第0个学生,第5个学生,第9个学生的全部成绩
p = tf.boolean_mask(scores,[True,False,False,False,False,
True,False,False,False,True],axis=1)
#抽取第0个班级第0个学生,第2个班级的第4个学生,第3个班级的第6个学生的全部成绩
s = tf.boolean_mask(scores,
[[True,False,False,False,False,False,False,False,False,False],
[False,False,False,False,False,False,False,False,False,False],
[False,False,False,False,True,False,False,False,False,False],
[False,False,False,False,False,False,True,False,False,False]])
#利用tf.boolean_mask可以实现布尔索引
#找到矩阵中小于0的元素
c = tf.constant([[-1,1,-1],[2,2,-2],[3,-3,3]],dtype=tf.float32)
tf.print(c,"\n")
tf.print(tf.boolean_mask(c,c<0),"\n")
tf.print(c[c<0]) #布尔索引,为boolean_mask的语法糖形式
以上这些方法仅能提取张量的部分元素值,但不能更改张量的部分元素值得到新的张量。如果要通过修改张量的部分元素值得到新的张量,可以使用tf.where和tf.scatter_nd。
tf.where可以理解为if的张量版本,此外它还可以用于找到满足条件的所有元素的位置坐标。
tf.scatter_nd的作用和tf.gather_nd有些相反,tf.gather_nd用于收集张量的给定位置的元素,
而tf.scatter_nd可以将某些值插入到一个给定shape的全0的张量的指定位置处。
#找到张量中小于0的元素,将其换成np.nan得到新的张量
#tf.where和np.where作用类似,可以理解为if的张量版本
c = tf.constant([[-1,1,-1],[2,2,-2],[3,-3,3]],dtype=tf.float32)
d = tf.where(c<0,tf.fill(c.shape,np.nan),c)
#如果where只有一个参数,将返回所有满足条件的位置坐标
indices = tf.where(c<0)
indices
#将张量的第[0,0]和[2,1]两个位置元素替换为0得到新的张量
d = c - tf.scatter_nd([[0,0],[2,1]],[c[0,0],c[2,1]],c.shape)
#scatter_nd的作用和gather_nd有些相反
#可以将某些值插入到一个给定shape的全0的张量的指定位置处。
indices = tf.where(c<0)
tf.scatter_nd(indices,tf.gather_nd(c,indices),c.shape)
维度变换:相关函数主要有 tf.reshape, tf.squeeze, tf.expand_dims, tf.transpose.
- tf.reshape 可以改变张量的形状。
- tf.squeeze 可以减少维度。
- tf.expand_dims 可以增加维度。
- tf.transpose 可以交换维度。
tf.reshape可以改变张量的形状,但是其本质上不会改变张量元素的存储顺序,所以,该操作实际上非常迅速,并且是可逆的。
# 改成 (3,6)形状的张量
b = tf.reshape(a,[3,6])
# 改回成 [1,3,3,2] 形状的张量
c = tf.reshape(b,[1,3,3,2])
如果张量在某个维度上只有一个元素,利用tf.squeeze可以消除这个维度。和tf.reshape相似,它本质上不会改变张量元素的存储顺序。
张量的各个元素在内存中是线性存储的,其一般规律是,同一层级中的相邻元素的物理地址也相邻。
s = tf.squeeze(a)
d = tf.expand_dims(s,axis=0) #在第0维插入长度为1的一个维度
tf.transpose可以交换张量的维度,与tf.reshape不同,它会改变张量元素的存储顺序。tf.transpose常用于图片存储格式的变换上。
转换成 Channel,Height,Width,Batch
s= tf.transpose(a,perm=[3,1,2,0])
合并分割:和numpy类似,可以用tf.concat和tf.stack方法对多个张量进行合并,可以用tf.split方法把一个张量分割成多个张量。
tf.concat和tf.stack有略微的区别,tf.concat是连接,不会增加维度,而tf.stack是堆叠,会增加维度。
a = tf.constant([[1.0,2.0],[3.0,4.0]])
b = tf.constant([[5.0,6.0],[7.0,8.0]])
c = tf.constant([[9.0,10.0],[11.0,12.0]])
tf.concat([a,b,c],axis = 0) # shape=(6, 2)
tf.concat([a,b,c],axis = 1) # shape=(2, 6)
tf.stack([a,b,c]) # shape=(3, 2, 2)
# tf.stack([a,b,c],axis=1) # shape=(2, 3, 2)
tf.split是tf.concat的逆运算,可以指定分割份数平均分割,也可以通过指定每份的记录数量进行分割。
d = tf.split(c,3,axis = 0) #指定分割份数,平均分割 shape=(2, 2)
e = tf.split(c,[2,2,2],axis = 0) #指定每份的记录数量 shape=(2, 2)
2、张量的数学运算
张量数学运算主要有:标量运算,向量运算,矩阵运算。另外我们会介绍张量运算的广播机制。
标量运算:加减乘除乘方,以及三角函数,指数,对数等常见函数,逻辑比较运算符等都是标量运算符。
标量运算符的特点是对张量实施逐元素运算。
有些标量运算符对常用的数学运算符进行了重载。并且支持类似numpy的广播特性。
许多标量运算符都在 tf.math 模块下。
a*b, a+b, a//b, tf.sqrt(a), tf.maximum(a,b)等
向量运算:只在一个特定轴上运算,将一个向量映射到一个标量或者另外一个向量。 许多向量运算符都以 reduce 开头。
#向量reduce
a = tf.range(1,10)
tf.print(tf.reduce_sum(a))
tf.print(tf.reduce_mean(a))
tf.print(tf.reduce_max(a))
tf.print(tf.reduce_min(a))
tf.print(tf.reduce_prod(a))
#张量指定维度进行reduce
b = tf.reshape(a,(3,3))
tf.print(tf.reduce_sum(b, axis=1, keepdims=True)) # [[6] [15] [24]]
tf.print(tf.reduce_sum(b, axis=0, keepdims=True)) # [[12 15 18]]
#bool类型的reduce
p = tf.constant([True,False,False])
q = tf.constant([False,False,True])
tf.print(tf.reduce_all(p)) # 0
tf.print(tf.reduce_any(q)) # 1
#利用tf.foldr实现tf.reduce_sum
s = tf.foldr(lambda a,b:a+b,tf.range(10)) # 45
#tf.math.top_k可以用于对张量排序 利用tf.math.top_k可以在TensorFlow中实现KNN算法
a = tf.constant([1,3,7,5,4,8])
values,indices = tf.math.top_k(a,3,sorted=True)
#arg最大最小值索引
a = tf.range(1,10)
tf.print(tf.argmax(a))
tf.print(tf.argmin(a))
矩阵运算:矩阵必须是二维的。类似tf.constant([1,2,3])这样的不是矩阵。
矩阵运算包括:矩阵乘法,矩阵转置,矩阵逆,矩阵求迹,矩阵范数,矩阵行列式,矩阵求特征值,矩阵分解等运算。
除了一些常用的运算外,大部分和矩阵有关的运算都在tf.linalg子包中。
#矩阵乘法
a = tf.constant([[1,2],[3,4]])
b = tf.constant([[2,0],[0,2]])
a@b #等价于tf.matmul(a,b)
#矩阵转置
a = tf.constant([[1.0,2],[3,4]])
tf.transpose(a)
#矩阵逆,必须为tf.float32或tf.double类型
a = tf.constant([[1.0,2],[3.0,4]],dtype = tf.float32)
tf.linalg.inv(a)
#矩阵求trace
a = tf.constant([[1.0,2],[3,4]])
tf.linalg.trace(a)
#矩阵求范数
a = tf.constant([[1.0,2],[3,4]])
tf.linalg.norm(a)
#矩阵行列式
a = tf.constant([[1.0,2],[3,4]])
tf.linalg.det(a)
#矩阵svd分解
a = tf.constant([[1.0,2.0],[3.0,4.0]],dtype = tf.float32)
v,s,d = tf.linalg.svd(a)
tf.matmul(tf.matmul(s,tf.linalg.diag(v)),d)
#利用svd分解可以在TensorFlow中实现主成分分析降维
广播机制:TensorFlow的广播规则和numpy是一样的:
- 1、如果张量的维度不同,将维度较小的张量进行扩展,直到两个张量的维度都一样。
- 2、如果两个张量在某个维度上的长度是相同的,或者其中一个张量在该维度上的长度为1,那么我们就说这两个张量在该维度上是相容的。
- 3、如果两个张量在所有维度上都是相容的,它们就能使用广播。
- 4、广播之后,每个维度的长度将取两个张量在该维度长度的较大值。
- 5、在任何一个维度上,如果一个张量的长度为1,另一个张量长度大于1,那么在该维度上,就好像是对第一个张量进行了复制。
tf.broadcast_to 以显式的方式按照广播机制扩展张量的维度。
a = tf.constant([1,2,3])
b = tf.constant([[0,0,0],[1,1,1],[2,2,2]])
b + a #等价于 b + tf.broadcast_to(a,b.shape) # shape=(3, 3)
tf.broadcast_to(a,b.shape)
<tf.Tensor: shape=(3, 3), dtype=int32, numpy=
array([[1, 2, 3],
[1, 2, 3],
[1, 2, 3]], dtype=int32)>
#计算广播后计算结果的形状,静态形状,TensorShape类型参数
tf.broadcast_static_shape(a.shape,b.shape)
TensorShape([3, 3])
#计算广播后计算结果的形状,动态形状,Tensor类型参数
c = tf.constant([1,2,3])
d = tf.constant([[1],[2],[3]])
tf.broadcast_dynamic_shape(tf.shape(c),tf.shape(d))
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([3, 3], dtype=int32)>
#广播效果
c+d #等价于 tf.broadcast_to(c,[3,3]) + tf.broadcast_to(d,[3,3])
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[6.5760484, 7.8174157],
[6.8174157, 6.4239516]], dtype=float32)>
3、AutoGraph的使用规范
有三种计算图的构建方式:静态计算图,动态计算图,以及Autograph。
TensorFlow 2.0主要使用的是动态计算图和Autograph。
动态计算图易于调试,编码效率较高,但执行效率偏低。
静态计算图执行效率很高,但较难调试。
而Autograph机制可以将动态图转换成静态计算图,兼收执行效率和编码效率之利。
当然Autograph机制能够转换的代码并不是没有任何约束的,有一些编码规范需要遵循,否则可能会转换失败或者不符合预期。
我们将着重介绍Autograph的编码规范和Autograph转换成静态图的原理。
并介绍使用 tf.Module 来更好地构建Autograph。
编码规范:
-
1,被@tf.function修饰的函数应尽可能使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print,使用tf.range而不是range,使用tf.constant(True)而不是True.
-
2,避免在@tf.function修饰的函数内部定义tf.Variable.
-
3,被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。
示例:
1,被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。
import numpy as np
import tensorflow as tf
@tf.function
def np_random():
a = np.random.randn(3,3)
tf.print(a)
@tf.function
def tf_random():
a = tf.random.normal((3,3))
tf.print(a)
#np_random每次执行都是一样的结果。
np_random()
np_random()
#tf_random每次执行都会有重新生成随机数。
tf_random()
tf_random()
2,避免在@tf.function修饰的函数内部定义tf.Variable.
x = tf.Variable(1.0,dtype=tf.float32)
@tf.function
def outer_var():
x.assign_add(1.0)
tf.print(x)
return(x)
outer_var()
outer_var()
@tf.function
def inner_var():
x = tf.Variable(1.0,dtype = tf.float32)
x.assign_add(1.0)
tf.print(x)
return(x)
#执行将报错
#inner_var()
#inner_var()
3,被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等结构类型变量。
ensor_list = []
#@tf.function #加上这一行切换成Autograph结果将不符合预期!!!
def append_tensor(x):
tensor_list.append(x)
return tensor_list
append_tensor(tf.constant(5.0))
append_tensor(tf.constant(6.0))
print(tensor_list)
[<tf.Tensor: shape=(), dtype=float32, numpy=5.0>, <tf.Tensor: shape=(), dtype=float32, numpy=6.0>]
tensor_list = []
@tf.function #加上这一行切换成Autograph结果将不符合预期!!!
def append_tensor(x):
tensor_list.append(x)
return tensor_list
append_tensor(tf.constant(5.0))
append_tensor(tf.constant(6.0))
print(tensor_list)
[<tf.Tensor 'x:0' shape=() dtype=float32>]
4、AutoGraph的机制原理
当我们使用@tf.function装饰一个函数的时候,后面到底发生了什么呢?
例如我们写下如下代码。
import tensorflow as tf
import numpy as np
@tf.function(autograph=True)
def myadd(a,b):
for i in tf.range(3):
tf.print(i)
c = a+b
print("tracing") # 创建时打印
return c
后面什么都没有发生。仅仅是在Python堆栈中记录了这样一个函数的签名。
当我们第一次调用这个被@tf.function装饰的函数时,后面到底发生了什么?
c = myadd(tf.constant("hello"),tf.constant("world"))
tracing
0
1
2
发生了 2 件事情,
第一件事情是创建计算图。即创建一个静态计算图,跟踪执行一遍函数体中的Python代码,确定各个变量的Tensor类型,并根据执行顺序将算子添加到计算图中。 在这个过程中,如果开启了autograph=True(默认开启),会将Python控制流转换成TensorFlow图内控制流。 主要是将if语句转换成 tf.cond算子表达,将while和for循环语句转换成tf.while_loop算子表达,并在必要的时候添加 tf.control_dependencies指定执行顺序依赖关系。
第二件事情是执行计算图。即 session.run(c)
当我们再次用相同的输入参数类型调用这个被@tf.function装饰的函数时,后面到底发生了什么?
只会发生一件事情,那就是上面步骤的第二步,执行计算图。
当我们再次用不同的的输入参数类型调用这个被@tf.function装饰的函数时,后面到底发生了什么?
由于输入参数的类型已经发生变化,已经创建的计算图不能够再次使用。
需要重新做2件事情:创建新的计算图、执行计算图。
需要注意的是,如果调用被@tf.function装饰的函数时输入的参数不是Tensor类型,则每次都会重新创建计算图。
例如我们写下如下代码。两次都会重新创建计算图。因此,一般建议调用@tf.function时应传入Tensor类型。
myadd("hello","world")
myadd("good","morning")
tracing
0
1
2
tracing
0
1
2
了解了以上Autograph的机制原理,我们也就能够理解Autograph编码规范的3条建议了。
1,被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print.
解释:Python中的函数仅仅会在跟踪执行函数以创建静态图的阶段使用,普通Python函数是无法嵌入到静态计算图中的,所以 在计算图构建好之后再次调用的时候,这些Python函数并没有被计算,而TensorFlow中的函数则可以嵌入到计算图中。使用普通的Python函数会导致 被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。
2,避免在@tf.function修饰的函数内部定义tf.Variable.
解释:如果函数内部定义了tf.Variable,那么在【eager执行】时,这种创建tf.Variable的行为在每次函数调用时候都会发生。但是在【静态图执行】时,这种创建tf.Variable的行为只会发生在第一步跟踪Python代码逻辑创建计算图时,这会导致被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。实际上,TensorFlow在这种情况下一般会报错。
3,被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。
解释:静态计算图是被编译成C++代码在TensorFlow内核中执行的。Python中的列表和字典等数据结构变量是无法嵌入到计算图中,它们仅仅能够在创建计算图时被读取,在执行计算图时是无法修改Python中的列表或字典这样的数据结构变量的。
5、tf.Module
应用tf.Module封装Autograph:前面在介绍Autograph的编码规范时提到构建Autograph时应该避免在@tf.function修饰的函数内部定义tf.Variable. 但是如果在函数外部定义tf.Variable的话,又会显得这个函数有外部变量依赖,封装不够完美。
一种简单的思路是定义一个类,并将相关的tf.Variable创建放在类的初始化方法中。而将函数的逻辑放在其他方法中。
这样一顿猛如虎的操作之后,我们会觉得一切都如同人法地地法天天法道道法自然般的自然。
惊喜的是,TensorFlow提供了一个基类tf.Module,通过继承它构建子类,我们不仅可以获得以上的自然而然,而且可以非常方便地管理变量,还可以非常方便地管理它引用的其它Module,最重要的是,我们能够利用tf.saved_model保存模型并实现跨平台部署使用。
实际上,tf.keras.models.Model,tf.keras.layers.Layer 都是继承自tf.Module的,提供了方便的变量管理和所引用的子模块管理的功能。
因此,利用tf.Module提供的封装,再结合TensoFlow丰富的低阶API,实际上我们能够基于TensorFlow开发任意机器学习模型(而非仅仅是神经网络模型),并实现跨平台部署使用。
定义一个简单的function。
import tensorflow as tf
x = tf.Variable(1.0,dtype=tf.float32)
# 在tf.function中用input_signature限定输入张量的签名类型:shape和dtype
@tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)])
def add_print(a):
x.assign_add(a)
tf.print(x)
return(x)
add_print(tf.constant(3.0))
#add_print(tf.constant(3)) #输入不符合张量签名的参数将报错
下面利用tf.Module的子类化将其封装一下。
class DemoModule(tf.Module):
def __init__(self,init_value = tf.constant(0.0),name=None):
super(DemoModule, self).__init__(name=name)
with self.name_scope: #相当于with tf.name_scope("demo_module")
self.x = tf.Variable(init_value,dtype = tf.float32,trainable=True)
@tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)])
def addprint(self,a):
with self.name_scope:
self.x.assign_add(a)
tf.print(self.x)
return(self.x)
#执行
demo = DemoModule(init_value = tf.constant(1.0))
result = demo.addprint(tf.constant(5.0))
6
#查看模块中的全部变量和全部可训练变量
print(demo.variables)
print(demo.trainable_variables)
(<tf.Variable 'demo_module/Variable:0' shape=() dtype=float32, numpy=6.0>,)
(<tf.Variable 'demo_module/Variable:0' shape=() dtype=float32, numpy=6.0>,)
#查看模块中的全部子模块
demo.submodules
#使用tf.saved_model 保存模型,并指定需要跨平台部署的方法
tf.saved_model.save(demo,"./data/demo/1",signatures = {"serving_default":demo.addprint})
#加载模型
demo2 = tf.saved_model.load("./data/demo/1")
demo2.addprint(tf.constant(5.0))
11
# 查看模型文件相关信息,红框标出来的输出信息在模型部署和跨平台使用时有可能会用到
!saved_model_cli show --dir ./data/demo/1 --all
在tensorboard中查看计算图,模块会被添加模块名demo_module,方便层次化呈现计算图结构。
import datetime
# 创建日志
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
logdir = './data/demomodule/%s' % stamp
writer = tf.summary.create_file_writer(logdir)
#开启autograph跟踪
tf.summary.trace_on(graph=True, profiler=True)
#执行autograph
demo = DemoModule(init_value = tf.constant(0.0))
result = demo.addprint(tf.constant(5.0))
#将计算图信息写入日志
with writer.as_default():
tf.summary.trace_export(
name="demomodule",
step=0,
profiler_outdir=logdir)
#启动 tensorboard在jupyter中的魔法命令
%reload_ext tensorboard
from tensorboard import notebook
notebook.list()
notebook.start("--logdir ./data/demomodule/")
除了利用tf.Module的子类化实现封装,我们也可以通过给tf.Module添加属性的方法进行封装。
mymodule = tf.Module()
mymodule.x = tf.Variable(0.0)
@tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)])
def addprint(a):
mymodule.x.assign_add(a)
tf.print(mymodule.x)
return (mymodule.x)
mymodule.addprint = addprint
mymodule.addprint(tf.constant(1.0)).numpy()
1.0
print(mymodule.variables)
(<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>,)
#使用tf.saved_model 保存模型
tf.saved_model.save(mymodule,"./data/mymodule",signatures = {"serving_default":mymodule.addprint})
#加载模型
mymodule2 = tf.saved_model.load("./data/mymodule")
mymodule2.addprint(tf.constant(5.0))
INFO:tensorflow:Assets written to: ./data/mymodule/assets
5
tf.keras中的模型和层:都是继承tf.Module实现的,也具有变量管理和子模块管理功能。
import tensorflow as tf
from tensorflow.keras import models,layers,losses,metrics
print(issubclass(tf.keras.Model,tf.Module))
print(issubclass(tf.keras.layers.Layer,tf.Module))
print(issubclass(tf.keras.Model,tf.keras.layers.Layer))
True
True
True
tf.keras.backend.clear_session()
model = models.Sequential()
model.add(layers.Dense(4,input_shape = (10,)))
model.add(layers.Dense(2))
model.add(layers.Dense(1))
model.summary()
model.variables
model.layers[0].trainable = False #冻结第0层的变量,使其不可训练
model.trainable_variables
model.submodules
model.layers
print(model.name)
print(model.name_scope())