RNN原理
- 循環神經網絡:處理序列模型,權值共享。
h[t] = fw(h[t-1], x[t]) #fw is some function with parameters W h[t] = tanh(W[h,h]*h[t-1] + W[x,h]*x[t]) #to be specific y[t] = W[h,y]*h[t]
- Sequence to Sequence 模型示意圖
- 語言模型示意圖
RNN實戰
- 3個句子,一個句子10個單詞,一個單詞100維
- 基礎用法
rnn = nn.RNN(100,10) #input_size(feature_len)=100,hidden_len = 10 rnn._parameters.keys() #how many tensor(4個l0層的w和b) nn.RNN(input_size,hidden_size, num_layers = 1) #h0 : [layer, batchsz, hidden] x : [seq, batchsz, input ] #ht : [layer, batchsz, hidden] out: [seq, batchsz, hidden] out, ht = forward(x, h0) #對于多層rnn,out不變(所有時間最後mem狀态),ht變為[2,~,~](所有層最後時間狀态) cell = nn.RNNcell() #參數與nn.RNN()相同,但是每個時間戳都要輸入xt for xt in x: h1 = cell(xt,h1) #如果你想在輸入x時按這個格式:[batchsz, seq_len, input] #需要在nn.RNN()中加參數: batch_first = True
- 建一個非常簡單的RNN模型(以一個簡單時間序列預測為例)
class Net(nn.Module): def __init__(self,input_size,hidden_size): super(net, self).__init__() self.rnn = nn.RNN( input_size=input_size, hidden_size=hidden_size, num_layers=1, batch_first=True, ) for p in self.rnn.parameters():#正态分布的權值初始化 nn.init.normal_(p, mean=0.0, std=0.001) self.linear = nn.Linear(hidden_size, output_size) def forward(self,x,h0): out, ht = self.rnn(x, h0) #out:[batch_sz,seq,hidden_sz] out = out.view(-1, hidden_size)#out:[seq,hidden_size](b=1) out = self.linear(out) #out:[seq,output_size] out = out.unsqueeze(dim=0) #out:[1,seq,output_size] return out, ht
- 由于我們在對RNN進行反向傳播求梯度的時候,最後的求導項裡面有一個 W h h k W_{hh}^{k} Whhk,會導緻梯度爆炸或梯度消失。
# 解決梯度爆炸 for p in net.parameters(): torch.nn.utils.clip_grad_norm_(p, 10) # 保證梯度的絕對值小于10 # 解決梯度離散:LSTM
- LSTM:Long Short-Term Memory(結構如下圖) 有三個門:分别是遺忘門— f f f,輸入門— i i i,輸出門— o o o。對于輸入輸出變量: c t − 1 c_{t-1} ct−1是輸入的memory(新增的,為了解決梯度離散增強記憶能力), x t x_t xt是輸入, h t − 1 h_{t-1} ht−1是前一個時間單元的輸出, c t c_{t} ct是傳到下一個時間單元的memory。
- 遺忘門: f t = σ ( W f × [ h t − 1 , x t ] + b f ) f_t = \sigma(W_f \times [h_{t-1},x_{t}]+b_f) ft=σ(Wf×[ht−1,xt]+bf)
- 輸入門: i t = σ ( W i × [ h t − 1 , x t ] + b i ) i_t = \sigma(W_i \times [h_{t-1},x_{t}]+b_i) it=σ(Wi×[ht−1,xt]+bi)
- 輸出門: o t = σ ( W o × [ h t − 1 , x t ] + b o ) o_t = \sigma(W_o \times [h_{t-1},x_{t}]+b_o) ot=σ(Wo×[ht−1,xt]+bo)
-
過濾輸入: c t ~ = t a n h ( W c × [ h t − 1 , x t ] + b c ) \widetilde{c_t} = tanh(W_c \times [h_{t-1},x_{t}]+b_c) ct
=tanh(Wc×[ht−1,xt]+bc),得到的結果是過濾後的輸入
那麼,新的memory就等于 “遺忘門作用後保留的之前的memory” + “輸入門作用後保留的新增的過濾後的輸入”, 即 c t = f t × c t − 1 + i t × c t ~ c_t = f_t \times c_{t-1} + i_t \times \widetilde{c_t} ct=ft×ct−1+it×ct
。而新的輸出(h)等于輸出門作用後保留的"經tanh處理過的新的memory",即 h t = o t × t a n h ( c t ) h_t = o_t \times tanh(c_t) ht=ot×tanh(ct)。
- LSTM layer
# initial nn.LSTM(input_size,hidden_size, num_layers = 1) # forward # x : [seq, batchsz, input] out : [seq, batchsz, hidden] # h/c : [layer, batchsz, hidden] out, (ht, ct) = lstm(x, [h0, c0]) # silimar to LSTMcell cell = nn.LSTMcell(~) for xt in x: h, c = cell(xt, [h, c])
情感分類問題實戰
- 例如淘寶,對好評差評分類。模型如下。分别對每個詞embedding後送入RNN,對所有輸出綜合出情感類别。
- 加載資料集(很重要的包__torchtext__)
from torchtext import data, datasets # data.Field() : 預設在空格上拆分字元串,token設定為spacy表示英語分詞 # 用來定義字段的處理方法 TEXT = data.Field(tokenize='spacy') # LabelField是Field的子類,專門用于處理标簽 LABEL = data.LabelField(dtype=torch.float) # 加載IMDB電影評論資料集 train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
- 使用Glove詞向量模型建構語料庫,并将處理後的資料進行batch操作。BucketIterator的作用是按相似長度分為若幹批,每一批進行對應長度的補齊操作。
TEXT.build_vocab(train_data,max_size=10000,vectors='glove.6B.100d') LABEL.build_vocab(train_data) train_iterator, test_iterator = data.BucketIterator.splits( (train_data, test_data), batch_size = batchsz, device=device )
- 網絡結構
class LSTM_Net(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_dim): super(LSTM_Net, self).__init__() # [0-10001] => [100] [vb -> embedding] self.embedding = nn.Embedding(vocab_size, embedding_dim) # [100] => [256] [embedding -> hidden] self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=2, bidirectional=True, dropout=0.5) # [256*2] => [1] self.fc = nn.Linear(hidden_dim*2, 1) self.dropout = nn.Dropout(0.5) def forward(self, x): # [seq, batchsz, 1(字元串)] => [seq, batchsz, 100] embedding = self.dropout(self.embedding(x)) # output: [seq, batchsz, hidden*2] 因為是雙層 # hidden/cell: [layers, batchsz, hidden] # hidden 是每個時間戳的輸出 output, (hidden, cell) = self.lstm(embedding) # [layers*2, batchsz, hidden] => [batchsz, hidden*2] # torch.cat():把前面兩個torch按次元1拼接起來 hidden = torch.cat([hidden[-2], hidden[-1]], dim=1) # [batchsz, hidden*2] => [b, 1] hidden = self.dropout(hidden) out = self.fc(hidden) return out
- Embedding初始化
rnn = LSTM_Net(len(TEXT.vocab), 100, 256) pretrained_embedding = TEXT.vocab.vectors # 指定初始權重(由glove得到) rnn.embedding.weight.data.copy_(pretrained_embedding)# 将初始化權重導入
- 定義優化器及損失函數
optimizer = optim.Adam(rnn.parameters(), lr=1e-3) criteon = nn.BCEWithLogitsLoss() # 二分類交叉熵損失函數
- 訓練與測試
def binary_acc(preds, y): preds = torch.round(torch.sigmoid(preds)) correct = torch.eq(preds, y).float() acc = correct.sum() / len(correct) return acc def train(rnn, iterator, optimizer, criteon): avg_acc = [] lstm.train() for i, batch in enumerate(iterator): # [seq, b] => [b, 1] => [b] pred = lstm(batch.text).squeeze(1) loss = criteon(pred, batch.label) acc = binary_acc(pred, batch.label).item() avg_acc.append(acc) optimizer.zero_grad() loss.backward() optimizer.step() avg_acc = np.array(avg_acc).mean() print('train acc:', avg_acc) def eval(rnn, iterator, criteon): avg_acc = [] lstm.eval() with torch.no_grad(): for batch in iterator: # [b, 1] => [b] pred = lstm(batch.text).squeeze(1) loss = criteon(pred, batch.label) acc = binary_acc(pred, batch.label).item() avg_acc.append(acc) avg_acc = np.array(avg_acc).mean() print('test acc:', avg_acc)