对于机器学习保险行业问答开放数据集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__()就全部分析完了。

        欲知后事如何,且听下回分解。

 

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