# 期末紙本報告 ###### tags: `報告` - Team: SpiderCatNotKnow - Member: <!-- ICAgIC0gMTA5NTI2MDExIOW9reW9pemclgogICAgLSAxMTA1MjIxMTAg5L2V5ZCN5pucCiAgICAtIDExMDUyMjE1NyDlvLXlnqPmuq8KICAgIC0gMTA5NTIyMTE4IOiRieWzu+e+sg== --> ## Outline - [Data Pre-processing](#Data-Pre-processing) - [Model](#Model) - [Model Training](#Model-Training) - [Conclusion](#Conclusion) ## Data Pre-processing ### Missing Value 此次的資料集存在一些缺失值問題需要處理。 缺失值大致上可以分成兩種, 1) **Timestamp 的缺失**,Timestamp 之間彼此間隔應該為 60,意思是每 1 分鐘有一筆列數值。 但實際上出現了一些 Timestamp 間隔大於 60 的狀況。 因此我們會用重新填補行的方式填補缺失的 Timestamp。 2) **Column Value 的缺失**,意思是某個欄位中存在缺失值或出現 Nan。 此類的缺失值,我們運用各種方法來填補。 我們填補這兩類缺失值的方法,有以下 6 種, - interpolate: 用內插法填值 - zero: 所有 Nan 值將都填 0 - fbfill: 用填前後值填補 (先用前值再用後值) - bffill: 用填後前值填補 (先用後值再用前值) - mean: 用平均值填補 - median: 用中位數填補 ```python= if method == 'fbfill': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method='pad') crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].fillna(method='ffill').fillna(method='bfill') elif method == 'bffill': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method='backfill') crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].fillna(method='bfill').fillna(method='ffill') elif method == 'zero': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method='pad') crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].fillna(0) elif method == 'interpolate': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method='nearest') crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].interpolate() elif method == 'mean': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method=None) mean_method = lambda col: col.fillna(col.mean()) crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].apply(lambda col: mean_method(col)) elif method == 'median': crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].reindex(range(min_start_time, max_finish_time+60,60), method=None) median_method = lambda col: col.fillna(col.median()) crypto_min[asset_symbols_map[i]] = crypto_min[asset_symbols_map[i]].apply(lambda col: median_method(col)) ``` <!-- 在此我們做實驗來找出最好的填補方法 (**這個寫在後面實驗,而不是這邊**) | 方法名稱 | 模型 Score | | ----------- | --------- | | interpolate | 0.348 | | zero | 0.2515 | | fbfill | 0.4001 | | bffill | 0.1509 | | mean | 0.4157 | | median | 0.3295 | 檢查上述的結果可以發現 **用中位數填補** 填補方法是最好的,而我們後續的任務也將使用這個方法。 --> ### Generate More Features 為了讓模型可以捕捉到更多的資訊,我們也嘗試生成更多的特徵來反應出更為細節的關係。 我們根據一些金融指標設計了以下的特徵, - spread : $High - Low$ - mean_trade : $Volume / Count$ - log_price_change : $\log(Close-Open)$ - upper_Shadow : $High - max(Close,Open)$ - lower_Shadow : $min(Close,Open) - Low$ - high_div_low : $High / Low$ - trade : $Close - Open$ ```python= df_feat['spread'] = df_feat['High'] - df_feat['Low'] df_feat['mean_trade'] = df_feat['Volume']/df_feat['Count'] df_feat['log_price_change'] = np.log(df_feat['Close']/df_feat['Open']) df_feat['upper_Shadow'] = upper_shadow(df_feat) df_feat['lower_Shadow'] = lower_shadow(df_feat) df_feat["high_div_low"] = df_feat["High"] / df_feat["Low"] df_feat['trade'] = df_feat['Close'] - df_feat['Open'] df_feat['Target'] = df['Target'] ``` ### Select the Features 我們模型由於無法在 Kaggle 平台上取得高分,因此我們覺得應該使用更多的特徵以利於模型的學習。 因此使用原先的特徵之外也納入額外生成的這些特徵。 所以模型將會運用的特徵有: Count、Open、High、Low、Close 、Volume、VWAP、spread、mean_trade、log_price_change、upper_Shadow、lower_Shadow、high_div_low、trade、Target。 共 15 個特徵。 而在後續的實驗中確實有發現這些特徵能為模型帶來更多的訊息,使得我們在 Kaggle 上的成績確實有所提升。 ### Generate Training Data 為了讓模型能學習更好的結果 ,原先模型的輸出 $y$ 應該只會有 `Target` 這個特徵,而我們將其改成輸出所有的特徵。也就是輸出維度從 1 調成 15 (也就是上一部分所提的特徵)。 在此我們也有比較輸出 $y$ 使用 **所有特徵** 與 **單一個特徵(Target)** 之間哪個比較好。 | 輸出 $y$ | Kaggle Score (越高越好) | | -------------- | -----------------------:| | 使用單一個特徵 | -0.0082 | | 使用所有特徵 | 0.1131 | 可以很明顯發現 **所有特徵** 的使用確實能提升模型的效能。 而輸入 $x$ 的部分則是使用了除去 `Target` 之外的特徵,共 14 特徵(維度)。 ### Generate Time Series Data 由於我們會使用 LSTM 模型,因此需設計時間序列格式的資料。 我們在此自己寫了一個生成器, ```python= def time_series(x,y,n_steps, type_='m2o'): # 製作時間序列 batch_size = x.shape[0]-n_steps+1 x_dim = x.shape[1] y_dim = y.shape[1] if type_ == 'm2o': x_ = np.zeros((batch_size,n_steps,x_dim)) y_ = np.zeros((batch_size,y_dim)) for j in range(batch_size): x_[j] = x[j:j+n_steps,:] y_[j] = y[j+n_steps-1,:] elif type_ == 'm2m': x_ = np.zeros((batch_size,n_steps,x_dim)) y_ = np.zeros((batch_size,n_steps,y_dim)) for j in range(batch_size): x_[j] = x[j:j+n_steps,:] y_[j] = y[j:j+n_steps,:] return x_, y_, x_dim, y_dim ``` 此生成器可以應對我們的模型,生成 Seq to Seq 或是 Sep to Vector 的格式。 以利於我們比較 Seq to Sep 與 Sep to Vector 的差異。 而參數 `n_steps` 就是 time series step 的大小,此變數需視記憶體大小來使用,設置過大很容易造成記憶體不足。 ## Model ### Model Type 由於模型需要預測 14 種加密貨幣的結果,因此我們設計了兩種類型的模型。 - **Single-model**,單一模型,其一次學習與預測 14 種加密貨幣的結果。 這種模型設計上相對直觀,不須將不同的加密貨幣分開來處理,因此它假設每個加密貨幣之間有某種關係在。在此先比較它的優缺點, - 優點: 1. 直觀,設計上簡單。 2. 模型訓練較快,預測也是。 3. 相較節省記憶體。 4. 若各種貨幣間有相關性可學習到 - 缺點: 1. 忽略各加密貨幣之間可能有不能共存的關係。 - **Multi-model**,多模型,對於 14 種加密貨幣各設計一個模型來學習與預測,因此它假設各個加密貨幣之間彼此都是獨立不相干。在此先比較它的優缺點, - 優點: 1. 假設加密貨幣之間彼此都是獨立不相干,模型能分開訓練 2. 能更專注於學習單一貨幣的特性 - 缺點: 1. 設計上麻煩不好維護。 2. 模型訓練多了至少十倍的時間,預測也是 (因為有 14 個模型需訓練與預測)。 3. 忽略貨幣之間潛在的相關性 若僅追求預測的正確率而不考慮執行時間與記憶體限制,Multi-model 會是比較好的一個選擇。 ### Output Type 相對於 Model Type 是模型的種類,而 Output Type 主要是說明輸出 $y$ 的種類,在此分為 Seq to Seq 與 Seq to Vector。 - **Seq to Seq** 也稱為 many to many,此方法可以使 RNN 類模型參考過往的結果,讓預測的結果更好。由於需要保存過往的紀錄,所以代價將會是高額的記憶體空間。 - **Seq to Vector** 也稱為 many to one,此方法設計上相對直觀,也較省記憶體空間。預測的結果可能相對於 Seq to Seq 差一些。 後續我們會比較者兩種 Type 的性能差異。 先前的 time step 會造成記憶體問題,所以無法設置過大。 而此處也有相同的問題。 同樣的記憶體容量下,**Seq to Vector** 可以放入更長的時間資料,而 **Seq to Seq** 則相對較少。因此需視任務需求取得一個 trade-off。 ### Model Architecture 下方的 pseudocode 可以用來表示出我們怎麼建構模型, ```python= model = keras.models.Sequential() # Input Layer model.add(keras.layers.LSTM(n_width, input_shape=[None, x_dim]))) # Hidden Layer for i in range(n_hidddn): model.add(keras.layers.LSTM(n_width)) model.add(keras.layers.Dropout(dropout_rate)) # Output Layer model.add(keras.layers.Dense(y_dim)) ``` 先說明下程式碼內的一些變數, - **x_dim**: 為輸入維度;**y_dim**: 為輸出維度 - **n_width**: 為該層的神經元數目。 - **n_hidddn**: 為 LSTM 隱藏層的數目。 實際的 LSTM 層數要包含當作輸入的 LSTM 層。 - **dropout_rate**: 為 Dropout 率。 上方的每個 LSTM 層都具有相同的 **n_width** 神經元數,且 LSTM 隱藏層的數目會根據 **n_hidddn** 來做改變,此外每個隱藏層之後都會跟隨著一個 Dropout 層。 這樣設計能讓我們使用類似 Grid Search 的方式,來找出哪些超參數是有利於我們的任務。 (上述程式碼,沒有表現出 seq2seq 或 seq2vec 的寫法,詳細寫法在 Code 中) ## Model Training ### Hyperparameter selection 先前我們設計出一個可以透過 Grid Search 的方式來找出超參數的模型,因此可以開始來找尋哪些超參數有助於提升效能了。 我們可以調的超參數有(為了避免無止境的測試,我們先假設這些數值), - Times step (n_steps): `5、10、15` - 每層神經元數目 (n_width):`32、128、512、1024` - LSTM 隱藏層數 (n_hidddn): `1、2、3` ### Training setting - 使用 2021.08.01 到 2021.09.21 之間所有加密貨幣的數據。 - 設置訓練集尾端的 0.1% 資料當作驗證集。 - 使用 Mean Square Error 計算 Loss,優化器則使用 Adam。 - 所有 Dropout 統一設定成 0.2。 - 使用 EarlyStop 觀測 Validation Loss,若無持續下降 2 次則終止訓練。 ### Experiment process 我們的實驗將依序分為以下流程: 1. 比較哪個 **缺失值添補方法** 較好 2. 比較哪個 **類型的模型(Model Type)** 較好 3. 比較哪個 **輸出類型(Ouput Type)** 較好 4. 透過以上的結果,找出哪些 **超參數** 較好 5. 統合以上結果上傳至 Kaggle 比較出成績 ### Scoring Model Method 在進入實驗結果前,先介紹我們評估模型好壞的方法。在此我們使用 Kaggle 上有人撰寫好的 [Local API Emulator](https://www.kaggle.com/jagofc/local-api-emulator),它模擬了官方 API 一樣的功能外,還能幫我們計算出 Score (越高表示越好),而這個 Score 分數計算的方式也與公開排行榜上的方法一樣,因此我們能使用來判斷模型或結果的好壞。 ### Experiment Result #### 1. **缺失值添補方法** : 根據結果,我們發現 **Mean** 方法有較好的分數 | 方法名稱 | 模型 Score | 模型 Loss | | ----------- | ----------:| ---------:| | interpolate | 0.3696 | 0.2408 | | zero | 0.3898 | 0.2407 | | fbfill | 0.3949 | 0.2561 | | bffill | 0.2817 | 0.2517 | | **mean** | **0.521** | 0.2546 | | median | 0.455 | 0.2484 | #### 2. **類型的模型(Model Type)** - 雖然 Multi-model 的 Score 可能比較高,但因為訓練時間長,再加上 **單一筆 Sample 預測時間** 會大於 0.25s,而導致 kaggle 無法提交,因此後續將改由 **Single-model** 進行實驗。 - 無法提交的原因說明: - 將預測的筆數(3個月): 3 x 30 x 24 x 60 = 129,600 (1分鐘1筆Sample) - 比賽預測時間限制(9個小時): 9 x 60 = 540 (分鐘) - (540/129600) x 60 = 0.25秒 (預測每筆資料的時間限制) - Single model (P100) 約 0.1 秒 - Multi-model (P100) 約 0.6 秒 -> Timeout - 實驗數據 | 方法名稱 | 模型 Score | 訓練時間 | 單一筆預測時間 | | ------------ | ---------- | -------- | -------------- | | Multi-model | 0.4824 | 2418.3s | 0.638s | | Single-model | 0.4402 | 205.4s | 0.137s | #### 3. **輸出類型(Ouput Type)** : 根據結果,我們發現 **Seq to seq** 有較好的分數,顯見 **Seq to seq** 確實能讓模型有更多的資訊學習。 | 方法名稱 | 模型 Score | | ------------ |:--------- | | Seq to vector | 0.3569 | | Seq to seq | **0.4157** | #### 4. **超參數(Hyperparameter) 選擇**: - 我們個別對以下超參數調整做實驗: - Time step 數目 - Hidden layer 數目 - Layer 中 unit 數目 - 對沒有測試的控制變數統一設定為: - 填補空缺值的方法: `mean` - 模型類型: `single-model` - 輸出類型: `seq to seq` - 時間序列步長: `10` - 測試結果: | 超參數 | 模型 Score | | -------------------------- | --------- | | step [5,10,15] |[5] 0.1496<br>[10] **0.4157**<br>[15] 0.1622| | 層數 [1,2,3] |[1] -0.0771<br>[2] **0.528**<br>[3] 0.4157| | 神經元數 [32,128,512,1024] |[32] 0.1806<br>[128] 0.2622<br>[512] **0.4157**<br>[1024] -0.0046| #### 5. **實驗結論**: 根據實驗結果,我們認為以下參數選擇有較佳的結果 - 填補空缺值的方法: `mean` - 時間序列步長: `10` - 模型類型: `single-model` - 輸出類型: `seq to seq` - 每層神經元數目: `512` - LSTM 隱藏層數: `2` (實際 LSTM 有 3 層,第一層是 input 用) ### Scoring on Kaggle 我們根據實驗的結論,重新設計模型並上傳至 Kaggle 評分。 ![](https://i.imgur.com/7L7m84N.png) 相較於其他 LSTM 預測模型,我們的 LSTM 有較高的成績。 ### Historical score on Kaggle 此外我們也附上過往提交 Kaggle 後的分數,以證實我們的結果有越來越好。 | Model | Training Time | Testing Time | Kaggle Score | | ------------------------------------------------------------------------- | ------------- | ------------ | ------------ | | Single model </br> (Seq to seq, fbfill,</br> 3 steps, 2 Layers, 600) | 337.5s | < 9hr | 0.0495 | | Single model </br> (Seq to vector, fbfill,</br> 10 steps, 2 Layers, 600) | 313.8s | < 9hr | 0.1131 | | Single model </br> (Seq to vector, fbfill,</br> 10 steps, 3 Layers, 1200) | 1030.0s | < 7hr | 0.1448 | | **Single model </br> (Seq to seq, mean,</br> 10 steps, 3 Layers, 512)** | 319.9s | < 7hr | **0.1555** | | Multi-model </br> (Seq to seq, interpolate,</br> 15 steps, 2 Layers, 8) | 255.0s | > 9hr | Timeout | ## Conclusion 基於本次的作業,我們提出了兩種 RNN 的模型,並對其參數與效能進行測試評估,最終結果須等待本次競賽結束之後官方進行測試評分,也因為我們的模型是基於時間序列下的假設,所以在公開測試資料分數上沒有得到好成績,也不應該獲得好成績,因為公開測試資料與我們所訓練的模型時序是非連續的。 此外我們在進行測試時所得到的測試資料是不公開且不穩定的,而在這非常符合實際應用的情況,我們必須對所有情況進行例外處理,否則會得到非預期的結果。 在未來,我們希望可以透過加上更多層的隱藏層去增加對整個資料特徵分析,例如CNN+LSTM,不過對於極度動盪不穩定的股票系統上,過於精確的資料分析可能會造成反效果,所以勢必得在增加更多的隱藏層的情況下,必須提出對應的解決辦法,才能獲得更高的準確度。