機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

PyTorch:LSTMの実践(CoNLL)

はじめに


前回、LSTMの基礎的な使い方を整理した。
kento1109.hatenablog.com

だいたい、使い方は分かったので実際にタスクに取り組んでみる。

今回は「固有表現抽出」で試してみる。

CoNLLについて



CoNLLは、「Conference on Computational Natural Language Learning」の略称。
色々と自然言語処理のShared Taskを開催しており、コーパスが自由に利用できるようになっている。
今回は「CoNLL 2003 shared task (NER) data」を利用する。
shared taskの詳細はこちら。
Language-Independent Named Entity Recognition (II)

尚、コーパスか下記よりダウンロードした。
NER/corpus/CoNLL-2003 at master · synalp/NER · GitHub

フォーマットは以下のようなタブ区切りになっている。
固有表現抽出を試す場合、4列目がラベルになる。

   U.N.         NNP  I-NP  I-ORG 
   official     NN   I-NP  O 
   Ekeus        NNP  I-NP  I-PER 
   heads        VBZ  I-VP  O 
   for          IN   I-PP  O 
   Baghdad      NNP  I-NP  I-LOC 
   .            .    O     O 

前処理



とりあえず、下記のオブジェクトを作って保存する。

  • word2idx(単語:インデックスのdictionary)
  • label2idx(ラベル:インデックスのdictionary)
  • sents_idx(文章の単語をインデックス変換したリスト)
  • labels_idx(文章のラベルをインデックス変換したリスト)

前処理のコードはここに置いた。
LSTM_CoNLL_PyTorch/preprocessing.ipynb at master · kento1109/LSTM_CoNLL_PyTorch · GitHub

尚、コードの一部はutilsを利用した。
model.utils — LM-LSTM-CRF documentation

学習



今回は標準的なLSTMで試す。
(CRFやBidirectional等は行わない)
コードはほとんどチュートリアルのものを利用した。

LSTMクラスは以下の通り(チュートリアルのまま)

class LSTMTagger(nn.Module):

    def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
        super(LSTMTagger, self).__init__()
        self.hidden_dim = hidden_dim

        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)

        # The LSTM takes word embeddings as inputs, and outputs hidden states
        # with dimensionality hidden_dim.
        self.lstm = nn.LSTM(embedding_dim, hidden_dim)

        # The linear layer that maps from hidden state space to tag space
        self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # Before we've done anything, we dont have any hidden state.
        # Refer to the Pytorch documentation to see exactly
        # why they have this dimensionality.
        # The axes semantics are (num_layers, minibatch_size, hidden_dim)
        return (autograd.Variable(torch.zeros(1, 1, self.hidden_dim)),
                autograd.Variable(torch.zeros(1, 1, self.hidden_dim)))

    def forward(self, sentence):
        embeds = self.word_embeddings(sentence)
        lstm_out, self.hidden = self.lstm(
            embeds.view(len(sentence), 1, -1), self.hidden)
        tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
        tag_scores = F.log_softmax(tag_space, dim=1)
        return tag_scores

prepare_sequenceで系列データをテンソル化してautogradでラッピングする。

def prepare_sequence(idxs):
    tensor = torch.LongTensor(idxs)
    return autograd.Variable(tensor)

学習前に1文だけ予測してみる。

print labels_idx[0]  # ground truth
>> [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
inputs = prepare_sequence(sents_idx[0])
tag_scores = model(inputs)
_, pred_tag = torch.max(tag_scores.data, 1)
print(pred_tag)
>>  5 5 1 1 3 1 1 5 3 3 5

予測が全くランダムにラベリングされていることが分かる。

学習はチュートリアルから少し変えている。

# optimizer = optim.SGD(model.parameters(), lr=0.1)
optimizer = optim.RMSprop(model.parameters())

EPOCHS = 20
loss_list = []
for epoch in range(EPOCHS):  # again, normally you would NOT do 300 epochs, it is toy data
#     for sentence, tags in training_data:

    print "epoch",  epoch + 1 , "/" , EPOCHS
    
    loss_ = 0

    for i, (sentence, tags) in enumerate(zip(sents_idx, labels_idx)):
        # Step 1. Remember that Pytorch accumulates gradients.
        # We need to clear them out before each instance
        model.zero_grad()

        # Also, we need to clear out the hidden state of the LSTM,
        # detaching it from its history on the last instance.
        model.hidden = model.init_hidden()

        # Step 2. Get our inputs ready for the network, that is, turn them into
        # Variables of word indices.
        sentence_in = prepare_sequence(sentence)
        targets = prepare_sequence(tags)

        # Step 3. Run our forward pass.
        tag_scores = model(sentence_in)

        # Step 4. Compute the loss, gradients, and update the parameters by
        #  calling optimizer.step()
        loss = loss_function(tag_scores, targets)
        loss.backward()
        optimizer.step()
        
        loss_ += loss.data[0]
        
    print 'loss: %.4f' % (loss_ / (i + 1))
    loss_list.append(loss_ / (i + 1))

エポック毎にロスが減少していることが分かる。
f:id:kento1109:20180303142933p:plain

予測

学習後に同様の文章のラベルを予測してみる。

inputs = prepare_sequence(sents_idx[0])
tag_scores = model(inputs)
_, pred_tag = torch.max(tag_scores.data, 1)
print(pred_tag)
>> 0 0 1 0 0 0 0 0 0 0 0

正解ラベルと一致していることが分かる。

Pytorchで標準のLSTMで系列ラベリングが出来ることを確認できた。

※全コードは以下
LSTM_CoNLL_PyTorch/LSTM.ipynb at master · kento1109/LSTM_CoNLL_PyTorch · GitHub