選自realworldnlpbook
作者:Masato Hagiwara
機器之心編譯
參與:Geek AI、路
本文介紹了如何利用 AllenNLP,使用不到一百行代碼訓練情感分類器。
什麼是情感分析?
情感分析是一種流行的文本分析技術,用來對文本中的主觀信息進行自動識別和分類。它被廣泛用於量化觀點、情感等通常以非結構化方式記錄的信息,而這些信息也因此很難用其他方式量化。情感分析技術可被用於多種文本資源,例如調查報告、評論、社交媒體上的帖子等。
情感分析最基本的任務之一是極性分類,換句話說,該任務需要判斷語言所表達的觀點是正面的、負面的還是中性的。具體而言,可能有三個以上的類別,例如:極其正面、正面、中性、消極、極其消極。這有些類似於你使用某些網站時的評價行為(比如 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 classificationword_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
句子分類模型
LSTM-RNN 句子分類模型
現在,我們來定義一個句子分類模型。這段代碼看起來很多,但是別擔心,我在代碼片段中添加了大量注釋:
# Model in AllenNLP represents a model that is trained.classLstmClassifier(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.defforward(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 isnotNone: 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」對應的是「非常積極」。所以你剛剛訓練的模型正確地預測出了這是一個非常正面的電影評論。
原文連結:http://www.realworldnlpbook.com/blog/training-sentiment-analyzer-using-allennlp.html
本文為機器之心編譯,轉載請聯繫本公眾號獲得授權。