回圈神經網路RNN和長短期記憶網路LSTM學習筆記

2020-08-10 12:20:39

一、RNN

首先思考一個問題,爲什麼需要RNN?
神經網路只能處理孤立的的輸入,每一個輸入之間是沒有關係的。但是,某些任務需要能夠更好的處理序列的資訊,即前面的輸入和後面的輸入是有關係的,例如音訊、視訊、句子等。
RNN較神經網路的不同是它將上一時刻的輸出作爲輸入一起傳入網路中,從而將輸入聯繫起來。
在这里插入图片描述

在这里插入图片描述
序列依次進入網路中,之前進入序列的數據會儲存資訊而對後面的數據產生影響。圖中X0X1…Xn不是同一時刻輸入網路的,圖中代表的是時間維度,實際上整個網路只有一個A單元,例如t=0時輸入X0,t=1時輸入X1。

  • 網路的輸入:X0,X1,…,Xt
  • 網路的輸出:h0,h1,…,ht,RNN的輸出比較多樣,可只將最後的ht作爲輸出,也可將h0到ht都作爲輸出。(Xt,ht都是代表向量Tensor,不是數值)
    網路輸出可選ht作爲輸出後接全連線層和softmax層將維度轉化爲需要的分類數從而實現序列的分類。

但是這種簡易的RNN結構有兩個致命缺點。

  • 越前面的數據進入序列的時間越早,所以對後面的數據的影響也就越弱,倘若重點在序列的前邊,當最後輸出結果時前邊重點對結果的影響已經接近於0,無法得到理想的結果。(可通過注意力機制 機製改善)
  • 由於網路只有一個A單元,網路的更新過程不過是不停地更新同一套權重,從結果往前進行反向傳播時,梯度會出現兩個結果,當梯度小於1時,多個小於1的梯度相乘,傳到前邊時梯度已經接近於0,發生梯度消失現象,當梯度大於1時,多個大於1的梯度相乘,傳到前方時發生梯度爆炸現象。(通過LSTM結構改善)

二、LSTM

2.1 LSTM結構介紹

在这里插入图片描述
LSTM(Long Short-Term Memory)長短期網路模型,它解決了RNN會遺忘序列前段資訊的問題。
網路外部結構與RNN相同,同樣是只有一個A單元,但A的內部有所不同。

  • LSTM輸入爲x和上一個時刻的h和c,輸出爲該時刻的h和c。
  • A單元內包括三條支路,分別爲遺忘門,輸入門和輸出門。可以將輸出c和h比作劇情的主線和支線,從圖中可以看出c是從頭到尾貫穿的,在上一時刻的h和該時刻的x輸入之後,先經過遺忘門,比較重要程度來看是否要遺忘c的部分內容,之後經過輸入門,根據重要程度看是否要將支線劇情新增至主線,最後經過輸出門獲得現在的狀態。

2.2 LSTM的pytorch程式碼解析

CLASS torch.nn.LSTM(*args, **kwargs)
參數列表

  • input_size – 輸入數據X的向量維度。
  • hidden_size – 隱含數據h的向量維度。
  • num_layers – LSTM 堆疊的層數,預設值是1層,每一層都只有一個A單元。如果設定爲2,第二個LSTM接收第一個LSTM的計算結果。也就是第一層輸入 [ X0 X1 X2 … Xt],計算出 [ h0 h1 h2 … ht ],第二層將 [ h0 h1 h2 … ht ] 作爲 [ X0 X1 X2 … Xt] 輸入再次計算,輸出最後的 [ h0 h1 h2 … ht ]。
  • bias – 若設定爲False,網路就不帶偏置,只有權重。預設爲True。
  • batch_first – 設爲True, 則輸入和輸出格式爲(batch, seq, feature),因爲輸入輸出格式預設爲(seq, batch, feature)。
  • dropout – 若不爲0,在除了最後一層外的單元新增dropout層,drop概率爲傳入的不爲0的數。
  • bidirectional – 如果爲真,則爲雙向LSTM。

輸入格式

  • Inputs: input, (h_0, c_0)
  • input 輸入數據 形狀(序列長度,batchsize,輸入向量維度) (seq_len, batch, input_size)
  • h_0 形狀 (num_layers * num_directions, batch, hidden_size)
  • c_0 形狀(num_layers * num_directions, batch, hidden_size)
    若h0和c0爲提供,則預設爲元素爲0的向量。若batch_first則batch爲首個參數。num_directions預設爲1,雙向時爲2。

輸出格式

  • output, (h_n, c_n)
  • output of shape (seq_len, batch, num_directions * hidden_size) 最後一層的hn(多層情況,一層時即爲所有hn)
  • h_n of shape (num_layers * num_directions, batch, hidden_size) 包含所有層的hn
  • c_n of shape (num_layers * num_directions, batch, hidden_size) 包含所有層的cn

2.3 LSTM實現MINST手寫數位數據集影象分類

參照庫檔案

import torch
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms as T
import matplotlib.pyplot as plt

超參設定

batch_size = 100   # 批次大小
lr = 1e-3          # 學習率
num_epoches = 30   # 訓練迭代週期
use_gpu = torch.cuda.is_available()  # 是否使用cpu

MINST數據集匯入

train_dataset = datasets.MNIST(root='G:/dl_dataset/',train=True,transform=T.ToTensor())
test_dataset = datasets.MNIST(root='G:/dl_dataset/',train=False,transform=T.ToTensor())
train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(test_dataset,batch_size=batch_size,shuffle=False)

定義LSTM結構
要使用的LSTM參數分析。
將影象的每一行作爲一個X輸入LSTM,所以每張圖片都包含一個X0-X27,每個X是28維向量。
定義 input_size=28,hidden_size=128,num_layers = 2,batch_first=True。

class RNN(nn.Module):
    def __init__(self, in_dim, hidden_dim, n_layer, n_class):
        super(RNN, self).__init__()
        self.lstm = nn.LSTM(in_dim, hidden_dim, n_layer, batch_first=True)
        self.classifer = nn.Linear(hidden_dim, n_class)
        
    def forward(self,x):
        out ,_ = self.lstm(x)   # 輸出爲output(batch,seq_len,  hidden_size * num_directions)
        out = out[:,-1,:]        # seq=-1,即取每張圖片數據的最後一個h,即hn
        out = self.classifer(out) # hn後接全連線層,使用softmax損失函數
        return out                # 返回最後的輸出
model = RNN(28,128,2,10)   
# in_dim=28(每個X爲圖片的一行,維度爲28), hidden_dim=128(hn的維度), n_layer=2(LSTM層數爲兩層), n_class(最後的分類數)
if use_gpu:
    model = model.cuda()  # 把模型放到gpu上

定義損失函數和優化器

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=lr)

訓練和測試過程

for epoch in range(num_epoches):
    print('*'*20)
    print("epoch{}".format(epoch+1))
    train_loss = 0.0          # 訓練損失函數計算
    train_acc = 0.0           # 訓練準確度計算
    
    model.train()  # 置模型於訓練模式
    for i, data in enumerate(train_loader):  # 一次回圈讀入一個batch
        img, label = data           # 讀入影象張量和標籤張量
        b, c, h, w = img.size()     # MINST爲黑白單通道圖片
        assert c == 1
        img = img.squeeze(1)  # size b h w  去除通道維度
        if use_gpu:
            img, label = img.cuda(), label.cuda()   # 放入gpu
        out = model(img)  # input(batch,seq_len,in_dim) # 輸入影象進行前向傳播,輸出各分類的得分
        loss = criterion(out,label)   # 計算損失函數
        train_loss += loss.item()     # 記錄總的損失函數
        _,pred = torch.max(out,1)     # 返回得分最大值的索引,dim=0爲batch,dim=1爲每個圖片的分類得分
        num_correct = (pred == label).sum()  # 計算得分最大值索引與label相同的個數
        train_acc += num_correct.item()    # 計算正確總個數
        
        optimizer.zero_grad()     # 梯度清零
        loss.backward()           # 梯度反向傳播
        optimizer.step()          # 優化器前進step
    # 完成一個epoch的訓練,列印訓練的loss和acc
    print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
        epoch + 1, train_loss / (len(train_dataset)), train_acc / (len(
            train_dataset))))    
    
    model.eval()  # 置模型於測試模式,使dropout和bn失去作用
    eval_loss = 0.0  # 測試模式損失記錄
    eval_acc = 0.0   # 測試模式準確率記錄
    for data in test_loader:  一次回圈讀入一個batch
        img, label = data
        b, c, h, w = img.size()
        assert c == 1, 'channel must be 1'
        img = img.squeeze(1)
        if use_gpu:
            img, label = img.cuda(), label.cuda()
        with torch.no_grad():        # 測試模式無需梯度
            out = model(img)
            loss = criterion(out, label)
        eval_loss += loss.item()
        _, pred = torch.max(out, 1)
        num_correct = (pred == label).sum()
        eval_acc += num_correct.item()
    print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
        test_dataset)), eval_acc / (len(test_dataset))))

訓練30個週期在測試集達到99.08%準確率。

********************
epoch30
Finish 30 epoch, Loss: 0.000106, Acc: 0.996667
Test Loss: 0.000364, Acc: 0.990800

儲存模型

torch.save(model.state_dict(),'./RNN.pth')

最終效能測試

model.load_state_dict(torch.load('./RNN.pth'))
print('Final Test')
model.eval()
eval_loss = 0.0
eval_acc = 0.0
for data in test_loader:
    img, label = data
    b, c, h, w = img.size()
    assert c == 1, 'channel must be 1'
    img = img.squeeze(1)
    if use_gpu:
        img, label = img.cuda(), label.cuda()
    with torch.no_grad():
        out = model(img)
        loss = criterion(out, label)
    eval_loss += loss.item()
    _,pred = torch.max(out,1)
    eval_acc += ( pred==label ).float().mean()
print(f'Test Loss: {eval_loss/len(test_loader):.6f}, Acc: {eval_acc/len(test_loader):.6f}')

提取一個範例進行直觀認識

imgs,labels = next(iter(test_loader)) # 提取一個batch
img = imgs[0]  # 取一張圖片
show = img     
show = show.view(28,28)
# img = img.view(img.size(0),-1)
plt.imshow(show.numpy(), cmap='Greys_r')  # 顯示該圖片
img = img.cuda()  
label = labels[0].cuda()  # 圖片和標籤放入gpu
output = model(img)       # 前向傳播
print(label,output,sep='\n')  # 列印各分類得分和標籤
pred = torch.squeeze(torch.max(output,dim=1)[1],dim=0)  #squeeze降維,max找到得分最大的索引 
print(label,pred,sep='\n') # 對比預測值和真實標籤
tensor(7, device='cuda:0')   # label
tensor([[-2.8946, -2.8025,  0.2505, -0.9841,  1.3492, -3.3886, -6.0807, 13.3174,
         -3.3386,  1.1477]], device='cuda:0', grad_fn=<AddmmBackward>)  # output
tensor(7, device='cuda:0')   # label
tensor(7, device='cuda:0')   # 預測標籤

在这里插入图片描述