# <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 計算)

```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 上升但剛好猜中,正確率也變高。

### 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 來做。

```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 出現新低時儲存模型,然後就能在那個節點調整參數繼續訓練。


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