4. 梯度与自动微分

自动微分在神经网络的向后反馈等机器学习算法时常用到。

梯度计算

当需要反馈时,TensorFlow需要追踪操作是以什么顺序在执行,以便于进行自动微分。反馈过程中,TensorFlow倒序遍历操作以计算梯度。

Gradient tapes

TensorFlow提供了tf.GradientTape的API进行自动微分,通常是对tf.Variables求梯度。TensorFlow会记录下tf.GradientTape上下文中的相关计算,然后通过倒序求解梯度。当使用tf.GradientTape记录下操作之后,就可以使用GradientTape.gradient(target, sources)来计算目因变量(通常是一个loss值)对于自变量的导数(通常是一个变量)
下面是一个小例子:

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
    y = x**2

dy_dx = tape.gradient(y, x)
dy_dx.numpy()

结果为:

6.0

尽管上面的小例子使用了变量是一个标量,但是tf.GradientTape可以作用于任意张量上。

w = tf.Variable(tf.random.normal((3, 2), name='w'))
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')

x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
    y = x @ w + b
    loss = tf.reduce_mean(y ** 2)

上述代码中,为了同时求解loss对w,b,你可以将w和b同时传递给gradient函数。tape的使用十分灵活,你可以将多个变量组合为一个list或一个dict传递给gradient函数,那么gradient也会按照你的传递的格式进行返回。如下列代码所示:

[dl_dw, dl_db] = tape.gradient(loss, [w, b])
print(dl_dw.shape)
或
my_vars = {'w': w, 'b': b}
grad = tape.gradient(loss, my_vars)
print(grad['b'])

模型中的梯度计算

多数情况下,TensorFlow会收集tf.Module或其子类(tf.Model,tf.Layer)中的变量用于检查点设置和模型导出。你应该经常需要对模块(tf.Module)的变量(Moudle.trainable_variables)进行求导,这是几行代码就是能搞定的事情。

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
    y = layer(x)
    loss = tf.reduce_mean(y ** 2)

grad = tape.gradient(loss, layer.trainable_variables)

for var, g in zip(layer.trainable_variables, grad):
    print(f'{var.name}, shape:{g.shape}')

上述代码的运算结果如下,代码没有什么实际意义,只是高速你变量和梯度的shape是一样的而已。

dense/kernel:0, shape:(3, 2)
dense/bias:0, shape:(2,)

控制tape监控的范围

tape默认情况下会记录tf.Variable的所有的相关计算操作。之所以这么设计,原因是:

  • tape需要知道所有计算的顺序才能逆向求解微分。
  • tape需要记录所有的中间运行结果,因此省却了使用者的麻烦。
  • loss对所有的tf.Variable进行微分是用户最常用的操作。
    下列代码中,微分只会对x1生效:
x0 = tf.Variable(3.0, name='x0')
x1 = tf.Variable(3.0, name='x1', trainable=False)
x2 = tf.Variable(2.0, name='x2') + 1.0
x3 = tf.constant(3.0, name='x3')

with tf.GradientTape() as tape:
    y = x0 ** 2 + x1 ** 2 + x2 ** 2

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
    print(g)

运行结果为:

tf.Tensor(6.0, shape=(), dtype=float32)
None
None
None

注意到,x2实际上是一个张量,值为:

tf.Tensor(3.0, shape=(), dtype=float32)

tf.Gradient提供了让用户自由控制tape监控范围的功能。当需要对一个张量求微分时,你可以调用GradientTape.watch(x):

x = tf.constant(3.0)
with tf.GradientTape() as tape:
    tape.watch(x)
    y = x ** 2

dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())

结果为

6.0

tape有可以修改默认的监控所有变量的行为。使用wach_accessed_variables=False可以关闭tape的默认行为,转而通过watch来自定义设置需要监控的变量。如下的代码中运算中使用了x0和x1两个变量,而微分过程只对x1进行。

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
    tape.watch(x1)
    y0 = tf.math.sin(x0)
    y1 = tf.nn.softplus(x1)
    y = y0 + y1
    ys = tf.reduce_sum(y)

grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print("dy_dx0:", grad['x0'])
print("dy_dx1:", grad['x1'])

运行结果为:

dy_dx0: None
dy_dx1: tf.Tensor(0.9999546, shape=(), dtype=float32)

中间结果

你可以使用tf.GradientTape来对中间的计算变量进行求微分。默认情况下,GradientTape所拥有的资源会在GradientTape.gradient之后释放内存,因此为了多次调用gradient方法以多次求微分,就需要使用persist=True参数来保存tape的监控内容。这种情况下,tape会随着Python生命作用域的消失而释放内存。

控制流

tape只记录执行过的操作,因此若是tape的代码里面有if-else,则tape只记录执行过的分支。

梯度计算出None的几种可能

  1. 无意中将变量替换成了一个张量
    默认情况下,tape只会追踪监控tf.Variable,若是不经意间将tf.Variable变成了tf.Tensor,那么tape对其求微分便会是一个None值。因此应该使用Variable.assign方法来更新tf.Variable。
x = tf.Variable(2.0)

for epoch in range(2):
    with tf.GradientTape() as tape:
        y = x + 1
    print(type(x).__name__, ":", tape.gradient(y, x))
    x = x + 1

下面的例子中,x = x + 1使得tf.Variable变成了一个tf.Tensor。只需要保证在输入之前x是一个tf.Variable。

ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None
  1. 未使用TensorFlow提供的操作符进行计算
    TensorFlow之外的计算并不能被tape所监控。例如:
x = tf.Variable([[1., 2.], [3., 4.]], dtype=tf.float32)

with tf.GradientTape() as tape:
    x2 = x ** 2
    y = np.mean(x2, axis=0)
    y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))

结果为None,因为y=np.mean(x2, axis=0)这一行使用了numpy操作,不属于tensorFlow所提供的操作符。因此y与x之间没有了关联。

  1. 对整型或字符串求梯度
    TensorFlow不能对整型和字符串求微分。开发时,用户自然不会考虑对字符串求微分,但是很有可能不经意间忘记指定dtype而创建了一个整型的张量或变量。
x = tf.constant(10)

with tf.GradientTape() as g:
    g.watch(x)
    y = x * x
print(g.gradient(y, x))

结果为空。

  1. 修改了变量的状态
    状态的改变会导致梯度运算中断。当你需读取一个状态对象时,tape只能观察到当前的状态,而不会记录它的历史状态。因此,要注意在tape的记录过程中,不要修改变量的值。
    下面这个程序的运行结果为None。
x0 = tf.Variable(3.0)
x1 = tf.Variable(4.0)
with tf.GradientTape() as tape:
    x1.assign_add(x0)
    y = x1**2   

print(tape.gradient(y, x1))

再下面这个运行结果为14

x0 = tf.Variable(3.0)
x1 = tf.Variable(4.0)
with tf.GradientTape() as tape:
    x1 = x1 + x0
    y = x1**2

print(tape.gradient(y, x1))

下面程序的运行结果为:

x0 = tf.Variable(3.0)
x1 = tf.constant(4.0)
with tf.GradientTape() as tape:
    x1 = x1 + 1
    y = x1**2

print(tape.gradient(y, x1))

运行结果为:

None

有些操作是无法进行微分的

一些tf.Operation被注册为不可微分的,一旦计算了微分就会返回为None。还有一些连注册都没有,一旦试图对其求微分就会收到一个错误。tf.raw_ops页面记录了那些操作可以被微分。

将None替换为0

可以通过设置unconnnected_gradients将返回为None的操作的返回值重置为0。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容