對於機器學習保險行業問答開放數據集DeepQA-1的詳細註解(一)

        首先感謝https://github.com/chatopera/insuranceqa-corpus-zh作者的辛苦付出,構建了保險行業的中文語料庫,並且提供了一個訓練以及測試例程,解決了很多人的燃眉之急,可以說是雪中送炭了。

        稍顯遺憾的是,項目中對於代碼的註釋非常少,https://blog.csdn.net/samurais/article/details/77036461https://blog.csdn.net/samurais/article/details/77193529這兩篇文章中雖然在宏觀上給予了一定註釋說明,但其程度遠遠不夠,初次接觸者需要花費很多精力去研究探究,筆者就經過了這一“痛苦”的過程。爲了使後來者免於受此“煎熬”,可以快速理解並且上手,在本文中對於項目源碼進行詳細註釋。

        閒言少敘,書歸正傳。

        https://github.com/chatopera/insuranceqa-corpus-zh/blob/release/deep_qa_1/network.py

        network.py中的源碼較多較長,分段來進行解讀。先貼出第一部分代碼。

 

class NeuralNetwork():

    def __init__(self, hidden_layers = [100, 50], 

                 question_max_length = 20, 

                 utterance_max_length = 99, 

                 lr = 0.001, epoch = 10, 

                 batch_size = 100,

                 eval_every_N_steps = 500):

        '''

        Neural Network to train question and answering model

        '''

        self.input_layer_size = question_max_length + utterance_max_length + 1 # 1 is for <GO>

        self.output_layer_size = 2 # just the same shape as labels

        self.layers = [self.input_layer_size] + hidden_layers + [self.output_layer_size] # [2] is for output layer

        self.layers_num = len(self.layers)

        self.weights = [np.random.randn(y, x) for x,y in zip(self.layers[:-1], self.layers[1:])]

        self.biases = [np.random.randn(x, 1) for x in self.layers[1:]]

        self.epoch = epoch

        self.lr = lr

        self.batch_size = batch_size

        self.eval_every_N_steps = eval_every_N_steps

        self.test_data = corpus.load_test()

        (1) input_layer_size:

        self.input_layer_size = question_max_length + utterance_max_length + 1 # 1 is for <GO>

        根據作者的說明,在預處理時,在詞彙表(vocab)中添加輔助Token: <PAD>, <GO>. 假設x是問題序列,是u回覆序列,輸入序列可以表示爲:

        (Q1, Q2, ......, Qquestion_max_length, <GO>, U1, U2, ......, Uutterance_max_length)

其中question_max_length代表模型中問題的最大長度,utterance_max_length代表模型中回覆的最大長度。

因此,input_layer_size表示的是輸入層的長度,即問題最大長度+分隔符+回覆最大長度,默認爲20+1+99=120。

        (2)output_layer_size:

        self.output_layer_size = 2 # just the same shape as labels

        根據作者的說明,回覆可能是正例,也可能是負例,正例標爲[1,0],負例標爲[0,1]。

因此,output_layer_size表示的是輸出層的長度,值爲2。

        (3)layers:

        self.layers = [self.input_layer_size] + hidden_layers + [self.output_layer_size] # [2] is for output layer

        根據作者的說明,hidden_layers表示隱含層,比如[100, 50]代表兩個隱含層,分別有100,50個神經元。

因此,layers實際上表示的是層的佈局,默認爲[120, 100, 50, 2],意義爲[輸入層共120個神經元,隱含層1共100個神經元,隱含層2共50個神經元,輸出層共2個神經元]。

        (4)layers_num:

        self.layers_num = len(self.layers)

        layers_num表示層的數量,1個輸入層+2個隱含層+1個輸出層共4個層,因此值爲4。

        (5)weights:

        self.weights = [np.random.randn(y, x) for x,y in zip(self.layers[:-1], self.layers[1:])]

        必須重點講一下weights以及接下來的biases,很多人前邊還能看懂,到這裏就有點懵了。可以看到,它可以拆開爲幾部分,下邊分別針對每一小部分進行講解。

        self.layers[:-1]:根據上邊的分析,實際上就是不包括輸出層,默認值爲[120, 100, 50]。

        self.layers[1:]:實際上就是不包含輸入層,默認值爲[100, 50, 2]。

        zip(self.layers[:-1], self.layers[1:])]:zip函數的功能自行查閱,這裏僅給出結果:[(120, 100), (100, 50), (50, 2)]。

實際上分別表示了輸入層到隱含層1,隱含層1到隱含層2,隱含層2到輸出。

        np.random.randn(y, x):

numpy.random.randn(d0,d1,…,dn)

  • randn函數返回一個或一組樣本,具有標準正態分佈。
  • dn表格每個維度
  • 返回值爲指定維度的array

這裏實際上分別返回了100行120列,50行100列,2行50列的array。

        到這裏,weights就已經明確了,是[array(100行120列), array(50行100列), array(2行50列)]。

        (6)biases:

        self.biases = [np.random.randn(x, 1) for x in self.layers[1:]]

        self.layers[1:]:實際上就是不包含輸入層,默認值爲[100, 50, 2]。

        np.random.randn(x, 1):

        實際上分別返回了100行1列,50行1列,2行1列的array。

        到這裏,biases也已經明確了,是[array(100行1列), array(50行1列), array(2行1列)]。

        (7)epoch

        self.epoch = epoch

        訓練輪數,默認值爲10。

        (8)lr

        self.lr = lr

        學習率,默認值爲0.001。

        (9)batch_size

        self.batch_size = batch_size

        每一批的樣本數,默認值爲100。

        (10)eval_every_N_steps:

        self.eval_every_N_steps = eval_every_N_steps

        訓練到第幾次時進行eval,默認值爲500,每500次進行一次驗證測試。

        (11)test_data:

        self.test_data = corpus.load_test()

        這個要深入講一下了。涉及到加載數據部分的內容。

        load_test()爲corpus的方法,corpus從何處來?在network.py中有這樣一句:import deep_qa_1.data as corpus。也就是說corpus實際上就是deep_qa_1.data。那麼deep_qa_1.data又在何處?其實deep_qa_1就是network.py的父目錄,即network.py在deep_qa_1文件夾下。這樣也就不難推斷出deep_qa_1.data其實是指與network.py同在deep_qa_1文件夾下的data.py文件。

        下邊貼出data.py中load_test()部分代碼:

def load_test(question_max_length = 20, utterance_max_length = 99):

    '''

    load test data

    '''

    result = []

    for o in _test_data:

        x = pack_question_n_utterance(o['question'], o['utterance'], question_max_length, utterance_max_length)

        y_ = o['label']

        assert len(x) == utterance_max_length + question_max_length + 1, "Wrong length afer padding"

        assert VOCAB_GO_ID in x, "<GO> must be in input x"

        assert len(y_) == 2, "desired output."

        result.append((x, y_))

    return result

        代碼一開始又遇到問題了。_test_data從何處來?又是什麼值?

        在data.py中有這樣一句:_test_data = insuranceqa.load_pairs_test()。_test_data是insuranceqa.load_pairs_test()的返回值。歷史又重複了。insuranceqa在何處?上邊有一句:import insuranceqa_data as insuranceqa。insuranceqa_data又在何處?這就比較複雜了。根據作者的說明,在運行本工程的代碼前要預先運行pip install insuranceqa_data命令下載語料庫。運行此命令後語料庫存放於XXX\Anaconda3\Lib\site-packages\路徑下(筆者是通過anaconda下載的),也就是系統的庫路徑。insuranceqa_data文件夾中有__init__.py文件,其中load_pairs_test()相關代碼如下:

'''
pair data are segmented and labeled after pool data
'''
PAIR_TEST_DATA = os.path.join(curdir, 'pairs','iqa.test.json.gz')
PAIR_VALID_DATA = os.path.join(curdir, 'pairs','iqa.valid.json.gz')
PAIR_TRAIN_DATA = os.path.join(curdir, 'pairs','iqa.train.json.gz')
PAIR_VOCAB_DATA = os.path.join(curdir, 'pairs', 'iqa.vocab.json.gz')

def load_pairs_vocab():
    '''
    Load vocabulary data
    '''
    return load(PAIR_VOCAB_DATA)

def load_pairs_test():
    return load(PAIR_TEST_DATA)

def load_pairs_valid():
    return load(PAIR_VALID_DATA)

def load_pairs_train():
    return load(PAIR_TRAIN_DATA)

def __test_pair_test():
    d = load_pairs_test()
    for x in d:
        print("index %s question %s utterance %s label %s" % (x['qid'], x['question'], x['utterance'], x['label']))
        break

        可以看到,load_pairs_test()實際調用了load('./pairs/iqa.test.json.gz'),load()實現也在__init__.py文件中,就在上邊,源碼如下:

def load(data_path):
    print("\ndata_path: %s" %data_path)
    if not os.path.exists(data_path):
        # download all pair data
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.test.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.test.json.gz", out = os.path.join(curdir, 'pairs'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.train.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.train.json.gz", out = os.path.join(curdir, 'pairs'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.valid.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.valid.json.gz", out = os.path.join(curdir, 'pairs'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.vocab.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pairs/iqa.vocab.json.gz", out = os.path.join(curdir, 'pairs'))

        # download all pool data
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/answers.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/answers.json.gz", out = os.path.join(curdir, 'pool'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/test.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/test.json.gz", out = os.path.join(curdir, 'pairs'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/train.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/train.json.gz", out = os.path.join(curdir, 'pairs'))
        print("\n [insuranceqa_data] downloading data %s ... \n" % "https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/valid.json.gz")
        wget.download("https://github.com/Samurais/insuranceqa-corpus-zh/raw/release/corpus/pool/valid.json.gz", out = os.path.join(curdir, 'pairs'))
        
    with gzip.open(data_path, 'rb') as f:
        #data = json.loads(f.read())
        data = json.loads(f.read().decode("utf-8")) #phph
        return data

        函數功能一目瞭然。如果當前路徑中不存在目標文件,則通過網絡下載。如果目標文件存在,則讀入其內容到data中並返回。由於目標文件爲.json格式的文件,因此通過json.loads()讀取。

        綜上,_test_data = insuranceqa.load_pairs_test() 實現的功能是:將XXX\Anaconda3\Lib\site-packages\pairs\insuranceqa_data\iqa.test.json.gz文件的內容讀取到_test_data中。iqa.test.json.gz文件的原始內容如下(截取一小部分):

[
{"qid":"344","question":[2462,3206,8878,17449,11331],"utterance":[8878,17449,11331,3206,9757,21338,4757,11331,13381,10310,10114,6069,5231,13346,4185,12750,6568,5425,3206,10114,11705,6194,13402,23991,11273,8231,490,10299,9757,9843,18157,13334,23611,1907,10099,6568,7344,1704,16818,2311,6683,12268,7197,9757,11869,23800,10617,1134,22430,23810,9843,14297,10227,14005,5526,12360,13467,16917,22724,22086,24346,20333,12268],"label":[1,0]},

……

]

        思路回到load_test()代碼中來。看這句代碼:for o in _test_data:。_test_data經過上邊分析已經知道了,是一個列表,列表中的每一項是一個字典。因此,o就表示了這一個個字典項。

        再往下看,x = pack_question_n_utterance(o['question'], o['utterance'], question_max_length, utterance_max_length)。

源碼如下:

def pack_question_n_utterance(q, u, q_length = 20, u_length = 99):

    '''

    combine question and utterance as input data for feed-forward network

    '''

    assert len(q) > 0 and len(u) > 0, "question and utterance must not be empty"

    q = padding(q, VOCAB_PAD_ID, q_length)

    u = padding(u, VOCAB_PAD_ID, u_length)

    assert len(q) == q_length, "question should be pad to q_length"

    assert len(u) == u_length, "utterance should be pad to u_length"

    return q + [VOCAB_GO_ID] + u

        這個函數並不難理解。以VOCAB_PAD_ID(VOCAB_PAD_ID = vocab_size+1)分別將q和u補齊爲q_length和u_length(不到q_length或u_length的補齊到q_length或u_length,超過的截取前q_length或u_length)。之後返回q+[VOCAB_GO_ID(VOCAB_GO_ID = vocab_size+2)]+u。其實就是將問題+分隔符+回覆放在一起,組成一個列表。

        y_ = o['label']

        這句代碼的意思就是將字典項o中的標籤'label'對應的值取出來賦給y_。

        result.append((x, y_))

        這句代碼的意思是每次將取到的x和y_添加到result列表中。

        綜上,可以總結load_test()的作用了:依次將iqa.test.json.gz中的每個字典項中的問題與答覆組合,生成一個列表,保存在x中;將iqa.test.json.gz中的每個字典項中的判定結果(正例或負例)取出,保存在y_中;再將x與y_組合成一個元組,添加到result[]中。依次添加,直到全部的數據項被添加完。

        至此,NeuralNetwork類的__init__()就全部分析完了。

        欲知後事如何,且聽下回分解。

 

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