天天看點

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

[論文閱讀] RNN 在阿裡DIEN中的應用

0x00 摘要

本文基于阿裡推薦DIEN代碼,梳理了下RNN一些概念,以及TensorFlow中的部分源碼。本部落格旨在幫助小夥伴們詳細了解每一步驟以及為什麼要這樣做。

0x01 背景知識

1.1 RNN

RNN,循環神經網絡,Recurrent Neural Networks。

人們思考問題往往不是從零開始的,比如閱讀時我們對每個詞的了解都會依賴于前面看到的一些資訊,而不是把前面看的内容全部抛棄再去了解某處的資訊。應用到深度學習上面,如果我們想要學習去了解一些依賴上文的資訊,RNN 便可以做到,它有一個循環的操作,可以使其可以保留之前學習到的内容。

最普通的RNN定義方式是:

  • U,W 是網絡參數(權重矩陣),b 是偏置參數,這些參數通過後向傳播訓練網絡學習得到。
  • act 是激活函數,通常選擇 sigmoid 或 tanh 函數。

1.2 DIEN項目代碼

在DIEN項目中,把TensorFlow的rnn代碼拿到自己項目中,做了一些修改,具體是:

  • 使用了 GRUCell;
  • 自定義了 VecAttGRUCell;
  • 因為修改了VecAttGRUCell接口,是以修改了rnn.py;

0x02 Cell

RNN的基本單元被稱為Cell,别小看那一個小小的cell,它并不是隻有1個neuron unit,而是n個hidden units。

是以,我們注意到tensorflow中定義一個cell(BasicRNNCell/BasicLSTMCell/GRUCell/RNNCell/LSTMCell)結構的時候需要提供的一個參數就是hidden_units_size。

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

在實際的神經網絡中,各個門處理函數 其實是由一定數量的隐含層神經元來處理。

在RNN中,M個神經元組成的隐含層,實際的功能應該是 f(wx + b), 這裡實作了兩步:

  • 首先M個隐含層神經元與輸入向量X之間全連接配接,通過w參數矩陣對x向量進行權重求和;
  • 其次就是對x向量各個次元上進行篩選,加上bias偏置矩陣後,通過f激勵函數, 得到隐含層的輸出;

在LSTM Cell中,一個cell 包含了若幹個門處理函數,假如每個門的實體實作,我們都可以看做是由num_hidden個神經元來實作該門函數功能, 那麼每個門各自都包含了相應的w參數矩陣以及bias偏置矩陣參數,這就是在上圖中的實作。

從圖中可以看出,cell單元裡有四個門,每個門都對應128個隐含層神經元,相當于四個隐含層,每個隐含層各自與輸入x 全連接配接,而輸入x向量是由兩部分組成,一部分是上一時刻cell 輸出,大小為128, 還有部分就是目前樣本向量的輸入,大小為6,是以通過該cell内部計算後,最終得到目前時刻的輸出,大小為128,即num_hidden,作為下一時刻cell的一部分輸入。

下面我們結合TensorFlow來具體剖析下Cell的實作機制和原理。

2.1 RNNCell(抽象父類)

2.1.1 基礎

“RNNCell”,它是TensorFlow中實作RNN的基本單元,每個RNNCell都有一個call方法,使用方式是:(output, next_state) = call(input, state)。

RNNCell是一個抽象的父類,其他的RNNcell都會繼承該方法,然後具體實作其中的call()函數。

RNNCell是包含一個State(狀态)并且能夠執行一些處理輸入矩陣的對象。RNNCell将輸入的矩陣(Input Matrix)運算輸出一個包含”self.output”列的輸出矩陣(Ouput Matrix)。

state: state就是rnn網絡中rnn cell的狀态,比如說如果你的rnn定義包含了N個單元(也就是你的self.state_size是個整數N),那麼在你每次執行RNN網絡時就應該給一個 [batch_size,self.state_size] 形狀的2D Tensor來表示目前RNN網絡的狀态,而如果你的 self.state_size 是一個元祖,那麼給定的狀态也應該是一個Tuple,每個Tuple裡的狀态表示和之前的方式一樣。

  • 如果定義了 “self.state_size”這個屬性,并且取值為一個整數,那麼RNNCell則會同時輸出一個狀态矩陣(State Matrix),包含 “self.state_size” 列。
  • 如果 “self.state_size” 定義為一個整數的Tuple,那麼則是輸出對應長度的狀态矩陣的Tuple,Tuple中的每一個狀态矩陣長度還是和之前的一樣,包含 “self.state_size” 列。

RNNCell其主要是zero_state()和call()兩個函數。

  • zero_state 用于初始化初始狀态 h0 為全零向量。
  • call 定義實際的RNNCell的操作(比如RNN就是一個激活,GRU的兩個門,LSTM的三個門控等,不同的RNN的差別主要展現在這個函數)。

除了call方法外,對于RNNCell,還有兩個類屬性比較重要,其中 state_size() 和 output_size() 方法設定為類屬性,可以當做屬性來調用(這裡用到的是Python内置的@property裝飾器,就是負責把一個方法變成屬性調用的,很像C#中的屬性、字段的那種概念):

  • state_size,是隐層的大小(代表 Cell 的狀态 state 大小)
  • output_size,是輸出的大小(輸出次元)

比如我們通常是将一個batch送入模型計算,設輸入資料的形狀為(batch_size, input_size),那麼計算時得到的隐層狀态就是(batch_size, state_size),輸出就是(batch_size, output_size)。

但這裡兩個方法都沒有實作,意思是說我們必須要實作一個子類繼承 RNNCell 類并實作這兩個方法。

class RNNCell(base_layer.Layer):

  def __call__(self, inputs, state, scope=None):
    if scope is not None:
      with vs.variable_scope(scope,
                             custom_getter=self._rnn_get_variable) as scope:
        return super(RNNCell, self).__call__(inputs, state, scope=scope)
    else:
      with vs.variable_scope(vs.get_variable_scope(),
                             custom_getter=self._rnn_get_variable):
        return super(RNNCell, self).__call__(inputs, state)

  def _rnn_get_variable(self, getter, *args, **kwargs):
    variable = getter(*args, **kwargs)
    if context.in_graph_mode():
      trainable = (variable in tf_variables.trainable_variables() or
                   (isinstance(variable, tf_variables.PartitionedVariable) and
                    list(variable)[0] in tf_variables.trainable_variables()))
    else:
      trainable = variable._trainable  # pylint: disable=protected-access
    if trainable and variable not in self._trainable_weights:
      self._trainable_weights.append(variable)
    elif not trainable and variable not in self._non_trainable_weights:
      self._non_trainable_weights.append(variable)
    return variable

  @property
  def state_size(self):
    raise NotImplementedError("Abstract method")

  @property
  def output_size(self):
    raise NotImplementedError("Abstract method")

  def build(self, _):
    pass

  def zero_state(self, batch_size, dtype):
    with ops.name_scope(type(self).__name__ + "ZeroState", values=[batch_size]):
      state_size = self.state_size
      return _zero_state_tensors(state_size, batch_size, dtype)
           

2.1.2 call

每個派生的RNNCell必須有以下的屬性并實作具有如下函數簽名的函數(output, next_state) = call(input, state)。 可選的第三個輸入參數 ‘scope’,用于向下相容,給子類定制化使用。scope傳入的值是tf.Variable類型,用于更友善的管理變量。

從給定的state開始運作,根據rnn cell的輸入

args:

inputs:是一個具有二維的張量shape為[batch_size, input_size]

states:如果

self.state_size

是一個整數,state就應該是一個二維張量 shape是

[batch_size, self.state_size]

,否則,如果

self.state_size

是一個整數的tuple(例如LSTM需要計算cell state和 hidden unit state ,就是一個tuple),那麼state就應該是

[batch_size, s] for s in self.state_size

形狀的tuple。

Scope:由其他子類建立的變量。

Return:

是一對,包括:

輸出:

[batch_size, self.output_size]

State: 和state相比對的shape

每調用一次RNNCell的call方法,就相當于在時間上“推進了一步”,這就是RNNCell的基本功能。

2.2 BasicRNNCell(基礎類)

2.2.1 基礎

RNNCell隻是一個抽象類,我們用的時候都是用的它的兩個子類 BasicRNNCell 和 BasicLSTMCell。顧名思義,前者是RNN的基礎類,後者是LSTM的基礎類。

BasicRNNCell 就是我們常說的 RNN。

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

最簡單的RNN結構如上圖所示。其代碼如下:

class BasicRNNCell(RNNCell):
  def __init__(self, num_units, activation=None, reuse=None):
    super(BasicRNNCell, self).__init__(_reuse=reuse)
    self._num_units = num_units
    self._activation = activation or math_ops.tanh
    self._linear = None

  @property
  def state_size(self):
    return self._num_units

  @property
  def output_size(self):
    return self._num_units

  def call(self, inputs, state):
    """Most basic RNN: output = new_state = act(W * input + U * state + B)."""
    if self._linear is None:
      self._linear = _Linear([inputs, state], self._num_units, True)

    output = self._activation(self._linear([inputs, state]))
    # output = Ht = tanh([x,Ht-1]*W + B)
    # 一個output作為下一時刻的輸入Ht,另一個作為下一層的輸入 Ht
    return output, output
           

2.3.2 參數意義

可以看到在初始化

__init__

中有若幹參數。

__init__

最重要的一個參數是 num_units,意思就是這個 Cell 中神經元的個數,另外還有一個參數 activation 即預設使用的激活函數,預設使用的 tanh,reuse 代表該 Cell 是否可以被重新使用。

我們知道一個最基本的RNN單元中有三個可訓練的參數W, U, B,以及兩個輸入變量。是以我們在構造RNN的時候就需要指定各個參數的次元了。

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

注,上圖中的

n

表示的是輸入的次元dim

從源碼中可以看出BasicRNNCell中:

  • state_size 就是num_units :

    def state_size(self): return self._num_units

  • output_size 就是num_units :

    def output_size(self): return self._num_units

  • 即 把state_size和output_size定義成相同,
  • ht和output也是相同的(call函數輸出是兩個output :

    return output, output

    ,即其并未定義輸出部分)。
  • 從 _linear 可以看出,

    output_size

    指的是偏置B的次元(下文會講解 _Linear)。

2.2.3 功能

其主要功能實作就是call函數的第一行注釋,就是input和前一時刻狀态state經過一個線性函數在經過一個激活函數即可,也就是最普通的RNN定義方式。也就是說

在 state_size()、output_size() 方法裡,其傳回的内容都是 num_units,即神經元的個數。

接下來 call() 方法中:

  • 傳入的參數為 inputs 和 state,即輸入的 x 和 上一次的隐含狀态
  • 首先執行個體化了一個 _Linear 類,這個類實際上就是做線性變換的類,将二者傳遞過來,然後直接調用,就實作了 w * [inputs, state] + b 的線性變換 :

    output = new_state = tanh(W * input + U * state + B).

  • 其次回到 BasicRNNCell 的 call() 方法中,在 _linear() 方法外面又包括了一層 _activation() 方法,即對線性變換應用一次 tanh 激活函數處理,作為輸出結果。
  • 最後傳回的結果是 output 和 output,第一個代表 output,第二個代表隐狀态,其值也等于 output。

2.2.4 Linear

上面寫到使用了 _linear 類,現在我們就介紹下。

這個類傳遞了 [inputs, state] 作為 call() 方法的 args,會執行 concat() 和 matmul() 方法,然後接着再執行 bias_add() 方法,這樣就實作了線性變換。

而output_size是輸出層的大小,我們可以看到

  • BasicRNNCell中,output_size就是_num_units;
  • GRUCell中是2 * _num_units;
  • BasicLSTMCell中是4 * _num_units;

這是因為_linear中執行的是RNN中的幾個等式的

Wx + Uh + B

的功能,但是不同的RNN中數量不同,比如LSTM中需要計算四次,然後直接把output_size定義為4_num_units,再把輸出進行拆分成四個變量即可。

下面是源碼縮減版

class _Linear(object):

  def __init__(self, args, output_size, build_bias, bias_initializer=None,
               kernel_initializer=None):
    
    self._build_bias = build_bias
    
    if not nest.is_sequence(args):
      args = [args]
      self._is_sequence = False
    else:
      self._is_sequence = True

    # Calculate the total size of arguments on dimension 1.
    total_arg_size = 0
    shapes = [a.get_shape() for a in args]
    for shape in shapes:
	    total_arg_size += shape[1].value

    dtype = [a.dtype for a in args][0]

  # 循環該函數 num_step(句子長度) 次,則該層計算完;
  def __call__(self, args):

    # 如果是第 0 時刻,那麼目前的 state(即上一時刻的輸出H0)的值全部為0;
    # input 的 shape為: [batch_size, emb_size]
    # state 的 shape為:[batch_zize, Hidden_size]
    # matmul : 矩陣相乘
    # array_ops.concat: 兩個矩陣連接配接,連接配接後的 shape 為 [batch_size,input_size + Hidden_size],實際就是[Xt,Ht-1]    
    
    if not self._is_sequence:
      args = [args]

    if len(args) == 1:
      res = math_ops.matmul(args[0], self._weights)
    else:
      # 此時計算: [input,state] * [W,U] == [Xt,Ht-1] * W,得到的shape為:[batch_size,Hidden_size] 
      res = math_ops.matmul(array_ops.concat(args, 1), self._weights)
    
    # B 的shape 為:【Hidden_size】
    # [Xt,Ht-1] * W 計算後的shape為:[batch_size,Hidden_size]
    # nn_ops.bias_add,這個函數的計算方法是,讓每個 batch 得到的值,都加上這個 B
	  # 加上B後:Ht = tanh([Xt, Ht-1] * W + B),得到的 shape 還是: [batch_size,Hidden_size]
	  # 那麼這個 Ht 将作為下一時刻的輸入和下一層的輸入;    
    if self._build_bias:
      res = nn_ops.bias_add(res, self._biases)
    return res
           

2.3 GRUCell

GRU,Gated Recurrent Unit。在 GRU 中,隻有兩個門:重置門(Reset Gate)和更新門(Update Gate)。同時在這個結構中,把 Ct 和隐藏狀态進行了合并,整體結構比标準的 LSTM 結構要簡單,而且這個結構後來也非常流行。

接下來我們看一下GRU的定義,相比BasicRNNCell隻改變了call函數部分,增加了重置門和更新門兩部分,分别由r和u表示。然後c表示要更新的狀态值。其對應的圖及公式如下所示:

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用
r = f(W1 * input + U1 * state + B1)
u = f(W2 * input + U2 * state + B2)
c = f(W3 * input + U3 * r * state + B3)
h_new = u * h + (1 - u) * c
           

GRUCell的實作代碼縮減版如下:

class GRUCell(RNNCell):
    
  def __init__(self,
               num_units,
               activation=None,
               reuse=None,
               kernel_initializer=None,
               bias_initializer=None):
    super(GRUCell, self).__init__(_reuse=reuse)
    self._num_units = num_units
    self._activation = activation or math_ops.tanh
    self._kernel_initializer = kernel_initializer
    self._bias_initializer = bias_initializer
    self._gate_linear = None
    self._candidate_linear = None

  @property
  def state_size(self):
    return self._num_units

  @property
  def output_size(self):
    return self._num_units

  def call(self, inputs, state):

    value = math_ops.sigmoid(self._gate_linear([inputs, state]))
    r, u = array_ops.split(value=value, num_or_size_splits=2, axis=1)

    r_state = r * state
    if self._candidate_linear is None:
      with vs.variable_scope("candidate"):
        self._candidate_linear = _Linear(
            [inputs, r_state],
            self._num_units,
            True,
            bias_initializer=self._bias_initializer,
            kernel_initializer=self._kernel_initializer)
    c = self._activation(self._candidate_linear([inputs, r_state]))
    new_h = u * state + (1 - u) * c
    return new_h, new_h
           

具體函數功能解析如下:

在 state_size()、output_size() 方法裡,其傳回的内容都是 num_units,即神經元的個數。

call() 方法中,因為 Reset Gate rt 和 Update Gate zt 分别用變量 r、u 表示,它們需要先對 ht-1 即 state 和 xt 做合并,然後再實作線性變換,再調用 sigmod 函數得到:

value = math_ops.sigmoid(self._gate_linear([inputs, state]))
r, u = array_ops.split(value=value, num_or_size_splits=2, axis=1)
           

然後需要求解 ht~,首先用 rt 和 ht-1 即 state 相乘:

r_state = r * state
           

然後将其放到線性函數裡面

_Linear

,再調用 tanh 激活函數即可:

最後計算隐含狀态和輸出結果,二者一緻:

new_h = u * state + (1 - u) * c
           

這樣即可傳回得到輸出結果和隐藏狀态。

return new_h, new_h
           

2.4 自定義RNNCell

自定義RNNCell的方法比較簡單,那就是繼承_LayerRNNCell這個抽象類,然後一定要實作__init__、build、__call__這三個函數就行了,其中在call函數中實作自己需要的功能即可。(注意:build隻調用一次,在build中進行變量執行個體化,在call中實作具體的rnncell操作)。

2.5 DIEN之VecAttGRUCell

調用VecAttGRUCell的代碼如下:

首先我們要注意到 tf.expand_dims的使用,這個函數是用來把 alphas 增加一維。

-1 表示在最後增加一維。

阿裡在這裡做的修改主要是call函數,是關于att_score的修改:

u = (1.0 - att_score) * u
new_h = u * state + (1 - u) * c
return new_h, new_h    
           

具體代碼是:

def call(self, inputs, state, att_score=None):
    ......
    c = self._activation(self._candidate_linear([inputs, r_state]))
    u = (1.0 - att_score) * u  # 這裡是新增加的
    new_h = u * state + (1 - u) * c # 這裡是新增加的
    return new_h, new_h
           

其中運作時變量如下:

inputs = {Tensor} Tensor("rnn_2/gru2/while/TensorArrayReadV3:0", shape=(?, 36), dtype=float32)
state = {Tensor} Tensor("rnn_2/gru2/while/Identity_2:0", shape=(?, 36), dtype=float32)
att_score = {Tensor} Tensor("rnn_2/gru2/while/strided_slice:0", shape=(?, 1), dtype=float32)
new_h = {Tensor} Tensor("rnn_2/gru2/while/add_1:0", shape=(?, 36), dtype=float32)
u = {Tensor} Tensor("rnn_2/gru2/while/mul_1:0", shape=(?, 36), dtype=float32)
c = {Tensor} Tensor("rnn_2/gru2/while/Tanh:0", shape=(?, 36), dtype=float32)
           

具體對應論文中就是:

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

0x03 RNN

3.1 一次執行多步

3.1.1 基礎

基礎的RNNCell有一個很明顯的問題:對于單個的RNNCell,我們使用它的call函數進行運算時,隻是在序列時間上前進了一步。比如使用x1、h0得到h1,通過x2、h1得到h2等**。這樣的h話,如果我們的序列長度為10,就要調用10次call函數,比較麻煩。對此,TensorFlow提供了一個tf.nn.dynamic_rnn函數,使用該函數就相當于調用了n次call函數。**即通過{h0,x1, x2, …., xn}直接得{h1,h2…,hn}。

def dynamic_rnn(cell, inputs, att_scores=None, sequence_length=None, initial_state=None,
                dtype=None, parallel_iterations=None, swap_memory=False,
                time_major=False, scope=None):
           

重要參數介紹:

  • cell:LSTM、GRU等的記憶單元。cell參數代表一個LSTM或GRU的記憶單元,也就是一個cell。例如,cell = tf.nn.rnn_cell.LSTMCell((num_units),其中,num_units表示rnn cell中神經元個數,也就是下文的cell.output_size。傳回一個LSTM或GRU cell,作為參數傳入。
  • inputs:輸入的訓練或測試資料,一般格式為[batch_size, max_time, embed_size],其中batch_size是輸入的這批資料的數量,max_time就是這批資料中序列的最長長度,embed_size表示嵌入的詞向量的次元。
  • sequence_length:是一個list,假設你輸入了三句話,且三句話的長度分别是5,10,25,那麼sequence_length=[5,10,25]。
  • time_major:決定了輸出tensor的格式,如果為True, 張量的形狀必須為 [max_time, batch_size,cell.output_size]。如果為False, tensor的形狀必須為[batch_size, max_time, cell.output_size],cell.output_size表示rnn cell中神經元個數。

傳回值如下:

outputs就是time_steps步裡所有的輸出。它的形狀為(batch_size, time_steps, cell.output_size)。

state是最後一步的隐狀态,它的形狀為(batch_size, cell.state_size)。

詳細如下:

  • outputs. outputs是一個tensor,是每個step的輸出值。
    • 如果time_major==True,outputs形狀為 [max_time, batch_size, cell.output_size ](要求rnn輸入與rnn輸出形狀保持一緻)
    • 如果time_major==False(預設),outputs形狀為 [ batch_size, max_time, cell.output_size ]
  • state. state是一個tensor。state是最終的狀态,也就是序列中最後一個cell輸出的狀态。一般情況下state的形狀為 [batch_size, cell.output_size ],但當輸入的cell為BasicLSTMCell時,state的形狀為[2,batch_size, cell.output_size ],其中2也對應着LSTM中的cell state和hidden state

max_time就是這批資料中序列的最長長度,如果輸入的三個句子,那max_time對應的就是最長句子的單詞數量,cell.output_size其實就是rnn cell中神經元的個數。

3.1.2 使用

假設們輸入資料的格式為(batch_size, time_steps, input_size),其中:

  • batch_size是輸入的這批資料的數量;
  • time_steps表示序列本身的長度,如在Char RNN中,長度為10的句子對應的time_steps就等于10;
  • input_size就表示輸入資料單個序列單個時間次元上固有的長度;

如下我們已經定義好了一個RNNCell,調用該RNNCell的call函數time_steps次

# inputs: shape = (batch_size, time_steps, input_size) 
# cell: RNNCell
# initial_state: shape = (batch_size, cell.state_size)。初始狀态。一般可以取零矩陣
outputs, state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)
           

對于參數舉例如下:

樣本資料:

小明愛學習

小王愛學習

小李愛學習

小花愛學習

通常樣本資料會以

(batch_size, time_step, embedding_size)

送入模型,對應的可以是(4,5,100)。

4表示批量送入也就是(小,小,小,小)第二批是(明,王,李,花)…

5表示時間步長,一句話共5個字。

又比如如下代碼:

import tensorflow as tf
import numpy as np
from tensorflow.python.ops import variable_scope as vs

output_size = 5
batch_size = 4
time_step = 3
dim = 3
cell = tf.nn.rnn_cell.BasicRNNCell(num_units=output_size)
inputs = tf.placeholder(dtype=tf.float32, shape=[time_step, batch_size, dim])
h0 = cell.zero_state(batch_size=batch_size, dtype=tf.float32)
X = np.array([[[1, 2, 1], [2, 0, 0], [2, 1, 0], [1, 1, 0]],  # x1
              [[1, 2, 1], [2, 0, 0], [2, 1, 0], [1, 1, 0]],  # x2
              [[1, 2, 1], [2, 0, 0], [2, 1, 0], [1, 1, 0]]])  # x3
outputs, final_state = tf.nn.dynamic_rnn(cell, inputs, initial_state=h0, time_major=True)

sess = tf.Session()
sess.run(tf.global_variables_initializer())
a, b = sess.run([outputs, final_state], feed_dict={inputs:X})
print(a)
print(b)
           

3.1.3 time_step

具體解釋如下:

文字資料

如果資料有1000段時序的句子,每句話有25個字,對每個字進行向量化,每個字的向量次元為300,那麼batch_size=1000,time_steps=25,input_size=300。

解析:time_steps一般情況下就是等于句子的長度,input_size等于字量化後向量的長度。

圖檔資料

拿MNIST手寫數字集來說,訓練資料有6000個手寫數字圖像,每個數字圖像大小為28*28,batch_size=6000沒的說,time_steps=28,input_size=28,我們可以了解為把圖檔圖檔分成28份,每份shape=(1, 28)。

音頻資料

如果是單通道音頻資料,那麼音頻資料是一維的,假如shape=(8910,)。使用RNN的資料必須是二維的,這樣加上batch_size,資料就是三維的,第一維是batch_size,第二維是time_steps,第三位是資料input_size。我們可以把資料reshape成三維資料。這樣就能使用RNN了。

3.2 如何循環調用

dnn有static和dynamic的分别。

  • static_rnn會把RNN展平,用空間換時間。
  • dynamic_rnn則是使用for或者while循環。

調用static_rnn實際上是生成了rnn按時間序列展開之後的圖。打開tensorboard你會看到sequence_length個rnn_cell stack在一起,隻不過這些cell是share weight的。是以,sequence_length就和圖的拓撲結構綁定在了一起,是以也就限制了每個batch的sequence_length必須是一緻。

調用dynamic_rnn不會将rnn展開,而是利用tf.while_loop這個api,通過Enter, Switch, Merge, LoopCondition, NextIteration等這些control flow的節點,生成一個可以執行循環的圖(這個圖應該還是靜态圖,因為圖的拓撲結構在執行時是不會變化的)。在tensorboard上,你隻會看到一個rnn_cell, 外面被一群control

flow節點包圍着。對于dynamic_rnn來說,sequence_length僅僅代表着循環的次數,而和圖本身的拓撲沒有關系,是以每個batch可以有不同sequence_length。

對于DIEN,程式運作時候,堆棧如下:

call, utils.py:144
__call__, utils.py:114
<lambda>, rnn.py:752
_rnn_step, rnn.py:236
_time_step, rnn.py:766
_BuildLoop, control_flow_ops.py:2590
BuildLoop, control_flow_ops.py:2640
while_loop, control_flow_ops.py:2816
_dynamic_rnn_loop, rnn.py:786
dynamic_rnn, rnn.py:615
__init__, model.py:364
train, train.py:142
<module>, train.py:231
           

循環的實作主要是在 control_flow_ops.py 之中。

while_loop 會 在 cond 參數為true時候,一直循環 body 參數對應的代碼。

def while_loop(cond, body, loop_vars, shape_invariants=None,
               parallel_iterations=10, back_prop=True, swap_memory=False,
               name=None):
  """Repeat `body` while the condition `cond` is true.

  `cond` is a callable returning a boolean scalar tensor. `body` is a callable
  returning a (possibly nested) tuple, namedtuple or list of tensors of the same
  arity (length and structure) and types as `loop_vars`. `loop_vars` is a
  (possibly nested) tuple, namedtuple or list of tensors that is passed to both
  `cond` and `body`. `cond` and `body` both take as many arguments as there are
  `loop_vars`.

  Args:
    cond: A callable that represents the termination condition of the loop.
    body: A callable that represents the loop body.
    loop_vars: A (possibly nested) tuple, namedtuple or list of numpy array,
      `Tensor`, and `TensorArray` objects.
  """
    if context.in_eager_mode():
      while cond(*loop_vars):
        loop_vars = body(*loop_vars)
      return loop_vars

    if shape_invariants is not None:
      nest.assert_same_structure(loop_vars, shape_invariants)

    loop_context = WhileContext(parallel_iterations, back_prop, swap_memory)  # pylint: disable=redefined-outer-name
    ops.add_to_collection(ops.GraphKeys.WHILE_CONTEXT, loop_context)
    result = loop_context.BuildLoop(cond, body, loop_vars, shape_invariants)
    return result
           

比如如下例子:

i = tf.constant(0)
c = lambda i: tf.less(i, 10)
b = lambda i: tf.add(i, 1)
r = tf.while_loop(c, b, [i])
print(sess.run(r) ) # 10
           

在rnn中,_time_step 就對 while_loop 進行了調用,這樣就完成了疊代。

_, output_final_ta, final_state = control_flow_ops.while_loop(
          cond=lambda time, *_: time < time_steps,
          body=_time_step,
          loop_vars=(time, output_ta, state),
          parallel_iterations=parallel_iterations,
          swap_memory=swap_memory)
           

3.3. DIEN之rnn

DIEN項目中,修改的部分主要是_time_step函數,因為需要加入att_scores參數。

其主要是:

  • 通過

    lambda: cell(input_t, state, att_score)

    調用 cell # call 函數,即我們事先寫的業務邏輯;
  • 通過調用

    control_flow_ops.while_loop(cond=lambda time, *_: time < time_steps, body=_time_step...)

    來進行循環疊代;

縮減版代碼如下:

def _time_step(time, output_ta_t, state, att_scores=None):
    """Take a time step of the dynamic RNN.
    Args:
      time: int32 scalar Tensor.
      output_ta_t: List of `TensorArray`s that represent the output.
      state: nested tuple of vector tensors that represent the state.

    Returns:
      The tuple (time + 1, output_ta_t with updated flow, new_state).
    """

    ......
    
    if att_scores is not None:
        att_score = att_scores[:, time, :]
        call_cell = lambda: cell(input_t, state, att_score)
    else:
        call_cell = lambda: cell(input_t, state)
        
    ......

    output_ta_t = tuple(
        ta.write(time, out) for ta, out in zip(output_ta_t, output))
    
    if att_scores is not None:
        return (time + 1, output_ta_t, new_state, att_scores)
    else:
        return (time + 1, output_ta_t, new_state)

  if att_scores is not None:  
      _, output_final_ta, final_state, _ = control_flow_ops.while_loop(
          cond=lambda time, *_: time < time_steps,
          body=_time_step,
          loop_vars=(time, output_ta, state, att_scores),
          parallel_iterations=parallel_iterations,
          swap_memory=swap_memory)
  else:
      _, output_final_ta, final_state = control_flow_ops.while_loop(
          cond=lambda time, *_: time < time_steps,
          body=_time_step,
          loop_vars=(time, output_ta, state),
          parallel_iterations=parallel_iterations,
          swap_memory=swap_memory)

    ......
           

0xEE 個人資訊

★★★★★★關于生活和技術的思考★★★★★★

微信公衆賬号:羅西的思考

如果您想及時得到個人撰寫文章的消息推送,或者想看看個人推薦的技術資料,可以掃描下面二維碼(或者長按識别二維碼)關注個人公衆号)。

[論文閱讀] RNN 在阿裡DIEN中的應用[論文閱讀] RNN 在阿裡DIEN中的應用

0xFF 參考

通過代碼學習RNN,徹底弄懂time_step

LSTM 實際神經元隐含層實體架構原了解析

機器學習之LSTM

知乎-何之源:TensorFlow中RNN實作的正确打開方式

解讀tensorflow之rnn

char-rnn-tensorflow源碼解析及結構流程分析

循環神經網絡(LSTM和GRU)(2)

TensorFlow中RNN實作的正确打開方式

完全圖解RNN、RNN變體、Seq2Seq、Attention機制

Tensorflow中RNNCell源碼解析

tensorflow中RNNcell源碼分析以及自定義RNNCell的方法

Tensorflow rnn_cell api 閱讀筆記

循環神經網絡系列(一)Tensorflow中BasicRNNCell

循環神經網絡系列(二)Tensorflow中dynamic_rnn

LSTM中tf.nn.dynamic_rnn處理過程詳解

小白循環神經網絡RNN LSTM 參數數量 門單元 cell units timestep batch_size

tensorflow筆記:多層LSTM代碼分析

Tensorflow dynamic rnn,源代碼的逐行解讀

Tensorflow RNN源碼了解

Tensorflow RNN源代碼解析筆記1:RNNCell的基本實作

Tensorflow RNN源代碼解析筆記2:RNN的基本實作

【tensorflow】static_rnn與dynamic_rnn的差別

繼續閱讀