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

線性迴歸模型

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

瞭解樣本數據

在機器學習算法自己沒有大的突破的狀況下,對樣本數據的選取、預處理每每是項目成功的關鍵。咱們接續前一篇繼續說一說樣本數據。
樣本數據的預處理依靠對樣本數據的瞭解和分析。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編碼方式:bash

>>> # 取出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)能夠用於粗略揭示數據中,不一樣列之間的相關性。能夠粗略估計哪些變量是正相關的,哪些是負相關的,進而爲下一步數據分析提供決策。固然這些圖須要行業專家的理解和分析。而後爲程序人員提供間接幫助。dom

數據規範化

從剛纔的樣本數據中,咱們能夠看出各列的數據,取值範圍仍是很不均衡的。在進入模型以前,咱們須要作數據規範化。也就是將全部列的數據統一爲在同一個取值範圍的浮點數。
咱們能夠利用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的位置。圖中能看出偏差基本符合正態分佈,表明預測數值偏大、偏小的數量和比例基本類似,模型是可信的。
固然限於樣本數量過少的問題,模型的優化餘地仍是很大的。

(待續...)

相關文章
相關標籤/搜索