2019年2月4日 星期一

深度學習 - Functional API

在處理實際問題時,可能會用到的網絡需要多個輸入,或多個輸出,而不是單一種資料的輸入。
假設現在需要有個定價策略,藉由訓練模型能預測一雙鞋子的市場價格,我們手邊有三種資料,鞋子的照片(圖像資料),鞋子的廠牌、製造商、材質...等(文本資料),鞋子在網路上的評價(文本資料),要如何利用這三種資料預測價格,最簡單的方法就是訓練三個獨立的模型,然後再將三個模型的預測做加權平均。但這樣做不是最好的,也不是最有效率的,這樣做在執行上最簡單易懂,但在實際操作時,獨立的模型可能提取的訊息不是那麼的重要或是與其它模型間的關聯性不高。
更好的方法是使用一個可以同時查看所有輸入的模型,從而"聯合"學習一個更加精確的數據模型。

同樣的,也可能需要多輸出的情況。

在需要多輸入、多輸出、類圖模型時,簡單的Keras Sequential() 模型無法實踐,我們需要使用更加通用且靈活的 Functional API。

首先,先簡單地將layer 當作函數使用:
from keras import Input, layers
input_tensor = Input(shape=(32,)) #輸入張量
dense = layers.Dense(32, activation='relu')  #將layer當作函數
output_tensor = dense(input_tensor)  #輸出張量

練習將先前使用過的Sequential() model 轉換成functional API 看看:
#先前練習過簡單的全連接層模型
model = Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

轉換成functional API:
from keras.models import Model
from keras import Input, layers
input_tensor = Input(shape=(64,))  #輸入
x = layers.Dense(32, activation='relu')(input_tensor)  #將輸入丟進layer函數
x = layers.Dense(32, activation='relu')(x)  #將x再丟進layer函數
output_tensor = layers.Dense(10, activation='softmax')(x)  #again
API_model = Model(input_tensor, output_tensor)  #Model 將輸入&輸出張量轉成模型

這邊的 input_tensor 與 output_tensor 是要有相關的,output是由input多次經過layer函數得到的,如果input與output不相關的話會出現RuntimeRrror。

選擇優化器、損失函數、指標編譯,訓練&預測皆跟Sequential一樣:
#選擇優化器、損失函數、指標
API_model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
#丟進fit訓練模型
API_model.fit(x_train, y_train, epochs=10, batch_size=128)
#測試訓練模型能力
score = API_model.evaluate(x_test, y_test)

  • 多輸入模型functional API 練習:
    多個輸入的模型在通常情況下,模型會在某一時刻用一個可以組合多個張量的層將不同的輸入分支合併,張量組合方式可能是相加、連接等。這邊使用 Keras 的合併運算來實現,如keras.layers.add、keras.layers.concatenate 等。

    這邊練習一個回答問題的機器模型,在極簡化的回答可用一個單詞表示,可以通過對某個預定義的詞表做 softmax 得到。
    from keras.models import Model
    from keras import layers, Input
    #先定義文本的size
    text_vocabulary_size = 10000
    question_vocabulary_size = 10000
    answer_vocabulary_size = 500

    參考文本輸入的functional API:
    """文本輸入function API,這邊可以對輸入命名,name='text'
    將輸入加入Embedding layer functional API
    最後再加入LSTM,如同在處理文本序列的方法,這邊用functional API呈現"""
    text_input = Input(shape=(None,), dtype='int32', name='text')
    embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
    encoded_text = layers.LSTM(32)(embedded_text)

    問題文本輸入的functional API:
    question_input = Input(shape=(None,),dtype='int32', name='question')
    embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)
    encoded_question = layers.LSTM(16)(embedded_question)

    將文本串接起來:
    #將兩個文本利用concatenate連接起來
    concatenated = layers.concatenate([encoded_text, encoded_question],axis=-1)

    最後使用全連接層連接兩個文本,對應輸出 answer 使用 softmax 分類器:
    #最後一層使用全連接層,對應到輸出Answer
    answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)

    利用 Model 將兩個輸入文本張量和一個輸出answer張量轉成模型:
    model = Model([text_input, question_input], answer)

    選擇優化器、損失函數、指標編譯:
    model.compile(optimizer='rmsprop',loss='categorical_crossentropy', metrics=['acc'])


    因為沒有現成的文本可以用驗證模型,我們先試著用虛構的文本來訓練模型看看。
    import numpy as np
    num_samples = 1000
    max_length = 100
    #用numpy生成隨機參考文本&問題文本
    text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))
    question = np.random.randint(1, question_vocabulary_size, size=(num_samples, max_length))
    answers = np.random.randint(answer_vocabulary_size, size=(num_samples))
    #回答標籤需使用one-hot編碼
    answers = keras.utils.to_categorical(answers, answer_vocabulary_size)

    丟進fit,訓練模型:
    #方法1.丟入fit訓練,使用輸入組成的列表
    model.fit([text, question], answers, epochs=2, batch_size=128)
    
    #方法2.使用輸入組成的字典來擬合 (只有對輸入進行命名之後才能用這種方法)
    model.fit({'text': text, 'question': question}, answers,epochs=2, batch_size=128)

  • 多輸出模型functional API 練習: 
    利用相同的方法,我們練習只用一個網絡,做不同的性質預測。
    假設我們想要利用網路上匿名發文者的文章,試圖用來預測該發文者的年齡、收入、性別,也就是利用該文章文本做輸入,讓模型做三種性質的輸出預測。
    from keras.models import Model
    from keras import layers, Input
    #先定義文本的size&imcome groups
    vocabulary_size = 50000
    num_income_groups = 10

    輸入文本,使用一維卷積神經網絡:
    """文本輸入function API"""
    posts_input = Input(shape=(None,), dtype='int32', name='posts')
    embedded_posts = layers.Embedding(256, vocabulary_size)(posts_input)
    x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
    x = layers.MaxPooling1D(5)(x)
    x = layers.Conv1D(256, 5, activation='relu')(x)
    x = layers.Conv1D(256, 5, activation='relu')(x)
    x = layers.MaxPooling1D(5)(x)
    x = layers.Conv1D(256, 5, activation='relu')(x)
    x = layers.Conv1D(256, 5, activation='relu')(x)
    x = layers.GlobalMaxPooling1D()(x)
    x = layers.Dense(128, activation='relu')(x)

    輸出預測。
    """模型預測輸出,皆可命名函數"""
    age_prediction = layers.Dense(1, name='age')(x)
    income_prediction = layers.Dense(num_income_groups,activation='softmax',name='income')(x)
    gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)

    利用 Model 將輸入文本張量和三個輸出預測張量轉成模型:
    model = Model(posts_input, [age_prediction, income_prediction, gender_prediction])

    選擇優化器、損失函數、指標編譯:
    在多輸出模型時,選擇損失函數要特別注意,因為一般在不同性質的預測問題,要計算的損失函數不盡相同,在預測年齡,會用標量回歸來計算損失,而在預測性別時使用的是二分類任務,兩者需要不同的訓練過程。但是在此單一輸入模型當中,只有一個梯度下降的單一標量能被使用,因此需要將三個損失標量合併為單一標量,才能夠訓練模型。
    簡單的做法是直接將個別損失合併:
    model.compile(optimizer='rmsprop',
                  loss=['mse',
                        'categorical_crossentropy',
                        'binary_crossentropy'])
    #有命名的情況下
    model.compile(optimizer='rmsprop',
                  loss={'age': 'mse',
                        'income': 'categorical_crossentropy',
                        'gender': 'binary_crossentropy'})

    實際情況,需調整每個問題的損失值的權重:
    model.compile(optimizer='rmsprop',
                  loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],
                  loss_weights=[0.25, 1., 10.])
    #有命名的情況下
    model.compile(optimizer='rmsprop',
                  loss={'age': 'mse',
                        'income': 'categorical_crossentropy',
                        'gender': 'binary_crossentropy'},
                  loss_weights={'age': 0.25,
                                'income': 1.,
                                'gender': 10.})

    丟進fit訓練模型:
    model.fit(posts, [age_targets, income_targets, gender_targets],
              epochs=10, batch_size=64)
    #有命名的情況下
    model.fit(posts, {'age': age_targets,
                      'income': income_targets,
                      'gender': gender_targets},
              epochs=10, batch_size=64)





沒有留言:

張貼留言