[PyTorch 學習筆記] 4.3 優化器
本章程式碼:
這篇文章主要介紹了 PyTorch 中的優化器,包括 3 個部分:優化器的概念、optimizer 的屬性、optimizer 的方法。
優化器的概念
PyTorch 中的優化器是用於管理並更新模型中可學習參數的值,使得模型輸出更加接近真實標籤。
optimizer 的屬性
PyTorch 中提供了 Optimizer 類,定義如下:
class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []
主要有 3 個屬性
- defaults:優化器的超參數,如 weight_decay,momentum
- state:參數的快取,如 momentum 中需要用到前幾次的梯度,就快取在這個變數中
- param_groups:管理的參數組,是一個 list,其中每個元素是字典,包括 momentum、lr、weight_decay、params 等。
- _step_count:記錄更新 次數,在學習率調整中使用
optimizer 的方法
-
zero_grad():清空所管理參數的梯度。由於 PyTorch 的特性是張量的梯度不自動清零,因此每次反向傳播之後都需要清空梯度。程式碼如下:
def zero_grad(self): r"""Clears the gradients of all optimized :class:`torch.Tensor` s.""" for group in self.param_groups: for p in group['params']: if p.grad is not None: p.grad.detach_() p.grad.zero_()
-
step():執行一步梯度更新
-
add_param_group():添加參數組,主要程式碼如下:
def add_param_group(self, param_group): params = param_group['params'] if isinstance(params, torch.Tensor): param_group['params'] = [params] ... self.param_groups.append(param_group)
-
state_dict():獲取優化器當前狀態資訊字典
-
load_state_dict():載入狀態資訊字典,包括 state 、momentum_buffer 和 param_groups。主要用於模型的斷點續訓練。我們可以在每隔 50 個 epoch 就保存模型的 state_dict 到硬碟,在意外終止訓練時,可以繼續載入上次保存的狀態,繼續訓練。程式碼如下:
def state_dict(self): r"""Returns the state of the optimizer as a :class:`dict`. ... return { 'state': packed_state, 'param_groups': param_groups, }
下面是程式碼示例:
step()
張量 weight 的形狀為 $2 \times 2$,並設置梯度為 1,把 weight 傳進優化器,學習率設置為 1,執行optimizer.step()
更新梯度,也就是所有的張量都減去 1。
weight = torch.randn((2, 2), requires_grad=True)
weight.grad = torch.ones((2, 2))
optimizer = optim.SGD([weight], lr=1)
print("weight before step:{}".format(weight.data))
optimizer.step() # 修改lr=1, 0.1觀察結果
print("weight after step:{}".format(weight.data))
輸出為:
weight before step:tensor([[0.6614, 0.2669],
[0.0617, 0.6213]])
weight after step:tensor([[-0.3386, -0.7331],
[-0.9383, -0.3787]])
zero_grad()
程式碼如下:
print("weight before step:{}".format(weight.data))
optimizer.step() # 修改lr=1 0.1觀察結果
print("weight after step:{}".format(weight.data))
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
print("weight.grad is {}\n".format(weight.grad))
optimizer.zero_grad()
print("after optimizer.zero_grad(), weight.grad is\n{}".format(weight.grad))
輸出為:
weight before step:tensor([[0.6614, 0.2669],
[0.0617, 0.6213]])
weight after step:tensor([[-0.3386, -0.7331],
[-0.9383, -0.3787]])
weight in optimizer:1932450477472
weight in weight:1932450477472
weight.grad is tensor([[1., 1.],
[1., 1.]])
after optimizer.zero_grad(), weight.grad is
tensor([[0., 0.],
[0., 0.]])
可以看到優化器的 param_groups 中存儲的參數和 weight 的記憶體地址是一樣的,所以優化器中保存的是參數的地址,而不是把參數複製到優化器中。
add_param_group()
向優化器中添加一組參數,程式碼如下:
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
w2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": w2, 'lr': 0.0001})
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
輸出如下:
optimizer.param_groups is
[{'params': [tensor([[0.6614, 0.2669],
[0.0617, 0.6213]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
optimizer.param_groups is
[{'params': [tensor([[0.6614, 0.2669],
[0.0617, 0.6213]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[-0.4519, -0.1661, -1.5228],
[ 0.3817, -1.0276, -0.5631],
[-0.8923, -0.0583, -0.1955]], requires_grad=True)], 'lr': 0.0001, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
state_dict()
首先進行 10 次反向傳播更新,然後對比 state_dict 的變化。可以使用torch.save()
把 state_dict 保存到 pkl 文件中。
optimizer = optim.SGD([weight], lr=0.1, momentum=0.9)
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
for i in range(10):
optimizer.step()
print("state_dict after step:\n", optimizer.state_dict())
torch.save(optimizer.state_dict(), os.path.join(BASE_DIR, "optimizer_state_dict.pkl"))
輸出為:
state_dict before step:
{'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [1976501036448]}]}
state_dict after step:
{'state': {1976501036448: {'momentum_buffer': tensor([[6.5132, 6.5132],
[6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [1976501036448]}]}
經過反向傳播後,state_dict 中的字典保存了1976501036448
作為 key,這個 key 就是參數的記憶體地址。
load_state_dict()
上面保存了 state_dict 之後,可以先使用torch.load()
把載入到記憶體中,然後再使用load_state_dict()
載入到模型中,繼續訓練。程式碼如下:
optimizer = optim.SGD([weight], lr=0.1, momentum=0.9)
state_dict = torch.load(os.path.join(BASE_DIR, "optimizer_state_dict.pkl"))
print("state_dict before load state:\n", optimizer.state_dict())
optimizer.load_state_dict(state_dict)
print("state_dict after load state:\n", optimizer.state_dict())
輸出如下:
state_dict before load state:
{'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [2075286132128]}]}
state_dict after load state:
{'state': {2075286132128: {'momentum_buffer': tensor([[6.5132, 6.5132],
[6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [2075286132128]}]}
學習率
學習率是影響損失函數收斂的重要因素,控制了梯度下降更新的步伐。下面構造一個損失函數 $y=(2x)^{2}$,$x$ 的初始值為 2,學習率設置為 1。
iter_rec, loss_rec, x_rec = list(), list(), list()
lr = 0.01 # /1. /.5 /.2 /.1 /.125
max_iteration = 20 # /1. 4 /.5 4 /.2 20 200
for i in range(max_iteration):
y = func(x)
y.backward()
print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))
x_rec.append(x.item())
x.data.sub_(lr * x.grad) # x -= x.grad 數學表達式意義: x = x - x.grad # 0.5 0.2 0.1 0.125
x.grad.zero_()
iter_rec.append(i)
loss_rec.append(y)
plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.xlabel("Iteration")
plt.ylabel("Loss value")
x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
plt.grid()
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.legend()
plt.show()
結果如下:

損失函數沒有減少,而是增大,這時因為學習率太大,無法收斂,把學習率設置為 0.01 後,結果如下;

從上面可以看出,適當的學習率可以加快模型的收斂。
下面的程式碼是試驗 10 個不同的學習率 ,[0.01, 0.5] 之間線性選擇 10 個學習率,並比較損失函數的收斂情況
iteration = 100
num_lr = 10
lr_min, lr_max = 0.01, 0.2 # .5 .3 .2
lr_list = np.linspace(lr_min, lr_max, num=num_lr).tolist()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()
for i, lr in enumerate(lr_list):
x = torch.tensor([2.], requires_grad=True)
for iter in range(iteration):
y = func(x)
y.backward()
x.data.sub_(lr * x.grad) # x.data -= x.grad
x.grad.zero_()
loss_rec[i].append(y.item())
for i, loss_r in enumerate(loss_rec):
plt.plot(range(len(loss_r)), loss_r, label="LR: {}".format(lr_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()
結果如下:

上面的結果表示在學習率較大時,損失函數越來越大,模型不能收斂。把學習率區間改為 [0.01, 0.2] 之後,結果如下:

這個損失函數在學習率為 0.125 時最快收斂,學習率為 0.01 收斂最慢。但是不同模型的最佳學習率不一樣,無法事先知道,一般把學習率設置為比較小的數就可以了。
momentum 動量
momentum 動量的更新方法,不僅考慮當前的梯度,還會結合前面的梯度。
momentum 來源於指數加權平均:$\mathrm{v}{t}=\boldsymbol{\beta} * \boldsymbol{v}{t-1}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}{t}$,其中 $v{t-1}$ 是上一個時刻的指數加權平均,$\theta_{t}$ 表示當前時刻的值,$\beta$ 是係數,一般小於 1。指數加權平均常用於時間序列求平均值。假設現在求得是 100 個時刻的指數加權平均,那麼
$\mathrm{v}{100}=\boldsymbol{\beta} * \boldsymbol{v}{99}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}{100}$
$=(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}{100}+\boldsymbol{\beta} *\left(\boldsymbol{\beta} * \boldsymbol{v}{98}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}{99}\right)$
$=(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}{100}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\beta} * \boldsymbol{\theta}{99}+\left(\boldsymbol{\beta}^{2} * \boldsymbol{v}_{98} \right)$
$=\sum_{i}^{N}(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\beta}^{i} * \boldsymbol{\theta}_{N-i}$
從上式可以看到,由於 $\beta$ 小於 1,越前面時刻的 $\theta$,$\beta$ 的次方就越大,係數就越小。
$\beta$ 可以理解為記憶周期,$\beta$ 越小,記憶周期越短,$\beta$ 越大,記憶周期越長。通常 $\beta$ 設置為 0.9,那麼 $\frac{1}{1-\beta}=\frac{1}{1-0.9}=10$,表示更關注最近 10 天的數據。
下面程式碼展示了 $\beta=0.9$ 的情況
weights = exp_w_func(beta, time_list)
plt.plot(time_list, weights, '-ro', label="Beta: {}\ny = B^t * (1-B)".format(beta))
plt.xlabel("time")
plt.ylabel("weight")
plt.legend()
plt.title("exponentially weighted average")
plt.show()
print(np.sum(weights))
結果為:

下面程式碼展示了不同的 $\beta$ 取值情況
beta_list = [0.98, 0.95, 0.9, 0.8]
w_list = [exp_w_func(beta, time_list) for beta in beta_list]
for i, w in enumerate(w_list):
plt.plot(time_list, w, label="Beta: {}".format(beta_list[i]))
plt.xlabel("time")
plt.ylabel("weight")
plt.legend()
plt.show()
結果為:

$\beta$ 的值越大,記憶周期越長,就會更多考慮前面時刻的數值,因此越平緩。
在 PyTroch 中,momentum 的更新公式是:
$v_{i}=m * v_{i-1}+g\left(w_{i}\right)$
$w_{i+1}=w_{i}-l r * v_{i}$
其中 $w_{i+1}$ 表示第 $i+1$ 次更新的參數,lr 表示學習率,$v_{i}$ 表示更新量,$m$ 表示 momentum 係數,$g(w_{i})$ 表示 $w_{i}$ 的梯度。展開表示如下:
$\begin{aligned} \boldsymbol{v}{100} &=\boldsymbol{m} * \boldsymbol{v}{99}+\boldsymbol{g}\left(\boldsymbol{w}{100}\right) \ &=\boldsymbol{g}\left(\boldsymbol{w}{100}\right)+\boldsymbol{m} *\left(\boldsymbol{m} * \boldsymbol{v}{98}+\boldsymbol{g}\left(\boldsymbol{w}{99}\right)\right) \ &=\boldsymbol{g}\left(\boldsymbol{w}{100}\right)+\boldsymbol{m} * \boldsymbol{g}\left(\boldsymbol{w}{99}\right)+\boldsymbol{m}^{2} * \boldsymbol{v}{98} \ &=\boldsymbol{g}\left(\boldsymbol{w}{100}\right)+\boldsymbol{m} * \boldsymbol{g}\left(\boldsymbol{w}{99}\right)+\boldsymbol{m}^{2} * \boldsymbol{g}\left(\boldsymbol{w}{98}\right)+\boldsymbol{m}^{3} * \boldsymbol{v}_{97} \end{aligned}$
下面的程式碼是構造一個損失函數 $y=(2x)^{2}$,$x$ 的初始值為 2,記錄每一次梯度下降並畫圖,學習率使用 0.01 和 0.03,不適用 momentum。
def func(x):
return torch.pow(2*x, 2) # y = (2x)^2 = 4*x^2 dy/dx = 8x
iteration = 100
m = 0 # .9 .63
lr_list = [0.01, 0.03]
momentum_list = list()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()
for i, lr in enumerate(lr_list):
x = torch.tensor([2.], requires_grad=True)
momentum = 0. if lr == 0.03 else m
momentum_list.append(momentum)
optimizer = optim.SGD([x], lr=lr, momentum=momentum)
for iter in range(iteration):
y = func(x)
y.backward()
optimizer.step()
optimizer.zero_grad()
loss_rec[i].append(y.item())
for i, loss_r in enumerate(loss_rec):
plt.plot(range(len(loss_r)), loss_r, label="LR: {} M:{}".format(lr_list[i], momentum_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()
結果為:

可以看到學習率為 0.3 時收斂更快。然後我們把學習率為 0.1 時,設置 momentum 為 0.9,結果如下:
雖然設置了 momentum,但是震蕩收斂,這是由於 momentum 的值太大,每一次都考慮上一次的比例太多,可以把 momentum 設置為 0.63 後,結果如下:

可以看到設置適當的 momentum 後,學習率 0.1 的情況下收斂更快了。
下面介紹 PyTroch 所提供的 10 種優化器。
PyTroch 提供的 10 種優化器
optim.SGD
optim.SGD(params, lr=<required parameter>, momentum=0, dampening=0, weight_decay=0, nesterov=False
隨機梯度下降法
主要參數:
- params:管理的參數組
- lr:初始學習率
- momentum:動量係數 $\beta$
- weight_decay:L2 正則化係數
- nesterov:是否採用 NAG
optim.Adagrad
自適應學習率梯度下降法
optim.RMSprop
Adagrad 的改進
optim.Adadelta
optim.Adam
RMSProp 集合 Momentum,這個是目前最常用的優化器,因為它可以使用較大的初始學習率。
optim.Adamax
Adam 增加學習率上限
optim.SparseAdam
稀疏版的 Adam
optim.ASGD
隨機平均梯度下降
optim.Rprop
彈性反向傳播,這種優化器通常是在所有樣本都一起訓練,也就是 batchsize 為全部樣本時使用。
optim.LBFGS
BFGS 在記憶體上的改進
參考資料
如果你覺得這篇文章對你有幫助,不妨點個贊,讓我有更多動力寫出好文章。