【详细整理】tensorflow2.0自动求导(Autograd)教程(一)

【详细整理】tensorflow2.0自动求导(Autograd)教程(一)_第1张图片

【详细整理】tensorflow2.0自动求导(Autograd)教程(一)_第2张图片

前言:前面分专题专门讲解了pytorch的自动求导功能,tensorflow其实也是具有相似的能力的,只不过可能相对的文章相对较少,本文以tensorflow2.0.0为例,来加以说明,比较说明了tensorflow和pytorch的自动求导的异同点

一、先从一个例子看起

由于对标量求导是最简单的,这里就不多说了,直接从张量开始,看下面的例子:

import tensorflow as tf

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])

with tf.GradientTape() as g:
    g.watch(x)
    with tf.GradientTape() as gg:
        gg.watch(x)
        y = x * x
    dy_dx = gg.gradient(y, x)     # 求一阶导数
d2y_dx2 = g.gradient(dy_dx, x)    # 求二阶导数

print(y)
print(dy_dx)
print(d2y_dx2)
'''运行结果为:
tf.Tensor(
[[ 1.  4.  9.]
 [16. 25. 36.]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[ 2.  4.  6.]
 [ 8. 10. 12.]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[2. 2. 2.]
 [2. 2. 2.]], shape=(2, 3), dtype=float32)
'''

不要感觉到很疑惑,这里面涉及到的方法和实现我们都会在后面说道,tensorflow里面的自动求导功能都是通过一个关键的类来实现的,即GradientTape。

二、求导的相关方法解析

2.1 tf.GradientTape()

先看一下它的原型:

class GradientTape(object):

    def __init__(self, persistent=False, watch_accessed_variables=True):
        #这是构造函数,由两个参数,后面会讲它的含义

    #实现了这两个方法说明它是支持with上下文管理器的
    def __enter__(self):

    def __exit__(self, typ, value, traceback):

    def watch(self, tensor):

    def stop_recording(self):

    def reset(self):

    def watched_variables(self):

    def gradient(self,
               target,
               sources,
               output_gradients=None,
               unconnected_gradients=UnconnectedGradients.NONE):


    def jacobian(self,
               target,
               sources,
               unconnected_gradients=UnconnectedGradients.NONE,
               parallel_iterations=None,
               experimental_use_pfor=True):
   
   #后面的一系列方法会在后面说道

为什么称之为GradientTape?

        下面是个人理解,Gradient是梯度的意思这没什么可说的,那为什么还要加一个Tape,Tape的本意是磁带,它是负责记录一些数据,一些信息的,所以这里的意思应该是记录函数在求导过程中的一些梯度数据的这样一个类。

2.2 一般自动求导的三步走

(1)创建一个GradientTape对象:g=tf.GradientTape()

(2)监视watch要求导的变量:g.watch(x)

(3)对函数进行求导:g.gredient(y,x)

下面实现一个稍微复杂点的:

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    y=tf.pow(x,2)   #需要走注意的是函数y,z一定要在GradientTape上下文的里面定义,否则会得不到结果,为None
    #g.watch(y)  #可省略
    z=tf.sqrt(y+1)

    dy_dx = g.gradient(y, x)     # y对x求导
    dz_dx = g.gradient(z, x)     # z对x求导
    dz_dy = g.gradient(z, y)     # z对y求导

print(dy_dx)
print(dz_dx)
print(dz_dy)

'''运行结果为:
tf.Tensor(
[[ 2.  4.  6.]
 [ 8. 10. 12.]], shape=(2, 3), dtype=float32)

tf.Tensor(
[[0.70710677 0.8944272  0.94868326]
 [0.97014254 0.9805807  0.9863939 ]], shape=(2, 3), dtype=float32)

tf.Tensor(
[[0.35355338 0.2236068  0.15811388]
 [0.12126782 0.09805807 0.08219949]], shape=(2, 3), dtype=float32)
'''

总结:相较于pytorch的求导,个人感觉tensorflow的求导功能更简单,

第一:前面的文章中说道,pytorch只能够默认标量对标量,标量对向量或矩阵求导,如果使用向量对向量求导,则需要传入关键参数gradient

第二:pytorch中只能够对叶子节点(leaf variable)求导,对中间变量的求导没办法直接显示,在上面中z对y求导是对中间变量求导,pytorch会返回none,而tensorflow会完整的显示出求导结果。

        当然他们也有相似的地方,比如他们默认都是求导一次完成后会销毁计算图,不能再求导第二次,除非明确说明求导之后不要销毁计算图,比如pytorch中在backward里面使用retain_graph关键字参数,而tensorflow里面在GradientTape里面使用persistent参数,来保证计算图不被销毁。

2.3 GradientTape关键方法解析

(1)GradientTape类及构造函数的参数

这个类是专门用于求导的类,它作为with上下文使用,可以对那些被watch“监视”的变量。即我们上面使用的watch方法,需要注意的是,tensorflow中默认的可以训练的张量(即trainable)是默认被监视watch的,即通过 `tf.Variable` 和 `tf.get_variable`创建的可以trainable的变量是默认watch的,我就不需要再使用watch了,当然我依然也可以手动watch这些变量,然后对他们求导。

另外GradientTape的构造函数有两个参数:

persistent参数:bool类型,决定是否在求导之后图就被销毁了,如果设置为True,则可以进行二次求导,默认为False。

watch_accessed_variables参数:bool类型,默认为True,它的作用是控制那些可以trainable的变量是否会被自动监视watch,默认情况下为true,即我可以不使用watch方法,那些trainable变量也会自动被watch,如果设置为False,则所有的要求导的变量都必须手动使用watch监视,否则会报错。

(2)watch函数

该函数监视要求导的变量,可以是一个tensor,也可以是多个参数组成的列表,如下所示:

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])
y=tf.Variable(initial_value=[[2.0,4.0,6.0],[8.0,10.0,12.0]])

with tf.GradientTape(persistent=True) as g:
    g.watch([x,y])   #同时监控多个变量
    z=tf.pow(x,2)+tf.pow(y,2)  #构造多元函数 z=f(x,y)
   
    dz_dx = g.gradient(z, x) 
    dz_dy = g.gradient(z, y) 

print(dz_dx)
print(dz_dy)
'''运行结果为:
tf.Tensor(
[[ 2.  4.  6.]
 [ 8. 10. 12.]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[ 4.  8. 12.]
 [16. 20. 24.]], shape=(2, 3), dtype=float32)
'''

(3)stop_recording函数

这个函数理解的不是特别清楚,如果有哪位大佬有比较深入的认识,望分享,将感激不尽。

(4)reset()函数

前面说了求导的过程会存储求导过程中的相关数据,该函数会清除Tape里面的所有信息

即 Clears all information stored in this tape

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])
y=tf.Variable(initial_value=[[2.0,4.0,6.0],[8.0,10.0,12.0]])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    z=tf.pow(x,2)
with tf.GradientTape(persistent=True) as g:
    g.watch(y)
    z+=tf.pow(y,2)

dz_dx = g.gradient(z, x) 
dz_dy = g.gradient(z, y) 

print(dz_dx)
print(dz_dy)
'''运行结果为:
None     # 对x的导数是没有的,返回None。因为第一个Tape的信息没有了
tf.Tensor(
[[ 4.  8. 12.]
 [16. 20. 24.]], shape=(2, 3), dtype=float32)
'''

等价于下面的代码:

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])
y=tf.Variable(initial_value=[[2.0,4.0,6.0],[8.0,10.0,12.0]])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    z=tf.pow(x,2)
    g.reset()   #清除前面的Tape里面已经存储的信息
    g.watch(y)
    z+=tf.pow(y,2)

dz_dx = g.gradient(z, x) 
dz_dy = g.gradient(z, y) 

print(dz_dx)
print(dz_dy)

(5)watched_variables函数

返回被这个Tape所监视追踪的变量,方便重构

x=tf.Variable(initial_value=[[1.0,2.0,3.0],[4.0,5.0,6.0]])
y=tf.Variable(initial_value=[[2.0,4.0,6.0],[8.0,10.0,12.0]])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    g.watch(y)
    z=tf.pow(x,2)+tf.pow(y,2)

dz_dx = g.gradient(z, x) 
dz_dy = g.gradient(z, y) 

variable_list=g.watched_variables()
print(variable_list)
'''返回结果是:
(, 
 )
'''

可以看出,本例子中监视两个变量x,y都被返回了,这方便再次使用x、y,便于重构。

(6)gradient函数

这个函数是实现求导的核心函数,前面的所有例子中均有使用。函数原型如下:

def gradient(self,target,sources,output_gradients=None,unconnected_gradients=UnconnectedGradients.NONE)

target参数:要求导的函数y,可以理解为要求导的因变量y,可以是一个tensor,也可以是几个tensor组成的集合;

sources参数:要对哪一个自变量x求导,可以是一个,也可以是几个tensor组成的集合;

output_gradients参数:这个参数默认是None,它的含义其实跟pytorch里面的那个backward函数里面的gradient参数一样,它会对应于所求得函数y的每一个元素,施加不同的权重,默认情况所有的权重是一样的。看一个例子就知道了:

x=tf.Variable(initial_value=[1.0,2.0,3.0])
y=tf.Variable(initial_value=[2.0,4.0,6.0])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    g.watch(y)
    z=tf.pow(x,2)+tf.pow(y,2)

output_gradients=tf.Variable([0.2,0.6,0.2])
dz = g.gradient(z, [x,y],output_gradients=output_gradients)   #给每一个元素施加不同的权重
print(dz)
'''运行结果为:
[, 
 ]
'''
'''
本来的结果是dz_dx=[2,4,6], 分别乘以权重[0.2,0.6,0.2]之后,得到[0.4,2.4,1.2]
本来的结果是dz_dy=[4,8,12],分别乘以权重[0.2,0.6,0.2]之后,得到[0.8,4.8,2.4]
'''

unconnected_gradients参数:

它是一个可选参数,有两个值,“none”和“zero”,none是它的默认值,表示当我们的target(因变量y)与sources(自变量x)之间在graph上没有办法联通的时候,会返回none,如下:

x=tf.Variable(initial_value=[1.0,2.0,3.0])
y=tf.Variable(initial_value=[2.0,4.0,6.0])

with tf.GradientTape(persistent=True) as g:
    g.watch(x)
    g.watch(y)
    z=tf.pow(y,2)  # z与x根本就没有关系,即在graph上面没有连接

dz = g.gradient(z, [x,y],unconnected_gradients="none")  # 默认就是none
print(dz)
'''返回值为:
[None, 
]
'''
'''
由上面可以看出,由于z与x之间并没有连通,所以返回的是None
'''

(7) batch_jacobian函数

这个函数是专门用来求雅克比矩阵的,参数就不再多说,什么是雅克比矩阵呢?这里就不再赘述了。

with tf.GradientTape() as g:
    x = tf.constant([[1, 2], [3, 4]], dtype=tf.float32)
    g.watch(x)
    y = x * x
batch_jacobian = g.batch_jacobian(y, x)
print(batch_jacobian)
'''运行结果为:
tf.Tensor(
[[[2. 0.]
  [0. 4.]]

 [[6. 0.]
  [0. 8.]]], shape=(2, 2, 2), dtype=float32)
'''

 

你可能感兴趣的:(TensorFlow)