寫給程序員的機器學習入門 (三) - 線性模型,激活函數與多層線性模型

生物神經元與人工神經元

在瞭解神經元網絡以前,咱們先簡單的看看生物學上的神經元是什麼樣子的,下圖摘自維基百科:html

(由於我不是專家,這裏的解釋只用於理解人工神經元模擬了生物神經元的什麼地方,不必定徹底準確)python

神經元主要由細胞體和細胞突組成,而細胞突分爲樹突 (Dendrites) 和軸突 (Axon),樹突負責接收其餘神經元輸入的電流,而軸突負責把電流輸出給其餘神經元。一個神經元能夠經過樹突從多個神經元接收電流,若是電流沒有達到某個閾值則神經元不會把電流輸出,若是電流達到了某個閾值則神經元會經過軸突的突觸把電流輸出給其餘神經元,這樣的規則被稱爲全有全無律。輸入電流達到閾值之後輸出電流的狀態又稱爲到達動做電位,動做電位會持續 1 ~ 2 毫秒,以後會進入約 0.5 毫秒的絕對不該期,不管輸入多大的電流都不會輸出,而後再進入約 3.5 毫秒的相對不該期,須要電流達到更大的閾值纔會輸出,最後返回靜息電位。神經元之間鏈接起來的網絡稱爲神經元網絡,人的大腦中大約有 860 億個神經元,由於 860 億個神經元能夠同時工做,因此目前的計算機沒法模擬這種工做方式 (除非開發專用的芯片),只能模擬一部分的工做方式和使用更小規模的網絡。git

計算機模擬神經元網絡使用的是人工神經元,單我的工神經元能夠用如下公式表達:github

其中 n 表明輸入的個數,你能夠把 n 看做這個神經元擁有的樹突個數,x 看做每一個樹突輸入電流的值;而 w (weight) 表明各個輸入的權重,也就是各個樹突對電流大小的調整;而 b (bias) 用於調整各個輸入乘權重相加後的值,使得這個值能夠配合某個閾值工做;而 g 則是激活函數,用於判斷值是否達到閾值並輸出和輸出多少,一般會使用非線性函數;而 y 則是輸出的值,能夠把它看做軸突輸出的電流,鏈接這個 y 到其餘神經元就能夠組建神經元網絡。網絡

咱們在前兩篇看到的其實就是隻有一個輸入而且沒有激活函數的單我的工神經元,把一樣的輸入傳給多個神經元 (第一層),而後再傳給其餘神經元 (第二層),而後再傳給其餘神經元 (第三層) 就能夠組建人工神經元網絡了,同一層的神經元個數越多,神經元的層數越多,網絡就越強大,但須要更多的運算時間而且更有可能發生第一篇文章講過的過擬合 (Overfitting) 現象。框架

下圖是人工神經元網絡的例子,有 3 輸入 1 個輸出,通過 3 層處理,第 1 層和第 2 層各有兩個神經元對應隱藏值 (中間值),第 3 層有一個神經元對應輸出值:dom

神經元中包含的 w 和 b 就是咱們須要經過機器學習調整的參數值。機器學習

若是你以爲圖片有點難以理解,能夠看轉換後的代碼:函數

h11 = g(x1 * w111 + x2 * w112 + x3 * w113 + b11)
h12 = g(x1 * w121 + x2 * w122 + x3 * w123 + b12)
h21 = g(h11 * w211 + h12 * w212 + b21)
h22 = g(h11 * w221 + h12 * w222 + b22)
y = g(h21 * w311 + h22 * w312 + b31)

不少癡迷人工神經元網絡的學者聲稱人工神經元網絡能夠模擬人腦的工做方式,作到某些領域上超過人腦的判斷,但實際上這還有很大的爭議,咱們能夠看到人工神經元的鏈接方式只會按固定的模式,判斷是否達到閾值並輸出的邏輯也沒法作到和生物神經元同樣(目前尚未解明),而且也沒有生物神經元的不該期,因此也有學者聲稱人工神經元不過只是作了複雜的數學運算來模擬邏輯判斷,須要根據不一樣的場景切換不一樣的計算方法,使用這種方式並不能達到人腦的水平。學習

單層線性模型

在前一篇文章咱們已經稍微瞭解過機器學習框架 pytorch,如今咱們來看看怎麼使用 pytorch 封裝的線性模型,如下代碼運行在 python 的 REPL 中:

# 導入 pytorch 類庫
>>> import torch

# 建立 pytorch 封裝的線性模型,設置輸入有 3 個輸出有 1 個
>>> model = torch.nn.Linear(in_features=3, out_features=1)

# 查看線性模型內部包含的參數列表
# 這裏一共包含兩個參數,第一個參數是 1 行 3 列的矩陣分別表示 3 個輸入對應的 w 值 (權重),第二個參數表示 b 值 (偏移)
# 初始值會隨機生成 (使用 kaiming_uniform 生成正態分佈)
>>> list(model.parameters())
[Parameter containing:
tensor([[0.0599, 0.1324, 0.0099]], requires_grad=True), Parameter containing:
tensor([-0.2772], requires_grad=True)]

# 定義輸入和輸出
>>> x = torch.tensor([1, 2, 3], dtype=torch.float)
>>> y = torch.tensor([6], dtype=torch.float)

# 把輸入傳給模型
>>> p = model(x)

# 查看預測輸出值
# 1 * 0.0599 + 2 * 0.1324 + 3 * 0.0099 - 0.2772 = 0.0772
>>> p
tensor([0.0772], grad_fn=<AddBackward0>)

# 計算偏差並自動微分
>>> l = (p - y).abs()
>>> l
tensor([5.9228], grad_fn=<AbsBackward>)
>>> l.backward()

# 查看各個參數對應的導函數值
>>> list(model.parameters())[0].grad
tensor([[-1., -2., -3.]])
>>> list(model.parameters())[1].grad
tensor([-1.])

以上能夠看做 1 層 1 個神經元,很好理解吧?咱們來看看 1 層 2 個神經元:

# 導入 pytorch 類庫
>>> import torch

# 建立 pytorch 封裝的線性模型,設置輸入有 3 個輸出有 2 個
>>> model = torch.nn.Linear(in_features=3, out_features=2)

# 查看線性模型內部包含的參數列表
# 這裏一共包含兩個參數
# 第一個參數是 2 行 3 列的矩陣分別表示 2 個輸出和 3 個輸入對應的 w 值 (權重)
# 第二個參數表示 2 個輸出對應的 b 值 (偏移)
>>> list(model.parameters())
[Parameter containing:
tensor([[0.1393, 0.5165, 0.2910],
        [0.2276, 0.1579, 0.1958]], requires_grad=True), Parameter containing:
tensor([0.2566, 0.1701], requires_grad=True)]

# 定義輸入和輸出
>>> x = torch.tensor([1, 2, 3], dtype=torch.float)
>>> y = torch.tensor([6, -6], dtype=torch.float)

# 把輸入傳給模型
>>> p = model(x)

# 查看預測輸出值
# 1 * 0.1393 + 2 * 0.5165 + 3 * 0.2910 + 0.2566 = 2.3019
# 1 * 0.2276 + 2 * 0.1579 + 3 * 0.1958 + 0.1701 = 1.3009
>>> p
tensor([2.3019, 1.3009], grad_fn=<AddBackward0>)

# 計算偏差並自動微分
# (abs(2.3019 - 6) + abs(1.3009 - -6)) / 2 = 5.4995
>>> l = (p - y).abs().mean()
>>> l
tensor(5.4995, grad_fn=<MeanBackward0>)
>>> l.backward()

# 查看各個參數對應的導函數值
# 由於偏差取了 2 個值的平均,因此求導函數值的時候會除以 2
>>> list(model.parameters())[0].grad
tensor([[-0.5000, -1.0000, -1.5000],
        [ 0.5000,  1.0000,  1.5000]])
>>> list(model.parameters())[1].grad
tensor([-0.5000,  0.5000])

如今咱們來試試用線性模型來學習符合 x_1 * 1 + x_2 * 2 + x_3 * 3 + 8 = y 的數據,輸入和輸出會使用矩陣定義:

# 引用 pytorch
import torch

# 給隨機數生成器分配一個初始值,使得每次運行均可以生成相同的隨機數
# 這是爲了讓訓練過程可重現,你也能夠選擇不這樣作
torch.random.manual_seed(0)

# 建立線性模型,設置有 3 個輸入 1 個輸出
model = torch.nn.Linear(in_features=3, out_features=1)

# 建立損失計算器
loss_function = torch.nn.MSELoss()

# 建立參數調整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 隨機生成原始數據集,一共 20 組數據,每條數據有 3 個輸入
dataset_x = torch.randn((20, 3))
dataset_y = dataset_x.mm(torch.tensor([[1], [2], [3]], dtype=torch.float)) + 8
print(f"dataset_x: {dataset_x}")
print(f"dataset_y: {dataset_y}")

# 切分訓練集 (12 組),驗證集 (4 組) 和測試集 (4 組)
random_indices = torch.randperm(dataset_x.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_x[traning_indices]
traning_set_y = dataset_y[traning_indices]
validating_set_x = dataset_x[validating_indices]
validating_set_y = dataset_y[validating_indices]
testing_set_x = dataset_x[testing_indices]
testing_set_y = dataset_y[testing_indices]

# 開始訓練過程
for epoch in range(1, 10000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練並修改參數
    # 切換模型到訓練模式,將會啓用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.train()

    # 計算預測值
    # 20 行 3 列的矩陣乘以 3 行 1 列的矩陣 (由 weight 轉置獲得) 等於 20 行 1 列的矩陣
    predicted = model(traning_set_x)
    # 計算損失
    loss = loss_function(predicted, traning_set_y)
    # 打印除錯信息
    print(f"loss: {loss}, weight: {model.weight}, bias: {model.bias}")
    # 從損失自動微分求導函數值
    loss.backward()
    # 使用參數調整器調整參數
    optimizer.step()
    # 清空導函數值
    optimizer.zero_grad()

    # 檢查驗證集
    # 切換模型到驗證模式,將會禁用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.eval()
    predicted = model(validating_set_x)
    validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).abs().mean()
    print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")

    # 若是驗證集正確率大於 99 %,則中止訓練
    print(f"validating accuracy: {validating_accuracy}")
    if validating_accuracy > 0.99:
        break

# 檢查測試集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).abs().mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")

輸出結果以下:

dataset_x: tensor([[ 0.8487,  0.6920, -0.3160],
        [-2.1152, -0.3561,  0.4372],
        [ 0.4913, -0.2041,  0.1198],
        [ 1.2377,  1.1168, -0.2473],
        [-1.0438, -1.3453,  0.7854],
        [ 0.9928,  0.5988, -1.5551],
        [-0.3414,  1.8530,  0.4681],
        [-0.1577,  1.4437,  0.2660],
        [ 1.3894,  1.5863,  0.9463],
        [-0.8437,  0.9318,  1.2590],
        [ 2.0050,  0.0537,  0.4397],
        [ 0.1124,  0.6408,  0.4412],
        [-0.2159, -0.7425,  0.5627],
        [ 0.2596,  0.5229,  2.3022],
        [-1.4689, -1.5867, -0.5692],
        [ 0.9200,  1.1108,  1.2899],
        [-1.4782,  2.5672, -0.4731],
        [ 0.3356, -1.6293, -0.5497],
        [-0.4798, -0.4997, -1.0670],
        [ 1.1149, -0.1407,  0.8058]])
dataset_y: tensor([[ 9.2847],
        [ 6.4842],
        [ 8.4426],
        [10.7294],
        [ 6.6217],
        [ 5.5252],
        [12.7689],
        [11.5278],
        [15.4009],
        [12.7970],
        [11.4315],
        [10.7175],
        [ 7.9872],
        [16.2120],
        [ 1.6500],
        [15.0112],
        [10.2369],
        [ 3.4277],
        [ 3.3199],
        [11.2509]])
epoch: 1
loss: 142.77590942382812, weight: Parameter containing:
tensor([[-0.0043,  0.3097, -0.4752]], requires_grad=True), bias: Parameter containing:
tensor([-0.4249], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
        [ 0.8487,  0.6920, -0.3160],
        [ 0.1124,  0.6408,  0.4412],
        [-1.0438, -1.3453,  0.7854]]), y: tensor([[ 3.3199],
        [ 9.2847],
        [10.7175],
        [ 6.6217]]), predicted: tensor([[-0.1385],
        [ 0.3020],
        [-0.0126],
        [-1.1801]], grad_fn=<AddmmBackward>)
validating accuracy: -0.04714548587799072
epoch: 2
loss: 131.40403747558594, weight: Parameter containing:
tensor([[ 0.0675,  0.4937, -0.3163]], requires_grad=True), bias: Parameter containing:
tensor([-0.1970], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
        [ 0.8487,  0.6920, -0.3160],
        [ 0.1124,  0.6408,  0.4412],
        [-1.0438, -1.3453,  0.7854]]), y: tensor([[ 3.3199],
        [ 9.2847],
        [10.7175],
        [ 6.6217]]), predicted: tensor([[-0.2023],
        [ 0.6518],
        [ 0.3935],
        [-1.1479]], grad_fn=<AddmmBackward>)
validating accuracy: -0.03184401988983154
epoch: 3
loss: 120.98343658447266, weight: Parameter containing:
tensor([[ 0.1357,  0.6687, -0.1639]], requires_grad=True), bias: Parameter containing:
tensor([0.0221], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
        [ 0.8487,  0.6920, -0.3160],
        [ 0.1124,  0.6408,  0.4412],
        [-1.0438, -1.3453,  0.7854]]), y: tensor([[ 3.3199],
        [ 9.2847],
        [10.7175],
        [ 6.6217]]), predicted: tensor([[-0.2622],
        [ 0.9860],
        [ 0.7824],
        [-1.1138]], grad_fn=<AddmmBackward>)
validating accuracy: -0.016991496086120605

省略途中輸出

epoch: 637
loss: 0.001102567883208394, weight: Parameter containing:
tensor([[1.0044, 2.0283, 3.0183]], requires_grad=True), bias: Parameter containing:
tensor([7.9550], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
        [ 0.8487,  0.6920, -0.3160],
        [ 0.1124,  0.6408,  0.4412],
        [-1.0438, -1.3453,  0.7854]]), y: tensor([[ 3.3199],
        [ 9.2847],
        [10.7175],
        [ 6.6217]]), predicted: tensor([[ 3.2395],
        [ 9.2574],
        [10.6993],
        [ 6.5488]], grad_fn=<AddmmBackward>)
validating accuracy: 0.9900396466255188
testing x: tensor([[-0.3414,  1.8530,  0.4681],
        [-1.4689, -1.5867, -0.5692],
        [ 1.1149, -0.1407,  0.8058],
        [ 0.3356, -1.6293, -0.5497]]), y: tensor([[12.7689],
        [ 1.6500],
        [11.2509],
        [ 3.4277]]), predicted: tensor([[12.7834],
        [ 1.5438],
        [11.2217],
        [ 3.3285]], grad_fn=<AddmmBackward>)
testing accuracy: 0.9757462739944458

能夠看到最終 weight 接近 1, 2, 3,bias 接近 8。和前一篇文章最後的例子比較還能夠發現代碼除了定義模型的部分之外幾乎如出一轍 (後面的代碼基本上都是相同的結構,這個系列是先學套路在學細節😈) 。

看到這裏你可能會以爲,怎麼咱們一直都在學習一次方程式,不能作更復雜的事情嗎😡?

如咱們看到的,線性模型只能計算一次方程式,若是咱們給的數據不知足任何一次方程式,這個模型將沒法學習成功,那麼疊加多層線性模型能夠學習更復雜的數據嗎?

如下是一層和兩層人工神經元網絡的公式例子:

由於咱們尚未學到激活函數,先去掉激活函數,而後展開沒有激活函數的兩層人工神經元網絡看看是什麼樣子:

從上圖能夠看出,若是沒有激活函數,兩層人工神經元網絡和一層神經元網絡效果是同樣的,實際上,若是沒有激活函數無論疊加多少層都和一層同樣🙀,因此若是咱們要構建多層網絡,必須添加激活函數。

激活函數

激活函數的做用是讓人工神經元網絡支持學習非線性的數據,所謂非線性的數據就是不知足任何一次方程式的數據,輸出和輸入之間不會按必定比例變化。舉個很簡單的例子,若是須要按碼農的數量計算某個項目所需的完工時間,一個碼農須要一個月,兩個碼農須要半個月,三個碼農須要十天,四個碼農須要一個星期,以後不管請多少個碼農都須要一個星期,多出來的碼農只會吃閒飯,使用圖表能夠表現以下:

若是不用激活函數,只用線性模型能夠調整 w 到 -5.4,b 到 30,使用圖表對比 -5.4 x + 30 和實際數據以下,咱們能夠看到不只預測的偏差較大,隨着碼農數量的增加預測所需的完工時間會變爲負數😱:

那麼使用激活函數會怎樣呢?咱們以最簡單也是最流行的激活函數 ReLU (Rectified Linear Unit) 爲例,ReLU 函數的定義以下:

意思是傳入值大於 0 時返回原值,不然返回 0,用 python 代碼能夠表現以下:

def relu(x):
    if x > 0:
        return x
    return 0

再看看如下結合了 ReLU 激活函數的兩層人工神經元網絡 (最後一層不使用激活函數):

試試計算上面的例子:

def relu(x):
    if x > 0:
        return x
    return 0

for x in range(1, 11):
    h1 = relu(x * -1 + 2)
    h2 = relu(x * -1 + 3)
    h3 = relu(x * -1 + 4)
    y = h1 * 10 + h2 * 2 + h3 * 3 + 7
    print(x, y)

輸入以下,能夠看到添加激活函數後模型可以支持計算上面的非線性數據😎:

1 30
2 15
3 10
4 7
5 7
6 7
7 7
8 7
9 7
10 7

激活函數除了上述介紹的 ReLU 還有不少,如下是一些經常使用的激活函數:

若是你想看它們的曲線和導函數能夠參考如下連接:

添加激活函數之後求導函數值一樣可使用連鎖律,若是第一層返回的是正數,那麼就和沒有 ReLU 時的計算同樣 (導函數爲 1):

>>> w1 = torch.tensor(5.0, requires_grad=True)
>>> b1 = torch.tensor(1.0, requires_grad=True)
>>> w2 = torch.tensor(6.0, requires_grad=True)
>>> b2 = torch.tensor(7.0, requires_grad=True)

>>> x = torch.tensor(2.0)
>>> y = torch.nn.functional.relu(x * w1 + b1) * w2 + b2

# 假設咱們要調整 w1, b1, w2, b2 使得 y 接近 0
>>> y
tensor(73., grad_fn=<AddBackward0>)
>>> y.abs().backward()

# w1 的導函數值爲 x * w2
>>> w1.grad
tensor(12.)

# b1 的導函數值爲 w2
>>> b1.grad
tensor(6.)

# w2 的導函數值爲 x * w1 + b1
>>> w2.grad
tensor(11.)

# b1 的導函數值爲 1
>>> b2.grad
tensor(1.)

假設第一層返回的是負數,那麼 ReLU 會讓上一層的導函數值爲 0 (導函數爲 0):

>>> w1 = torch.tensor(5.0, requires_grad=True)
>>> b1 = torch.tensor(1.0, requires_grad=True)
>>> w2 = torch.tensor(6.0, requires_grad=True)
>>> b2 = torch.tensor(7.0, requires_grad=True)

>>> x = torch.tensor(2.0)
>>> y = torch.nn.functional.relu(x * w1 + b1) * w2 + b2

>>> y
tensor(7., grad_fn=<AddBackward0>)
>>> y.backward()

>>> w1.grad
tensor(-0.)
>>> b1.grad
tensor(0.)
>>> w2.grad
tensor(0.)
>>> b2.grad
tensor(1.)

雖然 ReLU 能夠適合大部分場景,但有時候咱們仍是須要選擇其餘激活函數,選擇激活函數通常會考慮如下的因素:

  • 計算量
  • 是否存在梯度消失 (Vanishing Gradient) 問題
  • 是否存在中止學習問題

從上圖咱們能夠看到 ReLU 的計算量是最少的,Sigmoid 和 Tanh 的計算量則很大,使用計算量大的激活函數會致使學習過程更慢。

而梯度消失 (Vanishing Gradient, 以前的文章把 Gradient 翻譯爲傾斜,但數學上的正確叫法是梯度) 問題則是在人工神經元網絡層數增多之後出現的問題,若是層數不斷增多,使用連鎖律求導函數的時候會不斷疊加激活函數的導函數,部分激活函數例如 Sigmoid 和 Tanh 的導函數會隨着疊加次數增多而不斷的減小導函數值。例若有 3 層的時候,第 3 層導函數值多是 6, -2, 1,第 2 層的導函數值多是 0.07, 0.68, -0.002,第 1 層的導函數值多是 0.0004, -0.00016, -0.00003,也就是前面的層參數基本上不會調整,只有後面的層參數不斷變化,致使浪費計算資源和不能徹底發揮模型的能力。激活函數 ReLU 則不會存在梯度消失問題,由於無論疊加多少層只要中間不存在負數則導函數值會一直傳遞上去,這也是 ReLU 流行的緣由之一。

中止學習問題是模型達到某個狀態 (未學習成功) 之後無論怎麼調整參數都不會變化的問題,一個簡單的例子是使用 ReLU 時,若是第一層的輸出恰好所有都是負數,那隱藏值則所有爲 0,導函數值也爲 0,無論再怎麼訓練參數都不會變化。LeakyReLU 與 ELU 則是爲了解決中止學習問題產生的,但由於增長計算量和容許負數可能會帶來其餘影響,咱們通常都會先使用 ReLU,出現中止學習問題再試試 ReLU 的派生函數。

Sigmoid 和 Tanh 雖然有梯度消失問題,可是它們能夠用於在指定場景下轉換數值到 0 ~ 1 和 -1 ~ 1。例如 Sigmoid 能夠用在最後一層表現可能性,100 表示很是有可能 (轉換到 1),50 也表明很是有可能 (轉換到 1),1 表明比較有可能 (轉換到 0.7311),0 表明不肯定 (轉換到 0.5),-1 表明比較不可能 (轉換到 0.2689),-100 表明很不可能 (轉換到 0)。然後面文章介紹的 LSTM 模型也會使用 Sigmoid 決定須要忘記哪些內部狀態,Tanh 決定應該怎樣更新內部狀態。此外還有 Softmax 等通常只用在最後一層的函數,Softmax 能夠用於在分類的時候判斷哪一個類別可能性最大,例如識別貓狗豬的時候最後一層給出 6, 5, 8,數值越大表明屬於該分類的可能性越高,通過 Softmax 轉換之後就是 0.1142, 0.0420, 0.8438,表明有 11.42% 的可能性是貓,4.2% 的可能性是狗,84.38% 的可能性是豬。

多層線性模型

接下來咱們看看怎樣在 pytorch 裏面定義多層線性模型,上一節已經介紹過 torch.nn.Linear 是 pytorch 中單層線性模型的封裝,組合多個 torch.nn.Linear 就能夠實現多層線性模型。

組合 torch.nn.Linear 有兩種方法,一種建立一個自定義的模型類,關於模型類在上一篇已經介紹過:

# 引用 pytorch,nn 等同於 torch.nn
import torch
from torch import nn

# 定義模型
class MyModel(nn.Module):
    def __init__(self):
        # 初始化基類
        super().__init__()
        # 定義參數
        # 這裏一共定義了三層
        #   第一層接收 2 個輸入,返回 32 個隱藏值 (內部 weight 矩陣爲 32 行 2 列)
        #   第二層接收 32 個隱藏值,返回 64 個隱藏值 (內部 weight 矩陣爲 64 行 32 列)
        #   第三層接收 64 個隱藏值,返回 1 個輸出 (內部 weight 矩陣爲 1 行 64 列)
        self.layer1 = nn.Linear(in_features=2, out_features=32)
        self.layer2 = nn.Linear(in_features=32, out_features=64)
        self.layer3 = nn.Linear(in_features=64, out_features=1)

    def forward(self, x):
        # x 是一個矩陣,行數表明批次,列數表明輸入個數,例若有 50 個批次則爲 50 行 2 列

        # 計算第一層返回的隱藏值,例若有 50 個批次則 hidden1 爲 50 行 32 列
        # 計算矩陣乘法時會轉置 layer1 內部的 weight 矩陣,50 行 2 列乘以 2 行 32 列等於 50 行 32 列
        hidden1 = nn.functional.relu(self.layer1(x))

        # 計算第二層返回的隱藏值,例若有 50 個批次則 hidden2 爲 50 行 64 列
        hidden2 = nn.functional.relu(self.layer2(hidden1))

        # 計算第三層返回的輸出,例若有 50 個批次則 y 爲 50 行 1 列
        y = self.layer3(hidden2)

        # 返回輸出
        return y

# 建立模型實例
model = MyModel()

咱們能夠定義任意數量的層,但每一層的接收值個數 (in_features) 必須等於上一層的返回值個數 (out_features),第一層的接收值個數須要等於輸入個數,最後一層的返回值個數須要等於輸出個數。

第二種方法是使用 torch.nn.Sequential,這種方法更簡便:

# 引用 pytorch,nn 等同於 torch.nn
import torch
from torch import nn

# 建立模型實例,效果等同於第一種方法
model = nn.Sequential(
    nn.Linear(in_features=2, out_features=32),
    nn.ReLU(),
    nn.Linear(in_features=32, out_features=64),
    nn.ReLU(),
    nn.Linear(in_features=64, out_features=1))

# 注:
# nn.functional.relu(x) 等於 nn.ReLU()(x)

如前面所說的,層數越多隱藏值數量越多模型就越強大,但須要更長的訓練時間而且更容易發生過擬合問題,實際操做時咱們能夠選擇一個比較小的模型,再按須要增長層數和隱藏值個數。

三層線性模型的計算圖能夠表現以下,之後這個系列在講解其餘模型的時候也會使用相同形式的圖表表示模型的計算路徑:

實例 - 根據碼農條件求工資

咱們已經瞭解到如何建立多層線性模型,如今能夠試試解決比較實際的問題了,對於大部分碼農來講最實際的問題就是每月能拿多少工資😿,那就來創建一個根據碼農的條件,預測能夠拿到多少工資的模型吧。

如下是從某個地方祕密收集回來的碼農條件和工資數據(實際上是按某種規律隨機生成出來的,不是實際數據🙀):

年齡,性別,工做經驗,Java,NET,JS,CSS,HTML,工資
29,0,0,1,2,2,1,4,12500
22,0,2,2,3,1,2,5,15500
24,0,4,1,2,1,1,2,16000
35,0,6,3,3,0,1,0,19500
45,0,18,0,5,2,0,5,17000
24,0,2,0,0,0,1,1,13500
23,1,2,2,3,1,1,0,10500
41,0,16,2,5,5,2,0,16500
50,0,18,0,5,0,5,2,16500
20,0,0,0,5,2,0,1,12500
26,0,6,1,5,5,1,1,27000
46,0,12,0,5,4,4,2,12500
26,0,6,1,5,3,1,1,23500
40,0,9,0,0,1,0,1,17500
41,0,20,3,5,3,3,5,20500
26,0,4,0,1,2,4,0,18500
42,0,18,5,0,0,2,5,18500
21,0,1,1,0,1,2,0,12000
26,0,1,0,0,0,0,2,12500

完整數據有 50000 條,能夠從 https://github.com/303248153/BlogArchive/tree/master/ml-03/salary.csv 下載。

每一個碼農有如下條件:

  • 年齡
  • 性別 (0: 男性, 1: 女性)
  • 工做經驗年數 (僅限互聯網行業)
  • Java 編碼熟練程度 (0 ~ 5)
  • NET 編碼熟練程度 (0 ~ 5)
  • JS 編碼熟練程度 (0 ~ 5)
  • CSS 編碼熟練程度 (0 ~ 5)
  • HTML 編碼熟練程度 (0 ~ 5)

也就是有 8 個輸入,1 個輸出 (工資),咱們能夠創建三層線性模型:

  • 第一層接收 8 個輸入返回 100 個隱藏值
  • 第二層接收 100 個隱藏值返回 50 個隱藏值
  • 第三層接收 50 個隱藏值返回 1 個輸出

寫成代碼以下 (這裏使用了 pandas 類庫讀取 csv,使用 pip3 install pandas 便可安裝):

# 引用 pytorch 和 pandas
import pandas
import torch
from torch import nn

# 定義模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(in_features=8, out_features=100)
        self.layer2 = nn.Linear(in_features=100, out_features=50)
        self.layer3 = nn.Linear(in_features=50, out_features=1)

    def forward(self, x):
        hidden1 = nn.functional.relu(self.layer1(x))
        hidden2 = nn.functional.relu(self.layer2(hidden1))
        y = self.layer3(hidden2)
        return y

# 給隨機數生成器分配一個初始值,使得每次運行均可以生成相同的隨機數
# 這是爲了讓訓練過程可重現,你也能夠選擇不這樣作
torch.random.manual_seed(0)

# 建立模型實例
model = MyModel()

# 建立損失計算器
loss_function = torch.nn.MSELoss()

# 建立參數調整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.0000001)

# 從 csv 讀取原始數據集
df = pandas.read_csv('salary.csv')
dataset_tensor = torch.tensor(df.values, dtype=torch.float)

# 切分訓練集 (60%),驗證集 (20%) 和測試集 (20%)
random_indices = torch.randperm(dataset_tensor.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_tensor[traning_indices][:,:-1]
traning_set_y = dataset_tensor[traning_indices][:,-1:]
validating_set_x = dataset_tensor[validating_indices][:,:-1]
validating_set_y = dataset_tensor[validating_indices][:,-1:]
testing_set_x = dataset_tensor[testing_indices][:,:-1]
testing_set_y = dataset_tensor[testing_indices][:,-1:]

# 開始訓練過程
for epoch in range(1, 1000):
    print(f"epoch: {epoch}")

    # 根據訓練集訓練並修改參數
    # 切換模型到訓練模式,將會啓用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.train()

    for batch in range(0, traning_set_x.shape[0], 100):
        # 切分批次,一次只計算 100 組數據
        batch_x = traning_set_x[batch:batch+100]
        batch_y = traning_set_y[batch:batch+100]
        # 計算預測值
        predicted = model(batch_x)
        # 計算損失
        loss = loss_function(predicted, batch_y)
        # 從損失自動微分求導函數值
        loss.backward()
        # 使用參數調整器調整參數
        optimizer.step()
        # 清空導函數值
        optimizer.zero_grad()

    # 檢查驗證集
    # 切換模型到驗證模式,將會禁用自動微分,批次正規化 (BatchNorm) 與 Dropout
    model.eval()
    predicted = model(validating_set_x)
    validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean()
    print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")
    print(f"validating accuracy: {validating_accuracy}")

# 檢查測試集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")

# 手動輸入數據預測輸出
while True:
    try:
        print("enter input:")
        r = list(map(float, input().split(",")))
        x = torch.tensor(r).view(1, len(r))
        print(model(x)[0,0].item())
    except Exception as e:
        print("error:", e)

輸出以下,能夠看到最後沒有參與訓練的驗證集的正確度達到了 93.3% 測試集的正確度達到了 93.1%,模型成功的摸索出了某種規律:

epoch: 1
validating x: tensor([[42.,  0., 16.,  ...,  5.,  5.,  5.],
        [28.,  0.,  0.,  ...,  4.,  0.,  0.],
        [23.,  0.,  3.,  ...,  0.,  1.,  1.],
        ...,
        [44.,  1., 15.,  ...,  2.,  0.,  2.],
        [30.,  0.,  1.,  ...,  1.,  1.,  2.],
        [50.,  1., 18.,  ...,  5.,  5.,  2.]]), y: tensor([[24500.],
        [12500.],
        [17500.],
        ...,
        [10500.],
        [15000.],
        [16000.]]), predicted: tensor([[27604.2578],
        [15934.7607],
        [14536.8984],
        ...,
        [23678.5547],
        [18189.6953],
        [29968.8789]], grad_fn=<AddmmBackward>)
validating accuracy: 0.661293625831604
epoch: 2
validating x: tensor([[42.,  0., 16.,  ...,  5.,  5.,  5.],
        [28.,  0.,  0.,  ...,  4.,  0.,  0.],
        [23.,  0.,  3.,  ...,  0.,  1.,  1.],
        ...,
        [44.,  1., 15.,  ...,  2.,  0.,  2.],
        [30.,  0.,  1.,  ...,  1.,  1.,  2.],
        [50.,  1., 18.,  ...,  5.,  5.,  2.]]), y: tensor([[24500.],
        [12500.],
        [17500.],
        ...,
        [10500.],
        [15000.],
        [16000.]]), predicted: tensor([[29718.2441],
        [15790.3799],
        [15312.5791],
        ...,
        [23395.9668],
        [18672.0234],
        [31012.4062]], grad_fn=<AddmmBackward>)
validating accuracy: 0.6694601774215698

省略途中輸出

epoch: 999
validating x: tensor([[42.,  0., 16.,  ...,  5.,  5.,  5.],
        [28.,  0.,  0.,  ...,  4.,  0.,  0.],
        [23.,  0.,  3.,  ...,  0.,  1.,  1.],
        ...,
        [44.,  1., 15.,  ...,  2.,  0.,  2.],
        [30.,  0.,  1.,  ...,  1.,  1.,  2.],
        [50.,  1., 18.,  ...,  5.,  5.,  2.]]), y: tensor([[24500.],
        [12500.],
        [17500.],
        ...,
        [10500.],
        [15000.],
        [16000.]]), predicted: tensor([[22978.7656],
        [13050.8018],
        [18396.5176],
        ...,
        [11449.5059],
        [14791.2969],
        [16635.2578]], grad_fn=<AddmmBackward>)
validating accuracy: 0.9311849474906921
testing x: tensor([[48.,  1., 18.,  ...,  5.,  0.,  5.],
        [22.,  1.,  2.,  ...,  2.,  1.,  2.],
        [24.,  0.,  1.,  ...,  3.,  2.,  0.],
        ...,
        [24.,  0.,  4.,  ...,  0.,  1.,  1.],
        [39.,  0.,  0.,  ...,  0.,  5.,  5.],
        [36.,  0.,  5.,  ...,  3.,  0.,  3.]]), y: tensor([[14000.],
        [10500.],
        [13000.],
        ...,
        [15500.],
        [12000.],
        [19000.]]), predicted: tensor([[15481.9062],
        [11011.7266],
        [12192.7949],
        ...,
        [16219.3027],
        [11074.0420],
        [20305.3516]], grad_fn=<AddmmBackward>)
testing accuracy: 0.9330180883407593
enter input:

最後咱們手動輸入碼農條件能夠得出預測輸出 (35 歲男 10 年經驗 Java 5 NET 2 JS 1 CSS 1 HTML 2 大約可拿 26k 😤):

enter input:
35,0,10,5,2,1,1,2
26790.982421875

雖然訓練成功了,但以上代碼還有幾個問題:

  • 學習比率設置的很是低 (0.0000001),這是由於咱們沒有對數據進行正規化處理
  • 老是會固定訓練 1000 次,不能像第一篇文章提到過的那樣自動檢測哪一次的正確度最高,沒有考慮過擬合問題
  • 不能把訓練結果保存到硬盤,每次運行都須要從頭訓練
  • 須要把全部數據一次性讀取到內存中,數據過多時內存可能不夠用
  • 沒有提供一個使用訓練好的模型的接口

這些問題都會在下篇文章一個個解決。

寫在最後

在這一篇咱們終於看到怎樣應用機器學習到更復雜的數據中,但路還有很長🏃。

下一篇咱們會作個稍息,介紹一些訓練過程當中使用的技巧 (原本打算寫到這篇裏面的,但這篇已經足夠長了),再下一篇介紹能夠處理不定長連續數據的遞歸模型。

相關文章
相關標籤/搜索