1. 程式人生 > >機器學習:如何找到最優學習率

機器學習:如何找到最優學習率

學習率的重要性

目前深度學習使用的都是非常簡單的一階收斂演算法,梯度下降法,不管有多少自適應的優化演算法,本質上都是對梯度下降法的各種變形,所以初始學習率對深層網路的收斂起著決定性的作用,下面就是梯度下降法的公式

[Math Processing Error]w:=w−α∂∂wloss(w)

這裡[Math Processing Error]α就是學習率,如果學習率太小,會導致網路loss下降非常慢,如果學習率太大,那麼引數更新的幅度就非常大,就會導致網路收斂到區域性最優點,或者loss直接開始增加,如下圖所示。


學習率的選擇策略在網路的訓練過程中是不斷在變化的,在剛開始的時候,引數比較隨機,所以我們應該選擇相對較大的學習率,這樣loss下降更快;當訓練一段時間之後,引數的更新就應該有更小的幅度,所以學習率一般會做衰減,衰減的方式也非常多,比如到一定的步數將學習率乘上0.1,也有指數衰減等。

這裡我們關心的一個問題是初始學習率如何確定,當然有很多辦法,一個比較笨的方法就是從0.0001開始嘗試,然後用0.001,每個量級的學習率都去跑一下網路,然後觀察一下loss的情況,選擇一個相對合理的學習率,但是這種方法太耗時間了,能不能有一個更簡單有效的辦法呢?

一個簡單的辦法

Leslie N. Smith 在2015年的一篇論文“Cyclical Learning Rates for Training Neural Networks”中的3.3節描述了一個非常棒的方法來找初始學習率,同時推薦大家去看看這篇論文,有一些非常啟發性的學習率設定想法。

這個方法在論文中是用來估計網路允許的最小學習率和最大學習率,我們也可以用來找我們的最優初始學習率,方法非常簡單。首先我們設定一個非常小的初始學習率,比如1e-5,然後在每個batch之後都更新網路,同時增加學習率,統計每個batch計算出的loss。最後我們可以描繪出學習的變化曲線和loss的變化曲線,從中就能夠發現最好的學習率。

下面就是隨著迭代次數的增加,學習率不斷增加的曲線,以及不同的學習率對應的loss的曲線。



從上面的圖片可以看到,隨著學習率由小不斷變大的過程,網路的loss也會從一個相對大的位置變到一個較小的位置,同時又會增大,這也就對應於我們說的學習率太小,loss下降太慢,學習率太大,loss有可能反而增大的情況。從上面的圖中我們就能夠找到一個相對合理的初始學習率,0.1。

之所以上面的方法可以work,因為小的學習率對引數更新的影響相對於大的學習率來講是非常小的,比如第一次迭代的時候學習率是1e-5,引數進行了更新,然後進入第二次迭代,學習率變成了5e-5,引數又進行了更新,那麼這一次引數的更新可以看作是在最原始的引數上進行的,而之後的學習率更大,引數的更新幅度相對於前面來講會更大,所以都可以看作是在原始的引數上進行更新的。正是因為這個原因,學習率設定要從小變到大,而如果學習率設定反過來,從大變到小,那麼loss曲線就完全沒有意義了。

實現

上面已經說明了演算法的思想,說白了其實是非常簡單的,就是不斷地迭代,每次迭代學習率都不同,同時記錄下來所有的loss,繪製成曲線就可以了。下面就是使用PyTorch實現的程式碼,因為在網路的迭代過程中學習率會不斷地變化,而PyTorch的optim裡面並沒有把learning rate的介面暴露出來,導致顯示修改學習率非常麻煩,所以我重新寫了一個更加高層的包mxtorch,借鑑了gluon的一些優點,在定義層的時候暴露初始化方法,支援tensorboard,同時增加了大量的model zoo,包括inceptionresnetv2,resnext等等,提供預訓練權重。

下面就是部分程式碼,這裡使用的資料集是kaggle上的dog breed,使用預訓練的resnet50,ScheduledOptim的原始碼如下:

class ScheduledOptim(object):
    '''A wrapper class for learning rate scheduling'''

    def __init__(self, optimizer):
        self.optimizer = optimizer
        self.lr = self.optimizer.param_groups[0]['lr']
        self.current_steps = 0

    def step(self):
        "Step by the inner optimizer"
        self.current_steps += 1
        self.optimizer.step()

    def zero_grad(self):
        "Zero out the gradients by the inner optimizer"
        self.optimizer.zero_grad()

    def set_learning_rate(self, lr):
        self.lr = lr
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr

    @property
    def learning_rate(self):
        return self.lr


def find_lr():
    pass


def train_model():
    pass

整體程式碼如下
criterion = torch.nn.CrossEntropyLoss()
net = model_zoo.resnet50(pretrained=True)
net.fc = nn.Linear(2048, 120)
with torch.cuda.device(0):
    net = net.cuda()
basic_optim = torch.optim.SGD(net.parameters(), lr=1e-5)
optimizer = ScheduledOptim(basic_optim)
lr_mult = (1 / 1e-5) ** (1 / 100)
lr = []
losses = []
best_loss = 1e9
for data, label in train_data:
    with torch.cuda.device(0):
        data = Variable(data.cuda())
        label = Variable(label.cuda())
    # forward
    out = net(data)
    loss = criterion(out, label)
    # backward
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    lr.append(optimizer.learning_rate)
    losses.append(loss.data[0])
    optimizer.set_learning_rate(optimizer.learning_rate * lr_mult)
    if loss.data[0] < best_loss:
        best_loss = loss.data[0]
    if loss.data[0] > 4 * best_loss or optimizer.learning_rate > 1.:
        break
plt.figure()
plt.xticks(np.log([1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1]), (1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1))
plt.xlabel('learning rate')
plt.ylabel('loss')
plt.plot(np.log(lr), losses)
plt.show()
plt.figure()
plt.xlabel('num iterations')
plt.ylabel('learning rate')
plt.plot(lr)