├── .gitattributes ├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── setup.py ├── textrank ├── __init__.py ├── rank.py ├── sentence.py ├── summarizer.py ├── utils.py └── word.py └── tutorials ├── implement_pairwise_distance_with_numpy.md ├── keyword_keysentence_extractor.ipynb ├── lalaland.txt └── lalaland_komoran.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | tutorial.ipynb linguist-vendored 2 | tutorials/* linguist-vendored 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | 4 | # Python 5 | *.pyc 6 | __pycache__/ 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | 11 | # Experiments 12 | tmp/ 13 | 14 | # Jupyter notebook 15 | .ipynb_checkpoints/ 16 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2021 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Re-iplementation of TextRank [^1] 2 | 3 | To summarize La La Land user comments by keyword extraction with part-of-speech tagged documents 4 | 5 | ```python 6 | from textrank import KeywordSummarizer 7 | 8 | docs = ['list of str form', 'sentence list'] 9 | 10 | keyword_extractor = KeywordSummarizer( 11 | tokenize = lambda x:x.split(), # YOUR TOKENIZER 12 | window = -1, 13 | verbose = False 14 | ) 15 | 16 | keywords = keyword_extractor.summarize(sents, topk=30) 17 | for word, rank in keywords: 18 | # do something 19 | ``` 20 | 21 | 22 | You can specify word cooccurrence graph with arguments. 23 | 24 | ```python 25 | from textrank import KeywordSummarizer 26 | 27 | keyword_extractor = KeywordSummarizer( 28 | tokenize = lambda x:x.split() 29 | min_count=2, 30 | window=-1, # cooccurrence within a sentence 31 | min_cooccurrence=2, 32 | vocab_to_idx=None, # you can specify vocabulary to build word graph 33 | df=0.85, # PageRank damping factor 34 | max_iter=30, # PageRank maximum iteration 35 | bias=None, # PageRank initial ranking 36 | verbose=False 37 | ) 38 | ``` 39 | 40 | To summarize La La Land user comments by key-sentence extraction, 41 | 42 | 43 | ```python 44 | from textrank import KeysentenceSummarizer 45 | 46 | summarizer = KeysentenceSummarizer( 47 | tokenize = YOUR_TOKENIZER, 48 | min_sim = 0.5, 49 | verbose = True 50 | ) 51 | 52 | keysents = summarizer.summarize(sents, topk=5) 53 | for sent_idx, rank, sent in keysents: 54 | # do something 55 | ``` 56 | 57 | ``` 58 | 시사회 보고 왔어요 꿈과 사랑에 관한 이야기인데 뭔가 진한 여운이 남는 영화예요 59 | 시사회 갔다왔어요 제가 라이언고슬링팬이라서 하는말이아니고 너무 재밌어요 꿈과 현실이 잘 보여지는영화 사랑스런 영화 전 개봉하면 또 볼생각입니당 60 | 시사회에서 보고왔는데 여운쩔었다 엠마스톤과 라이언 고슬링의 케미가 도입부의 강렬한음악좋았고 예고편에 나왓던 오디션 노래 감동적이어서 눈물나왔다ㅠ 이영화는 위플래쉬처럼 꼭 영화관에봐야함 색감 노래 배우 환상적인 영화 61 | 방금 시사회로 봤는데 인생영화 하나 또 탄생했네 롱테이크 촬영이 예술 영상이 넘나 아름답고 라이언고슬링의 멋진 피아노 연주 엠마스톤과의 춤과 노래 눈과 귀가 호강한다 재미를 기대하면 약간 실망할수도 있지만 충분히 훌륭한 영화 62 | 황홀하고 따뜻한 꿈이었어요 imax로 또 보려합니다 좋은 영화 시사해주셔서 감사해요 63 | ``` 64 | 65 | You can also use KoNLPy as your tokenizer to summarize 20 sentences news with key-sentences or keywords. 66 | 67 | ```python 68 | sents = [ 69 | '오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다', 70 | '서울 연합뉴스 김은경 기자 사제 총기로 경찰을 살해한 범인 성모 46 씨는 주도면밀했다', 71 | '경찰에 따르면 성씨는 19일 오후 강북경찰서 인근 부동산 업소 밖에서 부동산업자 이모 67 씨가 나오기를 기다렸다 이씨와는 평소에도 말다툼을 자주 한 것으로 알려졌다', 72 | '이씨가 나와 걷기 시작하자 성씨는 따라가면서 미리 준비해온 사제 총기를 이씨에게 발사했다 총알이 빗나가면서 이씨는 도망갔다 그 빗나간 총알은 지나가던 행인 71 씨의 배를 스쳤다', 73 | '성씨는 강북서 인근 치킨집까지 이씨 뒤를 쫓으며 실랑이하다 쓰러뜨린 후 총기와 함께 가져온 망치로 이씨 머리를 때렸다', 74 | '이 과정에서 오후 6시 20분께 강북구 번동 길 위에서 사람들이 싸우고 있다 총소리가 났다 는 등의 신고가 여러건 들어왔다', 75 | '5분 후에 성씨의 전자발찌가 훼손됐다는 신고가 보호관찰소 시스템을 통해 들어왔다 성범죄자로 전자발찌를 차고 있던 성씨는 부엌칼로 직접 자신의 발찌를 끊었다', 76 | '용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기', 77 | '신고를 받고 번동파출소에서 김창호 54 경위 등 경찰들이 오후 6시 29분께 현장으로 출동했다 성씨는 그사이 부동산 앞에 놓아뒀던 가방을 챙겨 오패산 쪽으로 도망간 후였다', 78 | '김 경위는 오패산 터널 입구 오른쪽의 급경사에서 성씨에게 접근하다가 오후 6시 33분께 풀숲에 숨은 성씨가 허공에 난사한 10여발의 총알 중 일부를 왼쪽 어깨 뒷부분에 맞고 쓰러졌다', 79 | '김 경위는 구급차가 도착했을 때 이미 의식이 없었고 심폐소생술을 하며 병원으로 옮겨졌으나 총알이 폐를 훼손해 오후 7시 40분께 사망했다', 80 | '김 경위는 외근용 조끼를 입고 있었으나 총알을 막기에는 역부족이었다', 81 | '머리에 부상을 입은 이씨도 함께 병원으로 이송됐으나 생명에는 지장이 없는 것으로 알려졌다', 82 | '성씨는 오패산 터널 밑쪽 숲에서 오후 6시 45분께 잡혔다', 83 | '총격현장 수색하는 경찰들 서울 연합뉴스 이효석 기자 19일 오후 서울 강북구 오패산 터널 인근에서 경찰들이 폭행 용의자가 사제총기를 발사해 경찰관이 사망한 사건을 조사 하고 있다', 84 | '총 때문에 쫓던 경관들과 민간인들이 몸을 숨겼는데 인근 신발가게 직원 이모씨가 다가가 성씨를 덮쳤고 이어 현장에 있던 다른 상인들과 경찰이 가세해 체포했다', 85 | '성씨는 경찰에 붙잡힌 직후 나 자살하려고 한 거다 맞아 죽어도 괜찮다 고 말한 것으로 전해졌다', 86 | '성씨 자신도 경찰이 발사한 공포탄 1발 실탄 3발 중 실탄 1발을 배에 맞았으나 방탄조끼를 입은 상태여서 부상하지는 않았다', 87 | '경찰은 인근을 수색해 성씨가 만든 사제총 16정과 칼 7개를 압수했다 실제 폭발할지는 알 수 없는 요구르트병에 무언가를 채워두고 심지를 꽂은 사제 폭탄도 발견됐다', 88 | '일부는 숲에서 발견됐고 일부는 성씨가 소지한 가방 안에 있었다' 89 | ] 90 | ``` 91 | 92 | To summarize texts with key-sentences, 93 | 94 | ```python 95 | from konlpy.tag import Komoran 96 | 97 | komoran = Komoran() 98 | def komoran_tokenizer(sent): 99 | words = komoran.pos(sent, join=True) 100 | words = [w for w in words if ('/NN' in w or '/XR' in w or '/VA' in w or '/VV' in w)] 101 | return words 102 | 103 | summarizer = KeysentenceSummarizer( 104 | tokenize = komoran_tokenizer, 105 | min_sim = 0.3, 106 | verbose = False 107 | ) 108 | 109 | keysents = summarizer.summarize(sents, topk=3) 110 | ``` 111 | 112 | ``` 113 | 오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다 114 | 경찰에 따르면 성씨는 19일 오후 강북경찰서 인근 부동산 업소 밖에서 부동산업자 이모 67 씨가 나오기를 기다렸다 이씨와는 평소에도 말다툼을 자주 한 것으로 알려졌다 115 | 서울 연합뉴스 김은경 기자 사제 총기로 경찰을 살해한 범인 성모 46 씨는 주도면밀했다 116 | ``` 117 | 118 | To summarize texts with keywords, 119 | 120 | ```python 121 | from textrank import KeywordSummarizer 122 | 123 | summarizer = KeywordSummarizer(tokenize=komoran_tokenizer, min_count=2, min_cooccurrence=1) 124 | summarizer.summarize(sents, topk=20) 125 | ``` 126 | 127 | ``` 128 | [('용의자/NNP', 3.040833543583403), 129 | ('사제총/NNP', 2.505798518168069), 130 | ('성씨/NNP', 2.4254730689696298), 131 | ('서울/NNP', 2.399522533743009), 132 | ('경찰/NNG', 2.2541631612221043), 133 | ('오후/NNG', 2.154778397410354), 134 | ('폭행/NNG', 1.9019818685234693), 135 | ('씨/NNB', 1.7517679455874249), 136 | ('발사/NNG', 1.658959293729613), 137 | ('맞/VV', 1.618499063577056), 138 | ('분/NNB', 1.6164369966921637), 139 | ('번동/NNP', 1.4681655196749035), 140 | ('현장/NNG', 1.4530182347939307), 141 | ('시/NNB', 1.408892735491178), 142 | ('경찰관/NNP', 1.4012941012332316), 143 | ('조사/NNG', 1.4012941012332316), 144 | ('일/NNB', 1.3922748983755766), 145 | ('강북구/NNP', 1.332317291003927), 146 | ('연합뉴스/NNP', 1.3259099432277819), 147 | ('이씨/NNP', 1.2869280494707418)] 148 | ``` 149 | 150 | 151 | ## References 152 | - ^1 : Mihalcea, R., & Tarau, P. (2004). Textrank: Bringing order into text. In Proceedings of the 2004 conference on empirical methods in natural language processing 153 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import textrank 3 | 4 | setup( 5 | name=textrank.__name__, 6 | version=textrank.__version__, 7 | url='https://github.com/lovit/textrank/', 8 | author=textrank.__author__, 9 | author_email='soy.lovit@gmail.com', 10 | description='TextRank based Summarizer (Keyword and key-sentence extractor)', 11 | packages=find_packages(), 12 | long_description=open('README.md', encoding="utf-8").read(), 13 | zip_safe=False, 14 | setup_requires=[] 15 | ) 16 | -------------------------------------------------------------------------------- /textrank/__init__.py: -------------------------------------------------------------------------------- 1 | __name__ = 'textrank' 2 | __author__ = 'lovit' 3 | __version__ = '0.1.2' 4 | 5 | from .summarizer import KeywordSummarizer 6 | from .summarizer import KeysentenceSummarizer 7 | -------------------------------------------------------------------------------- /textrank/rank.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.preprocessing import normalize 3 | 4 | def pagerank(x, df=0.85, max_iter=30, bias=None): 5 | """ 6 | Arguments 7 | --------- 8 | x : scipy.sparse.csr_matrix 9 | shape = (n vertex, n vertex) 10 | df : float 11 | Damping factor, 0 < df < 1 12 | max_iter : int 13 | Maximum number of iteration 14 | bias : numpy.ndarray or None 15 | If None, equal bias 16 | 17 | Returns 18 | ------- 19 | R : numpy.ndarray 20 | PageRank vector. shape = (n vertex, 1) 21 | """ 22 | 23 | assert 0 < df < 1 24 | 25 | # initialize 26 | A = normalize(x, axis=0, norm='l1') 27 | R = np.ones(A.shape[0]).reshape(-1,1) 28 | 29 | # check bias 30 | if bias is None: 31 | bias = (1 - df) * np.ones(A.shape[0]).reshape(-1,1) 32 | else: 33 | bias = bias.reshape(-1,1) 34 | bias = A.shape[0] * bias / bias.sum() 35 | assert bias.shape[0] == A.shape[0] 36 | bias = (1 - df) * bias 37 | 38 | # iteration 39 | for _ in range(max_iter): 40 | R = df * (A * R) + bias 41 | 42 | return R 43 | -------------------------------------------------------------------------------- /textrank/sentence.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | import math 3 | import numpy as np 4 | import scipy as sp 5 | from scipy.sparse import csr_matrix 6 | from sklearn.metrics import pairwise_distances 7 | 8 | from .utils import scan_vocabulary 9 | from .utils import tokenize_sents 10 | 11 | 12 | def sent_graph(sents, tokenize=None, min_count=2, min_sim=0.3, 13 | similarity=None, vocab_to_idx=None, verbose=False): 14 | """ 15 | Arguments 16 | --------- 17 | sents : list of str 18 | Sentence list 19 | tokenize : callable 20 | tokenize(sent) return list of str 21 | min_count : int 22 | Minimum term frequency 23 | min_sim : float 24 | Minimum similarity between sentences 25 | similarity : callable or str 26 | similarity(s1, s2) returns float 27 | s1 and s2 are list of str. 28 | available similarity = [callable, 'cosine', 'textrank'] 29 | vocab_to_idx : dict 30 | Vocabulary to index mapper. 31 | If None, this function scan vocabulary first. 32 | verbose : Boolean 33 | If True, verbose mode on 34 | 35 | Returns 36 | ------- 37 | sentence similarity graph : scipy.sparse.csr_matrix 38 | shape = (n sents, n sents) 39 | """ 40 | 41 | if vocab_to_idx is None: 42 | idx_to_vocab, vocab_to_idx = scan_vocabulary(sents, tokenize, min_count) 43 | else: 44 | idx_to_vocab = [vocab for vocab, _ in sorted(vocab_to_idx.items(), key=lambda x:x[1])] 45 | 46 | x = vectorize_sents(sents, tokenize, vocab_to_idx) 47 | if similarity == 'cosine': 48 | x = numpy_cosine_similarity_matrix(x, min_sim, verbose, batch_size=1000) 49 | else: 50 | x = numpy_textrank_similarity_matrix(x, min_sim, verbose, batch_size=1000) 51 | return x 52 | 53 | def vectorize_sents(sents, tokenize, vocab_to_idx): 54 | rows, cols, data = [], [], [] 55 | for i, sent in enumerate(sents): 56 | counter = Counter(tokenize(sent)) 57 | for token, count in counter.items(): 58 | j = vocab_to_idx.get(token, -1) 59 | if j == -1: 60 | continue 61 | rows.append(i) 62 | cols.append(j) 63 | data.append(count) 64 | n_rows = len(sents) 65 | n_cols = len(vocab_to_idx) 66 | return csr_matrix((data, (rows, cols)), shape=(n_rows, n_cols)) 67 | 68 | def numpy_cosine_similarity_matrix(x, min_sim=0.3, verbose=True, batch_size=1000): 69 | n_rows = x.shape[0] 70 | mat = [] 71 | for bidx in range(math.ceil(n_rows / batch_size)): 72 | b = int(bidx * batch_size) 73 | e = min(n_rows, int((bidx+1) * batch_size)) 74 | psim = 1 - pairwise_distances(x[b:e], x, metric='cosine') 75 | rows, cols = np.where(psim >= min_sim) 76 | data = psim[rows, cols] 77 | mat.append(csr_matrix((data, (rows, cols)), shape=(e-b, n_rows))) 78 | if verbose: 79 | print('\rcalculating cosine sentence similarity {} / {}'.format(b, n_rows), end='') 80 | mat = sp.sparse.vstack(mat) 81 | if verbose: 82 | print('\rcalculating cosine sentence similarity was done with {} sents'.format(n_rows)) 83 | return mat 84 | 85 | def numpy_textrank_similarity_matrix(x, min_sim=0.3, verbose=True, min_length=1, batch_size=1000): 86 | n_rows, n_cols = x.shape 87 | 88 | # Boolean matrix 89 | rows, cols = x.nonzero() 90 | data = np.ones(rows.shape[0]) 91 | z = csr_matrix((data, (rows, cols)), shape=(n_rows, n_cols)) 92 | 93 | # Inverse sentence length 94 | size = np.asarray(x.sum(axis=1)).reshape(-1) 95 | size[np.where(size <= min_length)] = 10000 96 | size = np.log(size) 97 | 98 | mat = [] 99 | for bidx in range(math.ceil(n_rows / batch_size)): 100 | 101 | # slicing 102 | b = int(bidx * batch_size) 103 | e = min(n_rows, int((bidx+1) * batch_size)) 104 | 105 | # dot product 106 | inner = z[b:e,:] * z.transpose() 107 | 108 | # sentence len[i,j] = size[i] + size[j] 109 | norm = size[b:e].reshape(-1,1) + size.reshape(1,-1) 110 | norm = norm ** (-1) 111 | norm[np.where(norm == np.inf)] = 0 112 | 113 | # normalize 114 | sim = inner.multiply(norm).tocsr() 115 | rows, cols = (sim >= min_sim).nonzero() 116 | data = np.asarray(sim[rows, cols]).reshape(-1) 117 | 118 | # append 119 | mat.append(csr_matrix((data, (rows, cols)), shape=(e-b, n_rows))) 120 | 121 | if verbose: 122 | print('\rcalculating textrank sentence similarity {} / {}'.format(b, n_rows), end='') 123 | 124 | mat = sp.sparse.vstack(mat) 125 | if verbose: 126 | print('\rcalculating textrank sentence similarity was done with {} sents'.format(n_rows)) 127 | 128 | return mat 129 | 130 | def graph_with_python_sim(tokens, verbose, similarity, min_sim): 131 | if similarity == 'cosine': 132 | similarity = cosine_sent_sim 133 | elif callable(similarity): 134 | similarity = similarity 135 | else: 136 | similarity = textrank_sent_sim 137 | 138 | rows, cols, data = [], [], [] 139 | n_sents = len(tokens) 140 | for i, tokens_i in enumerate(tokens): 141 | if verbose and i % 1000 == 0: 142 | print('\rconstructing sentence graph {} / {} ...'.format(i, n_sents), end='') 143 | for j, tokens_j in enumerate(tokens): 144 | if i >= j: 145 | continue 146 | sim = similarity(tokens_i, tokens_j) 147 | if sim < min_sim: 148 | continue 149 | rows.append(i) 150 | cols.append(j) 151 | data.append(sim) 152 | if verbose: 153 | print('\rconstructing sentence graph was constructed from {} sents'.format(n_sents)) 154 | return csr_matrix((data, (rows, cols)), shape=(n_sents, n_sents)) 155 | 156 | def textrank_sent_sim(s1, s2): 157 | """ 158 | Arguments 159 | --------- 160 | s1, s2 : list of str 161 | Tokenized sentences 162 | 163 | Returns 164 | ------- 165 | Sentence similarity : float 166 | Non-negative number 167 | """ 168 | n1 = len(s1) 169 | n2 = len(s2) 170 | if (n1 <= 1) or (n2 <= 1): 171 | return 0 172 | common = len(set(s1).intersection(set(s2))) 173 | base = math.log(n1) + math.log(n2) 174 | return common / base 175 | 176 | def cosine_sent_sim(s1, s2): 177 | """ 178 | Arguments 179 | --------- 180 | s1, s2 : list of str 181 | Tokenized sentences 182 | 183 | Returns 184 | ------- 185 | Sentence similarity : float 186 | Non-negative number 187 | """ 188 | if (not s1) or (not s2): 189 | return 0 190 | 191 | s1 = Counter(s1) 192 | s2 = Counter(s2) 193 | norm1 = math.sqrt(sum(v ** 2 for v in s1.values())) 194 | norm2 = math.sqrt(sum(v ** 2 for v in s2.values())) 195 | prod = 0 196 | for k, v in s1.items(): 197 | prod += v * s2.get(k, 0) 198 | return prod / (norm1 * norm2) 199 | -------------------------------------------------------------------------------- /textrank/summarizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .rank import pagerank 3 | from .sentence import sent_graph 4 | from .word import word_graph 5 | 6 | 7 | class KeywordSummarizer: 8 | """ 9 | Arguments 10 | --------- 11 | sents : list of str 12 | Sentence list 13 | tokenize : callable 14 | Tokenize function: tokenize(str) = list of str 15 | min_count : int 16 | Minumum frequency of words will be used to construct sentence graph 17 | window : int 18 | Word cooccurrence window size. Default is -1. 19 | '-1' means there is cooccurrence between two words if the words occur in a sentence 20 | min_cooccurrence : int 21 | Minimum cooccurrence frequency of two words 22 | vocab_to_idx : dict or None 23 | Vocabulary to index mapper 24 | df : float 25 | PageRank damping factor 26 | max_iter : int 27 | Number of PageRank iterations 28 | verbose : Boolean 29 | If True, it shows training progress 30 | """ 31 | def __init__(self, sents=None, tokenize=None, min_count=2, 32 | window=-1, min_cooccurrence=2, vocab_to_idx=None, 33 | df=0.85, max_iter=30, verbose=False): 34 | 35 | self.tokenize = tokenize 36 | self.min_count = min_count 37 | self.window = window 38 | self.min_cooccurrence = min_cooccurrence 39 | self.vocab_to_idx = vocab_to_idx 40 | self.df = df 41 | self.max_iter = max_iter 42 | self.verbose = verbose 43 | 44 | if sents is not None: 45 | self.train_textrank(sents) 46 | 47 | def train_textrank(self, sents, bias=None): 48 | """ 49 | Arguments 50 | --------- 51 | sents : list of str 52 | Sentence list 53 | bias : None or numpy.ndarray 54 | PageRank bias term 55 | 56 | Returns 57 | ------- 58 | None 59 | """ 60 | 61 | g, self.idx_to_vocab = word_graph(sents, 62 | self.tokenize, self.min_count,self.window, 63 | self.min_cooccurrence, self.vocab_to_idx, self.verbose) 64 | self.R = pagerank(g, self.df, self.max_iter, bias).reshape(-1) 65 | if self.verbose: 66 | print('trained TextRank. n words = {}'.format(self.R.shape[0])) 67 | 68 | def keywords(self, topk=30): 69 | """ 70 | Arguments 71 | --------- 72 | topk : int 73 | Number of keywords selected from TextRank 74 | 75 | Returns 76 | ------- 77 | keywords : list of tuple 78 | Each tuple stands for (word, rank) 79 | """ 80 | if not hasattr(self, 'R'): 81 | raise RuntimeError('Train textrank first or use summarize function') 82 | idxs = self.R.argsort()[-topk:] 83 | keywords = [(self.idx_to_vocab[idx], self.R[idx]) for idx in reversed(idxs)] 84 | return keywords 85 | 86 | def summarize(self, sents, topk=30): 87 | """ 88 | Arguments 89 | --------- 90 | sents : list of str 91 | Sentence list 92 | topk : int 93 | Number of keywords selected from TextRank 94 | 95 | Returns 96 | ------- 97 | keywords : list of tuple 98 | Each tuple stands for (word, rank) 99 | """ 100 | self.train_textrank(sents) 101 | return self.keywords(topk) 102 | 103 | 104 | class KeysentenceSummarizer: 105 | """ 106 | Arguments 107 | --------- 108 | sents : list of str 109 | Sentence list 110 | tokenize : callable 111 | Tokenize function: tokenize(str) = list of str 112 | min_count : int 113 | Minumum frequency of words will be used to construct sentence graph 114 | min_sim : float 115 | Minimum similarity between sentences in sentence graph 116 | similarity : str 117 | available similarity = ['cosine', 'textrank'] 118 | vocab_to_idx : dict or None 119 | Vocabulary to index mapper 120 | df : float 121 | PageRank damping factor 122 | max_iter : int 123 | Number of PageRank iterations 124 | verbose : Boolean 125 | If True, it shows training progress 126 | """ 127 | def __init__(self, sents=None, tokenize=None, min_count=2, 128 | min_sim=0.3, similarity=None, vocab_to_idx=None, 129 | df=0.85, max_iter=30, verbose=False): 130 | 131 | self.tokenize = tokenize 132 | self.min_count = min_count 133 | self.min_sim = min_sim 134 | self.similarity = similarity 135 | self.vocab_to_idx = vocab_to_idx 136 | self.df = df 137 | self.max_iter = max_iter 138 | self.verbose = verbose 139 | 140 | if sents is not None: 141 | self.train_textrank(sents) 142 | 143 | def train_textrank(self, sents, bias=None): 144 | """ 145 | Arguments 146 | --------- 147 | sents : list of str 148 | Sentence list 149 | bias : None or numpy.ndarray 150 | PageRank bias term 151 | Shape must be (n_sents,) 152 | 153 | Returns 154 | ------- 155 | None 156 | """ 157 | g = sent_graph(sents, self.tokenize, self.min_count, 158 | self.min_sim, self.similarity, self.vocab_to_idx, self.verbose) 159 | self.R = pagerank(g, self.df, self.max_iter, bias).reshape(-1) 160 | if self.verbose: 161 | print('trained TextRank. n sentences = {}'.format(self.R.shape[0])) 162 | 163 | def summarize(self, sents, topk=30, bias=None): 164 | """ 165 | Arguments 166 | --------- 167 | sents : list of str 168 | Sentence list 169 | topk : int 170 | Number of key-sentences to be selected. 171 | bias : None or numpy.ndarray 172 | PageRank bias term 173 | Shape must be (n_sents,) 174 | 175 | Returns 176 | ------- 177 | keysents : list of tuple 178 | Each tuple stands for (sentence index, rank, sentence) 179 | 180 | Usage 181 | ----- 182 | >>> from textrank import KeysentenceSummarizer 183 | 184 | >>> summarizer = KeysentenceSummarizer(tokenize = tokenizer, min_sim = 0.5) 185 | >>> keysents = summarizer.summarize(texts, topk=30) 186 | """ 187 | n_sents = len(sents) 188 | if isinstance(bias, np.ndarray): 189 | if bias.shape != (n_sents,): 190 | raise ValueError('The shape of bias must be (n_sents,) but {}'.format(bias.shape)) 191 | elif bias is not None: 192 | raise ValueError('The type of bias must be None or numpy.ndarray but the type is {}'.format(type(bias))) 193 | 194 | self.train_textrank(sents, bias) 195 | idxs = self.R.argsort()[-topk:] 196 | keysents = [(idx, self.R[idx], sents[idx]) for idx in reversed(idxs)] 197 | return keysents 198 | -------------------------------------------------------------------------------- /textrank/utils.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from scipy.sparse import csr_matrix 3 | import numpy as np 4 | 5 | 6 | def scan_vocabulary(sents, tokenize=None, min_count=2): 7 | """ 8 | Arguments 9 | --------- 10 | sents : list of str 11 | Sentence list 12 | tokenize : callable 13 | tokenize(str) returns list of str 14 | min_count : int 15 | Minumum term frequency 16 | 17 | Returns 18 | ------- 19 | idx_to_vocab : list of str 20 | Vocabulary list 21 | vocab_to_idx : dict 22 | Vocabulary to index mapper. 23 | """ 24 | counter = Counter(w for sent in sents for w in tokenize(sent)) 25 | counter = {w:c for w,c in counter.items() if c >= min_count} 26 | idx_to_vocab = [w for w, _ in sorted(counter.items(), key=lambda x:-x[1])] 27 | vocab_to_idx = {vocab:idx for idx, vocab in enumerate(idx_to_vocab)} 28 | return idx_to_vocab, vocab_to_idx 29 | 30 | def tokenize_sents(sents, tokenize): 31 | """ 32 | Arguments 33 | --------- 34 | sents : list of str 35 | Sentence list 36 | tokenize : callable 37 | tokenize(sent) returns list of str (word sequence) 38 | 39 | Returns 40 | ------- 41 | tokenized sentence list : list of list of str 42 | """ 43 | return [tokenize(sent) for sent in sents] 44 | 45 | def vectorize(tokens, vocab_to_idx): 46 | """ 47 | Arguments 48 | --------- 49 | tokens : list of list of str 50 | Tokenzed sentence list 51 | vocab_to_idx : dict 52 | Vocabulary to index mapper 53 | 54 | Returns 55 | ------- 56 | sentence bow : scipy.sparse.csr_matrix 57 | shape = (n_sents, n_terms) 58 | """ 59 | rows, cols, data = [], [], [] 60 | for i, tokens_i in enumerate(tokens): 61 | for t, c in Counter(tokens_i).items(): 62 | j = vocab_to_idx.get(t, -1) 63 | if j == -1: 64 | continue 65 | rows.append(i) 66 | cols.append(j) 67 | data.append(c) 68 | n_sents = len(tokens) 69 | n_terms = len(vocab_to_idx) 70 | x = csr_matrix((data, (rows, cols)), shape=(n_sents, n_terms)) 71 | return x 72 | -------------------------------------------------------------------------------- /textrank/word.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from scipy.sparse import csr_matrix 3 | 4 | from .utils import scan_vocabulary 5 | from .utils import tokenize_sents 6 | 7 | 8 | def word_graph(sents, tokenize=None, min_count=2, window=2, 9 | min_cooccurrence=2, vocab_to_idx=None, verbose=False): 10 | """ 11 | Arguments 12 | --------- 13 | sents : list of str 14 | Sentence list 15 | tokenize : callable 16 | tokenize(str) returns list of str 17 | min_count : int 18 | Minumum term frequency 19 | window : int 20 | Co-occurrence window size 21 | min_cooccurrence : int 22 | Minimum cooccurrence frequency 23 | vocab_to_idx : dict 24 | Vocabulary to index mapper. 25 | If None, this function scan vocabulary first. 26 | verbose : Boolean 27 | If True, verbose mode on 28 | 29 | Returns 30 | ------- 31 | co-occurrence word graph : scipy.sparse.csr_matrix 32 | idx_to_vocab : list of str 33 | Word list corresponding row and column 34 | """ 35 | if vocab_to_idx is None: 36 | idx_to_vocab, vocab_to_idx = scan_vocabulary(sents, tokenize, min_count) 37 | else: 38 | idx_to_vocab = [vocab for vocab, _ in sorted(vocab_to_idx.items(), key=lambda x:x[1])] 39 | 40 | tokens = tokenize_sents(sents, tokenize) 41 | g = cooccurrence(tokens, vocab_to_idx, window, min_cooccurrence, verbose) 42 | return g, idx_to_vocab 43 | 44 | def cooccurrence(tokens, vocab_to_idx, window=2, min_cooccurrence=2, verbose=False): 45 | """ 46 | Arguments 47 | --------- 48 | tokens : list of list of str 49 | Tokenized sentence list 50 | vocab_to_idx : dict 51 | Vocabulary to index mapper 52 | window : int 53 | Co-occurrence window size 54 | min_cooccurrence : int 55 | Minimum cooccurrence frequency 56 | verbose : Boolean 57 | If True, verbose mode on 58 | 59 | Returns 60 | ------- 61 | co-occurrence matrix : scipy.sparse.csr_matrix 62 | shape = (n_vocabs, n_vocabs) 63 | """ 64 | counter = defaultdict(int) 65 | for s, tokens_i in enumerate(tokens): 66 | if verbose and s % 1000 == 0: 67 | print('\rword cooccurrence counting {}'.format(s), end='') 68 | vocabs = [vocab_to_idx[w] for w in tokens_i if w in vocab_to_idx] 69 | n = len(vocabs) 70 | for i, v in enumerate(vocabs): 71 | if window <= 0: 72 | b, e = 0, n 73 | else: 74 | b = max(0, i - window) 75 | e = min(i + window, n) 76 | for j in range(b, e): 77 | if i == j: 78 | continue 79 | counter[(v, vocabs[j])] += 1 80 | counter[(vocabs[j], v)] += 1 81 | counter = {k:v for k,v in counter.items() if v >= min_cooccurrence} 82 | n_vocabs = len(vocab_to_idx) 83 | if verbose: 84 | print('\rword cooccurrence counting from {} sents was done'.format(s+1)) 85 | return dict_to_mat(counter, n_vocabs, n_vocabs) 86 | 87 | def dict_to_mat(d, n_rows, n_cols): 88 | """ 89 | Arguments 90 | --------- 91 | d : dict 92 | key : (i,j) tuple 93 | value : float value 94 | 95 | Returns 96 | ------- 97 | scipy.sparse.csr_matrix 98 | """ 99 | rows, cols, data = [], [], [] 100 | for (i, j), v in d.items(): 101 | rows.append(i) 102 | cols.append(j) 103 | data.append(v) 104 | return csr_matrix((data, (rows, cols)), shape=(n_rows, n_cols)) 105 | -------------------------------------------------------------------------------- /tutorials/implement_pairwise_distance_with_numpy.md: -------------------------------------------------------------------------------- 1 | ```python 2 | import numpy as np 3 | 4 | z = np.random.random_sample((5,3)) 5 | z 6 | ``` 7 | 8 | ``` 9 | array([[0.04656175, 0.20145282, 0.67823318], 10 | [0.33197532, 0.48793961, 0.75195233], 11 | [0.51498774, 0.58938071, 0.98792026], 12 | [0.45006027, 0.70745067, 0.15536108], 13 | [0.42664766, 0.66697394, 0.94806522]]) 14 | ``` 15 | 16 | ```python 17 | rows = np.asarray([0, 4, 1]) 18 | cols = np.asarray([1, 2, 0]) 19 | z[rows, cols] 20 | ``` 21 | 22 | ``` 23 | array([0.20145282, 0.94806522, 0.33197532]) 24 | ``` 25 | 26 | Pairwise distance matrix 는 numpy.ndarray 의 형태이며, numpy.where 를 이용하여 특정 조건을 만족하는 값들의 rows, cols 를 가져온 뒤, 이 값을 각각 row 와 column 위치에 입력하면 해당 값이 slice 된다. 27 | 28 | pairwise distances 함수 결과는 n by n 크기의 행렬이기 때문에, 데이터가 클 경우에는 부분적으로 나눠서 행렬을 만든 뒤, vstack 을 이용하여 이를 합치는게 더 좋다. 29 | 30 | ```python 31 | def cosine_similarity_matrix(x, min_sim=0.3, batch_size=1000): 32 | n_rows = x.shape[0] 33 | mat = [] 34 | for bidx in range(math.ceil(n_rows / batch_size)): 35 | b = int(bidx * batch_size) 36 | e = min(n_rows, int((bidx+1) * batch_size)) 37 | psim = 1 - pairwise_distances(x[b:e], x, metric='cosine') 38 | rows, cols = np.where(psim >= min_sim) 39 | data = psim[rows, cols] 40 | mat.append(csr_matrix((data, (rows, cols)), shape=(e-b, n_rows))) 41 | mat = sp.sparse.vstack(mat) 42 | return mat 43 | ``` 44 | 45 | collections 의 Counter 를 이용하여 구현한 Cosine sentence similarity 함수를 이용한 경우 [(commit)](https://github.com/lovit/textrank/blob/c4fb13a0070167a55bef2c89dec219fd0867c2c8/textrank/sentence.py#L89) 약 21 분 23 초의 계산이 걸리던 작업이 numpy 를 이용한 경우에는 3.31 초로 개선되었다. 46 | 47 | ```python 48 | def textrank_similarity_matrix(x, min_sim=0.3, batch_size=1000): 49 | n_rows, n_cols = x.shape 50 | 51 | # Boolean matrix 52 | rows, cols = x.nonzero() 53 | data = np.ones(rows.shape[0]) 54 | z = csr_matrix((data, (rows, cols)), shape=(n_rows, n_cols)) 55 | 56 | # Inverse sentence length 57 | size = np.asarray(x.sum(axis=1)).reshape(-1) 58 | size[np.where(size <= min_length)] = 10000 59 | size = np.log(size) 60 | 61 | mat = [] 62 | for bidx in range(math.ceil(n_rows / batch_size)): 63 | 64 | # slicing 65 | b = int(bidx * batch_size) 66 | e = min(n_rows, int((bidx+1) * batch_size)) 67 | 68 | # dot product 69 | inner = z[b:e,:] * z.transpose() 70 | 71 | # sentence len[i,j] = size[i] + size[j] 72 | norm = size[b:e].reshape(-1,1) + size.reshape(1,-1) 73 | norm = norm ** (-1) 74 | norm[np.where(norm == np.inf)] = 0 75 | 76 | # normalize 77 | sim = inner.multiply(norm).tocsr() 78 | rows, cols = (sim >= min_sim).nonzero() 79 | data = np.asarray(sim[rows, cols]).reshape(-1) 80 | 81 | # append 82 | mat.append(csr_matrix((data, (rows, cols)), shape=(e-b, n_rows))) 83 | 84 | return sp.sparse.vstack(mat) 85 | ``` 86 | 87 | 두 문장에 공통으로 등장한 단어의 개수는 term frequency matrix 를 Boolean matrix 로 변환한 뒤, 이를 내적하면 알 수 있다. 하지만 두 문장 길이의 로그값의 합으로 normalize 하는 부분을 구현하는 부분은 행렬의 곲으로는 구현되지 않는데, norm(i,j) = size(i) + size(j) 형식이기 때문이다. norm 을 계산한 뒤, Boolean matrix 간의 inner product 와 element-wise muptiplication 을 한 뒤, scipy.sparse 의 where 를 실행하여 min_sim 이상의 값만으로 이뤄진 새로운 sparse matrix 를 만든다. sparse matrix 를 stack 에 쌓은 뒤, 병합하여 return 한다. 88 | 89 | Python 의 set 을 이용하여 구현한 TextRank sentence similarity 함수를 이용한 경우 [(commit)](https://github.com/lovit/textrank/blob/c4fb13a0070167a55bef2c89dec219fd0867c2c8/textrank/sentence.py#L69) 약 3분 20 초의 계산이 걸리던 작업이 numpy 를 이용한 경우에는 24 초로 개선되었다. 90 | 91 | Cosine 의 경우에는 numpy 에서 모든 계산이 끝나는 것과 비교하여, TextRank similarity 는 n by n 의 행렬을 메모리에 올리지 않기 위해서는 Python 과 numpy 를 왔다갔다 하는 작업을 몇 번 거쳐야 하기 때문에 계산 속도의 감소 폭이 적었다. 92 | -------------------------------------------------------------------------------- /tutorials/keyword_keysentence_extractor.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "KoNLPy 의 Komoran 을 이용하여 라라랜드 영화 리뷰를 토크나이징 한 텍스트를 `lalaland_komoran.txt` 에, 원문을 `lalaland.txt` 에 저장하였습니다. 총 15,595 건의 영화평입니다." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stdout", 17 | "output_type": "stream", 18 | "text": [ 19 | "15595 15595\n" 20 | ] 21 | } 22 | ], 23 | "source": [ 24 | "# Komoran tokenized La La Land review\n", 25 | "with open('./lalaland_komoran.txt', encoding='utf-8') as f:\n", 26 | " sents = [sent.strip() for sent in f]\n", 27 | "\n", 28 | "with open('./lalaland.txt', encoding='utf-8') as f:\n", 29 | " texts = [sent.strip() for sent in f]\n", 30 | "\n", 31 | "print(len(sents), len(texts))" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "TextRank 에서는 word cooccurrence graph 를 만들 때 명사와 동사 등을 이용할 것을 제안하였습니다. 이는 조사나 관사와 같이 의미를 지니지 않으면서도 자주 이용되는 단어들이 높은 PageRank 를 가지게 되는 것을 방지하기 위해서입니다.\n", 39 | "\n", 40 | "Komoran 에서의 명사 (NN), 어근 (XR), 형용사 (VA), 동사 (VV) 만을 이용하여 word cooccurrence graph 를 만듭니다. window 를 -1 로 설정하면 한 문장에서 얼마나 떨어져 있던지 상관없이 cooccurrence 를 계산하며, window 가 1 보다 클 경우에는 해당 간격만큼만 떨어진 단어들 간에만 cooccurrence 를 인정합니다.\n", 41 | "\n", 42 | "학습 결과 `영화`, `음악`, `꿈`, `마지막` 같은 라라랜드의 엔딩을 의미하는 단어들이 핵심 단어로 선택되었습니다." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "영화/NNG (1.73e+02)\n", 55 | "보/VV (1.29e+02)\n", 56 | "좋/VA (65.5)\n", 57 | "하/VV (52.0)\n", 58 | "것/NNB (47.4)\n", 59 | "같/VA (45.4)\n", 60 | "영화/NNP (43.8)\n", 61 | "음악/NNG (43.6)\n", 62 | "꿈/NNG (41.4)\n", 63 | "있/VV (40.8)\n", 64 | "없/VA (35.9)\n", 65 | "마지막/NNG (31.9)\n", 66 | "수/NNB (30.1)\n", 67 | "사랑/NNG (28.3)\n", 68 | "아름답/VA (26.5)\n", 69 | "현실/NNG (24.8)\n", 70 | "되/VV (23.9)\n", 71 | "노래/NNG (23.4)\n", 72 | "생각/NNG (23.2)\n", 73 | "스토리/NNP (21.4)\n", 74 | "번/NNB (20.3)\n", 75 | "거/NNB (19.7)\n", 76 | "최고/NNG (19.2)\n", 77 | "때/NNG (19.1)\n", 78 | "사람/NNG (19.0)\n", 79 | "여운/NNP (17.5)\n", 80 | "뮤지컬/NNP (16.9)\n", 81 | "나오/VV (16.5)\n", 82 | "듯/NNB (16.1)\n", 83 | "영상미/NNG (16.0)\n" 84 | ] 85 | } 86 | ], 87 | "source": [ 88 | "from textrank import KeywordSummarizer\n", 89 | "\n", 90 | "def komoran_tokenize(sent):\n", 91 | " words = sent.split()\n", 92 | " words = [w for w in words if ('/NN' in w or '/XR' in w or '/VA' in w or '/VV' in w)]\n", 93 | " return words\n", 94 | "\n", 95 | "keyword_extractor = KeywordSummarizer(\n", 96 | " tokenize = komoran_tokenize,\n", 97 | " window = -1,\n", 98 | " verbose = False\n", 99 | ")\n", 100 | "keywords = keyword_extractor.summarize(sents, topk=30)\n", 101 | "for word, rank in keywords:\n", 102 | " print('{} ({:.3})'.format(word, rank))" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "만약 모든 단어를 이용할 경우에는 조사 (JKG, JX, JKS, ... ) 어미 (EC, EP, ... ) 등이 핵심 단어로 선택됩니다. 이는 word cooccurrence graph 는 사실상 출현빈도가 높은 단어들이 높은 Rank 를 가지도록 유도하기 때문입니다." 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 3, 115 | "metadata": {}, 116 | "outputs": [ 117 | { 118 | "name": "stdout", 119 | "output_type": "stream", 120 | "text": [ 121 | "ㄴ/ETM (1.24e+02)\n", 122 | "고/EC (1.03e+02)\n", 123 | "영화/NNG (96.8)\n", 124 | "는/ETM (94.6)\n", 125 | "이/VCP (92.3)\n", 126 | "이/JKS (92.0)\n", 127 | "하/XSV (85.2)\n", 128 | "에/JKB (79.0)\n", 129 | "았/EP (76.1)\n", 130 | "보/VV (73.5)\n", 131 | "었/EP (72.8)\n", 132 | "다/EC (68.3)\n", 133 | "을/JKO (64.2)\n", 134 | "하/XSA (58.8)\n", 135 | "의/JKG (58.4)\n", 136 | "도/JX (52.7)\n", 137 | "ㄹ/ETM (50.2)\n", 138 | "가/JKS (47.2)\n", 139 | "게/EC (46.7)\n", 140 | "는/JX (42.3)\n", 141 | "어/EC (37.9)\n", 142 | "좋/VA (37.6)\n", 143 | "를/JKO (34.3)\n", 144 | "아/EC (33.8)\n", 145 | "은/ETM (33.7)\n", 146 | "들/XSN (32.6)\n", 147 | "은/JX (32.0)\n", 148 | "하/VV (29.8)\n", 149 | "것/NNB (26.7)\n", 150 | "과/JC (26.5)\n" 151 | ] 152 | } 153 | ], 154 | "source": [ 155 | "keyword_extractor = KeywordSummarizer(\n", 156 | " tokenize=lambda x:x.split(),\n", 157 | " verbose = False\n", 158 | ")\n", 159 | "keywords = keyword_extractor.summarize(sents, topk=30)\n", 160 | "for word, rank in keywords:\n", 161 | " print('{} ({:.3})'.format(word, rank))" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "window 의 크기를 바꾼다 하여도 큰 변화는 없습니다. 약간의 순위 변동은 있지만, 큰 맥락이 변하지는 않습니다." 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 4, 174 | "metadata": {}, 175 | "outputs": [ 176 | { 177 | "name": "stdout", 178 | "output_type": "stream", 179 | "text": [ 180 | "영화/NNG (1.9e+02)\n", 181 | "보/VV (1.5e+02)\n", 182 | "좋/VA (80.8)\n", 183 | "하/VV (51.2)\n", 184 | "음악/NNG (50.8)\n", 185 | "영화/NNP (50.3)\n", 186 | "것/NNB (44.6)\n", 187 | "꿈/NNG (42.5)\n", 188 | "같/VA (40.7)\n", 189 | "있/VV (40.6)\n", 190 | "없/VA (35.5)\n", 191 | "마지막/NNG (33.7)\n", 192 | "아름답/VA (32.1)\n", 193 | "사랑/NNG (30.4)\n", 194 | "수/NNB (29.5)\n", 195 | "현실/NNG (27.9)\n", 196 | "노래/NNG (26.1)\n", 197 | "최고/NNG (23.8)\n", 198 | "스토리/NNP (23.6)\n", 199 | "생각/NNG (23.5)\n", 200 | "되/VV (23.1)\n", 201 | "번/NNB (22.7)\n", 202 | "여운/NNP (22.1)\n", 203 | "감동/NNG (19.1)\n", 204 | "사람/NNG (18.6)\n", 205 | "때/NNG (18.0)\n", 206 | "거/NNB (18.0)\n", 207 | "지루/XR (17.6)\n", 208 | "영상미/NNG (16.8)\n", 209 | "재밌/VA (16.3)\n" 210 | ] 211 | } 212 | ], 213 | "source": [ 214 | "keyword_extractor = KeywordSummarizer(\n", 215 | " tokenize = komoran_tokenize,\n", 216 | " window = 2,\n", 217 | " verbose = False\n", 218 | ")\n", 219 | "keywords = keyword_extractor.summarize(sents, topk=30)\n", 220 | "for word, rank in keywords:\n", 221 | " print('{} ({:.3})'.format(word, rank))" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "핵심 문장을 선택하기 위하여 문장 간 유사도를 계산한 다음, `min_sim` 이상의 유사도를 지니는 문장 간에 adjacent sentence graph 를 만듭니다. 그리고 여기에 PageRank 를 적용하여 핵심 문장을 선택합니다.\n", 229 | "\n", 230 | "텍스트를 출력할 때에는 토크나이징된 문장은 가독이 어렵기 때문에 원 텍스트를 이용하여 출력합니다." 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 5, 236 | "metadata": {}, 237 | "outputs": [ 238 | { 239 | "name": "stdout", 240 | "output_type": "stream", 241 | "text": [ 242 | "constructing sentence graph was constructed from 15595 sents\n", 243 | "trained TextRank. n sentences = 15595\n", 244 | "#5 (2.56e+02) : 시사회 보고 왔어요 꿈과 사랑에 관한 이야기인데 뭔가 진한 여운이 남는 영화예요\n", 245 | "\n", 246 | "#14 (1.17e+02) : 시사회 갔다왔어요 제가 라이언고슬링팬이라서 하는말이아니고 너무 재밌어요 꿈과 현실이 잘 보여지는영화 사랑스런 영화 전 개봉하면 또 볼생각입니당\n", 247 | "\n", 248 | "#17 (94.4) : 시사회에서 보고왔는데 여운쩔었다 엠마스톤과 라이언 고슬링의 케미가 도입부의 강렬한음악좋았고 예고편에 나왓던 오디션 노래 감동적이어서 눈물나왔다ㅠ 이영화는 위플래쉬처럼 꼭 영화관에봐야함 색감 노래 배우 환상적인 영화\n", 249 | "\n", 250 | "#27 (77.3) : 방금 시사회로 봤는데 인생영화 하나 또 탄생했네 롱테이크 촬영이 예술 영상이 넘나 아름답고 라이언고슬링의 멋진 피아노 연주 엠마스톤과의 춤과 노래 눈과 귀가 호강한다 재미를 기대하면 약간 실망할수도 있지만 충분히 훌륭한 영화\n", 251 | "\n", 252 | "#6 (65.9) : 황홀하고 따뜻한 꿈이었어요 imax로 또 보려합니다 좋은 영화 시사해주셔서 감사해요\n", 253 | "\n", 254 | "#18 (56.3) : 시사회 갔다왔는데 실망했어요 너무 기대하면 안될 것 같습니다 꿈 같은 영화 마법 같은 영화는 맞는데 꿈과 마법이 깨지는 순간 이 영화는 어디로 가고 있는가 하는 생각이 들었어요 할 말은 많지만 욕먹을까봐 줄임\n", 255 | "\n", 256 | "#1 (55.7) : 사랑과 꿈 그 흐름의 아름다움을 음악과 영상으로 최대한 담아놓았다 배우들 연기는 두말할것없고\n", 257 | "\n", 258 | "#26 (54.2) : 시사회 봤네요 영화를 보고나면 지금 내 옆에 있는 사람이 소중하게 느껴질것\n", 259 | "\n", 260 | "#60 (51.9) : 최고의 뮤지컬 영화영화를 보는 내내 꿈 속에 있는듯한 황홀함\n", 261 | "\n", 262 | "#51 (50.7) : 이번 부산국제영화제서 봤습니다 정말 역대 100 최고의 영화라 말할수있을 정도로 훌륭하고 신나고 즐겁고 재밌었습니다 두번보고 세번보고 평생소장하고 싶은 영화로 추천합니다 사랑하는 사람과 함께 황홀경에 빠져보시길\n", 263 | "\n", 264 | "#22 (49.9) : 방금 시사회보고 왔어요 정말 힘든 하루였는데 눈이랑 귀가 절로 호강한 영화였어요ㅜ개봉하면 혼자 또 보러갈까해요 마지막에 라이언고슬링의 피아노연주는 아직도 여운이 남네요 뭔가 현실적이여서 더 와닿는 음악영화 좋아하시는분들은 꼭 보시길\n", 265 | "\n", 266 | "#21 (48.8) : 방금 전 시사회에서 보고 왔습니다 귀와 눈이 모두 즐거운 놀랍고 환상적인 영화입나다 강추합니다\n", 267 | "\n", 268 | "#3 (42.0) : 방금 시사회 보고 왔어요 배우들 매력이 눈을 뗄 수가 없게 만드네요 한편의 그림 같은 장면들도 많고 음악과 춤이 눈과 귀를 사로 잡았어요 한번 더 보고 싶네요\n", 269 | "\n", 270 | "#159 (40.7) : 두번봐도 감동이 전해지는 영화 인생영화라고 칭할 정도로 감동 받았다 음악과 영상미에 가장 먼저 매료되었고 내용도 꿈을 찾아가는 것이 현재 청춘들이 느낄 수 있는 감정들을 충분히 잘 표현했다고 생각한다 본지 꽤 되었지만 한달 째 ost에 빠져사는중\n", 271 | "\n", 272 | "#132 (38.0) : 생각보단 별루엿어요 연출과 음악은 좋았으나 그이상은 아니었습니다 너무 기대하구 봐서 그런것도 같유요\n", 273 | "\n", 274 | "#7 (37.7) : 엠마스톤의 노래 솜씨도 보겠군\n", 275 | "\n", 276 | "#20 (37.7) : 음악 속 그들에게 빠져들었다 꽤나 오래토록 라라랜드 노래가 내 하루에 떠오를 것 같다\n", 277 | "\n", 278 | "#2 (37.2) : 지금껏 영화 평가 해본 적이 없는데 진짜 최고네요 색감 스토리 음악 연기 모두ㅜㅜ최고입니다\n", 279 | "\n", 280 | "#187 (36.6) : 저가 본 영화중에서 두번째로 최고인 영화였던것같습니다 노래도너무좋았고 정말 한 장면도 놓칠수없었습니다 재밌었고 앞으로도 이런 비슷한 영화들이나와도 괜찮을것같다 싶었던것같습니다\n", 281 | "\n", 282 | "#47 (36.1) : 이 영화는 마법이다\n", 283 | "\n", 284 | "#35 (34.1) : 이거 2번보고 3번 보세요 진짜 최고입니다\n", 285 | "\n", 286 | "#171 (33.9) : 보는 내내 좋은 음악들 덕에 귀가 즐거웠고 현실적인 결말이 인상깊었다 생각하게 만드는 영화\n", 287 | "\n", 288 | "#39 (33.6) : 마지막에 엠마 스톤이랑 헤어져서 너무 슬펐네요 슬프면서도 현실적이라고 할까요 마지막 장면의 라이언고슬링의 피아노 연주는 진짜 여운이 최고였습니다 올해 최고의 영화입니다\n", 289 | "\n", 290 | "#52 (33.3) : 엠마스톤이랑 라이언고슬링이 또 만났네 넘좋다 꼭봐야지 혼자 꼭\n", 291 | "\n", 292 | "#15 (33.0) : 이 영화는 여름에 보면 안되겠음 색조합 난리나는 영상미에 취해 자꾸 입이 벌어져 날파리가 들어갈 위험이 있고 사운드에 지려 여름엔 암모니아 냄새가 더 진동할 수 있음\n", 293 | "\n", 294 | "#121 (32.2) : 눈과 귀를 사로잡는 라라랜드 영화를 보고 난 후 여운이 많이 남는 영화에요 중간 중간 동화적 요소가 조금 과한듯해서 당황했지만 주인공들의 연기력과 호흡이 정말 좋았던거 같아요\n", 295 | "\n", 296 | "#181 (31.2) : 정말 좋은 영화네요색감 화면 음악 연기 극본 뭐하나 빠지는게 없는 영화입니다굳이 보러갈만한 가치가 있네요약간 냉소적인 시선 현실적인 엔딩까지 모두 좋습니다다만 뮤지컬영화다 보니 좀 길다싶은 테이크도 있네요 그래도 꼭 한번 볼만한 영화\n", 297 | "\n", 298 | "#73 (30.5) : 부국제에서 봤는데 뭐 끝이네요 너무너무너무나도 재미있게 즐겁게 감동받으며 봤습니다 감독 배우 때문에 기대를 많이 받는것 같은데 그냥 둘다 최고입니다\n", 299 | "\n", 300 | "#120 (30.1) : 벌써 두번째 보는 영화인데요 아무리 봐도 잊혀지지 않네요\n", 301 | "\n", 302 | "#24 (28.7) : 엠마 스톤과 라이언 고슬링 여러번 만났지만 라라랜드에서 가장 빛난다 영화가 끝나고 나오면서 절로 흥얼거리게 되는 노래에서 이 영화가 올 겨울 가장 뜨거운 영화가 되지 않을까 생각하게 된다\n", 303 | "\n" 304 | ] 305 | } 306 | ], 307 | "source": [ 308 | "from textrank import KeysentenceSummarizer\n", 309 | "\n", 310 | "summarizer = KeysentenceSummarizer(\n", 311 | " tokenize = komoran_tokenize,\n", 312 | " min_sim = 0.5,\n", 313 | " verbose = True\n", 314 | ")\n", 315 | "keysents = summarizer.summarize(sents)\n", 316 | "for idx, rank, komoran_sent in keysents:\n", 317 | " print('#{} ({:.3}) : {}'.format(idx, rank, texts[idx]), end='\\n\\n')" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "metadata": {}, 323 | "source": [ 324 | "아래의 뉴스 기사에 대하여 3 개의 핵심 문장을 추출합니다." 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 1, 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "sents = [\n", 334 | " '오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다',\n", 335 | " '서울 연합뉴스 김은경 기자 사제 총기로 경찰을 살해한 범인 성모 46 씨는 주도면밀했다',\n", 336 | " '경찰에 따르면 성씨는 19일 오후 강북경찰서 인근 부동산 업소 밖에서 부동산업자 이모 67 씨가 나오기를 기다렸다 이씨와는 평소에도 말다툼을 자주 한 것으로 알려졌다',\n", 337 | " '이씨가 나와 걷기 시작하자 성씨는 따라가면서 미리 준비해온 사제 총기를 이씨에게 발사했다 총알이 빗나가면서 이씨는 도망갔다 그 빗나간 총알은 지나가던 행인 71 씨의 배를 스쳤다',\n", 338 | " '성씨는 강북서 인근 치킨집까지 이씨 뒤를 쫓으며 실랑이하다 쓰러뜨린 후 총기와 함께 가져온 망치로 이씨 머리를 때렸다',\n", 339 | " '이 과정에서 오후 6시 20분께 강북구 번동 길 위에서 사람들이 싸우고 있다 총소리가 났다 는 등의 신고가 여러건 들어왔다',\n", 340 | " '5분 후에 성씨의 전자발찌가 훼손됐다는 신고가 보호관찰소 시스템을 통해 들어왔다 성범죄자로 전자발찌를 차고 있던 성씨는 부엌칼로 직접 자신의 발찌를 끊었다',\n", 341 | " '용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기',\n", 342 | " '신고를 받고 번동파출소에서 김창호 54 경위 등 경찰들이 오후 6시 29분께 현장으로 출동했다 성씨는 그사이 부동산 앞에 놓아뒀던 가방을 챙겨 오패산 쪽으로 도망간 후였다',\n", 343 | " '김 경위는 오패산 터널 입구 오른쪽의 급경사에서 성씨에게 접근하다가 오후 6시 33분께 풀숲에 숨은 성씨가 허공에 난사한 10여발의 총알 중 일부를 왼쪽 어깨 뒷부분에 맞고 쓰러졌다',\n", 344 | " '김 경위는 구급차가 도착했을 때 이미 의식이 없었고 심폐소생술을 하며 병원으로 옮겨졌으나 총알이 폐를 훼손해 오후 7시 40분께 사망했다',\n", 345 | " '김 경위는 외근용 조끼를 입고 있었으나 총알을 막기에는 역부족이었다',\n", 346 | " '머리에 부상을 입은 이씨도 함께 병원으로 이송됐으나 생명에는 지장이 없는 것으로 알려졌다',\n", 347 | " '성씨는 오패산 터널 밑쪽 숲에서 오후 6시 45분께 잡혔다',\n", 348 | " '총격현장 수색하는 경찰들 서울 연합뉴스 이효석 기자 19일 오후 서울 강북구 오패산 터널 인근에서 경찰들이 폭행 용의자가 사제총기를 발사해 경찰관이 사망한 사건을 조사 하고 있다',\n", 349 | " '총 때문에 쫓던 경관들과 민간인들이 몸을 숨겼는데 인근 신발가게 직원 이모씨가 다가가 성씨를 덮쳤고 이어 현장에 있던 다른 상인들과 경찰이 가세해 체포했다',\n", 350 | " '성씨는 경찰에 붙잡힌 직후 나 자살하려고 한 거다 맞아 죽어도 괜찮다 고 말한 것으로 전해졌다',\n", 351 | " '성씨 자신도 경찰이 발사한 공포탄 1발 실탄 3발 중 실탄 1발을 배에 맞았으나 방탄조끼를 입은 상태여서 부상하지는 않았다',\n", 352 | " '경찰은 인근을 수색해 성씨가 만든 사제총 16정과 칼 7개를 압수했다 실제 폭발할지는 알 수 없는 요구르트병에 무언가를 채워두고 심지를 꽂은 사제 폭탄도 발견됐다',\n", 353 | " '일부는 숲에서 발견됐고 일부는 성씨가 소지한 가방 안에 있었다'\n", 354 | "]" 355 | ] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "metadata": {}, 360 | "source": [ 361 | "띄어쓰기 기준으로 adjacent sentence graph 를 만듭니다." 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": 2, 367 | "metadata": {}, 368 | "outputs": [ 369 | { 370 | "name": "stdout", 371 | "output_type": "stream", 372 | "text": [ 373 | "오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다\n", 374 | "총격현장 수색하는 경찰들 서울 연합뉴스 이효석 기자 19일 오후 서울 강북구 오패산 터널 인근에서 경찰들이 폭행 용의자가 사제총기를 발사해 경찰관이 사망한 사건을 조사 하고 있다\n", 375 | "용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기\n" 376 | ] 377 | } 378 | ], 379 | "source": [ 380 | "from textrank import KeysentenceSummarizer\n", 381 | "\n", 382 | "summarizer = KeysentenceSummarizer(\n", 383 | " tokenize = lambda x:x.split(),\n", 384 | " min_sim = 0.3,\n", 385 | " verbose = False\n", 386 | ")\n", 387 | "keysents = summarizer.summarize(sents, topk=3)\n", 388 | "for _, _, sent in keysents:\n", 389 | " print(sent)" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "KoNLPy 의 Komoran 을 이용하여 토크나이징과 핵심문장을 한 번에 추출하는 예시입니다." 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 3, 402 | "metadata": {}, 403 | "outputs": [ 404 | { 405 | "name": "stdout", 406 | "output_type": "stream", 407 | "text": [ 408 | "오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다\n", 409 | "용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기\n", 410 | "신고를 받고 번동파출소에서 김창호 54 경위 등 경찰들이 오후 6시 29분께 현장으로 출동했다 성씨는 그사이 부동산 앞에 놓아뒀던 가방을 챙겨 오패산 쪽으로 도망간 후였다\n" 411 | ] 412 | } 413 | ], 414 | "source": [ 415 | "from konlpy.tag import Komoran\n", 416 | "\n", 417 | "komoran = Komoran()\n", 418 | "def komoran_tokenizer(sent):\n", 419 | " words = komoran.pos(sent, join=True)\n", 420 | " words = [w for w in words if ('/NN' in w or '/XR' in w or '/VA' in w or '/VV' in w)]\n", 421 | " return words\n", 422 | "\n", 423 | "summarizer = KeysentenceSummarizer(\n", 424 | " tokenize = komoran_tokenizer,\n", 425 | " min_sim = 0.3,\n", 426 | " verbose = False\n", 427 | ")\n", 428 | "keysents = summarizer.summarize(sents, topk=3)\n", 429 | "for _, _, sent in keysents:\n", 430 | " print(sent)" 431 | ] 432 | }, 433 | { 434 | "cell_type": "markdown", 435 | "metadata": {}, 436 | "source": [ 437 | "사실 위의 결과를 얻기 위해서는 토크나이저도 제대로 구축할 필요가 없습니다. 어자피 많이 등장한 단어라면 해당 단어를 구성하는 부분어절 (subword) 역시 자주 등장하였을 것이며, 이를 이용한 문장 간 유사도를 측정하여도 비슷하기 때문입니다.\n", 438 | "\n", 439 | "아래는 띄어쓰기 기준으로 나뉘어진 어절에서 3음절의 subwords 를 잘라내는 토크나이저 입니다." 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": 4, 445 | "metadata": {}, 446 | "outputs": [ 447 | { 448 | "data": { 449 | "text/plain": [ 450 | "['이것은',\n", 451 | " '부분단',\n", 452 | " '분단어',\n", 453 | " '단어의',\n", 454 | " '예시입',\n", 455 | " '시입니',\n", 456 | " '입니다',\n", 457 | " '짧은',\n", 458 | " '어절은',\n", 459 | " '그대로',\n", 460 | " '나옵니',\n", 461 | " '옵니다']" 462 | ] 463 | }, 464 | "execution_count": 4, 465 | "metadata": {}, 466 | "output_type": "execute_result" 467 | } 468 | ], 469 | "source": [ 470 | "def subword_tokenizer(sent, n=3):\n", 471 | " def subword(token, n):\n", 472 | " if len(token) <= n:\n", 473 | " return [token]\n", 474 | " return [token[i:i+n] for i in range(len(token) - n + 1)]\n", 475 | " return [sub for token in sent.split() for sub in subword(token, n)]\n", 476 | "\n", 477 | "subword_tokenizer('이것은 부분단어의 예시입니다 짧은 어절은 그대로 나옵니다')" 478 | ] 479 | }, 480 | { 481 | "cell_type": "markdown", 482 | "metadata": {}, 483 | "source": [ 484 | "이를 이용하여도 핵심 문장은 위와 동일하게 출력됩니다." 485 | ] 486 | }, 487 | { 488 | "cell_type": "code", 489 | "execution_count": 5, 490 | "metadata": {}, 491 | "outputs": [ 492 | { 493 | "name": "stdout", 494 | "output_type": "stream", 495 | "text": [ 496 | "오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다\n", 497 | "용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기\n", 498 | "총격현장 수색하는 경찰들 서울 연합뉴스 이효석 기자 19일 오후 서울 강북구 오패산 터널 인근에서 경찰들이 폭행 용의자가 사제총기를 발사해 경찰관이 사망한 사건을 조사 하고 있다\n" 499 | ] 500 | } 501 | ], 502 | "source": [ 503 | "summarizer = KeysentenceSummarizer(\n", 504 | " tokenize = subword_tokenizer,\n", 505 | " min_sim = 0.3,\n", 506 | " verbose = False\n", 507 | ")\n", 508 | "keysents = summarizer.summarize(sents, topk=3)\n", 509 | "for _, _, sent in keysents:\n", 510 | " print(sent)" 511 | ] 512 | }, 513 | { 514 | "cell_type": "markdown", 515 | "metadata": {}, 516 | "source": [ 517 | "summarizer 의 R 에는 각 문장 별 중요도 (PageRank 값) 가 저장되어 있습니다." 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "execution_count": 6, 523 | "metadata": {}, 524 | "outputs": [ 525 | { 526 | "data": { 527 | "text/plain": [ 528 | "array([1.76438621, 0.74969733, 1.33782296, 0.61722741, 0.7377122 ,\n", 529 | " 1.07534516, 0.62928904, 1.71145208, 1.07601036, 1.13590053,\n", 530 | " 0.94446938, 0.67686714, 0.7008805 , 1.02103025, 1.61461996,\n", 531 | " 0.76911158, 0.78024047, 0.65793743, 1.02927478, 0.97072522])" 532 | ] 533 | }, 534 | "execution_count": 6, 535 | "metadata": {}, 536 | "output_type": "execute_result" 537 | } 538 | ], 539 | "source": [ 540 | "summarizer.R" 541 | ] 542 | }, 543 | { 544 | "cell_type": "markdown", 545 | "metadata": {}, 546 | "source": [ 547 | "문장의 위치에 따라 중요도를 다르게 설정할 수도 있습니다. 뉴스 기사는 대부분 첫 문장이 중요합니다. 실제로 위의 예시에서도 첫 문장이 가장 중요한 핵심 문장으로 선택되었습니다. 만약 마지막 문장이 중요하다고 가정한다면 이러한 정보를 bias 에 추가할 수 있습니다. numpy.ndarray 형태로 bias 를 만듭니다. 마지막 문장이 다른 문장보다 10 배 중요하다고 가정하였습니다. 이를 summarize 함수의 bias 에 입력하면 가장 먼저 맨 마지막 문장이 중요한 문장으로 선택됩니다. 다른 문장들 중에서도 맨 마지막 문장과 비슷할수록 상대적인 중요도가 더 커집니다." 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": 7, 553 | "metadata": {}, 554 | "outputs": [ 555 | { 556 | "name": "stdout", 557 | "output_type": "stream", 558 | "text": [ 559 | "일부는 숲에서 발견됐고 일부는 성씨가 소지한 가방 안에 있었다\n", 560 | "경찰은 인근을 수색해 성씨가 만든 사제총 16정과 칼 7개를 압수했다 실제 폭발할지는 알 수 없는 요구르트병에 무언가를 채워두고 심지를 꽂은 사제 폭탄도 발견됐다\n", 561 | "오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다\n" 562 | ] 563 | } 564 | ], 565 | "source": [ 566 | "import numpy as np\n", 567 | "\n", 568 | "bias = np.ones(len(sents))\n", 569 | "bias[-1] = 10\n", 570 | "\n", 571 | "keysents = summarizer.summarize(sents, topk=3, bias=bias)\n", 572 | "for _, _, sent in keysents:\n", 573 | " print(sent)" 574 | ] 575 | }, 576 | { 577 | "cell_type": "markdown", 578 | "metadata": {}, 579 | "source": [ 580 | "R 을 다시 확인해보면 PageRank 값이 달라졌음을 확인할 수 있습니다. 상대적인 위치 외에도 특정 단어가 포함된 문장에 preference (bias) 를 더 높게 설정할 수도 있습니다." 581 | ] 582 | }, 583 | { 584 | "cell_type": "code", 585 | "execution_count": 8, 586 | "metadata": {}, 587 | "outputs": [ 588 | { 589 | "data": { 590 | "text/plain": [ 591 | "array([1.22183954, 0.51902092, 0.92584783, 0.42671701, 0.50982682,\n", 592 | " 0.74430683, 0.43498201, 1.18547126, 0.74505343, 0.78632222,\n", 593 | " 0.65347844, 0.46802437, 0.48465947, 0.70684359, 1.11845189,\n", 594 | " 0.53125081, 0.53956034, 0.45476333, 3.14941282, 4.39416707])" 595 | ] 596 | }, 597 | "execution_count": 8, 598 | "metadata": {}, 599 | "output_type": "execute_result" 600 | } 601 | ], 602 | "source": [ 603 | "summarizer.R" 604 | ] 605 | }, 606 | { 607 | "cell_type": "code", 608 | "execution_count": null, 609 | "metadata": {}, 610 | "outputs": [], 611 | "source": [] 612 | } 613 | ], 614 | "metadata": { 615 | "kernelspec": { 616 | "display_name": "Python 3", 617 | "language": "python", 618 | "name": "python3" 619 | }, 620 | "language_info": { 621 | "codemirror_mode": { 622 | "name": "ipython", 623 | "version": 3 624 | }, 625 | "file_extension": ".py", 626 | "mimetype": "text/x-python", 627 | "name": "python", 628 | "nbconvert_exporter": "python", 629 | "pygments_lexer": "ipython3", 630 | "version": "3.7.1" 631 | } 632 | }, 633 | "nbformat": 4, 634 | "nbformat_minor": 2 635 | } 636 | --------------------------------------------------------------------------------