1. 程式人生 > >教程|百行Python程式碼訓練情感分類器,機器之心!

教程|百行Python程式碼訓練情感分類器,機器之心!

 

情感分析是一種流行的文字分析技術,用來對文字中的主觀資訊進行自動識別和分類。它被廣泛用於量化觀點、情感等通常以非結構化方式記錄的資訊,而這些資訊也因此很難用其他方式量化。情感分析技術可被用於多種文字資源,例如調查報告、評論、社交媒體上的帖子等。

教程|百行Python程式碼訓練情感分類器,機器之心!

 

 

情感分析最基本的任務之一是極性分類,換句話說,該任務需要判斷語言所表達的觀點是正面的、負面的還是中性的。具體而言,可能有三個以上的類別,例如:極其正面、正面、中性、消極、極其消極。這有些類似於你使用某些網站時的評價行為(比如 Amazon),人們可以用星星數表示 5 個等級來對物品進行評論(產品、電影或其他任何東西)。

斯坦福的情感分析樹庫(TreeBank)

目前,研究人員釋出了一些公開的情感分類資料集。在本文中,我們將使用斯坦福的情感分析樹庫(或稱 SST),這可能是最廣為使用的情感分析資料集之一。SST 與其它資料集最大的不同之處是,在 SST 中情感標籤不僅被分配到句子上,句子中的每個短語和單詞也會帶有情感標籤。這使我們能夠研究單詞和短語之間複雜的語義互動。例如,對下面這個句子的極性進行分析:

This movie was actually neither that funny, nor super witty.

 

這個句子肯定是消極的。但如果只看單個單詞(「funny」、「witty」)可能會被誤導,認為它的情感是積極的。只關注單個單詞的樸素詞袋分類器很難對上面的例句進行正確的分類。要想正確地對上述例句的極性進行分類,你需要理解否定詞(neither ... nor ...)對語義的影響。由於 SST 具備這樣的特性,它被用作獲取句子句法結構的神經網路模型的標準對比基準(https://nlp.stanford.edu/~socherr/EMNLP2013_RNTN.pdf)。

 

Pytorch 和 AllenNLP

PyTorch 是我最喜歡的深度學習框架。它提供了靈活、易於編寫的模組,可動態執行,且速度相當快。在過去一年中,PyTorch 在科研社群中的使用實現了爆炸性增長。

儘管 PyTorch 是一個非常強大的框架,但是自然語言處理往往涉及底層的公式化的事務處理,包括但不限於:閱讀和編寫資料集、分詞、建立單詞索引、詞彙管理、mini-batch 批處理、排序和填充等。儘管在 NLP 任務中正確地使用這些構建塊是至關重要的,但是當你快速迭代時,你需要一次又一次地編寫類似的設計模式,這會浪費很多時間。而這正是 AllenNLP 這類庫的亮點所在。

AllenNLP 是艾倫人工智慧研究院開發的開源 NLP 平臺。它的設計初衷是為 NLP 研究和開發(尤其是語義和語言理解任務)的快速迭代提供支援。它提供了靈活的 API、對 NLP 很實用的抽象,以及模組化的實驗框架,從而加速 NLP 的研究進展。

本文將向大家介紹如何使用 AllenNLP 一步一步構建自己的情感分類器。由於 AllenNLP 會在後臺處理好底層事務,提供訓練框架,所以整個指令碼只有不到 100 行 Python 程式碼,你可以很容易地使用其它神經網路架構進行實驗。

程式碼地址:https://github.com/mhagiwara/realworldnlp/blob/master/examples/sentiment/sst_classifier.py

 

接下來,下載 SST 資料集,你需要將資料集分割成 PTB 樹格式的訓練集、開發集和測試集,你可以通過下面的連結直接下載:https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip。我們假設這些檔案是在 data/stanfordSentimentTreebank/trees 下進行擴充套件的。

 

注意,在下文的程式碼片段中,我們假設你已經匯入了合適的模組、類和方法(詳情參見完整指令碼)。你會注意到這個指令碼和 AllenNLP 的詞性標註教程非常相似——在 AllenNLP 中很容易在只進行少量修改的情況下使用不同的模型對不同的任務進行實驗。

資料集讀取和預處理

AllenNLP 已經提供了一個名為 StanfordSentimentTreeBankDatasetReader 的便捷資料集讀取器,它是一個讀取 SST 資料集的介面。你可以通過將資料集檔案的路徑指定為為 read() 方法的引數來讀取資料集:

reader = StanfordSentimentTreeBankDatasetReader()
train_dataset = reader.read('data/stanfordSentimentTreebank/trees/train.txt')
dev_dataset = reader.read('data/stanfordSentimentTreebank/trees/dev.txt')

 

幾乎任何基於深度學習的 NLP 模型的第一步都是指定如何將文字資料轉換為張量。該工作包括把單詞和標籤(在本例中指的是「積極」和「消極」這樣的極性標籤)轉換為整型 ID。在 AllenNLP 中,該工作是由 Vocabulary 類來處理的,它儲存從單詞/標籤到 ID 的對映。

# You can optionally specify the minimum count of tokens/labels.
# `min_count={'tokens':3}` here means that any tokens that appear less than three times
# will be ignored and not included in the vocabulary.
vocab = Vocabulary.from_instances(train_dataset + dev_dataset,
 min_count={'tokens': 3})

 

下一步是將單詞轉換為嵌入。在深度學習中,嵌入是離散、高維資料的連續向量表徵。你可以使用 Embedding 建立這樣的對映,使用 BasicTextFieldEmbedder 將 ID 轉換為嵌入向量。

token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'),
 embedding_dim=EMBEDDING_DIM)
# BasicTextFieldEmbedder takes a dict - we need an embedding just for tokens,
# not for labels, which are used unchanged as the answer of the sentence classification
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})

 

句子分類模型

 

教程|百行Python程式碼訓練情感分類器,機器之心!

 

 

LSTM-RNN 句子分類模型

現在,我們來定義一個句子分類模型。這段程式碼看起來很多,但是別擔心,我在程式碼片段中添加了大量註釋:

# Model in AllenNLP represents a model that is trained.
class LstmClassifier(Model):
 def __init__(self,
 word_embeddings: TextFieldEmbedder,
 encoder: Seq2VecEncoder,
 vocab: Vocabulary) -> None:
 super().__init__(vocab)
 # We need the embeddings to convert word IDs to their vector representations
 self.word_embeddings = word_embeddings
 # Seq2VecEncoder is a neural network abstraction that takes a sequence of something
 # (usually a sequence of embedded word vectors), processes it, and returns it as a single
 # vector. Oftentimes, this is an RNN-based architecture (e.g., LSTM or GRU), but
 # AllenNLP also supports CNNs and other simple architectures (for example,
 # just averaging over the input vectors).
 self.encoder = encoder
 # After converting a sequence of vectors to a single vector, we feed it into
 # a fully-connected linear layer to reduce the dimension to the total number of labels.
 self.hidden2tag = torch.nn.Linear(in_features=encoder.get_output_dim(),
 out_features=vocab.get_vocab_size('labels'))
 self.accuracy = CategoricalAccuracy()
 # We use the cross-entropy loss because this is a classification task.
 # Note that PyTorch's CrossEntropyLoss combines softmax and log likelihood loss,
 # which makes it unnecessary to add a separate softmax layer.
 self.loss_function = torch.nn.CrossEntropyLoss()
 # Instances are fed to forward after batching.
 # Fields are passed through arguments with the same name.
 def forward(self,
 tokens: Dict[str, torch.Tensor],
 label: torch.Tensor = None) -> torch.Tensor:
 # In deep NLP, when sequences of tensors in different lengths are batched together,
 # shorter sequences get padded with zeros to make them of equal length.
 # Masking is the process to ignore extra zeros added by padding
 mask = get_text_field_mask(tokens)
 # Forward pass
 embeddings = self.word_embeddings(tokens)
 encoder_out = self.encoder(embeddings, mask)
 logits = self.hidden2tag(encoder_out)
 # In AllenNLP, the output of forward() is a dictionary.
 # Your output dictionary must contain a "loss" key for your model to be trained.
 output = {"logits": logits}
 if label is not None:
 self.accuracy(logits, label)
 output["loss"] = self.loss_function(logits, label)
 return output

 

這裡的關鍵是 Seq2VecEncoder,它基本上使用張量序列作為輸入,然後返回一個向量。我們在這裡使用 LSTM-RNN 作為編碼器(如有需要,可參閱文件 https://allenai.github.io/allennlp-docs/api/allennlp.modules.seq2vec_encoders.html#allennlp.modules.seq2vec_encoders.pytorch_seq2vec_wrapper.PytorchSeq2VecWrapper)。

lstm = PytorchSeq2VecWrapper(
 torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
model = LstmClassifier(word_embeddings, lstm, vocab)

 

訓練

一旦你定義了這個模型,其餘的訓練過程就很容易了。這就是像 AllenNLP 這樣的高階框架的亮點所在。你只需要指定如何進行資料迭代並將必要的引數傳遞給訓練器,而無需像 PyTorch 和 TensorFlow 那樣編寫冗長的批處理和訓練迴圈。

optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)
iterator = BucketIterator(batch_size=32, sorting_keys=[("tokens", "num_tokens")])
iterator.index_with(vocab)
trainer = Trainer(model=model,
 optimizer=optimizer,
 iterator=iterator,
 train_dataset=train_dataset,
 validation_dataset=dev_dataset,
 patience=10,
 num_epochs=20)
trainer.train()

 

這裡的 BucketIterator 會根據 token 的數量對訓練例項進行排序,從而使得長度類似的例項在同一個批中。注意,我們使用了驗證集,在測試誤差過大時採用了早停法避免過擬合。

如果將上面的程式碼執行 20 個 epoch,則模型在訓練集上的準確率約為 0.78,在驗證集上的準確率約為 0.35。這聽起來很低,但是請注意,這是一個 5 類的分類問題,隨機基線的準確率只有 0.20。

測試

為了測試剛剛訓練的模型是否如預期,你需要構建一個預測器(predictor)。predictor 是一個提供基於 JSON 的介面的類,它被用於將輸入資料傳遞給你的模型或將輸出資料從模型中匯出。接著,我便寫了一個句子分類預測器(https://github.com/mhagiwara/realworldnlp/blob/master/realworldnlp/predictors.py#L10),將其用作句子分類模型的基於 JSON 的介面。

 

tokens = ['This', 'is', 'the', 'best', 'movie', 'ever', '!']
predictor = SentenceClassifierPredictor(model, dataset_reader=reader)
logits = predictor.predict(tokens)['logits']
label_id = np.argmax(logits)
print(model.vocab.get_token_from_index(label_id, 'labels'))

 

執行這段程式碼後,你應該看到分類結果為「4」。「4」對應的是「非常積極」。所以你剛剛訓練的模型正確地預測出了這是一個非常正面的電影評論。