---
# System prepended metadata

title: <center>深度學習基石與實務 作業三</center>

---

# <center>深度學習基石與實務 作業三</center>
## <center>611121212 李奕承</center>

## 這份作業的過程讓我學習到
- 使用驗證集
- 使用GPU來訓練
- 建立checkpoint
- 使用RNN

## 我設計的Dataset
```python
class Train_Data(Dataset):
    def __init__(self, data, label):
        self.data = data
        self.label = label
        self.n = data.__len__()

    def __getitem__(self, idx):
        return self.data[idx], self.label[idx]

    def __len__(self):
        return self.n

```
## 我設計的神經網路
```python
class RNN(nn.Module):
    def __init__(self):
        super(RNN, self).__init__()
        self.rnn = nn.RNN(
            # 每一步的參數數量，本次作業共有 x、y 兩個參數
            input_size=2,
            # 隱藏層中的神經元數
            hidden_size=400,
            # 每個 RNN 中的隱藏層數
            num_layers=2,
            # batch 放在 tensor 中的第一個維度
            batch_first=True,
            # 設定 dropout
            dropout=0.05
        )
        
        # 在最後的輸出加上一層全連接層，把輸出降為 10 個(用來分類 0~9)
        self.out = nn.Linear(400, 10)

    def forward(self, x):
        r_out, (h_n, h_c) = self.rnn(x, None)
        # r_out[:, -1, :] 只取最後一步的結果
        out = self.out(r_out[:, -1, :])
        return out

```
## 遭遇的問題與解決方法、步驟
### 1 如何使用驗證集
雖然在課外的加分題有回答過如何分割資料，但是前兩份作業一直沒有研究如何應用在實際的訓練中，這次決定來使用看看。

本次作業提供的訓練資料共有 7494 筆，質因數分解後決定每 6 筆取 1 筆來當驗證資料。
```python
# 設定訓練資料所在的路徑(使用相對路徑，方便在實驗室電腦和自己的 linux 主機之間轉換)
train_in_path = "./data/train_in.csv"
train_out_path = "./data/train_out.csv"
test_in_path = "./data/test_in.csv"

# 用 pandas 讀取資料
train_in_data = pd.read_csv(train_in_path)
train_out_data = pd.read_csv(train_out_path)
test_in_data = pd.read_csv(test_in_path)

# 將要使用的行取出，建立 numpy 的陣列
train = np.array(
    train_in_data[['x1', 'y1', 'x2', 'y2', 'x3', 'y3', 'x4', 'y4', 'x5', 'y5', 'x6', 'y6', 'x7', 'y7', 'x8', 'y8']])
label = np.array(train_out_data["Label"])
test = np.array(
    test_in_data[['x1', 'y1', 'x2', 'y2', 'x3', 'y3', 'x4', 'y4', 'x5', 'y5', 'x6', 'y6', 'x7', 'y7', 'x8', 'y8']])

# 轉換所有資料類型為浮點數，並建立空陣列等下用來分割資料
tensor_all_train = torch.tensor(train, dtype=torch.float)
tensor_train_cat = []
tensor_valid_temp = []

tensor_all_label = torch.tensor(label, dtype=torch.float)
tensor_train_label_temp = []
tensor_valid_label_temp = []

tensor_test = torch.tensor(test, dtype=torch.float)
tensor_test = tensor_test.view(-1, 8, 2).to(DEVICE)

# 分割訓練資料
tensor_all_train_split = torch.split(tensor_all_train, 6)
tensor_all_label_split = torch.split(tensor_all_label, 6)

# 每6筆中拿1筆資料出來當驗證集
for block in tensor_all_train_split:
    tensor_train_cat.append(block[0])
    tensor_train_cat.append(block[1])
    tensor_train_cat.append(block[2])
    tensor_train_cat.append(block[3])
    tensor_train_cat.append(block[4])
    tensor_valid_temp.append(block[5])
    
for block in tensor_all_label_split:
    tensor_train_label_temp.append(block[0])
    tensor_train_label_temp.append(block[1])
    tensor_train_label_temp.append(block[2])
    tensor_train_label_temp.append(block[3])
    tensor_train_label_temp.append(block[4])
    tensor_valid_label_temp.append(block[5])

# 把 list 中的成員升維，拼接成 1 個 tensor
tensor_train = tensor_train_cat[0].unsqueeze(0)
for i in range(1, len(tensor_train_cat)):
    tensor_train = torch.cat((tensor_train, tensor_train_cat[i].unsqueeze(0)), dim=0)
tensor_train = tensor_train.view(-1, 8, 2).to(DEVICE)

tensor_valid = tensor_valid_temp[0].unsqueeze(0)
for i in range(1, len(tensor_valid_temp)):
    tensor_valid = torch.cat((tensor_valid, tensor_valid_temp[i].unsqueeze(0)), dim=0)
tensor_valid = tensor_valid.view(-1, 8, 2).to(DEVICE)

tensor_train_label = tensor_train_label_temp[0].unsqueeze(0)
for i in range(1, len(tensor_train_label_temp)):
    tensor_train_label = torch.cat((tensor_train_label, tensor_train_label_temp[i].unsqueeze(0)), dim=0)
tensor_train_label = tensor_train_label.to(DEVICE)

# 因為驗證集的正確率在 CPU 做比對，所以不需要轉為 tensor
tensor_valid_label = tensor_valid_label_temp
```
每做完 1 個 epoch ，就驗證一次正確率(要使用 cpu() 函數，把 output 移回 CPU 計算)
![](https://hackmd.io/_uploads/SkW7Du6v2.png)

```python
    # 開始訓練
    for epoch in range(EPOCH):
        for step, (x, y) in enumerate(train_loader):
            b_x = x
            b_y = y
            output = rnn(b_x)
            loss = loss_fn(output, b_y.long())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 每完成一個 EPOCH 就做一次驗證
        valid_output = rnn(tensor_valid)
        pred_y = torch.max(valid_output, 1)[1].data.cpu().numpy().squeeze()
        accuracy = sum(pred_y == tensor_valid_label) / len(pred_y)
        # 顯示第幾次 epoch、loss、驗證正確率
        print('Epoch: ', epoch, '| train loss: %.4f' % loss, '| valid accuracy: %.2f' % accuracy)
```
可以發現 loss 跟正確率並不完全相關，有時候 loss 上升但剛好猜中，正確率也變高。
![](https://hackmd.io/_uploads/S1Hq1_TDh.png)


### 2 如何使用GPU來訓練
前兩份作業一直沒有研究如何使用GPU，只能用CPU慢慢跑。這次來研究一下。

要使用GPU，必須要先檢查是否有GPU可用。
```python
# 使用 pytorch 提供的 is_available() 函數可以檢查是否能用 GPU
check_gpu = torch.cuda.is_available()
if check_gpu == 1:
    print('可以使用gpu')
    DEVICE = "cuda:0"
```
需要使用 pytorch 提供的 to() 函數，把三種東西都放進 GPU: 訓練資料、神經網路、損失函數。

移動訓練資料
```python
tensor_train = tensor_train.view(-1, 8, 2).to(DEVICE)
tensor_valid = tensor_valid.view(-1, 8, 2).to(DEVICE)
tensor_test = tensor_test.view(-1, 8, 2).to(DEVICE)
```
移動神經網路
```python
rnn = RNN()
rnn.to(DEVICE)
```
移動損失函數
```python
loss_fn = nn.CrossEntropyLoss().to(DEVICE)
```
因為驗證沒有用到 loss function ，而是直接和 label 比較計算正確率，所以要把輸出移動到 CPU 來做。
![](https://hackmd.io/_uploads/SkW7Du6v2.png)
```python
valid_output = rnn(tensor_valid)
pred_y = torch.max(valid_output, 1)[1].data.cpu().numpy().squeeze()
```
訓練完後輸入測試資料時也一樣
```python
# 將 test 資料輸入模型，取得預測結果
test_output = rnn(tensor_test)
pred_y = torch.max(test_output, 1)[1].data.cpu().numpy().squeeze()
```
借一個 GPU 實際使用後速度的確提升許多。

### 3 建立checkpoint
前兩份作業只在訓練完成後才儲存模型，當 EPOCHS 設定很多時，如果中途當機或需要暫停，就只能中止，前功盡棄。

於是我新建一個 checkpoint 資料夾，並在每完成一次 epoch 時就儲存一次模型(使用相同檔名不停覆蓋)，之後也可以視需求，在 loss 出現新低時儲存模型，然後就能在那個節點調整參數繼續訓練。
![](https://hackmd.io/_uploads/S191586wh.png)
![](https://hackmd.io/_uploads/HksmgupD2.png)

```python
    for epoch in range(EPOCH):
        for step, (x, y) in enumerate(train_loader):
            b_x = x
            b_y = y
            output = rnn(b_x)
            loss = loss_fn(output, b_y.long())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 每完成一個 EPOCH 就做一次驗證
        valid_output = rnn(tensor_valid)
        pred_y = torch.max(valid_output, 1)[1].data.cpu().numpy().squeeze()
        accuracy = sum(pred_y == tensor_valid_label) / len(pred_y)
        # 顯示第幾次 epoch、loss、驗證正確率
        print('Epoch: ', epoch, '| train loss: %.4f' % loss, '| valid accuracy: %.2f' % accuracy)
        # 每完成一個 EPOCH 就覆蓋一次checkpoint，中途當機或暫停時可以從斷掉的點繼續，不用全部重來
        torch.save(
            rnn,
            './checkpoint/{}.pt'
            .format(time.strftime("checkpoint-%Y-%m-%d %H_%M_%S", train_time_temp)))
```
### 4 使用RNN
本次作業需要使用 RNN 來做，於是學習如何使用 pytorch 的 RNN
```python
class RNN(nn.Module):
    def __init__(self):
        super(RNN, self).__init__()
        self.rnn = nn.RNN(
            # 每一步的參數數量，本次作業共有 x、y 兩個參數
            input_size=2,
            # 隱藏層中的神經元數
            hidden_size=400,
            # 每個 RNN 中的隱藏層數
            num_layers=2,
            # batch 放在 tensor 中的第一個維度
            batch_first=True,
            # 設定 dropout
            dropout=0.05
        )

        # 在最後的輸出加上一層全連接層，把輸出降為 10 個(用來分類 0~9)
        self.out = nn.Linear(400, 10)

    def forward(self, x):
        r_out, (h_n, h_c) = self.rnn(x, None)
        # r_out[:, -1, :] 只取最後一步的結果
        out = self.out(r_out[:, -1, :])
        return out

```
要將資料丟進 RNN 前，需要先把 tensor 擴展變形，加上時間步數(本次作業是 8 步)，還有每一步有幾個參數(本次作業每一步都有 x、y 兩個變數)
```python
tensor_train = tensor_train.view(-1, 8, 2).to(DEVICE)
tensor_valid = tensor_valid.view(-1, 8, 2).to(DEVICE)
tensor_test = tensor_test.view(-1, 8, 2).to(DEVICE)
```
## 創新的地方
### 建立可以重複使用的範本
這幾次作業面臨到了版本衝突、重構、還有在不同工作環境間移動時需要修改的麻煩，光解決這些就花費了大量的時間，嚴重的影響學習進度。最後終於在這次的作業突破了許多技術上的障礙，並學會了使用 GPU ，於是決定把這些新學到的東西整合起來，建立一個可以面對多種使用情境的範本。未來訓練其他模型時，只需要修改: 資料前處理、dataset、神經網路 的部分就可以了，大幅的提升學習效率。

這個版本的程式碼提供四種模式給使用者選擇，只需要在參數區塊填入想要的模式就好
- 0:從頭開始訓練
- 1:載入checkpoint繼續訓練 
- 2:載入模型繼續訓練 
- 3:單純使用模型

```python
# 設定訓練參數
BATCH_SIZE = 10
LEARN_RATE = 0.0001
MOMENTUM = 0.9
EPOCH = 10
MODE = 0  # 0:從頭開始訓練 1:載入checkpoint繼續訓練 2:載入模型繼續訓練 3:單純使用模型
LOAD_MODEL_PATH = "請貼上想要載入的模型的路徑"
LOAD_CHECKPOINT_PATH = "請貼上想要載入的checkpoint模型的路徑"
USE_GPU = 1  # 0:使用 CPU 
             # 1:使用 GPU (若檢查發現不能用 GPU ，會使用 CPU)
```
用 if 判斷是哪一種模式分別進行處理，模式 0、1、2 都是訓練，只是模型的狀態不同而有差異。
```python
if MODE == 0 or MODE == 1 or MODE == 2:
    # 實例化資料集
    train = Train_Data(tensor_train, tensor_train_label)
    # 實例化載入器
    train_loader = DataLoader(dataset=train, batch_size=BATCH_SIZE, shuffle=True)

    if MODE == 0:  # MODE 0:從頭開始訓練
        # 實例化神經網路
        rnn = RNN()
        rnn.to(DEVICE)
    elif MODE == 1:  # MODE 1:載入checkpoint繼續訓練
        rnn = torch.load(LOAD_CHECKPOINT_PATH)
        rnn.train()
        rnn.to(DEVICE)
    else:  # MODE 2:載入模型繼續訓練
        rnn = torch.load(LOAD_MODEL_PATH)
        rnn.train()
        rnn.to(DEVICE)

    # 實例化優化器，使用 SGD
    optimizer = torch.optim.SGD(rnn.parameters(), lr=LEARN_RATE, momentum=MOMENTUM)
    # 實例化 loss function ，使用 CrossEntropy
    loss_fn = nn.CrossEntropyLoss().to(DEVICE)

    # 開始訓練
    for epoch in range(EPOCH):
        for step, (x, y) in enumerate(train_loader):
            b_x = x
            b_y = y
            output = rnn(b_x)
            loss = loss_fn(output, b_y.long())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 每完成一個 EPOCH 就做一次驗證
        valid_output = rnn(tensor_valid)
        pred_y = torch.max(valid_output, 1)[1].data.cpu().numpy().squeeze()
        accuracy = sum(pred_y == tensor_valid_label) / len(pred_y)
        print('Epoch: ', epoch, '| train loss: %.4f' % loss, '| valid accuracy: %.2f' % accuracy)
        # 每完成一個 EPOCH 就覆蓋一次checkpoint，中途當機或暫停時可以從斷掉的點繼續，不用全部重來
        torch.save(
            rnn,
            './checkpoint/{}.pt'
            .format(time.strftime("checkpoint-%Y-%m-%d %H_%M_%S", train_time_temp)))

    # 將 10 筆驗證資料丟入模型預測
    valid_output = rnn(tensor_valid[:10].view(-1, 8, 2))
    pred_y = torch.max(valid_output, 1)[1].data.cpu().numpy().squeeze()

    # 將 10 筆驗證資料的預測結果整理成陣列顯示
    pred_temp = []
    for item in pred_y[:10]:
        pred_temp.append(item.item())
    print(pred_temp, 'prediction number')

    # 將 10 筆驗證資料的正確答案整理成陣列顯示
    real_temp = []
    for item in tensor_valid_label[:10]:
        real_temp.append(item.int().item())
    print(real_temp, 'real number')

    # 將 test 資料輸入模型，取得預測結果
    test_output = rnn(tensor_test)
    pred_y = torch.max(test_output, 1)[1].data.cpu().numpy().squeeze()

    # 將預測結果存成csv檔，以開始時間做為檔名
    test_save = pd.DataFrame(pred_y, range(1, len(pred_y) + 1))
    test_save.columns = ['Label']
    test_save.to_csv('./output/{}.csv'
                     .format(time.strftime("prediction-%Y-%m-%d %H_%M_%S", train_time_temp)),
                     index_label="Serial No."
                     )

    # 儲存訓練完的model
    torch.save(
        rnn,
        './model/{}.pt'
        .format(time.strftime("model-%Y-%m-%d %H_%M_%S", train_time_temp)))
```
模式 3 單純使用訓練過的模型
```python
elif MODE == 3:  # MODE 3:使用模式
    model = torch.load(LOAD_MODEL_PATH)
    model.eval()
    model.to(DEVICE)

    # 將 test 資料輸入模型，取得預測結果
    test_output = model(tensor_test)
    pred_y = torch.max(test_output, 1)[1].data.cpu().numpy().squeeze()

    # 將預測結果存成csv檔，以開始時間做為檔名
    test_save = pd.DataFrame(pred_y, range(1, len(pred_y) + 1))
    test_save.columns = ['Label']
    test_save.to_csv('./output/{}.csv'
                     .format(time.strftime("prediction-%Y-%m-%d %H_%M_%S", train_time_temp)),
                     index_label="Serial No."
                     )
```

### 適應不同環境的 GPU 處理
當變數 USE_GPU 設為 0 時只使用 CPU，設為 1 時若可以用 GPU 則用，若不能用就只用 CPU
```python
USE_GPU = 1  # 0:使用 CPU 
             # 1:使用 GPU (若檢查發現不能用 GPU ，會使用 CPU)

DEVICE = "cpu"
if USE_GPU == 1:
    check_gpu = torch.cuda.is_available()
    if check_gpu == 1:
        print('可以使用gpu')
        DEVICE = "cuda:0"
    else:
        print('不能使用gpu')
```
因為我用的都是 to() 函數，而不是 cuda() 函數，這樣在只有 CPU 的環境下就不需要更改程式碼。


```python
tensor_train = tensor_train.view(-1, 8, 2).to(DEVICE)
tensor_valid = tensor_valid.view(-1, 8, 2).to(DEVICE)
tensor_test = tensor_test.view(-1, 8, 2).to(DEVICE)

rnn = RNN()
rnn.to(DEVICE)

loss_fn = nn.CrossEntropyLoss().to(DEVICE)
```