1. 程式人生 > >從頭學pytorch(七):dropout防止過擬合

從頭學pytorch(七):dropout防止過擬合

上一篇講了防止過擬合的一種方式,權重衰減,也即在loss上加上一部分\(\frac{\lambda}{2n} \|\boldsymbol{w}\|^2\),從而使得w不至於過大,即不過分偏向某個特徵.

這一篇介紹另一種防止過擬合的方法,dropout,即丟棄某些神經元的輸出.由於每次訓練的過程裡,丟棄掉哪些神經元的輸出都是隨機的,從而可以使得模型不過分依賴於某些神經元的輸出,從而達到防止過擬合的目的.

需要注意的一點是:並不是簡單地丟棄掉某些神經元的輸出,對留下的輸出,我們要改變他們的值,以保證丟棄前後的輸出的期望不變

比如,對如下神經網路:輸入個數為4,隱藏單元個數為5,且隱藏單元\(h_i\)(\(i=1, \ldots, 5\))的計算表示式為

\[ h_i = \phi\left(x_1 w_{1i} + x_2 w_{2i} + x_3 w_{3i} + x_4 w_{4i} + b_i\right) \]

這裡\(\phi\)是啟用函式,\(x_1, \ldots, x_4\)是輸入,隱藏單元\(i\)的權重引數為\(w_{1i}, \ldots, w_{4i}\),偏差引數為\(b_i\)。當對該隱藏層使用丟棄法時,該層的隱藏單元將有一定概率被丟棄掉。設丟棄概率為\(p\),那麼有\(p\)的概率\(h_i\)會被清零,有\(1-p\)的概率\(h_i\)會除以\(1-p\)做拉伸。丟棄概率是丟棄法的超引數。具體來說,設隨機變數\(\xi_i\)為0和1的概率分別為\(p\)和\(1-p\)。使用丟棄法時我們計算新的隱藏單元\(h_i'\)

\[ h_i' = \frac{\xi_i}{1-p} h_i \]

由於\(E(\xi_i) = 1-p\),因此

\[ E(h_i') = \frac{E(\xi_i)}{1-p}h_i = h_i \]

從零開始實現

以一個三層的多層感知機為例.兩個隱藏層的輸出個數都是256.

匯入必要的包

import torch
import torch.nn as nn
import numpy as np
import sys
import utils

utils.py中包含了常用的一些實現,比如資料集載入,sgd實現之類的函式都實現在了這個檔案裡.

實現dropout

def dropout(X,drop_prob):
    X = X.float()
    keep_prob = 1 - drop_prob

    if keep_prob == 0:
        return torch.zeros_like(X)
    mask = (torch.rand(X.shape) < keep_prob).float()
    #print("mask:\n",mask)

    return mask * X/keep_prob

X=torch.arange(16).view(2,8)

drop_prob是丟棄的概率.其中我們用torch.rand(X.shape) < keep_prob去生成一組bool數,表明要丟棄哪些,保留哪些.

關於torch中幾種分佈的用法,參考https://zhuanlan.zhihu.com/p/31231210

  • 均勻分佈
    torch.rand(*sizes, out=None) → Tensor
    返回一個張量,包含了從區間[0, 1)的均勻分佈中抽取的一組隨機數。張量的形狀由引數sizes定義。

  • 標準正態分佈
    torch.randn(*sizes, out=None) → Tensor
    返回一個張量,包含了從標準正態分佈(均值為0,方差為1,即高斯白噪聲)中抽取的一組隨機數。張量的形狀由引數sizes定義。

  • 離散正態分佈
    torch.normal(means, std, out=None) → → Tensor
    返回一個張量,包含了從指定均值means和標準差std的離散正態分佈中抽取的一組隨機數。
    標準差std是一個張量,包含每個輸出元素相關的正態分佈標準差。

  • 線性間距向量
    torch.linspace(start, end, steps=100, out=None) → Tensor
    返回一個1維張量,包含在區間start和end上均勻間隔的step個點。
    輸出張量的長度由steps決定。

載入資料

batch_size,num_workers = 256,4
train_iter,test_iter = utils.load_data(batch_size,num_workers)

utils.py中

def load_data(batch_size,num_workers):
    mnist_train = torchvision.datasets.FashionMNIST(root='/home/sc/disk/keepgoing/learn_pytorch/Datasets/FashionMNIST',
                                                    train=True, download=True,
                                                    transform=transforms.ToTensor())
    mnist_test = torchvision.datasets.FashionMNIST(root='/home/sc/disk/keepgoing/learn_pytorch/Datasets/FashionMNIST',
                                                train=False, download=True,
                                                transform=transforms.ToTensor())

    train_iter = torch.utils.data.DataLoader(
        mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(
        mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    
    return train_iter,test_iter

定義模型引數

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)

params = [W1, b1, W2, b2, W3, b3]

模型定義

drop_prob1, drop_prob2 = 0.2, 0.5
def net(X, is_training=True):
    X = X.view(-1, num_inputs)
    H1 = (torch.matmul(X, W1) + b1).relu()
    if is_training:  # 只在訓練模型時使用丟棄法
        H1 = dropout(H1, drop_prob1)  # 在第一層全連線後新增丟棄層
    H2 = (torch.matmul(H1, W2) + b2).relu()
    if is_training:
        H2 = dropout(H2, drop_prob2)  # 在第二層全連線後新增丟棄層
    return torch.matmul(H2, W3) + b3

通常,傾向於在更靠近原始輸入的層,drop概率更低.

精度評估函式定義

def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        if isinstance(net, torch.nn.Module):
            net.eval() # 評估模式, 這會關閉dropout
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
            net.train() # 改回訓練模式
        else: # 自定義的模型
            if('is_training' in net.__code__.co_varnames): # 如果有is_training這個引數
                # 將is_training設定成False
                acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() 
            else:
                acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() 
        n += y.shape[0]
    return acc_sum / n

損失函式

loss = nn.CrossEntropyLoss()

優化演算法

def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad / batch_size # 注意這裡更改param時用的param.data

訓練

注意一點:這裡的loss我們是直接用的torch的nn.CrossEntropyLoss(),這個loss已經是除了batch_size,是求了均值的.所以l很小,相應的梯度也很小.
而更新引數的時候用的是手寫的sgd,又除了一次batch_size.所以為了快速收斂,lr要設定大一點.

def train(train_iter,test_iter,num_epochs,net,loss,params,sgd,batch_size,lr):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X,y in train_iter:
            y_hat=net(X)   #前向傳播
            """
            這裡的loss我們是直接用的torch的nn.CrossEntropyLoss(),這個loss是求了均值的.l很小,相應的梯度也很小.
            更新引數的時候用的是手寫的sgd. lr要設定大一點.
            """
            l = loss(y_hat,y).sum()#計算loss
            
            #清空梯度
            if params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()

            l.backward()#反向傳播
            sgd(params, lr, batch_size) #更新引數

            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train_acc %.3f,test_acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum/n, test_acc))

num_epochs,lr=5,100
train(train_iter,test_iter,num_epochs,net,loss,params,sgd,batch_size,lr)

輸出如下:

epoch 1, loss 0.0045, train_acc 0.558,test_acc 0.736
epoch 2, loss 0.0023, train_acc 0.786,test_acc 0.792
epoch 3, loss 0.0019, train_acc 0.825,test_acc 0.796
epoch 4, loss 0.0017, train_acc 0.839,test_acc 0.845
epoch 5, loss 0.0016, train_acc 0.850,test_acc 0.823

簡潔實現

資料載入

同前

模型定義及模型引數初始化

net = nn.Sequential(
    utils.FlattenLayer(),
    nn.Linear(num_inputs,num_hiddens1),
    nn.ReLU(),
    nn.Dropout(drop_prob1),
    nn.Linear(num_hiddens1,num_hiddens2),
    nn.ReLU(),
    nn.Dropout(drop_prob2),
    nn.Linear(num_hiddens2,10)
)

直接使用nn.Dropout()類.

for param in net.parameters():
    nn.init.normal_(param,mean=0,std=0.01)

精度評估函式定義

同前. 要注意的是在測試模式下,我們不想做dropout.要net.eval()關閉dropout.

def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        if isinstance(net, torch.nn.Module):
            net.eval() # 評估模式, 這會關閉dropout
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
            net.train() # 改回訓練模式
        else: # 自定義的模型
            if('is_training' in net.__code__.co_varnames): # 如果有is_training這個引數
                # 將is_training設定成False
                acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() 
            else:
                acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() 
        n += y.shape[0]
    return acc_sum / n

損失函式

loss = nn.CrossEntropyLoss()

優化演算法

optimizer = torch.optim.SGD(net.parameters(),lr=0.5)

用torch.optim.SGD(net.parameters(),lr=0.5)表明用隨機梯度下降法去計算net.parameters()的梯度.

 訓練

  • 前向傳播,計算輸出
  • 計算loss
  • 反向傳播,計算梯度
  • 根據梯度,更新引數
  • 迴圈往復,輸入下一個batch的資料
def train_use_torch(train_iter,test_iter,num_epochs,net,loss,params,optimizer,batch_size,lr):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X,y in train_iter:
            y_hat = net(X)  #前向傳播,計算輸出
            l = loss(y_hat,y).sum()#計算loss
            optimizer.zero_grad()#梯度清空

            l.backward()#反向傳播,計算梯度

            optimizer.step()#根據梯度,更新引數
            
            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train_acc %.3f,test_acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum/n, test_acc))

num_epochs,lr=5,0.5
train_use_torch(train_iter,test_iter,num_epochs,net,loss,params,optimizer,batch_size,lr)

輸出

epoch 1, loss 0.0045, train_acc 0.556,test_acc 0.745
epoch 2, loss 0.0023, train_acc 0.787,test_acc 0.817
epoch 3, loss 0.0019, train_acc 0.821,test_acc 0.803
epoch 4, loss 0.0017, train_acc 0.837,test_acc 0.809
epoch 5, loss 0.0016, train_acc 0.847,test_acc 0.840