TensorFlow從1到2(七)線性回歸模型預測汽車油耗以及訓練過程優化


線性回歸模型

“回歸”這個詞,既是Regression算法的名稱,也代表了不同的計算結果。當然結果也是由算法決定的。
不同於前面講過的多個分類算法或者邏輯回歸,線性回歸模型的結果是一個連續的值。
實際上我們第一篇的房價預測就屬於線性回歸算法,如果把這個模型用於預測,結果是一個連續值而不是有限的分類。
從代碼上講,那個例子更多的是為了延續從TensorFlow 1.x而來的解題思路,我不想在這個系列的第一篇就給大家印象,TensorFlow 2.0成為了完全不同的另一個東西。在TensorFlow 2.0中,有更方便的方法可以解決類似問題。
回歸算法在大多數機器學習課程中,也都是最早會學習的算法。所以對這個算法,我們都不陌生。
因此本篇的重點不在算法本身,也不在油耗的預測,而是通過油耗預測這樣簡單的例子,介紹在TensorFlow 2.0中,如何更好的對訓練過程進行監控和管理,還有其它一些方便有效的小技巧。

了解樣本數據

在機器學習算法本身沒有大的突破的情況下,對樣本數據的選取、預處理往往是項目成功的關鍵。我們接續前一篇繼續說一說樣本數據。
樣本數據的預處理依靠對樣本數據的了解和分析。Python的交互模式配合第三方工具包則是對樣本數據分析的強力武器。
下面我們使用Python的交互模式,載入油耗預測的樣本數據,先直觀的看一下樣本數據:
(第一次載入樣本數據會從網上下載,速度比較慢)

$ python3
Python 3.7.3 (default, Mar 27 2019, 09:23:39) 
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from tensorflow import keras
>>> import pandas as pd
>>> dataset_path = keras.utils.get_file("auto-mpg.data", "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
>>> column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight',
...                 'Acceleration', 'Model Year', 'Origin']
>>> raw_dataset = pd.read_csv(dataset_path, names=column_names,
...                       na_values = "?", comment='\t',
...                       sep=" ", skipinitialspace=True)
>>> 
>>> raw_dataset.tail()
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  Origin
393  27.0          4         140.0        86.0  2790.0          15.6          82       1
394  44.0          4          97.0        52.0  2130.0          24.6          82       2
395  32.0          4         135.0        84.0  2295.0          11.6          82       1
396  28.0          4         120.0        79.0  2625.0          18.6          82       1
397  31.0          4         119.0        82.0  2720.0          19.4          82       1
>>> raw_dataset
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  Origin
0    18.0          8         307.0       130.0  3504.0          12.0          70       1
1    15.0          8         350.0       165.0  3693.0          11.5          70       1
2    18.0          8         318.0       150.0  3436.0          11.0          70       1
3    16.0          8         304.0       150.0  3433.0          12.0          70       1
..    ...        ...           ...         ...     ...           ...         ...     ...
373  24.0          4         140.0        92.0  2865.0          16.4          82       1
374  23.0          4         151.0         NaN  3035.0          20.5          82       1
..    ...        ...           ...         ...     ...           ...         ...     ...
396  28.0          4         120.0        79.0  2625.0          18.6          82       1
397  31.0          4         119.0        82.0  2720.0          19.4          82       1

[398 rows x 8 columns]
>>> 

本篇就不用表格來逐行解釋屬性列的含義了,我們會在用到的每一列單獨說明。
使用raw_dataset.tail()列出數據的最后幾行,顯示數據一共只有398行。說明數據集並不是很大,可以直接全部列出來粗略的看一下。這一步使用Excel之類的工具效果可能更好。不過習慣命令行操作的工程師直接列出也是一樣的。
數據中可以看到第374行,在Horsepower(發動機功率)一列,意外的有NaN未知數據。這樣的數據當然是無效的,需要首先進行數據清洗。大數據轉行過來的技術人員都熟悉,數據清洗是保證數據有效性必不可少的手段。
其實這里的NaN並不能完全說意外,我們在使用Pandas打開數據集的時候使用了參數:na_values = "?",這是指數據集中如果有“?”字符,則數據當做無效數據,方便后續使用內置方法處理。這個參數可以根據你獲取的數據集修改。
比如檢查數據集是否有無效數據可以使用isna()方法:

>>> # 繼續上面的交互操作
... 
>>> raw_dataset.isna().sum()
MPG             0
Cylinders       0
Displacement    0
Horsepower      6
Weight          0
Acceleration    0
Model Year      0
Origin          0
dtype: int64
>>> 
>>> # 確認有6個無效數據,需要拋棄相應行
... # 將數據復制一份,防止誤操作
... 
>>> dataset = raw_dataset.copy()
>>> 
>>> # 拋棄無效數據所在行
... 
>>> dataset = dataset.dropna()
>>> 

接着Origin一列,實際是一個分類類型,並不是數字。分別代表車型的產地為美國、歐洲或者日本。上一篇中我們已經有了經驗,我們要把這個數據列轉成one-hot編碼方式:

>>> # 取出Origin數據列,原數據集中將不會再有這一列
... 
>>> origin = dataset.pop('Origin')
>>> 
>>> # 根據分類編碼,分別為新對應列賦值1.0
... 
>>> dataset['USA'] = (origin == 1)*1.0
>>> dataset['Europe'] = (origin == 2)*1.0
>>> dataset['Japan'] = (origin == 3)*1.0
>>> 
>>> # 列出新的數據集尾部,以觀察結果
... 
>>> dataset.tail()
      MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  Model Year  USA  Europe  Japan
393  27.0          4         140.0        86.0  2790.0          15.6          82  1.0     0.0    0.0
394  44.0          4          97.0        52.0  2130.0          24.6          82  0.0     1.0    0.0
395  32.0          4         135.0        84.0  2295.0          11.6          82  1.0     0.0    0.0
396  28.0          4         120.0        79.0  2625.0          18.6          82  1.0     0.0    0.0
397  31.0          4         119.0        82.0  2720.0          19.4          82  1.0     0.0    0.0
>>> 

不同前一篇,這次做One-hot編碼的方式直接使用了Python語言的邏輯計算表達式,效果一樣好。
關於怎么知道哪個數字代表哪個產地,如果是自己設計的數據采集方式,你自己當然應當知道。如果使用了別人的數據集,應當仔細閱讀數據的說明。這里就不多解釋了。
我們還可以使用seaborn工具(第一篇中已經安裝了)對數據做進一步分析,seaborn包含一組散列圖繪制工具,可以更直觀的揭示數據之間的關聯:

>>> # 繼續上面的交互操作
... 
>>> import seaborn as sns
>>> import matplotlib.pyplot as plt
>>> sns.pairplot(dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")
<seaborn.axisgrid.PairGrid object at 0x139405358>
>>> plt.show()


上圖選取了MPG(油耗)、Cylinders(氣缸數量)、Displacement(排氣量)、Weight(車重)四項數據,做兩兩對比形成的散點圖。
散點矩陣圖(SPLOM:Scatterplot Matrix)可以用於粗略揭示數據中,不同列之間的相關性。可以粗略估計哪些變量是正相關的,哪些是負相關的,進而為下一步數據分析提供決策。當然這些圖需要行業專家的理解和分析。然后為程序人員提供間接幫助。

數據規范化

從剛才的樣本數據中,我們可以看出各列的數據,取值范圍還是很不均衡的。在進入模型之前,我們需要做數據規范化。也就是將所有列的數據統一為在同一個取值范圍的浮點數。
我們可以利用Pandas中對數據的統計結果做數據的規范化,這樣可以省去自己寫程序做數據統計。

>>> # 繼續上面的交互操作
... 
>>> data_stats=dataset.describe()
>>> data_stats
              MPG   Cylinders  Displacement  Horsepower  ...  Model Year         USA      Europe       Japan
count  392.000000  392.000000    392.000000  392.000000  ...  392.000000  392.000000  392.000000  392.000000
mean    23.445918    5.471939    194.411990  104.469388  ...   75.979592    0.625000    0.173469    0.201531
std      7.805007    1.705783    104.644004   38.491160  ...    3.683737    0.484742    0.379136    0.401656
min      9.000000    3.000000     68.000000   46.000000  ...   70.000000    0.000000    0.000000    0.000000
25%     17.000000    4.000000    105.000000   75.000000  ...   73.000000    0.000000    0.000000    0.000000
50%     22.750000    4.000000    151.000000   93.500000  ...   76.000000    1.000000    0.000000    0.000000
75%     29.000000    8.000000    275.750000  126.000000  ...   79.000000    1.000000    0.000000    0.000000
max     46.600000    8.000000    455.000000  230.000000  ...   82.000000    1.000000    1.000000    1.000000

[8 rows x 10 columns]
>>> 

對於每一列,Pandas都進行了記錄總數、平均值、標准差、最小值等統計。我們做數據規范化,可以直接使用這些參數來進行。

>>> # 繼續上面的交互操作
... 
>>> data_stats=data_stats.transpose()
>>> data_stats
              count         mean         std     min       25%      50%       75%     max
MPG           392.0    23.445918    7.805007     9.0    17.000    22.75    29.000    46.6
Cylinders     392.0     5.471939    1.705783     3.0     4.000     4.00     8.000     8.0
Displacement  392.0   194.411990  104.644004    68.0   105.000   151.00   275.750   455.0
Horsepower    392.0   104.469388   38.491160    46.0    75.000    93.50   126.000   230.0
Weight        392.0  2977.584184  849.402560  1613.0  2225.250  2803.50  3614.750  5140.0
Acceleration  392.0    15.541327    2.758864     8.0    13.775    15.50    17.025    24.8
Model Year    392.0    75.979592    3.683737    70.0    73.000    76.00    79.000    82.0
USA           392.0     0.625000    0.484742     0.0     0.000     1.00     1.000     1.0
Europe        392.0     0.173469    0.379136     0.0     0.000     0.00     0.000     1.0
Japan         392.0     0.201531    0.401656     0.0     0.000     0.00     0.000     1.0
>>> norm_data = (dataset - data_stats['mean'])/data_stats['std']
>>> norm_data.tail()
          MPG  Cylinders  Displacement  Horsepower    Weight  Acceleration  Model Year       USA    Europe     Japan
393  0.455359  -0.862911     -0.519972   -0.479835 -0.220842      0.021267    1.634321  0.773608 -0.457538 -0.501749
394  2.633448  -0.862911     -0.930889   -1.363154 -0.997859      3.283479    1.634321 -1.289347  2.180035 -0.501749
395  1.095974  -0.862911     -0.567753   -0.531795 -0.803605     -1.428605    1.634321  0.773608 -0.457538 -0.501749
396  0.583482  -0.862911     -0.711097   -0.661694 -0.415097      1.108671    1.634321  0.773608 -0.457538 -0.501749
397  0.967851  -0.862911     -0.720653   -0.583754 -0.303253      1.398646    1.634321  0.773608 -0.457538 -0.501749
>>> 

初步的程序

有了上面這些嘗試,開始着手編程,主要有以下幾步工作:

  • 將樣本數據集分為訓練集和測試集兩部分
  • 將數據集中的MPG(百英里油耗數)去掉,單獨出來作為數據集的標注結果,達成監督學習
  • 構建模型,編譯模型
  • 使用訓練集數據對模型進行訓練
  • 使用測試集樣本進行數據預測,評估模型效果

我們使用附帶注釋的源碼來代替講解:

#!/usr/bin/env python3

from __future__ import absolute_import, division, print_function

# 引入各項擴展庫
import pathlib

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

# 下載樣本數據
dataset_path = keras.utils.get_file("auto-mpg.data", "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
# 樣本中所需要的列名稱
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight',
                'Acceleration', 'Model Year', 'Origin'] 
# 從樣本文件中讀取指定列的數據
raw_dataset = pd.read_csv(dataset_path, names=column_names,
                          na_values="?", comment='\t',
                          sep=" ", skipinitialspace=True)

# 復制一份數據做后續操作
dataset = raw_dataset.copy()

# 數據清洗,去掉無意義的數據
dataset = dataset.dropna()

# 將Origin數據做one-hot編碼,相當於轉換成3個產地字段
origin = dataset.pop('Origin')
dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0

# 隨機分配80%的數據作為訓練集
# frac是保留80%的數據
# random_state相當於隨機數的種子,在這里固定一個值是為了每次運行,隨機分配得到的樣本集是相同的
train_dataset = dataset.sample(frac=0.8, random_state=0)
# 訓練集的數據去除掉,剩下的是20%,作為測試集
test_dataset = dataset.drop(train_dataset.index)
# 獲取數據集的統計信息
train_stats = train_dataset.describe()
# MPG是訓練模型要求的結果,也就是標注字段,沒有意義,從統計中去除
train_stats.pop("MPG")
# 對統計結果做行列轉置,方便將統計結果作為下面做數據規范化的參數
train_stats = train_stats.transpose()

# 訓練集和測試集的數據集都去掉MPG列,單獨取出作為標注
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

# 定義一個數據規范化函數幫助簡化操作
def norm(x):
    return (x - train_stats['mean']) / train_stats['std']
# 訓練集和測試集數據規范化
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

# 構建回歸模型
def build_model():
    model = keras.Sequential([
        layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
        layers.Dense(64, activation='relu'),
        layers.Dense(1)    # 回歸的主要區別就是最后不需要激活函數,從而保證最后是一個連續值
    ])

    optimizer = tf.keras.optimizers.RMSprop(0.001)

    model.compile(loss='mse',
                  optimizer=optimizer,
                  metrics=['acc'])
    return model

model = build_model()
# 顯示構造的模型
model.summary()

EPOCHS = 1000
history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split=0.2, verbose=1)

# 使用測試集預測數據
test_result = model.predict(normed_test_data)
# 顯示預測結果
print('===================\ntest_result:', test_result)

使用的模型非常簡單,訓練和預測也沒有什么特別之處,無需再講解。執行程序的輸出大致如下:

$ ./mpg1.py
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________
Train on 251 samples, validate on 63 samples
Epoch 1/1000
251/251 [==============================] - 0s 2ms/sample - loss: 582.3197 - acc: 0.0000e+00 - val_loss: 582.2971 - val_acc: 0.0000e+00
Epoch 2/1000
251/251 [==============================] - 0s 67us/sample - loss: 542.1007 - acc: 0.0000e+00 - val_loss: 541.7508 - val_acc: 0.0000e+00
	......
Epoch 1000/1000
251/251 [==============================] - 0s 58us/sample - loss: 2.7232 - acc: 0.0000e+00 - val_loss: 9.4673 - val_acc: 0.0000e+00
===================
test_result: [[16.366997 ]
 [ 8.665408 ]
 [ 8.548    ]
 [25.14063  ]
 [18.678812 ]
	......

輸出的數據中,一開始是所使用的模型信息,這是model.summary()輸出的結果。如果有時間,翻翻前面vgg-19和resnet50網絡的模型,也試用一下,保證你得到一個驚訝的結果:)
隨后是1000次迭代的訓練輸出。最后是預測的結果。
如果你細心的話,可能已經發現了問題,從第一個訓練周期開始,一直到第1000次,雖然損失loss在降低,但正確率acc一直為0,這是為什么?
其實看看最后的預測結果就知道了。對於這種連續輸出值的回歸問題,結果不是有限的分類,而是很精確的浮點數。這樣的結果,只能保證大體比例上,同標注集是吻合的,不可能做到一一對應的相等。這是所有的正確率結果為0的原因,也是我們沒有跟前面的例子一樣,使用model.evaluate對模型進行評估的原因。
由此可見,我們在模型編譯的時候選取評價指標參數為'acc'(正確率)就是不合理的。替代的,我們可以使用MAE(Mean Abs Error)平均絕對誤差或者MSE(Mean Square Error)均方根誤差/標准誤差。
此外你可能看到了,程序數據集簡單、模型也簡單,所以訓練速度很快。1000次的迭代訓練,信息輸出本身占用的時間甚至多過了訓練所需的時間。
model.fit()訓練函數中可以指定verbose=0來屏蔽輸出,但完全沒有輸出也是很不友好的。我們可以使用前面用過的回調函數機制,來顯示自定義的輸出內容。比如我們可以在每個訓練循環中輸出一個“.”來顯示訓練的進展。
來改進一下程序,用以下程序段來替代上面代碼中,”構建回歸模型”之后所有的內容。

# 替代前面代碼中,構建回歸模型以下內容
# 構建回歸模型
def build_model():
    model = keras.Sequential([
        layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
        layers.Dense(64, activation='relu'),
        layers.Dense(1)    # 回歸的主要區別就是最后不需要激活函數,從而保證最后是一個連續值
    ])

    optimizer = tf.keras.optimizers.RMSprop(0.001)

    model.compile(loss='mse',
                  optimizer=optimizer,
                  metrics=['mae', 'mse'])
    return model

# 自定義一個類,實現keras的回調,在訓練過程中顯示“.”
class PrintDot(keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs):
        if epoch % 100 == 0:
            print('')
        print('.', end='')

model = build_model()
# 顯示構造的模型
model.summary()

EPOCHS = 1000
history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split=0.2, verbose=0,
  callbacks=[PrintDot()])

# 使用測試集評估模型
loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=0)
# 顯示評估結果
print("\nTesting set Mean Abs Error: {:5.2f} MPG".format(mae))

再次執行,輸出結果看起來干凈多了:

$ ./mpg2.py
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________

....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
Testing set Mean Abs Error:  1.98 MPG

最后我們也敢使用model.evaluate對模型進行評估了,得到的MAE是1.98。

但是MAE、MSE的數據,重點的是看訓練過程中的動態值,根據趨勢調整我們的程序,才談得上優化。只有最終一個值其實意義並不大。
我們繼續為程序增加功能,用圖形繪制出訓練過程的指標變化情況。前面的程序中,我們已經使用history變量保存了訓練過程的輸出信息,下面就是使用matplotlib將數值繪出。

# 接着上面的代碼,在最后添加以下內容:  
def plot_history(history):
    hist = pd.DataFrame(history.history)
    hist['epoch'] = history.epoch

    # plt.figure()
    plt.figure('MAE --- MSE', figsize=(8, 4))
    plt.subplot(1, 2, 1)
    plt.xlabel('Epoch')
    plt.ylabel('Mean Abs Error [MPG]')
    plt.plot(
        hist['epoch'], hist['mae'],
        label='Train Error')
    plt.plot(
        hist['epoch'], hist['val_mae'],
        label='Val Error')
    plt.ylim([0, 5])
    plt.legend()

    plt.subplot(1, 2, 2)
    plt.xlabel('Epoch')
    plt.ylabel('Mean Square Error [$MPG^2$]')
    plt.plot(
        hist['epoch'], hist['mse'],
        label='Train Error')
    plt.plot(
        hist['epoch'], hist['val_mse'],
        label='Val Error')
    plt.ylim([0, 20])
    plt.legend()
    plt.show()

plot_history(history)

執行程序,可以得到下圖的結果:

從圖中可以看出,雖然隨着迭代次數的增加,訓練錯誤率在降低,但大致從100次迭代之后,驗證的錯誤率就基本穩定不變了。限於樣本集數量及維度選取、模型設計等方面的原因,對這個結果的滿意度先放在一邊。這個模型在100次迭代之后就長時間無效的訓練顯然是一個可優化的點。
TensorFlow/Keras提供了EarlyStopping機制來應對這種問題,EarlyStopping也是一個回調函數,請看我們實現的代碼:

# 以下代碼添加到前面代碼的最后
# 設置EarlyStopping回調函數,監控val_loss指標
# 當該指標在10次迭代中均不變化后退出
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)
# 再次訓練模型
history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])
# 繪制本次訓練的指標曲線
plot_history(history)

執行后,這次得到的結果令人滿意了,大致在60次迭代之后,就得到了同前面1000次迭代基本相似的結果:

既然訓練完成,雖然我們使用模型預測的結果無法跟原標注一對一比較,我們可以用圖形的方式來比較一下兩組值,並做一下預測錯誤統計:

# 繼續在最后增加如下代碼
# 使用測試集數據用模型進行預測
test_predictions = model.predict(normed_test_data).flatten()

# 繪制同標注的對比圖和兩者誤差分布的直方圖
plt.figure('Prediction & TrueValues  --- Error', figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
plt.axis('equal')
plt.axis('square')
plt.xlim([0, plt.xlim()[1]])
plt.ylim([0, plt.ylim()[1]])
_ = plt.plot([-100, 100], [-100, 100])

error = test_predictions - test_labels
plt.subplot(1, 2, 2)
plt.hist(error, bins=25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")
plt.show()

程序得到結果圖如下:

左邊的圖中,如果預測結果同標注結果完全一致,藍色的點將落在主對角線上,偏離對角線則代表預測誤差。從圖中可以看出,所有的點大致是落在主對角線周邊的。這表示預測結果同標注值基本吻合。
右邊的圖是兩者之差的范圍統計結果,可以理解為左圖逆時針逆時針旋轉45度后所有點統計的直方圖,對角線就是誤差為0的位置。圖中能看出誤差基本符合正態分布,代表預測數值偏大、偏小的數量和比例基本相似,模型是可信的。
當然限於樣本數量過少的問題,模型的優化余地還是很大的。

(待續...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM