RNN人名分類器案例
1 任務目的:
目的: 給定一個人名,來判定這個人名屬于哪個國家
典型的文本分類任務: 18分類---多分類任務
2 數據格式
-
注意:兩列數據,第一列是人名,第二列是國家類別,中間用制表符號"\t"隔開
Ang Chinese
AuYong? Chinese
Yuasa?? Japanese
Yuhara? Japanese
Yunokawa??? Japanese
3 任務實現流程
1. 獲取數據:案例中是直接給定的
2. 數據預處理: 臟數據清洗、數據格式轉換、數據源Dataset的構造、數據迭代器Dataloader的構造
3. 模型搭建: RNN、LSTM、GRU一系列模型
4. 模型訓練和評估(測試)
5. 模型上線---API接口(后續會講)
4 數據預處理
4.1讀取txt文檔數據
目的:
將文檔里面的數據讀取到內存中,實際上我們做了一個操作: 將人名存放到一個列表中,國家類別存放到一個列表中
代碼實現
def read_data(filename):# 1. 初始化兩個空列表my_list_x, my_list_y = [], []# 2. 讀取文件內容with open(filename,'r', encoding='utf-8') as fr:for line in fr.readlines():if len(line) <= 5:continue# strip()方法默認將字符串首尾兩端的空白去掉x, y = line.strip().split('\t')my_list_x.append(x)my_list_y.append(y)
?return my_list_x, my_list_y
4.2 構建自己的數據源DataSet
目的:
使用Pytorch框架,一般遵從一個規矩:使用DataSet方法構造數據源,來讓模型進行使用
構造數據源的過程中:必須繼承torch.utils.data.Dataset類,必須構造兩個魔法方法:__len__(), __getitem__()
__len__(): 一般返回的是樣本的總個數,我們可以直接len(dataset對象)直接就可以獲得結果
__getitem__(): 可以根據某個索引取出樣本值,我們可以直接用dataset對象[index]來直接獲得結果
代碼實現:
class NameClassDataset(Dataset):def __init__(self, mylist_x, mylist_y):self.mylist_x = mylist_xself.mylist_y = mylist_yself.sample_len = len(mylist_x)
?# 定義魔法方法lendef __len__(self):return self.sample_len
?# 定義魔法方法getitemdef __getitem__(self, index):# 1.index異常值處理index = min(max(index, 0), self.sample_len - 1)# 2. 根據index取出人名和國家名x = self.mylist_x[index]# print(f'x--->{x}')y = self.mylist_y[index]# print(f'y--->{y}')# 3.需要對人名進行one-hot編碼表示:這里的思路是:針對每個人名組成的單詞進行one-hot,然后再拼接tensor_x = torch.zeros(len(x), n_letter)# print(f'tensor_x-->{tensor_x}')for li, letter in enumerate(x):tensor_x[li][all_letters.find(letter)] = 1# 4.獲取標簽# print(f'dataset內部的tensor_x--》{tensor_x.shape}')tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)# print(f'dataset內部的tensor_y-->{tensor_y}')return tensor_x, tensor_y
4.3 構建數據迭代器Dataloader
目的:
為了將Dataset我們上一步構建的數據源,進行再次封裝,變成一個迭代器,可以進行for循環,而且,可以自動為我們dataset里面的數據進行增維(bath_size),也可以隨機打亂我們的取值順序
代碼實現:
filename = './data/name_classfication.txt'
my_list_x, my_list_y = read_data(filename)
mydataset = NameClassDataset(mylist_x=my_list_x, mylist_y=my_list_y)
my_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)
5 模型搭建
5.1 搭建RNN模型
-
注意事項
RNN模型在實例化的時候,默認batch_first=False,因此,需要小心輸入數據的形狀
因為: dataloader返回的結果x---》shape--〉[batch_size, seq_len, input_size], 所以課堂上代碼和講義稍微有點不同,講義是默認的batch_first=False,而我們的代碼是batch_first=True,這樣做的目的,可以直接承接x的輸入。
-
代碼實現
class MyRNN(nn.Module):def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):super().__init__()# input_size 代表詞嵌入維度;self.input_size = input_size# hidden_size代表RNN隱藏層維度self.hidden_size = hidden_size# output_size代表:國家種類個數self.ouput_size = ouput_sizeself.num_layers = num_layers# 定義RNN網絡層# 和講義不一樣,我設定了batch_first=True,意味著rnn接受的input第一個參數是batch_sizeself.rnn = nn.RNN(self.input_size, self.hidden_size,num_layers=self.num_layers, batch_first=True)# 定義輸出網絡層self.linear = nn.Linear(self.hidden_size, self.ouput_size)
?# 定義softmax層self.softmax = nn.LogSoftmax(dim=-1)
?def forward(self, input, hidden):# input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]# hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]
?# 將input和hidden送入RNN模型得到結果rnn_output【1,9,128】,rnn_hn[1,1,128]rnn_output, rnn_hn = self.rnn(input, hidden)# print(f'rnn_output--》{rnn_output.shape}')# temp:[1, 128]tmep = rnn_output[0][-1].unsqueeze(0)# print(f'tmep--》{tmep.shape}')# 將臨時tmep:代表當前樣本最后一詞的隱藏層輸出結果[1, 18]output = self.linear(tmep)# print(f'output--》{output.shape}')# 經過softmaxreturn self.softmax(output), rnn_hn
?def inithidden(self):return torch.zeros(self.num_layers, 1, self.hidden_size)
RNN模型測試
def test_RNN():# 1.得到數據my_dataloader = get_dataloader()# 2.實例化模型input_size = n_letter # 57hidden_size = 128 # 自定設定RNN模型輸出結果維度output_size = len(categorys) # 18my_rnn = MyRNN(input_size, hidden_size, output_size)h0 = my_rnn.inithidden()# 3.將數據送入模型for i, (x, y) in enumerate(my_dataloader):print(f'x--->{x.shape}')output, hn = my_rnn(input=x, hidden=h0)print(f'output模型輸出結果-->{output.shape}')print(f'hn-->{hn.shape}')break
5.2 搭建LSTM模型
-
注意事項
LSTM模型在實例化的時候,默認batch_first=False,因此,需要小心輸入數據的形狀 因為: dataloader返回的結果x---》shape--〉[batch_size, seq_len, input_size], 所以課堂上代碼和講義稍微有點不同,講義是默認的batch_first=False,而我們的代碼是batch_first=True,這樣做的目的,可以直接承接x的輸入。
-
代碼實現
class MyLSTM(nn.Module):def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):super().__init__()# input_size 代表詞嵌入維度;self.input_size = input_size# hidden_size代表RNN隱藏層維度self.hidden_size = hidden_size# output_size代表:國家種類個數self.ouput_size = ouput_sizeself.num_layers = num_layers# 定義LSTM網絡層# 和講義不一樣,我設定了batch_first=True,意味著rnn接受的input第一個參數是batch_sizeself.lstm = nn.LSTM(self.input_size, self.hidden_size,num_layers=self.num_layers, batch_first=True)# 定義輸出網絡層self.linear = nn.Linear(self.hidden_size, self.ouput_size)
?# 定義softmax層self.softmax = nn.LogSoftmax(dim=-1)
?def forward(self, input, hidden, c0):# input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]# hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]
?# 將input和hidden送入RNN模型得到結果rnn_output【1,9,128】,rnn_hn[1,1,128]lstm_output, (lstm_hn, lstm_cn) = self.lstm(input, (hidden, c0))# print(f'rnn_output--》{rnn_output.shape}')# temp:[1, 128]tmep = lstm_output[0][-1].unsqueeze(0)# print(f'tmep--》{tmep.shape}')# 將臨時tmep:代表當前樣本最后一詞的隱藏層輸出結果[1, 18]output = self.linear(tmep)# print(f'output--》{output.shape}')# 經過softmaxreturn self.softmax(output), lstm_hn, lstm_cn
?def inithidden(self):h0 = torch.zeros(self.num_layers, 1, self.hidden_size)c0 = torch.zeros(self.num_layers, 1, self.hidden_size)return h0, c0
LSTM測試
def test_LSTM():# 1.得到數據my_dataloader = get_dataloader()# 2.實例化模型input_size = n_letter # 57hidden_size = 128 # 自定設定LSTM模型輸出結果維度output_size = len(categorys) # 18my_lstm = MyLSTM(input_size, hidden_size, output_size)h0, c0 = my_lstm.inithidden()# 3.將數據送入模型for i, (x, y) in enumerate(my_dataloader):print(f'x--->{x.shape}')output, hn, cn = my_lstm(input=x, hidden=h0, c0=c0)print(f'output模型輸出結果-->{output.shape}')print(f'hn-->{hn.shape}')print(f'cn-->{cn.shape}')break
5.3 搭建GRU模型
-
注意事項
GRU模型在實例化的時候,默認batch_first=False,因此,需要小心輸入數據的形狀 因為: dataloader返回的結果x---》shape--〉[batch_size, seq_len, input_size], 所以課堂上代碼和講義稍微有點不同,講義是默認的batch_first=False,而我們的代碼是batch_first=True,這樣做的目的,可以直接承接x的輸入。
-
代碼實現
class MyGRU(nn.Module):def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):super().__init__()# input_size 代表詞嵌入維度;self.input_size = input_size# hidden_size代表RNN隱藏層維度self.hidden_size = hidden_size# output_size代表:國家種類個數self.ouput_size = ouput_sizeself.num_layers = num_layers# 定義GRU網絡層# 和講義不一樣,我設定了batch_first=True,意味著rnn接受的input第一個參數是batch_sizeself.gru = nn.GRU(self.input_size, self.hidden_size,num_layers=self.num_layers, batch_first=True)# 定義輸出網絡層self.linear = nn.Linear(self.hidden_size, self.ouput_size)
?# 定義softmax層self.softmax = nn.LogSoftmax(dim=-1)
?def forward(self, input, hidden):# input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]# hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]
?# 將input和hidden送入RNN模型得到結果rnn_output【1,9,128】,rnn_hn[1,1,128]gru_output, gru_hn = self.gru(input, hidden)# print(f'rnn_output--》{rnn_output.shape}')# temp:[1, 128]tmep = gru_output[0][-1].unsqueeze(0)# print(f'tmep--》{tmep.shape}')# 將臨時tmep:代表當前樣本最后一詞的隱藏層輸出結果[1, 18]output = self.linear(tmep)# print(f'output--》{output.shape}')# 經過softmaxreturn self.softmax(output), gru_hn
?def inithidden(self):return torch.zeros(self.num_layers, 1, self.hidden_size)
GRU測試
def test_GRU():# 1.得到數據my_dataloader = get_dataloader()# 2.實例化模型input_size = n_letter # 57hidden_size = 128 # 自定設定RNN模型輸出結果維度output_size = len(categorys) # 18my_gru = MyGRU(input_size, hidden_size, output_size)# 2.1 初始化參數h0 = my_gru.inithidden()# 3.將數據送入模型for i, (x, y) in enumerate(my_dataloader):print(f'x--->{x.shape}')output, hn = my_gru(input=x, hidden=h0)print(f'output模型輸出結果-->{output.shape}')print(f'hn-->{hn.shape}')break
6 模型訓練
基本過程
1.獲取數據
2.構建數據源Dataset
3.構建數據迭代器Dataloader
4.加載自定義的模型
5.實例化損失函數對象
6.實例化優化器對象
7.定義打印日志參數
8.開始訓練
8.1 實現外層大循環epoch
(可以在這構建數據迭代器Dataloader)
8.2 內部遍歷數據迭代器dataloader
8.3 將數據送入模型得到輸出結果
8.4 計算損失
8.5 梯度清零: optimizer.zero_grad()
8.6 反向傳播: loss.backward()
8.7 參數更新(梯度更新): optimizer.step()
8.8 打印訓練日志
9. 保存模型: torch.save(model.state_dict(), "model_path")
6.1 RNN模型訓練代碼實現
my_lr = 1e-3
epochs = 1
# 訓練rnn模型
def train_rnn():# 讀取數據my_list_x, my_list_y = read_data(filepath='./data/name_classfication.txt')# 實例化dataset數據源對象my_dataset = NameClassDataset(my_list_x, my_list_y)# 實例化模型# n_letters=57, hidden_size=128,類別總數output_size=18my_rnn = My_RNN(input_size=57, hidden_size=128, output_size=18)# 實例化損失函數對象my_nll_loss = nn.NLLLoss()# 實例化優化器對象my_optim = optim.Adam(my_rnn.parameters(), lr=my_lr)# 定義打印日志的參數start_time = time.time()total_iter_num = 0 # 當前已經訓練的樣本總數total_loss = 0 ?# 已經訓練的損失值total_loss_list = [] # 每隔n個樣本,保存平均損失值total_acc_num = 0 # 預測正確的樣本個數total_acc_list = [] # 每隔n個樣本,保存平均準確率# 開始訓練for epoch_idx in range(epochs):# 實例化dataloadermy_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)# 開始內部迭代數據,送入模型for i, (x, y) in enumerate(tqdm(my_dataloader)):# print(f'x--》{x.shape}')# print(f'y--》{y}')output, hn = my_rnn(input=x[0], hidden=my_rnn.inithidden())# print(f'output--》{output}') # [1, 18]# 計算損失my_loss = my_nll_loss(output, y)# print(f'my_loss--》{my_loss}')# print(f'my_loss--》{type(my_loss)}')
?# 梯度清零my_optim.zero_grad()# 反向傳播my_loss.backward()# 梯度更新my_optim.step()
?# 統計一下已經訓練樣本的總個數total_iter_num = total_iter_num + 1
?# 統計一下已經訓練樣本的總損失total_loss = total_loss + my_loss.item()
?# 統計已經訓練的樣本中預測正確的個數i_predict_num = 1 if torch.argmax(output).item() == y.item() else 0total_acc_num = total_acc_num + i_predict_num# 每隔100次訓練保存一下平均損失和準確率if total_iter_num % 100 == 0:avg_loss = total_loss / total_iter_numtotal_loss_list.append(avg_loss)
?avg_acc = total_acc_num / total_iter_numtotal_acc_list.append(avg_acc)# 每隔2000次訓練打印一下日志if total_iter_num % 2000 == 0:temp_loss = total_loss / total_iter_numtemp_acc = total_acc_num / total_iter_numtemp_time = time.time() - start_timeprint('輪次:%d, 損失:%.6f, 時間:%d,準確率:%.3f' %(epoch_idx+1, temp_loss, temp_time, temp_acc))torch.save(my_rnn.state_dict(), './save_model/ai20_rnn_%d.bin'%(epoch_idx+1))# 計算總時間total_time = int(time.time() - start_time)print('訓練總耗時:', total_time)# 將結果保存到文件中dict1 = {"avg_loss":total_loss_list,"all_time": total_time,"avg_acc": total_acc_list}with open('./save_results/ai_rnn.json', 'w') as fw:fw.write(json.dumps(dict1))
?return total_loss_list, total_time, total_acc_list
6.2 LSTM模型訓練代碼實現
基本原理同上
6.3 GRU模型訓練代碼
基本原理同上
7 模型預測
基本過程
1.獲取數據
2.數據預處理:將數據轉化one-hot編碼
3.實例化模型
4.加載模型訓練好的參數: model.load_state_dict(torch.load("model_path"))
5.with torch.no_grad():
6.將數據送入模型進行預測(注意:張量的形狀變換)
RNN模型預測代碼:
def line2tensor(x):# x-->"bai"tensor_x = torch.zeros(len(x), n_letters)# one-hot表示for li, letter in enumerate(x):tensor_x[li][letters.find(letter)] = 1
?return tensor_x
# 構造rnn預測函數
def rnn_predict(x):# 將數據x進行張量的轉換tensor_x = ?line2tensor(x)# 加載訓練好的模型my_rnn = My_RNN(input_size=57, hidden_size=128, output_size=18)my_rnn.load_state_dict(torch.load('./save_model/ai20_rnn_3.bin'))# 實現模型的預測with torch.no_grad():# 將數據送入模型output, hn = my_rnn(tensor_x, my_rnn.inithidden())print(f'output--》{output}')# 獲取output最大的前3個值# output.topk(3, 1, True)values, indexes = torch.topk(output, k=3, dim=-1, largest=True)print(f'values-->{values}')print(f'indexes-->{indexes}')for i in range(3):value = values[0][i]index = indexes[0][i]category = categorys[index]print(f'當前預測的值是:{value}, 國家類別是:{category}')