1. 程式人生 > 程式設計 >pytorch之新增BN的實現

pytorch之新增BN的實現

pytorch之新增BN層

批標準化

模型訓練並不容易,特別是一些非常複雜的模型,並不能非常好的訓練得到收斂的結果,所以對資料增加一些預處理,同時使用批標準化能夠得到非常好的收斂結果,這也是卷積網路能夠訓練到非常深的層的一個重要原因。

資料預處理

目前資料預處理最常見的方法就是中心化和標準化,中心化相當於修正資料的中心位置,實現方法非常簡單,就是在每個特徵維度上減去對應的均值,最後得到 0 均值的特徵。標準化也非常簡單,在資料變成 0 均值之後,為了使得不同的特徵維度有著相同的規模,可以除以標準差近似為一個標準正態分佈,也可以依據最大值和最小值將其轉化為 -1 ~ 1之間,這兩種方法非常的常見,如果你還記得,前面我們在神經網路的部分就已經使用了這個方法實現了資料標準化,至於另外一些方法,比如 PCA 或者 白噪聲已經用得非常少了。

Batch Normalization

前面在資料預處理的時候,儘量輸入特徵不相關且滿足一個標準的正態分佈,

這樣模型的表現一般也較好。但是對於很深的網路結構,網路的非線性層會使得輸出的結果變得相關,且不再滿足一個標準的 N(0,1) 的分佈,甚至輸出的中心已經發生了偏移,這對於模型的訓練,特別是深層的模型訓練非常的困難。

所以在 2015 年一篇論文提出了這個方法,批標準化,簡而言之,就是對於每一層網路的輸出,對其做一個歸一化,使其服從標準的正態分佈,這樣後一層網路的輸入也是一個標準的正態分佈,所以能夠比較好的進行訓練,加快收斂速度。batch normalization 的實現非常簡單,對於給定的一個 batch 的資料

演算法的公式如下

第一行和第二行是計算出一個 batch 中資料的均值和方差,接著使用第三個公式對 batch 中的每個資料點做標準化,ϵ是為了計算穩定引入的一個小的常數,通常取 ,最後利用權重修正得到最後的輸出結果,非常的簡單,
實現一下簡單的一維的情況,也就是神經網路中的情況

import sys
sys.path.append('..')

import torch
def simple_batch_norm_1d(x,gamma,beta):
 eps = 1e-5
 x_mean = torch.mean(x,dim=0,keepdim=True) # 保留維度進行 broadcast
 x_var = torch.mean((x - x_mean) ** 2,keepdim=True)
 x_hat = (x - x_mean) / torch.sqrt(x_var + eps)
 return gamma.view_as(x_mean) * x_hat + beta.view_as(x_mean)
x = torch.arange(15).view(5,3)
gamma = torch.ones(x.shape[1])
beta = torch.zeros(x.shape[1])
print('before bn: ')
print(x)
y = simple_batch_norm_1d(x,beta)
print('after bn: ')
print(y)

可以看到這裡一共是 5 個數據點,三個特徵,每一列表示一個特徵的不同資料點,使用批標準化之後,每一列都變成了標準的正態分佈這個時候會出現一個問題,就是測試的時候該使用批標準化嗎?答案是肯定的,因為訓練的時候使用了,而測試的時候不使用肯定會導致結果出現偏差,但是測試的時候如果只有一個數據集,那麼均值不就是這個值,方差為 0 嗎?這顯然是隨機的,所以測試的時候不能用測試的資料集去算均值和方差,而是用訓練的時候算出的移動平均均值和方差去代替

實現以下能夠區分訓練狀態和測試狀態的批標準化方法

def batch_norm_1d(x,beta,is_training,moving_mean,moving_var,moving_momentum=0.1):
 eps = 1e-5
 x_mean = torch.mean(x,keepdim=True)
 if is_training:
 x_hat = (x - x_mean) / torch.sqrt(x_var + eps)
 moving_mean[:] = moving_momentum * moving_mean + (1. - moving_momentum) * x_mean
 moving_var[:] = moving_momentum * moving_var + (1. - moving_momentum) * x_var
 else:
 x_hat = (x - moving_mean) / torch.sqrt(moving_var + eps)
 return gamma.view_as(x_mean) * x_hat + beta.view_as(x_mean)

下面使用深度神經網路分類 mnist 資料集的例子來試驗一下批標準化是否有用

import numpy as np
from torchvision.datasets import mnist # 匯入 pytorch 內建的 mnist 資料
from torch.utils.data import DataLoader
from torch import nn
from torch.autograd import Variable

使用內建函式下載 mnist 資料集

train_set = mnist.MNIST('./data',train=True)
test_set = mnist.MNIST('./data',train=False)

def data_tf(x):
 x = np.array(x,dtype='float32') / 255
 x = (x - 0.5) / 0.5 # 資料預處理,標準化
 x = x.reshape((-1,)) # 拉平
 x = torch.from_numpy(x)
 return x

train_set = mnist.MNIST('./data',train=True,transform=data_tf,download=True) # 重新載入資料集,申明定義的資料變換
test_set = mnist.MNIST('./data',train=False,download=True)
train_data = DataLoader(train_set,batch_size=64,shuffle=True)
test_data = DataLoader(test_set,batch_size=128,shuffle=False)
class multi_network(nn.Module):
 def __init__(self):
 super(multi_network,self).__init__()
 self.layer1 = nn.Linear(784,100)
 self.relu = nn.ReLU(True)
 self.layer2 = nn.Linear(100,10)
 
 self.gamma = nn.Parameter(torch.randn(100))
 self.beta = nn.Parameter(torch.randn(100))
 
 self.moving_mean = Variable(torch.zeros(100))
 self.moving_var = Variable(torch.zeros(100))
 
 def forward(self,x,is_train=True):
 x = self.layer1(x)
 x = batch_norm_1d(x,self.gamma,self.beta,is_train,self.moving_mean,self.moving_var)
 x = self.relu(x)
 x = self.layer2(x)
 return x
net = multi_network()
# 定義 loss 函式
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(),1e-1) # 使用隨機梯度下降,學習率 0.1

from datetime import datetime
import torch
import torch.nn.functional as F
from torch import nn
from torch.autograd import Variable
def get_acc(output,label):
 total = output.shape[0]
 _,pred_label = output.max(1)
 num_correct = (pred_label == label).sum().item()
 return num_correct / total


#定義訓練函式
def train(net,train_data,valid_data,num_epochs,optimizer,criterion):
 if torch.cuda.is_available():
 net = net.cuda()
 prev_time = datetime.now()
 for epoch in range(num_epochs):
 train_loss = 0
 train_acc = 0
 net = net.train()
 for im,label in train_data:
  if torch.cuda.is_available():
  im = Variable(im.cuda()) # (bs,3,h,w)
  label = Variable(label.cuda()) # (bs,w)
  else:
  im = Variable(im)
  label = Variable(label)
  # forward
  output = net(im)
  loss = criterion(output,label)
  # backward
  optimizer.zero_grad()
  loss.backward()
  optimizer.step()
 train_loss += loss.item()
 train_acc += get_acc(output,label)

 cur_time = datetime.now()
 h,remainder = divmod((cur_time - prev_time).seconds,3600)
 m,s = divmod(remainder,60)
 time_str = "Time %02d:%02d:%02d" % (h,m,s)
 if valid_data is not None:
 valid_loss = 0
 valid_acc = 0
 net = net.eval()
 for im,label in valid_data:
  if torch.cuda.is_available():
  im = Variable(im.cuda(),volatile=True)
  label = Variable(label.cuda(),volatile=True)
  else:
  im = Variable(im,volatile=True)
  label = Variable(label,volatile=True)
  output = net(im)
  loss = criterion(output,label)
  valid_loss += loss.item()
  valid_acc += get_acc(output,label)
 epoch_str = (
  "Epoch %d. Train Loss: %f,Train Acc: %f,Valid Loss: %f,Valid Acc: %f,"
  % (epoch,train_loss / len(train_data),train_acc / len(train_data),valid_loss / len(valid_data),valid_acc / len(valid_data)))
 else:
 epoch_str = ("Epoch %d. Train Loss: %f," %
   (epoch,train_acc / len(train_data)))
 prev_time = cur_time
 print(epoch_str + time_str)

train(net,test_data,10,criterion)

#這裡的 γ都作為引數進行訓練,初始化為隨機的高斯分佈,

#moving_mean 和 moving_var 都初始化為 0,並不是更新的引數,訓練完 10 次之後,我們可以看看移動平均和移動方差被修改為了多少

#打出 moving_mean 的前 10 項

print(net.moving_mean[:10])
no_bn_net = nn.Sequential(
 nn.Linear(784,100),nn.ReLU(True),nn.Linear(100,10)
)

optimizer = torch.optim.SGD(no_bn_net.parameters(),1e-1) # 使用隨機梯度下降,學習率 0.1
train(no_bn_net,criterion)

可以看到雖然最後的結果兩種情況一樣,但是如果我們看前幾次的情況,可以看到使用批標準化的情況能夠更快的收斂,因為這只是一個小網路,所以用不用批標準化都能夠收斂,但是對於更加深的網路,使用批標準化在訓練的時候能夠很快地收斂從上面可以看到,我們自己實現了 2 維情況的批標準化,對應於卷積的 4 維情況的標準化是類似的,只需要沿著通道的維度進行均值和方差的計算,但是我們自己實現批標準化是很累的,pytorch 當然也為我們內建了批標準化的函式,一維和二維分別是 torch.nn.BatchNorm1d() 和 torch.nn.BatchNorm2d(),不同於我們的實現,pytorch 不僅將
γ
β作為訓練的引數,也將 moving_mean 和 moving_var 也作為引數進行訓練

下面在卷積網路下試用一下批標準化看看效果

def data_tf(x):
 x = np.array(x,dtype='float32') / 255
 x = (x - 0.5) / 0.5 # 資料預處理,標準化
 x = torch.from_numpy(x)
 x = x.unsqueeze(0)
 return x

train_set = mnist.MNIST('./data',shuffle=False)

使用批標準化

class conv_bn_net(nn.Module):
 def __init__(self):
 super(conv_bn_net,self).__init__()
 self.stage1 = nn.Sequential(
  nn.Conv2d(1,6,padding=1),nn.BatchNorm2d(6),nn.MaxPool2d(2,2),nn.Conv2d(6,16,5),nn.BatchNorm2d(16),2)
 )
 
 self.classfy = nn.Linear(400,10)
 def forward(self,x):
 x = self.stage1(x)
 x = x.view(x.shape[0],-1)
 x = self.classfy(x)
 return x

net = conv_bn_net()
optimizer = torch.optim.SGD(net.parameters(),1e-1) # 使用隨機梯度下降,學習率 0.1
train(net,5,criterion)

不使用批標準化

class conv_no_bn_net(nn.Module):
 def __init__(self):
 super(conv_no_bn_net,-1)
 x = self.classfy(x)
 return x

net = conv_no_bn_net()
optimizer = torch.optim.SGD(net.parameters(),criterion)

以上這篇pytorch之新增BN的實現就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。