主頁 >  其他 > 寫給程式員的機器學習入門 (三) - 線性模型,激活函式與多層線性模型

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

2020-09-14 10:40:34 其他

生物神經元與人工神經元

在了解神經元網路之前,我們先簡單的看看生物學上的神經元是什么樣子的,下圖摘自維基百科:

(因為我不是專家,這里的解釋只用于理解人工神經元模擬了生物神經元的什么地方,不一定完全準確)

神經元主要由細胞體和細胞突組成,而細胞突分為樹突 (Dendrites) 和軸突 (Axon),樹突負責接收其他神經元輸入的電流,而軸突負責把電流輸出給其他神經元,一個神經元可以通過樹突從多個神經元接收電流,如果電流沒有達到某個閾值則神經元不會把電流輸出,如果電流達到了某個閾值則神經元會通過軸突的突觸把電流輸出給其他神經元,這樣的規則被稱為全有全無律,輸入電流達到閾值以后輸出電流的狀態又稱為到達動作電位,動作電位會持續 1 ~ 2 毫秒,之后會進入約 0.5 毫秒的絕對不應期,無論輸入多大的電流都不會輸出,然后再進入約 3.5 毫秒的相對不應期,需要電流達到更大的閾值才會輸出,最后回傳靜息電位,神經元之間連接起來的網路稱為神經元網路,人的大腦中大約有 860 億個神經元,因為 860 億個神經元可以同時作業,所以目前的計算機無法模擬這種作業方式 (除非開發專用的芯片),只能模擬一部分的作業方式和使用更小規模的網路,

計算機模擬神經元網路使用的是人工神經元,單個人工神經元可以用以下公式表達:

其中 n 代表輸入的個數,你可以把 n 看作這個神經元擁有的樹突個數,x 看作每個樹突輸入電流的值;而 w (weight) 代表各個輸入的權重,也就是各個樹突對電流大小的調整;而 b (bias) 用于調整各個輸入乘權重相加后的值,使得這個值可以配合某個閾值作業;而 g 則是激活函式,用于判斷值是否達到閾值并輸出和輸出多少,通常會使用非線性函式;而 y 則是輸出的值,可以把它看作軸突輸出的電流,連接這個 y 到其他神經元就可以組建神經元網路,

我們在前兩篇看到的其實就是只有一個輸入并且沒有激活函式的單個人工神經元,把同樣的輸入傳給多個神經元 (第一層),然后再傳給其他神經元 (第二層),然后再傳給其他神經元 (第三層) 就可以組建人工神經元網路了,同一層的神經元個數越多,神經元的層數越多,網路就越強大,但需要更多的運算時間并且更有可能發生第一篇文章講過的過擬合 (Overfitting) 現象,

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

神經元中包含的 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 還有很多,以下是一些常用的激活函式:

如果你想看它們的曲線和導函式可以參考以下鏈接:

  • https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity
  • https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html

添加激活函式以后求導函式值同樣可以使用連鎖律,如果第一層回傳的是正數,那么就和沒有 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) 問題則是在人工神經元網路層數增多以后出現的問題,如果層數不斷增多,使用連鎖律求導函式的時候會不斷疊加激活函式的導函式,部分激活函式例如 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 次,不能像第一篇文章提到過的那樣自動檢測哪一次的正確度最高,沒有考慮過擬合問題
  • 不能把訓練結果保存到硬碟,每次運行都需要從頭訓練
  • 需要把所有資料一次性讀取到記憶體中,資料過多時記憶體可能不夠用
  • 沒有提供一個使用訓練好的模型的介面

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

寫在最后

在這一篇我們終于看到怎樣應用機器學習到更復雜的資料中,但路還有很長??,

下一篇我們會做個稍息,介紹一些訓練程序中使用的技巧 (本來打算寫到這篇里面的,但這篇已經足夠長了),再下一篇介紹可以處理不定長連續資料的遞回模型,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/33992.html

標籤:其他

上一篇:k8s創建一個service 型別NodePort 報錯 iptables-restore: exit status 1 (iptables-restore:

下一篇:網路基礎 資料的傳輸

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more