DL基礎補全計劃(一)—線性回歸及示例(Pytorch,平方損失)

PS:要轉載請註明出處,本人版權所有。

PS: 這個只是基於《我自己》的理解,

如果和你的原則及想法相衝突,請諒解,勿噴。

前置說明

  本文作為本人csdn blog的主站的備份。(BlogID=105)

環境說明
  • Windows 10
  • VSCode
  • Python 3.8.10
  • Pytorch 1.8.1
  • Cuda 10.2

前言


  從我2017畢業到現在為止,我的工作一直都是AI在邊緣端部署落地等相關內容。所以我的工作基本都集中在嵌入式+Linux+DL三者之內的範圍,由於個人興趣和一些工作安排,就會去做一些模型移植的工作,所以我會經常接觸模型基本結構,前處理、後處理等等基本的知識,但是其實我很少去接觸模型怎麼來的這個問題。雖然以前也硬啃過Lenet5和BP算法,也按照別人弄好的腳本訓練過一些簡單的模型,但是從來沒有認真仔細的看過這些腳本,這些腳本為什麼這樣寫。

  在2019年下半年,隨着我移植模型的工作深入,接觸的各種硬件平台越來越多,經常遇見一些層在此平台無法移植,需要拆出來特殊處理。這讓我產生了為啥這些層在這些特定平台不能夠移植的疑問?為啥替換為別的層就能正常工作?為啥此平台不提供這個層?於是我去請教我們的算法小夥伴們,他們建議我如果要解決這個問題,建議我學習一下DL的基本知識,至少要簡單了解從數據採集及處理、模型搭建及訓練、模型部署等知識,其中模型部署可能是我最了解的內容了。利用一些閑暇時間和工作中的一些機會,我對以上需要了解的知識有了一個大概的認知。隨着了解的深入,可能我也大概知道我比較缺一些基礎知識。經過小夥伴的推薦和自己的搜索,我選擇了《動⼿學深度學習.pdf》作為我的基礎補全資料。

  本文是以『補全資料』的Chapter3中線性規劃為基礎來編寫的,主要是對『補全資料』之前的基礎知識的一個簡單匯總,包含了深度學習中一些基本的知識點,同時會對這些基本知識點進行一些解釋。

  由於我也是一個初學者,文中的解釋是基於我的知識水平結構做的『特色適配』,如果解釋有誤,請及時提出,我這裡及時更正。寫本文的原因也是記錄我的學習歷程,算是我的學習筆記。

回歸概念


  回歸是一種建模方法,得到的模型表示了自變量和因變量的關係。因此回歸還可以解釋為一種事與事之間的聯繫。對我們來說最常見的例子就是我們學習過的函數。例如函數Y=aX+b,這裡的Y=aX+b就是我們模型, X代表自變量,Y代表因變量。

  這裡順便多說一句,深度學習是機器學習的子集。建模方法很多,回歸只是其中的一種。

線性回歸

  線性回歸就是自變量和因變量是線性關係,感覺跟廢話一樣,換個方式表達就是自變量是一次。例如:Y=aX+b, Y=aX1+bX2+c, 這裡的X、X1、X2都是一次的,不是二次或者更高的。

非線性回歸

  非線性回歸就是自變量和因變量不是是線性關係,同樣感覺跟廢話一樣,換個方式表達就是自變量是二次及以上的。這裡和線性回歸對比一下就行。

基於y=aX1^2 + bX1^2 + cX2+dX2+e的回歸Pytorch實例


  此實例是《動⼿學深度學習.pdf》中線性回歸的從零實現的變種。從零實現,可以了解到許多的基本知識。
  此小節基本按照數據採集及處理、模型搭建及訓練和模型部署來描述。

帶噪聲數據採集及處理

  我們知道,在我們準備數據時,肯定由於各種各樣的原因,會有各種干擾,導致我們根據實際場景採集到的數據,其實不是精準的,但是這並不影響我們建模,因為我們的模型是盡量去擬合真實場景的情況。

  生成我們的實際模型的噪聲數據,也就是我們常見的數據集的說法。如下是代碼:

import numpy as np
def synthetic_data(w1, w2, b, num_examples): #@save
    """⽣成y = X1^2w1 + X2w2 + b + 噪聲。"""
    # 根據正太分佈,隨機生成我們的X1和X2
    # X1和X2都是(1000, 2)的矩陣
    X1 = np.random.normal(0, 1, (num_examples, len(w1)))
    X2 = np.random.normal(0, 1, (num_examples, len(w2)))

    # 基於X1,X2,true_w1, true_w2, true_b, 通過向量內積、廣播等方法計算模型的真實結果
    y = np.dot(X1**2, w1) + np.dot(X2, w2) + b

    # 通過隨機噪聲加上真實結果,生成我們的數據集。
    # y是(1000, 1)的矩陣
    y += np.random.normal(0, 0.1, y.shape)
    
    return X1, X2, y.reshape((-1, 1))
    
true_w1 = np.array([5.7, -3.4])
true_w2 = np.array([4.8, -3.4])
true_b = 4.2

# 這裡我們得到了我們的數據集,包含了特徵和標籤
features1, features2, labels = synthetic_data(true_w1, true_w2, true_b, 1000)

# 因為我們知道我們的模型是y=aX1^2+bX1^2+cX2+dX2+e,於是我們知道a的數據分佈是類似y=ax^2+b的這種形狀。於是我們知道c的數據分佈是類似y=ax+b的這種形狀。
# 我們可以通過如下代碼驗證一下
plt.scatter(features1[:, 0], labels[:], 1, c='r')
plt.scatter(features2[:, 0], labels[:], 1, c='b')
plt.show()
rep_img

其實從圖中可以看到,紅色的是類似y=ax^2+b的這種形狀,藍色是類似y=ax+b這種形狀,但是他們都不是在一條線,說明我們的設置的噪聲項是有效的。

  同時這裡我們還要準備一個函數用來隨機抽取特徵和標籤,一批一批的進行訓練。

def data_iter(batch_size, features1, features2, labels):
    num_examples = len(features1)
    indices = list(range(num_examples))
    np.random.shuffle(indices) # 樣本的讀取順序是隨機的
    
    for i in range(0, num_examples, batch_size):
        j = np.array(indices[i: min(i + batch_size, num_examples)])
        # print(features1.take(j, axis=0).shape)
        yield torch.tensor(features1.take(j, 0)), torch.tensor(features2.take(j, 0)), torch.tensor(labels.take(j)) # take函數根據索引返回對應元素
模型搭建及訓練

  對於我們這個實例來說,模型就是一個二元二次函數,此外這裡的模型也叫作目標函數。所以,下面我們用torch來實現它就行。

def our_func(X1, X2, W1, W2, B):
    # print(X1.shape) (100, 2)
    # print(W1.shape) (2, 1)
    net_out = torch.matmul(X1**2, W1) + torch.matmul(X2, W2) + B
    # print(net_out.shape) (100, 1)
    return net_out

注意喲,這裡的X1,X2,W1,W2,B都是torch的tensor格式。

  由數據採集及處理可知,在我們設定的真實true_w1,true_w2和true_b的情況下,我們得到了許許多多的X1,X2和y。我們要做的事情是求出W1、W2和b,這裡看起是不是很矛盾?我們已知了true_w1,true_w2,true_b然後去求w1,w2,b。這裡其實是一個錯覺,由於在實際情況中,我們可能會得到許許多多的X1,X2和y,這些數據不是我們模擬生成的,而是某種關係實際產生的數據,只是這些數據被我們收集到了。在實際情況下,而且我們僅僅只能夠得到X1,X2和y,我們通過觀察X1,X2和y的關係,發現他們有一元二次和一元一次關係,所以我們建立的模型為y=aX12+bX12+cX2+dX2+e,這個過程稱為建模。

  通過上面的說明,我們知道這個模型我們要求a,b,c,d,e這些參數的值,我們求這些參數的過程叫做訓練。對於這個實例來說,這個過程也叫作求解方程,這裡其實我們可以通過解方程的方法把這5個參數解出來,但是在實際情況中,我們建立的模型可能參數較多,可能手動解不出來,於是我們要通過訓練的方式,去擬合這些參數。所以這裡一個重要的問題就是怎麼擬合這些參數?

  如果學習過《數值分析》這門課程的話,其實就比較好理解了,如果要擬合這些參數,我們有許多的方法可以使用,但是基本分為兩類,一類是針對誤差進行分析,一類是針對模型進行分析。那麼在深度學習中,我們一般是對誤差進行分析,也就是我們常說的梯度下降法,我們需要隨機生成這些參數初始值(w1,w2,b),然後根據我們得到的X1,X2和y,通過梯度下降方法可以得到新的w1′,w2′,b’,且w1′,w2′,b’更加接近true_w1,true_w2和true_b。這就是一種優化過程,經過多次優化,我們就有可能求出跟接近與true_w1,true_w2和true_b的值。

  上面啰嗦了一大堆之後,我這裡正式引入損失函數這個概念,然後我們根據損失函數去優化我們的w1,w2,b。我們定義這個實例的損失函數如下:

def loss_func(y_train, y_label):
    # print(y_train.shape)
    # print(y_label.shape)
    return ((y_train - y_label)**2)/2

  這裡我們y_train就是我們得到的訓練值,y_label就是我們採集到數值,這裡的損失函數就是描述訓練值和標籤的差多少,那麼我們只需要讓這個損失函數的值越來越小就行,那麼可能問題就變成了求損失函數的極小值問題,我們在這裡還是用梯度下降的方法來求損失函數的極小值。

  這裡要回憶起來一個概念,梯度是一個函數在這個方向增長最快的方向。我們求損失函數的極小值的話,就減去梯度就行了。

  這裡還要說一句,損失函數有很多(L1,MSE,交叉熵等等),大家以後可以自己選一個合適的即可,這裡的合適需要大家去學習每種損失函數的適用場景。這裡的平方損失的合理性我個人認為有兩種:

  • 1 直觀法,平方損失函數描述的是在數據集上,訓練值和真實值的誤差趨勢,我要想得到的參數最準確,就要求誤差最小,誤差最小就要求我去求解損失函數的極小值,這是我所了解的數學知識的直觀反映。 (直覺大法)
  • 2 數學證明如下:我們生成數據或者採樣數據的時候,他們的誤差服從正態分佈( $P(x) = 1/\sqrt{2\pi\sigma2}*exp((-1/2\sigma2)(x-\mu)^2)$ ),於是我們根據條件概率得到特定feature得到特定y的概率為: $P(y|X) = 1/\sqrt{2\pi\sigma2}*exp((-1/2\sigma2)(y-w1X12-w2X2-b)2)$,根據最大似然估計$P(y|X) = \prod\limits_{i=0}{n-1}P(y{i}|X^{i})$,此時最大似然估計最大,w1,w2,b就是最佳的參數,但是乘積函數的最大值不好求,我們用對數轉換一下,變為各項求和,只需要保證含義一致就行。由於一般的優化一般是說最小化,所以我們要取負的對數和$-\log(P(y|X))$,此時我們把最大似然估計函數轉換為對數形式:$-\log(P(y|X)) = \sum\limits_{i=0}{n-1}1/2\log(2\pi\sigma2)+1/2\sigma2(y-w1X12-w2X2-b)^2)$,我們可以看到要求此函數的最小值,其實就是後半部分的平方是最小值就行,因為其餘的都是常數項,而後面部分恰好就是我們的平方損失函數。(Copy書上大法)

  這裡的梯度下降法就是(w1,w2,b)-lr*(w1,w2,b).grad/batch_size,代碼如下:

def sgd(params, lr, batch_size): #@save
    """⼩批量隨機梯度下降。"""
    for param in params:
        with torch.no_grad():
            param[:] = param - lr * param.grad / batch_size

  這裡我畫出我們的損失函數的三維圖像,我們把損失函數簡化為Z=aX^2+bY+c的形式。其圖像如下圖:

rep_img

我們可以看到這個曲面有很多極小值,當我們隨機初始化a,b,c的時候,我們會根據X和Y得到部分損失點,這時我們求出a,b,c的梯度,當我們對a,b,c減去偏導數時,就會更快的靠近損失函數的谷底,我們的當我們的損失函數到極小值時,我們就認為此時的a,b,c是我們要找的參數。這就是我們的梯度下降法的意義所在。

  下面就直接寫訓練代碼就行。

w1 = torch.tensor(np.random.normal(0, 0.5, (2, 1)), requires_grad=True)
w2 = torch.tensor(np.random.normal(0, 0.5, (2, 1)), requires_grad=True)
b = torch.tensor(np.ones(1), requires_grad=True)

# print(w1.grad)
# print(w1.grad_fn)
# 
lr = 0.001
num_epochs = 10000
net = our_func
loss = loss_func

batch_size = 200

for epoch in range(num_epochs):
    for X1, X2, y in data_iter(batch_size, features1, features2, labels):
        # print(X1.shape) (100, 2)
        # print(y.shape) (100, 1)
        l = loss(net(X1, X2, w1, w2, b), y.reshape(batch_size, 1)) # `X`和`y`的⼩批量損失
        # print(f'epoch {epoch + 1}, before sdg loss {float(l.mean()):f}')
        # 計算l關於[`w`, `b`]的梯度
        # print(l.shape)
        
        # l.backward() default call, loss.backward(torch.Tensor(1.0))
        w1.grad = None
        w2.grad = None
        b.grad = None

        l.backward(torch.ones_like(y.reshape(batch_size, 1)))

        sgd([w1, w2, b], lr, batch_size) # 使⽤參數的梯度更新參數
        
        l1 = loss(net(X1, X2, w1, w2, b), y.reshape(batch_size, 1)) # `X`和`y`的⼩批量損失
        # print(f'epoch {epoch + 1}, after sdg loss {float(l1.mean()):f}')
    
    train_l = loss(net( torch.from_numpy(features1), torch.from_numpy(features2), w1, w2, b), torch.from_numpy(labels))
    # print(train_l.sum())
    if (epoch % 1000 == 0):
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

print('train_w1 ',w1)
print('train_w2 ',w2)
print('train_b ',b)

  這裡做的事情就是首先設定訓了次數,學習率,批次數量參數,然後隨機生成了w1,w2,b,然後通過data_iter取一批數據,算出loss,清空w1,w2,b的梯度,對loss求w1,w2,b的偏導數,調用sdg求新的w1,w2,b。最後計算新參數在整個數據集上的損失。

  這裡尤其需要注意的是在多次迭代過程中,一定要清空w1,w2,b的梯度,因為它的梯度是累加的,不會覆蓋。這裡用的Pytorch的自動求導模塊,其實底層就是調用的bp算法,利用了鏈式法則,反向從loss開始,能夠算出w1,w2,b的偏導數。

rep_img

這裡我們看到最終的平均誤差到了一定值後就下降不了,這和我們的數據有關係,我們只需要關心這個誤差我們能夠接受嗎?能接受,我們就訓練得到了成功的模型,如果不能接受,我們就改參數重新來,這是一個多次試驗嘗試的過程。

  我們從上文可知,true_w1 = np.array([5.7, -3.4]),true_w2 = np.array([4.8, -3.4]),true_b = 4.2。我們訓練出來的值是w1=(5.7012, -3.3955),w2=(4.8042, -3.4071),b=4.2000。可以看到其實訓練得到的值還是非常接近的,但是這個任務其實太簡單了。

後記


  這裡主要用到了許多基本的概念,一個是數據集的收集和處理,模型搭建,模型訓練,優化方法、損失函數等等。至少通過本文,我們知道了得到一個模型的基本過程,其實普遍性的深度學習就是使用新的損失函數、搭建新的模型、使用新的優化方法等等。

  從文中特例我們可以發現,我們只利用了Pytorch自帶的自動求導模塊,其他的一些常見的深度學習概念都是我們手動實現了,其實這些好多內容都是Pytorch這些框架封裝好的,我們沒有必要自己手寫,但是如果是初次學習的話,建議手動來寫,這樣認知更加深刻。

參考文獻


打賞、訂閱、收藏、丟香蕉、硬幣,請關注公眾號(攻城獅的搬磚之路)
qrc_img

PS: 請尊重原創,不喜勿噴。

PS: 要轉載請註明出處,本人版權所有。

PS: 有問題請留言,看到後我會第一時間回復。

Tags: