從線性分類到深度學(xué)習(xí)
機(jī)器學(xué)習(xí)、人工智能火了之后坎缭,大批新人涌入這個(gè)行業(yè)坎吻,尤其以深度學(xué)習(xí)為主的方向更為火爆拍棕。深度學(xué)習(xí)應(yīng)用在人臉識(shí)別、姿態(tài)估計(jì)等領(lǐng)域取得了比傳統(tǒng)視覺(jué)方法優(yōu)秀得多的性能表現(xiàn)≈埃現(xiàn)如今也涌現(xiàn)出許多優(yōu)秀的深度學(xué)習(xí)和機(jī)器學(xué)習(xí)框架姐仅,例如 TensorFlow、PyTorch刻盐、PaddlePaddle 等掏膏。
但是這些框架都是直接將算法封裝成 API 提供給人使用,雖然這樣做使開(kāi)發(fā)非常便利敦锌,但是大部分人其實(shí)對(duì)底層算法的實(shí)現(xiàn)并不了解馒疹、基礎(chǔ)薄弱,也就是被很多人詬病的 “調(diào)包俠”乙墙。
本課程將致力于了解深度學(xué)習(xí)的基本算法介紹颖变,并使用 NumPy 實(shí)現(xiàn)所介紹的算法生均,現(xiàn)學(xué)現(xiàn)用,真正了解框架底層的算法是如何運(yùn)作的腥刹。
線性擬合
機(jī)器學(xué)習(xí)基本概念有很多種定義马胧,最著名也是使用最廣泛的是 Mitchell 于 1997 年提出的: 對(duì)于某類任務(wù) T 和性能度量 P,如果計(jì)算機(jī)程序在 T 上以 P 衡量的性能隨著經(jīng)驗(yàn)E而自我完善衔峰,那么就稱這個(gè)計(jì)算機(jī)程序從經(jīng)驗(yàn) E 學(xué)習(xí)佩脊。
接下來(lái)舉一個(gè)簡(jiǎn)單的例子幫助理解這個(gè)概念。
線性擬合是初中的知識(shí)垫卤,原理非常威彰、非常簡(jiǎn)單,就是用一條直線擬合一些點(diǎn)穴肘,并使得所有點(diǎn)到直線的距離之和最短抱冷。以下是一個(gè)示意圖,使用藍(lán)線擬合紅點(diǎn):
假設(shè)有N個(gè)點(diǎn)梢褐,每個(gè)點(diǎn)的坐標(biāo)為(xi,yi)旺遮,直線的公式是y=ax+b,所以線性擬合的目的是找到a和b使得所有點(diǎn)到直線的距離之和最短盈咳。
復(fù)習(xí)一下點(diǎn)到直線的距離公式:
再對(duì)所有點(diǎn)到直線的距離求和可以表示為:
那么最終的目的耿眉,則是求得a和b使得L最小。
回顧機(jī)器學(xué)習(xí)的概念鱼响,可以將整個(gè)線性擬合的過(guò)程對(duì)應(yīng)到這些概念之中:
經(jīng)驗(yàn) E: 線性擬合中給出的這些點(diǎn)(紅點(diǎn))鸣剪。
某類任務(wù) T: 使用藍(lán)線擬合紅點(diǎn),求得最優(yōu)的a和b丈积。
衡量的性能 P: 所有點(diǎn)到直線的距離之和最短筐骇,即使得L最小。
再次更抽象地對(duì)應(yīng)到深度學(xué)習(xí)江滨,稱這些點(diǎn)為數(shù)據(jù)集铛纬,擬合過(guò)程為訓(xùn)練、構(gòu)建模型唬滑,L為損失函數(shù)(Loss Function)或者誤差函數(shù)(Error Function)告唆、目標(biāo)函數(shù)(Target Function)。
線性分類器
介紹完線性擬合之后晶密,大致對(duì)深度學(xué)習(xí)的一些概念有了初步的了解擒悬。接下來(lái)根據(jù)線性擬合,可以引申出一個(gè)非常簡(jiǎn)單的分類器稻艰,線性分類器懂牧。
線性分類器的公式和線性擬合類似,只是線性分類器是一個(gè)多元函數(shù)尊勿,而線性擬合只是一元函數(shù)僧凤,是線性分類中最簡(jiǎn)單的一種用狱。它的基本公式:
當(dāng)輸入和輸出有大于等于 2 個(gè)時(shí),W和b都為矩陣拼弃。在接下來(lái)的課程學(xué)習(xí)中夏伊,你會(huì)發(fā)現(xiàn)非常神奇的一件事,神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)之一就是這個(gè)最簡(jiǎn)單的公式吻氧。
為了內(nèi)容更加簡(jiǎn)潔和方便敘述溺忧,接下來(lái)的線性分類器將會(huì)舍去偏置項(xiàng)b,而使用更簡(jiǎn)單的形式:
既然W是矩陣盯孙,那么如何確定W的維度呢鲁森?
首先假設(shè)數(shù)據(jù)集中有N個(gè)數(shù)據(jù),每個(gè)數(shù)據(jù)有具體的 mm 項(xiàng)振惰,即X=x1 ,x2 ,…,xm 歌溉,那么訓(xùn)練數(shù)據(jù)X則為一個(gè)N行m列的矩陣。
假設(shè)輸出的類別有C個(gè)骑晶,期望輸出的y為每一個(gè)類別的概率痛垛,即Y=y1,y2,…,yC,那么輸出Y則為一個(gè)N行C列的矩陣桶蛔,每一行對(duì)應(yīng)一個(gè)樣本的C個(gè)類別的概率匙头。
接下來(lái)使用 MNIST 數(shù)據(jù)集數(shù)據(jù)集作為一個(gè)具體的實(shí)例進(jìn)行講解。
mnist 又叫手寫(xiě)體數(shù)據(jù)集仔雷,里面包含了從 0 到 9 的28?28 的灰度圖片蹂析,是深度學(xué)習(xí)中常用的一個(gè)數(shù)據(jù)集。其中訓(xùn)練集有 60000 個(gè)碟婆,測(cè)試集有 10000 個(gè)电抚,0 到 9 的數(shù)字均勻分布。
對(duì)應(yīng)到前面的敘述竖共,則訓(xùn)練集C=10, N=60000蝙叛,測(cè)試集C=10, N=10000。
# 下載手寫(xiě)體數(shù)據(jù)集
!wget -nc "http://labfile.oss.aliyuncs.com/courses/1213/mnist.zip"
!unzip -o mnist.zip
接下來(lái)肘迎,加載數(shù)據(jù)集:
import struct
import numpy as np
def read_mnist(filename):
with open(filename, 'rb') as f:
zero, data_type, dims = struct.unpack('>HBB', f.read(4))
shape = tuple(struct.unpack('>I', f.read(4))[0] for d in range(dims))
return np.frombuffer(f.read(), dtype=np.uint8).reshape(shape)
mnist_test_data = read_mnist('mnist/t10k-images.idx3-ubyte')
mnist_train_data = read_mnist('mnist/train-images.idx3-ubyte')
mnist_train_labels = read_mnist('mnist/train-labels.idx1-ubyte')
mnist_test_labels = read_mnist('mnist/t10k-labels.idx1-ubyte')
我們可以通過(guò)繪圖來(lái)看一下 MNIST 數(shù)據(jù)集數(shù)據(jù)集具體的樣子甥温。
import matplotlib.pyplot as plt
%matplotlib inline
plt.imshow(mnist_test_data[0, :, :])
讀取完成之后锻煌,我們需要對(duì)數(shù)據(jù)預(yù)處理妓布,主要完成 2 個(gè)操作:
對(duì)數(shù)據(jù)集的標(biāo)簽0,1,…,9 獨(dú)熱編碼。
將數(shù)據(jù)展平并歸一化宋梧。
one-hot(獨(dú)熱)編碼指的是使用一個(gè)只有一位為 1 的二進(jìn)制向量編碼標(biāo)簽匣沼,下面獨(dú)熱編碼一些數(shù)字,更為直觀:
from sklearn.preprocessing import OneHotEncoder
labels = [[1], [2], [3]] # 待編碼的標(biāo)簽
encoder = OneHotEncoder()
encoder.fit_transform(labels).toarray()
由上可見(jiàn)捂龄,1 被 [1, 0, 0] 替換释涛,2 被 [0, 1, 0] 替換加叁,3 被 [0, 0, 1] 替換。深度學(xué)習(xí)中使用獨(dú)熱編碼來(lái)表示真實(shí)概率唇撬,因?yàn)楠?dú)熱編碼的結(jié)果只有一位為 1它匕,這代表對(duì)應(yīng)的標(biāo)簽真實(shí)概率為 100%。
展平操作如下圖所示窖认,我們將 MNIST 數(shù)據(jù)集數(shù)據(jù)集每個(gè)28?28 的灰度圖展平成一個(gè)一維向量豫柬,以輸入線性分類器中。
encoder.fit(np.arange(10).reshape((-1, 1))) # 生成標(biāo)簽并獨(dú)熱編碼
mnist_test_labels = encoder.transform(np.reshape(
mnist_test_labels, (-1, 1))).toarray() # 生成標(biāo)簽并獨(dú)熱編碼
mnist_train_labels = encoder.transform(np.reshape(
mnist_train_labels, (-1, 1))).toarray() # 生成標(biāo)簽并獨(dú)熱編碼
# 將數(shù)據(jù)展平并歸一化扑浸,并歸一化到 -0.5~0.5 之間
mnist_train_data = (np.reshape(
mnist_train_data, (mnist_train_data.shape[0], -1))-127.0)/255.0
mnist_test_data = (np.reshape(
mnist_test_data, (mnist_test_data.shape[0], -1))-127.0)/255.0
mnist_train_data.shape, mnist_test_data.shape
至此烧给,數(shù)據(jù)預(yù)處理完畢。我們得到了訓(xùn)練數(shù)據(jù) mnist_train_data喝噪,測(cè)試數(shù)據(jù) mnist_test_data 和對(duì)應(yīng)的獨(dú)熱標(biāo)簽础嫡。
由上面的定義可知,完成手寫(xiě)體的分類是本次的任務(wù) T酝惧,所以接下來(lái)需要定義一個(gè)性能度量 P榴鼎,即損失函數(shù),和一種 算法 T 使得損失最小晚唇。
因此檬贰,接下來(lái)會(huì)分別講到這兩部分內(nèi)容:
損失函數(shù): Softmax 和 Cross Entropy Loss。
算法 T: 如何求解最優(yōu)的W缺亮。
Softmax 和交叉熵?fù)p失函數(shù)
Softmax 函數(shù)
Softmax 函數(shù)函數(shù)是機(jī)器學(xué)習(xí)和深度學(xué)習(xí)中相當(dāng)常用到的函數(shù)翁涤,它的公式如下:
其中sk表示的是輸入到 Softmax 函數(shù)的數(shù)據(jù)。
Softmax 函數(shù)具體的作用是將輸入標(biāo)準(zhǔn)化到和為 1 的輸出萌踱,經(jīng)過(guò) Softmax 函數(shù)的的數(shù)據(jù)可以被認(rèn)為是概率葵礼。
為了更好理解,假設(shè)最后的輸出y為(3.2,5.1,?1.7)并鸵,對(duì)應(yīng)的是每一個(gè)類別的概率鸳粉,但是顯然這個(gè)數(shù)字不能直接使用,通常來(lái)說(shuō)概率都是和為 1 的园担,所以才需要經(jīng)過(guò) Softmax 進(jìn)行標(biāo)準(zhǔn)化届谈。
流程如圖:
y=xW 最后輸出的是一個(gè)N×C 的矩陣,每行對(duì)應(yīng)一個(gè)樣本的結(jié)果弯汰,所以需要按行進(jìn)行標(biāo)準(zhǔn)化艰山,使用 numpy 實(shí)現(xiàn) Softmax 函數(shù)如下:
# 根據(jù)公式 5 實(shí)現(xiàn)
def softmax(input):
assert len(input.shape) == 2 # 輸入 softmax 函數(shù)的數(shù)據(jù)必須是一個(gè)二維矩陣
exp_value = np.exp(input) # 首先計(jì)算指數(shù)
output = exp_value/np.sum(exp_value, axis=1)[:, np.newaxis] # 然后按行標(biāo)準(zhǔn)化
return output
test_data = np.array([[3.2, 5.1, -1.7]])
softmax(test_data)
交叉熵?fù)p失函數(shù)(Cross Entropy Loss)
既然經(jīng)過(guò) Softmax 函數(shù)之后的是概率,那么最根本的想法當(dāng)然是使正確類別的概率最大咏闪。交叉熵?fù)p失函數(shù)出現(xiàn)的目的就是使正確類別概率最大曙搬。
如上圖所示,交叉熵是用來(lái)衡量?jī)蓚€(gè)概率之間的差別。深度學(xué)習(xí)中使用 softmax 輸出的概率擬合真實(shí)概率纵装,并使得這兩個(gè)兩個(gè)概率之間的交叉熵最小征讲。
假設(shè)交叉熵?fù)p失函數(shù)為L(zhǎng),那么單個(gè)樣本的損失定義為:
指的是在樣本是xi的情況下橡娄,使用概率分布Pi擬合真實(shí)概率分布Yi的誤差诗箍。
通常真實(shí)概率是由人類在數(shù)據(jù)集上打的標(biāo)簽,例如上圖的[1.00,0.00,0.00] 是人為打的標(biāo)簽挽唉。通常將標(biāo)簽 one-hot(獨(dú)熱)編碼為真實(shí)概率扳还。
為了更容易理解,代入 Softmax 函數(shù)的公式作為概率橱夭,交叉熵?fù)p失函數(shù)可以寫(xiě)成:
多個(gè)樣本時(shí)氨距,只需要將各個(gè)樣本的損失相加即可:
其中Y=(y1,…,yi,…) 是真實(shí)概率,因?yàn)槭仟?dú)熱編碼棘劣,所以yi要么為 0要么為 1俏让;X=(x1,…,xi,…) 是神經(jīng)網(wǎng)絡(luò)的輸出。
接下來(lái)用代碼來(lái)實(shí)現(xiàn)交叉熵?fù)p失函數(shù)茬暇,生成隨機(jī)數(shù)據(jù)數(shù)據(jù)計(jì)算 Loss:
# 最后的輸出是 N*C
N, C = 100, 3
test_data = np.random.randn(N, C) # 生成隨機(jī)數(shù)據(jù)
# 生成隨機(jī)標(biāo)簽首昔,不包括 C,并獨(dú)熱編碼
test_labels = encoder.fit_transform(np.random.randint(0, C, (N, 1))).toarray()
prob = softmax(test_data) # 每一行對(duì)應(yīng)一個(gè)樣本糙俗,按行計(jì)算概率
# 根據(jù)公式 7勒奇,8 實(shí)現(xiàn)
# loss = np.sum(-np.log(prob)) # 根據(jù)公式計(jì)算 loss
loss = np.sum(-np.log(prob) * test_labels) / N
loss
交叉熵?fù)p失函數(shù)是分類任務(wù)中常用的損失函數(shù),而且取得的效果很好巧骚。
如何選取最優(yōu)權(quán)重
介紹完交叉熵?fù)p失函數(shù)之后赊颠,最終的目的是優(yōu)化W使得L最小(這里選取的L為交叉熵?fù)p失函數(shù))劈彪,那么采用什么方法才能找到最優(yōu)的W取得很好的性能呢竣蹦?
接下來(lái)介紹兩種方法,一種非常 Naive 的隨機(jī)查找法沧奴,另一種是深度學(xué)習(xí)的基礎(chǔ)算法之一:梯度下降法痘括。
隨機(jī)查找法(Random search)
為什么說(shuō)隨機(jī)查找法非常 Naive?因?yàn)殡S機(jī)查找是隨機(jī)生成W以找到最優(yōu)的W滔吠。接下來(lái)用代碼講話纲菌,看看隨機(jī)查找法的效果怎么樣:
best_acc = -float('inf') # 最高的準(zhǔn)確度
# 迭代 100 次
for epoch in range(100):
W = np.random.randn(784, 10)*0.01 # 隨機(jī)生成 w,前面講過(guò) w 為 784*10 的矩陣
# 根據(jù)公式 4 實(shí)現(xiàn)
prob = softmax(np.dot(mnist_test_data, W)) # 只計(jì)算測(cè)試集 xW 和概率
y_predict = np.argmax(prob, axis=1) # 計(jì)算輸出的標(biāo)簽疮绷,概率最大的
y_real = np.argmax(mnist_test_labels, axis=1)
acc = np.mean(y_predict == y_real) # 計(jì)算測(cè)試集準(zhǔn)確度
if acc > best_acc:
best_acc = acc # 保存最好的準(zhǔn)確度
best_acc
迭代 100 次之后翰舌,隨機(jī)猜能達(dá)到 ~20% 左右的準(zhǔn)確度,相對(duì)于 10% 的隨機(jī)概率要提高了一倍矗愧。
但是不是隨機(jī)查找真的能取得非常好的效果灶芝,答案是不可能的郑原。實(shí)際的深度學(xué)習(xí)任務(wù)的網(wǎng)絡(luò)結(jié)構(gòu)唉韭、參數(shù)比這里多得多夜涕,隨機(jī)查找?guī)缀醪豢赡苋〉煤玫男Ч怯腥讼嘈盘焐夏艿麴W餅属愤。
梯度下降法(Gradient Descent)
梯度下降法是深度學(xué)習(xí)的基礎(chǔ)算法之一女器,它的基本思想是參數(shù)沿著梯度的相反方向更新。如果你未曾聽(tīng)過(guò)的話住诸,首先需要知道什么是梯度(Gradient)驾胆。
梯度的本意是一個(gè)向量(矢量),表示某一函數(shù)在該點(diǎn)處的方向?qū)?shù)沿著該方向取得最大值贱呐,即函數(shù)在該點(diǎn)處沿著該方向(此梯度的方向)變化最快丧诺,變化率最大。
以這張圖為例奄薇,現(xiàn)在你站在山頂驳阎,山的構(gòu)造其實(shí)就是對(duì)應(yīng)損失函數(shù),因此你下山其實(shí)是在尋找損失函數(shù)的最小值點(diǎn)∧俚伲現(xiàn)在你不知道自己在那里呵晚,也不知道東南西北。但是你只需要知道按照最陡的方向一直往下走就一定能到山腳了沫屡。
下圖可以更清晰表述梯度下降的過(guò)程:
因此梯度下降法有兩個(gè)重點(diǎn):計(jì)算梯度饵隙,以及沿著梯度的反方向更新參數(shù)。
其中沮脖,計(jì)算梯度只需要簡(jiǎn)單的高數(shù)知識(shí)金矛,在計(jì)算梯度時(shí)需要應(yīng)用求導(dǎo)的鏈?zhǔn)椒▌t,對(duì)于更深的網(wǎng)絡(luò)勺届,鏈?zhǔn)椒▌t和反向傳播算法非常重要绷柒,反向傳播算法會(huì)在接下來(lái)的課程中介紹。
多元函數(shù)計(jì)算梯度會(huì)涉及到偏導(dǎo)數(shù)的求解涮因,你可以參考 維基百科-梯度废睦。下面舉一個(gè)簡(jiǎn)單的例子:
由于最終損失函數(shù)可以換算成一個(gè)關(guān)于權(quán)重W的函數(shù),即L(x,W)养泡,梯度下降沿梯度的方向更新參數(shù)的基本公式為:
lr 為一個(gè)提前設(shè)定的值嗜湃,控制W更新的幅度,對(duì)應(yīng)到深度學(xué)習(xí)則稱之為 學(xué)習(xí)率澜掩。
在實(shí)際使用時(shí)當(dāng)然不會(huì)這么簡(jiǎn)單购披,不可能直接化簡(jiǎn)成關(guān)于W的函數(shù),這時(shí)候就需要鏈?zhǔn)椒▌t了肩榕。
為了更好的理解梯度下降刚陡,讓代碼來(lái)說(shuō)話惩妇。舉一個(gè)最簡(jiǎn)單的例子,一個(gè)簡(jiǎn)單的一元四次函數(shù)筐乳,使用梯度下降法求解其極小值點(diǎn)歌殃。首先,我們生成一個(gè)擁有兩個(gè)極小值點(diǎn)的四次函數(shù)蝙云,并將函數(shù)圖像繪制處理氓皱。
def f(x): return (x+2)*(x+1)*(x-2)*(x-1)
X = np.linspace(-3, 3, 10000)
Y = np.array([f(x) for x in X])
plt.plot(X, Y)
離散點(diǎn)的最小的 Y 和對(duì)應(yīng)的 X 為:
Y[np.argmin(Y)], X[np.argmin(Y)]
上圖同樣可以看出有兩個(gè)極小值點(diǎn),并且最小值是左邊的極小值點(diǎn)勃刨。這里為了求導(dǎo)方便這個(gè)四次函數(shù)關(guān)于 Y 軸對(duì)稱波材,兩個(gè)極值點(diǎn)都為最小值。
import math
def df(x): return 4*x**3-10*x # 導(dǎo)數(shù)
math.sqrt(5/2)
根據(jù)上文介紹的梯度下降的基本準(zhǔn)則身隐,沿著梯度的方向更新參數(shù)廷区,該一元函數(shù)更新x的公式為:
下面就是利用梯度下降法求函數(shù)的極值點(diǎn)。
lr = 0.001
x = -3
# 迭代 1000 次
for i in range(1000):
x -= lr*df(x) # 根據(jù)公式 11 實(shí)現(xiàn)
x
學(xué)習(xí)率衰減策略
上面講到了使用梯度下降法求解一個(gè)一元函數(shù)的極值贾铝,接下來(lái)為了更好的理解梯度下降法隙轻,繪制一個(gè)動(dòng)畫(huà)顯示整個(gè)梯度下降的過(guò)程。如何繪制動(dòng)畫(huà)并不要求掌握忌傻,但是關(guān)于如何使用 Matplotlib 繪圖大脉,可以參考藍(lán)橋云課課程 使用 Matplotlib 繪制 2D 和 3D 圖形。
from matplotlib import animation
from IPython.display import HTML
# 這是一個(gè)基本函數(shù)水孩,用來(lái)演示梯度下降過(guò)程镰矿,該函數(shù)不要求掌握
def show_anim(X, Y, f, df, max_iterations, lr, initial_x):
fig, ax = plt.subplots()
# 繪制曲線
ax.plot(X, Y)
line, = ax.plot(0, 0, 'ro')
# 繪制箭頭
annotation = ax.annotate("", xy=(0, 0), xytext=(
0, 0), arrowprops=dict(arrowstyle="->"))
# 返回梯度下降的點(diǎn),生成動(dòng)畫(huà)的箭頭
# 梯度下降法的函數(shù)俘种,傳入代求解函數(shù)秤标、導(dǎo)數(shù)、Y 值宙刘、最大迭代次數(shù)苍姜、學(xué)習(xí)率、初始起點(diǎn)
def gradient_descent(f, df, Y, max_iterations=30, lr=0.001, x=-3):
# 初始點(diǎn)
points = [(x, f(x))]
for _ in range(max_iterations):
# 梯度下降悬包,并保留中間的點(diǎn)
x -= lr*df(x)
points.append((x, f(x)))
arrows = []
for i in range(len(points)-1):
# 箭頭起始點(diǎn)和終點(diǎn)
arrows.append((points[i], points[i+1]))
return arrows
arrows = gradient_descent(f, df, Y, max_iterations, lr, initial_x)
def init():
return line,
# 更新畫(huà)布
def update(index):
start, end = arrows[index]
line.set_data(end[0], end[1])
annotation.set_position(start)
annotation.xy = end
return line, annotation
# 繪制動(dòng)畫(huà)衙猪,不需要掌握
anim = animation.FuncAnimation(
fig, update, interval=200, blit=False, frames=max_iterations, init_func=init)
return anim
接下來(lái),我們就可以通過(guò)動(dòng)畫(huà)演示上文的梯度下降法布近,這里設(shè)置學(xué)習(xí)率初始為 0.001垫释,起始點(diǎn)x=?3,最大迭代次數(shù) 100 次撑瞧。
lr = 0.001
initial_x = -3
max_iterations = 100
anim = show_anim(X, Y, f, df, max_iterations, lr, initial_x)
from IPython.display import HTML
HTML(anim.to_html5_video()) # 運(yùn)行查看動(dòng)畫(huà)
一般情況下棵譬,我們會(huì)先設(shè)置一個(gè)較大的學(xué)習(xí)率以盡快到達(dá)極小點(diǎn)。但固定學(xué)習(xí)率往往會(huì)帶來(lái)另一個(gè)問(wèn)題预伺,那就是無(wú)法準(zhǔn)確收斂订咸,并在極小值附近反復(fù)波動(dòng)曼尊。所以,為了兼顧梯度下降法后期的收斂速度和準(zhǔn)確度脏嚷,必須要應(yīng)用學(xué)習(xí)率衰減策略骆撇。
一般情況下,學(xué)習(xí)率衰減策略有三種然眼,分別是:
在實(shí)際應(yīng)用中艾船,按步長(zhǎng)衰減是最常用的葵腹,因此這里將只使用和介紹按步長(zhǎng)衰減高每。其基本公式如下:
其中,α為設(shè)定的衰減因子践宴,k為是迭代次數(shù)/提前設(shè)定的步長(zhǎng)鲸匿,通俗點(diǎn)講其實(shí)是每迭代一個(gè)設(shè)定的步長(zhǎng)數(shù)目,則學(xué)習(xí)率變?yōu)樯弦粋€(gè)步長(zhǎng)的α阻肩。
步長(zhǎng)需要根據(jù)實(shí)際應(yīng)用調(diào)整带欢,但是衰減因子常為 0.1,即學(xué)習(xí)率是上一個(gè)步長(zhǎng)的1/10烤惊。
接下來(lái)乔煞,我們實(shí)現(xiàn)按步長(zhǎng)衰減策略,這段代碼將在本次課程的后續(xù)內(nèi)容中多次使用柒室。
class lr_scheduler(object):
def __init__(self, base_lr, step_size, deacy_factor=0.1):
self.base_lr = base_lr # 最初的學(xué)習(xí)率
self.deacy_factor = deacy_factor # 學(xué)習(xí)率衰減因子
self.step_count = 0 # 當(dāng)前的迭代次數(shù)
self.lr = base_lr # 當(dāng)前學(xué)習(xí)率
self.step_size = step_size # 步長(zhǎng)
def step(self, step_count=1): # 默認(rèn) 1 次
self.step_count += step_count
def get_lr(self):
# 根據(jù)公式 12 實(shí)現(xiàn)
self.lr = self.base_lr * \
(self.deacy_factor**(self.step_count//self.step_size)) # 實(shí)現(xiàn)上面的公式
return self.lr
lr = 0.001
x = -3
max_iter = 10000
scheduler = lr_scheduler(lr, 1000)
for i in range(max_iter):
x -= scheduler.get_lr()*df(x) # 根據(jù)公式 11 實(shí)現(xiàn)
scheduler.step()
x
應(yīng)用學(xué)習(xí)率衰減策略時(shí)需要注意最大迭代次數(shù)足夠渡贾,否則可能收斂不完全。
通過(guò)學(xué)習(xí)率衰減最后迭代的結(jié)果是1.5811388301861444雄右,而如果不應(yīng)用結(jié)果是 ?1.581138831026283空骚,真實(shí)結(jié)果?1.5811388300841898,只看后面幾位擂仍,分別是 0186144401861444囤屹,10262831026283,0084189800841898逢渔。事實(shí)證明應(yīng)用學(xué)習(xí)率衰減獲得了更好的結(jié)果肋坚。
由上文知道了學(xué)習(xí)率非常小的時(shí)候,會(huì)導(dǎo)致收斂速度很慢肃廓,那么當(dāng)學(xué)習(xí)率很大時(shí)智厌,會(huì)有什么結(jié)果呢?
答案是:Loss 震蕩或超出數(shù)值范圍亿昏。
我們還是以上面的一元函數(shù)為例峦剔,當(dāng)設(shè)置學(xué)習(xí)率非常大時(shí),看看演示動(dòng)畫(huà)有什么效果角钩。
lr = 0.075
initial_x = -3
max_iterations = 100
anim = show_anim(X, Y, f, df, max_iterations, lr, initial_x)
HTML(anim.to_html5_video())
可以看出吝沫,當(dāng)學(xué)習(xí)率為 0.075 時(shí)呻澜,最開(kāi)始的階段點(diǎn)變化的幅度非常大。雖然這里能夠收斂惨险,是因?yàn)槿绻侔褜W(xué)習(xí)率調(diào)大羹幸,更新x時(shí),會(huì)直接超出計(jì)算機(jī)可表示的范圍辫愉。這就是深度學(xué)習(xí)中常討論的數(shù)值的上溢和下溢栅受。你可以嘗試自己把學(xué)習(xí)率調(diào)大之后,程序會(huì)報(bào) OverflowError恭朗。
所以初始學(xué)習(xí)率不應(yīng)過(guò)大也不應(yīng)過(guò)小屏镊,通常在 0.0001~0.001 之間。
權(quán)重初始化方法
對(duì)深度學(xué)習(xí)有過(guò)一些了解的肯定知道痰腮,梯度下降法最大的問(wèn)題是什么:迭代終止于局部最優(yōu)解而芥,很難找到全局最優(yōu)解。
在本例中膀值,當(dāng)x初始化為 3 呢棍丐?
lr = 0.001
x = 3
for i in range(1000):
x -= lr*df(x) # 根據(jù)公式 11 實(shí)現(xiàn)
x
除了初始值外,同樣的參數(shù)取得了不同的結(jié)果沧踏。這就是一個(gè)好的權(quán)重初始化的重要性的體現(xiàn)了歌逢,一個(gè)好的權(quán)重可以獲得可能更好的結(jié)果。這就是為什么遷移學(xué)習(xí)通常拿在 IMGAENET 訓(xùn)練好的模型翘狱,并可以獲得高準(zhǔn)確度的原因了秘案,因?yàn)橐呀?jīng)給定了一個(gè)非常好的初始化權(quán)重。
常用的初始化權(quán)重包括 Gaussian(高斯)初始化和 Xavier 初始化盒蟆。而偏置項(xiàng)一般是常數(shù)為 0 的初始化踏烙。以經(jīng)典的 Caffe 為例:
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
param {
lr_mult: 1
}
param {
lr_mult: 2
}
convolution_param {
num_output: 20
kernel_size: 5
stride: 1
weight_filler {
type: "xavier"
}
bias_filler {
type: "constant"
}
}
}
這是 Caffe 的配置文件設(shè)置一個(gè)卷積層的各項(xiàng)參數(shù),后續(xù)課程會(huì)介紹卷積神經(jīng)網(wǎng)絡(luò)历等。weight_filler
采用的是 Xavier 初始化讨惩,而 bias_filler
采用 constant 初始化,默認(rèn)為 0寒屯。
大部分深度學(xué)習(xí)框架會(huì)自動(dòng)調(diào)用某個(gè)初始化方法荐捻,如果需要按照自己的需求更改權(quán)重初始化方法,則需要手動(dòng)設(shè)置寡夹,例如 PyTorch 的初始化方法都在 torch.nn.init 中實(shí)現(xiàn)处面。
接下來(lái),我們嘗試實(shí)現(xiàn)各種初始化方法菩掏。
# 示例權(quán)重矩陣的形狀
D, H = 10, 10
下面魂角,使用高斯初始化生成一個(gè)零均值,標(biāo)準(zhǔn)差為 0.01 的權(quán)重:
W = 0.01*np.random.randn(D, H)
W
高斯初始化一般用于小網(wǎng)絡(luò)智绸,而且也能取得很好的效果野揪,但是對(duì)于更深的網(wǎng)絡(luò)访忿,例如殘差網(wǎng)絡(luò)等幾十層上百層的網(wǎng)絡(luò)一般使用 Xavier 初始化。因?yàn)樯窠?jīng)網(wǎng)絡(luò)的基本算法是線性的y=Wx+b斯稳,如果使用高斯初始化海铆,不停的做線性運(yùn)算,網(wǎng)絡(luò)越深挣惰,輸出的數(shù)據(jù)會(huì)呈現(xiàn)極端分布卧斟,要么是 1 要么是 -1。
下面憎茂,我們使用 Xavier 初始化:
W = np.random.randn(D, H)/np.sqrt(D)
W
本次課程前半部分不會(huì)使用很深的網(wǎng)絡(luò)珍语,所以只會(huì)用到高斯初始化,后續(xù)將會(huì)使用 PyTorch 的初始化方法唇辨。
線性分類器實(shí)現(xiàn)
本次試驗(yàn)選擇前面介紹的交叉熵?fù)p失函數(shù)廊酣,交叉熵?fù)p失函數(shù)如何計(jì)算 Loss 已經(jīng)介紹了能耻,接下來(lái)介紹如何對(duì)交叉熵?fù)p失函數(shù)計(jì)算梯度赏枚。首先對(duì)于單個(gè)樣本X=(x1,…,xi,…),真實(shí)概率Y=(y1,…,yi,…) 的交叉熵?fù)p失函數(shù)表達(dá)如下:
最初的公式是很復(fù)雜的晓猛,所以利用鏈?zhǔn)椒▌t先計(jì)算 Softmax 函數(shù)的梯度饿幅,再計(jì)算交叉熵?fù)p失函數(shù)的梯度,兩者相乘即為交叉熵?fù)p失函數(shù)關(guān)于輸入x的梯度戒职。
在此為了表述簡(jiǎn)潔栗恩,僅先討論單個(gè)樣本的情況。
Softmax 函數(shù)求導(dǎo)
假設(shè) Softmax 函數(shù)的輸出P=Softmax(x)為P=(p1,…,pi,…)洪燥,如下圖表述:
首先求 Softmax 函數(shù)關(guān)于x的梯度磕秤。
因此求導(dǎo)過(guò)程分這兩種情況進(jìn)行:
當(dāng)i=j 時(shí):
當(dāng)i!=j 時(shí):
因此對(duì) Softmax 求導(dǎo)完畢后發(fā)現(xiàn)導(dǎo)數(shù)值只與概率有關(guān)了,再對(duì)交叉熵?fù)p失函數(shù)關(guān)于輸入 xx 求導(dǎo)捧韵,會(huì)發(fā)現(xiàn)一件更神奇的事市咆。
交叉熵?fù)p失函數(shù)求導(dǎo)
代碼實(shí)現(xiàn)
為了代碼可復(fù)用,可以在后續(xù)實(shí)驗(yàn)中使用再来。在此實(shí)現(xiàn)一個(gè)類蒙兰,包含兩個(gè)函數(shù),forward 函數(shù)定義交叉熵?fù)p失函數(shù)的前向傳播過(guò)程芒篷,計(jì)算損失搜变;backward 函數(shù)定義交叉熵?fù)p失函數(shù)求解梯度的反向傳播過(guò)程。
在下一次實(shí)驗(yàn)中將會(huì)介紹對(duì)于多層網(wǎng)絡(luò)神經(jīng)反向傳播的概念针炉,在此只實(shí)現(xiàn)一層的線性分類器挠他。
class CrossEntropyLossLayer():
def __init__(self):
pass
def forward(self, input, labels):
# 做一些防止誤用的措施,輸入數(shù)據(jù)必須是二維的篡帕,且標(biāo)簽和數(shù)據(jù)必須維度一致
assert len(input.shape) == 2, '輸入的數(shù)據(jù)必須是一個(gè)二維矩陣'
assert len(labels.shape) == 2, '輸入的標(biāo)簽必須是獨(dú)熱編碼'
assert labels.shape == input.shape, '數(shù)據(jù)和標(biāo)簽數(shù)量必須一致'
self.data = input
self.labels = labels
self.prob = np.clip(softmax(input), 1e-9, 1.0) # 在取對(duì)數(shù)時(shí)不能為 0殖侵,所以用極小數(shù)代替 0
# 根據(jù)公式 13 實(shí)現(xiàn)
loss = -np.sum(np.multiply(self.labels, np.log(self.prob))
)/self.labels.shape[0]
return loss
def backward(self):
# 根據(jù)公式 22 實(shí)現(xiàn)
# 公式 22 只討論了單個(gè)樣本的情況摔蓝,所以這里需要除以樣本數(shù)
self.grad = (self.prob - self.labels)/self.labels.shape[0]
接下來(lái)生成隨機(jī)數(shù)據(jù)測(cè)試。
N, C = 10, 3
pred_prob = np.random.randn(N, C) # 隨機(jī)數(shù)據(jù)愉耙,代表線性分類器輸出
labels = encoder.fit_transform(
np.random.randint(0, C, (N, 1))).toarray() # 生成標(biāo)簽并獨(dú)熱編碼
loss_layer = CrossEntropyLossLayer()
# 前向傳播贮尉,傳入數(shù)據(jù)和獨(dú)熱編碼的標(biāo)簽
loss = loss_layer.forward(pred_prob, labels)
# 后向傳播計(jì)算梯度
loss_layer.backward()
loss, loss_layer.grad
上文已經(jīng)介紹了線性分類器W的形狀為784×10,在此不多做介紹朴沿。這里需要設(shè)置完各項(xiàng)參數(shù)猜谚,然后就開(kāi)始訓(xùn)練。
from tqdm.notebook import tqdm
D_in, D_out = 784, 10
# 設(shè)置基礎(chǔ)學(xué)習(xí)率
base_lr = 0.1
# 設(shè)置最大迭代次數(shù)
max_iter = 900
# 設(shè)置學(xué)習(xí)率衰減步長(zhǎng)
step_size = 400
# 初始化一個(gè)學(xué)習(xí)率調(diào)度器
scheduler = lr_scheduler(base_lr, step_size)
W = np.random.randn(D_in, D_out)*0.01 # 高斯初始化權(quán)重
best_acc = -float('inf')
best_weight = None # 保存最好的結(jié)果和準(zhǔn)確度
loss_list = []
for _ in tqdm(range(max_iter)):
# 測(cè)試階段赌渣,輸入測(cè)試集魏铅,然后計(jì)算準(zhǔn)確度
test_pred = np.dot(mnist_test_data, W)
# 預(yù)測(cè)和真實(shí)標(biāo)簽
pred_labels = np.argmax(test_pred, axis=1)
real_labels = np.argmax(mnist_test_labels, axis=1)
# 計(jì)算準(zhǔn)確度
acc = np.mean(pred_labels == real_labels)
if acc > best_acc:
best_acc = acc
best_weight = W # 保留結(jié)果最好的
# 訓(xùn)練并更新參數(shù)
train_pred = np.dot(mnist_train_data, W)
# 前向傳播輸出損失
loss = loss_layer.forward(train_pred, mnist_train_labels)
loss_list.append(loss/mnist_train_data.shape[0]) # 數(shù)據(jù)的平均 Loss
# 后向傳播計(jì)算梯度
loss_layer.backward()
# 損失關(guān)于權(quán)重的梯度,根據(jù)公式 25 實(shí)現(xiàn)
grad = np.dot(mnist_train_data.T, loss_layer.grad)
# 更新參數(shù)
W -= scheduler.get_lr()*grad
# 學(xué)習(xí)率衰減
scheduler.step()
best_acc
通過(guò)調(diào)節(jié)學(xué)習(xí)率和迭代次數(shù)最后獲得了 ~90% 的準(zhǔn)確度坚芜!而這只是一個(gè)簡(jiǎn)單的線性分類器览芳,甚至連偏置項(xiàng)都省去了,深度學(xué)習(xí)的性能表現(xiàn)可見(jiàn)一斑鸿竖。
我們可以繪制出 Loss 的變化曲線沧竟。
plt.plot(list(range(max_iter)), loss_list)
接下來(lái),使用訓(xùn)練好的權(quán)重測(cè)試一張圖片缚忧。
# 從測(cè)試機(jī)中隨機(jī)抽取一張圖片
import random
test_index = random.randint(0, mnist_test_data.shape[0]-1)
test_img = mnist_test_data[test_index, :]
# 展示圖片
plt.imshow(test_img.reshape((28, 28)))
預(yù)測(cè)得到結(jié)果:
prob = softmax(np.dot(test_img[np.newaxis, :], W)) # 歸一化為概率
np.argmax(prob, axis=1) # 取概率最大的坐標(biāo)
輸出應(yīng)當(dāng)很大可能是正確的悟泵。
隨機(jī)梯度下降法求解函數(shù)極值
使用梯度下降法求解多元函數(shù)極值
下面利用梯度下降法求該函數(shù)的極值點(diǎn)。
lr = 0.001
x = -3
def df(x): return 4*x**3-10*x # 導(dǎo)數(shù)
for i in range(1000):
# 根據(jù)公式 11 實(shí)現(xiàn)
x -= lr*df(x)
x