# <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) ```