【AllenNLP學習筆記】1.基於LSTM的詞性標註(POS)

github:https://github.com/JackKuo666/NLP_Learning_Way/blob/master/AllenNLP_notes.md

from typing import Iterator, List, Dict

import torch
import torch.optim as optim
import numpy as np

from allennlp.data import Instance
from allennlp.data.fields import TextField,SequenceLabelField
'''
在AllenNLP中,我們將每個訓練實例(example)表示爲包含各種類型的字段的Instance。
這裏每個實例(example)都有一個包含句子的TextField,
以及一個包含相應詞性標籤的SequenceLabelField
'''

from allennlp.data.dataset_readers import DatasetReader
'''
通常使用AllenNLP來解決這樣的問題,您必須實現兩個類(class)。
第一個是DatasetReader,它包含用於讀取數據文件和生成實例流(Instances)的邏輯。
'''

from allennlp.common.file_utils import cached_path
'''
我們經常要從URL加載數據集或模型。 cached_pa​​th幫助程序下載此類文件,在本地緩存它們,並返回本地路徑。它還接受本地文件路徑(它只是按原樣返回)。
'''

from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer
from allennlp.data.tokenizers import Token
'''
有多種方法可以將單詞表示爲一個或多個索引。例如,您可以維護唯一單詞的詞彙表,併爲每個單詞指定相應的ID。
或者你可能在單詞中每個字符有一個id,並將每個單詞表示爲一系列id。 AllenNLP使用具有TokenIndexer抽象的表示。
'''

from allennlp.data.vocabulary import Vocabulary
'''
TokenIndexer表示如何將token轉換爲索引的規則,而Vocabulary包含從字符串到整數的相應映射。
例如,您的token索引器可能指定將token表示爲字符ID序列,在這種情況下,Vocabulary將包含映射{character - > id}。
在另外一個特定的例子中,我們使用SingleIdTokenIndexer爲每個token分配一個唯一的id,因此Vocabulary只包含一個映射{token - > id}(以及反向映射)。
'''

from allennlp.models import Model
'''
除了DatasetReader之外,你通常需要實現的另一個類是Model,
它是一個PyTorch模塊,它接受張量輸入併產生張量輸出的dict(包括你想要優化的訓練損失)。
'''

from allennlp.modules.text_field_embedders import TextFieldEmbedder, BasicTextFieldEmbedder
from allennlp.modules.token_embedders import Embedding
from allennlp.modules.seq2seq_encoders import Seq2SeqEncoder, PytorchSeq2SeqWrapper
from allennlp.nn.util import get_text_field_mask, sequence_cross_entropy_with_logits
'''
如上所述,我們的模型將包含一個嵌入層(embedding layer),然後是LSTM,然後是前饋層。 
AllenNLP包括所有這些智能處理填充(padding)和批處理(batching)的抽象,以及各種實用功能。
'''

from allennlp.training.metrics import CategoricalAccuracy
'''
我們希望跟蹤訓練(training)和驗證(validation)數據集的準確性(accuracy)。
'''

from allennlp.data.iterators import BucketIterator
'''
在我們的訓練中,我們需要一個可以智能地批量處理數據的DataIterators。
'''

from allennlp.training.trainer import Trainer
'''
我們將使用AllenNLP的全功能training。
'''

from allennlp.predictors import SentenceTaggerPredictor
'''
最後,我們想要對新輸入做出預測,下面將詳細介紹。
'''
torch.manual_seed(1)
'''
設定生成隨機數的種子,並返回一個torch._C.Generator對象
'''


# ===================一、我們需要編寫的準備數據集類======================
class PosDatasetReader(DatasetReader):
    """
    DatasetReader for PoS tagging data, one sentence per line, like

        The###DET dog###NN ate###V the###DET apple###NN
    """
    def __init__(self, token_indexers:Dict[str, TokenIndexer] = None) -> None:
        super().__init__(lazy=False)
        self.token_indexers = token_indexers or {"tokens": SingleIdTokenIndexer()}
        '''
        我們的DatasetReader需要的唯一參數是TokenIndexers的dict,它指定如何將tokens轉換爲索引。
        默認情況下,我們只爲每個token(我們稱之爲“tokens”)生成一個索引,這只是每個不同token的唯一ID。 
        (這只是您在大多數NLP任務中使用的標準“索引詞”映射。)
        '''


    def text_to_instance(self, tokens: List[Token], tags: List[str] = None) -> Instance:
        sentence_field = TextField(tokens, self.token_indexers)
        fields = {"sentence": sentence_field}

        if tags:
            label_field = SequenceLabelField(labels=tags, sequence_field=sentence_field)
            fields["labels"] = label_field

        return Instance(fields)
    '''
    DatasetReader.text_to_instance獲取與訓練example相對應的輸入(在這種情況下是句子的tokens和相應的詞性標籤(part-of-speech tags)),
    實例化相應的Fields(在這種情況下是句子的TextField和其標籤的SequenceLabelField),並返回包含這些字段(Fields)的實例(Instance)。
    請注意,tags是可選的,因爲我們希望能夠從未標記的數據創建實例(instances)以對它們進行預測。 
    '''

    def _read(self, file_path: str) -> Iterator[Instance]:
        with open(file_path) as f:
            for line in f:
                pairs = line.strip().split()
                sentence, tags = zip(*(pair.split("###") for pair in pairs))
                yield self.text_to_instance([Token(word) for word in sentence], tags)

        '''
        我們必須實現的另一個部分是_read,它接受一個文件名並生成一個實例流(Instances)。大部分工作已經在text_to_instance中完成。
        '''


# ===================二、我們需要編寫的模型部分(Model)類======================
class LstmTagger(Model):
    '''
    您基本上必須實現的另一個類是Model,它是torch.nn.Module的子類。
    它的工作原理在很大程度上取決於你,它主要只是需要一個前向方法(forward method),它接受張量輸入併產生一個張量輸出的字典,
    其中包括你用來訓練模型的損失(losss)。
    如上所述,我們的模型將包括嵌入層(embedding layer),序列編碼器(sequence encoder)和前饋網絡(feedforward network)。
    '''

    '''
    可能看似不尋常的一件事是我們將嵌入器(embedder)和序列編碼器(sequence encoder)作爲構造函數參數(constructor parameters)傳遞。
    這使我們可以嘗試不同的嵌入器(embedders)和編碼器(encoders),而無需更改模型代碼。
    '''
    def __init__(self,

                 word_embeddings: TextFieldEmbedder,
                 # 嵌入層(embedding layer)被指定爲AllenNLP TextFieldEmbedder,它表示將tokens轉換爲張量(tensors)的一般方法。
                 # (這裏我們知道我們想要用學習的張量來表示每個唯一的單詞,但是使用通用類(general class)可以讓我們輕鬆地嘗試不同類型的嵌入,例如ELMo。)

                 encoder: Seq2SeqEncoder,
                 # 類似地,編碼器(encoder)被指定爲通用Seq2SeqEncoder,即使我們知道我們想要使用LSTM。
                 # 同樣,這使得可以很容易地嘗試其他序列編碼器(sequence encoders),例如Transformer。

                 vocab: Vocabulary) -> None:
                 # 每個AllenNLP模型還需要一個詞彙表(Vocabulary),其中包含tokens到索引(indices)和索引標籤(labels to indices)的命名空間映射。

        super().__init__(vocab)
        self.word_embeddings = word_embeddings
        self.encoder = encoder
        # 請注意,我們必須將vocab傳遞給基類構造函數(base class constructor)。

        self.hidden2tag = torch.nn.Linear(in_features=encoder.get_output_dim(),
                                          out_features=vocab.get_vocab_size('labels'))
        # 前饋層(feed forward layer)不作爲參數傳入,而是由我們構造。
        # 請注意,它會查看編碼器(encoder)以查找正確的輸入維度並查看詞彙表(vocabulary)(特別是在 label->index 映射處)以查找正確的輸出維度。

        self.accuracy = CategoricalAccuracy()
        # 最後要注意的是我們還實例化了一個CategoricalAccuracy指標,我們將用它來跟蹤每個訓練(training)和驗證(validation)epoch的準確性。

    def forward(self,
                sentence: Dict[str, torch.Tensor],
                labels: torch.Tensor = None) -> Dict[str, torch.Tensor]:
    # 接下來我們需要實現forward,這是實際計算髮生的地方。數據集中的每個實例(Instance)都將(與其他實例(instances)一起批處理)輸入forward。
    # 張量的輸入作爲forward方法的輸入,並且它們的名稱應該是實例(Instances)中字段(fields)的名稱。
    # 在這種情況下,我們有一個句子字段(sentence field)和(可能)標籤字段(labels field),所以我們將相應地構建我們的forward:

        mask = get_text_field_mask(sentence)
        # AllenNLP設計用於批量輸入,但不同的輸入序列具有不同的長度。
        # 因此,AllenNLP填充(padding)較短的輸入,以便批處理具有統一的形狀,這意味着我們的計算需要使用掩碼(mask)來排除填充。
        # 這裏我們只使用效用函數(utility function) get_text_field_mask,它返回與填充和未填充位置相對應的0和1的張量。

        embeddings = self.word_embeddings(sentence)
        # 我們首先將句子張量(每個句子一系列tokens ID)傳遞給word_embeddings模塊,該模塊將每個句子轉換爲嵌入式張量序列(a sequence of embedded tensors)。

        encoder_out = self.encoder(embeddings, mask)
        # 接下來,我們將嵌入式張量(embedded tensors)(和掩碼(mask))傳遞給LSTM,LSTM產生一系列編碼(encoded)輸出。

        tag_logits = self.hidden2tag(encoder_out)
        output = {"tag_logits": tag_logits}
        # 最後,我們將每個編碼輸出張量(encoded output tensor)傳遞給前饋層(feedforward),以產生對應於各種標籤(tags)的logits。

        if labels is not None:
            self.accuracy(tag_logits, labels, mask)
            output["loss"] = sequence_cross_entropy_with_logits(tag_logits, labels, mask)
        # 和以前一樣,標籤是可選的,因爲我們可能希望運行此模型來對未標記的數據進行預測。
        # 如果我們有標籤,那麼我們使用它們來更新我們的準確度指標(accuracy metric)並計算輸出中的“損失(loss)”。

        return output

        def get_metrics(self, reset: bool = False) -> Dict[str, float]:
            return {"accuracy": self.accuracy.get_metric(reset)}
        # 我們提供了一個準確度指標(accuracy metric),每個正向傳遞都會更新。
        # 這意味着我們需要覆蓋(override)從中提取數據的get_metrics方法。
        # 這意味着,CategoricalAccuracy指標存儲預測數量和正確預測的數量,在每次call to forward 期間更新這些計數。
        # 每次調用get_metric都會返回計算的精度,並(可選)重置計數,這使我們能夠重新跟蹤每個epoch的準確性。


# =====================  正式開始  ==========================
reader = PosDatasetReader()
# 現在我們已經實現了數據集讀取器(DatasetReader)和模型(Model),我們已準備好進行訓練。我們首先需要一個數據集讀取器的實例(instance)。

train_dataset = reader.read("data/training.txt")
validation_dataset = reader.read("data/validation.txt")
# 我們可以使用它來讀取訓練數據和驗證數據。這裏我們從URL讀取它們,但如果您的數據是本地的,您可以從本地文件中讀取它們。
# 我們使用cached_pa​​th在本地緩存文件(以及處理reader.read到本地緩存版本的路徑。)

vocab = Vocabulary.from_instances(train_dataset + validation_dataset)
# 一旦我們讀入了數據集,我們就會使用它們來創建我們的詞彙表(vocabulary)(即從 tokens/labels 到 ids 的 映射[s])。

EMBEDDING_DIM = 6
HIDDEN_DIM = 6
# 現在我們需要構建模型。我們將爲嵌入層(embedding layer)和 LSTM 的隱藏層(hidden layer)選擇一個大小(size)。

token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'),
                            embedding_dim=EMBEDDING_DIM)
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
# 對於embedding the tokens,我們將使用BasicTextFieldEmbedder,它從索引名稱到嵌入(embeddings)進行映射。
# 如果你回到我們定義DatasetReader的地方,默認參數包括一個名爲“tokens”的索引,所以我們的映射只需要一個對應於該索引的嵌入(embedding)。
# 我們使用詞彙表(vocabulary)來查找我們需要多少嵌入(embeddings),並使用EMBEDDING_DIM參數來指定輸出維度。
# 也可以從預先訓練的嵌入開始(例如,GloVe向量),但是沒有必要在這個小玩具數據集上做到這一點。

lstm = PytorchSeq2SeqWrapper(torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
# 接下來我們需要指定序列編碼器(sequence encoder)。
# 這裏對PytorchSeq2SeqWrapper的需求有點不幸(如果你使用配置文件就不用擔心了),
# 但是這裏需要爲內置的PyTorch模塊添加一些額外的功能(和更簡潔的接口)。
# 在AllenNLP中,我們首先完成所有批處理,因此我們也指定了它。

model = LstmTagger(word_embeddings, lstm, vocab)
# 最後,我們可以實例化 Model。

if torch.cuda.is_available():
    cuda_device = 0
    model = model.cuda(cuda_device)
else:
    cuda_device = -1
# 如果由DPU就使用GPU0,沒有就不使用

optimizer = optim.SGD(model.parameters(), lr=0.1)
# 現在我們已經準備好訓練模型了。我們需要的第一件事是優化器(optimizer)。我們可以使用PyTorch的隨機梯度下降( stochastic gradient descent)。

iterator = BucketIterator(batch_size=2, sorting_keys=[("sentence", "num_tokens")])
# 我們需要一個DataIterator來處理我們數據集的批處理。 BucketIterator按指定字段對實例(instances)進行排序,以創建具有相似序列長度的批次(batches)。
# 這裏我們指出我們想要通過句子字段(sentence field)中的tokens數對實例(instances)進行排序。

iterator.index_with(vocab)
# 我們還指定迭代器(iterator)應確保使用我們的詞彙表(vocab)對其實例(instances)進行索引;
# 也就是說,他們的字符串已經使用我們之前創建的映射轉換爲整數。

trainer = Trainer(model=model,
                  optimizer=optimizer,
                  iterator=iterator,
                  train_dataset=train_dataset,
                  validation_dataset=validation_dataset,
                  patience=10,
                  num_epochs=1000,
                  cuda_device=cuda_device)
# 現在我們實例化我們的Trainer並運行它。
# 在這裏,我們告訴它運行1000個epochs並且如果它花費10個epochs而沒有驗證指標(validation metric)提升則提前停止訓練。
# 默認驗證指標(validation metric)標準是損失(通過變小來改善),但也可以指定不同的指標和方向(例如,精度(accuracy)應該變大)。

trainer.train()
# 當我們啓動它時,它將爲每個epochs打印一個包含“損失(loss)”和“準確度(accuracy)”度量標準的進度條。
# 如果我們的模型是好的,那麼損失應該降低,並且在我們訓練時準確度會提高。

predictor = SentenceTaggerPredictor(model, dataset_reader=reader)
# 與最初的PyTorch教程一樣,我們希望查看模型生成的預測。
# AllenNLP包含一個Predictor抽象,它接受輸入,將它們轉換爲實例(instances),輸入模型,並返回JSON可序列化的結果。
# 通常你需要實現自己的Predictor,但AllenNLP已經有一個SentenceTaggerPredictor在這裏完美運行,所以我們可以使用它。
# 它需要我們的模型(用於進行預測)和數據集讀取器(用於創建實例)。

# ============  預測 ==========================
tag_logits = predictor.predict("The dog ate the apple")['tag_logits']
# 它有一個只需要一個句子的預測方法,並從前向(forward)返回輸出字典(JSON可序列化版本)。
# 這裏tag_logits將是(5X3)的logits數組,對應於5個單詞中每個單詞的3個可能標籤。

tag_ids = np.argmax(tag_logits, axis=-1)
# 爲了獲得實際的“預測”,我們可以採用argmax。

print([model.vocab.get_token_from_index(i, 'labels') for i in tag_ids])
# 然後使用我們的詞彙表來查找預測的標籤。


# ===========  保存模型 ========================
# 最後,我們希望能夠保存我們的模型並在以後重新加載它。我們需要保存兩件事。
with open("./tmp/model.th", 'wb') as f:
    torch.save(model.state_dict(), f)
# 首先是模型權重

vocab.save_to_files("./tmp/vocabulary")
# 然後是vacabulary

# ===========  從新加載模型 ====================
vocab2 = Vocabulary.from_files("./tmp/vocabulary")
# 我們只保存了模型權重,因此如果我們想重用它們,我們實際上必須使用代碼重新創建相同的模型結構。
# 首先,讓我們將詞彙表重新加載到一個新變量中。

model2 = LstmTagger(word_embeddings, lstm, vocab2)
# 然後讓我們重新創建模型(如果我們在不同的文件中執行此操作,我們當然必須重新實例化嵌入字(word_embeddings)和lstm)。

with open("./tmp/model.th", 'rb') as f:
    model2.load_state_dict(torch.load(f))
# 之後我們必須加載它的狀態。

if cuda_device > -1:
    model2.cuda(cuda_device)
# 在這裏,我們將加載的模型移動到我們之前使用的GPU。
# 這是必要的,因爲我們之前使用原始模型移動了word_embeddings和lstm。
# 所有模型的參數都需要在同一設備上。

predictor2 = SentenceTaggerPredictor(model2, dataset_reader=reader)
tag_logits2 = predictor2.predict("The dog ate the apple")['tag_logits']
np.testing.assert_array_almost_equal(tag_logits2, tag_logits)
# 預測部分是一樣的






































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章