CNN을 활용한 비속어 필터링

CNN을 활용해서 텍스트를 분류하는 예제는 이전에서 살펴봤습니다.
해당 예제는 영화평점 텍스트를 학습하고 그 평가가 긍정인지 부정인지를 판단하는 문제를 CNN을 통해서 예측하는 내용이었습니다.
내용이 궁금하신 분은 아래의 예제를 살펴보시기 바랍니다.

이번에도 CNN을 활용한 예제로 비속어를 필터링하는 내용입니다. 본 예제에서는 학습을 통해서 생성된 모델을 python에서 제공하는 웹 어플리케이션 제작 프레임워크인 django를 통해서 간단한 웹서버를 구현해보겠습니다.

먼저 아래의 그림은 이미 많은 연구 문서나 블로그에서 보셨을듯한 이미지로 CNN을 활용한 텍스트 분류의 대표적인 이미지입니다. 해당 이미지를 간단히 설면하면 아래의 문장 “wait for the video and don’t rent it” 문장을 어절단위로 분리하여 Embedding(n, 5) 레이어를 통과시키면 아래의 문장은 “문장어절 × 5” 형태의 값을 가지게 됩니다. 이것이 첫번째 이미지입니다. 이것을 CNN에서 처리하는 벡터 shape으로 만들기 위해서는 앞에 Channel 값을 입력하게 됩니다.

이렇게 되면 예시 문장은 하나의 이미지 데이터의 모양(Shape)을 가지게 됩니다. 그러나 이런 문장이 하나만 존재하지는 않고 여러개 존재합니다. 그렇게 되면 맨 앞에는 n_batch 정보를 입력 할 수 있습니다.

예를 들어서 100개의 문장이라면 [100 × 1 × 9 × 5] 형태의 shape이 되는 것이죠. 이는 CNN을 통해서 이미지를 분류한다고 생각할 때에 9×5의 1채널 이미지 100장에 해당됩니다.

이제 이것을 Conv2d 레이어를 통과시키고 나온 output 데이터를 Fully Connected 한 후에 Linear 레이어를 통과시키고 이 값을 Softmax로 시켜 최종 출력값을 얻습니다.

이 과정은 이미지를 분류하는 과정과 굉장히 유사합니다. 다만 앞부분에서 “어떻게 텍스트를 처리해서 매트릭스를 만드는가?” 하는 과정만 차이가 있습니다. 위의 논문에서는 어절 단위로 분리해서 처리했고 또 다른 논문에서는 한글 자소단위로 분리하여 필터를 적용하는 연구도 있습니다.

본 예제에서는 문장을 단어 단위로 분리하되 각 단어를 n-gram 하여 2글자씩 분리해서 워드 벡터를 생성하였습니다. 학습에 사용할 비속어 리스트는 아래와 같습니다.

txt 컬럼은 비속어 정보를 label 정보는 1의 경우는 비속어, 0은 일반 단어로 표시합니다. 각 텍스트는 2글자씩 분리하여 워드 벡터를 만든다고 했는데 해당 과정을 수행하면 “야해요야동” 문장의 경우”야해, 해요, 요야, 야동, 야해요야동” 이런 방법으로 구성됩니다. 비속어가 1글자 인경우도 상당히 많기 때문에 1글자의 경우는 한글자만 사용하는 것으로 했습니다. 2글자씩 분리한 다음 마지막에는 원문도 포함해서 훈련용 데이터셋을 생성합니다.

# n-gram
def textgram(text):
    tmp = []
    if len(text) > 1:
        for i in range(len(text)-1):
            tmp.append(text[i]+text[i+1])
        tmp.append(text)
    return tmp

textgram('야해요야동')
# output ['야해', '해요', '요야', '야동', '야해요야동']

아래 데이터는 테스트에 사용했던 도메인의 일반 텍스트입니다. 당연한 말이지만 도메인이 넓은 경우 보다는 한정된 범위로 축소하는 것이 더 좋은 예측 결과를 보입니다. 본 예제의 경우는 챗봇에 사용하는 일상 대화들로부터 데이터를 수집하였습니다. 아래의 데이터셋 역시 2글자로 분리합니다. label이 0인 것은 정상 단어라는 의미입니다.

생성된 단어의 리스트들을 통해서 vocab을 만듭니다. 이때 중복 제거는 필수입니다.
만들어진 vocab에 padding, unk 값을 추가합니다. 그 이유는 각 단어가 기준 크기 보다 작은 경우 빈 값을 패딩값으로 채우기 위함입니다. unk의 경우는 vocab에 존재하지 않는 단어가 나올 경우 해당 위치를 채워주는 코드입니다.

vocab = list(set([w for word in words for w in word]))
vocab = np.insert(vocab,0,'!') # padding
vocab = np.insert(vocab,0,'#') # unk

x_data = [[word2index[w] for w in word] for word in words]

이제 각 워드를 인덱스로 바꾸는 과정을 완료하면 아래와 같은 데이터셋을 얻을 수 있습니다. 아래의 데이터셋은 보시는 것처럼 그 크기가 각각 달라서 일정한 값으로 Shape을 맞출 필요가 있습니다.

print(x_data[0:10])

[[2179, 2693, 4402, 2776, 3215],
 [964, 1927, 2027, 1767, 6721, 3171],
 [964, 1927, 1525, 5679, 3310],
 [964, 1927, 4133, 257, 2462, 1061, 554, 1941, 1753, 1666],
 [964, 1927, 5247, 1177],
 [964, 1927, 3795, 6693, 191, 5585, 3299, 5066],
 [964, 1927, 601, 4397, 2938],
 [1298, 5558, 2423, 5374, 877, 4260],
 [3911, 229, 5374, 877, 4103],
 [3241, 5603, 3173]]

고정 크기를 정해주고 해당 길이보다 작은 데이터들은 아래와 같이 사전에 정의한 padding 값으로 채워줍니다. 그렇게 되면 아래와 같은 형태의 데이터 데이터 값을 얻을 수 있습니다. 여기서 vocab에 존재하지 않는 새로운 단어가 입력되면 해당 단어는 0으로 채우게 됩니다.

[array([2179, 2693, 4402, 2776, 3215,    1,    1,    1,    1,    1]),
 array([ 964, 1927, 2027, 1767, 6721, 3171,    1,    1,    1,    1]),
 array([ 964, 1927, 1525, 5679, 3310,    1,    1,    1,    1,    1]),
 array([ 964, 1927, 4133,  257, 2462, 1061,  554, 1941, 1753, 1666]),
 array([ 964, 1927, 5247, 1177,    1,    1,    1,    1,    1,    1]),
 array([ 964, 1927, 3795, 6693,  191, 5585, 3299, 5066,    1,    1]),
 array([ 964, 1927,  601, 4397, 2938,    1,    1,    1,    1,    1]),
 array([1298, 5558, 2423, 5374,  877, 4260,    1,    1,    1,    1]),
 array([3911,  229, 5374,  877, 4103,    1,    1,    1,    1,    1]),
 array([3241, 5603, 3173,    1,    1,    1,    1,    1,    1,    1])]

입력 데이터에 대한 준비가 마무리되면 파이토치의 nn.Module 모듈을 상속 받아서 훈련용 모듈을 생성합니다. 이전에 활용한 CNN을 활용한 텍스트 분류 글에서 사용했던 CNN 모듈을 그대로 사용하기 때문에 해당 부분은 생략합니다. 다만 생성된 모듈을 출력해보면 아래와 같은 정보를 얻을 수 있습니다.

CNN(
  (embedding): Embedding(7037, 100)
  (convs): ModuleList(
    (0): Conv2d(1, 100, kernel_size=(2, 100), stride=(1, 1))
    (1): Conv2d(1, 100, kernel_size=(3, 100), stride=(1, 1))
    (2): Conv2d(1, 100, kernel_size=(4, 100), stride=(1, 1))
    (3): Conv2d(1, 100, kernel_size=(5, 100), stride=(1, 1))
  )
  (fc): Linear(in_features=400, out_features=2, bias=True)
)

해당 모듈은 Embedding 레이어, 4개의 Conv2d 레이어, Linear 레이어로 구성되어 있습니다. Linear 레이어의 최종 값은 2이고 이는 (0,1) 둘 중에 하나의 값을 출력하게됩니다.

훈련용 데이터는 DataLoader를 통해서 데이터셋을 만들고 batch_size = 1000, epoch = 1000으로 학습을 수행합니다. 테스트 환경은 구글 코랩 프로(Colab Pro) 버전을 사용합니다. 일반 Colab 버전을 사용해서 테스트 하셔도 무방합니다. 속도의 차이가 있지만 그리 큰 차이는 아닌듯 합니다.

최종 학습이 수행하고 나온 model과 word2index 파일을 저장합니다.

이제 저장된 모델을 django를 통해서 간단한 웹서버를 만들어봅니다. 참고로 해당 부분에 대한 설명은 이번 글에서는 하지 않고 Django를 통해서 웹서버를 개발하는 예제는 이후에 다른 글에서 다뤄보겠습니다.

위와 같은 형태로 간단한 입력과 출력 결과를 표시합니다. 결과에 보면 “영화관”은 비속어가 아닌데 비속어로 처리된 부분이 있습니다. 이 부분은 영화관이라는 단어가 비속어 데이터에 추가 되어 있기 때문에 표시된 부분입니다. 학습용 데이터의 중요성이 다시 한번 확인되네요.

CNN을 통해서 필터링 하면 철자에 오타가 있거나 단어의 조합인 경우 앞뒤 순서가 바뀌는 경우에도 비교적 잘 탐지 하는 것을 확인했습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다