【Pytorch神经网络实战案例】40 TextCNN模型分析IMDB数据集评论的积极与消极
卷積神經網絡不僅在圖像視覺領域有很好的效果,而且在基于文本的NLP領域也有很好的效果。TextCN如模型是卷積神經網絡用于文本處理方面的一個模型。
在TextCNN模型中,通過多分支卷積技術實現對文本的分類功能。
1 TextCNN
1.1 TextCNN模型結構
TexCNN模型是利用卷積神經網絡對文本進行分類的模型,該模型的結構可以分為以下4個層次:
1.1.1 詞嵌入層
將每個詞對應的向量轉化成多維度的詞嵌入向量,將每個句子當作一幅圖來進行處理(詞的個數詞×嵌入向量維度)。
1.1.2 多分支卷積層
使用3、4、5等不同大小的卷積核對詞嵌入轉化后的句子做卷積操作,生成大小不同的特征數據。
1.1.3 多分支全局最大池化層
對多分支卷積層中輸出的每個分支的特征數據做全局最大池化操作。
1.1.4 全連接分類輸出層
將池化后的結果輸入全連接網絡中,輸出分類個數,得到最終結果。
1.2 TextCNN模型圖解
因為卷積神經網絡具有提取局部特征的功能,所以可用卷積神經網絡提取句子中類似N-Gram算法的關鍵信息。
1.3 數據集IMDB
MDB數據集相當于圖片處理領域的MNIST數據集,在NLP任務中經常被使用。
1.3.1 IMDB結構組成
IMDB數據集包含50000條評論,平均分成訓練數據集(25000條評論)和測試數據集(25000條評論)。標簽的總體分布是平衡的(25000條正面評論和25000條負面評論)。
另外,還包括額外的50000份無標簽文件,用于無監督學習。
1.3.2 IMDB文件夾組成
IMDB數據集主要包括兩個文件夾train與test,分別存放訓練數據集與測試數據集。每個文件夾中都包含正樣本和負樣本,分別放在pos與neg子文件中。rain文件夾下還額外包含一個unsup子文件夾,用于非監督訓練。
1.3.3 IMDB文件命名規則
每個樣本文件的命名規則為“序號_評級”。其中“評級”可以分為0~9級。
?IMDB是torchtext庫的內置數據集,可以直接通過運行torchtext庫的接口進行獲取。
2 代碼實現:TextCNN模型分析IMDB數據集評論的積極與消極
2.1 案例描述
同一個記錄評論語句的數據集,分為正面和負面兩種情緒。通過訓練,讓模型能夠理解正面與負面兩種情緒的語義,并對評論文本進行分類。
2.1.1 案例理解分析
本例的任務可以理解為通過句子中的關鍵信息進行語義分類,這與TextCNN模型的功能是相匹配的。TextCNN模型中使用了池化操作,在這個過程中丟失了一些信息,所以導致該模型所表征的句子特征有限。如果要使用處理相近語義的分類任務,則還需要對其進一步進行調整。
2.2 代碼實現:引入基礎庫: 固定PyTorch中的隨機種子和GPU運算方式---TextCNN.py(第1部分)
# 1.1 引入基礎庫: 固定PyTorch中的隨機種子和GPU運算方式。 import random #引入基礎庫 import time import torch#引入PyTorch庫 import torch.nn as nn import torch.nn.functional as F from torchtext.legacy import data ,datasets,vocab #引入文本處理庫 import spacytorch.manual_seed(1234) # 固定隨機種子,使其每次運行時對權重參數的初始化值一致。 # 固定GPU運算方式:提高GPU的運算效率,通常PyTorch會調用自動尋找最適合當前配置的高效算法進行計算,這一過程會導致每次運算的結果可能出現不一致的情況。 torch.backends.cudnn.deterministic = True # 表明不使用尋找高效算法的功能,使得每次的運算結果一致。[僅GPU有效]2.3 代碼實現:用torchtext加載IMDB并拆分為數據集---TextCNN.py(第2部分)
# 1.2 用torchtext加載IMDB并拆分為數據集 # IMDB是torchtext庫的內置數據集,可以直接通過torchtext庫中的datasets.MDB進行處理。 # 在處理之前將數據集的字段類型和分詞方法指定正確即可。# 定義字段,并按照指定標記化函數進行分詞 TEXT = data.Field(tokenize = 'spacy',lower=True) # data.Field函數指定數據集中的文本字段用spaCy庫進行分詞處理,并將其統一改為小寫字母。tokenize參數,不設置則默認使用str。 LABEL = data.LabelField(dtype=torch.float)# 加載數據集,并根據IMDB兩個文件夾,返回兩個數據集。 # datasets.MDB.splits()進行數據集的加載。該代碼執行時會在本地目錄的.data文件夾下查找是否有MDB數據集,如果沒有,則下載;如果有,則將其加載到內存。 # 被載入內存的數據集會放到數據集對象train_data與test_data中。 train_data , test_data = datasets.IMDB.splits(text_field=TEXT,label_field=LABEL) print("-----------輸出一條數據-----------") # print(vars(train_data.example[0]),len(train_data.example)) print(vars(train_data.examples[0]),len(train_data.examples)) print("---------------------------")# 將訓練數據集再次拆分 # 從訓練數據中拆分出一部分作為驗證數據集。數據集對象train_data的split方法默認按照70%、30%的比例進行拆分。 train_data,valid_data = train_data.split(random_state = random.seed(1234)) print("訓練數據集: ", len(train_data),"條") print("驗證數據集: ", len(valid_data),"條") print("測試數據集: ", len(test_data),"條")2.4 代碼實現:加載預訓練詞向量并進行樣本數據化---TextCNN.py(第3部分)
# 1.3 加載預訓練詞向量并進行樣本數據化 # 將數據集中的樣本數據轉化為詞向量,并將其按照指定的批次大小進行組合。 # buld_vocab方法實現文本到詞向量數據的轉化:從數據集對象train_data中取出前25000個高頻詞,并用指定的預訓練詞向量glove.6B.100d進行映射。 TEXT.build_vocab(train_data,max_size=25000,vectors="glove.6B.100d",unk_init = torch.Tensor.normal_) # 將樣本數據轉化為詞向量 # glove.6B.100d為torchtext庫中內置的英文詞向量,主要將每個詞映射成維度為100的浮點型數據,該文件會被下載到本地.vector_cache文件夾下。 LABEL.build_vocab(train_data) # ---start---創建批次數據:將數據集按照指定批次進行組合。 BATCH_SIZE = 64 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device) # ---end---創建批次數據:將數據集按照指定批次進行組合。?
2.5 代碼實現:定義帶有Mish激活函數的TextCNN模型---TextCNN.py(第4部分)
class Mish(nn.Module):def __init__(self):super(Mish, self).__init__()def forward(self,x):x = x * (torch.tanh(F.softplus(x)))return x# 在TextCNN類中,一共有兩個方法: # ①初始化方法.按照指定個數定義多分支卷積層,并將它們統一放在nn.ModuleList數組中。 # ②前向傳播方法:先將輸入數據依次輸入每個分支的卷積層中進行處理,再對處理結果進行最大池化,最后對池化結果進行連接并回歸處理 class TextCNN(nn.Module): #定義TextCNN模型# TextCNN類繼承了nn.Module類,在該類中定義的網絡層列表必須要使用nn.ModuleList進行轉化,才可以被TextCNN類識別。# 如果直接使用列表的話,在訓練模型時無法通過TextCNN類對象的parameters方法獲得權重。# 定義初始化方法def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim,dropout, pad_idx):super().__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx) # 定義詞向量權重# 定義多分支卷積層# 將定義好的多分支卷積層以列表形式存放,以便在前向傳播方法中使用。# 每個分支中卷積核的第一個維度由參數filter_sizes設置,第二個維度都是embedding_dim,即只在縱軸的方向上實現了真正的卷積操作,在橫軸的方向上是全尺度卷積,可以起到一維卷積的效果。self.convs = nn.ModuleList([nn.Conv2d(in_channels = 1,out_channels = n_filters,kernel_size = (fs, embedding_dim))for fs in filter_sizes]) #########注意不能用list# 定義輸出層self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)self.dropout = nn.Dropout(dropout)self.mish = Mish() # 實例化激活函數對象# 定義前向傳播方法def forward(self,text): # 輸入形狀為[sent len,batch size]text = text.permute(1, 0) # 將形狀變為[batch size, sent len]embedded = self.embedding(text) # 對于輸入數據進行詞向量映射,形狀為[batch size, sent len, emb dim]embedded = embedded.unsqueeze(1) # 進行維度變化,形狀為[batch size, 1, sent len, emb dim]# len(filter_sizes)個元素,每個元素形狀為[batch size, n_filters, sent len - filter_sizes[n] + 1]# 多分支卷積處理conved = [self.mish(conv(embedded)).squeeze(3) for conv in self.convs] # 將輸入數據進行多分支卷積處理。該代碼執行后,會得到一個含有len(fiter_sizes)個元素的列表,其中每個元素形狀為[batchsize,n_filters,sentlen-fltersizes[n]+1],該元素最后一個維度的公式是由卷積公式計算而來的。# 對于每個卷積結果進行最大池化操作pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]# 將池化結果進行連接cat = self.dropout(torch.cat(pooled, dim=1)) # 形狀為[batch size, n_filters * len(filter_sizes)]return self.fc(cat) # 輸入全連接,進行回歸輸出2.6 代碼實現:用數據集參數實例化模型---TextCNN.py(第5部分)
# 1.5 用數據集參數實例化模型 if __name__ == '__main__':# 根據處理好的數據集參數對TextCNN模型進行實例化。INPUT_DIM = len(TEXT.vocab) # 25002EMBEDDING_DIM = TEXT.vocab.vectors.size()[1] # 100N_FILTERS = 100 # 定義每個分支的數據通道數量FILTER_SIZES = [3, 4, 5] # 定義多分支卷積中每個分支的卷積核尺寸OUTPUT_DIM = 1 # 定義輸出維度DROPOUT = 0.5 # 定義Dropout丟棄率PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] # 定義填充值:獲取數據集中填充字符對應的索引。在詞向量映射過程中對齊數據時會使用該索引進行填充。# 實例化模型model = TextCNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)2.7?代碼實現:用預訓練詞向量初始化模型---TextCNN.py(第6部分)
# 1.6 用預訓練詞向量初始化模型:將加載好的TEXT字段詞向量復制到模型中,為其初始化。# 復制詞向量model.embedding.weight.data.copy_(TEXT.vocab.vectors)# 將填充的詞向量清0UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) #對未識別詞進行清零處理 :使該詞在詞向量空間中失去意義,目的是防止后面填充字符對原有的詞向量空間進行干擾。model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM) #對填充詞進行清零處理 :使該詞在詞向量空間中失去意義,目的是防止后面填充字符對原有的詞向量空間進行干擾。2.8?代碼實現:使用Ranger優化器訓練模型---TextCNN.py(第7部分)
# 1.7 使用Ranger優化器訓練模型import torch.optim as optim # 引入優化器庫from functools import partial # 引入偏函數庫from ranger import * # 載入Ranger優化器# 為Ranger優化器設置參數opt_func = partial(Ranger, betas=(.9, 0.99), eps=1e-6) # betas=(Momentum,alpha)optimizer = opt_func(model.parameters(), lr=0.004)# 定義損失函數criterion = nn.BCEWithLogitsLoss() # nn.BCEWithLogitsLoss函數是帶有Sigmoid函數的二分類交叉熵,即先對模型的輸出結果進行Sigmoid計算,再對其余標簽一起做Cross_entropy計算。# 分配運算資源model = model.to(device)criterion = criterion.to(device)# 定義函數,計算精確率def binary_accuracy(preds, y): # 計算準確率rounded_preds = torch.round(torch.sigmoid(preds)) # 把概率的結果 四舍五入correct = (rounded_preds == y).float() # True False -> 轉為 1, 0acc = correct.sum() / len(correct)return acc # 返回精確率#定義函數,訓練模型def train(model, iterator, optimizer, criterion):epoch_loss = 0epoch_acc = 0model.train() # 設置模型標志,保證Dropout在訓練模式下for batch in iterator: # 遍歷數據集進行訓練optimizer.zero_grad()predictions = model(batch.text).squeeze(1) # 在第1個維度上去除維度loss = criterion(predictions, batch.label) # 計算損失acc = binary_accuracy(predictions, batch.label) # 計算精確率loss.backward() # 損失函數反向optimizer.step() # 優化處理epoch_loss += loss.item() # 統計損失epoch_acc += acc.item() # 統計精確率return epoch_loss / len(iterator), epoch_acc / len(iterator)# 定義函數,評估模型def evaluate(model, iterator, criterion):epoch_loss = 0epoch_acc = 0model.eval() # 設置模型標志,保證Dropout在評估模型下with torch.no_grad(): # 禁止梯度計算for batch in iterator:predictions = model(batch.text).squeeze(1) # 計算結果loss = criterion(predictions, batch.label) # 計算損失acc = binary_accuracy(predictions, batch.label) # 計算精確率epoch_loss += loss.item()epoch_acc += acc.item()return epoch_loss / len(iterator), epoch_acc / len(iterator)# 定義函數,計算時間差def epoch_time(start_time, end_time):elapsed_time = end_time - start_timeelapsed_mins = int(elapsed_time / 60)elapsed_secs = int(elapsed_time - (elapsed_mins * 60))return elapsed_mins, elapsed_secsN_EPOCHS = 100 # 設置訓練的迭代次數best_valid_loss = float('inf') # 設置損失初始值,用于保存最優模型for epoch in range(N_EPOCHS): # 按照迭代次數進行訓練start_time = time.time()train_loss, train_acc = train(model, train_iterator, optimizer, criterion)valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)end_time = time.time()# 計算迭代時間消耗epoch_mins, epoch_secs = epoch_time(start_time, end_time)if valid_loss < best_valid_loss: # 保存最優模型best_valid_loss = valid_losstorch.save(model.state_dict(), 'textcnn-model.pt')# 輸出訓練結果print(f'Epoch: {epoch + 1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')print(f'\t訓練損失: {train_loss:.3f} | 訓練精確率: {train_acc * 100:.2f}%')print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc * 100:.2f}%')# 測試模型效果model.load_state_dict(torch.load('textcnn-model.pt'))test_loss, test_acc = evaluate(model, test_iterator, criterion)print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc * 100:.2f}%')2.9?代碼實現:使用模型進行訓練---TextCNN.py(第8部分)
# 1.8 使用模型進行訓練:編寫模型預測接口函數,對指定句子進行預測。列舉幾個句子輸入模型預測接口函數進行預測,查看預測結果。nlp = spacy.load("en_core_web_sm")# 用spacy加載英文語言包# 定義函數,實現預測接口# (1)將長度不足5的句子用′<pad>'字符補齊。(2)將句子中的單詞轉為索引。# (3)為張量增加維度,以與訓練場景下的輸入形狀保持一致。(4)輸入模型進行預測,并對結果進行Sigmoid計算。因為模型在訓練時,使用的計算損失函數自帶Sigmoid處理,但模型中沒有Sigmoid處理,所以要對結果增加Sigmoid處理。def predict_sentiment(model, sentence, min_len=5): # 設置最小長度為5model.eval() # 設置模型標志,保證Dropout在評估模型下tokenized = nlp.tokenizer(sentence).text.split() #拆分輸入的句子if len(tokenized) < min_len: # 長度不足,在后面填充tokenized += ['<pad>'] * (min_len - len(tokenized))indexed = [TEXT.vocab.stoi[t] for t in tokenized] # 將單詞轉化為索引tensor = torch.LongTensor(indexed).to(device)tensor = tensor.unsqueeze(1) # 為張量增加維度,模擬批次prediction = torch.sigmoid(model(tensor)) # 輸入模型進行預測return prediction.item() # 返回預測結果# 使用句子進行預測:大于0.5為正面評論,小于0.5為負面評論sen = "This film is terrible"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))sen = "This film is great"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))sen = "I like this film very much!"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))3 代碼總覽
3.1?TextCNN.py
# 1.1 引入基礎庫: 固定PyTorch中的隨機種子和GPU運算方式。 import random #引入基礎庫 import time import torch#引入PyTorch庫 import torch.nn as nn import torch.nn.functional as F from torchtext.legacy import data ,datasets,vocab #引入文本處理庫 import spacytorch.manual_seed(1234) # 固定隨機種子,使其每次運行時對權重參數的初始化值一致。 # 固定GPU運算方式:提高GPU的運算效率,通常PyTorch會調用自動尋找最適合當前配置的高效算法進行計算,這一過程會導致每次運算的結果可能出現不一致的情況。 torch.backends.cudnn.deterministic = True # 表明不使用尋找高效算法的功能,使得每次的運算結果一致。[僅GPU有效]# 1.2 用torchtext加載IMDB并拆分為數據集 # IMDB是torchtext庫的內置數據集,可以直接通過torchtext庫中的datasets.MDB進行處理。 # 在處理之前將數據集的字段類型和分詞方法指定正確即可。# 定義字段,并按照指定標記化函數進行分詞 TEXT = data.Field(tokenize = 'spacy',lower=True) # data.Field函數指定數據集中的文本字段用spaCy庫進行分詞處理,并將其統一改為小寫字母。tokenize參數,不設置則默認使用str。 LABEL = data.LabelField(dtype=torch.float)# 加載數據集,并根據IMDB兩個文件夾,返回兩個數據集。 # datasets.MDB.splits()進行數據集的加載。該代碼執行時會在本地目錄的.data文件夾下查找是否有MDB數據集,如果沒有,則下載;如果有,則將其加載到內存。 # 被載入內存的數據集會放到數據集對象train_data與test_data中。 train_data , test_data = datasets.IMDB.splits(text_field=TEXT,label_field=LABEL) print("-----------輸出一條數據-----------") # print(vars(train_data.example[0]),len(train_data.example)) print(vars(train_data.examples[0]),len(train_data.examples)) print("---------------------------")# 將訓練數據集再次拆分 # 從訓練數據中拆分出一部分作為驗證數據集。數據集對象train_data的split方法默認按照70%、30%的比例進行拆分。 train_data,valid_data = train_data.split(random_state = random.seed(1234)) print("訓練數據集: ", len(train_data),"條") print("驗證數據集: ", len(valid_data),"條") print("測試數據集: ", len(test_data),"條")# 1.3 加載預訓練詞向量并進行樣本數據化 # 將數據集中的樣本數據轉化為詞向量,并將其按照指定的批次大小進行組合。 # buld_vocab方法實現文本到詞向量數據的轉化:從數據集對象train_data中取出前25000個高頻詞,并用指定的預訓練詞向量glove.6B.100d進行映射。 TEXT.build_vocab(train_data,max_size=25000,vectors="glove.6B.100d",unk_init = torch.Tensor.normal_) # 將樣本數據轉化為詞向量 # glove.6B.100d為torchtext庫中內置的英文詞向量,主要將每個詞映射成維度為100的浮點型數據,該文件會被下載到本地.vector_cache文件夾下。 LABEL.build_vocab(train_data) # ---start---創建批次數據:將數據集按照指定批次進行組合。 BATCH_SIZE = 64 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device) # ---end---創建批次數據:將數據集按照指定批次進行組合。# 1.4 定義帶有Mish激活函數的TextCNN模型class Mish(nn.Module):def __init__(self):super(Mish, self).__init__()def forward(self,x):x = x * (torch.tanh(F.softplus(x)))return x# 在TextCNN類中,一共有兩個方法: # ①初始化方法.按照指定個數定義多分支卷積層,并將它們統一放在nn.ModuleList數組中。 # ②前向傳播方法:先將輸入數據依次輸入每個分支的卷積層中進行處理,再對處理結果進行最大池化,最后對池化結果進行連接并回歸處理 class TextCNN(nn.Module): #定義TextCNN模型# TextCNN類繼承了nn.Module類,在該類中定義的網絡層列表必須要使用nn.ModuleList進行轉化,才可以被TextCNN類識別。# 如果直接使用列表的話,在訓練模型時無法通過TextCNN類對象的parameters方法獲得權重。# 定義初始化方法def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim,dropout, pad_idx):super().__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx) # 定義詞向量權重# 定義多分支卷積層# 將定義好的多分支卷積層以列表形式存放,以便在前向傳播方法中使用。# 每個分支中卷積核的第一個維度由參數filter_sizes設置,第二個維度都是embedding_dim,即只在縱軸的方向上實現了真正的卷積操作,在橫軸的方向上是全尺度卷積,可以起到一維卷積的效果。self.convs = nn.ModuleList([nn.Conv2d(in_channels = 1,out_channels = n_filters,kernel_size = (fs, embedding_dim))for fs in filter_sizes]) #########注意不能用list# 定義輸出層self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)self.dropout = nn.Dropout(dropout)self.mish = Mish() # 實例化激活函數對象# 定義前向傳播方法def forward(self,text): # 輸入形狀為[sent len,batch size]text = text.permute(1, 0) # 將形狀變為[batch size, sent len]embedded = self.embedding(text) # 對于輸入數據進行詞向量映射,形狀為[batch size, sent len, emb dim]embedded = embedded.unsqueeze(1) # 進行維度變化,形狀為[batch size, 1, sent len, emb dim]# len(filter_sizes)個元素,每個元素形狀為[batch size, n_filters, sent len - filter_sizes[n] + 1]# 多分支卷積處理conved = [self.mish(conv(embedded)).squeeze(3) for conv in self.convs] # 將輸入數據進行多分支卷積處理。該代碼執行后,會得到一個含有len(fiter_sizes)個元素的列表,其中每個元素形狀為[batchsize,n_filters,sentlen-fltersizes[n]+1],該元素最后一個維度的公式是由卷積公式計算而來的。# 對于每個卷積結果進行最大池化操作pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]# 將池化結果進行連接cat = self.dropout(torch.cat(pooled, dim=1)) # 形狀為[batch size, n_filters * len(filter_sizes)]return self.fc(cat) # 輸入全連接,進行回歸輸出# 1.5 用數據集參數實例化模型 if __name__ == '__main__':# 根據處理好的數據集參數對TextCNN模型進行實例化。INPUT_DIM = len(TEXT.vocab) # 25002EMBEDDING_DIM = TEXT.vocab.vectors.size()[1] # 100N_FILTERS = 100 # 定義每個分支的數據通道數量FILTER_SIZES = [3, 4, 5] # 定義多分支卷積中每個分支的卷積核尺寸OUTPUT_DIM = 1 # 定義輸出維度DROPOUT = 0.5 # 定義Dropout丟棄率PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] # 定義填充值:獲取數據集中填充字符對應的索引。在詞向量映射過程中對齊數據時會使用該索引進行填充。# 實例化模型model = TextCNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)# 1.6 用預訓練詞向量初始化模型:將加載好的TEXT字段詞向量復制到模型中,為其初始化。# 復制詞向量model.embedding.weight.data.copy_(TEXT.vocab.vectors)# 將填充的詞向量清0UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) #對未識別詞進行清零處理 :使該詞在詞向量空間中失去意義,目的是防止后面填充字符對原有的詞向量空間進行干擾。model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM) #對填充詞進行清零處理 :使該詞在詞向量空間中失去意義,目的是防止后面填充字符對原有的詞向量空間進行干擾。# 1.7 使用Ranger優化器訓練模型import torch.optim as optim # 引入優化器庫from functools import partial # 引入偏函數庫from ranger import * # 載入Ranger優化器# 為Ranger優化器設置參數opt_func = partial(Ranger, betas=(.9, 0.99), eps=1e-6) # betas=(Momentum,alpha)optimizer = opt_func(model.parameters(), lr=0.004)# 定義損失函數criterion = nn.BCEWithLogitsLoss() # nn.BCEWithLogitsLoss函數是帶有Sigmoid函數的二分類交叉熵,即先對模型的輸出結果進行Sigmoid計算,再對其余標簽一起做Cross_entropy計算。# 分配運算資源model = model.to(device)criterion = criterion.to(device)# 定義函數,計算精確率def binary_accuracy(preds, y): # 計算準確率rounded_preds = torch.round(torch.sigmoid(preds)) # 把概率的結果 四舍五入correct = (rounded_preds == y).float() # True False -> 轉為 1, 0acc = correct.sum() / len(correct)return acc # 返回精確率#定義函數,訓練模型def train(model, iterator, optimizer, criterion):epoch_loss = 0epoch_acc = 0model.train() # 設置模型標志,保證Dropout在訓練模式下for batch in iterator: # 遍歷數據集進行訓練optimizer.zero_grad()predictions = model(batch.text).squeeze(1) # 在第1個維度上去除維度loss = criterion(predictions, batch.label) # 計算損失acc = binary_accuracy(predictions, batch.label) # 計算精確率loss.backward() # 損失函數反向optimizer.step() # 優化處理epoch_loss += loss.item() # 統計損失epoch_acc += acc.item() # 統計精確率return epoch_loss / len(iterator), epoch_acc / len(iterator)# 定義函數,評估模型def evaluate(model, iterator, criterion):epoch_loss = 0epoch_acc = 0model.eval() # 設置模型標志,保證Dropout在評估模型下with torch.no_grad(): # 禁止梯度計算for batch in iterator:predictions = model(batch.text).squeeze(1) # 計算結果loss = criterion(predictions, batch.label) # 計算損失acc = binary_accuracy(predictions, batch.label) # 計算精確率epoch_loss += loss.item()epoch_acc += acc.item()return epoch_loss / len(iterator), epoch_acc / len(iterator)# 定義函數,計算時間差def epoch_time(start_time, end_time):elapsed_time = end_time - start_timeelapsed_mins = int(elapsed_time / 60)elapsed_secs = int(elapsed_time - (elapsed_mins * 60))return elapsed_mins, elapsed_secsN_EPOCHS = 100 # 設置訓練的迭代次數best_valid_loss = float('inf') # 設置損失初始值,用于保存最優模型for epoch in range(N_EPOCHS): # 按照迭代次數進行訓練start_time = time.time()train_loss, train_acc = train(model, train_iterator, optimizer, criterion)valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)end_time = time.time()# 計算迭代時間消耗epoch_mins, epoch_secs = epoch_time(start_time, end_time)if valid_loss < best_valid_loss: # 保存最優模型best_valid_loss = valid_losstorch.save(model.state_dict(), 'textcnn-model.pt')# 輸出訓練結果print(f'Epoch: {epoch + 1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')print(f'\t訓練損失: {train_loss:.3f} | 訓練精確率: {train_acc * 100:.2f}%')print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc * 100:.2f}%')# 測試模型效果model.load_state_dict(torch.load('textcnn-model.pt'))test_loss, test_acc = evaluate(model, test_iterator, criterion)print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc * 100:.2f}%')# 1.8 使用模型進行訓練:編寫模型預測接口函數,對指定句子進行預測。列舉幾個句子輸入模型預測接口函數進行預測,查看預測結果。nlp = spacy.load("en_core_web_sm")# 用spacy加載英文語言包# 定義函數,實現預測接口# (1)將長度不足5的句子用′<pad>'字符補齊。(2)將句子中的單詞轉為索引。# (3)為張量增加維度,以與訓練場景下的輸入形狀保持一致。(4)輸入模型進行預測,并對結果進行Sigmoid計算。因為模型在訓練時,使用的計算損失函數自帶Sigmoid處理,但模型中沒有Sigmoid處理,所以要對結果增加Sigmoid處理。def predict_sentiment(model, sentence, min_len=5): # 設置最小長度為5model.eval() # 設置模型標志,保證Dropout在評估模型下tokenized = nlp.tokenizer(sentence).text.split() #拆分輸入的句子if len(tokenized) < min_len: # 長度不足,在后面填充tokenized += ['<pad>'] * (min_len - len(tokenized))indexed = [TEXT.vocab.stoi[t] for t in tokenized] # 將單詞轉化為索引tensor = torch.LongTensor(indexed).to(device)tensor = tensor.unsqueeze(1) # 為張量增加維度,模擬批次prediction = torch.sigmoid(model(tensor)) # 輸入模型進行預測return prediction.item() # 返回預測結果# 使用句子進行預測:大于0.5為正面評論,小于0.5為負面評論sen = "This film is terrible"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))sen = "This film is great"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))sen = "I like this film very much!"print('\n預測 sen = ', sen)print('預測 結果:', predict_sentiment(model, sen))3.2?ranger.py
#Ranger deep learning optimizer - RAdam + Lookahead combined. #https://github.com/lessw2020/Ranger-Deep-Learning-Optimizer#Ranger has now been used to capture 12 records on the FastAI leaderboard.#This version = 9.3.19 #Credits: #RAdam --> https://github.com/LiyuanLucasLiu/RAdam #Lookahead --> rewritten by lessw2020, but big thanks to Github @LonePatient and @RWightman for ideas from their code. #Lookahead paper --> MZhang,G Hinton https://arxiv.org/abs/1907.08610#summary of changes: #full code integration with all updates at param level instead of group, moves slow weights into state dict (from generic weights), #supports group learning rates (thanks @SHolderbach), fixes sporadic load from saved model issues. #changes 8/31/19 - fix references to *self*.N_sma_threshold; #changed eps to 1e-5 as better default than 1e-8.import math import torch from torch.optim.optimizer import Optimizer, required import itertools as itclass Ranger(Optimizer):def __init__(self, params, lr=1e-3, alpha=0.5, k=6, N_sma_threshhold=5, betas=(.95,0.999), eps=1e-5, weight_decay=0):#parameter checksif not 0.0 <= alpha <= 1.0:raise ValueError(f'Invalid slow update rate: {alpha}')if not 1 <= k:raise ValueError(f'Invalid lookahead steps: {k}')if not lr > 0:raise ValueError(f'Invalid Learning Rate: {lr}')if not eps > 0:raise ValueError(f'Invalid eps: {eps}')#parameter comments:# beta1 (momentum) of .95 seems to work better than .90...#N_sma_threshold of 5 seems better in testing than 4.#In both cases, worth testing on your dataset (.90 vs .95, 4 vs 5) to make sure which works best for you.#prep defaults and init torch.optim basedefaults = dict(lr=lr, alpha=alpha, k=k, step_counter=0, betas=betas, N_sma_threshhold=N_sma_threshhold, eps=eps, weight_decay=weight_decay)super().__init__(params,defaults)#adjustable thresholdself.N_sma_threshhold = N_sma_threshhold#now we can get to work...#removed as we now use step from RAdam...no need for duplicate step counting#for group in self.param_groups:# group["step_counter"] = 0#print("group step counter init")#look ahead paramsself.alpha = alphaself.k = k #radam buffer for stateself.radam_buffer = [[None,None,None] for ind in range(10)]#self.first_run_check=0#lookahead weights#9/2/19 - lookahead param tensors have been moved to state storage. #This should resolve issues with load/save where weights were left in GPU memory from first load, slowing down future runs.#self.slow_weights = [[p.clone().detach() for p in group['params']]# for group in self.param_groups]#don't use grad for lookahead weights#for w in it.chain(*self.slow_weights):# w.requires_grad = Falsedef __setstate__(self, state):print("set state called")super(Ranger, self).__setstate__(state)def step(self, closure=None):loss = None#note - below is commented out b/c I have other work that passes back the loss as a float, and thus not a callable closure. #Uncomment if you need to use the actual closure...#if closure is not None:#loss = closure()#Evaluate averages and grad, update param tensorsfor group in self.param_groups:for p in group['params']:if p.grad is None:continuegrad = p.grad.data.float()if grad.is_sparse:raise RuntimeError('Ranger optimizer does not support sparse gradients')p_data_fp32 = p.data.float()state = self.state[p] #get state dict for this paramif len(state) == 0: #if first time to run...init dictionary with our desired entries#if self.first_run_check==0:#self.first_run_check=1#print("Initializing slow buffer...should not see this at load from saved model!")state['step'] = 0state['exp_avg'] = torch.zeros_like(p_data_fp32)state['exp_avg_sq'] = torch.zeros_like(p_data_fp32)#look ahead weight storage now in state dict state['slow_buffer'] = torch.empty_like(p.data)state['slow_buffer'].copy_(p.data)else:state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32)state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32)#begin computations exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']beta1, beta2 = group['betas']#compute variance mov avgexp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad)#compute mean moving avgexp_avg.mul_(beta1).add_(1 - beta1, grad)state['step'] += 1buffered = self.radam_buffer[int(state['step'] % 10)]if state['step'] == buffered[0]:N_sma, step_size = buffered[1], buffered[2]else:buffered[0] = state['step']beta2_t = beta2 ** state['step']N_sma_max = 2 / (1 - beta2) - 1N_sma = N_sma_max - 2 * state['step'] * beta2_t / (1 - beta2_t)buffered[1] = N_smaif N_sma > self.N_sma_threshhold:step_size = math.sqrt((1 - beta2_t) * (N_sma - 4) / (N_sma_max - 4) * (N_sma - 2) / N_sma * N_sma_max / (N_sma_max - 2)) / (1 - beta1 ** state['step'])else:step_size = 1.0 / (1 - beta1 ** state['step'])buffered[2] = step_sizeif group['weight_decay'] != 0:p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32)if N_sma > self.N_sma_threshhold:denom = exp_avg_sq.sqrt().add_(group['eps'])p_data_fp32.addcdiv_(-step_size * group['lr'], exp_avg, denom)else:p_data_fp32.add_(-step_size * group['lr'], exp_avg)p.data.copy_(p_data_fp32)#integrated look ahead...#we do it at the param level instead of group levelif state['step'] % group['k'] == 0:slow_p = state['slow_buffer'] #get access to slow param tensorslow_p.add_(self.alpha, p.data - slow_p) #(fast weights - slow weights) * alphap.data.copy_(slow_p) #copy interpolated weights to RAdam param tensorreturn loss總結
以上是生活随笔為你收集整理的【Pytorch神经网络实战案例】40 TextCNN模型分析IMDB数据集评论的积极与消极的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机程序设计基础试题与答案,2018年
- 下一篇: python游戏开发工程师_Python