變量更新和控制依賴

變量更新

到目前爲止,我們已經將變量專門用於我們模型中的一些權重,這些權重將根據優化器的操作進行更新操作(如:Adam)。但是優化器並不是更新變量的唯一方法,還有別的一整套更高級的函數可以完成這個操作(你將再次看到,這些更高級的函數將作爲一種操作添加到你的圖中)。

最基本的自定義更新操作是 tf.assign() 操作。這個函數需要一個變量和一個值,並將值分配給這個變量,非常簡單吧。

讓我們來看一個例子:


import tensorflow as tf

# We define a Variable
x = tf.Variable(0, dtype=tf.int32)

# We use a simple assign operation
assign_op = tf.assign(x, x + 1)

with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())

  for i in range(5):
    print('x:', sess.run(x))
    sess.run(assign_op)

# outputs:
# x: 0
# x: 1
# x: 2
# x: 3
# x: 4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

這裏沒有什麼特別地,就跟任何其他操作一樣:你能在會話(session)中調用它,並且操作確保會發生變量更新。

我們將這個操作(assign)跟通常的優化器 train_op 進行比較。兩者都做同樣的事情:變量更新。唯一的區別是,優化器在進行變量更新之前,需要做大量的微積分操作。

TF 有許多的函數來支持手動更新變量,你可以在 TensorFlow 的函數幫助頁面進行查看,很多的操作都可以被一些張量操作來取代,然後調用 tf.assign 函數來實現更新操作,但在一些情況下,這將會是非常麻煩的一件事。所以,TensorFlow 爲我們提供了兩種更新操作:

我不會深挖這些函數的功能。其中一些函數可能你現在不是很理解,我的建議是你可以通過一個很簡單的腳本來學習這些函數,然後再寫入你的實際模型中,這種方法會幫助你節約很多的調試時間。

最後再談一下參數更新:如果我們想改變參數的維度呢?例如,在參數中多添加一行或者一列?到目前爲止,我們一直在談論 “assign” 這個概念,並沒有涉及到維度的改變。

這個問題是可以被解決的,但是比較棘手:

  • tf.Variable 函數中有一個參數 validate_shape 默認是設置爲 True 。它阻止你對參數進行維度更新,所以我們必須將這個參數設置爲 False 。

  • 這個參數也存在於 tf.assign 函數中,所以我們也必須將這個參數進行關閉。

讓我們看個例子:


import tensorflow as tf

# We define a "shape-able" Variable
x = tf.Variable(
    [], # A list of scalar
    dtype=tf.int32,
    validate_shape=False, # By "shape-able", i mean we don't validate the shape so we can change it
    trainable=False
)
# I build a new shape and assign it to x
concat = tf.concat([x, [0]], 0)
assign_op = tf.assign(x, concat, validate_shape=False) # We force TF, to skip the shape validation step

with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())

  for i in range(5):
    print('x:', sess.run(x), 'shape:', sess.run(tf.shape(x)))
    sess.run(assign_op)

# outputs:
# x: [] shape: [0]
# x: [0] shape: [1]
# x: [0 0] shape: [2]
# x: [0 0 0] shape: [3]
# x: [0 0 0 0] shape: [4]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

所以這也不是很難,對吧!讓我們繼續吧。

控制依賴

我們可以更新變量,但是如果你要在更新當前變量之前更新別的變量,那麼這會造成一個嚴重問題:你需要調用很多次的 sess.run 來滿足這個需求。這非常不實用,也沒有效率。請記住,我們將參數留在圖中更多,那麼效率會更高。

那麼有什麼辦法嗎?當然有,那就是控制依賴。TF 提供了一組的函數來處理不完全依賴情況下的操作排序問題(就是哪個操作先執行的問題)。

讓我們從最簡單的例子開始:我們先構造一個擁有一個變量(Variable)和一個佔位符(placeholder)的圖,用來執行一個乘法操作。在每次進行乘法之前,我們需要對參數(Variable)進行更新操作,每次加一。那麼,我們在實際的編程中怎麼做到這一點呢?

如果我們開始天真的方式,只需要添加一個 tf.assign 調用就可以了,那麼我們將得到如下結果:

import tensorflow as tf

# We define our Variables and placeholders
x = tf.placeholder(tf.int32, shape=[], name='x')
y = tf.Variable(2, dtype=tf.int32)

# We set our assign op
assign_op = tf.assign(y, y + 1)

# We build our multiplication (this could be a more complicated graph)
out = x * y

with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())

  for i in range(3):
    print('output:', sess.run(out, feed_dict={x: 1}))

# outputs:
# output: 2
# output: 2
# output: 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

從結果中我們可以看出,這種操作方式並不 work :我們的變量(Variable)並沒有增長,輸出結果一直都是 2 。

如果你仔細查看上面的代碼,並且在腦中構建這個圖,你就可以清楚的看到,如果要計算 x 和 y 之間的乘法,該圖不需要計算 assign_op:因爲如何對 y 進行更新操作,已經擁有了很好的定義。

爲了解決這個問題,使得 y 能進行更新,我們需要一種方法來強制 TF 運行 assign_op 操作。

這種操作確實是存在的!我們可以添加一個控制依賴來做這件事。這樣就像 Graph 或者 Variables 一樣,我們能將它和 Python 語句一起使用。

讓我們來看一個例子:

import tensorflow as tf

# We define our Variables and placeholders
x = tf.placeholder(tf.int32, shape=[], name='x')
y = tf.Variable(2, dtype=tf.int32)

# We set our assign op
assign_op = tf.assign(y, y + 1)

# We build our multiplication, but this time, inside a control depedency scheme!
with tf.control_dependencies([assign_op]):
    # Now, we are under the dependency scope:
    # All the operations happening here will only happens after 
    # the "assign_op" has been computed first
    out = x * y

with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())

  for i in range(3):
    print('output:', sess.run(out, feed_dict={x: 1}))

# outputs:
# output: 3
# output: 4
# output: 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

一切都按照我們的想法進行工作了。TF 看到了我們設置的依賴關係,所以它在運行依賴關係裏面的操作之前,它會運行 assign_op ,這裏有一個可視化結果:

  • 在上圖,圖並不會去計算 assign_op 。

  • 在下圖,控制依賴在計算乘法之前會強制圖去計算 assign_op 。

一個陷阱

在前面我們討論瞭如何去改變變量的維度。但是有一些地方需要注意,當我們使用控制依賴去改變變量維度時,那麼我們進入了一個黑盒優化層面。

比如,你可以先查看一下這段代碼:

import tensorflow as tf

# I define a "shape-able" Variable
x = tf.Variable(
    [], 
    dtype=tf.int32,
    validate_shape=False, # By "shape-able", i mean we don't validate the shape
    trainable=False
)
# I build a new shape and assign it to x
concat = tf.concat([x, [0]], 0)
assign_op = tf.assign(x, concat, validate_shape=False)

with tf.control_dependencies([assign_op]):
    # I print x after the assignment 
 # print_op_dep = tf.Print(x, data=[x], message="print_op_dep:")   
new_x = x.read_value()
print_op_dep = tf.Print(new_x, data=[new_x], message="print_op_dep:")
# The assign_op is called, but it seems that print statement happens # before the assignment, that is wrong.with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for i in range(3): sess.run(print_op_dep)# outputs:# x: [] , x_read: [0]# x: [0] , x_read: [0 0]# x: [0 0], x_read: [0 0 0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

讓我們仔細看看這段代碼:

  • 打印操作依賴於 assign_op ,它只能在 x 被更新之後計算。

  • 然而,當我們打印 x 的時候,它看起來好像沒有更新。

  • 但實際上,由於我們可以使用特殊的 read_value 函數來獲取 x 的真正值。

原因解釋:

 If you'd done tf.assign_add(x, x + 1) (or something else that preserved the shape of x) you would see things happen in the order you expected, because everything happens on the same device, and the "snapshot" remains an alias of the underlying buffer.) The tf.Print() op gets the old snapshot value, and prints that.

How can you avoid this? One way is to force an explicit x.read_value(), which forces a new snapshot to be taken, respecting the control dependencies.


結束語

那麼,我們怎麼來使用這些新的性能呢?其中一點我想到的是,維度變化這個功能可以用在 NLP 問題中的句子長度不一問題,如果你在處理詞向量問題時,遇到句子之間的長度不同,那麼你不需要添加 <UNK> 之類的標誌,直接改變維度就可以了。

注意:我不確定這個想法是否能產生好的效果,如果你做了實驗,那麼我很想聽到實驗結果,感謝!


Reference:

http://stackoverflow.com/questions/38994037/tensorflow-while-loop-for-training

https://github.com/tensorflow/tensorflow/issues/7782


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章