Transformer——代碼篇

最最前面

  • 本文是邊看代碼,邊思考,邊驗證寫的,所以很雜亂,我個人喜歡記錄這樣的筆記,方便看到自己的不足。
  • 我儘可能的分塊表達。
  • 上半部分代碼講完後,我最後還稍微做了些疑惑驗證,很建議首先看。

  • 理論基本看完了,宏觀認知有了,所以?開始愉快的lu代碼把。
  • 一般而言源碼解析博客都是對着源碼看得,只看博客感覺太重了,本人想不到寫源碼解析博客有什麼好辦法,所以這裏也只能放代碼加解析了。
  • 解析代碼:transformer
  • 你可能由於tensorflow版本原因會有一些報錯,但都是小問題,原因是原代碼的tensorflow版本一些函數被移除了,自己根據報錯定位一下,改改就可以了。
  • 有很多註釋我覺得加在代碼裏更清晰,尤其是維度變換,所以,並不是所有的東西都在外面的文字裏。代碼訓練即視感:

在這裏插入圖片描述

  • 我個人沒訓練,太久了,我就看波源碼把,代碼作者有訓練好的模型,想用的可以去試試。

  • 我主要是跑通代碼,至少沒有錯。跑的時候注意內存大小,你可以自己調超參數防止內存溢出,內存不夠也有報錯,所以我只能強制cpu上跑,個人顯存不夠。

  • PS: 原代碼跑起來很慢,如果我們單純只是想跑通,或者看某一個節點的情況,我們可以刪減它原本的train文件(import 那些沒有加,是因爲我自己Import 的model文件名是自己起的,我怕誤導你們),類似於這樣,跑起來又快,又利於理解代碼,簡直不要太爽:

hparams = Hparams()
parser = hparams.parser
hp = parser.parse_args()
save_hparams(hp, hp.logdir)



# 返回dataset
train_batches, num_train_batches, num_train_samples = get_batch(hp.train1, hp.train2,
                                             hp.maxlen1, hp.maxlen2,
                                             hp.vocab, hp.batch_size,
                                             shuffle=True)

eval_batches, num_eval_batches, num_eval_samples = get_batch(hp.eval1, hp.eval2,
                                             100000, 100000,
                                             hp.vocab, hp.batch_size,
                                             shuffle=False)

# create a iterator of the correct shape and type
iter = tf.data.Iterator.from_structure(train_batches.output_types, train_batches.output_shapes)

xs, ys = iter.get_next()

train_init_op = iter.make_initializer(train_batches)
eval_init_op = iter.make_initializer(eval_batches)

logging.info("# Load model")

m = Transformer(hp)

loss, train_op, global_step = m.train(xs, ys)

# 使用cpu
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"

with tf.Session() as sess:

    # 數據init
    sess.run(train_init_op)

    sess.run(tf.global_variables_initializer())

    _loss = sess.run(loss) # train loss

    print(_loss)

預處理模塊(prepro)

  • 這個不是重點,感興趣的可以細看,大致做法是:Load raw data -> Preprocessing -> Segmenting with sentencepice
  • 其中sentencepice主要是對數據做BPE處理,這個我也單獨成文寫過,感興趣的可以看看。

preprocessing後結果

在這裏插入圖片描述
segmenting with sentencepice後的結果

在這裏插入圖片描述

數據加載模塊(data_load)

該文件包含所有關於加載數據以及批量化數據的函數

載入數據

def load_data(fpath1, fpath2, maxlen1, maxlen2):
    '''Loads source and target data and filters out too lengthy samples.
    fpath1: source file path. string.
    fpath2: target file path. string.
    maxlen1: source sent maximum length. scalar.
    maxlen2: target sent maximum length. scalar.

    Returns
    sents1: list of source sents
    sents2: list of target sents
    '''
    sents1, sents2 = [], []
    
    with open(fpath1, 'r') as f1, open(fpath2, 'r') as f2:
        for sent1, sent2 in zip(f1, f2):
            if len(sent1.split()) + 1 > maxlen1: continue # 1: </s>
            if len(sent2.split()) + 1 > maxlen2: continue  # 1: </s>
            sents1.append(sent1.strip())
            sents2.append(sent2.strip())
    return sents1, sents2
  • 並沒有什麼難的,只是個人第一次見到同時加載兩個文件的寫法,覺得可以貼一貼。
def encode(inp, type, dict):
    '''Converts string to number. Used for `generator_fn`.
    inp: 1d byte array.
    type: "x" (source side) or "y" (target side)
    dict: token2idx dictionary

    Returns
    list of numbers
    '''
    inp_str = inp
    if type=="x": tokens = inp_str.split() + ["</s>"]
    else: tokens = ["<s>"] + inp_str.split() + ["</s>"]

    x = [dict.get(t, dict["<unk>"]) for t in tokens]
    return x
  • 神經網絡embedding的老套路了,把詞的index用於詞的表示,方便以後lookup,
  • 這裏放上是第一次見到:[dict.get(t, dict["< unk >"]這種寫法,很巧秒。
def generator_fn(sents1, sents2, vocab_fpath):
	token2idx, _ = load_vocab(vocab_fpath)
	for sent1, sent2 in zip(sents1, sents2):
	     x = encode(sent1, "x", token2idx)
	     y = encode(sent2, "y", token2idx)
	     decoder_input, y = y[:-1], y[1:]
	
	     x_seqlen, y_seqlen = len(x), len(y)
	     yield (x, x_seqlen, sent1), (decoder_input, y, y_seqlen, sent2)
  • 這個函數的yield值得說一下: (decoder_input, y, y_seqlen, sent2),這個和上面那個函數呼應,因爲traget句子多一個< s >用於decode的第一個開始,所以這裏,decode的input和output不一樣。

進入最重要的一個函數

def input_fn(sents1, sents2, vocab_fpath, batch_size, shuffle=False):
    '''Batchify data
    sents1: list of source sents
    sents2: list of target sents
    vocab_fpath: string. vocabulary file path.
    batch_size: scalar
    shuffle: boolean

    Returns
    xs: tuple of
        x: int32 tensor. (N, T1)
        x_seqlens: int32 tensor. (N,)
        sents1: str tensor. (N,)
    ys: tuple of
        decoder_input: int32 tensor. (N, T2)
        y: int32 tensor. (N, T2)
        y_seqlen: int32 tensor. (N, )
        sents2: str tensor. (N,)
    '''
    shapes = (([None], (), ()),
              ([None], [None], (), ()))
    types = ((tf.int32, tf.int32, tf.string),
             (tf.int32, tf.int32, tf.int32, tf.string))
    paddings = ((0, 0, ''),
                (0, 0, 0, ''))

    dataset = tf.data.Dataset.from_generator(
        lambda:generator_fn(sents1, sents2, vocab_fpath),
        output_shapes=shapes,
        output_types=types)

	# 參數參考[5]
    if shuffle: # for training
        dataset = dataset.shuffle(128*batch_size)
	
	# 多個epoch
    dataset = dataset.repeat()  # iterate forever

	# 填充爲固定長度,這個shpe沒有固定,經驗證,默認batch內最大句子的維度
    dataset = dataset.padded_batch(batch_size, shapes, paddings).prefetch(1)

    return dataset
  • 其中 tf.data.Dataset.from_generator可以參考[ 4 ]

一、Tensorflow讀入數據的三種方式
1)Feeding:Python代碼在運行每一步時提供數據
2)從文件中讀取:輸入管道從TensorFlow圖形的開頭讀取文件中的數據。
3)預加載數據:TensorFlow圖中的常量或變量保存所有數據(對於小數據集)

  • Dataset API屬於第二種方式,使讀取數據、複雜的數據格式變換變得更容易
def get_batch(fpath1, fpath2, maxlen1, maxlen2, vocab_fpath, batch_size, shuffle=False):
    '''Gets training / evaluation mini-batches
	    fpath1: source file path. string.
	    fpath2: target file path. string.
	    maxlen1: source sent maximum length. scalar.
	    maxlen2: target sent maximum length. scalar.
	    vocab_fpath: string. vocabulary file path.
	    batch_size: scalar
	    shuffle: boolean
	
	    Returns
	    batches
	    num_batches: number of mini-batches
	    num_samples
    '''
    # ....這還不是一次性加載到內存裏了
    sents1, sents2 = load_data(fpath1, fpath2, maxlen1, maxlen2)
    batches = input_fn(sents1, sents2, vocab_fpath, batch_size, shuffle=shuffle)

	# 計算batch個數
    num_batches = calc_num_batches(len(sents1), batch_size)
    
    return batches, num_batches, len(sents1)

模型篇(model)

embedding

  • 詞庫單詞的embedding,這裏直接xavier隨機初始化了,唯一特別的是最前面加了一行zero_pad,你可以認爲填充詞的編碼爲0,也就是第一行是專門針對填充詞的編碼。他是不變的,而且是常數0,不然填充就有意義了,然後我突然知道,自己以前也同樣有這樣的操作,**但是我沒有設置爲常數!!!
self.embeddings = get_token_embeddings(self.hp.vocab_size, self.hp.d_model, zero_pad=True)
def get_token_embeddings(vocab_size, num_units, zero_pad=True):
    '''Constructs token embedding matrix.
    Note that the column of index 0's are set to zeros.
    vocab_size: scalar. V.
    num_units: embedding dimensionalty. E.
    zero_pad: Boolean. If True, all the values of the first row (id = 0) should be constant zero
    To apply query/key masks easily, zero pad is turned on.

    Returns
    weight variable: (V, E)
    '''
    with tf.variable_scope("shared_weight_matrix"):
        embeddings = tf.get_variable('weight_mat',
                                   dtype=tf.float32,
                                   shape=(vocab_size, num_units),
                                   initializer=tf.contrib.layers.xavier_initializer())
        if zero_pad:
            embeddings = tf.concat((tf.zeros(shape=[1, num_units]),
                                    embeddings[1:, :]), 0)
    return embeddings

positional_encoding

在這裏插入圖片描述

def positional_encoding(inputs,
                        maxlen,
                        masking=True,
                        scope="positional_encoding"):
    '''Sinusoidal Positional_Encoding. See 3.5
    inputs: 3d tensor. (N, T, E)
    maxlen: scalar. Must be >= T
    masking: Boolean. If True, padding positions are set to zeros.
    scope: Optional scope for `variable_scope`.

    returns
    3d tensor that has the same shape as inputs.
    '''
	# 和embedding一樣,方便相加
    E = inputs.get_shape().as_list()[-1] # static
    
    N, T = tf.shape(inputs)[0], tf.shape(inputs)[1] # dynamic
    
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
    
        # position indices,T最大長度,N:batc_size,縱向維度
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1]) # (N, T)

        # First part of the PE function: sin and cos argument
        position_enc = np.array([
            [pos / np.power(10000, (i-i%2)/E) for i in range(E)]
            for pos in range(maxlen)])

        # Second part, apply the cosine to even columns and sin to odds.
        position_enc[:, 0::2] = np.sin(position_enc[:, 0::2])  # dim 2i
        position_enc[:, 1::2] = np.cos(position_enc[:, 1::2])  # dim 2i+1
        position_enc = tf.convert_to_tensor(position_enc, tf.float32) # (maxlen, E)

        # lookup
        # position_enc整個長度都編碼,這裏是找對應的編碼
        outputs = tf.nn.embedding_lookup(position_enc, position_ind)

        # masks
        # 原先位置爲0的位置,仍然爲0
        if masking:
            outputs = tf.where(tf.equal(inputs, 0), inputs, outputs)
            
        return tf.to_float(outputs)
  • 其實就是根據公式進行位置編碼,但是其中的維度變換亮瞎了我的眼,還好老子會print這種大法。

  • position_ind維度是[ N,T ] N,T是變化的,它究竟是什麼?

  • 類似於這種,它的作用其實就是索引,沒其他用。
    在這裏插入圖片描述

  • position_enc你可以看作position的embedding,他是根據上面的公式計算的,關鍵是,他的維度是[maxlen, E],請注意,maxlen不等於T。把這個信息利用起來的,是look_up這個我小瞧了的函數,我只用過一維索引,二維索引是什麼效果?見下:
    在這裏插入圖片描述

  • 你可以理解爲,這時候我索引的第一維度表示句子個數(batch),第二維度表示句子裏的單詞數(T),所以它直接將整個batch的位置編碼直接給弄出來了。

  • 所以最後的維度是?

  • [N,T,E]

  • 但是我一直有一點疑惑是,這裏我在load_data裏填充過數據了,這裏爲什麼還是不定長的T1,個人分析是,padding_batch時用的不定長維度,所以默認該batch內最大長度爲標準填充,而不是整個都用一個維度填充,自己這個函數用的不熟,暫時只是猜測。這裏對padding的部分編碼了也沒問題,我們有mask標記。

encode

    def encode(self, xs, training=True):
        '''
        Returns
        memory: encoder outputs. (N, T1, d_model)
        '''
        with tf.variable_scope("encoder", reuse=tf.AUTO_REUSE):

            x, seqlens, sents1 = xs

            # src_masks
            # 對比這兩個矩陣或者向量的相等的元素,如果是相等的那就返回True,反正返回False,返回的值的矩陣維度和A是一樣的
            # 找句子裏面填充爲0的位置
            # (N, T1)
            src_masks = tf.equal(x, 0) 

            # embedding
            enc = tf.nn.embedding_lookup(self.embeddings, x) # (N, T1, d_model)
		
			# 去除維度的影響
            enc *= self.hp.d_model**0.5 # scale

			# 句子加上位置信息
            enc += positional_encoding(enc, self.hp.maxlen1)
            enc = tf.layers.dropout(enc, self.hp.dropout_rate, training=training)

            ## Blocks
            for i in range(self.hp.num_blocks):
                with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
                    # self-attention
                    enc = multihead_attention(queries=enc,
                                              keys=enc,
                                              values=enc,
                                              key_masks=src_masks,
                                              num_heads=self.hp.num_heads,
                                              dropout_rate=self.hp.dropout_rate,
                                              training=training,
                                              causality=False)
                    # feed forward
                    enc = ff(enc, num_units=[self.hp.d_ff, self.hp.d_model])
                    
        # 最後輸出:(N, T1, d_model)            
        memory = enc
        return memory, sents1, src_masks
multi-attention
def multihead_attention(queries, keys, values, key_masks,
                        num_heads=8, 
                        dropout_rate=0,
                        training=True,
                        causality=False,
                        scope="multihead_attention"):
    '''Applies multihead attention. See 3.2.2
    queries: A 3d tensor with shape of [N, T_q, d_model].
    keys: A 3d tensor with shape of [N, T_k, d_model].
    values: A 3d tensor with shape of [N, T_k, d_model].
    key_masks: A 2d tensor with shape of [N, key_seqlen]
    num_heads: An int. Number of heads.
    dropout_rate: A floating point number.
    training: Boolean. Controller of mechanism for dropout.
    causality: Boolean. If true, units that reference the future are masked.
    scope: Optional scope for `variable_scope`.
        
    Returns
      A 3d tensor with shape of (N, T_q, C)  
    '''
    
    d_model = queries.get_shape().as_list()[-1]
    
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
    
        # Linear projections
        # 前向傳播,Q,K,V計算
        Q = tf.layers.dense(queries, d_model, use_bias=True) # (N, T_q, d_model)
        K = tf.layers.dense(keys, d_model, use_bias=True) # (N, T_k, d_model)
        V = tf.layers.dense(values, d_model, use_bias=True) # (N, T_k, d_model)
        
        # Split and concat
        Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*N, T_q, d_model/h)
        K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*N, T_k, d_model/h)
        V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*N, T_k, d_model/h)

        # Q_, K_, V_ 計算Attention
        outputs = scaled_dot_product_attention(Q_, K_, V_, key_masks, causality, dropout_rate, training)

        # Restore shape
        outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # (N, T_q, d_model)
              
        # Residual connection
        outputs += queries
              
        # Normalize (N, T_q, d_model)
        outputs = ln(outputs)
 
    return outputs

Attention計算

計算Q,K,V
在這裏插入圖片描述
計算相似度+softmax

在這裏插入圖片描述

def scaled_dot_product_attention(Q, K, V, key_masks,
                                 causality=False, dropout_rate=0.,
                                 training=True,
                                 scope="scaled_dot_product_attention"):
    '''See 3.2.1.
	    Q: Packed queries. 3d tensor. [N, T_q, d_k].
	    K: Packed keys. 3d tensor. [N, T_k, d_k].
	    V: Packed values. 3d tensor. [N, T_k, d_v].
	    key_masks: A 2d tensor with shape of [N, key_seqlen]
	    causality: If True, applies masking for future blinding
	    dropout_rate: A floating point number of [0, 1].
	    training: boolean for controlling droput
	    scope: Optional scope for `variable_scope`.
    '''
    
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        d_k = Q.get_shape().as_list()[-1]
        
        # 計算Q,K相似度。
        # Q: (h*N, T_q, d_model/h)  V: (h*N, T_k, d_model/h),其中 T_q == T_k
        # tf.transpose,高維度矩陣轉置,輸出維度:(h*N, d_model/h,T_k)
        # tf.matmul,最後兩維度做矩陣乘法,所以最後維度爲:
        # (h*N, T_q, T_k)
        outputs = tf.matmul(Q, tf.transpose(K, [0, 2, 1]))

        # scale,同樣,對值scale有點不清楚爲啥
        outputs /= d_k ** 0.5

        # key_masks: [N, key_seqlen]
        # outputs維度不會變化
        outputs = mask(outputs, key_masks=key_masks, type="key")

        # causality or future blinding masking
        if causality:
            outputs = mask(outputs, type="future")

        # softmax,數值轉化爲概率
        outputs = tf.nn.softmax(outputs)
        
        # (h*N, T_k,T_q),這個轉變只爲了下面畫圖
        attention = tf.transpose(outputs, [0, 2, 1])
        
        # tensorboard記錄,相當於attention可視化,但是注意:
        # TensorBord中看到的image summary永遠是最後一個global step的
        tf.summary.image("attention", tf.expand_dims(attention[:1], -1))


        # dropout
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=training)

        # weighted sum (context vectors)
        outputs = tf.matmul(outputs, V)  # (N, T_q, d_v)

    return outputs

masking

  • 解決填充問題:Masks paddings on keys or queries to inputs
  • 想讓那些key值的unit爲0的key對應的attention score極小,這樣在加權計算value的時候相當於對結果不造成影響。
  • 代碼內其實有點亂的是維度變化,我們先不管維度,先看一下他在幹啥??我舉一個簡單的例子:

在這裏插入圖片描述

  • 上圖代碼思路基本就是其paading_mask的思路
def mask(inputs, key_masks=None, type=None):
    """Masks paddings on keys or queries to inputs
	    inputs: 3d tensor. (h*N, T_q, T_k)
	    key_masks: 3d tensor. (N, 1, T_k)
	    type: string. "key" | "future"
	
	    e.g.,
	    >> inputs = tf.zeros([2, 2, 3], dtype=tf.float32)
	    >> key_masks = tf.constant([[0., 0., 1.],
	                                [0., 1., 1.]])
	    >> mask(inputs, key_masks=key_masks, type="key")
	    array([[[ 0.0000000e+00,  0.0000000e+00, -4.2949673e+09],
	        [ 0.0000000e+00,  0.0000000e+00, -4.2949673e+09]],
	
	       [[ 0.0000000e+00, -4.2949673e+09, -4.2949673e+09],
	        [ 0.0000000e+00, -4.2949673e+09, -4.2949673e+09]],
	
	       [[ 0.0000000e+00,  0.0000000e+00, -4.2949673e+09],
	        [ 0.0000000e+00,  0.0000000e+00, -4.2949673e+09]],
	
	       [[ 0.0000000e+00, -4.2949673e+09, -4.2949673e+09],
	        [ 0.0000000e+00, -4.2949673e+09, -4.2949673e+09]]], dtype=float32)
    """
    
    padding_num = -2 ** 32 + 1
    
    # padding_mask
    if type in ("k", "key", "keys"):
    	# [N,T1]
    	# True矩陣轉化爲float
        key_masks = tf.to_float(key_masks)
        
        # tf.title(key_masks,[h,1]) #同一維度上覆制的次數
        # 目的是:對應多頭的attention
        # 輸出: (h*N, T1)
        key_masks = tf.tile(key_masks, [tf.shape(inputs)[0] // tf.shape(key_masks)[0], 1]) 
        
        # 擴充維度爲:( h*N, 1, T1),行之間廣播
        # 要mask的目標: (h*N, T_q, T_k)
        key_masks = tf.expand_dims(key_masks, 1) 
        
        # 最後效果如上,需要mask的地方,全都爲很小的負數。
        outputs = inputs + key_masks * padding_num
        
    # 屏蔽未來信息    
    elif type in ("f", "future", "right"):
    
        diag_vals = tf.ones_like(inputs[0, :, :])  # (T_q, T_k)
        
        # 右上叫全爲0的矩陣
        tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()  # (T_q, T_k)
		
		# 將上面的操作擴充到batch裏
        future_masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(inputs)[0], 1, 1])  # (N, T_q, T_k)

        paddings = tf.ones_like(future_masks) * padding_num
		
		# 太強了
        outputs = tf.where(tf.equal(future_masks, 0), paddings, inputs)
    else:
        print("Check if you entered type correctly!")

    return outputs
  • 我們先來形容一下他在幹什麼,然後說它怎麼做。
  • encoder時的目的是特徵提取,什麼顧慮都沒有。但是到decoder時,每一個對應位置的attention就不能這麼弄了,它要加入一個限制條件,不能看到未來信息。簡單來說,就是第一個詞對應的輸出,他的attention計算時,q,k,v不能用用第一個以後的計算,但是!爲了維度統一,爲了方便我們可以用矩陣實現,我們還是照常算attention,但是算出的結果我們要處理一下。怎麼處理,見下面的說明,關鍵就是那個三角陣(不理解可以注意三角陣的維度:[q,k])
  • 怎麼屏蔽未來信息?
  • 該部分實現還是比較巧妙的,利用了一個三角陣的構思來實現。下面詳細介紹。
    首先定義一個和outputs後兩維的shape相同shape(T_q,T_k)的一個張量(矩陣)。
    然後將該矩陣轉爲三角陣tril。三角陣中,對於每一個T_q,凡是那些大於它角標的T_k值全都爲0,這樣作爲mask就可以讓query只取它之前的key(self attention中query即key)。由於該規律適用於所有query,接下來仍用tile擴展堆疊其第一個維度,構成masks,shape爲(h*N, T_q,T_k).

在這裏插入圖片描述

layer normalizition層 (ln)

在這裏插入圖片描述

def ln(inputs, epsilon = 1e-8, scope="ln"):

    '''Applies layer normalization. See https://arxiv.org/abs/1607.06450.
	    inputs: A tensor with 2 or more dimensions, where the first dimension has `batch_size`.
	    epsilon: A floating number. A very small number for preventing ZeroDivision Error.
	    scope: Optional scope for `variable_scope`.    
    Returns:
      A tensor with the same shape and data dtype as `inputs`.
    '''
    
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        inputs_shape = inputs.get_shape()
        params_shape = inputs_shape[-1:]
   
    	# 求均值,方差,以最後一維度求解, (N, T_q, d_model)
    	# 意味着只針對對應位置的輸出
        mean, variance = tf.nn.moments(inputs, [-1], keep_dims=True)
        
        beta= tf.get_variable("beta", params_shape, initializer=tf.zeros_initializer())
        
        gamma = tf.get_variable("gamma", params_shape, initializer=tf.ones_initializer())
        
        normalized = (inputs - mean) / ( (variance + epsilon) ** (.5) )
        
        outputs = gamma * normalized + beta
        
    return outputs

ff(前饋神經網絡層)

在這裏插入圖片描述

def ff(inputs, num_units, scope="positionwise_feedforward"):
    '''position-wise feed forward net. See 3.3
    
	    inputs: A 3d tensor with shape of [N, T, C].
	    num_units: A list of two integers.
	    scope: Optional scope for `variable_scope`.

    Returns:
      A 3d tensor with the same shape and dtype as inputs
    '''
    
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
    
        # Inner layer
        # num_units : [self.hp.d_ff, self.hp.d_model], 默認爲[2048,512]
        # outputs: [N,T,d_ff]
        outputs = tf.layers.dense(inputs, num_units[0], activation=tf.nn.relu)

        # Outer layer
        # [N,T,d_model]
        outputs = tf.layers.dense(outputs, num_units[1])

        # Residual connection
        outputs += inputs
        
        # Normalize
        outputs = ln(outputs)
    
    return outputs

decoder部分

在這裏插入圖片描述

  def decode(self, ys, memory, src_masks, training=True):
       '''
	       memory: encoder outputs. (N, T1, d_model)
	       src_masks: (N, T1)
	
	       Returns
	       logits: (N, T2, V). float32.
	       y_hat: (N, T2). int32
	       y: (N, T2). int32
	       sents2: (N,). string.
       '''
       
       with tf.variable_scope("decoder", reuse=tf.AUTO_REUSE):
           decoder_inputs, y, seqlens, sents2 = ys

           # tgt_masks
           tgt_masks = tf.equal(decoder_inputs, 0)  # (N, T2)

           # embedding
           dec = tf.nn.embedding_lookup(self.embeddings, decoder_inputs)  # (N, T2, d_model)
           dec *= self.hp.d_model ** 0.5  # scale

           dec += positional_encoding(dec, self.hp.maxlen2)
           dec = tf.layers.dropout(dec, self.hp.dropout_rate, training=training)

           # Blocks
           for i in range(self.hp.num_blocks):
               with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
                   # Masked self-attention (Note that causality is True at this time)
                   dec = multihead_attention(queries=dec,
                                             keys=dec,
                                             values=dec,
                                             key_masks=tgt_masks,
                                             num_heads=self.hp.num_heads,
                                             dropout_rate=self.hp.dropout_rate,
                                             training=training,
                                             causality=True,
                                             scope="self_attention")

                   # Vanilla attention,中間層
                   dec = multihead_attention(queries=dec,
                                             keys=memory,
                                             values=memory,
                                             key_masks=src_masks,
                                             num_heads=self.hp.num_heads,
                                             dropout_rate=self.hp.dropout_rate,
                                             training=training,
                                             causality=False,
                                             scope="vanilla_attention")
                   ### Feed Forward
                   dec = ff(dec, num_units=[self.hp.d_ff, self.hp.d_model])

       # Final linear projection (embedding weights are shared)
       
       weights = tf.transpose(self.embeddings) # (d_model, vocab_size)
       
       # 也是一種矩陣乘法,三維和二維之間
       logits = tf.einsum('ntd,dk->ntk', dec, weights) # (N, T2, vocab_size)
       
       # (N, T2)
       y_hat = tf.to_int32(tf.argmax(logits, axis=-1))

       return logits, y_hat, y, sents2
  • 最後輸出部分可以看:

在這裏插入圖片描述

  • 我覺得唯一一個需要注意的點就是,我可能被一個講解給誤導了:常裏來說,講decode時,都會講出一種循環的味道,但其實,這裏沒有循環操作!!!!沒有!!
  • decode的input是(< s >,你) 對應的label是( 你,好),你完全可以看作一個獨立的模塊一次執行,只不過,這個模塊,用了encode出來的memory信息,同時加上了防止未來信息泄漏的mask,可以說,mask的引入,使得我們不需要循環操作了。

train

  • 模型搭建後訓練操作,無非就那點東西。

額外探索

bleu評分

  • 全稱爲Bilingual Evaluation Understudy(雙語評估替換),是一種對生成語句進行評估的指標,用於比較候選文本翻譯與其他一個或多個參考翻譯的評價分數。其實就是一個公式,可以看一下下面的博客(當然相關介紹還有很多,這個評分就不展開了,通用方法)
    淺談BLEU評分

輸入數據維度的探索

  • 上面說了,我對填充完後的數據表示很疑惑,到底是最大長度填充,還是batch內最大句子長度填充,所以我就來了波玄學探索
    在這裏插入圖片描述

  • 32是我的batch_size,44是句子長度,但我的最大句子長度是100,噢或!我看到了什麼,我猜對了!

  • 我們打印兩次:
    在這裏插入圖片描述

  • 驗證一個猜想後,我又一個疑惑出來了,輸出的句子維度動態變化,我的模型接受數據也動態變化?不可能啊(可能我都是填充一個相同維度慣了)

再探索encode的維度

  • 拿我們的source數據舉例子,它進入encdoe時,會經過下面的代碼,提取出Q,K,V,那我們打印一下Q的維度
Q = tf.layers.dense(queries, d_model, use_bias=True)  # (N, T_q, d_model)

在這裏插入圖片描述

  • ???
  • 我們注意!layers.dense改變的只是最後一維的維度(embedding_size),當我們設定好後,他是固定的,所以,句子的長度變化不影響這一層的權重(ps:後補充,因爲句子裏的每一個單詞都用同一個)。所以這裏可以看作一個權重,不然隨着句子長度變化,權重維度不斷變化…想多了。不可能。
  • 我們尋找下一個tf.Variable,看看這個句子長度變換對它的影響。
enc = ff(enc, num_units=[self.hp.d_ff, self.hp.d_model])
# Inner layer
outputs = tf.layers.dense(inputs, num_units[0], activation=tf.nn.relu)

# Outer layer
outputs = tf.layers.dense(outputs, num_units[1])

# Residual connection
outputs += inputs

# Normalize
outputs = ln(outputs)
  • 這個num_units是人工設置的,相當於這裏又是,句子長度變化,不影響這個權重參數。

  • 然後encode裏就沒參數了…???

  • 所以我句子長度一直變,是沒事兒的,雖然它常理上違反我的認知(給我一種那參數權重不久一直變的錯覺!!!)

  • 上一張圖自己筆記裏的圖:

在這裏插入圖片描述

  • 一種自己沒仔細思考,自己喫苦果的mmp心情…

  • 那我覺得我還想看一下,decoder裏,有一個attention很特殊接受兩個信息,我還想看看它,就是下面這個:

在這裏插入圖片描述

上面那三個箭頭對應:
		queries=dec,
		keys=memory,
		values=memory
  • 我覺得要是理解attention的應該對這個很熟悉,這個其實就不是self-attention了,更像attention,query是target的Hi-1,我從memory裏做相似性匹配(所謂的對齊),找利於我翻譯的信息。
  • So,直白點,這一層往上走的信息還是從memory裏提取的,但是怎麼提取,要看我現在已經產生的traget,其實就是注意力機制。

label smoothing

  • 把之前的one_hot中的0改成了一個很小的數,1改成了一個比較接近於1的數
  • 個人沒有探究爲啥這樣做。
def label_smoothing(inputs, epsilon=0.1):
    '''Applies label smoothing. See 5.4 and https://arxiv.org/abs/1512.00567.
		    inputs: 3d tensor. [N, T, V], where V is the number of vocabulary.
		    epsilon: Smoothing rate.
		    
		    For example,
		    
		    ```
		    import tensorflow as tf
		    inputs = tf.convert_to_tensor([[[0, 0, 1], 
		       [0, 1, 0],
		       [1, 0, 0]],
		
		      [[1, 0, 0],
		       [1, 0, 0],
		       [0, 1, 0]]], tf.float32)
		       完畢
		    outputs = label_smoothing(inputs)
		    
		    with tf.Session() as sess:
		        print(sess.run([outputs]))
		    
		    >>
		    [array([[[ 0.03333334,  0.03333334,  0.93333334],
		        [ 0.03333334,  0.93333334,  0.03333334],
		        [ 0.93333334,  0.03333334,  0.03333334]],
		
		       [[ 0.93333334,  0.03333334,  0.03333334],
		        [ 0.93333334,  0.03333334,  0.03333334],
		        [ 0.03333334,  0.93333334,  0.03333334]]], dtype=float32)]   
		    ```
	'''
	
    V = inputs.get_shape().as_list()[-1] # number of channels
    return ((1-epsilon) * inputs) + (epsilon / V)

datasets

  • 代碼裏用了tf.data.Dataset的API,像我這種用慣了place_holder的人一下子不習慣,可以參考下面教程:
  • 簡單教程

END

  • 本人疑惑的坑全部填完

本文完!

參考

[ 1 ]The Annotated Transformer

[ 2 ]A TensorFlow Implementation of the Transformer: Attention Is All You Need

[ 3 ]機器翻譯模型Transformer代碼詳細解析

[ 4 ]ensorflow中API------tf.data.Dataset使用

[ 5 ]tf.data.Dataset.shuffle(buffer_size)中buffer_size的理解

[ 6 ] tensorflow 多張量計算

[ 7 ]einsum的基礎使用

[ 8 ]淺談BLEU評分

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