簡單的Char RNN生成文本

我來錢廟復知世依,似我心苦難歸久,相須萊共游來愁報遠。近王只內蓉者征衣同處,規廷去豈無知草木飄。

你可能以為上面的詩句是某個大詩人所作,事實上上面所有的內容都是循環神經網路寫的,是不是感覺很神奇呢?其實這裡面的原理非常簡單,只需要對循環神經網路有個清楚的理解,那麼就能夠實現上面的效果,在讀完本篇文章之後,大家都能夠學會如何使用循環神經網路來創作文本。

Char RNN的原理

在之前的文章中介紹過RNN的基本結構,其非常擅長處理序列問題,那麼對於文本而言,其相當於也是一個序列,因為每句話都是由單詞或漢字按照順序組成的,所以也能夠使用RNN對其進行處理,但是如何使用RNN進行文本生成呢?其實原理非常簡單,下面我們就介紹一下Char RNN。

訓練過程

一般而言,RNN的輸入和輸出存在著多種關係,比如1對多,多對多等等,不同的輸入輸出關係對應著不同的應用,網上也有很多這方面的文章可以去看看,這裡我們要講的Char RNN在訓練網路的時候是相同長度的多對多的類型,也就是輸入一個序列,輸出一個相同的長度的序列。

具體的網路結構就是下面這個樣子

輸入一句話作為輸入序列,這句話中的每個字元都按照順序進入RNN,每個字元傳入RNN之後都能夠得到一個輸出,而這個輸出就是這個字元在這句話中緊跟其後的一個字元,可以通過上面的圖示清晰地看到這一點。這裡要注意的是,一個序列最後一個輸入對應的輸出可以有多種選擇,上面的圖示是將這個序列的最開始的字元作為其輸出,當然也可以將最後一個輸入作為輸出,以上面的例子說明就是光的輸出就是光本身。

生成文本過程

在預測的時候需要給網路一段初始的序列進行預熱,預熱的過程並不需要實際的輸出結果,只是為了生成具有記憶的隱藏狀態,然後將隱藏狀態保留,傳入之後的網路,不斷地更新句子,直到達到要求的輸出長度,具體可以看下面的圖示

生成文本的過程就是每個字不斷輸入網路,然後將輸出作為下一次的輸出,不斷循環遞歸,因為其會不限循環下去,所以可以設置一個長度讓其停止。

實現細節

這裡我們使用PyToch作為例子進行講解,同時也提供了MXNet-Gluon的版本,因為他們的語法非常相似,所以實現兩個幾乎沒有太大的區別,如果你不知道Gluon是什麼,可以看看之前的一篇文章介紹。同時github也能找到tensorflow的實現。

數據預處理

在進行網路構建之前,需要對數據進行預處理,其實大體的思路很簡單,就是建立字元的數字表示,因為字元沒有辦法直接輸入到網路中,所以需要用不同的數字去代表不同的字元,同時可以設定一個最大字元數,如果文本中讀取到的所有不重複的字元數超過了這個最大字元數,就按照字元出現的頻率截取掉最後的部分。

實現的代碼也非常簡單

with open(text_path, r) as f:n text_file = f.readlines()nword_list = [v for s in text_file for v in s]nvocab = set(word_list)n# 如果單詞超過最長限制,則按單詞出現頻率去掉最小的部分nvocab_count = {}nfor word in vocab:n vocab_count[word] = 0nfor word in word_list:n vocab_count[word] += 1nvocab_count_list = []nfor word in vocab_count:n vocab_count_list.append((word, vocab_count[word]))nvocab_count_list.sort(key=lambda x: x[1], reverse=True)nif len(vocab_count_list) > max_vocab:n vocab_count_list = vocab_count_list[:max_vocab]nvocab = [x[0] for x in vocab_count_list]nself.vocab = vocabnnself.word_to_int_table = {c: i for i, c in enumerate(self.vocab)}nself.int_to_word_table = dict(enumerate(self.vocab))n

建立好一個字典用於字元和數字的相互轉換之後,我們可以使用PyTorch中的Dataset類進行自定義我們的數據集合,只需要重載__getitem__和__len__這兩個函數就可以了。

class TextData(data.Dataset):n def __init__(self, text_path, n_step, arr_to_idx):n self.n_step = n_stepnn with open(text_path, r) as f:n data = f.readlines()n text = [v for s in data for v in s]n num_seq = int(len(text) / n_step)n self.num_seq = num_seqn text = text[:num_seq * n_step] # 截去最後不夠長的部分n arr = arr_to_idx(text)n arr = arr.reshape((num_seq, -1))n self.arr = torch.from_numpy(arr)nn def __getitem__(self, index):n x = self.arr[index, :]n y = torch.zeros(x.size())n y[:-1], y[-1] = x[1:], x[0]n return x, ynn def __len__(self):n return self.num_seqn

網路定義

處理好數據之後,就可以進行網路的定義了,非常簡單,網路只需要定義三層就可以了,第一層是word embedding,也就是詞嵌入層,第二層是RNN層,第三層是線性映射,因為最後是一個分類問題,所以將結果的位數隱射到類別數目。

class CharRNN(nn.Module):n def __init__(self, num_classes, embed_dim, hidden_size, num_layers,n dropout):n super(CharRNN, self).__init__()n self.num_layers = num_layersn self.hidden_size = hidden_sizenn self.word_to_vec = nn.Embedding(num_classes, embed_dim)n self.rnn = nn.GRU(embed_dim, hidden_size, num_layers, dropout)n self.proj = nn.Linear(hidden_size, num_classes)nn def forward(self, x, hs=None):n batch = x.size(0)n if hs is None:n hs = Variable(n torch.zeros(self.num_layers, batch, self.hidden_size))n if torch.cuda.is_available():n hs = hs.cuda()n word_embed = self.word_to_vec(x) # batch x len x embedn word_embed = word_embed.permute(1, 0, 2) # len x batch x embedn out, h0 = self.rnn(word_embed, hs) # len x batch x hiddenn le, mb, hd = out.size()n out = out.view(le * mb, hd)n out = self.proj(out)n out = out.view(le, mb, -1)n out = out.permute(1, 0, 2).contiguous() # batch x len x hiddenn return out.view(-1, out.size(2)), h0n

在向前傳播的時候,我們可以指定傳入的隱藏狀態,雖然訓練中可以不用特別指定,但是在生成文本的時候是需要指定的,同時裡面有一些小細節,需要將數據的維度進行調換和處理,這是因為PyTorch中RNN的輸入有要求。

另外在最後網路輸出的時候,我們會將輸出進行out.view(-1, out.size(1))這個操作,這個操作是為了將所有的序列拼起來,比如現在的輸出是(batch, length),通過這個操作之後就變成了(batch x length, 1),這樣做是為了方便loss的計算。

進行訓練

訓練過程非常簡單,只需要把序列扔到網路中即可,這裡有兩個小細節,第一個是將label y進行y.view(-1),這對應於前面網路輸出結構的操作,第二個細節是通nn.utils.clip_grad_norm()對網路進行梯度裁剪,因為RNN中容易出現梯度爆炸的問題。

for batch in dataloader:n x, y = batchn y = y.type(torch.LongTensor)n mb_size = x.size(0)n if use_gpu:n x = x.cuda()n y = y.cuda()n x, y = Variable(x), Variable(y)n out, _ = model(x)n batch_loss = criterion(out, y.view(-1))n # 反向傳播n optimizer.zero_grad()n batch_loss.backward()n nn.utils.clip_grad_norm(model.parameters(), 5)n optimizer.step()n

生成文本

在生成文本中,為了增加隨機性,我們會將預測概率最高的前五個依概率進行選擇,並不是每次都選擇概率最大的,相關的代碼如下。

def pick_top_n(preds, top_n=5):n top_pred_prob, top_pred_label = torch.topk(preds, top_n, 1)n top_pred_prob /= torch.sum(top_pred_prob)n top_pred_prob = top_pred_prob.squeeze(0).cpu().numpy()n top_pred_label = top_pred_label.squeeze(0).cpu().numpy()n c = np.random.choice(top_pred_label, size=1, p=top_pred_prob)nreturn cn

在生成文本的時候,先通過一句話對網路進行預熱,主要是為了得到預熱後的隱藏狀態,然後將這句話的最後一個詞和預熱之後的隱藏狀態作為網路的第一個輸入,得到結果,然後將結果作為下一步的輸入,不斷循環,直到達到最後的要求的長度。

model.load_state_dict(torch.load(checkpoint))nmodel.eval()nsamples = [convert(c) for c in prime]ninput_txt = torch.LongTensor(samples).unsqueeze(0)nif use_gpu:n input_txt = input_txt.cuda()ninput_txt = Variable(input_txt)n_, init_state = model(input_txt) # 預熱nresult = samplesnmodel_input = input_txt[:, -1].unsqueeze(1)nfor i in range(text_len):n # out是輸出的字元,大小為1 x vocabn # init_state是RNN傳遞的hidden staten out, init_state = model(model_input, init_state)n pred = pick_top_n(out.data)n model_input = Variable(torch.LongTensor(pred)).unsqueeze(0)n if use_gpu:n model_input = model_input.cuda()n result.append(pred[0])n

總結

通過訓練之後的網路,我們能夠生成一些有意思的文本,比如可以將小說,歌曲,詩歌等等輸入訓練,然後可以生成一個相對應的文本,非常有意思,國外就有一個人使用Char RNN對《權利的遊戲》進行了續寫。

有趣歸有趣,但是讀完本篇文章,大家對Char RNN的原理有了深入的理解之後,發現這本質上其實只是一種語句邏輯的學習,比如前面的字元是」你的「,那麼後面緊跟的一個字元就很大概率是一個名詞,而不太可能是一個動詞,這樣不斷的遞歸形成了一個又一個完整的句子,但是因為RNN長時依賴的問題,比較久之前的內容RNN其實已經遺忘,所以Char RNN並沒有辦法像作家一樣創造出一片文章來表達一個觀點,其只不過是對邏輯通順語句的不斷累加而已,所以這只是一個簡單有趣的演算法。

關於RNN的應用非常多,比如機器翻譯,問答系統等等,都是用了seq2seq的模型,所以下一篇文章應該會將一下seq2seq的模型,並且實現一個簡單的聊天機器人。

PyTorch完整代碼

Gluon完整代碼

歡迎關注我的知乎專欄深度煉丹

歡迎訪問我的博客


推薦閱讀:

用循環神經網路進行文件無損壓縮:斯坦福大學提出DeepZip
開源代碼「All in One」:6 份最新「Paper + Code」等你復現 | PaperDaily #12
循環神經網路RNN介紹1:什麼是RNN、為什麼需要RNN、前後向傳播詳解、Keras實現
CW-RNN 收益率時間序列回歸

TAG:RNN | 深度学习DeepLearning | PyTorch |