앞서 Python으로 어떻게 초성을 추출할지, 초성과 한글은 어떤 구조들을 가지고 있는지 등을 알아보았습니다. 한글을 가지고 놀아봅시다 - part I 이제 이 스킬을 바탕으로 제가 궁극적으로 원하는 초성검색 기능을 구현해 가도록 하겠습니다 :) 사실 이런 기능을 경험해본 적이 없는지라 이런 저런 생각과 접근을 많이 해봤습니다. 혹시라도 더 좋은 제안이나 생각이 있으시면 기탄없이 말씀해 주시면 감사하겠습니다 :)

먼저, 검색되어야 할 문장들이 있습니다. 이 문장들은 환자들이 주로 사용하는 문장들을 기반으로 작성되었다고 합니다. 그리고 각 문장에 따라서 어떤 과에 관련된 것인지, 그리고 그에 따른 키워드는 어떤 것들이 있는지를 포함한 아주 정교하게 전처리가 된 데이터셋을 받게 되었습니다. (고생하셨습니다~)

데이터셋은 대충 아래와 같이 생겼습니다.

문장 분류1 분류2 분류3 태그1 태그2 태그3 태그4
소변에 피가 섞여 나옵니다. 증상 비뇨기   소변 오줌  

이럴 경우 위와 같은 문장은 ‘ㅅㅂ’, ‘ㅇㅈ’, ‘ㅍ’ 이런 초성들로 검색을 했을 경우 검색이 되어야 할 것입니다. 여러 가지 다양한 방법들을 생각해보다가 제가 생각한 첫번째 버전의 방법은 검색어(초성)를 문장에서 검색하는 것이 아닌 오히려 태그의 초성들을 검색어에서 찾아보는 방법이었습니다. 검색어를 문장에서 검색하는 것은 너무 variation이 커지기 때문에 정확도를 올리기 힘들고, 또 엉뚱한 결과들만 오히려 나타내기 쉽상이라는 결론을 얻었습니다. (몇번의 삽질과 실험들을 거쳐서 얻은 결론입니다.) 우리가 입력한 검색어에 얼마나 많은 태그의 단어들이 포함되는지를 나타낼 수 있다면, 조사들도 신경쓰지 않아도 되고 정확한 검색을 할 수 있지 않을까 생각했습니다. 물론 현재는 태그의 전체가 포함됐는지를 검색하지만, 앞으로의 버전들에서는 오탈자까지도 감안한 검색이 되게 만들어야겠죠… 어쨌든 일단 첫 작품을 만들어 내기 위해서 필요한 것들을 구현해 보겠습니다.

첫번째로 주어진 한글 문자열에서 초성만 뽑아내서 새로운 문자열을 리턴해주는 함수를 만들어 보겠습니다.

def convertToInitialLetters(text):
    CHOSUNG_START_LETTER = 4352
    JAMO_START_LETTER = 44032
    JAMO_END_LETTER = 55203
    JAMO_CYCLE = 588
    
    def isHangul(ch):
        return ord(ch) >= JAMO_START_LETTER and ord(ch) <= JAMO_END_LETTER
    
    result = ""
    for ch in text:
        if isHangul(ch): #한글이 아닌 글자는 걸러냅니다.
            result += unichr((ord(ch) - JAMO_START_LETTER)/JAMO_CYCLE + CHOSUNG_START_LETTER)
        
    return result

함수가 잘 동작하는지 확인해 보겠습니다.

print convertToInitialLetters("소변에 피가 섞여 나옵니다.".decode('utf-8'))
# ᄉᄇᄋᄑᄀᄉᄋᄂᄋᄂᄃ

무사히 초성들을 잘 걸러냄을 확인할 수 있죠? :)

이제 다음으로 원본 데이터를 읽어서 보관할 corpus 객체를 만들어 보겠습니다. 기본적으로 문장따로(‘sentence’), 분류따로(‘category’), 태그따로(‘tag’) 원본 데이터를 저장하고, 우리가 사용할 태그들만 초성처리를 해서 ‘tag_initial’이라는 항목으로 추가 생성을 하겠습니다.

f = open("hospital sentences.tsv", "r") #파일을 읽어옵니다.
corpus = []
for line in f:
    columns = line.decode('utf-8').strip('\n').split('\t') #각 문장당 8개의 열
    element = {}
    element['sentence'] = columns[0]
    element['category'] = set()
    for i in range(1, 4):
        if columns[i]:
            element['category'].add(columns[i])
    element['tag'] = set()
    element['tag_initial'] = set()
    for i in range(4, 8):
        if columns[i]:
            element['tag'].add(columns[i])
            element['tag_initial'].add(convertToInitialLetters(columns[i]))
    corpus.append(element)

f.close() #파일을 닫습니다.

결국 결과적으로 corpus 객체는 list구조를 가지는데, 각 element는 문장별로 문장, 분류, 태그, 초성태그의 정보를 가지고 있는 것입니다. 예로 초성태그들만 한번 쭉 출력해 보시려면,

for item in corpus:
    print item['tag_initial']

# set([u'\u1111', u'\u1109\u1107', u'\u110b\u110c'])
# set([u'\u1111', u'\u1109\u1107', u'\u110b\u110c'])
# set([u'\u1111', u'\u1103\u1107', u'\u1104'])
# ...

태그들이 초성들만 잘 빠져서 들어가있음을 확인할 수 있죠? :) 이제 제법 대부분이 갖춰졌네요 :) 그럼 이제 마지막으로 초성검색어가 입력 됐을 때 N개의 상위 관련 문장을 출력해주는 함수를 만들어 볼까요~?

def findTopNRelatedSentences(inputInitialLetters, N, corpus):
    searchResults = []
    for item in corpus:
        totalLengthOfOcc = 0
        for tag in item['tag_initial']:
            if tag in inputInitialLetters:
                totalLengthOfOcc += len(tag)
        searchResults.append((totalLengthOfOcc, item['sentence']))
    return sorted(searchResults, reverse=True)[:N] #상위 N개의 검색 결과만 리턴합니다.

위 함수에서의 키포인트는 totalLengthOfOcc에다가 단순히 +1을 하는 것이 아니라 +len(tag)을 하는 것입니다! 이렇게 했던 이유는, 글자가 한글자인 태그들이 존재하는데, (‘입’, ‘피’ 등..) 이러한 것들과 (‘오줌’, ‘다리’, ‘불면증’ 등..) 이렇게 더 길고 정확한 태그들이 같은 weight으로 점수를 산정하게 되면 정확한 검색결과들이 가려진다는 점 때문입니다. 매우 간단하지만 이런 변화는 큰 알고리즘의 성공을 가지고 왔습니다! +_+

어쨌건 그러면 이제 우리가 원하는 기능을 발휘하는지 테스트 해볼까요~?

inputLetters = u"\u1100\u1109"
print "Input :", inputLetters
print "검색결과 : "
searchResults = findTopNRelatedSentences(inputLetters, 5, corpus)
for totalOcc, sentence in searchResults:
    print totalOcc, sentence

# Input : ᄀᄉ
# 검색결과 : 
# 2 심장이 빨리뜁니다.
# 2 숨쉬기가 힘듭니다.
# 2 갈비뼈 주변에 통증이 있습니다.
# 2 가슴이 두근거립니다.
# 2 가슴이 답답합니다.

inputLetters = u"\u1103\u1105"
print "Input :", inputLetters
print "검색결과 : "
searchResults = findTopNRelatedSentences(inputLetters, 5, corpus)
for totalOcc, sentence in searchResults:
    print totalOcc, sentence

# Input : ᄃᄅ
# 검색결과 : 
# 2 허벅지 감각이 이상합니다.
# 2 종아리가 아픕니다.
# 2 다리의 감각이 이상합니다.
# 2 다리가 저립니다.
# 2 다리가 아픕니다.

inputLetters = u"\u110b\u1109"
print "Input :", inputLetters
print "검색결과 : "
searchResults = findTopNRelatedSentences(inputLetters, 5, corpus)
for totalOcc, sentence in searchResults:
    print totalOcc, sentence

# Input : ᄋᄉ
# 검색결과 : 
# 3 위산이 목으로 올라옵니다.
# 3 삼키는게 어렵습니다.
# 2 입술이 아픕니다.
# 2 입술이 건조합니다.
# 2 오심이 있습니다.

inputLetters = u"\u110b\u110c"
print "Input :", inputLetters
print "검색결과 : "
searchResults = findTopNRelatedSentences(inputLetters, 5, corpus)
for totalOcc, sentence in searchResults:
    print totalOcc, sentence

# Input : ᄋᄌ
# 검색결과 : 
# 2 소변에 피가 섞여 나옵니다.
# 2 소변보는게 불편합니다.
# 1 혓바닥이 아픕니다.
# 1 입맛이 없습니다.
# 1 입 안이 건조합니다.

검색결과에 score들까지 보면 굉장히 간단한 알고리즘인데도 강력한 결과를 가져옴을 확인할 수 있죠~? 앞으로 더 개선해나갈 점이 많겠지만, 스타트로 나쁘지 않았던 것 같습니다 :) 정답은 없으니 여러분들도 다양한 생각들을 쉽게 구현해 보셨으면 좋겠네요 :)