ALBERT與BERT的異同

論文地址https://openreview.net/pdf?id=H1eA7AEtvS

中文預訓練ALBERT模型https://github.com/brightmart/albert_zh

 

1、對Embedding因式分解(Factorized embedding parameterization)

在BERT中,詞embedding與encoder輸出的embedding維度是一樣的都是768。但是ALBERT認爲,詞級別的embedding是沒有上下文依賴的表述,而隱藏層的輸出值不僅包括了詞本生的意思還包括一些上下文信息,理論上來說隱藏層的表述包含的信息應該更多一些,因此應該讓H\gg E,所以ALBERT的詞向量的維度是小於encoder輸出值維度的。

在NLP任務中,通常詞典都會很大,embedding matrix的大小是E\times V,如果和BERT一樣讓H=E,那麼embedding matrix的參數量會很大,並且反向傳播的過程中,更新的內容也比較稀疏。

結合上述說的兩個點,ALBERT採用了一種因式分解的方法來降低參數量。首先把one-hot向量映射到一個低維度的空間,大小爲E,然後再映射到一個高維度的空間,說白了就是先經過一個維度很低的embedding matrix,然後再經過一個高維度matrix把維度變到隱藏層的空間內,從而把參數量從O(V\times H)降低到了O(V\times E+E\times ×H),當E\ll H時參數量減少的很明顯。
modeling.py中,

embedding因式分解的tensorflow代碼如下:

def embedding_lookup_factorized(input_ids, # Factorized embedding parameterization provide by albert
                     vocab_size,
                     hidden_size,
                     embedding_size=128,
                     initializer_range=0.02,
                     word_embedding_name="word_embeddings",
                     use_one_hot_embeddings=False):
    """
    :param input_ids: [batch_size, seq_length]
    :param vocab_size: 
    :param hidden_size: 
    :param embedding_size: 
    :param initializer_range: 
    :param word_embedding_name: 
    :param use_one_hot_embeddings: 
    :return: 
    """
    # 1. 將one-hot向量映射到embedding_size大小的低維稠密空間
    print("embedding_lookup_factorized. factorized embedding parameterization is used.")
    if input_ids.shape.ndims == 2:
        input_ids = tf.expand_dims(input_ids, axis=[-1])  # shape of input_ids is:[ batch_size, seq_length, 1]

    embedding_table = tf.get_variable(  # [vocab_size, embedding_size]
        name=word_embedding_name,
        shape=[vocab_size, embedding_size],
        initializer=create_initializer(initializer_range))

    flat_input_ids = tf.reshape(input_ids, [-1])  # one rank. shape as (batch_size * sequence_length,)
    if use_one_hot_embeddings:
        one_hot_input_ids = tf.one_hot(flat_input_ids,depth=vocab_size)  
        output_middle = tf.matmul(one_hot_input_ids, embedding_table)  # [batch_size * sequence_length,embedding_size]
    else:
        output_middle = tf.gather(embedding_table,flat_input_ids)  # [batch_size * sequence_length,embedding_size]

    # 2. 將第一步的輸出映射到hidden_size的向量空間
    project_variable = tf.get_variable(  # [embedding_size, hidden_size]
        name=word_embedding_name+"_2",
        shape=[embedding_size, hidden_size],
        initializer=create_initializer(initializer_range))
    output = tf.matmul(output_middle, project_variable) # [batch_size * sequence_length, hidden_size]
    # reshape back to 3 rank
    input_shape = get_shape_list(input_ids)  
    batch_size, sequene_length, _=input_shape
    output = tf.reshape(output, (batch_size,sequene_length,hidden_size))  # [batch_size, sequence_length, hidden_size]
    return (output, embedding_table, project_variable)

2、跨層的參數共享(Cross-layer parameter sharing)

在ALBERT還提出了一種參數共享的方法,Transformer中共享參數有多種方案,只共享全連接層,只共享attention層,ALBERT結合了上述兩種方案,全連接層與attention層都進行參數共享,也就是說共享encoder內的所有參數,同樣量級下的Transformer採用該方案後實際上效果是有下降的,但是參數量減少了很多,訓練速度也提升了很多。

modeling.py中,在variable_scope中設置reuse=True實現跨層共享參數。

在原始的Transformer中,Layer Norm在跟在Residual之後的,我們把這個稱爲Post-LN Transformer。Post-LN Transformer對參數非常敏感,需要很仔細地調參才能取得好的結果,比如必備的warm-up學習率策略,這會非常耗時間。

既然warm-up是訓練的初始階段使用的,那肯定是訓練的初始階段優化有問題,包括模型的初始化。

Post-LN Transformer在訓練的初始階段,輸出層附近的期望梯度非常大,所以,如果沒有warm-up,模型優化過程就會炸裂,非常不穩定。把LayerNorm換個位置,比如放在Residual的過程之中(稱爲Pre-LN Transformer),再觀察訓練初始階段的梯度變化,發現比Post-LN Transformer好很多,甚至不需要warm-up,從而進一步減少訓練時間。

參考論文:On Layer Normalization in the TransformerArchitecture

def prelln_transformer_model(input_tensor,
						attention_mask=None,
						hidden_size=768,
						num_hidden_layers=12,
						num_attention_heads=12,
						intermediate_size=3072,
						intermediate_act_fn=gelu,
						hidden_dropout_prob=0.1,
						attention_probs_dropout_prob=0.1,
						initializer_range=0.02,
						do_return_all_layers=False,
						shared_type='all', # None,
						adapter_fn=None):
	
	prev_output = bert_utils.reshape_to_matrix(input_tensor)

	all_layer_outputs = []

	def layer_scope(idx, shared_type):
		if shared_type == 'all':
			tmp = {
				"layer":"layer_shared",
				'attention':'attention',
				'intermediate':'intermediate',
				'output':'output'
			}
		elif shared_type == 'attention':
			tmp = {
				"layer":"layer_shared",
				'attention':'attention',
				'intermediate':'intermediate_{}'.format(idx),
				'output':'output_{}'.format(idx)
			}
		elif shared_type == 'ffn':
			tmp = {
				"layer":"layer_shared",
				'attention':'attention_{}'.format(idx),
				'intermediate':'intermediate',
				'output':'output'
			}
		else:
			tmp = {
				"layer":"layer_{}".format(idx),
				'attention':'attention',
				'intermediate':'intermediate',
				'output':'output'
			}

		return tmp

	all_layer_outputs = []

	for layer_idx in range(num_hidden_layers):

		idx_scope = layer_scope(layer_idx, shared_type)
        # 跨層共享參數
		with tf.variable_scope(idx_scope['layer'], reuse=tf.AUTO_REUSE):
			layer_input = prev_output
            # 共享注意力層的參數
			with tf.variable_scope(idx_scope['attention'], reuse=tf.AUTO_REUSE):
				attention_heads = []
                # 共享全連接層的參數,改爲Pre-LN
				with tf.variable_scope("output", reuse=tf.AUTO_REUSE):
					layer_input_pre = layer_norm(layer_input)

				with tf.variable_scope("self"):
					attention_head = attention_layer(
							from_tensor=layer_input_pre,
							to_tensor=layer_input_pre,
							attention_mask=attention_mask,
							num_attention_heads=num_attention_heads,
							size_per_head=attention_head_size,
							attention_probs_dropout_prob=attention_probs_dropout_prob,
							initializer_range=initializer_range,
							do_return_2d_tensor=True,
							batch_size=batch_size,
							from_seq_length=seq_length,
							to_seq_length=seq_length)
					attention_heads.append(attention_head)

				attention_output = None
				if len(attention_heads) == 1:
					attention_output = attention_heads[0]
				else:
					# In the case where we have other sequences, we just concatenate
					# them to the self-attention head before the projection.
					attention_output = tf.concat(attention_heads, axis=-1)

				# Run a linear projection of `hidden_size` then add a residual
				# with `layer_input`.
                # 共享全連接層的參數
				with tf.variable_scope("output", reuse=tf.AUTO_REUSE):
					attention_output = tf.layers.dense(
							attention_output,
							hidden_size,
							kernel_initializer=create_initializer(initializer_range))
					attention_output = dropout(attention_output, hidden_dropout_prob)

					# attention_output = layer_norm(attention_output + layer_input)
					attention_output = attention_output + layer_input
            # 共享全連接層的參數
			with tf.variable_scope(idx_scope['output'], reuse=tf.AUTO_REUSE):
				attention_output_pre = layer_norm(attention_output)

            # 共享全連接層的參數
			with tf.variable_scope(idx_scope['intermediate'], reuse=tf.AUTO_REUSE):
				intermediate_output = tf.layers.dense(
						attention_output_pre,
						intermediate_size,
						activation=intermediate_act_fn,
						kernel_initializer=create_initializer(initializer_range))

            # 共享全連接層的參數
			with tf.variable_scope(idx_scope['output'], reuse=tf.AUTO_REUSE):
				layer_output = tf.layers.dense(
						intermediate_output,
						hidden_size,
						kernel_initializer=create_initializer(initializer_range))
				layer_output = dropout(layer_output, hidden_dropout_prob)

				# layer_output = layer_norm(layer_output + attention_output)
				layer_output = layer_output + attention_output
				prev_output = layer_output
				all_layer_outputs.append(layer_output)

	if do_return_all_layers:
		final_outputs = []
		for layer_output in all_layer_outputs:
			final_output = bert_utils.reshape_from_matrix(layer_output, input_shape)
			final_outputs.append(final_output)
		return final_outputs
	else:
		final_output = bert_utils.reshape_from_matrix(prev_output, input_shape)
		return final_output

 

3、句間連貫(Inter-sentence coherence loss)

BERT的NSP任務實際上是一個二分類,訓練數據的正樣本是通過採樣同一個文檔中的兩個連續的句子,而負樣本是通過採用兩個不同的文檔的句子。

在ALBERT中,爲了只保留一致性任務去除主題識別的影響,提出了一個新的任務 sentence-order prediction(SOP)。

  • NSP(Next Sentence Prediction):下一句預測, 正樣本=上下相鄰的2個句子,負樣本=隨機2個句子
  • SOP (Sentence ):句子順序預測,正樣本=正常順序的2個相鄰句子,負樣本=調換順序的2個相鄰句子

對於NLI自然語言推理任務。研究發現NSP任務效果並不好,主要原因是因爲其任務過於簡單。NSP其實包含了兩個子任務,主題預測與關係一致性預測,但是主題預測相比於關係一致性預測簡單太多了,因爲只要模型發現兩個句子的主題不一樣就行了,而SOP預測任務能夠讓模型學習到更多的信息。SOP因爲是在同一個文檔中選的,其只關注句子的順序並沒有主題方面的影響。

在create_pretraining_data.py的create_instances_from_document_albert函數中,負例的選取是調換正常順序的兩個句子。

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