---
# System prepended metadata

title: 期末紙本報告
tags: [報告]

---

# 期末紙本報告
###### 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，不過對於極度動盪不穩定的股票系統上，過於精確的資料分析可能會造成反效果，所以勢必得在增加更多的隱藏層的情況下，必須提出對應的解決辦法，才能獲得更高的準確度。