word_frequecy를 활용한 영단어 분리기 toy project


약 6개월 전에, 3일 동안 간단하게 만든 영어 단어 분리기입니다.

함수에 넣으면 최적의 경우의 수를 찾아서 영어 단어를 분리해주는 방식으로

애초에는 주어진 데이터로 정확도를 산출하는 것이었지만, 데이터가 비공개 이므로 예시로만 보여드리고

하도 블로그에 내용을 안 올린 것 같아서 깃허브와 캐글에 업로드 하는 김에

여기에도 업로드를 진행합니다.

1. 사전 설명 및 패키지 import

해당 notebook의 경우, 복합적인 영어 단어를 단순한 단어로 분리를 시키는 과제에 대한 풀이입니다.

데이터는 비공개이기 때문에, 예시의 단어들로만 결과를 보여드리며,

실제 데이터를 가지고 한 결과에서는 해당 알고리즘만 가지고는 500개의 단어를 가지고

약 97%의 정확도를 가졌었습니다.

현재는 노트북에서 함수로만 실행이 가능하지만, 나중에는 class로 구현하여서

간편하게 불러오고, cmd 창에서 바로 실행이 가능하도록 알고리즘을 구현하는 것을 생각은 하고 있습니다.

In [1]:
import pandas as pd # 데이터 전처리
import numpy as np # 데이터 전처리
import random #데이터 전처리

from pandas import DataFrame #데이터 전처리
from collections import Counter #데이터 전처리

import re
import nltk
nltk.download('words')
from nltk.corpus import words, brown
[nltk_data] Downloading package words to
[nltk_data]     C:\Users\user\AppData\Roaming\nltk_data...
[nltk_data]   Package words is already up-to-date!

2. 핵심 알고리즘

인터넷에서 찾은 hashtag splitter의 함수입니다.

In [ ]:
word_dictionary = list(set(words.words()))
for alphabet in "bcdefghjklmnopqrstuvwxyz":
    word_dictionary.remove(alphabet)

def split_hashtag_to_words_all_possibilities(hashtag):
    all_possibilities = []

    split_posibility = [hashtag[:i] in word_dictionary for i in reversed(range(len(hashtag)+1))]
    possible_split_positions = [i for i, x in enumerate(split_posibility) if x == True]

    for split_pos in possible_split_positions:
        split_words = []
        word_1, word_2 = hashtag[:len(hashtag)-split_pos], hashtag[len(hashtag)-split_pos:]

        if word_2 in word_dictionary:
            split_words.append(word_1)
            split_words.append(word_2)
            all_possibilities.append(split_words)

            another_round = split_hashtag_to_words_all_possibilities(word_2)

            if len(another_round) > 0:
                all_possibilities = all_possibilities + [[a1] + a2 for a1, a2, in zip([word_1]*len(another_round), another_round)]
        else:
            another_round = split_hashtag_to_words_all_possibilities(word_2)

            if len(another_round) > 0:
                all_possibilities = all_possibilities + [[a1] + a2 for a1, a2, in zip([word_1]*len(another_round), another_round)]
                
    return all_possibilities

3. 단어 패턴 구분

하지만 이를 이대로 사용하기에는 무리 였던 점이,

해당 함수는 모든 경우의 수를 반환해주기 때문에, 매번 사용자가 확인도 해줘야되고 만약에 단어의 갯수가 엄청나게 많다면

이를 제대로 활용하지 못하게 됩니다.

In [3]:
def print_3(original):
    word_space=[]
    for i in original:
        if len(i)<=3:
            word_space.append(i)
    return word_space

그렇기 때문에, 최적의 경우의 수를 찾는 것을 생각하였고,

가장 좋은 경우는 단어가 최대 3개로 나누어지고, 마지막 글자에 따라서 패턴만 조정하는 방식을 선택하였습니다.

In [4]:
def print_er(original_word):
    word_space2=[]
    for j in original_word:
        if (len(j)==3) & ( len(j[-1])<=3 ):
            temp=[]
            temp.append( j[0] )
            temp.append( j[1]+j[2])
            word_space2.append(temp)
        else:
            word_space2.append(j)
    word_space=[]
    for i in word_space2:
        if 'er' in i:
            pass
        else:
            word_space.append(i)
    return [list(t) for t in set(tuple(element) for element in word_space)]

def print_ing(original_word):
    q=p = re.compile('ing$')
    word_space2=[]
    for j in original_word:
        if (len(j)==3) & ( len(j[-1])<=4 ):
            temp=[]
            temp.append( j[0] )
            temp.append( j[1]+j[2])
            word_space2.append(temp)
        elif (len(j)==3) & ( q.findall(j[-2])==['ing'] ):
            temp=[]
            temp.append( j[0]+j[1] )
            temp.append( j[2])
            word_space2.append(temp)
        else:
            word_space2.append(j)
    word_space=[]
    for i in word_space2:
        if 'ing' in i:
            pass
        else:
            word_space.append(i)
    return [list(t) for t in set(tuple(element) for element in word_space)]

def print_ed(original_word):
    word_space2=[]
    for j in original_word:
        if (len(j)==3) & ( len(j[-1])<=3 ):
            temp=[]
            temp.append( j[0] )
            temp.append( j[1]+j[2])
            word_space2.append(temp)
        else:
            word_space2.append(j)
    word_space=[]
    for i in word_space2:
        if 'ed' in i:
            pass
        else:
            word_space.append(i)
    return [list(t) for t in set(tuple(element) for element in word_space)]

최대 3개만 선택하게 한 이후에는, er, ing, ed로 끝나는 단어에 대한 구분을 추가하였고

In [ ]:
def print_man(original_word):
    raw=''.join(original_word[0])
    original_word.append( [ raw[:-3],raw[-3:] ]  )

    return [list(t) for t in set(tuple(element) for element in original_word)]

def print_wm(original_word):
    raw=''.join(original_word[0])
    original_word.append( [ raw[:-5],raw[-5:] ]  )

    return [list(t) for t in set(tuple(element) for element in original_word)]

나중에는 man, woman이라는 단어로 끝나는 경우에 대한 구분도 추가하였습니다.

4. English Word Frequency 활용

하지만, 그렇게 하더라도 최적의 경우가 나오지 않는 경우가 있었는데

가장 큰 것은 3개의 단어로 구분을 할 경우, 경우의 수가 2가지가 나오는 것이었습니다.

예를들어 blueberrycake라는 단어의 경우,

blue berry cake, blueberry cake로 나뉠 수가 있는데

어떠한 것이 최적의 경우인지 찾는 요소로 https://www.kaggle.com/rtatman/english-word-frequency 의 단어 빈도 데이터를 활용하였습니다.

In [ ]:
count_v = pd.read_csv("unigram_freq.csv")
count_v['type'] = [type(i) for i in count_v['word']]
count_v2=count_v[count_v['type']==str]
count_v2['len'] = [len(i) for i in count_v2['word']]

count_v2=count_v2[count_v2['len']>=2]

a, b, c 와 같은 한 글자 단어는 제외하고 사용하며,

to, in, by 등과 같은 전치사, 접속사 등의 단어들은 remove_list를 만들어서 제외하는 방식을 사용하였습니다.

In [7]:
remove_list=['to','in','by','go','of','in','on','as','the','and','up']
def regulatoin_list(next_word):
    word_space=[]
    if next_word==[]:
        return [next_word]
    else:
        for list1 in next_word:
            word_space2 = [len(i) for i in list1]
            if (1 in word_space2) :  #길이가 하나라도 1인 경우
                pass
            elif (word_space2.count(2)==2) :  # 2개 단어의 길이가 2인 경우는 비정상적이므로 제외
                pass
            else:
                word_space.append(list1)

        if len(word_space)>=2:
            sum_list=[]
            real_list=[]
            for splitting in word_space:
                if len(splitting)==2:
                    if ( (len(splitting[-1])==2) & ( splitting[-1] not in remove_list ) ) | ( ( len(splitting[-2])==2 ) & ( splitting[-2] not in remove_list ) ) :
                        pass
                    else:
                        real_list.append(splitting)
                else:
                    if ( len(splitting[-1])==2 ) | ( ( len(splitting[-2])==2 ) & ( splitting[-2] not in remove_list ) ) | ( ( len(splitting[-3])==2 ) & ( splitting[-3] not in remove_list ) ) :
                        pass
                    else:
                        real_list.append(splitting)

            for j in real_list:
                sum1 = 1
                for y in range(len(j)):
                    try:
                        sum1 += count_v[count_v['word']==j[y]].index[0]
                    except:
                        sum1 += 99999999
                sum_list.append(sum1)
            return real_list[ sum_list.index(min(sum_list)) ]
            
        elif len(word_space)==0:
            return []

        else:
            return word_space[0]

해당 알고리즘의 핵심은, 이미 단어가 분리가 된 상태에서

remove_list에 있는 단어들로는 분리가 되지 않으면서, word_frequecy 데이터의 빈도를 활용하여서

최대한 가장 잘 사용이 되는 단어가 최적의 경우의 수가 되도록 분리를 하는 방식을 사용한 것입니다.

5. word_frequecy 아이디어 적용

해당 함수들을 만든 다음에, 끝에 끝나는 글자들을을 통해서 각각 다른 함수가 적용이 되도록 하였습니다.

중간에 내용을 추가하다보니 불필요하게 if, elif 등으로 반복되는 내용이 들어가긴 했지만

3일안에 해당 알고리즘을 처리를 하여야 됬기 때문에 구현에만 초점을 두었습니다.

In [8]:
def word_space(j):
    p = re.compile('er$')
    
    if p.findall(j)==['er']:
        try:
            return regulatoin_list( print_er( print_3( split_hashtag_to_words_all_possibilities(j) ) ) ) 
        except:
            return [j]

    elif j.find("ing")>(-1):
        try:
            return regulatoin_list( print_ing( print_3( split_hashtag_to_words_all_possibilities(j) ) ) )
        except:
            return [j]

    elif j[-5:]=="woman":
        try:
            return regulatoin_list( print_wm( print_3( split_hashtag_to_words_all_possibilities(j) ) ) )
        except:
            return [j]

    elif (j[-3:]=="man") &  (j[-5:]!="woman"):
        try:
            return regulatoin_list( print_man( print_3( split_hashtag_to_words_all_possibilities(j) ) ) )
        except:
            return [j]
    elif j[-2:]=="ed" :
        try:
            return regulatoin_list( print_ed( print_3( split_hashtag_to_words_all_possibilities(j) ) ) )
        except:
            return [j]
    
    else:
        try:
            return regulatoin_list( print_3( split_hashtag_to_words_all_possibilities( j ) ) )
        except:
            return [j]

6. 결과 확인

In [26]:
print( word_space('snowman') )

print( word_space('longwinded') )

print( word_space('hashtagsplit') )

print( word_space('strawberry') )

print( word_space('strawberrycake') )

print( word_space('blueberrycake') )

print( word_space('watermelonsugar'))

print( word_space('watermelonsugarsalt'))

print( word_space('themselves'))
['snow', 'man']
['long', 'winded']
['hash', 'tag', 'split']
['straw', 'berry']
['strawberry', 'cake']
['blue', 'berry', 'cake']
['water', 'melon', 'sugar']
['watermelon', 'sugar', 'salt']
[[]]

일단 단어가 들어가면 무조건 분리가 일어나도록 진행이 되었으며,

단어 4개 이상으로 분리되는 경우는 어쩔수 없이 최대 3개가 되도록 처리를 하였으며

분리가 불가능한, 즉 아예 경우의 수가 없는 경우에는 빈 값이 나오도록 되어있습니다.

빈 값이 나오게 한 이유는 해당 단어는 직접 처리를 해야됨을 의미하며,

이는 함수 적용시 [[]]가 나오면 원본을 반환하도록 하는 것을 구현하면

간단하게 해결이 가능합니다.