天天看點

【自然語言處理】RNN文本生成Python(純Numpy)實作前言資料集算法實作

前言

Github:代碼下載下傳

由于RNN具有記憶功能,之前文章有介紹RNN來實作二進制相加,并取得了比較好的效果。那這次本文使用RNN來進行古詩生成。

資料集

資料集就是我們的古詩了,每行都是一首古詩,并且以格式"題目:古詩"。

【自然語言處理】RNN文本生成Python(純Numpy)實作前言資料集算法實作

首先需要建立一個詞典,詞典可以是每個字的詞頻高的前6000字作為詞典,然後用one-hot來表示詞向量。

def getVocab(filename='poetry.txt'):
    with open(filename, encoding='utf-8') as f:
        lines = f.readlines()   #讀取每一行
        wordFreqDict = dict()   #将每個詞都進行詞頻計算,取詞頻高的前多少詞用來做詞典
        for line in lines:  #周遊每一行
            tokens = dict(nltk.FreqDist(list(line)))    #分詞,并且計算詞頻
            wordFreqDict = dict(Counter(wordFreqDict) + Counter(tokens))    #把每個詞的詞頻相加
        wordFreqTuple = sorted(wordFreqDict.items(), key = lambda x: x[1], reverse=True)    #按詞頻排序,逆排序
        fw = open('vocab.txt','w', encoding='utf-8')    #将詞典的每個詞(這裡是每個字作為一個詞)寫入到檔案
        vocab = wordFreqTuple[:6000]    #詞典
        for word in vocab:
            fw.write(word[0] + '\n')
           

這個函數就是按詞頻的前6000個字做成詞典,并寫入到txt檔案中,儲存。

def loadData(filename = 'poetry.txt'):
    vocab = readVocab() #擷取詞典,詞典是以['詞', '典']這樣存放的,類似這樣
    word2Index = {word: index for index, word in enumerate(vocab)}  #将詞典映射成{詞:0, 典:1},類似這樣
    fr = open(filename, 'r', encoding='utf-8')  #讀取檔案,準備資料集
    lines = fr.readlines()  #所有行
    dataSet = list()    #資料集
    labels = list() #标簽
    for line in lines:
        poetry = line.split(":")[1].rstrip()  #去除标題
        X = [word2Index.get(word, 0) for word in list(poetry)] #把對應的文字轉成索引,形成向量
        y = X.copy()
        X.insert(0, 1)  #1代表是詞典裡的START,代表開始
        dataSet.append(X)
        y.append(2) #2代表是詞典裡的END
        labels.append(y)
    return dataSet, labels
           

這裡讀取每首古詩的詩,就是去掉标題的部分。比如"臨洛水:春蒐馳駿骨,總辔俯長河。霞處流萦錦,風前漾卷羅。水花翻照樹,堤蘭倒插波。豈必汾陰曲,秋雲發棹歌。",那隻要"春蒐馳駿骨,總辔俯長河。霞處流萦錦,風前漾卷羅。水花翻照樹,堤蘭倒插波。豈必汾陰曲,秋雲發棹歌。",并且轉成索引清單,形成詞向量[],并且添加首位标志,那麼這裡可能會産生一個問題,這裡的标簽是從何而來?其實這裡标簽也是古詩,不過是詞向量加上一個末尾标志。是以最終的資料集和标簽集可能長這樣。因為我們希望’春’能夠通過RNN預測’蒐’,‘蒐’預測’馳’。

至此,有了資料集和标簽,就可以來訓練RNN了。

算法實作

def __init__(self):
        self.wordDim = 6000
        wordDim = self.wordDim
        self.hiddenDim = 100
        hiddenDim = self.hiddenDim
        self.Wih = np.random.uniform(-np.sqrt(1. / wordDim), np.sqrt(1. / wordDim), (hiddenDim, wordDim))  #輸入層到隐含層的權重矩陣(100, 6000)
        self.Whh = np.random.uniform(-np.sqrt(1. / self.hiddenDim), np.sqrt(1. / self.hiddenDim), (hiddenDim, hiddenDim))  #隐含層到隐含層的權重矩陣(100, 100)
        self.Why = np.random.uniform(-np.sqrt(1. / self.hiddenDim), np.sqrt(1. / self.hiddenDim), (wordDim, hiddenDim))  #隐含層到輸出層的權重矩陣(10, 1)

           

最開始先初始化詞典大小(次元),一集隐含層大小。

前向傳播

這裡先給出前向傳播的公式:

z t h = W i h x + W h h a t − 1 h a t h = tanh ⁡ ( z t h ) z t k = W h y a t h a t k = s o f t m a x ( z t k ) E = − ∑ t = 1 N y t log ⁡ ( a t k ) \begin{array}{l} z_t^h = {W_{ih}}x + {W_{hh}}a_{{\rm{t - 1}}}^h\\ a_t^h = \tanh \left( {z_t^h} \right)\\ z_t^k = {W_{hy}}a_t^h\\ a_t^k = {\mathop{\rm softmax}\nolimits} \left( {z_t^k} \right)\\ E =- \sum\limits_{t = 1}^N {{y_t}\log \left( {a_t^k} \right)} \end{array} zth​=Wih​x+Whh​at−1h​ath​=tanh(zth​)ztk​=Why​ath​atk​=softmax(ztk​)E=−t=1∑N​yt​log(atk​)​

相比上一節RNN來做二進制相加的公式來看,可以看到之前的2個sigmoid函數被改成tanh函數和softmax函數了。

同時損失函數改成了交叉熵。

def forward(self, data):  #前向傳播,原則上傳入一個資料樣本和标簽
        T = len(data)
        output = np.zeros((T, self.wordDim, 1)) #輸出
        hidden = np.zeros((T+1, self.hiddenDim, 1)) #隐層狀态
        for t in range(T): #時間循環
            X = np.zeros((self.wordDim, 1)) #建構(6000,1)的向量
            X[data[t]][0] = 1   #将對應的值置為1,形成詞向量
            Zh = np.dot(self.Wih, X) + np.dot(self.Whh, hidden[t-1])   #(100, 1)
            ah = np.tanh(Zh)   #(100, 1),隐層值
            hidden[t] = ah
            Zk = np.dot(self.Why, ah)   #(6000,1)
            ak = self.softmax(Zk)   #(6000, 1),輸出值
            output[t] = ak #把index寫進去
        return hidden, output
           

代碼不難懂,跟公式是對應的。

反向傳播

反向傳播的公式:

δ t k = ∂ E ∂ a k ∂ a k ∂ z k = { i = j , a j − 1 i = ̸ j , a j δ T h = ∂ E ∂ a T k ∂ a T k ∂ z T k ∂ z T k ∂ a T h ∂ a T h ∂ z T h = ( W h y ) T δ T k × ( 1 − tanh ⁡ 2 ( a t h ) ) δ t h = ∂ E ∂ a t k ∂ a t k ∂ z t k ∂ z t k ∂ a t h ∂ a t h ∂ z t h + ∂ E ∂ a t + 1 k ∂ a t + 1 k ∂ z t + 1 k ∂ z t + 1 k ∂ a t + 1 h ∂ a t + 1 h ∂ z t + 1 h ∂ z t + 1 h ∂ a t h ∂ a t h ∂ z t h = ( ( W h h ) T δ t + 1 h + ( W h y ) T δ t k ) ∂ a t h ∂ z t h = ( ( W h h ) T δ t + 1 h + ( W h y ) T δ t k ) × ( 1 − tanh ⁡ 2 ( a t h ) ) W i h = W i h − η δ t h ( x t ) T W h h = W h h − η δ t h ( a t − 1 h ) T W h y = W h y − η δ t k ( a t h ) T \begin{array}{l} \delta _t^k = \frac{{\partial E}}{{\partial {a^k}}}\frac{{\partial {a^k}}}{{\partial {z^k}}} = \left\{ \begin{array}{l} i = j,{a_j} - 1\\ i =\not j,{a_j} \end{array} \right.\\ \delta _T^h = \frac{{\partial E}}{{\partial a_T^k}}\frac{{\partial a_T^k}}{{\partial z_T^k}}\frac{{\partial z_T^k}}{{\partial a_{\rm{T}}^h}}\frac{{\partial a_T^h}}{{\partial z_{\rm{T}}^h}} = {\left( {{W_{hy}}} \right)^T}\delta _T^k \times {\rm{(}}1 - {\tanh ^2}\left( {a_t^h} \right))\\ {\delta _t^h = \frac{{\partial E}}{{\partial a_t^k}}\frac{{\partial a_t^k}}{{\partial z_t^k}}\frac{{\partial z_t^k}}{{\partial a_t^h}}\frac{{\partial a_t^h}}{{\partial z_t^h}} + \frac{{\partial E}}{{\partial a_{t + 1}^k}}\frac{{\partial a_{t + 1}^k}}{{\partial z_{t + 1}^k}}\frac{{\partial z_{t + 1}^k}}{{\partial a_{t{\rm{ + }}1}^h}}\frac{{\partial a_{t{\rm{ + }}1}^h}}{{\partial z_{t + 1}^h}}\frac{{\partial z_{t + 1}^h}}{{\partial a_t^h}}\frac{{\partial a_t^h}}{{\partial z_t^h}}}\\ {\rm{ }} = \left( {{{\left( {{W_{hh}}} \right)}^T}\delta _{t + 1}^h + {{\left( {{W_{hy}}} \right)}^T}\delta _t^k} \right)\frac{{\partial a_t^h}}{{\partial z_t^h}}\\ {\rm{ }} = \left( {{{\left( {{W_{hh}}} \right)}^T}\delta _{t + 1}^h + {{\left( {{W_{hy}}} \right)}^T}\delta _t^k} \right) \times \left( {1 - \tanh^{2} \left( {a_t^h} \right)} \right)\\ {W_{ih}} = {W_{ih}} - \eta \delta _t^h{\left( {{x_t}} \right)^T}\\ {W_{hh}} = {W_{hh}} - \eta \delta _t^h{\left( {a_{t - 1}^h} \right)^T}\\ {W_{hy}} = {W_{hy}} - \eta \delta _t^k{\left( {a_t^h} \right)^T} \end{array} δtk​=∂ak∂E​∂zk∂ak​={i=j,aj​−1i≠​j,aj​​δTh​=∂aTk​∂E​∂zTk​∂aTk​​∂aTh​∂zTk​​∂zTh​∂aTh​​=(Why​)TδTk​×(1−tanh2(ath​))δth​=∂atk​∂E​∂ztk​∂atk​​∂ath​∂ztk​​∂zth​∂ath​​+∂at+1k​∂E​∂zt+1k​∂at+1k​​∂at+1h​∂zt+1k​​∂zt+1h​∂at+1h​​∂ath​∂zt+1h​​∂zth​∂ath​​=((Whh​)Tδt+1h​+(Why​)Tδtk​)∂zth​∂ath​​=((Whh​)Tδt+1h​+(Why​)Tδtk​)×(1−tanh2(ath​))Wih​=Wih​−ηδth​(xt​)TWhh​=Whh​−ηδth​(at−1h​)TWhy​=Why​−ηδtk​(ath​)T​

這裡主要是softmax求導可能會有問題,這裡我推薦一個文章,寫得很好。softmax求導

def backPropagation(self, data, label, alpha = 0.002):  #反向傳播
        hidden, output = self.forward(data)  #(N, 6000)
        T = len(output) #時間長度=詞向量的長度
        deltaHPre = np.zeros((self.hiddenDim, 1))   #前一時刻的隐含層偏導
        WihUpdata = np.zeros(self.Wih.shape)    #權重更新值
        WhhUpdata = np.zeros(self.Whh.shape)
        WhyUpdata = np.zeros(self.Why.shape)
        for t in range(T-1, -1, -1):
            X = np.zeros((self.wordDim, 1))  # (6000,1)
            X[data[t]][0] = 1   #建構出詞向量
            output[t][label[t]][0] -= 1 #求導後,輸出結點的誤差跟output隻差在i=j時需要把值減去1
            deltaK = output[t].copy()   #輸出結點的誤差
            deltaH = np.multiply(np.add(np.dot(self.Whh.T, deltaHPre),np.dot(self.Why.T, deltaK)), (1 - (hidden[t] ** 2)))  #隐含層結點誤差
            deltaHPre=deltaH.copy()
            WihUpdata += np.dot(deltaH, X.T)
            WhhUpdata += np.dot(deltaH, hidden[t-1].T)
            WhyUpdata += np.dot(deltaK, hidden[t].T)
        self.Wih -= alpha * WihUpdata
        self.Whh -= alpha * WhhUpdata
        self.Why -= alpha * WhyUpdata
           

最後就是訓練了。

def train(self,dataSet, labels):    #訓練
        N = len(dataSet)
        for i in range(N):
            if (i % 100 == 0 and i >= 100):
                self.calcEAll(dataSet[i-100:i], labels[i-100:i])
            self.backPropagation(dataSet[i], labels[i])
        pass
           

到這裡,RNN的核心部分就是如此,這時可以利用RNN來進行文本生成了。

繼續閱讀