概述
- 學習如何使用PyTorch執行文本分類
- 理解解決文本分類時所涉及的要點
- 學習使用包填充(Pack Padding)特性
介紹
我總是使用最先進的架構來在一些比賽提交模型結果。得益於PyTorch、Keras和TensorFlow等深度學習框架,實現最先進的體系結構變得非常容易。這些框架提供了一種簡單的方法來實現複雜的模型體系結構和算法,而只需要很少的概念知識和代碼技能。簡而言之,它們是數據科學社區的一座金礦!
在本文中,我們將使用PyTorch,它以其快速的計算能力而聞名。因此,在本文中,我們將介紹解決文本分類問題的關鍵點。然後我們將在PyTorch中實現第一個文本分類器!
目錄
- 為什麼使用PyTorch進行文本分類? 處理詞彙表外單詞 處理可變長度序列 包裝器和預訓練模型
- 理解問題
- 實現文本分類
為什麼使用PyTorch進行文本分類?
在深入研究技術概念之前,讓我們先快速熟悉一下將要使用的框架——PyTorch。PyTorch的基本單位是張量,類似於python中的「numpy」數組。使用PyTorch有很多好處,但最重要的兩個是:
- 動態網絡——運行時架構的變化
- 跨gpu的分布式訓練
我敢肯定你想知道——為什麼我們要使用PyTorch來處理文本數據?讓我們討論一下PyTorch的一些令人難以置信的特性,這些特性使它不同於其他框架,特別是在處理文本數據時。
1. 處理詞彙表外單詞
文本分類模型根據固定的詞彙量進行訓練。但在推理過程中,我們可能會遇到一些詞彙表中沒有的詞。這些詞彙被稱為詞彙量外單詞(Out of Vocabulary),大多數深度學習框架缺乏處理詞彙量不足的能力。這是一個關鍵的問題,甚至可能導致信息的丟失。
為了處理詞彙量不足的單詞,PyTorch支持一個很好的功能,它用未知的token替換訓練數據中的稀有單詞。這反過來又幫助我們解決了詞彙量不足的問題。
除了處理詞彙之外,PyTorch還有一個可以處理可變長度序列的特性!
2. 處理可變長度序列
你聽說過循環神經網絡是如何處理可變長度序列的嗎?有沒有想過如何實現它?PyTorch提供了一個有用的特性「填充序列」(Packed Padding sequence),它實現了動態循環神經網絡。
填充是在句首或句尾添加一個稱為填充標記的額外標記的過程。由於每個句子中的單詞數量不同,我們通過添加填充標記將可變長度的輸入句子轉換為具有相同長度的句子。
填充是必須的,因為大多數框架支持靜態網絡,即架構在整個模型訓練過程中保持不變。雖然填充解決了可變長度序列的問題,但是這種思想還有另一個問題——體系結構現在像處理任何其他信息/數據一樣處理這些填充標記。讓我用一個簡單的圖表來解釋一下
正如你在下圖中所看到的,在生成輸出時還使用了最後一個元素,即padding標記。這是由PyTorch中的填充序列來處理的。
壓縮填充會對填充標記忽略輸入時間步。這些值不輸入給循環神經網絡,這幫助我們建立動態循環神經網絡。
3.包裝器和預訓練模型
最新的模型架構狀態正在為PyTorch框架發布。Hugging Face發布Transformers,其中提供超過32個自然語言理解生成的最新架構!
不僅如此,PyTorch還為文本到語音、對象檢測等任務提供了預訓練模型,這些任務可以在幾行代碼內執行。
不可思議,不是嗎?這些是PyTorch的一些非常有用的特性。現在讓我們使用PyTorch解決一個文本分類問題。
理解問題陳述
作為本文的一部分,我們將研究一個非常有趣的問題。
Quora希望在他們的平台上追蹤不真誠的問題,以便讓用戶在分享知識的同時感到安全。在這種情況下,一個不真誠的問題被定義為一個旨在發表聲明的問題,而不是尋找有用的答案。為了進一步分析這個問題,這裡有一些特徵可以表明一個特定的問題是不真誠的:
- 語氣非中性
- 是貶低還是煽動性的
- 沒有現實根據
- 使用性內容(亂倫、獸交、戀童癖)來達到令人震驚的效果,而不是尋求真正的答案
訓練數據包括被詢問的問題,以及一個表示是否被識別為不真誠的標記(target = 1)。標籤包含一些噪音,即它們不能保證是完美的。我們的任務是識別某個問題是否「不真誠」。你可以從這裡下載數據集。
https://drive.google.com/file/d/1fcip8PgsrX7m4AFgvUPLaac5pZ79mpwX/view?usp=drive_open
現在是使用PyTorch編寫我們自己的文本分類模型的時候了。
實現文本分類
讓我們首先導入構建模型所需的所有必要庫。下面是我們將使用的包/庫的簡要概述
- Torch包用於定義張量和張量上的數學運算
- torchtext是PyTorch中的一個自然語言處理(NLP)庫。這個庫包含預處理文本的腳本和一些流行的NLP數據集的源。
#導入庫import torch #處理數據from torchtext import data
為了使結果可重複,我指定了種子值。由於深度學習模型在執行時由於其隨機性可能會產生不同的結果,因此指定種子值是很重要的。
#產生同樣的結果SEED = 2019#Torchtorch.manual_seed(SEED)#Cuda 算法torch.backends.cudnn.deterministic = True
預處理數據:
現在,讓我們看看如何使用欄位對象對文本進行預處理。欄位對象有兩種不同的類型——field和LabelField。讓我們快速了解一下兩者之間的區別
- field:數據模塊中的欄位對象用於為數據集中的每一列指定預處理步驟。
- LabelField: LabelField對象是Field對象的一個特例,它只用於分類任務。它的惟一用途是默認將unk_token和sequential設置為None。
在我們使用field之前,讓我們看看field的不同參數和它們的用途。
field的參數:
- Tokenize:指定標記句子的方法,即將句子分詞。我正在使用spacy分詞器,因為它使用了新的分詞算法
- Lower:將文本轉換為小寫
batch_first:輸入和輸出的第一個維度總是批處理大小
接下來,我們將創建一個元組列表,其中每個元組中的第一個值包含一個列名,第二個值是上面定義的欄位對象。此外,我們將按照csv列的順序排列每個元組,並指定為(None,None)以忽略csv文件中的列。讓我們只讀需要的列-問題和標籤
fields = [(None, None), ('text',TEXT),('label', LABEL)]
在下面的代碼塊中,我通過定義欄位對象加載了自定義數據集。
#載入自定義數據集training_data=data.TabularDataset(path = 'quora.csv',format = 'csv',fields = fields,skip_header = True)print(vars(training_data.examples[0]))
現在,讓我們將數據集分為訓練和驗證數據
import randomtrain_data, valid_data = training_data.split(split_ratio=0.3, random_state = random.seed(SEED))
準備輸入和輸出序列:
下一步是為文本構建詞彙表,並將它們轉換為整數序列。詞彙表包含了整篇文章中出現的詞彙。每個唯一的單詞都有一個索引。下面列出了相同的參數
參數:
- min_freq:忽略詞彙表中頻率小於指定頻率的單詞,並將其映射到未知標記。
- 兩個特殊的標記(稱為unknown和padding)將被添加到詞彙表中 unknown標記用於處理詞彙表中的單詞 padding標記用於生成相同長度的輸入序列
讓我們構建詞彙表,並使用預訓練好的嵌入來初始化單詞。如果希望隨機初始化嵌入,請忽略vectors參數。
#初始化glove embeddingsTEXT.build_vocab(train_data,min_freq=3,vectors = "glove.6B.100d") LABEL.build_vocab(train_data)print("Size of TEXT vocabulary:",len(TEXT.vocab))print("Size of LABEL vocabulary:",len(LABEL.vocab))print(TEXT.vocab.freqs.most_common(10)) print(TEXT.vocab.stoi)
現在我們準備批訓練模型。BucketIterator以需要最小填充量的方式形成批。
#檢查cuda是否可用device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') #設置batch大小BATCH_SIZE = 64#載入疊代器train_iterator, valid_iterator = data.BucketIterator.splits( (train_data, valid_data), batch_size = BATCH_SIZE, sort_key = lambda x: len(x.text), sort_within_batch=True, device = device)
模型架構
現在是定義體系結構來解決二分類問題的時候了。torch中的神經網絡模塊是所有模型的基礎模型。這意味著每個模型都必須是nn模塊的子類。
我在這裡定義了兩個函數:init和forward。讓我來解釋一下這兩個函數的用例
- Init:每當創建類的實例時,都會自動調用Init函數。因此,它被稱為構造函數。傳遞給類的參數由構造函數初始化。我們將定義將在模型中使用的所有層
- Forward: Forward函數定義輸入的前向傳播。
最後,讓我們詳細了解用於構建體系結構的不同層及其參數
嵌入層:嵌入對於任何與NLP相關的任務都是非常重要的,因為它以向量格式表示一個單詞。嵌入層創建一個查找表,其中每一行表示一個單詞的嵌入。嵌入層將整數序列轉換成向量表示。這裡是嵌入層兩個最重要的參數-
- num_embeddings:字典中的單詞數量
- embedding_dim:單詞的維度
LSTM: LSTM是RNN的一個變體,能夠捕獲長期依賴項。遵循你應該熟悉的LSTM的一些重要參數。以下是這一層的參數:
- input_size:輸入的維度
- hidden_size:隱藏節點的數量
- num_layers:要堆疊的層數
- batch_first:如果為真,則輸入和輸出張量以(batch, seq, feature)的形式提供。
- dropout:如果非零,則在除最後一層外的每一LSTM層的輸出上引入一個dropout層,dropout機率等於dropout。默認值:0
- bidirection:如果為真,則引入雙向LSTM
線性層:線性層是指Dense層。這裡的兩個重要參數如下:
- in_features:輸入的特徵數量
- out_features:隱藏層的節點數量
包填充:如前所述,包填充用於定義動態循環神經網絡。如果沒有填充包,填充輸入也由rnn處理,並返回填充元素的隱狀態。這是一個非常棒的包裝器,它不顯示填充的輸入。它只是忽略這些值並返回未填充元素的隱藏狀態。
現在我們已經很好地理解了架構的所有塊,讓我們來看代碼!我將從定義架構的所有層開始:
import torch.nn as nnclass classifier(nn.Module): #定義所有層 def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout): super().__init__() #embedding 層 self.embedding = nn.Embedding(vocab_size, embedding_dim) #lstm 層 self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers, bidirectional=bidirectional, dropout=dropout, batch_first=True) #全連接層 self.fc = nn.Linear(hidden_dim * 2, output_dim) #激活函數 self.act = nn.Sigmoid() def forward(self, text, text_lengths): #text = [batch size,sent_length] embedded = self.embedding(text) #embedded = [batch size, sent_len, emb dim] packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths,batch_first=True) packed_output, (hidden, cell) = self.lstm(packed_embedded) #hidden = [batch size, num layers * num directions,hid dim] #cell = [batch size, num layers * num directions,hid dim] #連接最後的正向和反向隱狀態 hidden = torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1) #hidden = [batch size, hid dim * num directions] dense_outputs=self.fc(hidden) #激活 outputs=self.act(dense_outputs) return outputs
下一步是定義超參數並實例化模型。下面是相同的代碼塊:
#定義超參數size_of_vocab = len(TEXT.vocab)embedding_dim = 100num_hidden_nodes = 32num_output_nodes = 1num_layers = 2bidirection = Truedropout = 0.2#實例化模型model = classifier(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers, bidirectional = True, dropout = dropout)
讓我們看看模型摘要,並使用預先訓練好的嵌入來初始化嵌入層
#模型體系print(model)def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) print(f'The model has {count_parameters(model):,} trainable parameters')#初始化預訓練embeddingpretrained_embeddings = TEXT.vocab.vectorsmodel.embedding.weight.data.copy_(pretrained_embeddings)print(pretrained_embeddings.shape)
這裡我已經為模型定義了優化器,損失和度量:
import torch.optim as optim#定義優化器和損失optimizer = optim.Adam(model.parameters())criterion = nn.BCELoss()#定義度量def binary_accuracy(preds, y): #四捨五入到最接近的整數 rounded_preds = torch.round(preds) correct = (rounded_preds == y).float() acc = correct.sum() / len(correct) return acc #如果cuda可用model = model.to(device)criterion = criterion.to(device)
構建模型分為兩個階段:
- 訓練階段:model.train()將模型設置在訓練階段,並激活dropout層。
- 推理階段:model.eval()將模型設置在評估階段,並停用dropout層。
下面是定義用於訓練模型的函數的代碼塊
def train(model, iterator, optimizer, criterion): #初始化 epoch_loss = 0 epoch_acc = 0 #設置為訓練模式 model.train() for batch in iterator: #在每一個batch後設置0梯度 optimizer.zero_grad() text, text_lengths = batch.text #轉換成一維張量 predictions = model(text, text_lengths).squeeze() #計算損失 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 = 0 epoch_acc = 0 #停用dropout層 model.eval() #取消autograd with torch.no_grad(): for batch in iterator: text, text_lengths = batch.text #轉換為一維張量 predictions = model(text, text_lengths).squeeze() #計算損失和準確性 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)
最後,我們將對模型進行若干個epoch的訓練,並在每個epoch保存最佳模型。
N_EPOCHS = 5best_valid_loss = float('inf')for epoch in range(N_EPOCHS): #訓練模型 train_loss, train_acc = train(model, train_iterator, optimizer, criterion) #評估模型 valid_loss, valid_acc = evaluate(model, valid_iterator, criterion) #保存最佳模型 if valid_loss < best_valid_loss: best_valid_loss = valid_loss torch.save(model.state_dict(), 'saved_weights.pt') print(f'\\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%') print(f'\\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
讓我們加載最佳模型並定義接受用戶定義的輸入並進行預測的推理函數
#載入權重path='/content/saved_weights.pt'model.load_state_dict(torch.load(path));model.eval();#推理 import spacynlp = spacy.load('en')def predict(model, sentence): tokenized = [tok.text for tok in nlp.tokenizer(sentence)] #標記句子 indexed = [TEXT.vocab.stoi[t] for t in tokenized] #轉換為整數序列 length = [len(indexed)] tensor = torch.LongTensor(indexed).to(device) #轉換為tensor tensor = tensor.unsqueeze(1).T length_tensor = torch.LongTensor(length) #轉換為tensor prediction = model(tensor, length_tensor) #預測 return prediction.item()
讓我們用這個模型來預測幾個問題:
#作出預測predict(model, "Are there any sports that you don't like?")#不真誠的問題predict(model, "Why Indian girls go crazy about marrying Shri. Rahul Gandhi ji?")
結尾
我們已經看到了如何在PyTorch中構建自己的文本分類模型,並了解了包填充的重要性。
你可以嘗試使用調試LSTM模型的超參數,並嘗試進一步提高準確性。一些要調優的超參數可以是LSTM層的數量、每個LSTM單元中的隱藏單元的數量等等。