나무위키 데이터베이스 워드벡터(word2vec) 만들기
나무위키 워드벡터(word2vec) 만들기
1. 나무위키 데이터베이스 다운로드
https://mu-star.net/wikidb
미러링 사이트에서 찾았다. 다운로드 안되는 곳이 많음.
7z압축되어 있는 최신 파일 다운로드하니 1.2기가 정도 됨.
+ 압축해제
우분투에 7z 압축해제 프로그램이 없어서 p7zip을 설치하고 다운받은 압축 파일을 풀었더니 무려 8.3기가였다.
설치
#apt-get install p7zip
파일압축해제
#7zr x [파일명].7z
+파일이 너무 커서, 조금만 읽어보고 형태를 보자.
압축을 풀면 확장자가 JSON 포맷임.
# wc -l namuwiki_20180326.json
0
줄 구분없이 한 줄로 구성된 텍스트 파일이다. more로 한 페이지만 보자.
# head namuwiki_20180326.json | more
다음과 같이 나온다.
[{"namespace":"0","title":"!","text":"#redirect \ub290\ub08c\ud45c\n","contributors":["namubot","R:hoon12560"]},{"nam
espace":"0","title":"!!\uc544\uc557!!","text":"[[\ud30c\uc77c:3444050440.jpg]]\n([[\uc2e0 \uc138\uacc4\uc218\uc758 \u
bbf8\uad81 2]]\uc5d0\uc11c \ub72c !!\uc544\uc557!!)\n{{{+1 \uff01\uff01\u3042\u3042\u3063\u3068\uff01\uff01 }}}\n\n[[
\uc138\uacc4\uc218\uc758 \ubbf8\uad81 \uc2dc\ub9ac\uc988]]\uc5d0 \uc804\ud1b5\uc73c\ub85c \ub4f1\uc7a5\ud558\ub294 \u
b300\uc0ac. [[\uc138\uacc4\uc218\uc758 \ubbf8\uad81 2 \uc81c\uc655\uc758 \uc131\ubc30|2\ud3b8 \uc81c\uc655\uc758 \uc1
31\ubc30]]\ubd80\ud130 \ub4f1\uc7a5\ud588\uc73c\uba70, \ud6cc\ub96d\ud55c [[\uc0ac\ub9dd \ud50c\ub798\uadf8]]\uc758 \
uc608\uc2dc\uc774\ub2e4.\n\n\uc138\uacc4\uc218\uc758 \ubaa8\ud5d8\uac00\ub4e4\uc774 \ud0d0\ud5d8\ud558\ub294 \ub358\u
c804\uc778 \uc218\ud574\uc758 \uad6c\uc11d\uad6c\uc11d\uc5d0\ub294 \ucc44\ucde8\/\ubc8c\ucc44\/\ucc44\uad74 \ud3ec\uc
778\ud2b8\uac00 \uc788\uc73c\uba70, \uc774\ub97c \uc704\ud55c \ucc44\uc9d1 \uc2a4\ud0ac\uc5d0 \ud22c\uc790\ud558\uba7
4 \uc81c\ud55c\ub41c \ucc44\uc9d1 \uae30\ud68c\uc5d0 \ubcf4\ub2e4 \ud070 \uc774\ub4dd\uc744 \ucc59\uae38 \uc218 \uc78
8\ub2e4. \uadf8\ub7ec\ub098 \ubd84\ubc30\ud560 \uc218 \uc788\ub294 \uc2a4\ud0ac \ud3ec\uc778\ud2b8\ub294 \ud55c\uc815
\ub41c \ub9cc\ud07c \ucc44\uc9d1 \uc2a4\ud0ac\uc5d0 \ud22c\uc790\ud558\ub294 \ub9cc\ud07c \uc804\ud22c \uc2a4\ud0ac \
ub808\ubca8\uc740 \ub0ae\uc544\uc9c0\uac8c \ub41c\ub2e4.\n\n 1. \ucc44\uc9d1\uc6a9 \uce90\ub9ad\ud130\ub4e4\ub85c \uc
774\ub8e8\uc5b4\uc9c4 \uc57d\ud55c \ud30c\ud2f0(ex: [[\ub808\uc778\uc800(\uc138\uacc4\uc218\uc758 \ubbf8\uad81 2)|\ub
808\uc778\uc800]] 5\uba85)\uac00 \uc218\ud574\uc5d0 \uc785\uc7a5\ud55c\ub2e4.\n 1. \ud544\ub4dc \uc804\ud22c\ub97c \u
d68c\ud53c\ud558\uba74\uc11c \ucc44\uc9d1 \ud3ec\uc778\ud2b8\uc5d0 \ub3c4\ucc29\ud574 \uc5f4\uc2ec\ud788 \uc544\uc774
\ud15c\uc744 \uce90\ub294 \uc911\uc5d0...\n ..............................
유니코드 인코딩인 것으로 보인다. title, text 필드가 제목과 내용인 것 같다.
JSON 파일이 너무 커서 한 번에 읽을 수도 없다. python은 ijson 패키지에서 처리한다고 하니 살펴본다.
https://pypi.org/project/ijson/
+시험삼아 데이터 몇개만 꺼내 확인해 본다.
import ijson
def load_json(filename):
count=0
with open(filename, 'r') as fd:
parser = ijson.parse(fd)
for prefix, event, value in parser:
if prefix.endswith('.title'):
print('index=', count+1)
print("\nTITLE: %s" % value)
elif prefix.endswith('.text'):
print("\nCONTENT: %s" % value)
count += 1
# debug
if count==10 :
break
if __name__ == "__main__":
print('load_json start')
load_json('/home/psychic/download/namuwiki_20180326.json')
print('load_json end')
글 제목과 내용이 번갈아가면서 출력된다. 알아서 디코딩되어 한글이 바로 잘 출력되었다.
데이터가 너무 많기 때문에 10개만 출력되게 디버깅코드를 추가하였다.
얼마나 많은 데이터가 있는지 카운트를 해 보았다. (출력문이 있으면 상당히 느려지기 때문에 카운트만 계산한다. ) 카운트만 해도 오래 걸린다. 565985 개가 있다. (약 56만건)
+용이한 개발 및 디버깅 테스트를 위해 일부만 자르자. 원본 갖고 바로 작업하는 것은 상당히 비효율적이다. 개발시에는 디버깅용으로 라이트한 데이터만 다루고, 이후 개발이 다 되고 난 후에 전체데이터로 학습을 시키면 된다. 물론, DB가 크다는 가정하에 개발해야 될 것이다. (메모리 관리)
+디버깅용 DB 만들기 / 미니나무위키DB
일부만 자른다. 10메가로 하자.
# head -c 10000000 > mini_namu.json
파일을 열고 마지막 부분을 편집하여 JSON 형식을 맞춘다.
vi로 편집하려했더니 느려서 안되겠다. 그냥 커맨드로 append해야 겠다.
tail -c 800 mini_namu.json | grep -E "title|text"
:"#redirect \uc81c18\ub300 \uad6d\ud68c\uc758\uc6d0 \uc120\uac70\n","contributors":["namubot","R:ttgrgt24","211.187.44.56"]},{"namespace":"0","title":"18\ub85c \uc81c\ud6c4","text":"[include(\ud2c0:18\ub85c \uc81c\ud6c4)]\n\n\u5341\u516b\u8def\u8af8\u4faf\n\n[\ubaa9\ucc28]\n== \uc124\uba85 ==\n[[\ud6c4\ud55c]]\ub9d0 [[190\ub144]], [[\uc5ed\uc801]] [[\ub3d9\ud0c1#s-1|\ub3d9\ud0c1]]\uc744 \ud1a0\ubc8c\ud558\uace0 [[\ud55c\ub098\ub77c#s-1.2|\ud55c\ub098\ub77c]] \ud669\uc2e4\uc744 \uad73\uac74\ud788 \ubc1b\ub4e0\ub2e4\ub294 \uac83\uc744 \ub300\uc758 \uba85\ubd84\uc73c\ub85c \ud558\uc5ec \uc77c\uc5b4\uc120 [[\uc81c\ud6c4]]\ub4e4\uc758 [[\uc5f0\ud569]]. \uc774\ub978\ubc14 '[[\ubc18\ub3d9\ud0c1 \uc5f0\ud569]]'\uc774\ub77c\uace0 \ubd80\ub974\uba70, \ud568\uace1\uad00\uc758 \ub3d9\ucabd \uc9c0\ubc2
#echo -n "0\"}]" >> mini_namu.json
#tail -c 100 mini_namu.json
ud569]]'\uc774\ub77c\uace0 \ubd80\ub974\uba70, \ud568\uace1\uad00\uc758 \ub3d9\ucabd \uc9c0\ubc20"}]
+미니 DB를 사용하여 워드 벡터 생성.
import ijson
import codecs
import sys, os
import time
import nltk
from gensim.models import word2vec
from konlpy.tag import Okt
import pickle
'''
JSON 나무위키 파싱
'''
def load_json(filename):
count=0
with open(filename, 'r') as fd:
# parser = ijson.parse(fd)
# for prefix, event, value in parser:
# print ( 'prefix=', prefix, 'event=', event )
# if prefix.endswith('.title'):
# # print('index=', count+1)
# # print("\nTITLE: %s" % value)
# elif prefix.endswith('.text'):
# # print("\nCONTENT: %s" % value)
# count += 1
# print ('total cnt=', count)
for item in ijson.items(fd, 'item'):
print(item['title'])
# print(item['text'])
def load_and_write_content(filename, filename2):
count=0
file = codecs.open(filename2, 'w', encoding='utf-8')
with open(filename, 'r') as fd:
for item in ijson.items(fd, 'item'):
count+=1
file.write('[[제목]]: ')
file.write(item['title'])
file.write('\n')
file.write('[[내용]]: \n')
file.write(item['text'])
file.write("\n")
file.close()
print('contents count=', count)
def make_wakati(f1, f2, f3):
file = codecs.open(f1, 'r', encoding='utf-8')
text = file.read()
twitter = Okt()
lines = text.split('\r\n')
results = []
for line in lines:
r = []
malist = twitter.pos(line) # part of speech (POS)
# pumsa : Noun, Josa, Verb, Eomi, Punctuation, Number, KoreanParticle,
for word, pumsa in malist:
if not pumsa in ['Josa', 'Eomi', 'Punctuation']:
r.append(word)
results.append((" ".join(r)).strip())
output = (" ".join(results)).strip()
with open(f2, "w", encoding="utf-8") as fp:
fp.write(output)
print('make word2vec')
data = word2vec.LineSentence(f2)
model = word2vec.Word2Vec(data, size=200, window=10, hs=1, min_count=5, sg=1, workers=3)
model.save(f3)
print('end word2vec')
if __name__ == "__main__":
print('load_json start')
print('curdir=', os.getcwd())
filedir = os.path.dirname( os.path.realpath(__file__))
os.chdir(filedir)
print('filedir=', filedir)
filename ='mini_namu.json'
filename2='mini_namu.txt'
filename3='mini_namu.wakati'
filename4='mini_namu.model'
filename5='mini_namu.pkl'
# load_json(filename)
# 나무위키 JSON DB에서 제목과 컨텐트를 스트링으로 기록한 파일 생성. (txt)
if True:
print('Create WordTxT')
t1=time.time()
load_and_write_content(filename, filename2)
t2=time.time()
print('End WordTxt ', 'time=',t2-t1)
if True:
print('Create Wakati')
t1=time.time()
make_wakati(filename2, filename3, filename4)
t2=time.time()
print('End Wakati', 'time=',t2-t1)
if True:
print('Model test')
t1=time.time()
model = word2vec.Word2Vec.load(filename4)
t2=time.time()
print(model.most_similar(positive=["조선", "일본"]))
print(model.most_similar(positive=["고려"]))
print(model.most_similar(positive=["고려"], negative=["왕"]))
print(model.most_similar(positive=["고려", "경제"]))
print(model.most_similar(positive=["RPG", "게임"]))
print(model.most_similar(positive=["임진왜란"]))
print(model.most_similar(positive=["왕", "여자"], negative=["남자"]))
print(model.similarity('고려','공민왕'))
print(model.doesnt_match("아침 점심 저녁 조선 밥".split() ) )
print(model.doesnt_match("총 무기 칼 게임 하늘".split() ) )
print('time=',t2-t1)
gensim.model.word2vec을 사용하여 간단하게 만들어서 테스트해서 아래의 결과가 나왔다.
[('1536년', 0.7473468780517578), ('음력', 0.7256854772567749), ('이이', 0.7070702910423279), ('도요토미', 0.7009462118148804), ('1552년', 0.6966617703437805), ('정조', 0.6919983625411987), ('문인', 0.6826009750366211), ('메이지', 0.6784789562225342), ('덴노', 0.6729492545127869), ('1337년', 0.6693580150604248)]
[('폐위', 0.7308624982833862), ('공민왕', 0.7183995246887207), ('정종', 0.7026641368865967), ('충숙왕', 0.7024295926094055), ('인종', 0.6962566375732422), ('즉위', 0.6933885216712952), ('충혜왕', 0.6890701651573181), ('명종', 0.6795019507408142), ('무신정권', 0.6763734817504883), ('원나라', 0.6668024659156799)]
[('마진', 0.2932665944099426), ('국호', 0.28764602541923523), ('발전', 0.27793973684310913), ('인종', 0.26443612575531006), ('말기', 0.2635525166988373), ('부족하다는', 0.2609822154045105), ('경향', 0.2581954002380371), ('1.6', 0.2573755383491516), ('줄어든', 0.2572077512741089), ('시기', 0.25421619415283203)]
[('천도', 0.6886128783226013), ('무신정권', 0.6836416721343994), ('인종', 0.6704342365264893), ('요나라', 0.6599131226539612), ('공민왕', 0.6575653553009033), ('권력자', 0.6571816205978394), ('도모', 0.6510674357414246), ('신변', 0.6495651006698608), ('폐위', 0.646056056022644), ('몽골', 0.6421409845352173)]
[('동봉', 0.6660709381103516), ('FPS', 0.6493582129478455), ('플래시', 0.6356097459793091), ('슈팅', 0.6346113681793213), ('로더', 0.629481852054596), ('플레이스테이션', 0.6227577924728394), ('TECHNIKA', 0.6164207458496094), ('DJMAX', 0.6132534742355347), ('온라인게임', 0.6100178360939026), ('오어', 0.606633186340332)]
[('1598년', 0.9288400411605835), ('이순신', 0.9017009735107422), ('1618년', 0.7677931189537048), ('1821년', 0.7676668167114258), ('도요토미', 0.7628185749053955), ('1865년', 0.7540280222892761), ('1839년', 0.7511029243469238), ('1868년', 0.7489482760429382), ('문인', 0.7481677532196045), ('1863년', 0.7473239898681641)]
[('속특', 0.4879986345767975), ('왕비', 0.48508623242378235), ('성종', 0.4771915674209595), ('선조', 0.4723415970802307), ('샤', 0.46159034967422485), ('세조', 0.4432491660118103), ('충혜왕', 0.43805891275405884), ('후궁', 0.4356191158294678), ('철종', 0.4354616701602936), ('문종', 0.43034279346466064)]
0.7184
조선
게임
데이터가 적기 때문에 오차가 좀 있지만 그런대로 결과는 나온다.
+실제 나무위키DB 텍스트 변환 (대용량 변환)
파일명들을 실제 나무위키 원본으로 바꾸고 실행해보자.
1단계. create wordtxt에서 encoding 에러 발생함.
open에 옵션으로 errors='ignore'로 무시하고 진행하도록 설정.
참고로 작업 서버 사양은...
CPU : intel i7-7800X CPU 3.5GHz 12core
메모리 : 64기가
GPU: GeForce GTX 1080
시간이 오래걸릴 것으로 예상. 원본json이 8기가가 넘어서, 생성되는 것도 그 쯤 될 것이다.
7분정도 되니까 1기가까지 만들어짐.
25분 걸려서 txt 파일 5기가 정도로 생성 완료하였다.
2단계.wakati 생성. 텍스트 파일을 읽어서 필요한 단어만 모아놓음. (명사, 동사, 형용사, 부사..) word2vec 클래스에서 에러가 발생할 것으로 생각됨. 메모리 부족??
VM 에러 발생. Memory.
# ulimit -c unlimited ; 파일 사이즈 제한 해제도 미리 해 둠.
품사 정보 생성을 위해 Okt (구 Twitter)를 쓰는데, 거기서 발생한 것으로 보임. 라인 길이가 너무 크다. 2424660146 이정도된다.
메모리가 되는 선에서 적당한 크기로 잘라서 부분씩 처리하기로 함. 여기서는 20메가씩 스트링을 처리. 좀 더 크게 잡았더니 계속 실패해서 줄이고 줄임. 스트링을 자르는 방식도 여러가지인데, 그냥 구현하기로... 메모리를 상당히 내부적으로 많이 쓰는 듯 함.
make_wakati 수정
# make wakati file
twitter = Okt(max_heap_size=1024*8) # heap size.. 8G
lines = text.split('\r\n')
t1=time.time()
print('making wakati start')
print('lines count=', len(lines))
fp = open(f2, "w", encoding="utf-8")
for line in lines:
linelen = len(line)
print("line length=", linelen)
# split.
# lineparts = map(''.join, zip(*[iter(line)] * 1000*1000*20)) # 20MB / 1line
blocksize = 1000*1000*20
if linelen % blocksize == 0:
blockcnt = int(linelen/blocksize)
else:
blockcnt = int(linelen / blocksize) + 1
for li in range(blockcnt):
if li==blockcnt-1:
linepart = line[li*blocksize:]
else:
linepart = line[li*blocksize:(li+1)*blocksize]
print('progress=', li, '/', blockcnt, len(linepart) )
malist = twitter.pos(linepart) # part of speech (POS)
# pumsa : Noun, Josa, Verb, Eomi, Punctuation, Number, KoreanParticle,
for word, pumsa in malist:
if not pumsa in ['Josa', 'Eomi', 'Punctuation']:
fp.write(word.strip())
fp.write(" ")
fp.close()
t2=time.time()
print('making wakati end time=', t1-t1)
다음 날 보니 wakati 파일 생성완료. 4G 크기.
3단계로 word vector 생성 진행. 얼마나 진행되었는지 알 수 없다. 언제 끝나지?
메모리 상황을 보면 가상 메모리로 파이썬이 66기가를 쓰고있다. cpu는 3개 쓰기로 설정해서 300% 풀로 나온다.
기나긴 기다림 끝에 드디어 끝났다. 워드 벡터 생성에 걸린 시간은 41564 초. 시간으로 바꾸니 11시간 걸렸다.
전체 과정(와카티 생성, 워드 벡터 생성) 시간은 96503초. 시간으로는 26시간 걸렸다. 반드시 학습한 모델을 저장하는 코드를 넣어라. 저장을 깜빡했다면 이제까지 했던게 허사. 처음 부터 다시 ^^;;
모델 저장이 됐으니 이제, 필요할때 마다 로딩하여 질의를 하면 된다.
워드벡터 모델 파일 사이즈는 236메가. 그리고 넘파일파일이 3개 더 생긴다. 모델파일만 있으니 안돌아가는 걸로 봐서 넘파이 파일도 쓰는것 같다. 다 합쳐서 2기가 정도 된다.
모델 로딩에만 8초 정도 걸린다.
+ 나무위키 모델 테스트
질의 내용
print(model.most_similar(positive=["조선", "일본"]))
print(model.most_similar(positive=["고려"]))
print(model.most_similar(positive=["고려"], negative=["왕"]))
print(model.most_similar(positive=["고려", "경제"]))
print(model.most_similar(positive=["RPG", "게임"]))
print(model.most_similar(positive=["임진왜란"]))
print(model.most_similar(positive=["왕", "여자"], negative=["남자"]))
print(model.similarity('고려','공민왕'))
print(model.doesnt_match("아침 점심 저녁 조선 밥".split() ) )
print(model.doesnt_match("총 무기 칼 게임 하늘".split() ) )
Model test
[('대한제국', 0.6866917610168457), ('중국', 0.6801478862762451), ('우리나라', 0.6566961407661438), ('청나라', 0.6511589288711548), ('일제강점기', 0.6480269432067871), ('한국', 0.6324470043182373), ('조선국', 0.6295247077941895), ('경술국치', 0.6277073621749878), ('임진왜란', 0.623056173324585), ('역사', 0.6140305995941162)]
[('조선', 0.6692752838134766), ('고려왕조', 0.6515953540802002), ('공민왕', 0.6487005949020386), ('문종', 0.6360877752304077), ('현종', 0.6323516368865967), ('무신정권', 0.6292731761932373), ('태조', 0.6256691217422485), ('감안', 0.6252619624137878), ('冊封', 0.6246188879013062), ('충렬왕', 0.6229778528213501)]
[('감안', 0.4361225962638855), ('염두', 0.3996908664703369), ('제승방략', 0.39954277873039246), ('유념', 0.3989650011062622), ('바람직하지', 0.39659619331359863), ('해보아야', 0.39381834864616394), ('갖추자는', 0.39022499322891235), ('추세였음', 0.38095635175704956), ('타당', 0.3788178563117981), ('받아들여지기에', 0.37214767932891846)]
[('정책', 0.7264151573181152), ('동북아', 0.702474057674408), ('사회', 0.7002162933349609), ('정치', 0.6759592294692993), ('불평등', 0.6736841797828674), ('노동시장', 0.6685125827789307), ('경제정책', 0.668291449546814), ('한반도', 0.6653428673744202), ('민생', 0.6610323190689087), ('발전', 0.6543104648590088)]
[('TPS', 0.757465124130249), ('롤플레잉', 0.7494930028915405), ('로그라이크', 0.7455687522888184), ('FPS', 0.7437580823898315), ('플랫포머', 0.7379695177078247), ('RTS', 0.7236455082893372), ('모바일', 0.7205138206481934), ('MOBA', 0.718673586845398), ('SRPG', 0.7134039402008057), ('MMORPG', 0.7068561911582947)]
[('정유재란', 0.8903812170028687), ('병자호란', 0.8541854619979858), ('임란', 0.8498165607452393), ('왜란', 0.8400564193725586), ('정묘호란', 0.7854044437408447), ('1592년', 0.7841730117797852), ('의병장', 0.781627893447876), ('류성룡', 0.7787343263626099), ('왜군', 0.7751662731170654), ('광해군', 0.7695599794387817)]
[('왕인', 0.6398370265960693), ('준왕', 0.6377674341201782), ('왕중', 0.5966447591781616), ('극', 0.5917797088623047), ('최후', 0.5905873775482178), ('선왕', 0.5905437469482422), ('왕전', 0.588803231716156), ('中山王', 0.5885385274887085), ('楚穆王', 0.5835995674133301), ('南平王', 0.5762388706207275)]
0.648701
조선
게임
model.wv.get_vector('노트북') 을 해보니 200개의 수가 나온다. 200차원. 모델 생성시에 사이즈를 200으로 줘서 그렇다. 값이 클 수록 표현하는 정보가 큰데, 학습 시간이 더 오래 걸린다.
그런대로 재미가 있다.
다음엔 이걸로 무엇을 할 수 있을지 생각해 봐야겠다.