1. 程式人生 > >TensorFlow從1到2(十四)評估器的使用和泰坦尼克號乘客分析

TensorFlow從1到2(十四)評估器的使用和泰坦尼克號乘客分析

線性分類 基本數據 size urn NPU dataset copyright 清洗 用戶

技術分享圖片

三種開發模式

使用TensorFlow 2.0完成機器學習一般有三種方式:

  • 使用底層邏輯
    這種方式使用Python函數自定義學習模型,把數學公式轉化為可執行的程序邏輯。接著在訓練循環中,通過tf.GradientTape()叠代,使用tape.gradient()梯度下降,使用optimizer.apply_gradients()更新模型權重,逐次逼近,完成模型訓練。
  • 使用Keras高層接口
    TensorFlow 1.x的開發中,Keras就作為第三方庫存在。2.0中,更是已經成為標準配置。我們前面大多的例子都是基於Keras或者自定義Keras模型配合底層訓練循環完成。從網上的一些開源項目來看,這已經是應用最廣泛的方式。
  • 今天要介紹的評估器tf.estimator
    評估器是TensorFlow官方推薦的內置高級API,層次上看跟Keras實際處於同樣位置,只是似乎大家都視而不見了,以至於現在從用戶的實際情況看用的人要遠遠少於Keras。

技術分享圖片
通常認為評估器因為內置的緊密結合,運行速度要高於Keras。Keras一直是一個通用的高層框架,除了支持TensorFlow作為後端,還同時支持Theano和CNTK。高度的抽象肯定會影響Keras的速度,不過本人並未實際對比測試。我覺的,對於大量數據導致的長時間訓練來說,這點效率上的差異不應當成為大問題,否則Python這種解釋型的語言就不會成為優選的機器學習基礎平臺了。

在TensorFlow 1.x中可以使用tf.estimator.model_to_estimator方法將Keras模型轉換為TensorFlow評估器。TensorFlow 2.0中,統一到了tf.keras.estimator.model_to_estimator方法。所以如果偏愛評估器的話,使用Keras也不會成為障礙。

評估器基本工作流程

其實從編程邏輯來看,這些高層API所提供的工作方式是很相似的。使用評估器開發機器學習大致分為如下步驟:

  • 載入數據
  • 數據清洗和數據預處理
  • 編寫數據流水線輸入函數
  • 定義評估器模型
  • 訓練
  • 評估

在這個流程裏面,只有“編寫數據流水線輸入函數”這一步是跟Keras模型是不同的。在Keras模型中,我們直接準備數據集,把數據集送入到模型即可。而在評估器中,數據的輸入,需要指定一個函數供評估器調用。

使用評估器的實例

這一個來自官方文檔的實例比較殘酷,使用泰坦尼克號的乘客名單,評估在沈船事件發生後,客戶能生存下來的可能性。
數據格式是csv,建議先下載,保存到工作目錄:
訓練集數據:https://storage.googleapis.com/tf-datasets/titanic/train.csv
評估集數據:https://storage.googleapis.com/tf-datasets/titanic/eval.csv
文件下載後不要修改名稱。

數據包含如下屬性維度:

屬性名稱 屬性描述
sex 乘客性別
age 乘客年齡
n_siblings_spouses 隨行兄弟或者配偶數量
parch 隨行父母或者子女數量
fare 船費金額
class 船艙等級
deck 甲板編號
embark_town 登船地點
alone 是否為獨自旅行

從這些屬性中能看出,數據的收集者是非常用心的。
比如隨行兄弟或者配偶、隨行父母或者子女這種特征,在大多人的傳統觀念中,肯定會用類似“隨行家屬數量”這樣的維度合並在一起。
但在這個案例中,兩個不同的維度,對於最終存活影響肯定是不同的。

基本數據分析

這部分的工作其實跟評估器的使用沒有什麽關系,但這正是大數據時代的魅力所在,所以我們還是延續官方文檔的思路來看一看。

先在命令行執行Python,啟動交互環境。然後把下面這部分代碼拷貝到Python執行。這些代碼完成引用擴展庫、載入數據等基本工作。

# 引入擴展庫
from __future__ import absolute_import, division, print_function, unicode_literals

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf

# 載入數據
dftrain = pd.read_csv('train.csv')
dfeval = pd.read_csv('eval.csv')
# 分離標註字段
y_train = dftrain.pop('survived')
y_eval = dfeval.pop('survived')

dftrain.head()

這時候命令行看起來大致是這個樣子:

$ 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 __future__ import absolute_import, division, print_function, unicode_literals
>>> 
>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> import tensorflow as tf
>>> 
>>> # 載入數據
... dftrain = pd.read_csv('train.csv')
>>> dfeval = pd.read_csv('eval.csv')
>>> # 分離標註字段
... y_train = dftrain.pop('survived')
>>> y_eval = dfeval.pop('survived')
>>> 
>>> dftrain.head()
      sex   age  n_siblings_spouses  parch     fare  class     deck  embark_town alone
0    male  22.0                   1      0   7.2500  Third  unknown  Southampton     n
1  female  38.0                   1      0  71.2833  First        C    Cherbourg     n
2  female  26.0                   0      0   7.9250  Third  unknown  Southampton     y
3  female  35.0                   1      0  53.1000  First        C  Southampton     n
4    male  28.0                   0      0   8.4583  Third  unknown   Queenstown     y
>>> 

最後是列出的訓練集頭5條記錄。
我們先看看乘客的年齡分布(後續的代碼都是直接拷貝到Python命令行執行):

dftrain.age.hist(bins=20)
plt.show()

技術分享圖片
直方圖中顯示,乘客年齡主要分布在20歲至30歲之間。
再來看看性別分布:

dftrain.sex.value_counts().plot(kind='barh')
plt.show()

技術分享圖片
男性乘客的數量,幾乎是女性乘客的兩倍。
接著是船艙等級的分布,這個參數能間接體現乘客的經濟實力:

dftrain['class'].value_counts().plot(kind='barh')
plt.show()

技術分享圖片
圖中顯示,大多數乘客還是在三等艙。
繼續看乘客上船的地點:

dftrain['embark_town'].value_counts().plot(kind='barh')
plt.show()

技術分享圖片
大多數乘客來自南安普頓。
繼續,把性別跟最後生存標註關聯起來:

pd.concat([dftrain, y_train], axis=1).groupby('sex').survived.mean().plot(kind='barh').set_xlabel('% survive')
plt.show()

技術分享圖片
女性的存活率幾乎超過男性的5倍。
再來一個更復雜的統計,我們首先把年齡分段,然後看看不同年齡段的乘客最終存活率:

def calc_age_section(n, lim):
    return'[%.f,%.f)' % (lim*(n//lim), lim*(n//lim)+lim)  # map function

addone = pd.Series([calc_age_section(s, 10) for s in dftrain.age])
dftrain['ages'] = addone
pd.concat([dftrain, y_train], axis=1).groupby('ages').survived.mean().plot(kind='barh').set_xlabel('% survive');
plt.show()

技術分享圖片
10歲以下兒童和80歲以上的老人得到了最多的生存機會。
在那個寒冷、慌亂的沈船夜晚,弱者反而更多的活了下來。

數據的預處理

數據預處理這個話題我們講了很多次,這是通常機器學習研發工作中,工程師需要做的最多工作。
泰坦尼克號乘客名單的數據雖然不復雜,也屬於典型的結構化數據。
其中主要包含兩類,一種是分類型的數據,比如船艙等級,比如上船城市名稱。另一類則是簡單的數值,比如年齡和購票價格。
對於數值型的數據可以直接規範化後進入模型,對於分類型的數據,則還需要做編碼,我們這裏還是使用最常見的one-hot。

# 定義所需的數據列,分為分類型屬性和數值型屬性分別定義
CATEGORICAL_COLUMNS = ['sex', 'n_siblings_spouses', 'parch', 'class', 'deck', 
                       'embark_town', 'alone']
NUMERIC_COLUMNS = ['age', 'fare']

# 輔助函數,把給定數據列做one-hot編碼
def one_hot_cat_column(feature_name, vocab):
    return tf.feature_column.indicator_column(
        tf.feature_column.categorical_column_with_vocabulary_list(feature_name,
                                                                  vocab))

# 最終使用的數據列,先置空
feature_columns = []
for feature_name in CATEGORICAL_COLUMNS:
    # 分類的屬性都要做one-hot編碼,然後加入數據列
    vocabulary = dftrain[feature_name].unique()
    feature_columns.append(one_hot_cat_column(feature_name, vocabulary))

for feature_name in NUMERIC_COLUMNS:
    # 數值類的屬性直接入列
    feature_columns.append(tf.feature_column.numeric_column(feature_name,
                                                            dtype=tf.float32))

數據輸入函數

評估器的訓練、評估都需要使用數據輸入函數作為參數。輸入函數本身不接受任何參數,返回一個tf.data.Dataset對象給模型用於供給數據。
因為除了數據集不同,訓練和評估模型所使用的數據格式通常都是一樣的。所以經常會在程序代碼上,共用一個函數,然後用參數來區分用於評估還是用於訓練。
然而輸入函數相當於回調函數,由評估器控制著調用,這過程中並沒有參數傳遞。所以比較聰明的做法可以使用嵌套函數的方法來定義,比如:

# 這是一個很少量數據的樣本,直接把整個數據集當做一批
NUM_EXAMPLES = len(y_train)
# 輸入函數的構造函數
def make_input_fn(X, y, n_epochs=None, shuffle=True):
    def input_fn():
        dataset = tf.data.Dataset.from_tensor_slices((dict(X), y))
        # 亂序
        if shuffle:
            dataset = dataset.shuffle(NUM_EXAMPLES)
        # 訓練時讓數據重復盡量多的次數
        dataset = dataset.repeat(n_epochs)
        dataset = dataset.batch(NUM_EXAMPLES)
        return dataset
    return input_fn

# 訓練、評估所使用的數據輸入函數,區別只是數據是否亂序以及叠代多少次
train_input_fn = make_input_fn(dftrain, y_train)
eval_input_fn = make_input_fn(dfeval, y_eval, shuffle=False, n_epochs=1)

模型和源碼

本例中我們直接使用預定義的評估器模型(pre-made estimator)。所以代碼非常簡單,定義、訓練、評估都是只需要一行代碼:

# 使用線性分類器作為模型
linear_est = tf.estimator.LinearClassifier(feature_columns)

# 訓練
linear_est.train(train_input_fn, max_steps=100)

# 評估
result = linear_est.evaluate(eval_input_fn)

我們來看看完整代碼:

#!/usr/bin/env python3

# 引入擴展庫
from __future__ import absolute_import, division, print_function, unicode_literals

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf

# 載入數據
dftrain = pd.read_csv('train.csv')
dfeval = pd.read_csv('eval.csv')
# 分離標註字段
y_train = dftrain.pop('survived')
y_eval = dfeval.pop('survived')

################################################################
# 定義所需的數據列,分為分類型屬性和數值型屬性分別定義
CATEGORICAL_COLUMNS = ['sex', 'n_siblings_spouses', 'parch', 'class', 'deck', 
                       'embark_town', 'alone']
NUMERIC_COLUMNS = ['age', 'fare']

# 輔助函數,把給定數據列做one-hot編碼
def one_hot_cat_column(feature_name, vocab):
    return tf.feature_column.indicator_column(
        tf.feature_column.categorical_column_with_vocabulary_list(feature_name,
                                                                  vocab))

# 最終使用的數據列,先置空
feature_columns = []
for feature_name in CATEGORICAL_COLUMNS:
    # 分類的屬性都要做one-hot編碼,然後加入數據列
    vocabulary = dftrain[feature_name].unique()
    feature_columns.append(one_hot_cat_column(feature_name, vocabulary))

for feature_name in NUMERIC_COLUMNS:
    # 數值類的屬性直接入列
    feature_columns.append(tf.feature_column.numeric_column(feature_name,
                                                            dtype=tf.float32))

################################################################
# 這是一個很少量數據的樣本,直接把整個數據集當做一批
NUM_EXAMPLES = len(y_train)
# 輸入函數的構造函數
def make_input_fn(X, y, n_epochs=None, shuffle=True):
    def input_fn():
        dataset = tf.data.Dataset.from_tensor_slices((dict(X), y))
        # 亂序
        if shuffle:
            dataset = dataset.shuffle(NUM_EXAMPLES)
        # 訓練時讓數據重復盡量多的次數
        dataset = dataset.repeat(n_epochs)
        dataset = dataset.batch(NUM_EXAMPLES)
        return dataset
    return input_fn

# 訓練、評估所使用的數據輸入函數,區別只是數據是否亂序以及叠代多少次
train_input_fn = make_input_fn(dftrain, y_train)
eval_input_fn = make_input_fn(dfeval, y_eval, shuffle=False, n_epochs=1)

# 使用線性分類器作為模型
linear_est = tf.estimator.LinearClassifier(feature_columns)

# 訓練
linear_est.train(train_input_fn, max_steps=100)

# 評估
result = linear_est.evaluate(eval_input_fn)
print("----------------------------------")
print(pd.Series(result))

程序執行的最後顯示了評估的結果,在我的電腦上顯示的結果是這樣的:

----------------------------------
accuracy                  0.765152
accuracy_baseline         0.625000
auc                       0.832844
auc_precision_recall      0.789631
average_loss              0.478908
label/mean                0.375000
loss                      0.478908
precision                 0.703297
prediction/mean           0.350790
recall                    0.646465
global_step             100.000000

正確率不算太高。
評估器的模型使用起來很簡單,我們嘗試換用另外一種模型,比如提升樹分類器。

# 以下代碼放在程序最後,因為這個數據集非常小,速度很快,所以做兩次學習也並不感覺慢
n_batches = 1
est = tf.estimator.BoostedTreesClassifier(feature_columns,
                                          n_batches_per_layer=n_batches)

# 訓練
est.train(train_input_fn, max_steps=100)

# 評估
result = est.evaluate(eval_input_fn)
print("----------------------------------")
print(pd.Series(result))

這次得到的結果是這樣的:

----------------------------------
accuracy                  0.825758
accuracy_baseline         0.625000
auc                       0.872360
auc_precision_recall      0.857325
average_loss              0.411853
label/mean                0.375000
loss                      0.411853
precision                 0.784946
prediction/mean           0.382282
recall                    0.737374
global_step             100.000000

雖然準確率仍然並不高,但比起來線性分類器,提高還是算的上明顯。

性能評價

評價機器學習模型的性能,除了看剛才的統計信息,繪圖是非常好的一種方式,可以更直觀,某些問題也能體現的一目了然。
我們在上面程序的最後再增加幾行代碼,繪制預測概率的統計信息:

# 繪制預測概率直方圖
pred_dicts1 = list(linear_est.predict(eval_input_fn))
pred_dicts2 = list(bt_est.predict(eval_input_fn))
probs1 = pd.Series([pred['probabilities'][1] for pred in pred_dicts1])
probs2 = pd.Series([pred['probabilities'][1] for pred in pred_dicts2])

plt.figure(figsize=(14, 5))
plt.subplot(1, 2, 1)
probs1.plot(kind='hist', bins=20, title='linear-est predicted probabilities');
plt.subplot(1, 2, 2)
probs2.plot(kind='hist', bins=20, title='bt-est predicted probabilities');
plt.show()

技術分享圖片
大量集中在圖形左側的數據簇,顯示了乘客九死一生的悲慘命運。
因為我們的預測結果只有兩種可能:0表示未能生存;1表示生存下來。所以預測的結果,應當明顯的盡量靠近0和1兩端。中間懸而未決的部分應當盡可能少。從圖形的情況看,如果不考慮分類準確率問題,提升樹分類器效果要更好一些。
當然作為成熟的預定義模型,模型都是很優秀的,只是提升樹可能更適合本應用的場景。

盡管這個例子很簡單,但現在的分類算法實際越來越復雜。預測結果在不同類別數據上表現並不不均衡,使得使用正確率這樣的傳統標準不能恰當的反應分類器的性能,本例中也已經出現了這種傾向。或者說,分類器,對於不同類別的樣本,性能表現是不一致的。
這種情況,使用ROC(Receiver Operating Characteristic)觀察者操作曲線能夠表現的更清楚。
對於一個分類器的分類結果,一般有以下四種情況:

  1. 真陽性(TP):判斷為1,實際上也為1。
  2. 偽陽性(FP):判斷為1,實際上為0。
  3. 真陰性(TN):判斷為0,實際上也為0。
  4. 偽陰性(FN):判斷為0,實際上為1。

ROC圖中,左上角是真陽性的極點,曲線越接近左上角,意味著分類器性能越好。所以左上角是分類器追求的方向。
下面代碼,請接續在上面代碼之後,用來繪制ROC曲線:

# 繪制ROC(Receiver Operating Characteristic)曲線
from sklearn.metrics import roc_curve

def plot_roc(probs, title):
    fpr, tpr, _ = roc_curve(y_eval, probs)
    plt.plot(fpr, tpr)
    plt.title(title)
    plt.xlabel('false positive rate')
    plt.ylabel('true positive rate')
    plt.xlim(0,)
    plt.ylim(0,)
plt.figure(figsize=(14, 5))
plt.subplot(1, 2, 1)
plot_roc(probs1, "linear-est ROC")
plt.subplot(1, 2, 2)
plot_roc(probs2, "bt-est ROC")
plt.show()

技術分享圖片
從ROC曲線看,在本例中使用提升樹模型的優勢更為明顯。

(待續...)

TensorFlow從1到2(十四)評估器的使用和泰坦尼克號乘客分析