深度学习–>NLP–>RNNLM实现

释放双眼,带上耳机,听听看~!

本篇博文将详细总结$RNNLM$ 的原理以及如何在$tensorflow$ 上实现$RNNLM$。

我们要实现的网络结构如下:

数据预处理

创建$vocab$

分词:

将句子中的每个单词以空格,符号分开,形成一个单词列表


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1def blank_tokenizer(sentence):
2    ##以空格对句子进行切分
3    return sentence.strip().split()
4
5def basic_tokenizer(sentence):
6    '''
7    _WORD_SPLIT=re.compile(b"([.,!?\"':;)(])")
8    首先以空格对句子进行切分,然后再以标点符号切分,切分出一个个词,然后词列表
9    '''
10    words=[]
11    for space_separated_fragment in sentence.strip().split():
12        words.extend(_WORD_SPLIT.split(space_separated_fragment))
13    return [w for w in words if w]
14

对单词列表添加特殊词汇:

  • $\_PAD$ 填充词汇
  • $\_GO$ 句子开始
  • $\_EOS$ 句子结束
  • $\_UNK$ 未知词(低频的词替换为UNK)

如$"i\ love\ you"$ 创建成$vocab$ 时,应为:
$"\_GO\ i\ love\ you\ \_EOS$

将单词替换成数字

对$vocab$ 内的单词按出现频率排序,用其索引代替单词。
如:1 3 102 3424 2


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
1def create_vocabulary(vocabulary_path,data_paths,max_vocabulary_size,tokenizer=None,normalize_digits=False):
2    '''
3    读取data_paths路径下的文件,并且一行行的读取,对每句做分词处理,得到每个词的频率,然后存储频率最高的max_vocabulary_size的词,存入vocabulary_path
4    :param vocabulary_path: 新建的文件夹,将返回的结果写入
5    :param data_paths:存储原始文件的路径
6    :param max_vocabulary_size:最大存储的词的个数
7    :param tokenizer:对句子做分词处理
8    :param normalize_digits:是否对句子中的数字以0替换
9    :return:返回的vocabulary_path中一行一个词
10    '''
11    if not gfile.Exists(vocabulary_path):
12        print ("Create vocabulary %s from data %s" %(vocabulary_path,",".join(data_paths)))
13        vocab={}
14        for data_path in data_paths:
15            with gfile.GFile(data_path,mode='rb') as f:
16                print (data_path)
17                counter=0
18                for line in f:
19                    counter+=1
20                    if counter%100000==0:
21                        print ("processing line %d" %counter)
22                    #Converts either bytes or unicode to bytes, using utf-8 encoding for text.
23                    line=tf.compat.as_bytes(line)
24                    tokens=tokenizer(line) if tokenizer else blank_tokenizer(line)
25                    for w in tokens:
26                        #replace digit to 0
27                        #_DIGIT_RE=re.compile(br"\d")
28                        word=_DIGIT_RE.sub(b"0",w) if normalize_digits else w
29                        if word in vocab:
30                            vocab[word]+=1
31                        else:
32                            vocab[word]=1
33                print (len(vocab))
34        # _START_VOCAB=[_PAD,_GO,_EOS,_UNK]
35        # 按词频率降序排序
36        vocab_list=_START_VOCAB+sorted(vocab,key=vocab.get,reverse=True)
37        if len(vocab_list)>max_vocabulary_size:
38            vocab_list=vocab_list[:max_vocabulary_size]##只取出现频率最高的max_vocabulary_size
39        with gfile.GFile(vocabulary_path,mode='rb') as vocab_file:
40            for w in vocab_list:
41                vocab_file.write(w+b'\n')##注意将分出的单词一行一行的写入到vocabulary_path
42
43
44def initialize_vocabulary(vocabulary_path):
45    '''
46    :param vocabulary_path:一行一个词
47    读取vocabulary_path文件内每行的每个单词到rev_vocab,然后枚举rev_vocab,然后字典列表[(word,index)]
48    :return:
49    '''
50    if gfile.Exists(vocabulary_path):
51        rev_vocab=[]
52        with gfile.GFile(vocabulary_path,mode='rb') as f:
53            rev_vocab.extend(f.readlines())
54        rev_vocab=[tf.compat.as_bytes(line.strip()) for line in rev_vocab]
55        vocab=dict([(x,y) for (y,x) in enumerate(rev_vocab)])
56        return vocab,rev_vocab
57    else:
58        raise ValueError("Vocabulary file % not found",vocabulary_path)
59
60
61def sentence_to_token_ids(sentence,vocabulary,tokenizer=None,normalize_digits=False,with_start=True,with_end=True):
62    '''
63    对sentence句子进行分词处理,并且用其在vocabulary中的索引代替其词,并且加上GO_ID,EOS_ID,UNK等特殊数字,返回数字列表。
64    :param sentence:需要分词的句子
65    :param vocabulary:字典列表[(word,index)]
66    :param tokenizer:分词处理方法
67    :param normalize_digits:是否将句子中数字用0替换
68    :param with_start:是否在句头带上GO_ID
69    :param with_end:是否在句尾带上EOS_ID
70    :return:
71    '''
72    if tokenizer:
73        #对sentence进行分词处理
74        words=tokenizer(sentence)
75    else:
76        # 对sentence进行分词处理
77        words=basic_tokenizer(sentence)
78    if not normalize_digits:
79        #在vocabulary中找到Word,返回其index,否则以UNK_ID代替返回
80        #UNK_ID=3
81        ids=[vocabulary.get(w,UNK_ID) for w in words]
82    else:
83        #_DIGIT_RE=re.compile(br"\d")
84        ids=[vocabulary.get(_DIGIT_RE.sub(b"0",w),UNK_ID) for w in words]
85
86    if with_start:
87        ids=[GO_ID]+ids
88    if with_end:
89        ids=ids+[EOS_ID]
90    return ids
91
92
93def data_to_token_ids(data_path,target_path,vocabulary_path,tokenizer=None,normalize_digits=False,with_go=True,with_end=True):
94    '''
95    读取data_path路径下的文件内容,读取其每一行,喂给sentence_to_token_ids方法处理,得到所有词的索引列表,然后存入到target_path
96    :param data_path:原文件
97    :param target_path:原文件处理完要存入的地址
98    :param vocabulary_path:一行一个词
99    :param tokenizer:
100    :param normalize_digits:
101    :param with_go:
102    :param with_end:
103    :return:
104    '''
105    if not gfile.Exists(target_path):
106        print ("Tokenizing data in %s" % data_path)
107        vocab,_=initialize_vocabulary(vocabulary_path)
108        #vocab是字典列表[(word,index)]
109        with gfile.GFile(data_path,mode='rb') as data_file:
110            with gfile.GFile(target_path,mode='w') as tokens_file:
111                counter=0
112                for line in data_file:
113                    counter+=1
114                    if counter%100000==0:
115                        print ("tokenizing line %d" % counter)
116                    token_ids=sentence_to_token_ids(tf.compat.as_bytes(line),vocab,tokenizer,normalize_digits)
117                    tokens_file.write(" ".join([str(tok) for tok in token_ids])+'\n')#注意一行一句话
118

训练RNN模型

$Mini-batch\ Gradient\ Descent$ 梯度下降法

适当的条件更新$learning\ rate\ η$,直到收敛。
适当的条件:
每处理了一半的训练数据,就去验证集 计算$perplexity$

  • 如果$perplexity$ 比上次下降了,保持$learning\ rate$不变, 记录下现在最好的参数。
  • 否则, $learning\ rate *= 0.5$ 缩小一半。

如果连续10次$learning\ rate$ 没有变,就停止训练。

  1. 读取训练数据 $train$ 和验证数据$dev$
  2. 建立模型; $patience = 0$
  3. $while$

从数据中随机取$m$ 个句子进行训练
到达半个$epoch$,计算$ppx(dev)$
比之前降低:更新$best\ parameters$,$patience =0$
比之前升高:$learning\ rate$ 减半,$patience +=1$
$if\ (patience>10): break$

$mini-batch$ 在$RNN$ 上问题

句子的长度不一样

解决方法:句子的长度不一样: 增加$padding$

$loss$ 增大了

$$loss=logP(I) + logP(like) + logP(it)+logP(.)+logP(\_EOS)+logP(YES)+logP(\_EOS)+logP(\_PAD)+logP(\_PAD)+logP(\_PAD)$$

解决方法:乘以一个0/1 mask矩阵

$LOSS = [[logP(I), logP(like), logP(it), logP(.), logP(\_EOS)], [logP(YES),logP(\_EOS),logP(\_PAD),logP(\_PAD),logP(\_PAD)]] * [[1,1,1,1,1], [1,1,0,0,0]] = logP(I) + logP(like) + logP(it)+logP(.)+logP(\_EOS) +logP(YES)+logP(\_EOS)$

效率过低问题

随之而来另外一个问题,我们在增加$padding$ 填充时,以什么样的标准长度进行填充?以所有句子中最长长度进行填充?

例如:我们有长度为10的句子有1101句,长度为11的句子有1226句,长度为81的只有一句,长度为82的也只有1句,那么我们尝试将所有句子补齐到82个字。

  • 实际计算了(1101++1226+1+1) * 82 = 190978 步
  • 有效的步数:1101*10 +1226 * 11 + 1* 81+ 1*82 = 24659
  • 利用率: 12.9% 浪费!

解决低效问题
将句子分成两组, 一组补齐到11,一组补齐到82,相当于建两个RNN,一个11步,另外一个82步。

  • (1101+1226) * 11 + (1+1)*82 = 25761
  • 利用率: 24659 / 25761 = 95.7%

当然也可以建四个RNN,分别为11步,10步,81步,82步,这样效率就到达100%了。但是显然四个RNN训练比较耗时耗存。

显然,这就有一个问题了,该如何决定分组个数?该如何决定每组的应补齐的步长。

best_buckets问题

这里采用一种贪心算法,贪心的最后结果可能不是全局最优,但肯定不会太差。

我们以下为例:
$length\_array$:表示所有句子长度的列表。
$length\_array = [1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,4,4]$

$max\_buckets$:表示计划分的组数
$max\_buckets = 3$

$max\_length$:表示最长的句子长度
$max\_length = 4$

$running\_sum$:元祖列表形式。表示长度小于等于1的有5句,长度小于等于有15句,….
$running\_sum = [(1,5),(2,15),(3,18),(4,20)]$

下面是尝试分组:
①:不作分组,相当于只分一组。
$running\_sum = [(1,5),(2,15),(3,18),(4,20)]$
灰色面积是 有效计算步数
空白面积是 无效计算步数

横坐标:$running\_sum$ 所有元组的第一个数。
纵坐标:$running\_sum$ 所有元组的第二个数。

由图可以看出这种分组方式效率较低。

②分为两组。
如果buckets = [2,4];
实际 = 红框 – 红色区域
红色区域:在当前这种分组下,可以去掉的无效计算。

如果buckets = [3,4]

如果buckets = [1,4]

比较以上三种二分方式,得出以句子长度为2划分方式效率最高。然后我们再尝试在这中最优二分划分方式基础上再进行划分。

③分为三组。在buckets = [2,4]基础上载进行划分分组。
如果buckets = [2,4,3]
实际 = 红框 – 红色区域
红色区域:在当前这种分组下,可以去掉的无效计算。

buckets = [2,4,1]

比较以上两种三分组划分方式,显然最好的buckets = [1,2,4]。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
1def calculate_buckets(length_array, max_length, max_buckets):
2    '''
3
4    :param length_array:所有句子的长度列表[1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,4,4]
5    :param max_length:最长句子的长度4
6    :param max_buckets:分为几个组
7    :return:
8    '''
9    d = {}
10    for length in length_array:
11        if not length in d:
12            d[length] = 0
13        d[length] += 1
14
15    #dd:[(句子长度,该长度出现次数)]
16    dd = [(x, d[x]) for x in d]
17    dd = sorted(dd, key=lambda x: x[0])##以长度升序排序
18
19    #计算running_sum
20    running_sum = []
21    s = 0
22    for l, n in dd:
23        s += n
24        running_sum.append((l, s))#running_sum = [(1,5),(2,15),(3,18),(4,20)]
25
26    def best_point(ll):
27        ## ll即running_sum:[(句子长度,小于等于该长度出现次数)]
28        #找出最大可以去掉的无效面积
29        index = 0
30        maxv = 0
31        base = ll[0][1]
32        for i in xrange(len(ll)):
33            l, n = ll[i]
34            v = (ll[-1][0] - l) * (n - base)
35            if v > maxv:
36                maxv = v
37                index = i
38        return index, maxv
39
40    def arg_max(array, key):
41        # 找出最大可以去掉的无效面积
42        maxv = -10000
43        index = -1
44
45        for i in xrange(len(array)):
46            item = array[i]
47            v = key(item)
48            if v > maxv:
49                maxv = v
50                index = i
51        return index
52
53    end_index = 0
54    for i in xrange(len(running_sum) - 1, -1, -1):
55        if running_sum[i][0] <= max_length:
56            end_index = i + 1
57            break
58
59    # print "running_sum [(length, count)] :"
60    # print running_sum
61
62    if end_index <= max_buckets:
63        buckets = [x[0] for x in running_sum[:end_index]]
64    else:
65        '''
66        不断递归的以可以去掉最大的无效面积为原则不断的划分
67        '''
68        buckets = []
69        # (array,  maxv, index)
70        states = [(running_sum[:end_index], 0, end_index - 1)]#[([(1,5),(2,15),(3,18),(4,20)],0,end_index-1)],列表长度为1
71        while len(buckets) < max_buckets:
72            index = arg_max(states, lambda x: x[1])##最大可以去掉的无效面积对应的索引
73            state = states[index]
74            del states[index]
75            # split state
76            array = state[0]
77            split_index = state[2]
78            buckets.append(array[split_index][0])
79            array1 = array[:split_index + 1]
80            array2 = array[split_index + 1:]
81            if len(array1) > 0:
82                id1, maxv1 = best_point(array1)
83                states.append((array1, maxv1, id1))
84            if len(array2) > 0:
85                id2, maxv2 = best_point(array2)
86                states.append((array2, maxv2, id2))
87    return sorted(buckets)
88
89def split_buckets(array, buckets, withOrder=False):
90    """
91
92    :param array:句子的集合
93    :param buckets:上面计算出来的最优划分组
94    :param withOrder:
95    :return:d[buckets_id,属于该组的items];order((buckets_id,len(d[buckets_id]) - 1))
96    """
97    order = []
98    d = [[] for i in xrange(len(buckets))]
99    for items in array:
100        index = get_buckets_id(len(items), buckets)
101        if index >= 0:
102            d[index].append(items)
103            order.append((index, len(d[index]) - 1))
104    return d, order
105
106
107def get_buckets_id(l, buckets):
108    '''
109    将某句子长度划到对应的分组中,返回该句子的组号
110    :param l:
111    :param buckets:
112    :return:
113    '''
114    id = -1
115    for i in xrange(len(buckets)):
116        if l <= buckets[i]:
117            id = i
118            break
119    return id
120

我们计算处buckets,需要对其中不同的bucket建立不同步长的RNN模型。并且在对不同模型的loss求和。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
1    def model_with_buckets(self, inputs, targets, weights,
2                           buckets, cell, dtype,
3                           per_example_loss=False, name=None, devices=None):
4
5        all_inputs = inputs + targets + weights
6
7        losses = []
8        hts = []
9        logits = []
10        topk_values = []
11        topk_indexes = []
12
13        # initial state
14        with tf.device(devices[1]):
15            init_state = cell.zero_state(self.batch_size, dtype)
16
17        # softmax
18        with tf.device(devices[2]):
19            softmax_loss_function = lambda x, y: tf.nn.sparse_softmax_cross_entropy_with_logits(logits=x, labels=y)
20
21        with tf.name_scope(name, "model_with_buckets", all_inputs):
22            for j, bucket in enumerate(buckets):
23                with variable_scope.variable_scope(variable_scope.get_variable_scope(), reuse=True if j > 0 else None):
24
25                    # ht
26                    with tf.device(devices[1]):
27                        _hts, _ = tf.contrib.rnn.static_rnn(cell, inputs[:bucket], initial_state=init_state)
28                        hts.append(_hts)
29
30                    # logits / loss / topk_values + topk_indexes
31                    with tf.device(devices[2]):
32                        _logits = [tf.add(tf.matmul(ht, tf.transpose(self.output_embedding)), self.output_bias) for ht
33                                   in _hts]
34                        logits.append(_logits)
35
36                        if per_example_loss:
37                            losses.append(sequence_loss_by_example(
38                                logits[-1], targets[:bucket], weights[:bucket],
39                                softmax_loss_function=softmax_loss_function))
40
41                        else:
42                            losses.append(sequence_loss(
43                                logits[-1], targets[:bucket], weights[:bucket],
44                                softmax_loss_function=softmax_loss_function))
45
46                        topk_value, topk_index = [], []
47
48                        for _logits in logits[-1]:
49                            value, index = tf.nn.top_k(tf.nn.softmax(_logits), self.topk_n, sorted=True)
50                            topk_value.append(value)
51                            topk_index.append(index)
52                        topk_values.append(topk_value)
53                        topk_indexes.append(topk_index)
54
55        self.losses = losses
56        self.hts = hts
57        self.logits = logits
58        self.topk_values = topk_values
59        self.topk_indexes = topk_indexes
60
61

如何随机选择m个数据?

inputs, outputs, weights, _ = self.model.get_batch(self.data_set, bucket_id)

  1. 先随机一个buckets

  2. 再随机取m个数据

  3. 将m个数据变成一个矩阵,加上padding


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
1    def get_batch(self, data_set, bucket_id, start_id=None):
2        '''
3        :param data_set:[ [ s1,s1,s1,s1,s1] , [s2,s2,s2,s2,s2,s2,s2,s2,s2,s2],
4[s3,s3,s3,s4,s4] ],注意每个字母表示一个句子。
5        :param bucket_id:第几个分组
6        :param buckets:[1,2,4]
7        :param batch_size
8        :param start_id:
9        :return:
10        '''
11        length = self.buckets[bucket_id]##当前组的句子长度,即需要补齐的长度
12
13        input_ids, output_ids, weights = [], [], []
14
15        for i in xrange(self.batch_size):##获取batch_size个句子。
16            if start_id == None:
17                word_seq = random.choice(data_set[bucket_id])
18            else:
19                if start_id + i < len(data_set[bucket_id]):
20                    word_seq = data_set[bucket_id][start_id + i]
21                else:
22                    word_seq = []
23
24            word_input_seq = word_seq[:-1]  # without _EOS
25            word_output_seq = word_seq[1:]  # target without _GO
26
27            target_weight = [1.0] * len(word_output_seq) + [0.0] * (length - len(word_output_seq))
28            word_input_seq = word_input_seq + [self.PAD_ID] * (length - len(word_input_seq))
29            word_output_seq = word_output_seq + [self.PAD_ID] * (length - len(word_output_seq))
30
31            input_ids.append(word_input_seq)
32            output_ids.append(word_output_seq)
33            weights.append(target_weight)
34
35        # Now we create batch-major vectors from the data selected above.
36        def batch_major(l):
37            output = []
38            for i in xrange(len(l[0])):
39                temp = []
40                for j in xrange(self.batch_size):
41                    temp.append(l[j][i])
42                output.append(temp)
43            return output
44
45        batch_input_ids = batch_major(input_ids)
46        batch_output_ids = batch_major(output_ids)
47        batch_weights = batch_major(weights)
48
49        finished = False
50        if start_id != None and start_id + self.batch_size >= len(data_set[bucket_id]):
51            finished = True
52
53        return batch_input_ids, batch_output_ids, batch_weights, finished
54

模型训练


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1    def step(self, session, inputs, targets, target_weights,
2             bucket_id, forward_only=False, dump_lstm=False):
3
4        length = self.buckets[bucket_id]
5
6        input_feed = {}
7        for l in xrange(length):
8            input_feed[self.inputs[l].name] = inputs[l]
9            input_feed[self.targets[l].name] = targets[l]
10            input_feed[self.target_weights[l].name] = target_weights[l]
11
12        # output_feed
13        if forward_only:
14            output_feed = [self.losses[bucket_id]]
15            if dump_lstm:
16                output_feed.append(self.states_to_dump[bucket_id])
17
18        else:
19            output_feed = [self.losses[bucket_id]]
20            output_feed += [self.updates[bucket_id], self.gradient_norms[bucket_id]]
21
22        outputs = session.run(output_feed, input_feed, options=self.run_options, run_metadata=self.run_metadata)
23
24        if forward_only and dump_lstm:
25            return outputs
26        else:
27            return outputs[0]  # only return losses
28

总结

分词
将所有句子按空格,符号切分成单词列表,转成数字,并添加上特殊数字。然后再按照已经获取的单词和其对应的数字元组列表,将指定的文件内容进行转换,以一句话作为单位进行转换,存到指定文件内,并且一行一句话。

分组
计算获取$best\_buckets$,然后还需要对上面获取的分词结果按照句子长度和$best\_buckets$进行分组,如:[ [ s1,s1,s1,s1,s1] , [s2,s2,s2,s2,s2,s2,s2,s2,s2,s2],[s3,s3,s3,s4,s4] ],每一个字母表示一句话。

随机选取m个样本
随机选择$bucket\_id$,然后在该组内随机选取m个样本,即m个句子,得到每个句子对应的$Input$和$output$,并计算出该句对应的mask矩阵。

如果分为n组,则需要训练n个RNN模型。将上面所得的训练样本丢进对应RNN模型中进行训练预测。并且计算loss之和。

给TA打赏
共{{data.count}}人
人已打赏
安全运维

基于spring boot和mongodb打造一套完整的权限架构(四)【完全集成security】

2021-12-11 11:36:11

安全运维

Ubuntu上NFS的安装配置

2021-12-19 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索