之前面試過程中被問到過
兩個問題
:
(1)深度學習中batch size的大小對訓練過程的影響是什麼樣的?
(2)有些時候不可避免地要用超大batch,比如人臉識別,可能每個batch要有幾萬甚至幾十萬張人臉影象,訓練過程中超大batch有什麼優缺點,如何儘可能地避免超大batch帶來的負面影響?
-------------------------------面試版回答-------------------------------
在
不考慮Batch Normalization
的情況下(這種情況我們之後會在bn的文章裡專門探討),
先給個自己當時回答的答案吧(相對來說學究一點)
:
(1) 不考慮bn的情況下,batch size的大小決定了深度學習訓練過程中的
完成每個epoch所需的時間
和
每次迭代(iteration)之間梯度的平滑程度。(感謝評論區的韓飛同學提醒,batchsize
只能說影響完成每個epoch所需要的時間,決定也算不上吧。根本原因還是CPU,GPU算力吧。瓶頸如果在CPU,例如隨機資料增強,batch size越大有時候計算的越慢。
)
對於一個大小為N的訓練集,如果每個epoch中mini-batch的取樣方法採用最常規的N個樣本每個都取樣一次,設mini-batch大小為b,那麼每個epoch所需的迭代次數(正向+反向)為
,
因此完成每個epoch所需的時間大致也隨著迭代次數的增加而增加
。
由於目前主流深度學習框架處理mini-batch的反向傳播時,預設都是先將每個mini-batch中每個instance得到的loss平均化之後再反求梯度,也就是說每次反向傳播的梯度是對mini-batch中每個instance的梯度平均之後的結果,所以b的大小決定了相鄰迭代之間的梯度平滑程度,
b太小,相鄰mini-batch間的差異相對過大,那麼相鄰兩次迭代的梯度震盪情況會比較嚴重,不利於收斂
;
b越大,相鄰mini-batch間的差異相對越小,雖然梯度震盪情況會比較小,一定程度上利於模型收斂,但如果b極端大,相鄰mini-batch間的差異過小,相鄰兩個mini-batch的梯度沒有區別了,整個訓練過程就是沿著一個方向蹭蹭蹭往下走,很容易陷入到區域性最小值出不來
。
總結下來:
batch size過小,花費時間多,同時梯度震盪嚴重,不利於收斂;batch size過大,不同batch的梯度方向沒有任何變化,容易陷入區域性極小值。
(2)(
存疑,只是突發奇想
)如果硬體資源允許,想要追求訓練速度使用超大batch,可以採用一次正向+多次反向的方法,避免模型陷入區域性最小值。即使用超大epoch做正向傳播,在反向傳播的時候,分批次做多次反向轉播,比如將一個batch size為64的batch,一次正向傳播得到結果,instance級別求loss(先不平均),得到64個loss結果;反向傳播的過程中,分四次進行反向傳播,每次取16個instance的loss求平均,然後進行反向傳播,這樣可以做到在節約一定的訓練時間,利用起硬體資源的優勢的情況下,避免模型訓練陷入區域性最小值。
-------------------------------通俗版回答-------------------------------
那麼我們可以把第一個問題簡化為一個小時候經常玩的遊戲:
深度學習訓練過程: 貼鼻子
訓練樣本:負責指揮的小朋友們(觀察角度各不一樣)
模型:負責貼的小朋友
模型衡量指標:最終貼的位置和真實位置之間的距離大小
由於每個小朋友站的位置各不一樣,所以他們對鼻子位置的觀察也各不一樣。(訓練樣本的差異性),這時候假設小明是負責貼鼻子的小朋友,小朋友A、B、C、D、E是負責指揮的同學(A, B站在圖的右邊,C,D, E站在左邊),這時候小明如果採用:
每次隨機詢問一個同學,那麼很容易出現,先詢問到了A,A說向左2cm,再問C,C說向右5cm,然後B,B說向左4cm,D說向右3cm,這樣每次指揮的差異都比較大,結果調過來調過去,沒什麼進步。
每次隨機詢問兩個同學,每次取詢問的意見的平均,比如先問到了(A, C),A說向左2cm,C說向右5cm,那就取個均值,向右1。5cm。然後再問(B, D),這樣的話減少了極端情況(前後兩次迭代差異巨大)這種情況的發生,能更好更快的完成遊戲。
每次全問一遍,然後取均值,這樣每次移動的方向都是所有人決定的均值,這樣的話,最後就是哪邊的小朋友多最終結果就被很快的拉向哪邊了。(梯度方向不變,限於極小值)
-------------------------------科學版回答-------------------------------
就用MINST做一下實驗吧(程式碼主要轉自
https://www。
cnblogs。com/jiangnanyan
yuchen/p/9782223。html
):
實驗環境:
1080ti * 1
Pytorch 0。4。1
超引數:SGD(lr = 0。02, momentum=0。5)
偷懶沒有根據batch size細調
我們先建立一個簡單的模型:
from
torch。nn
import
*
import
torch。nn。functional
as
F
class
SimpleModel
(
Module
):
def
__init__
(
self
):
super
(
SimpleModel
,
self
)
。
__init__
()
self
。
conv1
=
Conv2d
(
in_channels
=
1
,
out_channels
=
10
,
kernel_size
=
5
)
self
。
conv2
=
Conv2d
(
10
,
20
,
5
)
self
。
conv3
=
Conv2d
(
20
,
40
,
3
)
self
。
mp
=
MaxPool2d
(
2
)
self
。
fc
=
Linear
(
40
,
10
)
def
forward
(
self
,
x
):
in_size
=
x
。
size
(
0
)
x
=
F
。
relu
(
self
。
mp
(
self
。
conv1
(
x
)))
x
=
F
。
relu
(
self
。
mp
(
self
。
conv2
(
x
)))
x
=
F
。
relu
(
self
。
mp
(
self
。
conv3
(
x
)))
x
=
x
。
view
(
in_size
,
-
1
)
x
=
self
。
fc
(
x
)
(
x
。
size
())
return
F
。
log_softmax
(
x
,
dim
=
1
)
然後把MINST加載出來訓練一下:
import
time
import
torch
import
torch。nn
as
nn
import
torch。nn。functional
as
F
import
torch。optim
as
optim
import
os
from
torchvision
import
datasets
,
transforms
from
simple_model
import
SimpleModel
os
。
environ
[
‘CUDA_VISIBLE_DEVICES’
]
=
“0”
use_cuda
=
torch
。
cuda
。
is_available
()
batch_size
=
6
lr
=
1e-2
# MNIST Dataset
train_dataset
=
datasets
。
MNIST
(
root
=
‘。/data/’
,
train
=
True
,
transform
=
transforms
。
ToTensor
(),
download
=
True
)
test_dataset
=
datasets
。
MNIST
(
root
=
‘。/data/’
,
train
=
False
,
transform
=
transforms
。
ToTensor
())
# Data Loader (Input Pipeline)
train_loader
=
torch
。
utils
。
data
。
DataLoader
(
dataset
=
train_dataset
,
batch_size
=
batch_size
,
shuffle
=
True
)
test_loader
=
torch
。
utils
。
data
。
DataLoader
(
dataset
=
test_dataset
,
batch_size
=
batch_size
,
shuffle
=
False
)
model
=
SimpleModel
()
optimizer
=
optim
。
SGD
(
model
。
parameters
(),
lr
=
lr
,
momentum
=
0。5
)
if
use_cuda
:
model
=
nn
。
DataParallel
(
model
)
。
cuda
()
iter_losses
=
[]
def
train
(
epoch
):
model
。
train
()
total_loss
=
0
compution_time
=
0
e_sp
=
time
。
time
()
for
batch_idx
,
(
data
,
target
)
in
enumerate
(
train_loader
):
if
use_cuda
:
data
=
data
。
cuda
()
target
=
target
。
cuda
()
b_sp
=
time
。
time
()
output
=
model
(
data
)
loss
=
F
。
nll_loss
(
output
,
target
)
optimizer
。
zero_grad
()
loss
。
backward
()
optimizer
。
step
()
compution_time
+=
time
。
time
()
-
b_sp
# optimizer。step()
epoch_time
=
time
。
time
()
-
e_sp
(
‘Train Epoch:
{}
\t
Loss:
{:。6f}
\t
epoch time:
{:。6f}
s
\t
epoch compution time:
{:。6f}
s’
。
format
(
epoch
,
total_loss
/
len
(
train_loader
),
epoch_time
,
compution_time
))
return
total_loss
/
len
(
train_loader
)
def
test
():
model
。
eval
()
with
torch
。
no_grad
():
test_loss
=
0
correct
=
0
for
data
,
target
in
test_loader
:
if
use_cuda
:
data
=
data
。
cuda
()
target
=
target
。
cuda
()
output
=
model
(
data
)
# sum up batch loss
test_loss
+=
F
。
nll_loss
(
output
,
target
)
。
item
()
# get the index of the max log-probability
pred
=
output
。
data
。
max
(
1
,
keepdim
=
True
)[
1
]
correct
+=
pred
。
eq
(
target
。
data
。
view_as
(
pred
))
。
cpu
()
。
sum
()
test_loss
/=
len
(
test_loader
)
(
‘
\n
Test set: Average loss:
{:。4f}
, Accuracy:
{}
/
{}
(
{:。0f}
%)
\n
’
。
format
(
test_loss
,
correct
,
len
(
test_loader
。
dataset
),
100。
*
correct
/
len
(
test_loader
。
dataset
)))
return
test_loss
,
100。
*
correct
。
item
()
/
len
(
test_loader
。
dataset
)
if
__name__
==
“__main__”
:
for
epoch
in
range
(
1
,
10000
):
train_l
=
train
(
epoch
)
val_l
,
val_a
=
test
()
我們從以下指標來看一下不同batch size之間的區別:
迭代速度
感覺之前做的實驗有點不太科學,重新捋了一下思路,把時間計算的程式碼也放到了前面的程式碼之中,有興趣的同學也可以自己做一下看看。
(表中 Epoch Time是在此batch size下完成一個epoch所需的所有時間,包括載入資料和計算的時間,Epoch Computation Time拋去了載入資料所需的時間。)
(時間確實是
有偏量
,上面的資料可以大體做個參考,要做科學考究的話,還是要多做幾次實驗求均值減少偏差。)
其實純粹cuda計算的角度來看,完成每個iter的時間大batch和小batch區別並不大,這可能是因為
本次實驗中,反向傳播的時間消耗要比正向傳播大得多
,
所以batch size的大小對每個iter所需的時間影響不明顯,未來將在大一點的資料庫和更復雜的模型上做一下實驗
。(因為反向的過程取決於模型的複雜度,與batchsize的大小關係不大,而正向則同時取決於模型的複雜度和batch size的大小。而本次實驗中反向的過程要比正向的過程時間消耗大得多,所以batch size的大小對完成每個iter所需的耗時影響不大。)
完成每個epoch運算的所需的全部時間主要卡在:1。 load資料的時間,2。 每個epoch的iter數量。 因此對於每個epoch,不管是純計算時間還是全部時間,大體上還是大batch能夠更節約時間一點,但隨著batch增大,iter次數減小,完成每個epoch的時間更取決於載入資料所需的時間,此時也不見得大batch能帶來多少的速度增益了。
梯度平滑程度
我們再來看一下不同batch size下的梯度的平滑程度,我們選取了每個batch size下前1000個iter的loss,來看一下loss的震盪情況,結果如下圖所示:
如果感覺這張圖不太好看,可以看一下這張圖:
由於現在絕大多數的框架在進行mini-batch的反向傳播的時候,預設都是將batch中每個instance的loss平均化之後在進行反向傳播,所以相對大一點的batch size能夠防止loss震盪的情況發生。從這兩張圖中可以看出batch size越小,相鄰iter之間的loss震盪就越厲害,相應的,反傳回去的梯度的變化也就越大,也就越不利於收斂。同時很有意思的一個現象,batch size為1的時候,loss到後期會發生爆炸,這主要是lr=0。02設定太大,所以某個異常值的出現會嚴重擾動到訓練過程。
這也是為什麼對於較小的batchsize,要設定小lr的原因之一,避免異常值對結果造成的擾巨大擾動。而對於較大的batchsize,要設定大一點的lr的原因則是大batch每次迭代的梯度方向相對固定,大lr可以加速其收斂過程。
收斂速度
在衡量不同batch size的優劣這一點上,我選用衡量不同batch size在同樣引數下的收斂速度快慢的方法。
下表中可以看出,在minst資料集上,從整體時間消耗上來看(考慮了載入資料所需的時間),同樣的引數策略下 (lr = 0。02, momentum=0。5 ),要模型收斂到accuracy在98左右,batchsize在 6 - 60 這個量級能夠花費最少的時間,而batchsize為1的時候,收斂不到98;batchsize過大的時候,因為模型收斂快慢取決於
梯度方向和更新次數,
所以大batch儘管梯度方向更為穩定,但要達到98的accuracy所需的更新次數並沒有量級上的減少,所以也就需要花費更多的時間,當然這種情況下可以配合一些調參策略比如warmup LR,衰減LR等等之類的在一定程度上進行解決(這個先暫且按下不表),但也不會有本質上的改善。
不過單純從計算時間上來看,大batch還是可以很明顯地節約所需的計算時間的,原因前面講過了,主要因為本次實驗中純計算時間中,反向佔的時間比重遠大於正向。
(我一直覺得直接比較不同batch size下的絕對收斂精度來衡量batch size的好壞是沒有太大意義的,因為不同的batch size要配合不同的調參策略用才能達到其最佳效果,而要想在每個batch size下都找到合適的調參策略那可太難了,所以用這種方法來決定batch size未免有點武斷。)
-------------------------------一次正向,多次反向-------------------------------
這部分在pytorch上進行了實驗,但發現pytorch在backward中加入retain_graph進行多次反向傳播的時候,時間消耗特別大,所以儘管採用一次正向,多次反向的方法,減少了超大batch size收斂到98的accuracy所需的iteration,但由於每個iteration時間消耗增加,所以並沒有帶來時間節省,我後續還要探究一下原因,再重新做一下實驗,然後再貼結果,給結論。
做了幾次實驗,基本失敗,一個大batch分成10份反向傳播,基本等同於lr放大10倍。大batch還是要配合著更復雜的lr策略來做,比如warmup,迴圈lr,lr衰減等等。
-------------------------------------實驗的漏洞---------------------------------------
為了保證獨立變數,我在實驗中不同batch設定了同樣的lr,然後比較收斂速度,這樣是不公平的,畢竟大batch還是要配合更大的初始lr,所以後續還要做一下實驗,固定每個batch size, 看lr的變化對不同batch size收斂素的的影響。