目錄
LSTM(Long Short Term Memory)長短期記憶網絡
GRU(Gated Recurrent Unit)門控循環單元
前言
Stay hungry. Stay Foolish.
RNN(循環神經網絡)
上一篇介紹到深度學習最經典的CNN,這一篇來研究一下深度學習同樣經典的RNN。首先先研究清楚爲什麼我們有了CNN這麼好用的神經網絡後還需要循環神經網絡。
爲什麼要用循環神經網絡(RNN)?
首先上圖是一張普通的神經網絡模型,卷積神經網絡也是這樣的,輸入一張圖片,中間經過一個神經網絡,輸出他的類別。
但是上面的輸入是固定輸入固定輸出的,比如輸入一張圖片,輸出一個類別,那麼如果拿來做輸入不固定,或者輸出不固定,又或者二者都不固定的任務,是不是就沒轍了,比如輸入是一個句子,或者要求輸出是一個句子,而且輸出的句子長度也不固定。循環神經網絡就是用於應對這樣的序列式問題,所以爲了處理這一類變長的數據和序列式的問題,我們需要使用循環神經網絡。
循環神經網絡(RNN)可以處理什麼類型的任務?
它一般可以處理三種類型的任務:1. 輸入不固定,輸出固定單一(多對一問題)2. 輸入固定單一,輸出不固定(一對多問題) 3. 輸入和輸出都不固定(多對多問題)
多對一問題
這樣的問題的模型結構一般是這樣的:
輸入可能是一句話,輸出是一個固定的輸出,這種情況下卷積神經網絡可能是無法處理的,這就需要循環神經網絡,這樣的問題對應到任務上有:文本分類、情感分析(給一句評論輸出這句話的情感態度)
一對多問題
一對多問題的模型結構一般是這樣的:
這種問題可能輸入是一張圖片,輸出是一句話對吧,常見任務很經典的就是:Image Caption(當然輸入前也是要加一層CNN抽特徵的)
多對多問題
對於這種多對多的問題我們也稱爲序列到序列的問題(Seq2Seq),最經典的毫無疑問就是機器翻譯了,輸入一個源語言的句子,輸出一個目標語言的句子。
循環神經網絡結構
單層網絡情況
下圖就是循環神經網絡的一個結構,可以看到有一個自指向的一個箭頭,它代表一箇中間狀態,從頭到尾的輸出通過維持一箇中間狀態來記錄之前輸出的情況(也就是上下文信息),該狀態和下一步的輸入一起作爲輸入。
正向傳播
===> ===>
代表當前輸入,代表上一步輸出的隱藏狀態,他們通過F函數這樣的非線性變換得到這一步輸出的隱藏狀態。W、U、V均爲參數矩陣
損失函數就是每一步的輸出計算一個損失,然後求和作爲損失
最終通過softmax歸一化算出最終概率,正向傳播的過程也就是序列從頭到尾的一個過程。
反向傳播
其過程是序列的尾部到頭部的過程,設E爲Loss的值,梯度下降現在E要對每個參數進行求偏導,保證每個方向都成下降趨勢。
可以看到參數矩陣都是共享的,每一步的W都是一樣的,可以看成是一個變量。
我們計算對W的偏導計算梯度 :
通過一個變換:
但對W求偏導是比較困難的,通過上面的公式可以知道是的函數:
(正向傳播中的公式)
那麼,知道這一點我們的公式可以變換爲:
當 j = 0的時候,對W求偏導是比較容易的,那麼我們的反向傳播的機制就會變成如下的一個流程:
它會從當前時間的損失,一直反向傳播到最開始的狀態,影響到最開始的節點。
存在問題
梯度消失,或者說較遠的步驟對梯度下降貢獻小。
來看看這個激活函數:tanh
可以看到激活函數的導數的範圍是在 [0 , 1] 之間的,那麼我們上面這條公式的中間求偏導假如步驟拉的很長的話,中間有十幾步連乘,那麼每一個 的值都在[ 0, 1 ]之間,假設他們的值都是0.1也就是10的-1次方, 那麼全部相乘也就是相當於10的負十幾次方,這樣的數微乎其微,基本可以當做消失了。那麼說明我們的步驟拉的越長,對梯度的影響就越小,導致梯度消失。
優化方案
對於這種問題我們可以通過只在黃色背景框的部分進行反向傳播,縮短了序列長度。提高了程序執行效率並且解決了這樣的問題。
多層網絡情況
多層的網絡結構和單層的相似,就是再疊多幾層,每一層的梯度下降還是和單層一致,輸出給到下一層
好處
每一層經過一個非線性的變換,它的好處就是可以增加網絡的擬合能力,神經網絡的層數每多一層,它的語義層次,表達能力就更上一層樓,語義信息更強,對於具體的實際應用問題能更好擬合。
雙向網絡結構
我們的序列按照正向的方向輸入循環神經網絡,那麼逆向思維,我們能不能反向輸入網絡中,以未來狀態作爲初始狀態呢,答案是ok的,這種做法就是雙向的網絡。
可以看到上圖,輸入的序列分別從兩個方向輸入網絡中。
好處
如果單向的循環神經網絡,我們當前的狀態只能得到上文的信息,而得不到文末的信息,通過雙向的網絡結構,我們可以在當前時刻獲得上下文的信息,將兩個隱含狀態做拼接給到輸出。提高語言的表達能力。
不足
無法用於實時任務,比如輸入的一句話是實時產生的,一開始的時候不知道最後的幾個單詞是什麼,那麼這種情況句子不完整,就沒有句末的狀態。
LSTM(Long Short Term Memory)長短期記憶網絡
LSTM大家可能都是非常熟悉了,隨便翻幾篇論文基本都能看見,那麼爲什麼要提出LSTM,首先先來看RNN的不足之處。
剛纔說到了RNN的參數共享的問題,也就是每一步的參數矩陣W是共享的,但是思考一個問題,就是一個W的矩陣將承載過多的信息,比如一個訓練集有29000條句子,那麼這個W矩陣要記錄着29000種主謂情況,還有什麼定狀語從句,那麼會導致一個什麼問題,W矩陣信息過載,舉個具體的例子:比如說句子裏面有一個信息,小紅去上學這一特徵,但是這個特徵假設對於我們整體的應用可能是沒有用的,可能這一特徵是有訓練集有,測試集沒有,那麼如果我們把這些無關緊要的特徵全部都記下來了,那我們的模型就變成了只能擬合訓練集,在其他的測試集上表現不好導致過擬合,而且還有過載的問題就是這一個W的矩陣參數有限可能記錄不了那麼多的狀態,有些不重要的信息記下來了,重要的可能記不下來了。
那麼我們RNN的侷限性就比較大了,每一句話過來都要記住全部的信息,對於我們的參數而言也有些負載,於是引入了LSTM選擇性機制。
LSTM與RNN的不同
LSTM是在RNN的單元上進行擴展,加入選擇性機制:
- 選擇性輸入
- 選擇性遺忘
- 選擇性輸出
選擇性機制的實現
通過"門"機制,比如遺忘門等,其實際是通過Sigmoid的函數,Sigmoid的輸出是0或者1,那麼當一個值經過這個Sigmoid後若輸出是0,選擇將其遺忘,輸出是1的話,選擇將其記下來。
LSTM模型結構
上面是一張經典的LSTM的結構圖,可以看到它的大體結構和RNN的模型結構式一樣的,只是在每個單元裏面做了一些工作。
爲了理解模型圖先來看看每個組件代表什麼意思,黃色的框代表一個網絡結構,粉色的圓形代表點積操作,一個箭頭代表向量傳遞,第四個代表向量拼接,最後一個代表向量copy。
記憶單元狀態
這裏就代表一個隱含狀態。隱含狀態會經過一個點乘,也就是一個遺忘門,後面還會經過一個相加,是加入輸入的信息,然後傳給下一個狀態。
遺忘門
是上一個單元的輸出,是當前狀態的輸入,和同時作爲輸入輸入到遺忘門中,經過遺忘門會得到0和1的向量,和上一個隱含狀態做點積,物理意義可以理解爲:當輸入一些新的東西之後,模型會考慮遺忘之前的那些東西。
輸入門
輸入仍是上一個單元的輸出和當前的輸入,可以看到左邊的門機制是傳入門,控制傳入的信息,其機制和遺忘門類似,而右側的tanh看他的公式和RNN的隱含狀態的公式差不多:
輸入通過這樣的激活函數得到一個新的隱含狀態,然後在通過傳入門輸出的 i 進行選擇,他從物理意義上也就是一句話的語義有一部分要輸入有一部分不要輸入,選擇後和隱含狀態進行相加(融入隱含狀態中)
輸出門
和上面兩個門的機制一樣,輸出門是選擇性輸出,輸入通過一個門機制選擇那些東西要輸出,計算出選擇向量,然後和融合完的隱含狀態做點積,得到輸出
上面就是所謂的LSTM模型結構。
GRU(Gated Recurrent Unit)門控循環單元
考慮到LSTM網絡輸入們和遺忘門之間有一些互補的關係,因此同時使用兩個門機制顯得有些冗餘。
於是思考將輸入門和遺忘門進行合併,合併的思路如下:
通過一個門機制互補的形式合併輸入門和遺忘門,並將其叫做更新門
GRU的結構圖如下:
1.輸入門與和遺忘門合併成一個門:更新門 zt:
左側的紅色矩形框相當於原本的遺忘門,通過門機制點積上一個隱含狀態,Sigmoid的取值爲0或1,可以決定哪些特徵遺忘,哪些特徵保留。右側的紅色橢圓框代表原本的輸入門,通過門機制去決定哪些需要輸入,哪些不需要輸入。
2.引入重置門 ,用來控制輸入的內容的計算是否依賴上一時刻的狀態 ,決定上一個隱含狀態,對當前的輸入內容有多少影響。這麼說可能有點抽象,大家可能就會有一個疑問:那麼我不是有輸入門(現融進更新門)控制哪些需要輸入哪些不用輸入嗎?那這個重置門又來控制一遍,不是做了重複工?
解決疑問:OK,這個問題也是我最初的問題,後來思考明白了,首先我們來看輸入內容的公式
可以看到這裏是乘以,那麼可以說明什麼?說明重置門控制的是上一個隱含狀態在輸入內容裏面的佔比
而輸入門的公式是怎麼樣的?
輸入門是乘以整一個輸入內容,那麼兩者控制的內容是不是不一樣?
如果還是很迷可以這麼來看,輸入門控制的是這個整體的比例,而重置門控制的是 中ht-1這一個變量的比例。
3.去除 LSTM 中的內部細胞記憶單元 , 直接在當前狀態 和歷史狀態 之間引入線性依賴關係
LSTM實現簡單文本分類
預處理
數據集:
格式
類別 \t 一段一百字左右的中文新聞\n
樣本截圖:
預處理分詞後結果:
數據集及預處理完畢文件下載:
鏈接:https://pan.baidu.com/s/1H_xSJvfoS3r2bS97KtFsaQ
提取碼:i2h6
代碼
版本:
tensorflow 1.13.1
python 3.6
代碼逐行註釋,所以不做文字說明:
import tensorflow as tf
import os
import sys
import numpy as np
import math
# 打印日誌
tf.logging.set_verbosity(tf.logging.INFO)
def get_default_params():
# 返回對象
return tf.contrib.training.HParams(
# 詞向量大小
num_embedding_size=16,
# 步長
num_timesteps=50,
# lstm單元輸出維度(又叫輸出神經元數)
num_lstm_nodes=[32, 32],
# 層數
num_lstm_layers=2,
# 全連接輸出維度,最後一維
num_fc_nodes=32,
batch_size=100,
# 控制梯度,梯度上線
clip_lstm_grads=1.0,
# 學習率
learning_rate=0.001,
# 詞頻最低下限
num_word_threshold=10
)
# 設置參數
hps = get_default_params()
train_file = 'data/new_train_seg.txt'
val_file = 'data/new_val_seg.txt'
test_file = 'data/new_test_seg.txt'
vocab_file = 'data/new_vocab.txt'
category_file = 'data/new_category.txt'
class Vocab:
def __init__(self, filename, num_word_threshold):
# 詞典
self._word_to_id = {}
# <UNK> 的id(初始值)
self._unk = -1
# 頻率下限
self._num_word_threshold = num_word_threshold
# 將詞典讀出來存到dict裏
self._read_dict(filename)
def _read_dict(self, filename):
with open(filename, 'r', encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
word, frequency = line.strip('\r\n').split('\t')
frequency = int(frequency)
# 低於下限不要
if frequency < self._num_word_threshold:
continue
idx = len(self._word_to_id)
if word == '<UNK>':
# 刷新UNK的id
self._unk = idx
self._word_to_id[word] = idx
def word_to_id(self, word):
# 如果沒有word返回UNK
return self._word_to_id.get(word, self._unk)
@property
def unk(self):
return self._unk
def size(self):
# 返回大小
return len(self._word_to_id)
def sentence_to_id(self, sentence):
# 分詞進字典裏吧id取出來變成list
word_ids = [self.word_to_id(cur_word) for cur_word in sentence.split()]
return word_ids
class CategoryDict:
def __init__(self, filename):
self._category_to_id = {}
# 讀取類別並存入字典,給每個一個id
with open(filename, 'r', encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
category = line.strip('\r\n')
idx = len(self._category_to_id)
self._category_to_id[category] = idx
def size(self):
return len(self._category_to_id)
def category_to_id(self, category):
# 傳入類別返回id
if not category in self._category_to_id:
print("%s is not in our category list" % category)
return self._category_to_id[category]
# 建立詞典
vocab = Vocab(vocab_file, hps.num_word_threshold)
vocab_size = vocab.size()
# 打日誌
tf.logging.info('vocab_size: %d' % vocab_size)
# 建立類別詞典
category_vocab = CategoryDict(category_file)
num_classes = category_vocab.size()
tf.logging.info('num_classes: %d' % num_classes)
class TextDataSet:
def __init__(self, filename, vocab, category_vocab, num_timesteps):
self._vocab = vocab
self._category_vocab = category_vocab
self._num_timesteps = num_timesteps
# matrix
self._inputs = []
# vector
self._outputs = []
self._indicator = 0
self._parse_file(filename)
def _parse_file(self, filename):
tf.logging.info('Loading data from %s', filename)
with open(filename, 'r', encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
label, content = line.strip('\r\n').split('\t')
# 將傳入的類別轉化爲對應的id
id_label = self._category_vocab.category_to_id(label)
# 將傳入的句子轉化爲一個詞一個詞對應的id
id_words = self._vocab.sentence_to_id(content)
# 控制句長在50,超過截斷
id_words = id_words[0: self._num_timesteps]
# padding
padding_num = self._num_timesteps - len(id_words)
id_words = id_words + [
self._vocab.unk for i in range(padding_num)]
# 將句子的向量放到輸入list
self._inputs.append(id_words)
# 將類別的向量放到輸出的list
self._outputs.append(id_label)
self._inputs = np.asarray(self._inputs, dtype=np.int32)
self._outputs = np.asarray(self._outputs, dtype=np.int32)
self._random_shuffle()
self._num_examples = len(self._inputs)
def _random_shuffle(self):
# 整個input的順序
p = np.random.permutation(len(self._inputs))
self._inputs = self._inputs[p]
self._outputs = self._outputs[p]
def num_examples(self):
# 返回輸入list的長度
return self._num_examples
def next_batch(self, batch_size):
# 獲取下一個batch
end_indicator = self._indicator + batch_size
# 當獲取的指針超過數據集大小時,歸0
if end_indicator > len(self._inputs):
self._random_shuffle()
self._indicator = 0
end_indicator = batch_size
# 歸0還超過就報錯
if end_indicator > len(self._inputs):
print("batch_size: %d is too large" % batch_size)
# 取值
batch_inputs = self._inputs[self._indicator: end_indicator]
batch_outputs = self._outputs[self._indicator: end_indicator]
self._indicator = end_indicator
return batch_inputs, batch_outputs
train_dataset = TextDataSet(
train_file, vocab, category_vocab, hps.num_timesteps)
val_dataset = TextDataSet(
val_file, vocab, category_vocab, hps.num_timesteps)
test_dataset = TextDataSet(
test_file, vocab, category_vocab, hps.num_timesteps)
# 創建模型
def create_model(hps, vocab_size, num_classes):
num_timesteps = hps.num_timesteps
batch_size = hps.batch_size
# 佔位,當run的時候賦值
inputs = tf.placeholder(tf.int32, (batch_size, num_timesteps))
outputs = tf.placeholder(tf.int32, (batch_size,))
# dropout
keep_prob = tf.placeholder(tf.float32, name='keep_prob')
# 保存模型運行進度
global_step = tf.Variable(
tf.zeros([], tf.int64), name='global_step', trainable=False)
# embedding的值初始化到-1到1
embedding_initializer = tf.random_uniform_initializer(-1.0, 1.0)
# 上下文管理器
with tf.variable_scope(
'embedding', initializer=embedding_initializer):
embeddings = tf.get_variable(
'embedding',
[vocab_size, hps.num_embedding_size],
tf.float32)
#在embedding的命名空間內創建名字爲embedding的變量
# [1, 10, 7] -> [embeddings[1], embeddings[10], embeddings[7]]
embed_inputs = tf.nn.embedding_lookup(embeddings, inputs)
# 初始化隨機值
scale = 1.0 / math.sqrt(hps.num_embedding_size + hps.num_lstm_nodes[-1]) / 3.0
lstm_init = tf.random_uniform_initializer(-scale, scale)
with tf.variable_scope('lstm_nn', initializer=lstm_init):
# 雙層所以寫一個list
cells = []
for i in range(hps.num_lstm_layers):
# 先設置每一層的
cell = tf.contrib.rnn.BasicLSTMCell(
# 輸出神經元的數量,也就是輸出一個32維的向量
hps.num_lstm_nodes[i],
state_is_tuple=True)
# dropout
cell = tf.contrib.rnn.DropoutWrapper(
cell,
output_keep_prob=keep_prob)
# 雙層的,將每一層加入列表
cells.append(cell)
# 合併兩層
cell = tf.contrib.rnn.MultiRNNCell(cells)
# 初始化隱含狀態爲0
initial_state = cell.zero_state(batch_size, tf.float32)
# rnn_outputs => [batch_size, num_timesteps, lstm_outputs[-1](32)]
rnn_outputs, _ = tf.nn.dynamic_rnn(
cell, embed_inputs, initial_state=initial_state)
# 取最後一步
# [batch_size, 1, lstm_outputs[-1](32)]
last = rnn_outputs[:, -1, :]
# 輸出和全連接層拼接
fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)
with tf.variable_scope('fc', initializer=fc_init):
# 上面lstm的cell的輸出拿過來
# 激活函數用relu
# 第二個參數是輸出維度大小,就是最後一個參數,這裏是沒有變化的
fc1 = tf.layers.dense(last,
hps.num_fc_nodes,
activation=tf.nn.relu,
name='fc1')
# dropout
fc1_dropout = tf.contrib.layers.dropout(fc1, keep_prob)
# [batch_size, 1, lstm_outputs[-1](32)]
# 在經過一層全連接映射到多少個類別上 [batch_size, 1, class_size]
# [batch_size, 1 , 10]
logits = tf.layers.dense(fc1_dropout,
num_classes,
name='fc2')
with tf.name_scope('metrics'):
# 做了softmax並且計算了差值,如果單單softmax => tf.nn.softmax(self.logits)
softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
logits=logits, labels=outputs)
# 計算均值
loss = tf.reduce_mean(softmax_loss)
# [0, 1, 5, 4, 2] -> argmax: 2(下標)
# 這裏第一個參數就是softmax,第二個參數意思是取第幾維最大,比如是1的話就是在第一維裏去最大
y_pred = tf.argmax(tf.nn.softmax(logits),
1,
output_type=tf.int32)
# 計算準確率,看看算對多少個
correct_pred = tf.equal(outputs, y_pred)
# tf.cast 將數據轉換成 tf.float32 類型
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
with tf.name_scope('train_op'):
# 獲得所有可訓練的變量(優化器優化列表中的變量)
tvars = tf.trainable_variables()
# 看看這些變量叫啥
for var in tvars:
tf.logging.info('variable name: %s' % (var.name))
# tf.gradients求導(梯度下降,對每個訓練變量求偏導,每個方向都往下走)
# 不超過hps.clip_lstm_grads,防止梯度爆炸
grads, _ = tf.clip_by_global_norm(
tf.gradients(loss, tvars), hps.clip_lstm_grads)
# Adam優化器
optimizer = tf.train.AdamOptimizer(hps.learning_rate)
# 這裏是應用,將梯度grads應用到變量上,讓函數吧global_step回調
train_op = optimizer.apply_gradients(
zip(grads, tvars), global_step=global_step)
return ((inputs, outputs, keep_prob),
(loss, accuracy),
(train_op, global_step))
placeholders, metrics, others = create_model(
hps, vocab_size, num_classes)
inputs, outputs, keep_prob = placeholders
loss, accuracy = metrics
train_op, global_step = others
def eval_holdout(sess, accuracy, dataset_for_test, batch_size):
# 數據集大小/batch_size 其實就是iter,校驗集拿來校驗一輪
num_batches = dataset_for_test.num_examples() // batch_size
tf.logging.info("Eval holdout: num_examples = %d, batch_size = %d",
dataset_for_test.num_examples(), batch_size)
accuracy_vals = []
for i in range(num_batches):
batch_inputs, batch_labels = dataset_for_test.next_batch(batch_size)
accuracy_val = sess.run(accuracy,
feed_dict = {
inputs: batch_inputs,
outputs: batch_labels,
keep_prob: 1.0,
})
accuracy_vals.append(accuracy_val)
# 計算平均準確率
return np.mean(accuracy_vals)
init_op = tf.global_variables_initializer()
# dropout
train_keep_prob_value = 0.8
# 跑一萬個batch
num_train_steps = 10000
with tf.Session() as sess:
sess.run(init_op)
for i in range(num_train_steps):
batch_inputs, batch_labels = train_dataset.next_batch(
hps.batch_size)
# 那三個佔位符輸進去
# 計算loss, accuracy, train_op, global_step的圖
outputs_val = sess.run([loss, accuracy, train_op, global_step],
feed_dict={
inputs: batch_inputs,
outputs: batch_labels,
keep_prob: train_keep_prob_value,
})
loss_val, accuracy_val, _, global_step_val = outputs_val
# 兩百個batch輸出一次
if global_step_val % 200 == 0:
tf.logging.info("Step: %5d, loss: %3.3f, accuracy: %3.3f"
% (global_step_val, loss_val, accuracy_val))
# 一千個batch校驗一次
if global_step_val % 1000 == 0:
accuracy_eval = eval_holdout(sess, accuracy, val_dataset, hps.batch_size)
accuracy_test = eval_holdout(sess, accuracy, test_dataset, hps.batch_size)
tf.logging.info("Step: %5d, val_accuracy: %3.3f, test_accuracy: %3.3f"
% (global_step_val, accuracy_eval, accuracy_test))
效果截圖:
以上是對循環神經網絡的粗淺認識
本菜鳥學習不好,如有不妥望各位大佬指點
如要轉載請說明原文:https://blog.csdn.net/qq_36652619/article/details/95952068