👨🏻‍🏫IT 활동/인공지능교육 - NLP

[NLP] Day 20 - Project2

728x90
반응형

Project 2

비정형 데이터를 이용한 검색엔진

가중치와 Similarity를 이용 

In [2]:
import os

def getFileList(base='./', ext='.txt'):
    fileList = list()
    
    for file in os.listdir(base):
        if file.endswith(ext): # == if file.split('.')[-1] == ext:
            fileList.append('{0}/{1}'.format(base, file))
            
    return fileList
In [3]:
# def getNewsRank():
#     for file in os.listdir(base):
#         with open(file,encoding='utf-8') as f:
#             content = f.read()
#         return content
In [4]:
def getContent(file):
    with open(file, encoding='utf-8') as f:
        content = f.read()
    
    return content
In [5]:
corpus= list()
for a in getFileList('./News'):
    document = getContent(a)
    corpus.append(document)
In [6]:
type(corpus)
Out[6]:
list
In [7]:
len(corpus)
Out[7]:
125

전처리

In [8]:
def ngramEojeol(sentence, n=2):
    tokens = sentence.split()
    ngram = []
    
    for i in range(len(tokens) - n + 1):
        ngram.append(' '.join(tokens[i:i + n]))    
        
    return ngram
In [9]:
def ngramUmjeol(term, n=2):
    ngram = []
    
    for i in range(len(term) - n + 1):
        ngram.append(''.join(term[i:i + n]))   
        
    return ngram
In [10]:
def getPatternList():
    patternList = {}
    patternList['Korean'] = re.compile(r'([^ㄱ-ㅎㅏ-ㅣ가-힣]+)')
    patternList['rmeng'] = re.compile(r'[a-zA-Z]')
    patternList['Email'] = re.compile(r'(\w+@[a-zA-Z0-9\-\_]{3,}(.[a-zA-Z]{2,})+)')
    patternList['Whitespace'] = re.compile(r'\s{2,}')
    patternList['Punctuation'] =  re.compile(r'[%s]{2,}' % re.escape(punctuation))
    patternList['Punctuation2'] =  re.compile(r'[%s]' % re.escape(punctuation))
    return patternList
In [11]:
import re
from string import punctuation
corpus2 = list()

for i in range(len(corpus)):
    
    corpus[i] = getPatternList()['Email'].sub(" ", corpus[i])
    corpus[i] = getPatternList()['Whitespace'].sub(" ", corpus[i])
    corpus[i] = getPatternList()['Punctuation'].sub(" ", corpus[i])
    corpus[i] = getPatternList()['Punctuation2'].sub(" ", corpus[i])
    corpus[i] = getPatternList()['rmeng'].sub("", corpus[i])
#     corpus[i] = getPatternList()['Nonword'].sub(" ", corpus[i])
#     corpus[i] = getPatternList()['Numeric'].sub(" ", corpus[i])
#     corpus[i] = getPatternList()['Maxlength'].sub(" ", corpus[i])
    corpus2.append(corpus[i])
In [12]:
from nltk.tokenize import sent_tokenize

newCorpus = list()
for i in range(len(corpus)):
    tokenNews = sent_tokenize(corpus[i]) 
    newCorpus.extend(tokenNews)
In [13]:
# sent_tokenize한 corpus
# newCorpus
In [14]:
from konlpy.tag import Kkma

dictTerm = list()
dictPos = list()
dictNoun = list()
dictNgram = list()

for sentence in newCorpus:
    for token in sentence.split():
        if len(token) > 1:
            dictTerm.append(token)
            dictPos.extend([morpheme for morpheme in Kkma().morphs(token) if len(morpheme) > 1])
            dictNoun.extend([noun for noun in Kkma().nouns(token) if len(noun) > 1])
            dictNgram.extend(ngramUmjeol(token))
        
dictTerm = list(set(dictTerm))
dictPos = list(set(dictPos))
dictNoun = list(set(dictNoun))
dictNgram = list(set(dictNgram))
In [15]:
len(dictTerm), len(dictPos), len(dictNoun), len(dictNgram)
Out[15]:
(18933, 7084, 8797, 15771)
In [657]:
# print(dictTerm)
In [658]:
# print(dictPos)
In [659]:
# print(dictNoun)
In [660]:
# print(dictNgram)
In [20]:
def getNewsBySet():
    corpus3 = list()
    for docName in getFileList('./News'):
        document = getContent(docName)
        
        for token in document.split():
            corpus3.append(token)

    return list(set(corpus3))
In [661]:
txt = getNewsBySet()
# txt
In [22]:
from konlpy.tag import Kkma
from collections import defaultdict

ma = Kkma().morphs
def getNewsReprByDefaultDict(document):
    docRepr = defaultdict(lambda: defaultdict(int))
    
    for docName in getFileList('./News'):
        document = getContent(docName)
        
        for token in document.split():
            for morpheme in ma(token):
                docRepr[docName][morpheme] += 1
    return docRepr
In [ ]:
DTM = getNewsReprByDefaultDict(txt)
# DTM

가중치

In [24]:
from math import log10

def rawTF(freq):
    return freq

def normTF(freq,totalCount):
    return (freq / totalCount)

def logTF(freq):
    if freq > 0:
        return 1 + log10(freq)
    else:
        return 0

def maxTF(a,freq,maxFreq):   # double normalization K -  doc : 0 / query : 0.5
    return a + ((1-a)* (freq/maxFreq))
In [25]:
def convertInvertedDocument(DTM):
    TDM = defaultdict(lambda: defaultdict(int))
    
    for fileName, termList in DTM.items():  
        maxFreq = max(termList.values())
        for term, freq in termList.items():
            TDM[term][fileName] = maxTF(0,freq,maxFreq)
            
    return TDM
In [ ]:
TDM = convertInvertedDocument(DTM)
# TDM
In [27]:
# 일반적인 IDF
def rawIdf(df, N):
    return log10(N / df)

# the,a, 불용어 안날림 => to be or not to be 
def smoothingIdf(df,N):
    return log10((N+1) / df)

def probabilityIdf(df,N):
    return log10((N-df+1) / df)
In [28]:
# term-document -> term weight
# defaultdict 를 써서 key 걱정을 안해도 된다. 
N = len(DTM)

def TDM2TWM(TDM):
    TWM = defaultdict(lambda: defaultdict(float))
    DVL = defaultdict(float)
    
    for term, tfList in TDM.items():
        df = len(tfList)
        idf = rawIdf(df,N)
        for fileName, tf in tfList.items():
            TWM[term][fileName] = tf * idf
            DVL[fileName] += TWM[term][fileName]  ** 2
            
    return TWM, DVL
    
In [29]:
TWM,DVL = TDM2TWM(TDM)
In [ ]:
# TWM
In [ ]:
globalTF = list()
globalDocument = list()


for (docName, docContent) in enumerate(newCorpus):
    docIdx = len(globalDocument)
    globalDocument.append(docName)
    localPosting = dict()
    maxCount = 0
    # 로컬 / 띄어쓰기 단위로 
    for term in docContent.lower().split():   
        maxCount += 1
        if term not in localPosting.keys():
            localPosting[term] = 1
        else:
            localPosting[term] += 1
            
    print(docName)
    a = 0.5
    maxFreq = max(localPosting.values())
    
    for term,freq in localPosting.items():
#         print("1. {0} rawTF : {1}".format(term,rawTF(freq)))
#         print("2. {0} normTF : {1}".format(term,normTF(freq,maxCount)))
#         print("3. {0} logTF : {1}".format(term,logTF(freq)))
#         print("4. {0} maxTF : {1}".format(term,maxTF(a,freq,maxFreq)))
#         print()
        localPosting[term] = maxTF(a,freq,maxFreq)
        
    for indexTerm, termTF in localPosting.items():
        if indexTerm not in localPosting.keys():
            lexiconIdx = len(localPosting)
            postingIdx = len(globalTF) # fseek
            postingData = (lexiconIdx,docIdx,termTF, -1)
            globalTF.append(postingData)
            localPosting[indexTerm] =  postingIdx   # globalPosting 위치 (ptr:idx)
        else:
            lexiconIdx = list(localPosting.keys()).index(indexTerm)
            postingIdx =  len(globalTF)
            beforeIdx = localPosting[indexTerm]
            postingData = (lexiconIdx,docIdx,termTF, beforeIdx)
            globalTF.append(postingData)
            localPosting[indexTerm] =  postingIdx           
In [32]:
globalLexicon = dict()
globalDocument = list()
globalPosting = list()

for (docName, docContent) in enumerate(newCorpus):
    # Pointer 대체용, Key, Document이름은 절대로 겹치지 않는다는 가정
    docIdx = len(globalDocument)
    globalDocument.append(docName)
    
    # {단어idx:빈도, 단어idx:빈도, ...}
    localPosting = dict()
    
    # 로컬 / 띄어쓰기 단위로 
    for term in docContent.lower().split():   
        if term not in localPosting.keys():
            localPosting[term] = 1
        else:
            localPosting[term] += 1
    
    maxFreq = max(localPosting.values())
    
    # fp -> struct(단어,빈도) ( localPosting)
    # Merge와 sorting이 같이 있는 것 
    for indexTerm, termFreq in localPosting.items():
        if indexTerm not in globalLexicon.keys():
            lexiconIdx = len(globalLexicon)
            postingIdx = len(globalPosting) # fseek
            postingData = [lexiconIdx,docIdx,maxTF(0,termFreq,maxFreq), -1]
            globalPosting.append(postingData)
            globalLexicon[indexTerm] =  postingIdx   # globalPosting 위치 (ptr:idx)
        else:
            lexiconIdx = list(globalLexicon.keys()).index(indexTerm)
            postingIdx =  len(globalPosting)
            beforeIdx = globalLexicon[indexTerm]
            postingData = [lexiconIdx,docIdx,maxTF(0,termFreq,maxFreq), beforeIdx]
            globalPosting.append(postingData)
            globalLexicon[indexTerm] =  postingIdx   # globalPosting 위치 (ptr:idx)
#     print(localPosting)
# print(globalDocument)  
#         if term not in globalLexicon.keys():
#             localPosting
#             lexiconIdx = len(globalLexicon)   # 처음엔 Length가 0일 것. 
            
            
In [33]:
query = "서울시에 거래되는 아파트 전세값은?"
In [34]:
queryRepr = defaultdict(int) # 빈도를 갖는

for token in query.split(): 
    for morpheme in ma(token):
        queryRepr[morpheme] += 1
        
queryWeight = defaultdict(float)
maxFreq = max(queryRepr.values())

for token, freq in queryRepr.items():
    if token in TWM.keys():
        tf = maxTF(0.5,freq,maxFreq)
        df = len(TWM[token])
        idf = rawIdf(df,N)
        queryWeight[token] = tf * idf
In [35]:
queryWeight
Out[35]:
defaultdict(float,
            {'서울시': 1.494850021680094,
             '에': 0.0,
             '거래': 1.193820026016113,
             '되': 0.01055018233330815,
             '는': 0.0,
             '아파트': 1.6197887582883939,
             '값': 1.7958800173440752,
             '은': 0.0,
             '?': 0.453457336521869})
In [48]:
def innerProduct(x,y):
    return x * y
In [ ]:
from math import sqrt

candidateList = defaultdict(float)

for token, weight in queryWeight.items():
    for fileName, tfidf in TWM[token].items():
#         print(" {0} : {1} = {2} * {3}".format(
        token, fileName,weight,tfidf))
        candidateList[fileName] += innerProduct(weight, tfidf)
        
for fileName, sumProduct in candidateList.items():
    candidateList[fileName] /= sqrt(DVL[fileName])
In [37]:
from nltk.tokenize import sent_tokenize
K=5

resultList = sorted(candidateList.items(), key = lambda x:x[1], reverse=True)

for i,(fileName, similarity) in enumerate(resultList):
    if i < K:
        print(" Rank:{0} / Document:{1} / Similarity:{2:.4f}".format((i+1),fileName,similarity))
        
        with open(fileName,encoding='utf-8') as f:
            content = f.read()
            content = sent_tokenize(content)
            
        print(content[:5])     
 Rank:1 / Document:./News/세계_0000718407.txt / Similarity:0.4094
['\n\n\n\n\n// flash 오류를 우회하기 위한 함수 추가\nfunction _flash_removeCallback() {}\n\n『알쓸中잡』알고 보면 쓸모 있을 수 있는 중국 잡학사전138만 위안.', '최근 중국 온라인 경매에서 낙찰된 베이징 아파트 주차장 한 면의 가격입니다.', '원화로 하면 2억 3천만 원이 넘는 돈입니다.', '중국신문망에 따르면 138만 위안이면 중국 각 성의 수도에서 100 평방미터 남짓한 집 한 채를 살 수 있고, 후선 도시에서는 2채도 사는 것이 가능한 금액입니다.먼저, 중국 아파트의 특징을 말씀드려야 할 것 같습니다.', '중국의 많은 아파트들은 집값에 주차장이 포함돼 있지 않습니다.']
 Rank:2 / Document:./News/경제_0002892918.txt / Similarity:0.1386
['\n\n\n\n\n// flash 오류를 우회하기 위한 함수 추가\nfunction _flash_removeCallback() {}\n\n[현장에서]1339만 가구 공동주택 공시가격550명이 4개월여간 조사·산정정부 "세부 산정 과정 공개 못한다"국민들 의혹·불신 더욱 키워  지난 14일 서울시의 아파트 단지들 [연합뉴스]           한 사람이 하루에 약 180가구의 공시가격을 계산했다.', '최근 발표된 공동주택 공시가격 이야기다.', '한국감정원 직원 550명이 지난해 8월 27일부터 올해 1월 11일까지 138일 동안 1339만 가구를 조사했는데, 이를 단순화해 본 것이다.', '조사 기간에서 쉬는 날을 빼면 1명이 하루에 맡아야 할 집 수는 180가구 이상이다.', '여기다 조사 인력 상당수는 비슷한 시기 단독주택 22만 가구의 공시가격 산정 업무도 떠안았다.']
 Rank:3 / Document:./News/세계_0003891624.txt / Similarity:0.1146
['\n\n\n\n\n// flash 오류를 우회하기 위한 함수 추가\nfunction _flash_removeCallback() {}\n\n\t\n\t(서울=뉴스1) 박형기 기자 = 중국 여대생들이 최고 5만 위안(841만원)을 받고 난자를 팔고 있는 것으로 드러났다고 중국의 온라인매체인 ‘thepaper.cn’이 20일 보도했다.이 매체는 한 여대생과 인터뷰를 통해 여대생들이 어떻게 난자를 팔고 있는 지를 적나라하게 소개했다.', '◇ 난자 가격 최소 1만위안 최고 5만위안 : 중국의 여대생들은 부채를 갚거나 용돈을 벌기 위해 난자를 판다.', '난자의 가격은 최소 1만 위안(168만원)에서 최고 5만 위안을 호가한다.', '만약 키도 크고 예쁘면 값이 더 올라간다.', '여대생의 난자가 인기를 끌고 있는 것은 중국의 가족계획가 깊은 관계가 있다.']
 Rank:4 / Document:./News/경제_0004330537.txt / Similarity:0.0660
['\n\n\n\n\n// flash 오류를 우회하기 위한 함수 추가\nfunction _flash_removeCallback() {}\n\n가맹점\xa0모두\xa0법\xa0시행전에\xa0가맹계약\xa0체결적용돼도\xa0배상은\xa0소송-분쟁조정신청\xa0해야전문가\xa0"조정신청이\xa0가장\xa0현실적인\xa0방법"          빅뱅 승리가 운영하는 프렌차이즈 \'아오리라멘\'이 불매운동 움직임까지 나타나며 매출감소 직격탄을 맞았지만 \'프랜차이즈 오너리스크 배상법\'의 적용은 어려운 것으로 확인됐다.', '현재 운영중인 가맹점은 모두 법 시행 이전에 가맹계약을 체결했고 실제 배상을 위해서는 소송으로 가야한다.', '공정거래위원회 분쟁조정신청이 현실적인 대안이라는 게 전문가들의 조언이다.', '14일 공정위 관계자는 "가맹사업거래의 공정화에 관한 법률 개정으로 올해부터 가맹본부 대표나 임원이 위법행위·이미지 실추 등으로 점주에게 손해를 끼치면 손해배상 책임이 있다"면서 "신규 계약을 하거나 갱신계약을 한 경우에는 계약서에 이런 손해배상 책임이 있다는 내용을 넣도록 했다"고 말했다.', '문제는 현재 국내에서 운영중인 아오리라멘 가맹점은 모두 법 개정 이전에 계약을 체결했다는 점이다.']
 Rank:5 / Document:./News/사회_0009124156.txt / Similarity:0.0602
['\n\n\n\n\n// flash 오류를 우회하기 위한 함수 추가\nfunction _flash_removeCallback() {}\n\n베일 벗기 시작한 \'이희진 부모 피살 사건\'경찰 "2000만원 때문에 청부살인 석연치 않아"5억원 중 1800만원 빼고는 아직 행방 몰라【안양=뉴시스】추상철 기자 = \'청담동 주식부자\'로 불리는 이희진 씨의 부모를 살해한 혐의를 받는 김모 씨가 20일 오전 구속 전 피의자 심문(영장실질심사)를 받기 위해 경기 안양동안경찰서를 나서고 있다.', '2019.03.20. scchoo@newsis.com【안양=뉴시스】 조성필 기자 = 경기 안양에서 발생한 \'청담동 주식부자\' 이희진 씨 부모 피살 사건의 실체가 경찰 수사결과 조금씩 드러나고 있다.20일 안양동안경찰서 등에 따르면 이 사건의 피의자인 김모(34)씨는 전날 경찰조사에서 "내가 죽이지 않았다"며 혐의를 부인한 것으로 전해졌다.김 씨는 이날 오전 구속 전 피의자심문(영장실질심사)을 위해 안양동안경찰서를 나서면서도 "내가 안 죽였다.', '억울하다"고 했다.자신이 고용한 중국 동포 공범 3명이 범행을 주도했다는 주장인 셈이다.', '이전까지 경찰 수사에서는 김 씨가 이 사건의 주범격 피의자였다.경찰 관계자는 "수사가 진행 중이라서 구체적인 진술 내용은 알려줄 수 없다"면서도 "피의자가 혐의를 부인한 것은 맞다"고 했다.경찰은 김 씨가 자신의 죄를 중국으로 달아난 공범들에게 뒤집어 씌우려고 이 같은 진술을 했을 가능성도 열어두고 있다.', '또 김 씨의 진술과 별개로 강도살인 혐의를 그대로 적용할 방침인 것으로 알려졌다.전날 조사에서는 사라진 5억원의 행방도 어느 정도 밝혀졌다.']


728x90
반응형