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