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