深度學習-->NLP-->RNNLM實現

本篇博文將詳細總結RNNLM 的原理以及如何在tensorflow 上實現RNNLM

我們要實現的網絡結構如下:

這裏寫圖片描述

數據預處理

創建vocab

分詞:

將句子中的每個單詞以空格,符號分開,形成一個單詞列表

def blank_tokenizer(sentence):
    ##以空格對句子進行切分
    return sentence.strip().split()

def basic_tokenizer(sentence):
    '''
    _WORD_SPLIT=re.compile(b"([.,!?\"':;)(])")
    首先以空格對句子進行切分,然後再以標點符號切分,切分出一個個詞,然後詞列表
    '''
    words=[]
    for space_separated_fragment in sentence.strip().split():
        words.extend(_WORD_SPLIT.split(space_separated_fragment))
    return [w for w in words if w]

對單詞列表添加特殊詞彙:

  • _PAD 填充詞彙
  • _GO 句子開始
  • _EOS 句子結束
  • _UNK 未知詞(低頻的詞替換爲UNK)

"i love you" 創建成vocab 時,應爲:
"_GO i love you _EOS

將單詞替換成數字

vocab 內的單詞按出現頻率排序,用其索引代替單詞。
如:1 3 102 3424 2

def create_vocabulary(vocabulary_path,data_paths,max_vocabulary_size,tokenizer=None,normalize_digits=False):
    '''
    讀取data_paths路徑下的文件,並且一行行的讀取,對每句做分詞處理,得到每個詞的頻率,然後存儲頻率最高的max_vocabulary_size的詞,存入vocabulary_path
    :param vocabulary_path: 新建的文件夾,將返回的結果寫入
    :param data_paths:存儲原始文件的路徑
    :param max_vocabulary_size:最大存儲的詞的個數
    :param tokenizer:對句子做分詞處理
    :param normalize_digits:是否對句子中的數字以0替換
    :return:返回的vocabulary_path中一行一個詞
    '''
    if not gfile.Exists(vocabulary_path):
        print ("Create vocabulary %s from data %s" %(vocabulary_path,",".join(data_paths)))
        vocab={}
        for data_path in data_paths:
            with gfile.GFile(data_path,mode='rb') as f:
                print (data_path)
                counter=0
                for line in f:
                    counter+=1
                    if counter%100000==0:
                        print ("processing line %d" %counter)
                    #Converts either bytes or unicode to bytes, using utf-8 encoding for text.
                    line=tf.compat.as_bytes(line)
                    tokens=tokenizer(line) if tokenizer else blank_tokenizer(line)
                    for w in tokens:
                        #replace digit to 0
                        #_DIGIT_RE=re.compile(br"\d")
                        word=_DIGIT_RE.sub(b"0",w) if normalize_digits else w
                        if word in vocab:
                            vocab[word]+=1
                        else:
                            vocab[word]=1
                print (len(vocab))
        # _START_VOCAB=[_PAD,_GO,_EOS,_UNK]
        # 按詞頻率降序排序
        vocab_list=_START_VOCAB+sorted(vocab,key=vocab.get,reverse=True)
        if len(vocab_list)>max_vocabulary_size:
            vocab_list=vocab_list[:max_vocabulary_size]##只取出現頻率最高的max_vocabulary_size
        with gfile.GFile(vocabulary_path,mode='rb') as vocab_file:
            for w in vocab_list:
                vocab_file.write(w+b'\n')##注意將分出的單詞一行一行的寫入到vocabulary_path


def initialize_vocabulary(vocabulary_path):
    '''
    :param vocabulary_path:一行一個詞
    讀取vocabulary_path文件內每行的每個單詞到rev_vocab,然後枚舉rev_vocab,然後字典列表[(word,index)]
    :return:
    '''
    if gfile.Exists(vocabulary_path):
        rev_vocab=[]
        with gfile.GFile(vocabulary_path,mode='rb') as f:
            rev_vocab.extend(f.readlines())
        rev_vocab=[tf.compat.as_bytes(line.strip()) for line in rev_vocab]
        vocab=dict([(x,y) for (y,x) in enumerate(rev_vocab)])
        return vocab,rev_vocab
    else:
        raise ValueError("Vocabulary file % not found",vocabulary_path)


def sentence_to_token_ids(sentence,vocabulary,tokenizer=None,normalize_digits=False,with_start=True,with_end=True):
    '''
    對sentence句子進行分詞處理,並且用其在vocabulary中的索引代替其詞,並且加上GO_ID,EOS_ID,UNK等特殊數字,返回數字列表。
    :param sentence:需要分詞的句子
    :param vocabulary:字典列表[(word,index)]
    :param tokenizer:分詞處理方法
    :param normalize_digits:是否將句子中數字用0替換
    :param with_start:是否在句頭帶上GO_ID
    :param with_end:是否在句尾帶上EOS_ID
    :return:
    '''
    if tokenizer:
        #對sentence進行分詞處理
        words=tokenizer(sentence)
    else:
        # 對sentence進行分詞處理
        words=basic_tokenizer(sentence)
    if not normalize_digits:
        #在vocabulary中找到Word,返回其index,否則以UNK_ID代替返回
        #UNK_ID=3
        ids=[vocabulary.get(w,UNK_ID) for w in words]
    else:
        #_DIGIT_RE=re.compile(br"\d")
        ids=[vocabulary.get(_DIGIT_RE.sub(b"0",w),UNK_ID) for w in words]

    if with_start:
        ids=[GO_ID]+ids
    if with_end:
        ids=ids+[EOS_ID]
    return ids


def data_to_token_ids(data_path,target_path,vocabulary_path,tokenizer=None,normalize_digits=False,with_go=True,with_end=True):
    '''
    讀取data_path路徑下的文件內容,讀取其每一行,餵給sentence_to_token_ids方法處理,得到所有詞的索引列表,然後存入到target_path
    :param data_path:原文件
    :param target_path:原文件處理完要存入的地址
    :param vocabulary_path:一行一個詞
    :param tokenizer:
    :param normalize_digits:
    :param with_go:
    :param with_end:
    :return:
    '''
    if not gfile.Exists(target_path):
        print ("Tokenizing data in %s" % data_path)
        vocab,_=initialize_vocabulary(vocabulary_path)
        #vocab是字典列表[(word,index)]
        with gfile.GFile(data_path,mode='rb') as data_file:
            with gfile.GFile(target_path,mode='w') as tokens_file:
                counter=0
                for line in data_file:
                    counter+=1
                    if counter%100000==0:
                        print ("tokenizing line %d" % counter)
                    token_ids=sentence_to_token_ids(tf.compat.as_bytes(line),vocab,tokenizer,normalize_digits)
                    tokens_file.write(" ".join([str(tok) for tok in token_ids])+'\n')#注意一行一句話

訓練RNN模型

Minibatch 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 parameterspatience=0
      比之前升高:learning rate 減半,patience+=1
    if (patience>10):break

minibatchRNN 上問題

句子的長度不一樣

這裏寫圖片描述

解決方法:句子的長度不一樣: 增加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]。

def calculate_buckets(length_array, max_length, max_buckets):
    '''

    :param length_array:所有句子的長度列表[1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,4,4]
    :param max_length:最長句子的長度4
    :param max_buckets:分爲幾個組
    :return:
    '''
    d = {}
    for length in length_array:
        if not length in d:
            d[length] = 0
        d[length] += 1

    #dd:[(句子長度,該長度出現次數)]
    dd = [(x, d[x]) for x in d]
    dd = sorted(dd, key=lambda x: x[0])##以長度升序排序

    #計算running_sum
    running_sum = []
    s = 0
    for l, n in dd:
        s += n
        running_sum.append((l, s))#running_sum = [(1,5),(2,15),(3,18),(4,20)]

    def best_point(ll):
        ## ll即running_sum:[(句子長度,小於等於該長度出現次數)]
        #找出最大可以去掉的無效面積
        index = 0
        maxv = 0
        base = ll[0][1]
        for i in xrange(len(ll)):
            l, n = ll[i]
            v = (ll[-1][0] - l) * (n - base)
            if v > maxv:
                maxv = v
                index = i
        return index, maxv

    def arg_max(array, key):
        # 找出最大可以去掉的無效面積
        maxv = -10000
        index = -1

        for i in xrange(len(array)):
            item = array[i]
            v = key(item)
            if v > maxv:
                maxv = v
                index = i
        return index

    end_index = 0
    for i in xrange(len(running_sum) - 1, -1, -1):
        if running_sum[i][0] <= max_length:
            end_index = i + 1
            break

    # print "running_sum [(length, count)] :"
    # print running_sum

    if end_index <= max_buckets:
        buckets = [x[0] for x in running_sum[:end_index]]
    else:
        '''
        不斷遞歸的以可以去掉最大的無效面積爲原則不斷的劃分
        '''
        buckets = []
        # (array,  maxv, index)
        states = [(running_sum[:end_index], 0, end_index - 1)]#[([(1,5),(2,15),(3,18),(4,20)],0,end_index-1)],列表長度爲1
        while len(buckets) < max_buckets:
            index = arg_max(states, lambda x: x[1])##最大可以去掉的無效面積對應的索引
            state = states[index]
            del states[index]
            # split state
            array = state[0]
            split_index = state[2]
            buckets.append(array[split_index][0])
            array1 = array[:split_index + 1]
            array2 = array[split_index + 1:]
            if len(array1) > 0:
                id1, maxv1 = best_point(array1)
                states.append((array1, maxv1, id1))
            if len(array2) > 0:
                id2, maxv2 = best_point(array2)
                states.append((array2, maxv2, id2))
    return sorted(buckets)

def split_buckets(array, buckets, withOrder=False):
    """

    :param array:句子的集合
    :param buckets:上面計算出來的最優劃分組
    :param withOrder:
    :return:d[buckets_id,屬於該組的items];order((buckets_id,len(d[buckets_id]) - 1))
    """
    order = []
    d = [[] for i in xrange(len(buckets))]
    for items in array:
        index = get_buckets_id(len(items), buckets)
        if index >= 0:
            d[index].append(items)
            order.append((index, len(d[index]) - 1))
    return d, order


def get_buckets_id(l, buckets):
    '''
    將某句子長度劃到對應的分組中,返回該句子的組號
    :param l:
    :param buckets:
    :return:
    '''
    id = -1
    for i in xrange(len(buckets)):
        if l <= buckets[i]:
            id = i
            break
    return id

我們計算處buckets,需要對其中不同的bucket建立不同步長的RNN模型。並且在對不同模型的loss求和。

    def model_with_buckets(self, inputs, targets, weights,
                           buckets, cell, dtype,
                           per_example_loss=False, name=None, devices=None):

        all_inputs = inputs + targets + weights

        losses = []
        hts = []
        logits = []
        topk_values = []
        topk_indexes = []

        # initial state
        with tf.device(devices[1]):
            init_state = cell.zero_state(self.batch_size, dtype)

        # softmax
        with tf.device(devices[2]):
            softmax_loss_function = lambda x, y: tf.nn.sparse_softmax_cross_entropy_with_logits(logits=x, labels=y)

        with tf.name_scope(name, "model_with_buckets", all_inputs):
            for j, bucket in enumerate(buckets):
                with variable_scope.variable_scope(variable_scope.get_variable_scope(), reuse=True if j > 0 else None):

                    # ht
                    with tf.device(devices[1]):
                        _hts, _ = tf.contrib.rnn.static_rnn(cell, inputs[:bucket], initial_state=init_state)
                        hts.append(_hts)

                    # logits / loss / topk_values + topk_indexes
                    with tf.device(devices[2]):
                        _logits = [tf.add(tf.matmul(ht, tf.transpose(self.output_embedding)), self.output_bias) for ht
                                   in _hts]
                        logits.append(_logits)

                        if per_example_loss:
                            losses.append(sequence_loss_by_example(
                                logits[-1], targets[:bucket], weights[:bucket],
                                softmax_loss_function=softmax_loss_function))

                        else:
                            losses.append(sequence_loss(
                                logits[-1], targets[:bucket], weights[:bucket],
                                softmax_loss_function=softmax_loss_function))

                        topk_value, topk_index = [], []

                        for _logits in logits[-1]:
                            value, index = tf.nn.top_k(tf.nn.softmax(_logits), self.topk_n, sorted=True)
                            topk_value.append(value)
                            topk_index.append(index)
                        topk_values.append(topk_value)
                        topk_indexes.append(topk_index)

        self.losses = losses
        self.hts = hts
        self.logits = logits
        self.topk_values = topk_values
        self.topk_indexes = topk_indexes

如何隨機選擇m個數據?

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

  1. 先隨機一個buckets
  2. 再隨機取m個數據
  3. 將m個數據變成一個矩陣,加上padding
    def get_batch(self, data_set, bucket_id, start_id=None):
        '''
        :param data_set:[ [ s1,s1,s1,s1,s1] , [s2,s2,s2,s2,s2,s2,s2,s2,s2,s2],
[s3,s3,s3,s4,s4] ],注意每個字母表示一個句子。
        :param bucket_id:第幾個分組
        :param buckets:[1,2,4]
        :param batch_size
        :param start_id:
        :return:
        '''
        length = self.buckets[bucket_id]##當前組的句子長度,即需要補齊的長度

        input_ids, output_ids, weights = [], [], []

        for i in xrange(self.batch_size):##獲取batch_size個句子。
            if start_id == None:
                word_seq = random.choice(data_set[bucket_id])
            else:
                if start_id + i < len(data_set[bucket_id]):
                    word_seq = data_set[bucket_id][start_id + i]
                else:
                    word_seq = []

            word_input_seq = word_seq[:-1]  # without _EOS
            word_output_seq = word_seq[1:]  # target without _GO

            target_weight = [1.0] * len(word_output_seq) + [0.0] * (length - len(word_output_seq))
            word_input_seq = word_input_seq + [self.PAD_ID] * (length - len(word_input_seq))
            word_output_seq = word_output_seq + [self.PAD_ID] * (length - len(word_output_seq))

            input_ids.append(word_input_seq)
            output_ids.append(word_output_seq)
            weights.append(target_weight)

        # Now we create batch-major vectors from the data selected above.
        def batch_major(l):
            output = []
            for i in xrange(len(l[0])):
                temp = []
                for j in xrange(self.batch_size):
                    temp.append(l[j][i])
                output.append(temp)
            return output

        batch_input_ids = batch_major(input_ids)
        batch_output_ids = batch_major(output_ids)
        batch_weights = batch_major(weights)

        finished = False
        if start_id != None and start_id + self.batch_size >= len(data_set[bucket_id]):
            finished = True

        return batch_input_ids, batch_output_ids, batch_weights, finished

模型訓練

    def step(self, session, inputs, targets, target_weights,
             bucket_id, forward_only=False, dump_lstm=False):

        length = self.buckets[bucket_id]

        input_feed = {}
        for l in xrange(length):
            input_feed[self.inputs[l].name] = inputs[l]
            input_feed[self.targets[l].name] = targets[l]
            input_feed[self.target_weights[l].name] = target_weights[l]

        # output_feed
        if forward_only:
            output_feed = [self.losses[bucket_id]]
            if dump_lstm:
                output_feed.append(self.states_to_dump[bucket_id])

        else:
            output_feed = [self.losses[bucket_id]]
            output_feed += [self.updates[bucket_id], self.gradient_norms[bucket_id]]

        outputs = session.run(output_feed, input_feed, options=self.run_options, run_metadata=self.run_metadata)

        if forward_only and dump_lstm:
            return outputs
        else:
            return outputs[0]  # only return losses

總結

  1. 分詞
    將所有句子按空格,符號切分成單詞列表,轉成數字,並添加上特殊數字。然後再按照已經獲取的單詞和其對應的數字元組列表,將指定的文件內容進行轉換,以一句話作爲單位進行轉換,存到指定文件內,並且一行一句話。

  2. 分組
    計算獲取best_buckets ,然後還需要對上面獲取的分詞結果按照句子長度和best_buckets 進行分組,如:[ [ s1,s1,s1,s1,s1] , [s2,s2,s2,s2,s2,s2,s2,s2,s2,s2],[s3,s3,s3,s4,s4] ],每一個字母表示一句話。

  3. 隨機選取m個樣本
    隨機選擇bucket_id ,然後在該組內隨機選取m個樣本,即m個句子,得到每個句子對應的Inputoutput ,並計算出該句對應的mask矩陣。

  4. 如果分爲n組,則需要訓練n個RNN模型。將上面所得的訓練樣本丟進對應RNN模型中進行訓練預測。並且計算loss之和。

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