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
. :. .