Tagging Task

( 참고 : “딥러닝을 이용한 자연어 처리 입문” (https://wikidocs.net/book/2155) )

1. What is Tagging ?

NLP에서 Tagging은 다음과 같이 크게 둘로 구분된다.

  • 1 ) [NER] 개체명 인식(Name Entity Recognition) : 각 단어의 유형(사람/장소/단체 등) 파악
  • 2 ) [POS] 품사 태깅(Part-of-Speech Tagging / POS) : 각 단어의 품사(명사/동사/형용사 등) 파악

목표 : 인공 신경망을 이용한 태깅 작업을 하는 모델 생성


개채명 인식(NER) & 품사 태깅(POS) 의 공통점

  • 1) RNN의 Many-to-Many(다대다)작업

    .

  • 2) Bidirectional(양방향) RNN 이용

    .


2. [Bi-LSTM] Named Entity Recognition (NER)

BIO란?

개체(Entity)명 인식에서 사용되는 보편적인 방법

  • B : Begin (개체명이 시작되는 부분)

  • I : Inside (개체명의 내부 부분)

  • O : Outside (개체명이 아닌 부분)

    $\rightarrow$ ex) 해(B) 리(I) 포(I) 터(I) 보(O) 러(O) 가(O) 자(O)


1) Import Dataset

데이터 양식 : [단어] [품사 태깅] [청크 태깅] [개체명 태깅]

  • 품사태깅 ex) : NNP = 고유명사 단수형 & VBZ = 3인칭 단수 동사 현재형
  • 개체명 태깅 ex) LOC=location & ORG=organization & PER=person
  • 공란 : 새로운 문장이 시작됨을 의미!
import re
import numpy as np
import matplotlib.pyplot as plt

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split
f = open('train.txt','r')


2) Data Preprocessing

a. word & tag 기록

[단어,태그]형식으로 데이터를 변환한다.

tagged_sentence = []
sentence = []

for line in f:
    if len(line)==0 or line.startswith('-DOCSTART') or line[0]=='\n':
        if len(sentence) >0:
            tagged_sentence.append(sentence)
            sentence = []
        continue
    splits = line.split(' ')
    splits[-1] = re.sub(r'\n','',splits[-1])
    word = splits[0].lower()
    sentence.append([word,splits[-1]]) # 단어 & 태깅만 기록            


총 14041개의 태깅된 문장들이 있다.

len(tagged_sentence)
14041


첫 번째 문장을 보면 다음과 같다. 각 단어 옆에는, 그에 해당하는 태깅이 붙어있음을 확인할 수 있다.

tagged_sentence[0]
[['eu', 'B-ORG'],
 ['rejects', 'O'],
 ['german', 'B-MISC'],
 ['call', 'O'],
 ['to', 'O'],
 ['boycott', 'O'],
 ['british', 'B-MISC'],
 ['lamb', 'O'],
 ['.', 'O']]


우리가 만든 tagged_sentence에서, ‘단어’만을 담은 sentence_list와 ‘태깅’만을 담은 tag_list를 만든다.

sentence_list = []
tag_list = []

for tagged_sentence in tagged_sentences:
    sentence, tag_info = zip(*tagged_sentence)
    sentence_list.append(list(sentence))
    tag_list.append(list(tag_info))


첫 번째 문장의 ‘단어’들은 다음과 같고,

sentence_list[0]
['eu', 'rejects', 'german', 'call', 'to', 'boycott', 'british', 'lamb', '.']


첫 번째 문장의 ‘태깅’들은 다음과 같다.

tag_list[0]
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']


한 문장이 가지는 단어 개수는 평균적으로 14.5개이고, 가장 긴 문장은 113개의 단어로 구성되어 있다.

print('MAX length : %d' % max(len(l) for l in sentence_list))
print('AVG length : %f' % (sum(map(len, sentence_list))/len(sentence_list)))
MAX length : 113
AVG length : 14.501887


b. Tokenize

sentence_list와 tag_list를 tokenize 해준다.

이때, 가장 빈번하게 등장한 단어 상위 4천개만을 남겨두고 나머지는 OOV(그 외)로 처리한다.

max_words = 4000

src_t = Tokenizer(num_words=max_words, oov_token='OOV')
src_t.fit_on_texts(sentence_list)

tar_t = Tokenizer()
tar_t.fit_on_texts(tag_list)
vocab_size = max_words
tag_size = len(tar_t.word_index) + 1
print(vocab_size, tag_size)
4000 10


이렇게 tokenized된 단어들을 **index로 바꿔준다. **

X_train = src_t.texts_to_sequences(sentence_list)
y_train = tar_t.texts_to_sequences(tag_list)


다음과 같이 텍스트가 숫자로 바뀜을 확인할 수 있다.

X_train[0]
[989, 1, 205, 629, 7, 3939, 216, 1, 3]
y_train[0]
[4, 1, 7, 1, 1, 1, 7, 1, 1]


  • index2word = key:index & value : 단어
  • index2ner = key:index & value : 태깅
index2word = src_t.index_word
index2ner = tar_t.index_word
index2ner
{1: 'o',
 2: 'b-loc',
 3: 'b-per',
 4: 'b-org',
 5: 'i-per',
 6: 'i-org',
 7: 'b-misc',
 8: 'i-loc',
 9: 'i-misc'}


c. Padding

패딩을 해주고, 8:2로 train data를 train&test 데이터로 나눈다.

max_len = 70
X_train = pad_sequences(X_train, padding='post', maxlen=max_len)
y_train = pad_sequences(y_train, padding='post', maxlen=max_len)
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=.2, random_state=42)


d. One-Hot Encoding

y_train = to_categorical(y_train, num_classes=tag_size)
y_test = to_categorical(y_test, num_classes=tag_size)
print('train 샘플 문장의 크기 : {}'.format(X_train.shape))
print('train 샘플 레이블의 크기 : {}'.format(y_train.shape))
print('test 샘플 문장의 크기 : {}'.format(X_test.shape))
print('test 샘플 레이블의 크기 : {}'.format(y_test.shape))
train 샘플 문장의 크기 : (11232, 70)
train 샘플 레이블의 크기 : (11232, 70, 10)
test 샘플 문장의 크기 : (2809, 70)
test 샘플 레이블의 크기 : (2809, 70, 10)


3) Bi-directional LSTM

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding, LSTM, Bidirectional, TimeDistributed
from tensorflow.keras.optimizers import Adam


a. Model Architecture

  • embedding vector의 차원 : 128
  • LSTM에는 256개의 neuron / return_sequences=True (Many-to-Many 문제이므로)
  • TimeDistributed : 10개의 time에 각각 적용!
model = Sequential()
model.add(Embedding(input_dim=vocab_size, output_dim=128, input_length=max_len, mask_zero=True))
model.add(Bidirectional(LSTM(256, return_sequences=True)))
model.add(TimeDistributed(Dense(tag_size, activation='softmax')))
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 70, 128)           512000    
_________________________________________________________________
bidirectional_1 (Bidirection (None, 70, 512)           788480    
_________________________________________________________________
time_distributed_1 (TimeDist (None, 70, 10)            5130      
=================================================================
Total params: 1,305,610
Trainable params: 1,305,610
Non-trainable params: 0
_________________________________________________________________
model.compile(loss='categorical_crossentropy', optimizer=Adam(0.001), metrics=['accuracy'])


b. train

history = model.fit(X_train, y_train, batch_size=128, epochs=4,  validation_data=(X_test, y_test))
Train on 11232 samples, validate on 2809 samples
Epoch 1/4
11232/11232 [==============================] - 185s 16ms/sample - loss: 0.1872 - accuracy: 0.8228 - val_loss: 0.1283 - val_accuracy: 0.8373
Epoch 2/4
11232/11232 [==============================] - 201s 18ms/sample - loss: 0.1010 - accuracy: 0.8516 - val_loss: 0.0806 - val_accuracy: 0.8845
Epoch 3/4
11232/11232 [==============================] - 213s 19ms/sample - loss: 0.0681 - accuracy: 0.9014 - val_loss: 0.0583 - val_accuracy: 0.9184
Epoch 4/4
11232/11232 [==============================] - 222s 20ms/sample - loss: 0.0486 - accuracy: 0.9325 - val_loss: 0.0449 - val_accuracy: 0.9391


c. evaluation

model.evaluate(X_test, y_test)[1]
2809/1 [========================================] - 39s 14ms/sample - loss: 0.0421 - accuracy: 0.9391

0.9390847


4) Result

def result_index(i):
    y_pred = model.predict(np.array([X_test[i]]))
    y_pred = np.argmax(y_pred,axis=-1)
    true = np.argmax(y_test[i],-1)
    
    print("{:15}|{:5}|{}".format('word','actual','predicted'))
    print(35*"-")
    
    for w,t,pred in zip(X_test[i], true, y_pred[0]):
        if w!= 0:
            print("{:17}:{:7}{}".format(index2word[w], index2ner[t].upper(),index2ner[pred].upper()))
result_index(13)
word           |actual|predicted
-----------------------------------
amsterdam        :B-LOC  B-LOC
1996-08-28       :O      O

정확도는 높게 보인다. 하지만 대부분의 단어가 개체명이 아니라 ‘O’가 태깅된 상황이다! 이를 해결하는 방법 중 하나가 F-1 score를 지표로 사용하는 것이다. (자세한건 Multi-class Classification Metric 포스트 참고)


3. [Bi-LSTM] POS Tagging

( 태깅하는 대상이 ‘개체명’이 아니라 ‘품사’라는 점을 제외하면 위와 전부 동일하다. 구체적인 설명은 생략한다 )

1) Import Dataset

import nltk
import numpy as np
import matplotlib.pyplot as plt

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from sklearn.model_selection import train_test_split

%matplotlib inline
tagged_sentences = nltk.corpus.treebank.tagged_sents() # token화에 POS 태깅이 된 데이터
len(tagged_sentences)
3914


2) Data Preprocessing

a. word & tag 분리

sentence_list = []
pos_list = []

for tagged_sentence in tagged_sentences:
    sentence, tag_info = zip(*tagged_sentence)
    sentence_list.append(list(sentence))
    pos_list.append(list(tag_info))
print(sentence_list[0])
['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'will', 'join', 'the', 'board', 'as', 'a', 'nonexecutive', 'director', 'Nov.', '29', '.']


print(pos_list[0])
['NNP', 'NNP', ',', 'CD', 'NNS', 'JJ', ',', 'MD', 'VB', 'DT', 'NN', 'IN', 'DT', 'JJ', 'NN', 'NNP', 'CD', '.']


print('MAX length : %d' % max(len(l) for l in sentence_list))
print('AVG length : %f' % (sum(map(len, sentence_list))/len(sentence_list)))
MAX length : 271
AVG length : 25.722024


b. Tokenize

def tokenize(samples):
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(samples)
    return tokenizer
src_t = tokenize(sentence_list)
tar_t = tokenize(pos_list)


vocab_size = len(src_t.word_index) + 1
tag_size = len(tar_t.word_index) + 1
vocab_size, tag_size
(11388, 47)


c. Padding

X_train = src_t.texts_to_sequences(sentence_list)
y_train = tar_t.texts_to_sequences(pos_list)
print(X_train[0])
[5601, 3746, 1, 2024, 86, 331, 1, 46, 2405, 2, 131, 27, 6, 2025, 332, 459, 2026, 3]
max_len = 150
X_train = pad_sequences(X_train, padding='post', maxlen=max_len)
y_train = pad_sequences(y_train, padding='post', maxlen=max_len)


d. One-hot Encoding

X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=.2, random_state=777)
y_train = to_categorical(y_train, num_classes=tag_size)
y_test = to_categorical(y_test, num_classes=tag_size)
print('train 샘플 문장의 크기 : {}'.format(X_train.shape))
print('train 샘플 레이블의 크기 : {}'.format(y_train.shape))
print('test 샘플 문장의 크기 : {}'.format(X_test.shape))
print('test 샘플 레이블의 크기 : {}'.format(y_test.shape))
train 샘플 문장의 크기 : (3131, 150)
train 샘플 레이블의 크기 : (3131, 150, 47)
test 샘플 문장의 크기 : (783, 150)
test 샘플 레이블의 크기 : (783, 150, 47)


3) Bi-directional LSTM

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, InputLayer, Bidirectional, TimeDistributed, Embedding
from tensorflow.keras.optimizers import Adam


a. Model Architecture

model = Sequential()
model.add(Embedding(vocab_size, 128, input_length=max_len, mask_zero=True))
model.add(Bidirectional(LSTM(256, return_sequences=True)))
model.add(TimeDistributed(Dense(tag_size, activation=('softmax'))))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 150, 128)          1457664   
_________________________________________________________________
bidirectional (Bidirectional (None, 150, 512)          788480    
_________________________________________________________________
time_distributed (TimeDistri (None, 150, 47)           24111     
=================================================================
Total params: 2,270,255
Trainable params: 2,270,255
Non-trainable params: 0
_________________________________________________________________
model.compile(loss='categorical_crossentropy',optimizer=Adam(0.001),metrics=['accuracy'])


b. Train

history = model.fit(X_train, y_train, batch_size=128, epochs=6,  validation_data=(X_test, y_test))
Train on 3131 samples, validate on 783 samples
Epoch 1/6
3131/3131 [==============================] - 171s 55ms/sample - loss: 0.5732 - accuracy: 0.1328 - val_loss: 0.5070 - val_accuracy: 0.1922
Epoch 2/6
3131/3131 [==============================] - 165s 53ms/sample - loss: 0.4925 - accuracy: 0.2194 - val_loss: 0.4632 - val_accuracy: 0.3645
Epoch 3/6
3131/3131 [==============================] - 175s 56ms/sample - loss: 0.4151 - accuracy: 0.4265 - val_loss: 0.3386 - val_accuracy: 0.4882
Epoch 4/6
3131/3131 [==============================] - 245s 78ms/sample - loss: 0.2713 - accuracy: 0.5927 - val_loss: 0.2062 - val_accuracy: 0.7091
Epoch 5/6
3131/3131 [==============================] - 264s 84ms/sample - loss: 0.1501 - accuracy: 0.8007 - val_loss: 0.1127 - val_accuracy: 0.8508
Epoch 6/6
3131/3131 [==============================] - 259s 83ms/sample - loss: 0.0773 - accuracy: 0.9040 - val_loss: 0.0727 - val_accuracy: 0.8964


c. Evaluate

model.evaluate(X_test, y_test)[1]
783/1 [=====================================] - 19s 24ms/sample - loss: 0.0658 - accuracy: 0.8964

0.8963676


4) Result

index2word=src_t.index_word
index2tag=tar_t.index_word
def result_index(i):
    y_pred = model.predict(np.array([X_test[i]]))
    y_pred = np.argmax(y_pred,axis=-1)
    true = np.argmax(y_test[i],-1)
    
    print("{:15}|{:5}|{}".format('word','actual','predicted'))
    print(35*"-")
    
    for w,t,pred in zip(X_test[i], true, y_pred[0]):
        if w!= 0:
            print("{:17}:{:7}{}".format(index2word[w], index2tag[t].upper(),index2tag[pred].upper()))
result_index(10)
word           |actual|predicted
-----------------------------------
in               :IN     IN
addition         :NN     NN
,                :,      ,
buick            :NNP    NNP
is               :VBZ    VBZ
a                :DT     DT
relatively       :RB     RB
respected        :VBN    VBN
nameplate        :NN     NN
among            :IN     IN
american         :NNP    NNP
express          :NNP    NNP
card             :NN     NN
holders          :NNS    NNS
,                :,      ,
says             :VBZ    VBZ
0                :-NONE- -NONE-
*t*-1            :-NONE- -NONE-
an               :DT     DT
american         :NNP    NNP
express          :NNP    NNP
spokeswoman      :NN     NN
.                :.      .