前言
Github:代碼下載
由於RNN具有記憶功能,之前文章有介紹RNN來實現二進制相加,並取得了比較好的效果。那這次本文使用RNN來進行古詩生成。
數據集
數據集就是我們的古詩了,每行都是一首古詩,並且以格式"題目:古詩"。
首先需要創建一個詞典,詞典可以是每個字的詞頻高的前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)
最開始先初始化詞典大小(維度),一集隱含層大小。
前向傳播
這裏先給出前向傳播的公式:
相比上一節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
代碼不難懂,跟公式是對應的。
反向傳播
反向傳播的公式:
這裏主要是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來進行文本生成了。