character-RNN模型介紹以及代碼解析


RNN是一個很有意思的模型。早在20年前就有學者發現了它強大的時序記憶能力,另外學術界以證實RNN模型屬於Turning-Complete,即理論上可以模擬任何函數。但實際運作上,一開始由於vanishing and exploiting gradient問題導致BPTT算法學習不了長期記憶。雖然之后有了LSTM(長短記憶)模型對普通RNN模型的修改,但是訓練上還是公認的比較困難。在Tensorflow框架里,之前的兩篇博客已經就官方給出的PTB和Machine Translation模型進行了講解,現在我們來看一看傳說中的機器寫詩的模型。原模型出自安德烈.卡帕西大神的char-rnn項目,意在顯示RNN強大的能力以及並非那么困難的訓練方法。對這個方面有興趣的朋友請點擊這里查看詳情。原作的框架為Torch,點擊這里查看原作代碼。中山大學的zhangzibin以卡帕西大神的代碼為樣本制作了一款基於卡帕西RNN模型以及Samy Bengio(Bengio大神的親弟弟)提出的Schedule Sampling算法的可運行中文的RNN模型,源代碼請點擊這里查看。作為Tensorflow的玩家,我本人當然很想了解下這個框架的運行情況,特別是在Tensorflow框架里的運行情況。好在有人已經捷足先登,將代碼移植完畢了。今天我們就來看看這個神奇框架在Tensorflow下的代碼。對該項目感興趣的朋友可以在這里下載到項目的源碼並在自己的機器上運行。

既然有了Tensorflow版本的代碼了,那么我們開始解剖這段代碼吧!

在解剖代碼之前,讓我們先對代碼的運行做一個了解。在運行時,我們需要做的是cd到項目里后,先運行train.py文件來訓練代碼。默認的迭代數是50個迭代,默認的訓練文件是tinyshakespear目錄里的input.txt文件,也就是莎士比亞的一些作品。由於默認都是設定好的,我們不需要做任何更改,直接運行python train.py就好了。訓練速度還是比較客觀的,大約需要運行一個小時(沒算具體時間),我們會發現訓練完成,參數已經保存。之后,如果我們想看看運行的結果如何,打入python sample.py后,就會隨機產生一段文字,該段文字是由機器學習了訓練文本后自行計算的。之后我會放上機器在學習了郭敬明的幻成和小時代后自己寫出的句子供大家參考。

在了解了運行方式后,既然入口文件是train.py,那么我們就先來看看該文件的設計。不出所料,train.py文件的開始為一系列的parser.add_argument。在之前的代碼里我們已經多次見到,無非是加入了運行系統所需的參數,他們的默認值以及參數的解釋。從這里我們發現默認的RNN框架為lstm,2層RNN結構,每層有128個神經元節點。另外,我們的sequence length定義為50,也就是每一次可以執行50個時間序列。之后便是train函數。如同往常,我們發現textloader函數為錄入訓練集的函數,這個函數存在於utils.py文件里。該文件很容易理解,在讀入數據后通過collections.Counter收集文本中不一樣的character,並將他們寫入vocab_file文件做保存,已備后用。之后,根據總數據大小,minibatch大小以及時序長短來界定運行完整個文件需要多少個minibatches,並將文本分類成minibatch的訓練以及目標batches。由於這個模型的目的是學習一個character后下一個character的概率,訓練集跟目標函數間的差異為一個character,即在訓練句子My name is Edward時,假設訓練集為: My name is Edwar, 相對應的目標集為y name is Edward。 從邏輯角度上說,不管是這個util.py文件還是之前博客里的CBOW模型,他們的核心邏輯都是相似的,只是在處理上由於目標不同而產生出工程上的差異。有興趣的朋友可以對比這個util.py文件里的邏輯和CBOW模型里讀入輸入的函數做對比。

之后,train.py文件對需要的目錄以及文件進行確認后就是建立模型了。通過model = Model(args),我們建立了這個RNN所需要的模型。那么模型是如何建立的呢?讓我們仔細來看看model.py文件。這個model.py文件里存在兩個函數:init函數以及sample函數。他們分別被用來訓練模型以及測試模型。讓我們首先來看看模型的訓練:

def __init__(self, args, infer=False):
        self.args = args
        # 這里的infer被默認為False,只有在測試效果
        # 的時候才會被設計為True,在True的狀態下
        # 只有一個batch,time step也被設計為1,我們
        # 可以由此觀測訓練成功
        if infer:
            args.batch_size = 1
            args.seq_length = 1

        # 這里是選擇RNN cell的類型,備選的有lstm, gru和simple rnn
        # 這里由輸入的arg里的model參數作為測試標准,默認為lstm
        # 但是,我們可以看到,這里通過不同的模型我們可以用不同
        # 的cell。
        if args.model == 'rnn':
            cell_fn = rnn_cell.BasicRNNCell
        elif args.model == 'gru':
            cell_fn = rnn_cell.GRUCell
        elif args.model == 'lstm':
            cell_fn = rnn_cell.BasicLSTMCell
        else:
            raise Exception("model type not supported: {}".format(args.model))
        # 定義cell的神經元數量,等同於cell = rnn_cell.BasicLSTMCell(args.rnn_size)
        cell = cell_fn(args.rnn_size)
        # 由於結構為多層結構,我們運用MultiRNNCell來定義神經元層。
        self.cell = cell = rnn_cell.MultiRNNCell([cell] * args.num_layers)
        # 輸入,同PTB模型,輸入的格式為batch_size X sequence_length(step)
        self.input_data = tf.placeholder(tf.int32, [args.batch_size, args.seq_length])
        self.targets = tf.placeholder(tf.int32, [args.batch_size, args.seq_length])
        self.initial_state = cell.zero_state(args.batch_size, tf.float32)

        with tf.variable_scope('rnnlm'):
            softmax_w = tf.get_variable("softmax_w", [args.rnn_size, args.vocab_size])
            softmax_b = tf.get_variable("softmax_b", [args.vocab_size])
            with tf.device("/cpu:0"):
                # 這里運用embedding來將輸入的不同詞匯map到隱匿層的神經元上
                embedding = tf.get_variable("embedding", [args.vocab_size, args.rnn_size])
                # 這里對input的shaping很有意思。這個地方如果我們仔細去讀PTB模型就會發現在他的
                # outputs = []這行附近有一段注釋的文字,解釋了一個alternative做法,這個做法就是那
                # alternative的方法。首先,我們將embedding_loopup所得到的[batch_size, seq_length, rnn_size]
                # tensor按照sequence length划分為一個list的[batch_size, 1, rnn_size]的tensor以表示每個
                # 步驟的輸入。之后通過squeeze把那個1維度去掉,達成一個list的[batch_size, rnn_size]
                # 輸入來被我們的rnn模型運用。
                inputs = tf.split(1, args.seq_length, tf.nn.embedding_lookup(embedding, self.input_data))
                inputs = [tf.squeeze(input_, [1]) for input_ in inputs]
        # 這里定義的loop實際在於當我們要測試運行結果,即讓機器自己寫文章時,我們需要對每一步
        # 的輸出進行查看。如果我們是在訓練中,我們並不需要這個loop函數。
        def loop(prev, _):
            prev = tf.matmul(prev, softmax_w) + softmax_b
            prev_symbol = tf.stop_gradient(tf.argmax(prev, 1))
            return tf.nn.embedding_lookup(embedding, prev_symbol)
        # 這里我們得益於tensorflow強大的內部函數,rnn_decoder可以作為黑盒子直接運用,省去了編寫
        # 的麻煩。另外,上面的loop函數只有在infer是被定為true的時候才會啟動,一如我們剛剛所述。另外
        # rnn_decoder在tensorflow中的建立方式是以schedule sampling算法為基礎制作的,故其自身已經融入
        # 了schedule sampling算法。
        outputs, last_state = seq2seq.rnn_decoder(inputs, self.initial_state, cell, loop_function=loop if infer else None, scope='rnnlm')
        # 這里的過程可以說基本等同於PTB模型,首先通過對output的重新梳理得到一個
        # [batch_size*seq_length, rnn_size]的輸出,並將之放入softmax里,並通過sequence
        # loss by example函數進行訓練。
        output = tf.reshape(tf.concat(1, outputs), [-1, args.rnn_size])
        self.logits = tf.matmul(output, softmax_w) + softmax_b
        self.probs = tf.nn.softmax(self.logits)
        loss = seq2seq.sequence_loss_by_example([self.logits],
                [tf.reshape(self.targets, [-1])],
                [tf.ones([args.batch_size * args.seq_length])],
                args.vocab_size)
        self.cost = tf.reduce_sum(loss) / args.batch_size / args.seq_length
        self.final_state = last_state
        self.lr = tf.Variable(0.0, trainable=False)
        tvars = tf.trainable_variables()
        grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars),
                args.grad_clip)
        optimizer = tf.train.AdamOptimizer(self.lr)
        self.train_op = optimizer.apply_gradients(zip(grads, tvars))

 由上述代碼可見,在制作RNN的模型里,不可或缺的步驟如下:

# 制作RNN模型的大概步驟:

# 1.定義cell類型以及模型框架(假設為lstm):
basic_cell = tf.nn.rnn_cell.BasicLSTMCell(rnn_size)
cell = tf.nn.rnn_cell.MultiRNNCell([basic_cell]*number_layers)

# 2.定義輸入
input_data = tf.placeholder(tf.int32, [batch_size, sequence_length])
target = tf.placeholder(tf.int32, [batch_size, sequence_length])
 
# 3. init zero state
initial_state = cell.zero_state(batch_size, tf.float32)

# 4. 整理輸入,可以運用PTB的方法或上文介紹的方法,不過要注意
# 你的輸入是什么形狀的。最后數列要以格式[sequence_length, batch_size, rnn_size]
# 為輸入才可以。

# 5. 之后為按照你的應用所需的函數運用了。這里運用的是rnn_decoder, 當然,別的可以
# 運用,比如machine translation里運用的就是embedding_attention_seq2seq

# 6. 得到輸出,重新編輯輸出的結構后可以運用softmax,一般loss為sequence_loss_by_example

# 7. 計算loss, final_state以及選用learning rate,之后用clip_by_global norm來定義gradient
# 並運用類似於adam來optimise算法。可以運用minimize或者apply_gradients來訓練。

 在了解了模型后,我們發現剩下的代碼都是比較常見的,例如initialize_all_variables, 以及運用learning rate decay的方式訓練模型。由此,train.py文件的訓練過程我們已經做了一個大概的了解了。那么,系統又是如何讓我們可以測試訓練好的模型呢?讓我們來看看sample.py文件。通過parser.add_arugment函數,我們發現文件會選取我們保存模型的地點,並會產生500字符的sample,至於sample選項,我們發現設定為0是得到最多的timestep,1是每一個timestep, 2是sample on spaces。之后,我們讀取存儲的模型內容后將內容傳遞進Model函數,並將infer設為True。在output的時候,我們運用sample函數來的到輸出。在這個函數里,我們發現一般的prime開頭是‘The’,這里我們可以通過sample.py里的prime函數來指定一個開頭。在之后,我們發現那個sample參數設為0時選取的是argmax,1時是weighted_pick,2時以space為標准,如果有space則選擇weighted_pick, 不然就是argmax。好了,實際運行的效果如何呢?讓我們來幾個例子看看。

這里是the為開始,我們看到了一開始有點亂碼。之后,我們看到可是我知道了,他們三個女生等短句都是通順的,同時,也有一些及其不通的,例如底忘記新地把滿些了,這句話什么含義完全不清楚。再看下一個例子,如果以我開頭會怎么樣呢?

如果把sample設為0又會如何呢?

這里篇幅緊湊多了。再次運行,我們得到了相同的結果,因為是argmax么,所以在沒改變的情況下我們會得到相同的結果。

這個運行結果還是很有意思的,有興趣的朋友可以自行下載項目然后試着去操作一下!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM