機器閱讀理解模型Stanford Attentive Reader原始碼

post200發表於2021-09-09
0 前言

Stanford Attentive Reader是史丹佛在2016年的ACL會議上的《A Thorough Examination of the CNN/Daily Mail Reading Comprehension Task》(,,程式碼要求Python 2.7,Theano >= 0.7,深度學習框架Lasagne 0.2.dev1,程式碼整體風格非常簡潔易懂)釋出的一個機器閱讀理解模型。我們對Stanford Attentive Reader模型的原始碼進行了解析,力求透過分析其原始碼來深入研究一個機器閱讀理解模型是如何工作的。

採用的資料集有兩個:CNN和Daily Mail,下載地址分別為 (546M)和 (1.4G);100維glove詞向量。

1 載入資料load_data

這部分主要是載入訓練集、驗證集以及測試集的CNN和Daily Mail資料,輸入引數主要有三個:

  1. in_file:表示資料檔案地址;
  2. max_example:如果在debug模式下,該值為100,表示只採用資料集中前100個樣本,如果在非debug模式下,該值為None,表示採用資料集中全部資料;
  3. relabeling:bool型,如果要求對資料集中實體類單詞重新編號,則該值為True,若要求重新編號則按照實體類單詞出現的順序進行編號,預設為True。

輸出為文件集合、問題集合和答案集合。

從load_data輸入輸出可看出,其功能主要是分別提取每個資料集樣本中的文件、問題以及答案,並按對應順序放到三個不同的list中,從answer = entity_dict[answer]中可以看出,答案一定是文件或問題中的某個實體類單詞。

原始碼為:

def load_data(in_file, max_example=None, relabeling=True):
    documents = []
    questions = []
    answers = []
    num_examples = 0
    f = open(in_file, 'r')
    while True:
        line = f.readline()
        if not line:
            break
        question = line.strip().lower()
        answer = f.readline().strip()
        document = f.readline().strip().lower()

        if relabeling:
            q_words = question.split(' ')
            d_words = document.split(' ')
            assert answer in d_words

            entity_dict = {}
            entity_id = 0
            for word in d_words + q_words:
                if (word.startswith('@entity')) and (word not in entity_dict):
                    entity_dict[word] = '@entity' + str(entity_id)
                    entity_id += 1

            q_words = [entity_dict[w] if w in entity_dict else w for w in q_words]
            d_words = [entity_dict[w] if w in entity_dict else w for w in d_words]
            answer = entity_dict[answer]

            question = ' '.join(q_words)
            document = ' '.join(d_words)

        questions.append(question)
        answers.append(answer)
        documents.append(document)
        num_examples += 1

        f.readline()
        if (max_example is not None) and (num_examples >= max_example):
            break
    f.close()
    return (documents, questions, answers)

載入訓練集和驗證集資料後,則開始構建單詞字典。

2 單詞字典build_dict

輸入為文件集合和問題集合sentences;最大詞數max_words,預設為50000。

輸出即為文件和問題集合的單詞字典word_dictkey為單詞,value為按照詞頻排序的序號。

build_dict使用了Python集合類的Counter子類來完成文件與問題的單詞統計,Counter可以便捷和快速地進行雜湊的物件計數,並返回一個無序的容器,元素被作為字典的key儲存,它們的計數作為字典的value儲存。然後使用most_common方法從多到少返回一個有前max_words多的元素的列表ls,相同數量的元素次序任意。最後返回一個由ls組成的單詞字典,值得注意的是,ls列表的資料從字典的第三個開始填充,單詞字典第一個為“無法識別的詞”即字典第一個元素為”:0,第二個為分隔符即”|||”:1

原始碼為:

def build_dict(sentences, max_words=50000):
    word_count = Counter()
    for sent in sentences:
        for w in sent.split(' '):
            word_count[w] += 1

    ls = word_count.most_common(max_words)

    return {w[0]: index + 2 for (index, w) in enumerate(ls)}

把機器閱讀理解問題看作一個分類問題

entity_markers = list(set([w for w in word_dict.keys() if w.startswith('@entity')] + train_examples[2]))
entity_markers = [''] + entity_markers
entity_dict = {w: index for (index, w) in enumerate(entity_markers)}
args.num_labels = len(entity_dict)
3 embedding字典

根據單詞字典word_dict來構建嵌入層gen_embeddingsgen_embeddings的輸入包含四個引數:

  1. word_dict:表示單詞字典;
  2. dim:表示詞向量維度;
  3. in_file:如果為None,則表示不使用預訓練好的詞向量來初始化嵌入層,這時使用第四個引數的引數初始化方法lasagne.init.Uniform(),即對嵌入層權值進行均勻分佈初始化,如果不為None,則應該輸入預訓練詞向量的檔案地址,這裡預設使用的是100維的glove詞向量來初始化嵌入層,值得注意的是,類似於“@entity1”這種單詞顯然不包含在glove詞向量中,那麼對於這類詞也採用均勻分佈初始化的方法;
  4. init:預設為使用深度學習框架lasagne的均勻分佈初始化方法。

gen_embeddings的輸出即為embeddings字典,key表示word_dict中對應詞的value,即按照詞頻排序的序號,value為一個列表,表示key的詞向量,詞向量中的值為float型別。

原始碼為:

def gen_embeddings(word_dict, dim, in_file=None,
                   init=lasagne.init.Uniform()):

    num_words = max(word_dict.values()) + 1
    embeddings = init((num_words, dim))

    if in_file is not None:
        pre_trained = 0
        for line in open(in_file).readlines():
            sp = line.split()
            if sp[0] in word_dict:
                pre_trained += 1
                embeddings[word_dict[sp[0]]] = [float(x) for x in sp[1:]]
    return embeddings
4 Encode層

將構建的embeddings字典來初始化lasagne.layers.EmbeddingLayer的權重,這裡預設使用雙向GRU(lasagne.layers.GRULayer)來構建文件和問題的encode層,在rnn_layer中的引數backwards表示了是否使用雙向GRU。對於文件的encode,即name=‘d’stack_rnn,其中引數only_return_final表示是否要只返回GRU的最終輸出,args.att_func預設為bilinear,所以only_return_finalFalse,即對於文件的encode是使用GRU的正向與反向的隱藏層輸出來構建的;對於問題的encode,即name=‘q’stack_rnnonly_return_final=True代表問題的encode是使用GRU的正向與反向的最終輸出來構建的。

原始碼為:

l_in1 = lasagne.layers.InputLayer((None, None), in_x1)
l_mask1 = lasagne.layers.InputLayer((None, None), in_mask1)
l_emb1 = lasagne.layers.EmbeddingLayer(l_in1, args.vocab_size, args.embedding_size, W=embeddings)

l_in2 = lasagne.layers.InputLayer((None, None), in_x2)
l_mask2 = lasagne.layers.InputLayer((None, None), in_mask2)
l_emb2 = lasagne.layers.EmbeddingLayer(l_in2, args.vocab_size, args.embedding_size, W=l_emb1.W)

network1 = nn_layers.stack_rnn(l_emb1, l_mask1, args.num_layers, args.hidden_size,
                               grad_clipping=args.grad_clipping,
                               dropout_rate=args.dropout_rate,
                               only_return_final=(args.att_func == 'last'),
                               bidir=args.bidir,
                               name='d',
                               rnn_layer=args.rnn_layer)

network2 = nn_layers.stack_rnn(l_emb2, l_mask2, args.num_layers, args.hidden_size,
                               grad_clipping=args.grad_clipping,
                               dropout_rate=args.dropout_rate,
                               only_return_final=True,
                               bidir=args.bidir,
                               name='q',
                               rnn_layer=args.rnn_layer)

stack_rnn封裝了lasagne.layers.GRULayer,對外使用stack_rnn來構建雙向GRU。

def stack_rnn(l_emb, l_mask, num_layers, num_units,
              grad_clipping=10, dropout_rate=0.,
              bidir=True,
              only_return_final=False,
              name='',
              rnn_layer=lasagne.layers.LSTMLayer):

    def _rnn(backwards=True, name=''):
        network = l_emb
        for layer in range(num_layers):
            if dropout_rate > 0:
                network = lasagne.layers.DropoutLayer(network, p=dropout_rate)
            c_only_return_final = only_return_final and (layer == num_layers - 1)
            network = rnn_layer(network, num_units,
                                grad_clipping=grad_clipping,
                                mask_input=l_mask,
                                only_return_final=c_only_return_final,
                                backwards=backwards,
                                name=name + '_layer' + str(layer + 1))
        return network

    network = _rnn(True, name)
    if bidir:
        network = lasagne.layers.ConcatLayer([network, _rnn(False, name + '_back')], axis=-1)
    return network
5 Attention層

從Encode層得到文件的上下文表示network1以及問題的上下文表示network2,使用一個雙線性函式nn_layers.BilinearAttentionLayer作為network1network2的匹配函式。

att = nn_layers.BilinearAttentionLayer([network1, network2], args.rnn_output_size, mask_input=l_mask1)

核心計算程式碼為:

M = T.dot(inputs[1], self.W).dimshuffle(0, 'x', 1)
alpha = T.nnet.softmax(T.sum(inputs[0] * M, axis=2))
if len(inputs) == 3:
    alpha = alpha * inputs[2]
    alpha = alpha / alpha.sum(axis=1).reshape((alpha.shape[0], 1))
return T.sum(inputs[0] * alpha.dimshuffle(0, 1, 'x'), axis=1)
6 全連線層Dense
network = lasagne.layers.DenseLayer(att, args.num_labels, nonlinearity=lasagne.nonlinearities.softmax)

其中的att為雙線性函式的輸出,args.num_labels為資料集中各類@entity,可參考單詞字典build_dict,使用非線性啟用函式softmax,得到每個@entity的為答案的機率。

模型使用SGD最佳化函式: updates = lasagne.updates.sgd(loss, params, args.learning_rate)

損失函式:lasagne.objectives.categorical_crossentropy(train_prediction, in_y),計算正確答案和預測答案的交叉熵。

7 可能遇到的問題
ImportError: cannot import name downsample

解決方法:

這主要是因為你安裝的時候直接使用了pip install lasagne,這樣安裝的是lasagneV0.1,而這個模型需要的是lasagneV0.2,檢視官網發現需使用pip install --upgrade 來安裝lasagneV0.2

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2249/viewspace-2801164/,如需轉載,請註明出處,否則將追究法律責任。

相關文章