iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 2
1
AI & Data

30 天學會深度學習和 Tensorflow系列 第 2

01. 使用邏輯迴歸預測誰能在鐵達尼船難中存活?

  • 分享至 

  • xImage
  •  

談完了類神經網路的源起,我們還得提到另外一個線性模型分類器,才能真正的開始進入深度學習的領域中 。這個線性模型分類器,就是邏輯迴歸(Logistic Regression)。

雖然 Logistic Regression 是一個線性模型,但是這個線性的關係是建立在透過 log 函式對 predicting variables 做轉換上。在這個線性關係上,原本應該是連續數值的 response variable 則為兩分類預測機率的 log-odds ratio,而在線性迴歸上使用的 mean squared error 因為分類標籤的離散性和使用 log 函式, cross-entropy 是相較於 mean squared error 更佳的選擇。因為這種近似於線性回歸的特性,對 mean squared error 損失函式做最佳化的演算法都能使用在 Logistic Regression 上。

從另外一個觀點來看,Logistic Regression 可以看做一個沒有 hidden layer 的類神經網路,其輸出則是藉由非線性的 sigmoid function 來限制在 0 與 1 之間,以符合機率值的數值域。在過去類神經網路,廣泛使用 sigmoid function 作為 activation function,架構深度達到兩三層就能算是深度網路的時代,都會遭遇到 所謂 gradient vanishing 或 gradient explosion 問題。這是因為 sigmoid function 本身的特性,會將輸入位於兩端的極值,壓縮到相當相當靠近於 0 或 1 的值,而造成沒有 gradient 或極小 gradient 的結果。這類具有極限值的函式特性,在英文文獻中通常被稱為 “saturate”,中文則用“飽和”這個翻譯,可捕捉到因為達到極限值而無法繼續訓練的性質。

sigmoid plot
圖一:左方是一個 sigmoid function,右方則是 sigmoid function 的數學式子。可以看到,sigmoid function 在靠近 0 的地方(輸出為 0.5)近似於線性,然而在兩側,遠離 0 的數值,輸出皆為平緩的直線,並近似於 0 或 1。圖右方上方數學式是 sigmoid function 的方程式,而下方的方程式則是以 log odds-ratio 的方式來建立與原輸入建立起線性關係。

雖然 sigmoid function 帶給類神經網路的訓練,莫大的阻礙,但是對於將本來無界限的輸入值應對到 0 和 1 區間內,卻是相當的稱職。在今天的文章中,我們將要藉由 Logistic Regression 來一探 sigmoid function 的能耐,以及來和此系列的另外一個主角,Tensorflow,來打個程式設計師式 “Hello Tensorflow” 的招呼吧!

至於作為 “Hello Tensorflow” 的訓練實例,則是使用在知名的資料競賽網站,Kaggle 的入門競賽「鐵達尼存活」預測問題。在此篇中,將不會使用較受歡迎的 Keras 函式庫,而是使用 Tensorflow 為了針對巨量資料處理而推出的 estimator 和 data API

在這之前,你若對深度學習的架構軟體不熟悉的話,可以參見筆者針對一般讀者所寫的現行深度學習架構概況 做一個基礎入門了解。

現在,就打開你的 python 直譯器來 import Tensorflow package,以及啟動 Tensorflow 的 eager mode 吧!

import tensorflow as tf
tf.enable_eager_execution()
tf.__version__
# output '1.11.0'

首先,我們需要定義 input function。這個 input function 會對 feature 做一些使用者自訂的資料前處理以及如何在訓練時餵送資料。這個 input function,需要回傳以下任一物件:一個 python tuple 或定義在Tensorflow data 模組下的 Dataset 物件。

如果你的 input function 回傳的是一個 python tuple,則 tuple 裡的第一個元素是一個 dict,其 key 值為 feature 的名字, value 則是該 feature 的值。第二個元素,則是分類標籤值。 如果是回傳 Dataset 物件,Dataset 的方法會代勞回傳上述 python tuple 的這個部分。

因為這才是我們的第一個 tensorflow 模型,所以我們的程式碼當然是愈簡單愈好。在以下的 input_fn 範例,就是回傳一個 TextLineDataset 物件。使用 Dataset 的好處,就是這個類別封裝了資料與抽象 Tensor 符號之間對應的程式邏輯。Tensor 抽象符號在 Tensorflow 架構中,使用的靜態計算圖資料結構裡的功用,是作為運算元的輸入及輸出資料預先存放空間(placeholder),即是先對資料做記憶體配置的處理。也可將這些抽象 Tensor 符號視為連結計算圖間不同運算元之間的資料傳遞。

根據 Kaggle 所提供的鐵達尼訓練資料,我們可以看到使用 pandas 函式庫中的模組函式,read_csv function 解析完且存入 DataFrame 中的資料預視,和解析的資料型別如下圖:

data preview

可以從上圖的資料預視看到,總共有 11 行,而第一行的 'Survived' 則是作為分類訓練依據的真實分類標籤,其他的行別都可當作 feature 使用。在這裡,我們只取用 'Age', 'Pclass', 'Parch', 'SibSp', 'Sex', 'Fare' 這六個欄位來執行。

FEATURES = ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare']
data_dir = '../input'
train_data = pd.read_csv(os.path.join(data_dir, 'train.csv'), header=0, index_col=0)
# handling missing value in Age
train_data['Age'] = train_data['Age'].fillna(0).astype(np.int32)
# only output the 
filepath = './train_simple.csv'
# remove PassengerID
train_data[FEATURES].to_csv(filepath, index=False)

dataset = tf.data.TextLineDataset(filiepath)

在這段程式碼裡,為了能夠讓 tf.decode_csv 順利解析內容,特別先處理了 missing value 的部分,同時也將需要的 feature 另外寫成一個 csv 檔案來供 TextLineDataset 使用。

要建構一個 TextLineDataset 非常簡單,就只要輸入檔案的路徑位置。這裏,我們輸入 Kaggle 為比賽提供的訓練資料,通常是 csv 格式。其檔案路徑在 Kaggle 的 kernel 上,通常在程式碼上層 input 檔案夾下的這個位置。但,建立一個 TextLineDataset 並不是故事的全部,我們還需要一個能夠對檔案做逐行解析的程式。

在這裡,我們定義了 parse_csv function,這個 function 只接受一個參數,那就是 python 的 str 物件,或一個 UTF-8 編碼的 line buffer。程式邏輯內,就需要定義解析完後的哪一個元素是分類標籤,哪些元素是作為 feature 使用,因為這個 parse function 需要回傳一個剛才所提及的 python tuple。

同時,要注意的是 tf.decode_csv 需要提供一個欄位預設值,因為此 function 會根據 default 值,將解析出來的欄位值轉換成與預設值相同的資料型態。

COLUMNS_DEFALT = [[-1], [-1], [''], [-1], [-1], [-1], [0.]]
COLUMNS_NAMES = FEATURES
def parse_csv(line):
    columns = tf.decode_csv(line, record_defaults=COLUMNS_DEFALT, 
                            use_quote_delim=False, na_value='')
    features = dict(zip(COLUMNS_NAMES , columns))
    labels = features.pop('Survived')
    return (features, labels)

我們可以看到從下面的 code 看到 input_fn 的所有程式碼內容。同時,我們可以呼叫 input_fn,可以看到所回傳的 Dataset 物件,其實並未開始對檔案進行解析,而僅僅都是 tensor 物件!使用 dataset.take 函式,來讓這個 input_fn 真正讀取資料,並執行 parse_csv 內的程式邏輯。而開啟 tensorflow 的 eager mode 後,則可以讓我們藉由 numpy 的屬性,看到目前 tensor 所得到所對應的值。

def input_fn(filepath):
    dataset = tf.data.TextLineDataset(filepath)
    dataset = dataset.skip(1) # skip header
    dataset = dataset.map(parse_csv)
    return dataset

dataset = input_fn(filepath)

for feature, label in dataset.take(1):
    for key in feature:
        print("feature Name=", key, "Tensor Type=", feature[key], "value=", feature[key].numpy())
    print("label:", label, "value=", label.numpy())

輸出如下:
feature Name= Pclass Tensor Type= tf.Tensor([3], shape=(1,), dtype=int32) value= [3]
feature Name= Sex Tensor Type= tf.Tensor([b'male'], shape=(1,), dtype=string) value= [b'male']
feature Name= Age Tensor Type= tf.Tensor([22], shape=(1,), dtype=int32) value= [22]
feature Name= SibSp Tensor Type= tf.Tensor([1], shape=(1,), dtype=int32) value= [1]
feature Name= Parch Tensor Type= tf.Tensor([0], shape=(1,), dtype=int32) value= [0]
feature Name= Fare Tensor Type= tf.Tensor([7.25], shape=(1,), dtype=float32) value= [7.25]
label: tf.Tensor([0], shape=(1,), dtype=int32) value= [0]

最後,則是關於 feature 的處理,在這部分我們需要定義 feature_column。 feature_column 的定義,有助於模型對其資料型態做相符合的對應,並為其建立與定義相符合的 tensor。要注意的是,這裡所定義的 feature_column 是模型所看到的輸入,在 tensorflow 的程式碼中,會對 input function 所讀入的 tensor type 和使用者透過 feature_column 定義的 tensor type 做比對。所以,feature_column 的 key 引數名稱,必須要能在 input function 傳回的 feature dict 中找到對應的值。若不符合定義,或無法找到該 key 值,則會發出失敗訊息。

在下段程式碼中,針對 feature 是屬於具有連續數值的 numerical feature 或是離散數值的 categorial feature 做處理。此外,需要注意的是,在處理 categorical featue 經常使用 one-hot encoding,因此是 sparse matrix,必須用 tf.feature_column.indicator_column 從 sparse array 轉換成 dense,也就是使每一個 categorical feature 內的 category 是一個獨立 variable。

# numerical features
feature_columns = []
feature_columns.append(
    tf.feature_column.numeric_column('Fare', normalizer_fn=lambda x: x*normalizer.scale_[0]))
feature_columns.append(
    tf.feature_column.numeric_column('Age', normalizer_fn=lambda x: x*normalizer.scale_[1]))

# categorical features
feature_columns.append(tf.feature_column.indicator_column(
    tf.feature_column.categorical_column_with_identity('Pclass', num_buckets=4)))
feature_columns.append(tf.feature_column.bucketized_column(
    source_column=tf.feature_column.numeric_column('SibSp'),
    boundaries=[1, 2, 4]))
feature_columns.append(tf.feature_column.bucketized_column(
    source_column=tf.feature_column.numeric_column('Parch'), 
    boundaries=[1, 2, 3]))
feature_columns.append(tf.feature_column.indicator_column(
    tf.feature_column.categorical_column_with_vocabulary_list(
    'Sex', vocabulary_list=['female', 'male'])))

print("final feature = ", tf.feature_column.input_layer(feature, feature_columns).numpy())

輸出如下,為一個 1 x 16 型別為 tf.float32 的矩陣:
final feature =
[[0. 0.01415106 1. 0. 0. 0.
0. 0. 0. 1. 0. 1.
0. 1. 0. 0. ]]

終於,有了 feature_column 的 list 和 input function,我們就可以來使用 Tensorflow estiamtor API 下現有的 LinearClassifier 來做訓練了。

from functools import partial
classifier = tf.estimator.LinearClassifier(
        feature_columns=feature_columns
        )
classifier.train(input_fn=partial(input_fn, filepath), max_steps=5)

到這裡,我們就完成了第一個用 Tensorflow 建立的線性分類器了!在明天,我們將會開始訓練分類器,看看誰能在鐵達尼船難中存活呢?是 Jack 還是 Rose?或是沒有人在乎的無辜北極熊?!

圖片來源:

圖一:Machine Learning Crash Course,Logistic Regression Unit


上一篇
00. 深度學習的緣起:單一神經元分類器
下一篇
02. 鐵達尼預測內幕:資料和特徵處理
系列文
30 天學會深度學習和 Tensorflow30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言