# 期末紙本報告
###### 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 評分。

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