목차
  1. 프로젝트 소개
  2. 개발 과정
    1. Crawling
    2. Preprocessing
    3. Analysis
    4. Labeling
    5. Training
    6. Test
  3. 데모
  4. 마치며

문장의 끝점(마침표, 물음표)을 검출하는 딥러닝 모델을 개발한 이야기를 공유합니다.

프로젝트 소개

WebRTC의 Speech Recognition API를 이용하면 손쉽게 STT(Speech To Text) 기능을 구현할 수 있습니다. WebRTC 연구실의 음성 인식 사이트에서 데모를 경험해보실 수 있는데, 생각보다 인식이 잘되어 깜짝 놀라실 겁니다. 아래 예시는 “마침표가 찍히지 않는 것을 볼 수 있습니다” 라고 발화한 예시입니다.

WebRTC의 Speech Recognition API는 발화를 하는 즉시 실시간으로 음향모델(Acoustic Model)이 음성을 텍스트로 변환한 결과를 반환하다가 몇 초 이상 발화가 없는 경우 지금까지 인식된 문장을 언어모델(Language Model)이 문맥에 맞게 수정하여 반환해 줍니다.

사내 프로젝트에서 WebRTC의 Speech Recognition API를 이용하던 중 STT 결과물에 마침표나 물음표 같은 문장의 끝점이 없다는 것을 발견하였고, 해당 프로젝트에서는 문장의 구분점이 필요하여 마침표나 물음표와 같은 문장의 끝점을 검출해주는 딥러닝 모델을 구현해보았습니다.


개발 과정

개발 과정은 Crawling > Preprocessing > Analysis > Labeling > Training 순서로 진행하였습니다.


Crawling

학습에 필요한 데이터를 수집하는 단계로, 이번 프로젝트에 사용된 데이터는 실제 유입될 데이터와 가장 유사한 제 20대 국회 회의록을 이용하였습니다. 데이터가 hwp 형식으로 되어있어, GUI 매크로를 이용하여 txt 파일로 변환하였습니다.


Preprocessing

수집한 데이터를 가공하고 정제하는 전처리 단계로, 회의 데이터가 일정한 형식으로 구현되어 있어 이를 분석하여 다음과 같은 전처리를 하였습니다.

국회 회의록 전처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 텍스트 배열 추출(라인별)

- 라인별 전처리
'^\n' : 공백 라인 삭제
'o' : 소문자 영어o를 이용한 단락 설명 삭제 (ex: o 국무위원 인사)

- 텍스트 배열 to 텍스트 문자열로 변환

- 회의시작 전과 후 데이터 삭제
'\(\d+?[월시]\d+?[일분]\s.+?\)'가 처음 등장하는 부분이 시작
'\(\d+?[월시]\d+?[일분]\s.+?\)'가 마지막 등장하는 부분이 종료

- 대화가 아닌 내용 삭제
'(\(.+\))' : 괄호 데이터 삭제
'\d+\.\s[^◯]*' : 회의 안건 목록 삭제

- 시간에 따른 화자별 대화로 구분
'◯.+?\s.+?\s+' : 화자 추출
화자 시퀀스와 대화 시퀀스

- 화자별 대화 문단 정제
'\n' 줄바꿈 기호 삭제
strip(앞뒤 공백제거)
'\s{2,}' 두 개 이상의 띄어쓰기 한 개로 변경
'\s$' 문단 마지막에 있는 띄어쓰기 삭제
연속된 명사 사이에 찍히는 '․'(마침표 아님)를 ','로 변경

- 문단으로 부터 문장 추출
strip(앞뒤 공백제거)
'\s?(.+?[\.\!\?…])\D' : 문장 추출

학습에 불필요한 데이터를 제거하고 화자 단위로 분리하여 전체 2,084,007 문장을 수집했습니다.

화자 단위로 분리한 데이터화자 단위로 분리한 데이터

Analysis

데이터를 시각화하고 전처리가 올바르게 되었는지 확인하는 단계로, 수집된 문장들을 분석해보면 평균 40글자에 최대 1,131 글자의 길이를 가지고 있습니다.

1
2
3
4
5
6
7
문장 길이 최대 값: 1131
문장 길이 최소 값: 2
문장 길이 평균 값: 49.34
문장 길이 표준편차: 40.68
문장 길이 중간 값: 38.0
문장 길이 제 1 사분위: 19.0
문장 길이 제 3 사분위: 67.0
log-histogram of length of sentencelog-histogram of length of sentence
box plot of sentencebox plot of sentence

수집된 단어들을 분석해보면 평균 12글자에 최대 256 글자의 길이를 가지고 있습니다.

1
2
3
4
5
6
7
단어 개수 최대 값: 256
단어 개수 최소 값: 1
단어 개수 평균 값: 11.89
단어 개수 표준편차: 10.00
단어 개수 중간 값: 9.0
단어 개수 제 1 사분위: 5.0
단어 개수 제 3 사분위: 16.0
log-histogram of word count in sentencelog-histogram of word count in sentence

또한 특수문자 및 끝점의 종류를 분석한 결과 다음과 같습니다.

1
2
3
4
5
물음표로 끝나는 문장: 11.44%
마침표로 끝나는 문장: 83.64%
…로 끝나는 문장: 4.64%
,각 있는 문장 문장: 30.27%
특수문자가 있는 문장: 36.72%
word cloudword cloud

Labeling

데이터에 라벨링을 하는 단계로, 이번에 해결하려는 문제를 두 단어 사이에 공백( ), 마침표(.), 물음표(?) 중 어떤게 들어올지 선택하는 분류 문제로 정의하였습니다.

따라서 단어들을 Bigram으로 분리하였고, 문장의 마지막 단어가 혼자 오는 경우를 위해 [PAD]를 추가하였습니다. label은 Bigram 사이에 들어오는 실제 값을 이용하여 공백은 0, 마침표는 1 그리고 물음표는 2로 붙였습니다.

의석을 정리해 주시기 바랍니다. 그럼 준비 되셨나요?라는 문장을 라벨링한 경우 아래와 같습니다.

data label
의석을 [PAD] 0
의석을 정리해 0
정리해 [PAD] 0
정리해 주시기 0
주시기 [PAD] 0
주시기 바랍니다 0
바랍니다 [PAD] 1
바랍니다 그럼 1
그럼 [PAD] 0
그럼 준비 0
준비 [PAD] 0
준비 되셨나요 0
되셨나요 [PAD] 2

라벨링된 데이터의 비율을 분석해보면 다음과 같습니다.

1
2
3
4
전체 데이터 개수 : 16,519,997개
물음표 데이터 개수 : 158,842개 (0.9%)
마침표 데이터 개수 : 1,183,299개 (7.2%)
띄어쓰기 데이터 개수 : 15,177,856개 (91.9%)

Training

학습은 Keras를 이용하였고, 다음과 같은 방법으로 692,862개의 단어사전을 구축하여 단어를 양의 정수로 표현하였습니다.

tokenize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def createVocabulary(bigram_array):
vocab = {}
index = 1
for i, bigram in enumerate(bigram_array):
words = bigram.split(' ')
if len(words) != 2:
continue

for word in words:
if word not in vocab:
vocab[word] = index
index += 1
return vocab, index

def text2seq(vocab, bigram_array):
bigram_seq = []
for i, bigram in enumerate(bigram_array):
words = bigram.split(' ')
seq = []
for word in words:
if word in vocab:
seq.append(vocab[word])
else:
seq.append(0) # OOV 인 경우 0
bigram_seq.append(seq)
return bigram_seq

앞서 공백(0), 마침표(1), 물음표(2)로 라벨링한 데이터는 keras.util.to_categorical 함수를 이용하여 one-hot 인코딩을 하였고 전체 학습 데이터와 테스트 데이터는 8:2로 나누어 진행하였습니다.

1
2
학습 데이터 : 13,215,997 개
테스트 데이터 : 3,304,000 개

먼저 Embedding 레이어를 이용하여 앞서 양의 정수로 표현한 단어들을 고정된 크기의 밀집 벡터로 바꿔주었습니다. 다음 LSTM 레이어를 추가하였고 공백, 마침표, 띄어쓰기 셋 중 하나를 분류하는 문제이므로 output 차원이 3인 softmax Dense 레이어를 추가하였습니다. loss는 categorical_crossentropy를 이용하였고, adam optimizer를 이용하여 모델을 컴파일하였습니다.

model
1
2
3
4
5
6
model = Sequential()
model.add(Embedding(vocab_size, 2, input_length=2))
model.add(LSTM(128))
model.add(Dense(3, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(X_train, y_train, epochs=8, validation_split=0.3, batch_size = 2048)

총 8에폭을 학습하였고, 학습 시간은 30분이 소요되었습니다. 학습 종료시 validation accuracy는 대략 98.6%가 나왔는데, 데이터가 굉장히 공백(0)에 편중된(biased) 데이터이므로 전부 공백(0)으로 해도 90% 이상의 accuracy가 보장되기 때문에 큰 의미는 없다고 보여집니다.

train and validation accuracy per epochstrain and validation accuracy per epochs

따라서 여기서는 precision, recall 그리고 f1-score 값을 보는게 중요합니다.

종류 precision recall f1-score 데이터 수
공백(0) 0.99 1.00 0.99 1,520,203개
마침표(1) 0.95 0.93 0.94 232,575개
물음표(2) 0.90 0.66 0.76 31,706개
micro avg 0.98 0.98 0.98 1,784,484개
macro avg 0.95 0.86 0.90 1,784,484개
weighted avg 0.98 0.98 0.98 1,784,484개
samples avg 0.98 0.98 0.98 1,784,484개

확실히 데이터가 많은 공백(0)은 precision, recall, f1-score가 모두 높게 나온것을 보실 수 있습니다. 마침표(1)도 공백(0)에 비하면 데이터가 6.5배 정도 적음에도 불구하고 거의 모든 수치가 0.9 이상으로 생각보다 성능이 잘 나왔습니다. 그러나 물음표(2)는 데이터가 너무 적어서 recall이 0.66으로 조금 낮게 나왔지만 precision은 0.90으로 꽤나 신뢰할만 수치가 나왔습니다.


Test

본 테스트에 사용된 스크립트는 2018년 8월 22일(목) 김용민 브리핑 방송 대본의 일부 내용을 발췌한 내용으로 개인적 견해나 의미와는 아무런 상관이 없음을 밝힙니다.

2018년 8월 22일(목) 김용민 브리핑 방송 대본의 일부 내용을 발췌하여 마침표와 느낌표를 제거한 뒤 모델에 input 데이터로 입력하여 테스트하였습니다.

왼쪽: 마침표와 물음표가 제거된 데이터, 오른쪽: 모델 예측 결과왼쪽: 마침표와 물음표가 제거된 데이터, 오른쪽: 모델 예측 결과

3번째 줄에 공백이 들어갈 부분에 마침표가 들어간 문장과 12번째 줄에 마침표가 빠진 것 이외에는 모두 괜찮게 예측했음을 볼 수 있습니다.

데모

아래 영상은 WebRTC의 Speech Recognition API를 이용한 STT에 끝점 검출 모델을 탑재한 시연 영상입니다.
끝점 검출 모델은 Python Flask에 탑재하여 API형태로 서비스하였고, 화자의 발언이 종료되면 해당 API를 호출하여 마침표나 물음표를 찍어주는 형태로 구현하였습니다.


마치며

WebRTC Speech Recognition은 정말 성능이 좋은데 심지어 무료입니다. 이를 이용하면 정말 다양한 서비스를 만들어 볼 수 있을 것 같습니다. 추가로 hwp 파일은 텍스트를 추출하기가 정말 어려운 것 같습니다.. 😭

이상 딥러닝으로 문장의 끝점 검출기 만들기 프로젝트를 마치겠습니다.