diff --git a/.github/doc/khaiii_for_space_error.pptx b/.github/doc/khaiii_for_space_error.pptx new file mode 100644 index 0000000..6eaaa80 Binary files /dev/null and b/.github/doc/khaiii_for_space_error.pptx differ diff --git a/.github/img/network.pptx b/.github/doc/network.pptx similarity index 52% rename from .github/img/network.pptx rename to .github/doc/network.pptx index be6de9e..37a757f 100644 Binary files a/.github/img/network.pptx and b/.github/doc/network.pptx differ diff --git a/.github/img/multi-task-learning.png b/.github/img/multi-task-learning.png new file mode 100644 index 0000000..48f5718 Binary files /dev/null and b/.github/img/multi-task-learning.png differ diff --git a/.github/img/network.png b/.github/img/network.png index 59886ce..23b6c84 100644 Binary files a/.github/img/network.png and b/.github/img/network.png differ diff --git a/README.md b/README.md index f395999..bd8ac3d 100644 --- a/README.md +++ b/README.md @@ -22,18 +22,34 @@ CNN 모델에 대한 상세한 내용은 [CNN 모델](https://github.com/kakao/k 성능 ---- ### 정확도 + +#### v0.3 CNN 모델의 주요 하이퍼 파라미터는 분류하려는 음절의 좌/우 문맥의 크기를 나타내는 win 값과, 음절 임베딩의 차원을 나타내는 emb 값입니다. win 값은 {2, 3, 4, 5, 7, 10}의 값을 가지며, emb 값은 {20, 30, 40, 50, 70, 100, 150, 200, 300, 500}의 값을 가집니다. 따라서 이 두 가지 값의 조합은 6 x 10으로 총 60가지를 실험하였고 아래와 같은 성능을 보였습니다. 성능 지표는 정확률과 재현율의 조화 평균값인 F-Score입니다. ![](.github/img/win_emb_f.png) win 파라미터의 경우 3 혹은 4에서 가장 좋은 성능을 보이며 그 이상에서는 오히려 성능이 떨어집니다. emb 파라미터의 경우 150까지는 성능도 같이 높아지다가 그 이상에서는 별 차이가 없습니다. 최 상위 5위 중 비교적 작은 모델은 win=3, emb=150으로 F-Score 값은 97.11입니다. 이 모델을 large 모델이라 명명합니다. +#### v0.4 +[띄어쓰기 오류에 강건한 모델을 위한 실험](https://github.com/kakao/khaiii/wiki/%EB%9D%84%EC%96%B4%EC%93%B0%EA%B8%B0-%EC%98%A4%EB%A5%98%EC%97%90-%EA%B0%95%EA%B1%B4%ED%95%9C-%EB%AA%A8%EB%8D%B8%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%8B%A4%ED%97%98)을 통해 모델을 개선하였습니다. v0.4 모델은 띄어쓰기가 잘 되어있지 않은 입력에 대해 보다 좋은 성능을 보이는데 반해 세종 코퍼스에서는 다소 정확도가 떨어집니다. 이러한 점을 보완하기 위해 base 및 large 모델의 파라미터를 아래와 같이 조금 변경했습니다. + +* base 모델: win=4, emb=35, F-Score: 94.96 +* large 모델: win=4, emb=180, F-Score: 96.71 + ### 속도 + +#### v0.3 모델의 크기가 커지면 정확도가 높아지긴 하지만 그만큼 계산량 또한 많아져 속도가 떨어집니다. 그래서 적당한 정확도를 갖는 모델 중에서 크기가 작아 속도가 빠른 모델을 base 모델로 선정하였습니다. F-Score 값이 95 이상이면서 모델의 크기가 작은 모델은 win=3, emb=30이며 F-Score는 95.30입니다. 속도를 비교하기 위해 1만 문장(총 903KB, 문장 평균 91)의 텍스트를 분석해 비교했습니다. base 모델의 경우 약 10.5초, large 모델의 경우 약 78.8초가 걸립니다. +#### v0.4 +모델의 크기가 커짐에 따라 아래와 같이 base, large 모델의 속도를 다시 측정했으며 v0.4 버전에서 다소 느려졌습니다. + +* base 모델: 10.8 -> 14.4 +* large 모델: 87.3 -> 165 + 사용자 사전 ---- diff --git a/include/khaiii/khaiii_api.h b/include/khaiii/khaiii_api.h index c064ed1..40826e1 100644 --- a/include/khaiii/khaiii_api.h +++ b/include/khaiii/khaiii_api.h @@ -12,7 +12,7 @@ // constants // /////////////// #define KHAIII_VERSION_MAJOR 0 -#define KHAIII_VERSION_MINOR 3 +#define KHAIII_VERSION_MINOR 4 #define _MAC2STR(m) #m #define _JOIN_VER(x,y) _MAC2STR(x) "." _MAC2STR(y) // NOLINT #define KHAIII_VERSION _JOIN_VER(KHAIII_VERSION_MAJOR,KHAIII_VERSION_MINOR) // NOLINT diff --git a/rsc/bin/compile_errpatch.py b/rsc/bin/compile_errpatch.py index 7e49fd9..7f1cecf 100755 --- a/rsc/bin/compile_errpatch.py +++ b/rsc/bin/compile_errpatch.py @@ -21,7 +21,7 @@ from typing import Dict, List, Tuple from khaiii.resource.char_align import Aligner, align_patch -from khaiii.resource.resource import load_restore_dic, load_vocab_out +from khaiii.resource.resource import load_vocab_out, parse_restore_dic from khaiii.resource.morphs import Morph, ParseError, mix_char_tag from khaiii.resource.trie import Trie @@ -221,7 +221,7 @@ def run(args: Namespace): args: program arguments """ aligner = Aligner(args.rsc_src) - restore_dic = load_restore_dic('{}/restore.dic'.format(args.rsc_src)) + restore_dic = parse_restore_dic('{}/restore.dic'.format(args.rsc_src)) if not restore_dic: sys.exit(1) vocab_out = load_vocab_out(args.rsc_src) diff --git a/rsc/bin/compile_preanal.py b/rsc/bin/compile_preanal.py index 649e94c..fcc30fd 100755 --- a/rsc/bin/compile_preanal.py +++ b/rsc/bin/compile_preanal.py @@ -24,7 +24,7 @@ from khaiii.munjong import sejong_corpus from khaiii.resource.char_align import Aligner, AlignError, align_to_tag from khaiii.resource.morphs import Morph, ParseError -from khaiii.resource.resource import load_restore_dic, load_vocab_out +from khaiii.resource.resource import load_vocab_out, parse_restore_dic from khaiii.resource.trie import Trie from compile_restore import append_new_entries @@ -231,7 +231,7 @@ def run(args: Namespace): args: program arguments """ aligner = Aligner(args.rsc_src) - restore_dic = load_restore_dic('{}/restore.dic'.format(args.rsc_src)) + restore_dic = parse_restore_dic('{}/restore.dic'.format(args.rsc_src)) if not restore_dic: sys.exit(1) restore_new = defaultdict(dict) diff --git a/rsc/bin/compile_restore.py b/rsc/bin/compile_restore.py index c5a0172..2f5951d 100755 --- a/rsc/bin/compile_restore.py +++ b/rsc/bin/compile_restore.py @@ -20,7 +20,7 @@ from typing import Dict from khaiii.resource.morphs import TAG_SET -from khaiii.resource.resource import load_restore_dic, load_vocab_out +from khaiii.resource.resource import load_vocab_out, parse_restore_dic ############# @@ -139,7 +139,7 @@ def run(args: Namespace): Args: args: program arguments """ - restore_dic = load_restore_dic('{}/restore.dic'.format(args.rsc_src)) + restore_dic = parse_restore_dic('{}/restore.dic'.format(args.rsc_src)) if not restore_dic: sys.exit(1) vocab_out = load_vocab_out(args.rsc_src) diff --git a/rsc/src/base.config.json b/rsc/src/base.config.json index b8989ec..55d53fe 100644 --- a/rsc/src/base.config.json +++ b/rsc/src/base.config.json @@ -1,24 +1,8 @@ { - "batch_grow": 10000, - "batch_size": 500, - "check_iter": 10000, - "context_len": 7, - "cutoff": 2, - "debug": false, - "embed_dim": 30, - "epoch": 104, - "gpu_num": 7, - "hidden_dim": 310, - "in_pfx": "./data/pos_tagger/munjong", - "iter_best": 4440000, - "iteration": 4440000, - "learning_rate": 3.3813919135227317e-06, - "log_dir": "./logdir", - "lr_decay": 0.9, - "model_id": "munjong.cnn.cut2.win3.emb30.lr0.001.lrd0.9.bs500.ci10000.bg10000", - "model_name": "cnn", - "out_dir": "./logdir/munjong.cnn.cut2.win3.emb30.lr0.001.lrd0.9.bs500.ci10000.bg10000", - "patience": 10, - "rsc_src": "../rsc/src", - "window": 3 -} + "cutoff": 1, + "embed_dim": 35, + "hidden_dim": 320, + "model_id": "munjong.cut1.win4.sdo0.1.emb35.lr0.001.lrd0.9.bs500", + "rsc_src": "../rsc/src", + "window": 4 +} \ No newline at end of file diff --git a/rsc/src/base.errpatch.auto b/rsc/src/base.errpatch.auto index b3081cb..cfeea0b 100644 --- a/rsc/src/base.errpatch.auto +++ b/rsc/src/base.errpatch.auto @@ -1,847 +1,546 @@ -_______________ _/SS + __/SS + __/SS + __/SS + __/SS + __/SS + __/SS + __/SS + | _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + | -아야지 하 아야지/EC + _ + 하/VX 아야지/EC + _ + 하/VV - 승승장구 _ + 승승/NNG + 장구/NNG _ + 승승장구/NNG -_______________ _/SS + __/SS + __/SS + __/SS + __/SS + __/SS + __/SS + __/SS _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS + _/SS - 어불성설 _ + 어불/NNG + 성설/NNG _ + 어불성설/NNG -닥터 오는 닥터/NNG + _ + 오/VV + 는/ETM 닥터/NNG + _ + 오/NNP + 는/JX -라기보다 이/VCP + 라/ETN + 기/NNG + 보다/JKB 이/VCP + 라기/ETN + 보다/JKB - 만병통치약 _ + 만병/NNG + 통치약/NNG _ + 만병통치약/NNG -한국야구위원회 한국야/NNP + 구/NNG + 위/NNP + 원회/NNG 한국야구위원회/NNP -원시토기문화 원시/NNG + 토기문화/NNG 원시/NNG + 토기/NNG + 문화/NNG - 박장대소 _ + 박장/NNG + 대소/NNG _ + 박장대소/NNG -블록버스터 블록버스/NNG + 터/NNP 블록버스터/NNG -국제통화기금( 국제/NNG + 통화/NNG + 기금/NNG + (/SS 국제통화기금/NNP + (/SS -총영사관 총영/NNG + 사관/NNG 총영사관/NNG - 방울방울 _ + 방울/NNG + 방울/NNP _ + 방울방울/NNG - 자동차회사 _ + 자동/NNG + 차회사/NNG _ + 자동차/NNG + 회사/NNG -지나친 솟구침' 지나치/VA + ㄴ/ETM + _ + 솟/VV + 구/NNP + 치/VV + ㅁ/ETN + '/SS 지나치/VA + ㄴ/ETM + _ + 솟구치/VV + ㅁ/ETN + '/SS - 로마시대 _ + 로마시/NNP + 대/NNG _ + 로마/NNP + 시대/NNG - 기성회비 _ + 기성/NNG + 회비/NNG _ + 기성회비/NNG -과 음악사이" 과/JKB + _ + 음악사이/NNG + "/SS 과/JKB + _ + 음악/NNG + 사이/NNG + "/SS -보통 사람 보/NNG + 통/MAG + _ + 사람/NNG 보통/NNG + _ + 사람/NNG - 피투성이 _ + 피투/NNG + 성이/XSN _ + 피/NNG + 투성이/XSN -과 의논 과/JC + _ + 의논/NNG 과/JKB + _ + 의논/NNG -알타스투르 소르노바 알타스투르/NNP + _ + 소르노바/NNP 알타스투르/NNG + _ + 소르노바/NNG -통일국가 통일국가/NNG 통일/NNG + 국가/NNG -승승장구하 승승/NNG + 장구/NNG + 하/XSV 승승장구/NNG + 하/XSV -더라니까. 더/EC + 라니까/EF + ./SF 더라니까/EF + ./SF - 전지전능한 _ + 전지/NNG + 전능/NNG + 하/XSA + ㄴ/ETM _ + 전지전능/NNG + 하/XSA + ㄴ/ETM - 규모면에서 _ + 규모면/NNG + 에서/JKB _ + 규모/NNG + 면/NNG + 에서/JKB - 원상회복 _ + 원상/NNG + 회복/NNG _ + 원상회복/NNG -대외경제정책연구원 대외/NNG + 경제정책/NNG + 연구원/NNG 대외/NNG + 경제/NNG + 정책/NNG + 연구원/NNG -올라가 있 올라가/VV + 아/EC + _ + 있/VV 올라가/VV + 아/EC + _ + 있/VX -해설 및 감상 해설/NNG + _ + 및/MAG + _ + 감상/NNG 해설/NNG + _ + 및/MAJ + _ + 감상/NNG - 인지상정 _ + 인지/NNG + 상정/NNG _ + 인지상정/NNG -을 한 차원 을/JKO + _ + 하/VV + ㄴ/ETM + _ + 차원/NNG 을/JKO + _ + 한/MM + _ + 차원/NNG -지나친 솟구침 지나치/VA + ㄴ/ETM + _ + 솟/VV + 구/NNP + 치/VV + ㅁ/ETN 지나치/VA + ㄴ/ETM + _ + 솟구치/VV + ㅁ/ETN -여러 가지 여러/MM + _ + 가/VV + 지/NNB 여러/MM + _ + 가지/NNB -세계 대전 이후 세계/NNG + _ + 대/NNG + 전/NNP + _ + 이후/NNG 세계/NNG + _ + 대전/NNG + _ + 이후/NNG -방울방울 방울/NNG + 방울/NNP 방울방울/NNG -했겠어요 하/VV + 였겠/EP + 어요/EF 하/VV + 였/EP + 겠/EP + 어요/EF -아침 국제신문 아침/NNG + _ + 국제/NNG + 신문/NNG 아침/NNG + _ + 국제신문/NNP -의 서구화 의/JKG + _ + 서/NNP + 구/NNG + 화/XSN 의/JKG + _ + 서구/NNP + 화/XSN -통행금지 통행/NNG + 금지/NNG 통행금지/NNG -<전국패트롤> /SS /SS -제일 기획 제/XPN + 일/NR + _ + 기획/NNG 제일/NNP + _ + 기획/NNG -<전국패트롤 전/NNG + 국패/NNP + 트/NNG + 롤/NNP + >/SS 전국패트롤/NNP + >/SS -논리정연 논리정/NNG + 연/XR 논리/NNG + 정연/XR -통일전선정부 통일/NNG + 전선정부/NNG 통일/NNG + 전선/NNG + 정부/NNG - 미술사가 _ + 미술/NNG + 사가/NNG _ + 미술사가/NNG -담임교사 담임/NNG + 교사/NNG 담임교사/NNG -월북작가 월북작가/NNG 월북/NNG + 작가/NNG - 반벌거숭이 _ + 반벌/NNG + 거숭이/NNG _ + 반/NNG + 벌거숭이/NNG -그래 놓 | + 그/IC + 러/VV + 어/EC + _ + 놓/VX | + 그러/VV + 어/EC + _ + 놓/VX - 요지부동 _ + 요지/NNG + 부동/NNG _ + 요지부동/NNG -남한학계 남한학/NNP + 계/XSN 남한/NNP + 학계/NNG -경기 전 경기/NNP + _ + 전/MM 경기/NNG + _ + 전/NNG - 성냥개비 _ + 성냥/NNG + 개비/NNG _ + 성냥개비/NNG -방랑시인 방랑시인/NNG 방랑/NNG + 시인/NNG -인신매매범 인신/NNG + 매매범/NNG 인신매매범/NNG -호이 호이 호이/NNG + _ + 호이/NNG 호이/MAG + _ + 호이/MAG -같으니라구 같/VA + 으/EC + 니라구/EF 같/VA + 으니라구/EF -사정사정 사정/NNG + 사정/NNG 사정사정/NNG -공원묘지 공원/NNG + 묘지/NNG 공원묘지/NNG +·중증급성호흡기증후군 ·/SP + 중증급/NNG + 성/XSN + 호흡기증후군/NNG ·/SP + 중증/NNG + 급성/NNG + 호흡기/NNG + 증후군/NNG +·중증급성호흡기증후군) ·/SP + 중증급/NNG + 성/XSN + 호흡기증후군/NNG + )/SS ·/SP + 중증/NNG + 급성/NNG + 호흡기/NNG + 증후군/NNG + )/SS + 신보수주의 _ + 신보수주의/NNG _ + 신/XPN + 보수주의/NNG +동아시아문학사비교론》 동아시아문/NNP + 학/NNG + 사비교론/NNP + 》/SS 동아시아문학사비교론/NNP + 》/SS +세시 풍속 세/MM + 시/NNG + _ + 풍속/NNG 세/MM + 시/NNB + _ + 풍속/NNG +주워들었 줍/VV + 어/EC + 들/VV + 었/EP 주워듣/VV + 었/EP +의 절대 다수 의/JKG + _ + 절/MAG + 대/NNG + _ + 다수/NNG 의/JKG + _ + 절대/NNG + _ + 다수/NNG +펴 나가 펴/VV + 어/EC + _ + 나/VV + 가/VX 펴/VV + 어/EC + _ + 나가/VX +《동아시아문학사비교론》 《/SS + 동아시아문/NNP + 학/NNG + 사비교론/NNP + 》/SS 《/SS + 동아시아문학사비교론/NNP + 》/SS +가죽가방 가죽가방/NNG 가죽/NNG + 가방/NNG +배 코리안 리그 배/NNB + _ + 코/NNG + 리/NNP + 안/NNG + _ + 리그/NNG 배/NNB + _ + 코리안/NNG + _ + 리그/NNG +카니 정 카니/NNP + _ + 정/NNG 카니/NNP + _ + 정/NNP +남북 고위급 회담 남북/NNP + _ + 고위/NNG + 급/NNG + _ + 회담/NNG 남북/NNP + _ + 고위급/NNG + _ + 회담/NNG + 정신착란 _ + 정신착란/NNG _ + 정신/NNG + 착란/NNG + 후생복지 _ + 후생복지/NNG _ + 후생/NNG + 복지/NNG +이탈리아전 이탈/NNP + 리아전/NNG 이탈리아전/NNG +배 코리안 배/NNB + _ + 코/NNG + 리/NNP + 안/NNG 배/NNB + _ + 코리안/NNG +벤처기업육성 벤처/NNG + 기업육/NNG + 성/XSN 벤처/NNG + 기업/NNG + 육성/NNG +알 로버릭 알/NNG + _ + 로버릭/NNP 알/NNP + _ + 로버릭/NNP +가져다줄 가지/VV + 어다/EC + 주/VX + ㄹ/ETM + _ 가져다주/VV + ㄹ/ETM + _ +중증급성호흡기증후군) 중증급/NNG + 성/XSN + 호흡기증후군/NNG + )/SS 중증/NNG + 급성/NNG + 호흡기/NNG + 증후군/NNG + )/SS +이 되살아났 이/JKC + _ + 되살아나/VV + 았/EP 이/JKS + _ + 되살아나/VV + 았/EP + 완전식민지 _ + 완전식민지/NNG _ + 완전/NNG + 식민지/NNG +떠나야 했 떠나/VV + 아야/EC + _ + 하/VX + 였/EP 떠나/VV + 야/EC + _ + 하/VX + 였/EP + 정아무개 _ + 정/NNG + 아무개/NP _ + 정/NNP + 아무개/NP +남북대화 남/NNG + 북/NNP + 대화/NNG 남북/NNP + 대화/NNG +기능면에서 기능면/NNG + 에서/JKB 기능/NNG + 면/NNG + 에서/JKB +절대 다수 절/MAG + 대/NNG + _ + 다수/NNG 절대/NNG + _ + 다수/NNG +《동아시아문학사비교론 《/SS + 동아시아문/NNP + 학/NNG + 사비교론/NNP 《/SS + 동아시아문학사비교론/NNP +불공정보도 불/XPN + 공정보도/NNG 불/XPN + 공정/NNG + 보도/NNG +고위급 회담 고위/NNG + 급/NNG + _ + 회담/NNG 고위급/NNG + _ + 회담/NNG +미래지향적 미래지향/NNG + 적/XSN 미래/NNG + 지향/NNG + 적/XSN +만기 형 만/NNG + 기/NNP + _ + 형/NNG 만기/NNP + _ + 형/NNG +"물론입니다 "/SS + 물/MAG + 론/NNG + 이/VCP + ㅂ니다/EF "/SS + 물론/NNG + 이/VCP + ㅂ니다/EF +"부처님 "/SS + 부처/NNG + 님/XSN "/SS + 부처/NNP + 님/XSN +비정규직 비/XPN + 정/NNG + 규직/NNG 비/XPN + 정규/NNG + 직/NNG +고칼슘혈증 고칼슘/NNG + 혈증/NNG 고/XPN + 칼슘/NNG + 혈증/NNG +경남 마산합포 경남/NNP + _ + 마산합포/NNP 경남/NNP + _ + 마산/NNP + 합포/NNP + 국제통화기금( _ + 국제/NNG + 통화기금/NNG + (/SS _ + 국제통화기금/NNP + (/SS +이올시다. 이/VCP + 오/VV + ㄹ시다/EF + ./SF 이/VCP + 올시다/EF + ./SF +민 중위 민/NNG + _ + 중위/NNG 민/NNP + _ + 중위/NNG +동아시아문학사비교론 동아시아문/NNP + 학/NNG + 사비교론/NNP 동아시아문학사비교론/NNP +하고 이원명 하/VV + 고/JKQ + _ + 이원명/NNP 하/VV + 고/EC + _ + 이원명/NNP +가계금전신탁 가계금전/NNG + 신탁/NNG 가계/NNG + 금전/NNG + 신탁/NNG +의 부정부패 의/JKG + _ + 부정/NNG + 부패/NNG 의/JKG + _ + 부정부패/NNG +(한국시각 (/SS + 한/NNG + 국/NNP + 시각/NNG (/SS + 한국/NNP + 시각/NNG +을 한 눈 을/JKO + _ + 하/VV + ㄴ/ETM + _ + 눈/NNG 을/JKO + _ + 한/MM + _ + 눈/NNG + 서비스산업 _ + 서비스산업/NNG _ + 서비스/NNG + 산업/NNG + 사각지대 _ + 사각/NNG + 지대/NNG _ + 사각지대/NNG +태조 왕건 태조/NNP + _ + 왕건/NNG 태조/NNP + _ + 왕건/NNP 여성주의 여성주의/NNG 여성/NNG + 주의/NNG - 위기관리 _ + 위기/NNG + 관리/NNG _ + 위기관리/NNG -하루하루 | + 하/NNG + 루하루/MAG | + 하루하루/MAG -까지 나가 까지/JX + _ + 나/VV + 가/VX 까지/JX + _ + 나가/VV -탄소배출권 탄소배출/NNG + 권/XSN 탄소/NNG + 배출/NNG + 권/XSN -안전지대 안전/NNG + 지대/NNG 안전지대/NNG - 캐리커처 _ + 캐리/NNG + 커처/NNG _ + 캐리커처/NNG -의미심장 의미심/XR + 장/NNG 의미심장/XR -적이 되 적/XSN + 이/VCP + _ + 되/VV 적/XSN + 이/JKC + _ + 되/VV -다는 것 다/EF + 는/ETM + _ + 것/NNB 다는/ETM + _ + 것/NNB - 아이러니 _ + 아이러/NNG + 니/EC _ + 아이러니/NNG -다고 생각 다/EF + 고/EC + _ + 생각/NNG 다고/EC + _ + 생각/NNG -소이기아 소/NNG + 이기아/NNP 소이기아/NNP -적반하장 적반/NNG + 하장/NNG 적반하장/NNG -컴퓨터그래픽스 컴퓨터그래픽스/NNG 컴퓨터/NNG + 그래픽스/NNG -민족상잔 민족/NNG + 상잔/NNG 민족상잔/NNG -경제정의 경제/NNG + 정/NNG + 의/NNP 경제/NNG + 정의/NNG -국립국어연구원 국립국어/NNG + 연구원/NNG 국립국어연구원/NNP - 현모양처 _ + 현모/NNG + 양처/NNG _ + 현모양처/NNG -신보수주의 신/XPN + 보/NNG + 수주의/NNG 신/XPN + 보수주의/NNG -기로서니 기/ETN + 로서니/EC + _ 기로서니/EC + _ -네 사람 | + 너/NP + 의/JKG + _ + 사람/NNG | + 네/MM + _ + 사람/NNG -계열사간 계열/NNG + 사/NNG + 간/NNB 계열사/NNG + 간/NNB -사립학교 사립/NNG + 학교/NNG 사립학교/NNG -위기관리 위기/NNG + 관리/NNG 위기관리/NNG -와 헤어져 와/JC + _ + 헤어지/VV + 어/EC 와/JKB + _ + 헤어지/VV + 어/EC -학력고사 학력/NNG + 고사/NNG 학력고사/NNG -천진난만 천진난/NNG + 만/NNB 천진난만/NNG - 틈새시장 _ + 틈새/NNG + 시장/NNG _ + 틈새시장/NNG -핵심인물 핵심인물/NNG 핵심/NNG + 인물/NNG -아 나서 아/EC + _ + 나/VX + 서/VV 아/EC + _ + 나서/VV - 블록버스터 _ + 블록버스/NNG + 터/NNP _ + 블록버스터/NNG -보수주의 보/NNG + 수주의/NNG 보수주의/NNG -대외경제정책 대외/NNG + 경제정책/NNG 대외/NNG + 경제/NNG + 정책/NNG -해설 및 해설/NNG + _ + 및/MAG 해설/NNG + _ + 및/MAJ - 성평등성 _ + 성평등/NNG + 성/XSN _ + 성/NNG + 평등/NNG + 성/XSN -주심포식 주심/NNG + 포식/NNG 주심포식/NNG -한국교사휴양원 한국교사휴/NNP + 양원/NNG 한국교사휴양원/NNP -{장자} {/SS + 장/NNP + 자/NNG + }/SS {/SS + 장자/NNP + }/SS -한국방송사 한국방/NNP + 송사/NNG 한국/NNP + 방송사/NNG - 논리정연 _ + 논리정/NNG + 연/XR _ + 논리/NNG + 정연/XR - 천진난만 _ + 천/NNP + 진난/NNG + 만/NNB _ + 천진난만/NNG -'란 ' '/SS + 이/VCP + 란/ETM + _ + '/SS '/SS + 란/JX + _ + '/SS -김포매립지 김포매/NNP + 립지/NNG + _ 김포/NNP + 매립지/NNG + _ -겨울리그 겨울리그/NNG 겨울/NNG + 리그/NNG - 태평성대 _ + 태평/NNG + 성대/NNG _ + 태평성대/NNG -에서 등을 에서/JKB + _ + 등/NNB + 을/JKO 에서/JKB + _ + 등/NNG + 을/JKO -로마시대 로마시/NNP + 대/NNG 로마/NNP + 시대/NNG -을 막아서 을/JKO + _ + 막/VV + 아/EC + 서/VV 을/JKO + _ + 막아서/VV -지 말기 지/EC + _ + 말기/NNG 지/EC + _ + 말/VX + 기/ETN - 전력투구 _ + 전력/NNG + 투구/NNG _ + 전력투구/NNG -제국주의시대 제국주의시대/NNG 제국주의/NNG + 시대/NNG -안하무인 안/MAG + 하무인/NNG 안하무인/NNG - 자급자족 _ + 자급/NNG + 자족/NNG _ + 자급자족/NNG -방송사노조 방송/NNG + 사노조/NNG 방송사/NNG + 노조/NNG -자가당착 자가/NNG + 당착/NNG 자가당착/NNG -개체수가 개체수/NNG + 가/JKS 개체/NNG + 수/NNG + 가/JKS -사회민주당 사회/NNG + 민주당/NNG 사회민주당/NNP - 반전평화 _ + 반/XPN + 전/NNG + 평화/NNG _ + 반전/NNG + 평화/NNG -이전투구 이전/NNG + 투구/NNG 이전투구/NNG -나무그늘 나무그늘/NNG 나무/NNG + 그늘/NNG -조정실장 조정실장/NNG 조정/NNG + 실장/NNG - 국제법상 _ + 국제/NNG + 법/NNG + 상/XSN _ + 국제법/NNG + 상/XSN - 산전수전 _ + 산전/NNG + 수전/NNG + _ _ + 산전수전/NNG + _ -▽상소= ▽/SW + 상소/NNG + =/SW ▽/SW + 상소/NNP + =/SW - 방랑시인 _ + 방랑시인/NNG _ + 방랑/NNG + 시인/NNG -장난기가 장난기/NNG + 가/JKS 장난/NNG + 기/XSN + 가/JKS -하지만, 하지/MAJ + 만/EC + ,/SP 하지만/MAJ + ,/SP - 핵심인물 _ + 핵심인물/NNG _ + 핵심/NNG + 인물/NNG -………" …/SE + ……/SE + "/SS …/SE + …/SE + …/SE + "/SS -본질주의 본질주의/NNG 본질/NNG + 주의/NNG -다고 지적 다고/EC + _ + 지/NNG + 적/XSN 다고/EC + _ + 지적/NNG -손 오공 손/NNG + _ + 오공/NNG 손/NNP + _ + 오공/NNP -이란 개념 이란/JX + _ + 개념/NNG 이/VCP + 란/ETM + _ + 개념/NNG -취업자수 취업/NNG + 자수/NNG 취업자/NNG + 수/NNG -밸런타인감독 밸/NNP + 런/NNG + 타인/NNP + 감독/NNG 밸런타인/NNP + 감독/NNG -연중무휴 연중/NNG + 무휴/NNG 연중무휴/NNG -물항아리 물항아리/NNG 물/NNG + 항아리/NNG -농촌진흥청 농촌/NNG + 진흥청/NNG 농촌/NNG + 진흥/NNG + 청/NNG - 신부전증 _ + 신부/NNG + 전증/NNG _ + 신부전증/NNG - 대구지역 _ + 대구지/NNP + 역/NNG _ + 대구/NNP + 지역/NNG -불국사의 불국/NNP + 사/NNG + 의/JKG 불국사/NNP + 의/JKG -깨나 있 깨나/JX + _ + 있/VX 깨나/JX + _ + 있/VV -간 쇠고기 가/VV + ㄴ/ETM + _ + 쇠고기/NNG 갈/VV + ㄴ/ETM + _ + 쇠고기/NNG -동네아이 동네아이/NNG 동네/NNG + 아이/NNG -현대 문명 | + 현/NNG + 대/NNP + _ + 문명/NNG | + 현대/NNG + _ + 문명/NNG -경남지역 경남지/NNP + 역/NNG 경남/NNP + 지역/NNG -이탈리아전 이탈리/NNP + 아전/NNG 이탈리아전/NNG -이목구비 이목/NNG + 구비/NNG 이목구비/NNG - 삼성자동차 _ + 삼성자동/NNP + 차/NNG _ + 삼성자동차/NNP -반벌거숭이 반벌/NNG + 거숭이/NNG 반/NNG + 벌거숭이/NNG - 실수요자 _ + 실수/NNG + 요자/NNG _ + 실수요자/NNG -장 담그 장/NNP + _ + 담그/VV 장/NNG + _ + 담그/VV -무역자유화 무역/NNG + 자유화/NNG 무역/NNG + 자유/NNG + 화/XSN -조차 하다 조차/JX + _ + 하/VV + 다/EF 조차/JX + _ + 하/VX + 다/EF -하나하나 하/MAG + 나하나/NNG 하나하나/NNG - 전지전능 _ + 전지/NNG + 전능/NNG _ + 전지전능/NNG -브로콜리 브/NNP + 로콜리/NNG 브로콜리/NNG -제스프리 제스프/NNP + 리/NNG 제스프리/NNP -저 자신 | + 저/MM + _ + 자신/NNG | + 저/NP + _ + 자신/NNG -전용면적 전용면적/NNG 전용/NNG + 면적/NNG -생활쓰레기 생활쓰레기/NNG 생활/NNG + 쓰레기/NNG -한편 1997 | + 한편/MAG + _ + 1997/SN | + 한편/NNG + _ + 1997/SN -나 보지 나/EC + _ + 보/VV + 지/EF 나/EC + _ + 보/VX + 지/EF -16쿠데타 16/SN + 쿠/NNP + 데타/NNG 16/SN + 쿠데타/NNG -으리라는 으/EC + 리라는/ETM 으리라는/ETM -절대 빈곤 절대/MAG + _ + 빈곤/NNG 절대/NNG + _ + 빈곤/NNG -진오기굿 진오기굿/NNG 진오기/NNG + 굿/NNG -서먹서먹 서/VV + 먹서먹/XR 서먹서먹/XR -으니라구 으/EC + 니라구/EF 으니라구/EF -나르시시즘 나/NNP + 르시시즘/NNG 나르시시즘/NNG -요지부동 요지/NNG + 부동/NNG 요지부동/NNG -이슬람 교도 이슬람/NNG + _ + 교도/NNG 이슬람/NNP + _ + 교도/NNG -제네바협정 제네바협정/NNP 제네바/NNP + 협정/NNG - 가족계획 _ + 가족/NNG + 계획/NNG _ + 가족계획/NNG - 화기애애 _ + 화/NNG + 기애애/XR _ + 화기애애/XR -태평성대 태평/NNG + 성대/NNG 태평성대/NNG -국어교과서 국어교과서/NNG 국어/NNG + 교과서/NNG -성의 있 성/NNG + 의/JKG + _ + 있/VV 성의/NNG + _ + 있/VV -탄화수소 탄화/NNG + 수소/NNG 탄화수소/NNG -한편 1997 한편/MAG + _ + 1997/SN 한편/NNG + _ + 1997/SN - 전원도시 _ + 전원/NNG + 도시/NNG _ + 전원도시/NNG -그러니만큼 그러/VV + 니만/EC + 큼/JKB + _ 그러/VV + 니만큼/EC + _ - 음악사이 _ + 음악사이/NNG _ + 음악/NNG + 사이/NNG -질서 있는 질서/NNG + _ + 있/VX + 는/ETM 질서/NNG + _ + 있/VV + 는/ETM -자자손손 자자/NNG + 손손/NNG 자자손손/NNG -통신업체 통신/NNG + 업체/NNG 통신업체/NNG -수사의문문 수사의문문/NNG 수사/NNG + 의문문/NNG -밸런타인 밸/NNP + 런/NNG + 타인/NNP 밸런타인/NNP -서울지역 서울지/NNP + 역/NNG 서울/NNP + 지역/NNG -우리교육 우/NNP + 리/NNG + 교육/NNG 우리교육/NNP -대처 총리 대처/NNG + _ + 총리/NNG 대처/NNP + _ + 총리/NNG - 봉두완이 _ + 봉두완/NNP + 이/NNG _ + 봉두완이/NNP -기성회비 기성/NNG + 회비/NNG 기성회비/NNG - 재산형성 _ + 재산형성/NNG _ + 재산/NNG + 형성/NNG - 장례식장 _ + 장례/NNG + 식장/NNG _ + 장례식장/NNG -과 어긋나 과/JC + _ + 어긋나/VV 과/JKB + _ + 어긋나/VV -영양권장량 영양권장량/NNG 영양/NNG + 권장/NNG + 량/NNG -사사건건 사사건/MAG + 건/NNG 사사건건/MAG -받아쓰기 받아쓰/VV + 기/ETN 받아쓰기/NNG -인간문화재 인간/NNG + 문화재/NNG 인간문화재/NNG - 전지훈련 _ + 전지/NNG + 훈련/NNG _ + 전지훈련/NNG -입후보자 입후/NNG + 보자/NNG 입후보자/NNG -부당이득 부당이득/NNG 부당/NNG + 이득/NNG -시키자는 시키/XSV + 자/ETM + 는/JX + _ 시키/XSV + 자는/ETM + _ -와 어미 와/JKB + _ + 어미/NNG 와/JC + _ + 어미/NNG -공주에서 공주/NNG + 에서/JKB 공주/NNP + 에서/JKB -을 지나치 을/JKO + _ + 지나/VV + 치/VA 을/JKO + _ + 지나치/VV -교육학과 교육학과/NNG 교육학/NNG + 과/NNG -동국세시기 동국/NNP + 세시기/NNG 동국세시기/NNP - 영화감독 _ + 영화/NNG + 감독/NNG _ + 영화감독/NNG -미술사가 미술/NNG + 사가/NNG 미술사가/NNG -공공건물 공공/NNG + 건물/NNG 공공건물/NNG -정순자, 정순/NNP + 자/NNG + ,/SP 정순자/NNP + ,/SP -화랭이패 화랭이패/NNG 화랭이/NNG + 패/NNG -삼성투신운용 삼성투신운/NNP + 용/NNG 삼성투신운용/NNP -고추잠자리 고추/NNG + 잠자리/NNG 고추잠자리/NNG -자동차회사 자동/NNG + 차회사/NNG 자동차/NNG + 회사/NNG -도이구지 도이구/NNP + 지/EC 도이구지/NNP -(정순자 (/SS + 정순/NNP + 자/NNG (/SS + 정순자/NNP -파운드화 파운드/NNG + 화/XSN 파운드화/NNG -카지노업소 카지/NNG + 노업소/NNG 카지노/NNG + 업소/NNG - 이목구비 _ + 이목/NNG + 구비/NNG _ + 이목구비/NNG -그러니만큼 그러/VV + 니만/EC + 큼/JKB 그러/VV + 니만큼/EC -야심만만 야/NNG + 심만만/XR 야심만만/XR -전력투구 전력/NNG + 투구/NNG 전력투구/NNG -외눈박이 외눈박/NNG + 이/JKS + _ 외눈박이/NNG + _ - 반신불수 _ + 반신/NNG + 불수/NNG _ + 반신불수/NNG -하루하루 | + 하/NNG + 루하루/MAG + _ | + 하루하루/MAG + _ - 인신공격 _ + 인신/NNG + 공격/NNG _ + 인신공격/NNG - 교통수단 _ + 교통/NNG + 수단/NNG _ + 교통수단/NNG - 시기상조 _ + 시기/NNG + 상조/NNG _ + 시기상조/NNG - 사립학교 _ + 사립/NNG + 학교/NNG _ + 사립학교/NNG -해창거리 해창거리/NNG 해창/NNP + 거리/NNG -돼지머리 돼지머리/NNG 돼지/NNG + 머리/NNG -한국철도차량 한국/NNP + 철/NNG + 도차량/NNG 한국철도차량/NNP -일 교도 일/NNG + _ + 교도/NNG 일/NNP + _ + 교도/NNP -이란 자기 이/VCP + 란/JX + _ + 자기/NP 이란/JX + _ + 자기/NP -가상학교 가상학교/NNG 가상/NNG + 학교/NNG -이해집단 이해집단/NNG 이해/NNG + 집단/NNG -약속이나 약속/NNG + 이/JC + 나/JX + _ 약속/NNG + 이나/JX + _ -지만 아직 지/EC + 만/MAJ + _ + 아직/MAG 지만/EC + _ + 아직/MAG - 프랜차이즈 _ + 프/NNG + 랜/NNP + 차이즈/NNG _ + 프랜차이즈/NNG -서해대교 서해대/NNP + 교/NNG 서해대교/NNP -코피 아난 코피/NNG + _ + 아난/NNP 코피/NNP + _ + 아난/NNP -동물학자 동물/NNG + 학자/NNG 동물학자/NNG -구 소련 구/NNG + _ + 소련/NNP 구/XPN + _ + 소련/NNP -개성사람 개/NNP + 성/NNG + 사람/NNG 개성/NNP + 사람/NNG -회중전등 회중/NNG + 전등/NNG 회중전등/NNG -일부일처 일부/NNG + 일처/NNG 일부일처/NNG - 오산학교 _ + 오산/NNP + 학교/NNG _ + 오산학교/NNP -음담패설 음담/NNG + 패설/NNG 음담패설/NNG -민자역사 민자/NNG + 역사/NNG 민자/NNP + 역사/NNG -성신여대 성/NNG + 신여대/NNP 성신여대/NNP -전문대학원 전문대학원/NNG 전문/NNG + 대학원/NNG - 사사건건 _ + 사사건/MAG + 건/NNG _ + 사사건건/MAG -아들녀석 아들/NNG + 녀/NNB + 석/NNG 아들/NNG + 녀석/NNB - 나무그늘 _ + 나무그늘/NNG _ + 나무/NNG + 그늘/NNG -다는구나 다/EC + 는구나/EF 다는구나/EF -더라니까요 더/EC + 라니까요/EF 더라니까요/EF -이른바 ` 이른바/MAG + _ + `/SS 이른바/MAJ + _ + `/SS -어불성설 어불/NNG + 성설/NNG 어불성설/NNG -유럽공동체 유럽공동/NNP + 체/NNG 유럽공동체/NNP -천생연분 천생/NNG + 연분/NNG 천생연분/NNG -와 부합 와/JC + _ + 부합/NNG 와/JKB + _ + 부합/NNG -외눈박이 외눈박/NNG + 이/JKS 외눈박이/NNG -금리인하 금리인하/NNG 금리/NNG + 인하/NNG -과천경마장 과천경/NNP + 마장/NNG 과천경마장/NNP -, 북, ,/SP + _ + 북/NNP + ,/SP ,/SP + _ + 북/NNG + ,/SP -화기애애 화/NNG + 기애애/XR 화기애애/XR + 큰소리치 _ + 큰/NNG + 소리치/VV _ + 큰소리치/VV +에게 유리하고 에게/JKB + _ + 유리/NNG + 하/XSV + 고/EC 에게/JKB + _ + 유리/XR + 하/XSA + 고/EC + 정보처장 _ + 정보/NNG + 처장/NNG _ + 정보처장/NNG +의 생활상 의/JKG + _ + 생활/NNG + 상/XSN 의/JKG + _ + 생활상/NNG + 조개껍질 _ + 조개/NNG + 껍질/NNG _ + 조개껍질/NNG +이 뜨거워졌 이/JKS + _ + 뜨거워/VV + 지/VX + 었/EP 이/JKS + _ + 뜨거워지/VV + 었/EP +운전기사 운전/NNG + 기사/NNG 운전기사/NNG +한국시각 한/NNG + 국/NNP + 시각/NNG 한국/NNP + 시각/NNG +의 대가 의/JKG + _ + 대/NNG + 가/JKS 의/JKG + _ + 대가/NNG +『금양잡록』 『/SS + 금/NNG + 양잡록/NNP + 』/SS 『/SS + 금양잡록/NNP + 』/SS +금양잡록』 금/NNG + 양잡록/NNP + 』/SS 금양잡록/NNP + 』/SS +을 벗겨내 을/JKO + _ + 벗겨/VV + 내/VX 을/JKO + _ + 벗기/VV + 어/EC + 내/VX +고 이원명 고/JKQ + _ + 이원명/NNP 고/EC + _ + 이원명/NNP + 하루종일 _ + 하루종일/NNG _ + 하루/NNG + 종일/NNG + 이겁니다. _ + 이/NNG + 것/NP + 이/VCP + ㅂ니다/EF + ./SF _ + 이것/NP + 이/VCP + ㅂ니다/EF + ./SF +이지마는 이/VCP + 지마/EC + 는/JX + _ 이/VCP + 지마는/EC + _ + 방울방울 _ + 방울/NNG + 방울/NNG + _ _ + 방울방울/NNG + _ +그 민 경위 그/MM + _ + 민/NNG + _ + 경위/NNG 그/MM + _ + 민/NNP + _ + 경위/NNG +임수경 양 임수경/NNP + _ + 양/NNG 임수경/NNP + _ + 양/NNB +아세안지역안보포럼 아세안/NNP + 지역안보포럼/NNG 아세안/NNP + 지역/NNG + 안보/NNG + 포럼/NNG +을 적은 을/JKO + _ + 적/VA + 은/ETM 을/JKO + _ + 적/VV + 은/ETM + 위험천만 _ + 위험천/NNG + 만/JX _ + 위험천만/NNG + 바람개비 _ + 바람/NNG + 개비/NNG _ + 바람개비/NNG +소련 동구 소련/NNP + _ + 동구/NNG 소련/NNP + _ + 동구/NNP +의 이해관계를 의/JKG + _ + 이해/NNG + 관계/NNG + 를/JKO 의/JKG + _ + 이해관계/NNG + 를/JKO + 이겁니다 _ + 이/NNG + 것/NP + 이/VCP + ㅂ니다/EF _ + 이것/NP + 이/VCP + ㅂ니다/EF + 가져다줄 _ + 가지/VV + 어다/EC + 주/VX + ㄹ/ETM + _ _ + 가져다주/VV + ㄹ/ETM + _ +신자유주의 신/XPN + 자/NNG + 유주의/NNG 신/XPN + 자유주의/NNG +지난 지금 지나/VV + ㄴ/ETM + _ + 지/NNG + 금/MAG 지나/VV + ㄴ/ETM + _ + 지금/MAG +일상생활에서 일상/NNG + 생활/NNG + 에서/JKB 일상생활/NNG + 에서/JKB + 특수학교 _ + 특수/NNG + 학교/NNG _ + 특수학교/NNG +의 사회상 의/JKG + _ + 사회/NNG + 상/XSN 의/JKG + _ + 사회상/NNG +권상무가 | + 권상무/NNG + 가/JKS | + 권/NNP + 상무/NNG + 가/JKS +가져다줄 가지/VV + 어다/EC + 주/VX + ㄹ/ETM 가져다주/VV + ㄹ/ETM +조개껍질 조개/NNG + 껍질/NNG 조개껍질/NNG +노이로제 노이/NNG + 로/NNP + 제/NNG 노이로제/NNG +창씨개명 창/NNP + 씨/NNB + 개명/NNG 창씨개명/NNG + 유유자적 _ + 유유자/NNG + 적/XSN _ + 유유자적/NNG +정부기구 정부기구/NNG 정부/NNG + 기구/NNG +소련 동구권 소련/NNP + _ + 동구/NNG + 권/XSN 소련/NNP + _ + 동구/NNP + 권/XSN +암중모색 암중/NNG + 모색/NNG 암중모색/NNG +소매치기 소매/NNG + 치/VV + 기/ETN 소매치기/NNG +만족할 수 만족/NNG + 하/XSV + ㄹ/ETM + _ + 수/NNB 만족/NNG + 하/XSA + ㄹ/ETM + _ + 수/NNB +을 가져다줄 을/JKO + _ + 가지/VV + 어다/EC + 주/VX + ㄹ/ETM 을/JKO + _ + 가져다주/VV + ㄹ/ETM +'이라고도 '/SS + 이/JKQ + 라/EC + 고/JKQ + 도/JX '/SS + 이라고/JKQ + 도/JX + 의미심장 _ + 의/NNG + 미심장/XR _ + 의미심장/XR +중증급성호흡기증후군 중증급/NNG + 성/XSN + 호흡기증후군/NNG 중증/NNG + 급성/NNG + 호흡기/NNG + 증후군/NNG +유치원생 유치원생/NNG 유치원/NNG + 생/XSN +현대건설 현대건/NNP + 설/NNG 현대건설/NNP +불기소처분 불기소처분/NNG 불/XPN + 기소/NNG + 처분/NNG +옛날얘기 옛날얘기/NNG 옛날/NNG + 얘기/NNG +브리티시 스틸 브리/NNG + 티시/NNP + _ + 스틸/NNG 브리티시/NNG + _ + 스틸/NNG +물론입니다 물/MAG + 론/NNG + 이/VCP + ㅂ니다/EF 물론/NNG + 이/VCP + ㅂ니다/EF +2002월드컵/ 2002/SN + 월/NNB + 드컵/NNG + //SP 2002/SN + 월드컵/NNG + //SP + 흘러내려 _ + 흘러내/VV + 려/EC _ + 흘러내리/VV + 어/EC +타이틀곡 타이틀곡/NNG + _ 타이틀/NNG + 곡/NNG + _ +공동체운동 공동/NNG + 체/NNG + 운동/NNG 공동체/NNG + 운동/NNG + 소매치기 _ + 소매치/NNG + 기/ETN _ + 소매치기/NNG +테크노픽션' 테크노픽션/NNG + '/SS 테크노/NNG + 픽션/NNG + '/SS + 국제통화기금 _ + 국제/NNG + 통화기금/NNG _ + 국제통화기금/NNP +[날개] [/SS + 날개/NNG + ]/SS [/SS + 날개/NNP + ]/SS + 옛날얘기 _ + 옛날얘기/NNG _ + 옛날/NNG + 얘기/NNG +을 거친 을/JKO + _ + 거/VA + 치/VV + ㄴ/ETM 을/JKO + _ + 거치/VV + ㄴ/ETM +최우수선수 최/XPN + 우/NNG + 수/NNG + 선수/NNG 최/XPN + 우수/NNG + 선수/NNG + 곡창지대 _ + 곡창지대/NNG _ + 곡창/NNG + 지대/NNG +디지털시대 디지털시대/NNG 디지털/NNG + 시대/NNG +아이러니 아이러/NNG + 니/MAG 아이러니/NNG +구 소련 구/NNP + _ + 소련/NNP 구/XPN + _ + 소련/NNP +피해 보상 피/VV + 해/NNG + _ + 보상/NNG 피해/NNG + _ + 보상/NNG +핵폐기장 핵폐기장/NNG 핵/NNG + 폐기장/NNG +의 노동조합 의/JKG + _ + 노동/NNG + 조합/NNG 의/JKG + _ + 노동조합/NNG + 들어올려 _ + 들어올리/VV + 어/EC _ + 들/VV + 어/EC + 올리/VV + 어/EC + 요령부득 _ + 요령/NNG + 부득/NNG _ + 요령부득/NNG +어나갔다 어/EC + 나/VV + 가/VX + 았/EP + 다/EF 어/EC + 나가/VV + 았/EP + 다/EF + 발로자에게 _ + 발로자/NNG + 에게/JKB _ + 발로자/NNP + 에게/JKB + 반사회적 _ + 반사회/NNG + 적/XSN _ + 반/XPN + 사회/NNG + 적/XSN +의 끝자락 의/JKG + _ + 끝자락/NNG 의/JKG + _ + 끝/NNG + 자락/NNG +그리하여, | + 그/MAJ + 리/MAG + 하여/MAJ + ,/SP | + 그리하여/MAJ + ,/SP +이사직을 이사직/NNG + 을/JKO 이사/NNG + 직/NNG + 을/JKO +『금양잡록 『/SS + 금/NNG + 양잡록/NNP 『/SS + 금양잡록/NNP +의 진수를 의/JKG + _ + 진/NNP + 수/NNG + 를/JKO 의/JKG + _ + 진수/NNG + 를/JKO + 삼삼오오 _ + 삼삼/NNG + 오오/NNP _ + 삼삼오오/NNG + 만주지방 _ + 만주지방/NNG _ + 만주/NNP + 지방/NNG + 가계금전신탁 _ + 가계금전/NNG + 신탁/NNG _ + 가계/NNG + 금전/NNG + 신탁/NNG +자나깨나 자나/NNG + 깨/JX + 나/JC 자/VV + 나/EC + 깨/VV + 나/EC + 방울방울 _ + 방울/NNG + 방울/NNG _ + 방울방울/NNG + 방송민주화 _ + 방송민주/NNG + 화/XSN _ + 방송/NNG + 민주/NNG + 화/XSN +까지밖에 까지밖에/JX 까지/JX + 밖에/JX +된 조국 되/XSV + ㄴ/ETM + _ + 조국/NNP 되/XSV + ㄴ/ETM + _ + 조국/NNG +제임스 박 제임스/NNP + _ + 박/NNG 제임스/NNP + _ + 박/NNP +세계선수권 세계/NNG + 선수/NNG + 권/XSN 세계/NNG + 선수권/NNG +유고슬라비아 유/NNG + 고슬라비아/NNP 유고슬라비아/NNP +대형 전광판 대형/NNG + _ + 전광판/NNG 대/NNG + 형/XSN + _ + 전광판/NNG +남북 고위급 남북/NNP + _ + 고위/NNG + 급/NNG 남북/NNP + _ + 고위급/NNG +국가보안법 국가보안법/NNG 국가/NNG + 보안법/NNG +민 경위 밀/VV + ㄴ/ETM + _ + 경위/NNG 민/NNP + _ + 경위/NNG +에게 유리하 에게/JKB + _ + 유리/NNG + 하/XSV 에게/JKB + _ + 유리/XR + 하/XSA +농수산물 농수/NNG + 산물/NNG 농수산물/NNG +는 군. 는/ETM + _ + 군/NNB + ./SF 는/ETM + _ + 군/NNG + ./SF +점심식사 점심식사/NNG 점심/NNG + 식사/NNG +배반포기 배반/NNG + 포기/NNG 배반포기/NNG +나인 우리 나/NP + 인/NNG + _ + 우리/NP 나/NP + 이/VCP + ㄴ/ETM + _ + 우리/NP + 사회정의 _ + 사회정의/NNG _ + 사회/NNG + 정의/NNG +것 말고 것/NNB + _ + 말/VX + 고/EC 것/NNB + _ + 말/VV + 고/EC +소문나 있다 소문나/VV + 아/EC + _ + 있/VV + 다/EF 소문나/VV + 아/EC + _ + 있/VX + 다/EF +이 불어왔 이/JKS + _ + 불어/VV + 오/VX + 았/EP 이/JKS + _ + 불어오/VV + 았/EP + 권상무가 _ + 권상무/NNG + 가/JKS _ + 권/NNP + 상무/NNG + 가/JKS +의 의복색 의/JKG + _ + 의복색/NNG 의/JKG + _ + 의복/NNG + 색/NNG +의 상관관계를 의/JKG + _ + 상관/NNG + 관계/NNG + 를/JKO 의/JKG + _ + 상관관계/NNG + 를/JKO + 기독교도 _ + 기독/NNG + 교도/NNG _ + 기독교도/NNG + 고정관념 _ + 고정/NNG + 관념/NNG _ + 고정관념/NNG +장군바위 장군바위/NNG 장군/NNG + 바위/NNG +의 자연환경 의/JKG + _ + 자연/NNG + 환경/NNG 의/JKG + _ + 자연환경/NNG +으로 걸었 으로/JKB + _ + 걸/VV + 었/EP 으로/JKB + _ + 걷/VV + 었/EP +사담 후세인 사담/NNG + _ + 후세인/NNP 사담/NNP + _ + 후세인/NNP +수로 부인 수로/NNG + _ + 부인/NNG 수로/NNP + _ + 부인/NNG +생선초밥 생선초밥/NNG 생선/NNG + 초밥/NNG +'테크노픽션' '/SS + 테크노픽션/NNG + '/SS '/SS + 테크노/NNG + 픽션/NNG + '/SS +기대 앉 기대/NNG + _ + 앉/VV 기대/VV + 어/EC + _ + 앉/VV +새로 산 새로/MAG + _ + 살/VV + ㄴ/ETM 새로/MAG + _ + 사/VV + ㄴ/ETM +이란 바로 이/JX + 란/ETM + _ + 바로/MAG 이란/JX + _ + 바로/MAG +을 내려주 을/JKO + _ + 내려주/VV 을/JKO + _ + 내리/VV + 어/EC + 주/VX +왁자지껄한 왁자지껄/MAG + 하/XSA + ㄴ/ETM 왁자지껄/MAG + 하/XSV + ㄴ/ETM +경제정의 경제정/NNG + 의/JKG 경제/NNG + 정의/NNG +자나깨나 자나/NNG + 깨/JX + 나/JC + _ 자/VV + 나/EC + 깨/VV + 나/EC + _ +황당 무계 황당/NNG + _ + 무계/NNG 황당/XR + _ + 무계/XR + 삼삼오오 _ + 삼삼/NNG + 오/NNP + 오/NNG _ + 삼삼오오/NNG +월드컵/ 월/NNB + 드컵/NNG + //SP 월드컵/NNG + //SP +후세인 정권 후세/NNG + 인/NNP + _ + 정권/NNG 후세인/NNP + _ + 정권/NNG +한 줄로 한/MM + _ + 줄/NNB + 로/JKB 한/MM + _ + 줄/NNG + 로/JKB +불안정성 불/XPN + 안/NNG + 정/NNG + 성/XSN 불/XPN + 안정/NNG + 성/XSN + 일상생활에서 _ + 일상/NNG + 생활/NNG + 에서/JKB _ + 일상생활/NNG + 에서/JKB + 술래잡기 _ + 술래/NNG + 잡/VV + 기/ETN _ + 술래잡기/NNG + 연석회의에서 _ + 연석/NNG + 회의/NNG + 에서/JKB _ + 연석회의/NNG + 에서/JKB +국제통화기금 국제/NNG + 통화기금/NNG 국제통화기금/NNP +곡창지대 곡창지대/NNG 곡창/NNG + 지대/NNG + 유치원생 _ + 유치원생/NNG _ + 유치원/NNG + 생/XSN +서비스산업 서비스산업/NNG 서비스/NNG + 산업/NNG +주제넘은 주제/NNG + 넘/VV + 은/ETM 주제넘/VA + 은/ETM +의 도는 의/JKG + _ + 돌/VV + 는/JX 의/JKG + _ + 도/NNG + 는/JX +시비거리 시비거리/NNG 시비/NNG + 거리/NNB +뮤추얼펀드 뮤추얼/NNG + 펀드/NNG 뮤추얼펀드/NNG +유유자적 유유자/NNG + 적/XSN 유유자적/NNG +강경대 군 강경대/NNP + _ + 군/NNG 강경대/NNP + _ + 군/NNB +미래지향 미래지향/NNG 미래/NNG + 지향/NNG +이 뜨거워졌다 이/JKS + _ + 뜨거워/VV + 지/VX + 었/EP + 다/EF 이/JKS + _ + 뜨거워지/VV + 었/EP + 다/EF +잠잘 때 잠/NNG + 자/VV + ㄹ/ETM + _ + 때/NNG 잠자/VV + ㄹ/ETM + _ + 때/NNG +브리티시 브리/NNG + 티시/NNP + _ 브리티시/NNG + _ + 하루종일 _ + 하루종일/NNG + _ _ + 하루/NNG + 종일/NNG + _ + 가죽가방 _ + 가죽가방/NNG _ + 가죽/NNG + 가방/NNG +사사건건 사사/NNG + 건건/MAG 사사건건/MAG + 방송민주 _ + 방송민주/NNG _ + 방송/NNG + 민주/NNG +얘기 들었 얘기/NNG + _ + 들/VV + 었/EP 얘기/NNG + _ + 듣/VV + 었/EP +경제정의실천 경제정/NNG + 의/JKG + 실천/NNG 경제/NNG + 정의/NNG + 실천/NNG + 부풀어오르 _ + 부풀어오르/VV _ + 부풀/VV + 어/EC + 오르/VV +나 실장 나/NNG + _ + 실장/NNG 나/NNP + _ + 실장/NNG +알 파치노 알/NNG + _ + 파치노/NNG 알/NNP + _ + 파치노/NNP +과 화려 과/JKB + _ + 화려/XR 과/JC + _ + 화려/XR +월 들어서는 월/NNB + _ + 들어/VV + 서/EC + 는/JX 월/NNB + _ + 들/VV + 어서/EC + 는/JX +남목고개 남목/NNG + 고개/NNG 남목/NNP + 고개/NNG + 명예퇴직 _ + 명예/NNG + 퇴직/NNG _ + 명예퇴직/NNG +, 아니 ,/SP + _ + 아/MAG + 니/IC ,/SP + _ + 아니/IC +▴ 이 = ▴/SW + _ + 이/MM + _ + =/SW ▴/SW + _ + 이/NNP + _ + =/SW +소주잔을 소주/NNG + 잔/NNG + 을/JKO 소주잔/NNG + 을/JKO + 대중가요 _ + 대중/NNG + 가요/NNG _ + 대중가요/NNG +공정보도 공정보도/NNG 공정/NNG + 보도/NNG +소문나 있 소문나/VV + 아/EC + _ + 있/VV 소문나/VV + 아/EC + _ + 있/VX +예측 불허 예측/NNG + _ + 불/XPN + 허/NNG 예측/NNG + _ + 불허/NNG +이겁니다. 이/NNG + 것/NP + 이/VCP + ㅂ니다/EF + ./SF 이것/NP + 이/VCP + ㅂ니다/EF + ./SF +그리하여, 그/MAJ + 리/MAG + 하여/MAJ + ,/SP 그리하여/MAJ + ,/SP +자동차사업 자동차사업/NNG 자동차/NNG + 사업/NNG + 사설시조 _ + 사설/NNG + 시조/NNG _ + 사설시조/NNG +·가명) ·/SP + 가/NNG + 명/NNP + )/SS ·/SP + 가명/NNG + )/SS + 대북사업 _ + 대북사업/NNG _ + 대북/NNG + 사업/NNG +의 발로 의/JKG + _ + 발/NNG + 로/NNP 의/JKG + _ + 발로/NNG + 신춘문예 _ + 신춘/NNG + 문예/NNG _ + 신춘문예/NNG +술래잡기 술래/NNG + 잡/VV + 기/ETN 술래잡기/NNG +천만다행 천/NR + 만다행/NNG 천만다행/NNG +을 한 단계 을/JKO + _ + 하/VV + ㄴ/ETM + _ + 단계/NNG 을/JKO + _ + 한/MM + _ + 단계/NNG +치 못하 하/XSA + 지/EC + _ + 못/MAG + 하/VX 하/XSA + 지/EC + _ + 못하/VX +풍비박산 풍비/NNG + 박산/NNG 풍비박산/NNG +부풀어오르 부풀어오르/VV 부풀/VV + 어/EC + 오르/VV +흘러내려 흘러내/VV + 려/EC 흘러내리/VV + 어/EC +법계도> 법/NNG + 계도/NNP + >/SS 법계도/NNP + >/SS +비닐가방 비닐가방/NNG 비닐/NNG + 가방/NNG +과 만나 과/JC + _ + 만나/VV + 아/EC 과/JKB + _ + 만나/VV + 아/EC +기업개선작업) 기업/NNG + 개선작업/NNG + )/SS 기업/NNG + 개선/NNG + 작업/NNG + )/SS +아 보기 아/EC + _ + 보/VV + 기/ETN 아/EC + _ + 보/VX + 기/ETN +2회전에서 2/SN + 회/NNB + 전/NNG + 에서/JKB 2/SN + 회전/NNB + 에서/JKB +지난 지금, 지나/VV + ㄴ/ETM + _ + 지/NNG + 금/MAG + ,/SP 지나/VV + ㄴ/ETM + _ + 지금/MAG + ,/SP + 안기부장 _ + 안기/NNG + 부장/NNG _ + 안기부장/NNG +에게 물려주 에게/JKB + _ + 물려/VV + 주/VX 에게/JKB + _ + 물려주/VV +지역안보포럼 지역안보포럼/NNG 지역/NNG + 안보/NNG + 포럼/NNG + 대우전에서 _ + 대우전/NNP + 에서/JKB _ + 대우전/NNG + 에서/JKB +민 경위 민/NNG + _ + 경위/NNG 민/NNP + _ + 경위/NNG +묵묵부답 묵묵/NNG + 부답/NNG 묵묵부답/NNG + 상관없이 _ + 상/MAG + 관/NNG + 없이/MAG + _ _ + 상관없이/MAG + _ +제네바협정 제네바협/NNP + 정/NNG 제네바/NNP + 협정/NNG +의 경지 의/JKG + _ + 경/NNG + 지/NNP 의/JKG + _ + 경지/NNG + 구경거리였 _ + 구경/NNG + 거리/NNG + 이/VCP + 었/EP _ + 구경거리/NNG + 이/VCP + 었/EP +전국민적 전국민/NNG + 적/XSN 전/MM + 국민/NNG + 적/XSN +나 실장 | + 나/NP + _ + 실장/NNG | + 나/NNP + _ + 실장/NNG +구경거리였 구경/NNG + 거리/NNG + 이/VCP + 었/EP 구경거리/NNG + 이/VCP + 었/EP +이지마는 이/VCP + 지마/EC + 는/JX 이/VCP + 지마는/EC +실업학교 실업/NNG + 학교/NNG 실업학교/NNG +동위원소 동위원소/NNG 동위/NNG + 원소/NNG +하루종일 하루종일/NNG + _ 하루/NNG + 종일/NNG + _ +하청업체 하청업체/NNG 하청/NNG + 업체/NNG +의 일본관 의/JKG + _ + 일본/NNP + 관/NNG 의/JKG + _ + 일본관/NNG +(총상금 (/SS + 총상금/NNG + _ (/SS + 총/MM + 상금/NNG + _ +반리얼리즘 반리얼리즘/NNG 반/XPN + 리얼리즘/NNG + 미래지향적 _ + 미래지향/NNG + 적/XSN _ + 미래/NNG + 지향/NNG + 적/XSN + 주워들었 _ + 줍/VV + 어/EC + 들/VV + 었/EP _ + 주워듣/VV + 었/EP +가잘 씨 가잘/NNP + _ + 씨/NNG 가잘/NNP + _ + 씨/NNB +발로자에게 발로자/NNG + 에게/JKB 발로자/NNP + 에게/JKB + 의미심장하 _ + 의/NNG + 미심장/XR + 하/XSA _ + 의미심장/XR + 하/XSA +먹을거리 먹/VV + 을거리/NNG 먹을거리/NNG +이나 좀 이/JC + 나/JX + _ + 좀/MAG 이나/JX + _ + 좀/MAG +지하도시 지하도시/NNG 지하/NNG + 도시/NNG + 대성공이 _ + 대성공/NNG + 이/VCP _ + 대/XPN + 성공/NNG + 이/VCP +아니요, 아니/IC + 요/EC + ,/SP 아니요/IC + ,/SP + 반리얼리즘 _ + 반리얼리즘/NNG _ + 반/XPN + 리얼리즘/NNG +기술씨름 기술씨름/NNG 기술/NNG + 씨름/NNG +의 신진대사 의/JKG + _ + 신진/NNG + 대사/NNG 의/JKG + _ + 신진대사/NNG +오늘밤은 오늘밤/NNG + 은/JX 오늘/NNG + 밤/NNG + 은/JX +비인간화 비/XPN + 인/NNG + 간/NNG + 화/XSN 비/XPN + 인간/NNG + 화/XSN +2002월드컵 2002/SN + 월/NNB + 드컵/NNG 2002/SN + 월드컵/NNG +그러면, 그/VV + 러면/MAJ + ,/SP 그러면/MAJ + ,/SP +레이디경향 레/NNP + 이/NNG + 디/NNP + 경향/NNG 레이디경향/NNP +반사회적 반사회/NNG + 적/XSN 반/XPN + 사회/NNG + 적/XSN +서 있을 서/VV + 어/EC + _ + 있/VV + 을/ETM 서/VV + 어/EC + _ + 있/VX + 을/ETM +제임스 박과 제임스/NNP + _ + 박/NNG + 과/JC 제임스/NNP + _ + 박/NNP + 과/JC +우리교육 우리/NP + 교육/NNG 우리교육/NNP +매 순간 매/NNG + _ + 순간/NNG 매/MM + _ + 순간/NNG +무동답교놀이 무동답교놀이/NNG 무동/NNG + 답교놀이/NNG + 반민중적 _ + 반민중/NNG + 적/XSN _ + 반/XPN + 민중/NNG + 적/XSN +기업개선작업 기업/NNG + 개선작업/NNG 기업/NNG + 개선/NNG + 작업/NNG +완전식민지 완전식민지/NNG 완전/NNG + 식민지/NNG + 만화가게 _ + 만화가게/NNG _ + 만화/NNG + 가게/NNG +협궤열차 협궤열차/NNG 협궤/NNG + 열차/NNG +국제통화기금( 국제/NNG + 통화기금/NNG + (/SS 국제통화기금/NNP + (/SS +보내 달 보내/VV + 어/EC + _ + 달/VV 보내/VV + 어/EC + _ + 달/VX + 보릿고개 _ + 보릿/NNG + 고개/NNG _ + 보릿고개/NNG +비정부기구 비/XPN + 정부기구/NNG 비/XPN + 정부/NNG + 기구/NNG +사회정의 사회정의/NNG 사회/NNG + 정의/NNG +을 가져다주 을/JKO + _ + 가져다/VV + 주/VX 을/JKO + _ + 가져다주/VV +한편 1997 한/MAG + 편/NNG + _ + 1997/SN 한편/NNG + _ + 1997/SN +파이프오르간 파이프오르간/NNG 파이프/NNG + 오르간/NNG +빙고게임 빙고게임/NNG 빙고/NNG + 게임/NNG +신보수주의 신보수주의/NNG 신/XPN + 보수주의/NNG + 미래지향 _ + 미래지향/NNG _ + 미래/NNG + 지향/NNG +보릿고개 보릿/NNG + 고개/NNG 보릿고개/NNG +낼 아침 내/VV + ㄹ/ETM + _ + 아침/NNG 낼/NNG + _ + 아침/NNG + 들추어내 _ + 들추/VV + 어/EC + 내/VV _ + 들추어내/VV +코리안 리그 코/NNG + 리/NNP + 안/NNG + _ + 리그/NNG 코리안/NNG + _ + 리그/NNG +기 탤런트 기/NNB + _ + 탤런트/NNG 기/NNG + _ + 탤런트/NNG +한편 1997 | + 한/MAG + 편/NNG + _ + 1997/SN | + 한편/NNG + _ + 1997/SN +복합오염 복합/NNG + 오염/NNG 복합오염/NNG +의 부주의 의/JKG + _ + 부주의/NNG 의/JKG + _ + 부/XPN + 주의/NNG +삼삼오오 삼삼/NNG + 오오/NNP + _ 삼삼오오/NNG + _ +그 승신목 그/MM + _ + 승신목/NNG 그/MM + _ + 승신목/NNP +과소평가 과소/NNG + 평가/NNG 과소평가/NNG +편집기자 편집기자/NNG 편집/NNG + 기자/NNG +이올시다 이/VCP + 오/VV + ㄹ시다/EF 이/VCP + 올시다/EF +오래 전 오래/MAG + _ + 전/MM 오래/MAG + _ + 전/NNG +삼삼오오 삼삼/NNG + 오/NNP + 오/NNG 삼삼오오/NNG +사사건건 사사/NNG + 건건/MAG + _ 사사건건/MAG + _ +방울방울 방울/NNG + 방울/NNG 방울방울/NNG +찬 공기 차/VV + ㄴ/ETM + _ + 공기/NNG 차/VA + ㄴ/ETM + _ + 공기/NNG + 비닐가방 _ + 비닐가방/NNG _ + 비닐/NNG + 가방/NNG + 대통령제 _ + 대통령제/NNG _ + 대통령/NNG + 제/XSN +소매치기 소매치/NNG + 기/ETN 소매치기/NNG +들추어내 들추/VV + 어/EC + 내/VV 들추어내/VV +왁자지껄한 왁자지껄/MAG + 하/XSA + ㄴ/ETM + _ 왁자지껄/MAG + 하/XSV + ㄴ/ETM + _ + 가져다줄 _ + 가지/VV + 어다/EC + 주/VX + ㄹ/ETM _ + 가져다주/VV + ㄹ/ETM +현대 물리학 현/NNP + 대/NNG + _ + 물리학/NNG 현대/NNG + _ + 물리학/NNG + 공립학교 _ + 공립/NNG + 학교/NNG _ + 공립학교/NNG +, 부정부패 ,/SP + _ + 부정/NNG + 부패/NNG ,/SP + _ + 부정부패/NNG + 소주잔을 _ + 소주/NNG + 잔/NNG + 을/JKO _ + 소주잔/NNG + 을/JKO +후생복지 후생복지/NNG 후생/NNG + 복지/NNG +의 절대 의/JKG + _ + 절/MAG + 대/NNG 의/JKG + _ + 절대/NNG +<법계도> /SS /SS +연석회의에서 연석/NNG + 회의/NNG + 에서/JKB 연석회의/NNG + 에서/JKB +타이틀곡 ' 타이틀곡/NNG + _ + '/SS 타이틀/NNG + 곡/NNG + _ + '/SS + 하청업체 _ + 하청업체/NNG _ + 하청/NNG + 업체/NNG +자산운용 자산운용/NNG 자산/NNG + 운용/NNG +그러면, | + 그/VV + 러면/MAJ + ,/SP | + 그러면/MAJ + ,/SP +이나 있 이나/JX + _ + 있/VX 이나/JX + _ + 있/VV + 옛사람들 _ + 옛/MM + 사람/NNG + 들/XSN _ + 옛사람/NNG + 들/XSN + 동성연애자 _ + 동성/NNG + 연애자/NNG _ + 동성연애자/NNG +리바운드) 리바운/NNG + 드/NNP + )/SS 리바운드/NNG + )/SS +고 방안으로 고/EC + _ + 방안/NNG + 으로/JKB 고/EC + _ + 방/NNG + 안/NNG + 으로/JKB + 배반포기 _ + 배반/NNG + 포기/NNG _ + 배반포기/NNG + 마을주민 _ + 마을주민/NNG _ + 마을/NNG + 주민/NNG +사회 내 사회/NNG + _ + 나/NP + 의/JKG 사회/NNG + _ + 내/NNB +기사회생 기사/NNG + 회생/NNG 기사회생/NNG +신춘문예 신춘/NNG + 문예/NNG 신춘문예/NNG +안기부장 안기/NNG + 부장/NNG 안기부장/NNG +개편안을 개편안/NNG + 을/JKO 개편/NNG + 안/NNG + 을/JKO +크리스마스 크리/NNG + 스/NNP + 마스/NNG 크리스마스/NNG +너랑 나 너/NP + 랑/JKB + _ + 나/NP 너/NP + 랑/JC + _ + 나/NP +한 송이 한/MM + _ + 송/NNG + 이/JKS 한/MM + _ + 송이/NNG +가상공간 가상/NNG + 공간/NNG 가상공간/NNG + 남북대화 _ + 남/NNG + 북/NNP + 대화/NNG _ + 남북/NNP + 대화/NNG +향벽설위 향벽/NNG + 설위/NNG 향벽설위/NNG +미스 민을 미스/NNG + _ + 민/NNG + 을/JKO 미스/NNG + _ + 민/NNP + 을/JKO +전 부장 전/MM + _ + 부장/NNG 전/NNP + _ + 부장/NNG + 부스러기 _ + 부스러/NNG + 기/ETN _ + 부스러기/NNG +위험천만 위험천/NNG + 만/JX 위험천만/NNG +그녀와 나 그녀/NP + 와/JKB + _ + 나/NP 그녀/NP + 와/JC + _ + 나/NP +"낼 아침 "/SS + 내/VV + ㄹ/ETM + _ + 아침/NNG "/SS + 낼/NNG + _ + 아침/NNG + 주막거리 _ + 주막거/NNG + 리/NNB _ + 주막거리/NNG + 암중모색 _ + 암중/NNG + 모색/NNG _ + 암중모색/NNG +남존여비 남존/NNG + 여비/NNG 남존여비/NNG +기와 지붕 기/NNG + 와/JC + _ + 지붕/NNG 기와/NNG + _ + 지붕/NNG +는가 등 는가/EC + _ + 등/NNG 는가/EC + _ + 등/NNB +매일경제신문사 매일경제신/NNP + 문사/NNG 매일경제신문사/NNP +리바운드 리바운/NNG + 드/NNP 리바운드/NNG + 전국민적 _ + 전국민/NNG + 적/XSN _ + 전/MM + 국민/NNG + 적/XSN +우찌무라 간조 우찌무라/NNG + _ + 간조/NNG 우찌무라/NNP + _ + 간조/NNP +가 방안 가/JKS + _ + 방안/NNG 가/JKS + _ + 방/NNG + 안/NNG +큰소리치 큰/NNG + 소리치/VV 큰소리치/VV +평가절하 평가절하/NNG 평가/NNG + 절하/NNG +단도직입 단도/NNG + 직입/NNG 단도직입/NNG +사사건건 사/MAG + 사/NNG + 건건/MAG + _ 사사건건/MAG + _ +사담 후세인 사/NNG + 담/NNP + _ + 후세인/NNP 사담/NNP + _ + 후세인/NNP +공동주최 공동주최/NNG 공동/NNG + 주최/NNG +연해주에 연해/NNP + 주/NNG + 에/JKB 연해주/NNP + 에/JKB +<법계도 ( bsearch(&chr, _keys, _vals.size(), sizeof(wchar_t), Embed::_key_cmp)); - int idx = 0; + int idx = 1; // unknown character index is 1 if (found != nullptr) idx = found - _keys; #ifndef NDEBUG wchar_t wstr[2] = {chr, 0}; @@ -67,22 +67,22 @@ const embedding_t& Embed::operator[](wchar_t chr) const { const embedding_t& Embed::left_word_bound() const { - return _vals.at(1); + return _vals.at(2); } const embedding_t& Embed::right_word_bound() const { - return _vals.at(2); + return _vals.at(3); } const embedding_t& Embed::left_padding() const { - return _vals.at(3); + return _vals.at(0); // padding index is 0 which is zero vector } const embedding_t& Embed::right_padding() const { - return _vals.at(4); + return _vals.at(0); // padding index is 0 which is zero vector } diff --git a/src/main/cpp/khaiii/ErrPatch.cpp b/src/main/cpp/khaiii/ErrPatch.cpp index 955b128..899114f 100644 --- a/src/main/cpp/khaiii/ErrPatch.cpp +++ b/src/main/cpp/khaiii/ErrPatch.cpp @@ -11,6 +11,7 @@ // includes // ////////////// #include +#include #include #include "khaiii/KhaiiiApi.hpp" diff --git a/src/main/cpp/khaiii/ErrPatch.hpp b/src/main/cpp/khaiii/ErrPatch.hpp index b0690f0..00a53df 100644 --- a/src/main/cpp/khaiii/ErrPatch.hpp +++ b/src/main/cpp/khaiii/ErrPatch.hpp @@ -11,6 +11,7 @@ ////////////// // includes // ////////////// +#include #include #include diff --git a/src/main/cpp/khaiii/KhaiiiImpl.hpp b/src/main/cpp/khaiii/KhaiiiImpl.hpp index 3a627c3..6dc539b 100644 --- a/src/main/cpp/khaiii/KhaiiiImpl.hpp +++ b/src/main/cpp/khaiii/KhaiiiImpl.hpp @@ -13,6 +13,7 @@ ////////////// #include #include +#include #include // NOLINT #include #include diff --git a/src/main/cpp/khaiii/Morph.cpp b/src/main/cpp/khaiii/Morph.cpp index 53c8be9..5a97e6a 100644 --- a/src/main/cpp/khaiii/Morph.cpp +++ b/src/main/cpp/khaiii/Morph.cpp @@ -55,8 +55,8 @@ Morph::Morph(wstring wlex, pos_tag_t tag, const wchar_t* wbegin, int wlength) // methods // ///////////// const char* Morph::pos_str(pos_tag_t num) { - assert(num < POS_TAG_SIZE); - return _TAG_SET[num]; + assert(0 < num && num <= POS_TAG_SIZE); + return _TAG_SET[num-1]; } void Morph::organize(const wstring& wraw, const vector& wbegins, const vector& wends) { diff --git a/src/main/cpp/khaiii/Preanal.cpp b/src/main/cpp/khaiii/Preanal.cpp index c592cd8..5f30581 100644 --- a/src/main/cpp/khaiii/Preanal.cpp +++ b/src/main/cpp/khaiii/Preanal.cpp @@ -11,6 +11,7 @@ // includes // ////////////// #include +#include #include "khaiii/KhaiiiApi.hpp" #include "khaiii/Word.hpp" diff --git a/src/main/cpp/khaiii/Preanal.hpp b/src/main/cpp/khaiii/Preanal.hpp index 35c7992..3c5bdf6 100644 --- a/src/main/cpp/khaiii/Preanal.hpp +++ b/src/main/cpp/khaiii/Preanal.hpp @@ -11,6 +11,7 @@ ////////////// // includes // ////////////// +#include #include #include "spdlog/spdlog.h" diff --git a/src/main/cpp/khaiii/Resource.cpp b/src/main/cpp/khaiii/Resource.cpp index 45a2c2f..07b2dc0 100644 --- a/src/main/cpp/khaiii/Resource.cpp +++ b/src/main/cpp/khaiii/Resource.cpp @@ -11,6 +11,7 @@ // includes // ////////////// #include +#include #include "khaiii/Config.hpp" #include "khaiii/KhaiiiApi.hpp" diff --git a/src/main/cpp/khaiii/Resource.hpp b/src/main/cpp/khaiii/Resource.hpp index 872a9d2..4ce3632 100644 --- a/src/main/cpp/khaiii/Resource.hpp +++ b/src/main/cpp/khaiii/Resource.hpp @@ -11,6 +11,7 @@ ////////////// // includes // ////////////// +#include #include #include "spdlog/spdlog.h" diff --git a/src/main/cpp/khaiii/Restore.cpp b/src/main/cpp/khaiii/Restore.cpp index aae9395..3412670 100644 --- a/src/main/cpp/khaiii/Restore.cpp +++ b/src/main/cpp/khaiii/Restore.cpp @@ -81,7 +81,7 @@ void Restore::open(string dir) { _one_mmf.open(dir + "/restore.one"); #ifndef NDEBUG for (int i = 0; i < _one_mmf.size(); ++i) { - _log->trace("{}: {}, ", i, _one_mmf.data()[i]); + SPDLOG_TRACE(_log, "{}: {}, ", i, _one_mmf.data()[i]); } #endif _log->info("restore dictionary opened"); diff --git a/src/main/cpp/khaiii/Restore.hpp b/src/main/cpp/khaiii/Restore.hpp index 0ade9f1..918f00c 100644 --- a/src/main/cpp/khaiii/Restore.hpp +++ b/src/main/cpp/khaiii/Restore.hpp @@ -11,6 +11,7 @@ ////////////// // includes // ////////////// +#include #include #include @@ -34,13 +35,12 @@ struct chr_tag_t { BI bi; ///< B-, I- notation inline void set_tag(uint16_t tag_out) { - assert(tag_out <= 2 * POS_TAG_SIZE); - tag = tag_out - 1; - if (tag >= POS_TAG_SIZE) { + assert(0 < tag_out && tag_out <= 2 * POS_TAG_SIZE); + tag = tag_out; + if (tag > POS_TAG_SIZE) { tag -= POS_TAG_SIZE; bi = I; } - tag += 1; } inline void from_val(uint32_t val) { diff --git a/src/main/cpp/khaiii/Sentence.cpp b/src/main/cpp/khaiii/Sentence.cpp index 26a158c..196e809 100644 --- a/src/main/cpp/khaiii/Sentence.cpp +++ b/src/main/cpp/khaiii/Sentence.cpp @@ -71,6 +71,18 @@ void Sentence::organize() { } +int Sentence::get_lwb_delta(int wrd_idx, int chr_idx) { + assert(0 <= chr_idx && chr_idx < words[wrd_idx]->wlength); + return -chr_idx; +} + + +int Sentence::get_rwb_delta(int wrd_idx, int chr_idx) { + assert(0 <= chr_idx && chr_idx < words[wrd_idx]->wlength); + return words[wrd_idx]->wlength - chr_idx - 1; +} + + void Sentence::_tokenize() { bool is_in_space = true; for (int idx = 0; idx < _wraw.size(); ++idx) { diff --git a/src/main/cpp/khaiii/Sentence.hpp b/src/main/cpp/khaiii/Sentence.hpp index 04d531b..96960d7 100644 --- a/src/main/cpp/khaiii/Sentence.hpp +++ b/src/main/cpp/khaiii/Sentence.hpp @@ -47,6 +47,22 @@ class Sentence { return _raw; } + /** + * get delta from left word boundary to this character + * @param wrd_idx word index + * @param chr_idx character index + * @return delta (always less or equal to 0) + */ + int get_lwb_delta(int wrd_idx, int chr_idx); + + /** + * get delta from right word boundary to this character + * @param wrd_idx word index + * @param chr_idx character index + * @return delta (always more or equal to 0) + */ + int get_rwb_delta(int wrd_idx, int chr_idx); + private: static std::shared_ptr _log; ///< logger diff --git a/src/main/cpp/khaiii/Tagger.cpp b/src/main/cpp/khaiii/Tagger.cpp index 2712969..3f74e3f 100644 --- a/src/main/cpp/khaiii/Tagger.cpp +++ b/src/main/cpp/khaiii/Tagger.cpp @@ -15,7 +15,6 @@ #include #include - #include "khaiii/Config.hpp" #include "khaiii/Embed.hpp" #include "khaiii/Sentence.hpp" @@ -75,7 +74,16 @@ void Tagger::tag() { copy(batch[i][j]->data(), batch[i][j]->data() + _cfg.embed_dim, &data[i * col_dim + j * _cfg.embed_dim]); } + _add_lwb_rwb(&data[i * col_dim], index[i].first, index[i].second); nn::add_positional_enc(&data[i * col_dim], _cfg.window * 2 + 1, _cfg.embed_dim); +#ifndef NDEBUG + for (int j = 0; j < batch[i].size(); ++j) { + SPDLOG_TRACE(_log, "batch[{}][{}]", i, j); + for (int k = 0; k < _cfg.embed_dim; ++k) { + SPDLOG_TRACE(_log, "\t{}: {}", k, data[i * col_dim + j * _cfg.embed_dim + k]); + } + } +#endif } _tag_cnn(data.data(), batch.size(), col_dim, index); @@ -86,18 +94,33 @@ void Tagger::tag() { } +void Tagger::_add_lwb_rwb(float* data, int wrd_idx, int chr_idx) { + int context_len = _cfg.window * 2 + 1; + int lwb_idx = context_len / 2 + _sent->get_lwb_delta(wrd_idx, chr_idx); + if (lwb_idx >= 0) { + nn::add_vec(&data[lwb_idx * _cfg.embed_dim], _rsc.embed.left_word_bound().data(), + _cfg.embed_dim); + } + int rwb_idx = context_len / 2 + _sent->get_rwb_delta(wrd_idx, chr_idx); + if (rwb_idx < context_len) { + nn::add_vec(&data[rwb_idx * _cfg.embed_dim], _rsc.embed.right_word_bound().data(), + _cfg.embed_dim); + } +} + + void Tagger::_tag_cnn(float* data, int batch_size, int col_dim, const vector>& index) { - nn::matrix_t conv_outs(batch_size, 4 * _cfg.embed_dim); - conv_outs << _rsc.convs[2].forward_max_pool_mat(data, batch_size, col_dim), + nn::matrix_t features(batch_size, 4 * _cfg.embed_dim); + features << _rsc.convs[2].forward_max_pool_mat(data, batch_size, col_dim), _rsc.convs[3].forward_max_pool_mat(data, batch_size, col_dim), _rsc.convs[4].forward_max_pool_mat(data, batch_size, col_dim), _rsc.convs[5].forward_max_pool_mat(data, batch_size, col_dim); - auto hidden_outs = _rsc.cnv2hdn.forward_mat(conv_outs); - auto tag_outs = _rsc.hdn2tag.forward_mat(hidden_outs); + auto hidden_outs = _rsc.cnv2hdn.forward_mat(features); + auto logits = _rsc.hdn2tag.forward_mat(hidden_outs); for (int i = 0; i < batch_size; ++i) { nn::vector_t::Index max_idx; - tag_outs.row(i).maxCoeff(&max_idx); + logits.row(i).maxCoeff(&max_idx); int wrd_idx = index[i].first; int chr_idx = index[i].second; _sent->words[wrd_idx]->char_tags[chr_idx] = max_idx + 1; @@ -124,7 +147,7 @@ void Tagger::_revise_tags() { } if (0 < curr_tag && curr_tag <= POS_TAG_SIZE) { // B- 태그이면서 이전 카테고리와 다른 경우 I- 태그로 보정해준다. - if (prev_tag != (curr_tag + POS_TAG_SIZE)) { + if (j == 0 || !_is_same_tag_cat(word->wbegin[j-1], prev_tag, curr_tag)) { curr_tag += POS_TAG_SIZE; _log->debug("B->I tag: {} -> {}", word->char_tags[j], curr_tag); word->char_tags[j] = curr_tag; @@ -136,6 +159,20 @@ void Tagger::_revise_tags() { } +bool Tagger::_is_same_tag_cat(wchar_t prev_chr, int prev_tag, int curr_tag) { + assert(0 < curr_tag && curr_tag <= POS_TAG_SIZE); + if (prev_tag == 0) return false; // 맨 첫번째 음절인 경우 항상 false + if (0 < prev_tag && prev_tag <= 2 * POS_TAG_SIZE) { + // 이전 태그가 단순 태그일 경우 + return (prev_tag-1) % POS_TAG_SIZE == (curr_tag-1) % POS_TAG_SIZE; + } + // 이전 태그가 복합 태그일 경우 원형복원 후 마지막 음절의 태그로 판단한다. + auto restored = _rsc.restore.restore(prev_chr, prev_tag, true); + int prev_last_tag = restored[restored.size()-1].tag; + return (prev_last_tag-1) % POS_TAG_SIZE == (curr_tag-1) % POS_TAG_SIZE; +} + + void Tagger::_restore() { for (int i = 0; i < _sent->words.size(); ++i) { auto word = _sent->words[i]; @@ -174,9 +211,6 @@ vector Tagger::_get_left_context(int wrd_idx, int chr_idx) { for (int c = chr_idx - 1; c >= 0 && left_context.size() < _cfg.window; --c) { left_context.emplace_back(&(word->embeds.at(c))); } - if (left_context.size() < _cfg.window) { // left word mark - left_context.emplace_back(&_rsc.embed.left_word_bound()); - } // from left word for (int w = wrd_idx - 1; w >= 0 && left_context.size() < _cfg.window; --w) { word = _sent->words[w]; @@ -201,9 +235,6 @@ vector Tagger::_get_right_context(int wrd_idx, int chr_idx) for (int c = chr_idx + 1; c < word->wlength && right_context.size() < _cfg.window; ++c) { right_context.emplace_back(&(word->embeds.at(c))); } - if (right_context.size() < _cfg.window) { // right word mark - right_context.emplace_back(&_rsc.embed.right_word_bound()); - } // from right word for (int w = wrd_idx + 1; w < _sent->words.size() && right_context.size() < _cfg.window; ++w) { word = _sent->words[w]; diff --git a/src/main/cpp/khaiii/Tagger.hpp b/src/main/cpp/khaiii/Tagger.hpp index 8b98bf9..8c0bdca 100644 --- a/src/main/cpp/khaiii/Tagger.hpp +++ b/src/main/cpp/khaiii/Tagger.hpp @@ -11,6 +11,7 @@ ////////////// // includes // ////////////// +#include #include #include @@ -46,14 +47,13 @@ class Tagger { const Resource& _rsc; ///< resource std::shared_ptr _sent; ///< Sentence object - /** - * tag characters with FNN method - * @param data data start point - * @param batch_size batch size - * @param col_dim column dimension for each batch - */ - void _tag_fnn(float* data, int batch_size, int col_dim, - const std::vector>& index); + /** + * add left/right word boundary embedding to batch + * @param data data start point + * @param wrd_idx word index + * @param chr_idx character index + */ + void _add_lwb_rwb(float* data, int wrd_idx, int chr_idx); /** * tag characters with CNN method @@ -71,6 +71,17 @@ class Tagger { */ void _revise_tags(); + /** + * 이전 태그와 현재 태그가 B-, I- 만 다르고 같은 카테고리인지 여부. + * 이전 태그가 복합 태그일 경우 마지막 태그와 비교한다. + * 현재 태그는 단순 태그이며 B- 태그인 경우에 한해 동작한다. + * @param prev_chr 이전 음절 + * @param prev_tag 이전 태그 + * @param curr 현재 태그 + * @return 태그 카테고리가 동일한지 여부 + */ + bool _is_same_tag_cat(wchar_t prev_chr, int prev_tag, int curr); + void _restore(); ///< restore morphemes /** diff --git a/src/main/cpp/khaiii/Trie.cpp b/src/main/cpp/khaiii/Trie.cpp index ff5327a..4344759 100644 --- a/src/main/cpp/khaiii/Trie.cpp +++ b/src/main/cpp/khaiii/Trie.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "boost/lexical_cast.hpp" diff --git a/src/main/cpp/khaiii/Trie.hpp b/src/main/cpp/khaiii/Trie.hpp index 1599017..45db902 100644 --- a/src/main/cpp/khaiii/Trie.hpp +++ b/src/main/cpp/khaiii/Trie.hpp @@ -13,6 +13,7 @@ ////////////// #include #include +#include #include #include diff --git a/src/main/cpp/khaiii/Word.cpp b/src/main/cpp/khaiii/Word.cpp index b3c289c..1784f9b 100644 --- a/src/main/cpp/khaiii/Word.cpp +++ b/src/main/cpp/khaiii/Word.cpp @@ -57,11 +57,11 @@ void Word::set_embeds(const Resource& rsc) { } -void Word::add_morph(const wstringstream& wlex, uint8_t tag1, int begin_idx, int end_idx) { +void Word::add_morph(const wstringstream& wlex, uint8_t tag, int begin_idx, int end_idx) { const wchar_t* morph_wbegin = wbegin + begin_idx; int morph_wlength = end_idx - begin_idx + 1; - pos_tag_t tag0 = static_cast(tag1 - 1); - morph_vec.emplace_back(make_shared(wlex.str(), tag0, morph_wbegin, morph_wlength)); + morph_vec.emplace_back(make_shared(wlex.str(), static_cast(tag), morph_wbegin, + morph_wlength)); } diff --git a/src/main/cpp/khaiii/Word.hpp b/src/main/cpp/khaiii/Word.hpp index 792e0ab..db4233d 100644 --- a/src/main/cpp/khaiii/Word.hpp +++ b/src/main/cpp/khaiii/Word.hpp @@ -68,7 +68,7 @@ class Word: public khaiii_word_t { * @param begin_idx 시작 인덱스 (유니코드 음절 인덱스) * @param end_idx 끝 인덱스 (유니코드 음절 인덷스) */ - void add_morph(const std::wstringstream& wlex, uint8_t tag1, int begin_idx, int end_idx); + void add_morph(const std::wstringstream& wlex, uint8_t tag, int begin_idx, int end_idx); /** * API 결과 구조체의 내용을 채운다. diff --git a/src/main/cpp/khaiii/nn/tensor.hpp b/src/main/cpp/khaiii/nn/tensor.hpp index 62c95c9..e1227c6 100644 --- a/src/main/cpp/khaiii/nn/tensor.hpp +++ b/src/main/cpp/khaiii/nn/tensor.hpp @@ -47,6 +47,16 @@ extern activation_t RELU; */ void add_positional_enc(float* data, int len, int dim); +/** + * add two vector in-place (update left vector) + * @param left vector (will be updated) + * @param right vector + */ +inline void add_vec(float* left, const float* right, int dim) { + assert(dim > 0); + for (; dim > 0; --dim) *left++ += *right++; +} + } // namespace nn } // namespace khaiii diff --git a/src/main/cpp/khaiii/util.hpp b/src/main/cpp/khaiii/util.hpp index 606820b..6dc8dcb 100644 --- a/src/main/cpp/khaiii/util.hpp +++ b/src/main/cpp/khaiii/util.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "boost/locale/encoding_utf.hpp" diff --git a/src/main/python/khaiii/munjong/libpatch.py b/src/main/python/khaiii/munjong/libpatch.py index 13e3ecf..acdb3af 100644 --- a/src/main/python/khaiii/munjong/libpatch.py +++ b/src/main/python/khaiii/munjong/libpatch.py @@ -143,9 +143,8 @@ def _load_corpus(path: str, enc: str) -> Tuple[List[Line], Dict[str, int]]: wid, content = line.split('\t', 1) if wid in wid_dic: raise RuntimeError('duplicated word ID: %s' % line) - else: - wid_dic[wid] = len(lines) - lines.append(Line(WORD_TYPE, wid, content)) + wid_dic[wid] = len(lines) + lines.append(Line(WORD_TYPE, wid, content)) elif line in SENT_OPEN_TAGS: lines.append(Line(BOS_TYPE, None, line)) elif line in SENT_CLOSE_TAGS: diff --git a/src/main/python/khaiii/resource/char_align.py b/src/main/python/khaiii/resource/char_align.py index a2052d8..71c8d06 100644 --- a/src/main/python/khaiii/resource/char_align.py +++ b/src/main/python/khaiii/resource/char_align.py @@ -555,7 +555,7 @@ def _align_forward_backward(self, raw_word: str, mrp_chrs: List[MrpChr]) -> List (MrpChr.to_str(pfx_mrp_chrs), MrpChr.to_str(mdl_mrp_chrs), MrpChr.to_str(sfx_mrp_chrs))) raise algn_err - elif not mdl_mrp_chrs: + if not mdl_mrp_chrs: algn_err = AlignError('{N:0}') algn_err.add_msg('[%s] [%s] [%s]' % (pfx_word, mdl_word, sfx_word)) algn_err.add_msg('[%s] [%s] [%s]' % \ diff --git a/src/main/python/khaiii/resource/resource.py b/src/main/python/khaiii/resource/resource.py index 110ee70..381b20f 100644 --- a/src/main/python/khaiii/resource/resource.py +++ b/src/main/python/khaiii/resource/resource.py @@ -23,13 +23,8 @@ ############# # constants # ############# -SPECIAL_CHARS = [ - '', # unknown character - '', '', # begin/end of word - '', '' # begin/end of sentence -] - -PAD_CHR = '

' # sepcial character for padding +UNK_CHR = '@@UNKNOWN@@' +SPECIAL_CHARS = ['', ''] # begin/end of word ######### @@ -45,14 +40,14 @@ def __init__(self, cfg: Namespace): cfg: config """ vocab_in_path = '{}/vocab.in'.format(cfg.rsc_src) - self.vocab_in = Vocabulary(vocab_in_path, cfg.cutoff, SPECIAL_CHARS) + self.vocab_in = Vocabulary(vocab_in_path, cfg.cutoff, UNK_CHR, SPECIAL_CHARS) vocab_out_path = '{}/vocab.out'.format(cfg.rsc_src) - self.vocab_out = Vocabulary(vocab_out_path, 0, None) + self.vocab_out = Vocabulary(vocab_out_path) # no unknown, no special restore_dic_path = '{}/restore.dic'.format(cfg.rsc_src) - self.restore_dic = self._load_restore_dic(restore_dic_path) + self.restore_dic = self.load_restore_dic(restore_dic_path) @classmethod - def _load_restore_dic(cls, path: str) -> Dict[str, str]: + def load_restore_dic(cls, path: str) -> Dict[str, str]: """ load character to output tag mapping Args: @@ -74,7 +69,7 @@ def _load_restore_dic(cls, path: str) -> Dict[str, str]: ############# # functions # ############# -def load_restore_dic(file_path: str) -> Dict[Tuple[str, str], Dict[int, str]]: +def parse_restore_dic(file_path: str) -> Dict[Tuple[str, str], Dict[int, str]]: """ 원형복원 사전을 로드한다. Args: diff --git a/src/main/python/khaiii/resource/vocabulary.py b/src/main/python/khaiii/resource/vocabulary.py index eb7a67c..c9ec9fb 100644 --- a/src/main/python/khaiii/resource/vocabulary.py +++ b/src/main/python/khaiii/resource/vocabulary.py @@ -11,7 +11,6 @@ ########### # imports # ########### -import copy import logging import os from typing import List @@ -24,26 +23,25 @@ class Vocabulary: """ vocabulary class """ - def __init__(self, path: str, cutoff: int = 1, special: List[str] = None, padding: str = ''): + def __init__(self, path: str, cutoff: int = 1, unk: str = '', special: List[str] = None): """ + padding index is always 0. None and '' get padding index. + if `unk` is given (such as input vocab), its index is always 1. + if `unk` is not given (such as output vocab), an exception will be thrown for unknown entry Args: path: file path cutoff: cutoff frequency + unk: unknown(OOV) entry special: special entries located at the first - padding: add padding special char at the end """ self.dic = {} # {entry: number} dictionary - self.rev = copy.deepcopy(special) if special else [] # reverse dictionary + self.unk = unk + self.rev = ['', unk] if unk else [] # reverse dictionary + if special: + self.rev.extend(special) for num, entry in enumerate(self.rev): self.dic[entry] = num self._load(path, cutoff) - self.padding = padding - if padding: - if padding in self.dic: - raise ValueError('padding special character already in vocab: {}'.format(padding)) - padding_idx = len(self.dic) - self.dic[padding] = padding_idx - self.rev.append(padding) assert len(self.dic) == len(self.rev) def __getitem__(self, key): @@ -57,22 +55,14 @@ def __getitem__(self, key): return self.rev[key] try: return self.dic[key] - except KeyError: - return 0 # unknown word number + except KeyError as key_err: + if self.unk: + return self.dic[self.unk] + raise key_err def __len__(self): return len(self.dic) - def padding_idx(self) -> int: - """ - 맨 마지막에 추가한 패딩의 인덱스를 리턴한다. - Returns: - 패딩 인덱스 - """ - if not self.padding: - raise RuntimeError('vocabulary has no padding') - return self.dic[self.padding] - def _load(self, path: str, cutoff: int = 1): """ load vocabulary from file diff --git a/src/main/python/khaiii/train/dataset.py b/src/main/python/khaiii/train/dataset.py index 7aee785..b8b963d 100644 --- a/src/main/python/khaiii/train/dataset.py +++ b/src/main/python/khaiii/train/dataset.py @@ -12,15 +12,17 @@ # imports # ########### from argparse import Namespace +import itertools import logging import os import random -from typing import List, TextIO, Tuple +from typing import Dict, List, TextIO, Tuple -from torch import LongTensor, Tensor # pylint: disable=no-member, no-name-in-module +import torch +from torch import Tensor from tqdm import tqdm -from khaiii.resource.resource import PAD_CHR, Resource +from khaiii.resource.resource import Resource from khaiii.train.sentence import PosSentence, PosWord @@ -44,104 +46,171 @@ def __len__(self): return sum([len(w.raw) for w in self.pos_tagged_words]) + len(self.pos_tagged_words) + 1 return 0 - def make_labels(self, with_spc: bool) -> List[str]: + @classmethod + def to_tensor(cls, arr: List, gpu_num: int = -1) -> Tensor: """ - 각 음절별로 출력 레이블(태그)를 생성한다. Args: - with_spc: 공백(어절 경계) 포함 여부 + arr: array to convert + gpu_num: GPU device number. default: -1 for CPU Returns: - 레이블 리스트 + tensor """ - if not with_spc: - # 문장 경계, 어절 경계 등 가상 음절을 제외하고 순수한 음절들의 레이블 - return [tag for pos_word in self.pos_tagged_words for tag in pos_word.tags] - labels = [PAD_CHR, ] # 문장 시작 - for pos_word in self.pos_tagged_words: - if len(labels) > 1: - labels.append(PAD_CHR) # 어절 경계 - labels.extend(pos_word.tags) - labels.append(PAD_CHR) # 문장 종료 - return labels + # pylint: disable=no-member + device = torch.device('cuda', gpu_num) if torch.cuda.is_available() and gpu_num >= 0 \ + else torch.device('cpu') + return torch.tensor(arr, device=device) # pylint: disable=not-callable - def make_contexts(self, window: int, spc_dropout: float) -> List[str]: + def make_contexts(self, window: int) -> List[List[str]]: """ 각 음절 별로 좌/우 window 크기 만큼 context를 만든다. Args: window: left/right window size - spc_dropout: space(word delimiter) dropout rate Returns: contexts """ - contexts = [] - for wrd_idx, word in enumerate(self.words): - for chr_idx, char in enumerate(word): - left_context = list(reversed(word[:chr_idx])) - if random.random() >= spc_dropout: - left_context.append('') - for left_word in reversed(self.words[:wrd_idx]): - left_context.extend(reversed(left_word)) - if len(left_context) >= window: - break - if len(left_context) < window: - left_context.extend(['', ] * (window - len(left_context))) - left_context = list(reversed(left_context[:window])) - assert len(left_context) == window - - right_context = list(word[chr_idx+1:]) - if random.random() >= spc_dropout: - right_context.append('') - for right_word in self.words[wrd_idx+1:]: - right_context.extend(list(right_word)) - if len(right_context) >= window: - break - if len(right_context) < window: - right_context.extend(['', ] * (window - len(right_context))) - right_context = right_context[:window] - assert len(right_context) == window - contexts.append(left_context + [char, ] + right_context) + chars = [c for w in self.words for c in w] + chars_len = len(chars) + chars_padded = ['', ] * window + chars + ['', ] * window + contexts = [chars_padded[idx-window:idx+window+1] + for idx in range(window, chars_len + window)] return contexts - def to_tensor(self, cfg: Namespace, rsc: Resource, is_train: bool) -> Tuple[Tensor, Tensor]: + @classmethod + def _flatten(cls, list_of_lists): """ - 문장 내에 포함된 전체 음절들과 태그를 모델의 forward 메소드에 넣을 수 있는 텐서로 변환한다. + flatten one level of nesting Args: - cfg: config - rsc: Resource object - is_train: whether is train or not + list_of_lists: list of lists Returns: - labels tensor - contexts tensor - """ - # 차원: [문장내 음절 갯수, ] - label_nums = [rsc.vocab_out[l] for l in self.make_labels(False)] - labels_tensor = LongTensor(label_nums) - # 차원: [문장내 음절 갯수 x context 크기] - spc_dropout = cfg.spc_dropout if is_train else 0.0 - context_nums = [[rsc.vocab_in[c] for c in context] \ - for context in self.make_contexts(cfg.window, spc_dropout)] - contexts_tensor = LongTensor(context_nums) - return labels_tensor, contexts_tensor - - def make_chars(self) -> List[str]: - """ - 문장 내 포함된 음절들을 만든다. 문장 경계 및 어절 경계를 포함한다. + flattened list + """ + return list(itertools.chain.from_iterable(list_of_lists)) + + def make_left_spc_masks(self, window: int, left_vocab_id: int, spc_dropout: float) \ + -> List[List[int]]: + """ + 각 음절 별로 좌/우 window 크기 만큼 context를 만든다. + Args: + window: left/right window size + left_vocab_id: vocabulary ID for '' + spc_dropout: space dropout rate Returns: - 음절의 리스트 + left space masks + """ + def _filter_left_spc_mask(left_spc_mask): + """ + 중심 음절로부터 첫번째 왼쪽 공백만 남기고 나머지는 제거한다. + Args: + left_spc_mask: 왼쪽 공백 마스크 + """ + for idx in range(window, -1, -1): + if left_spc_mask[idx] == left_vocab_id: + if random.random() < spc_dropout: + left_spc_mask[idx] = 0 + for jdx in range(idx-1, -1, -1): + left_spc_mask[jdx] = 0 + break + + left_spcs = self._flatten([[left_vocab_id, ] + [0, ] * (len(word)-1) + for word in self.words]) + left_padded = [0, ] * window + left_spcs + [0, ] * window + left_spc_masks = [left_padded[idx-window:idx+1] + [0, ] * window + for idx in range(window, len(left_spcs) + window)] + for left_spc_mask in left_spc_masks: + _filter_left_spc_mask(left_spc_mask) + return left_spc_masks + + def make_right_spc_masks(self, window: int, right_vocab_id: int, spc_dropout: float) \ + -> List[List[int]]: + """ + 각 음절 별로 좌/우 window 크기 만큼 context를 만든다. + Args: + window: left/right window size + right_vocab_id: vocabulary ID for '' + spc_dropout: space dropout rate + Returns: + right space masks + """ + def _filter_right_spc_mask(right_spc_mask): + """ + 중심 음절로부터 첫번째 오른쪽 공백만 남기고 나머지는 제거한다. + Args: + right_spc_mask: 오른쪽 공백 마스크 + """ + for idx in range(window, len(right_spc_mask)): + if right_spc_mask[idx] == right_vocab_id: + if random.random() < spc_dropout: + right_spc_mask[idx] = 0 + for jdx in range(idx+1, len(right_spc_mask)): + right_spc_mask[jdx] = 0 + break + + right_spcs = self._flatten([[0, ] * (len(word)-1) + [right_vocab_id, ] + for word in self.words]) + right_padded = [0, ] * window + right_spcs + [0, ] * window + right_spc_masks = [[0, ] * window + right_padded[idx:idx+window+1] + for idx in range(window, len(right_spcs) + window)] + for right_spc_mask in right_spc_masks: + _filter_right_spc_mask(right_spc_mask) + return right_spc_masks + + def get_contexts(self, cfg: Namespace, rsc: Resource) -> List[List[int]]: + """ + 문맥을 반환하는 메서드 + Args: + cfg: config + rsc: Resource object + Returns + 문맥 리스트. shape: [(문장 내 음절 길이), (문맥의 크기)] + """ + contexts = self.make_contexts(cfg.window) + return [[rsc.vocab_in[c] for c in context] for context in contexts] + + def get_spc_masks(self, cfg: Namespace, rsc: Resource, do_spc_dropout: bool) \ + -> Tuple[List[List[int]], List[List[int]]]: + """ + 공백 마스킹 벡터를 반환하는 메소드 + Args: + cfg: config + rsc: Resource object + do_spc_dropout: 공백 마스크 시 dropout 적용 여부 + Returns + 좌측 공백 마스킹 벡터. shape: [(문장 내 음절 길이), (문맥의 크기)] + 우측 공백 마스킹 벡터. shape: [(문장 내 음절 길이), (문맥의 크기)] + """ + spc_dropout = cfg.spc_dropout if do_spc_dropout else 0.0 + left_spc_masks = self.make_left_spc_masks(cfg.window, rsc.vocab_in[''], spc_dropout) + right_spc_masks = self.make_right_spc_masks(cfg.window, rsc.vocab_in[''], spc_dropout) + return left_spc_masks, right_spc_masks + + def get_labels(self, rsc: Resource) -> List[int]: + """ + 레이블(출력 태그)를 반환하는 메서드 + Args: + rsc: Resource object + Returns + 레이블 리스트. shape: [(문장 내 음절 길이), ] + """ + return [rsc.vocab_out[tag] for pos_word in self.pos_tagged_words for tag in pos_word.tags] + + def get_spaces(self) -> List[int]: + """ + 음절 별 공백 여부를 반환하는 메서드 + Returns + 공백 여부 리스트. shape: [(문장 내 음절 길이), ] """ - chars = ['', ] # 문장 시작 + spaces = [] for word in self.words: - if len(chars) > 1: - chars.append('') # 어절 경계 - chars.extend(word) - chars.append('') # 문장 종료 - return chars + spaces.extend([0, ] * (len(word)-1)) + spaces.append(1) + return spaces class PosDataset: """ part-of-speech tag dataset """ - def __init__(self, cfg: Namespace, restore_dic: dict, fin: TextIO): + def __init__(self, cfg: Namespace, restore_dic: Dict[str, str], fin: TextIO): """ Args: cfg: config diff --git a/src/main/python/khaiii/train/embedder.py b/src/main/python/khaiii/train/embedder.py index 61acf2d..957fefa 100644 --- a/src/main/python/khaiii/train/embedder.py +++ b/src/main/python/khaiii/train/embedder.py @@ -33,19 +33,26 @@ def __init__(self, cfg: Namespace, rsc: Resource): super().__init__() self.cfg = cfg self.rsc = rsc - self.embedding = nn.Embedding(len(rsc.vocab_in), cfg.embed_dim) + self.embedding = nn.Embedding(len(rsc.vocab_in), cfg.embed_dim, 0) - def forward(self, inputs): # pylint: disable=arguments-differ + def forward(self, *inputs): # pylint: disable=arguments-differ """ 임베딩을 생성하는 메소드 Args: - inputs: contexts of batch size + inputs: batch size list of (context, left space mask, right space mask) Returns: embedding """ - embeds = self.embedding(inputs) + contexts, left_spc_masks, right_spc_masks = inputs + embeds = self.embedding(contexts) + if left_spc_masks is not None: + embeds += self.embedding(left_spc_masks) + if right_spc_masks is not None: + embeds += self.embedding(right_spc_masks) + # 왼쪽과 오른쪽 패딩에는 zero 벡터인데 아래 positional encoding이 더해짐 + # 사소하지만 아래도 패딩 영역에 대해 마스킹 후 더해줘야 하지 않을까? embeds += positional_encoding(self.cfg.context_len, self.cfg.context_len, - self.cfg.embed_dim, 1) + self.cfg.embed_dim, 1, self.cfg.gpu_num) return embeds @@ -71,7 +78,8 @@ def __missing__(self, key): @memoize -def positional_encoding(sent_len: int, max_dim: int, embed_dim: int, method: int = 1) -> Tensor: +def positional_encoding(sent_len: int, max_dim: int, embed_dim: int, method: int = 1, + gpu_num: int = -1) -> Tensor: """ positional encoding Tensor 출력. embeds [batch_size, context_len, embed_dim]에 Broadcasting 으로 더해짐 @@ -80,10 +88,12 @@ def positional_encoding(sent_len: int, max_dim: int, embed_dim: int, method: int max_dim: maximum dimension embed_dim: embedding dimension method: method number (1. end-to-end memory networks or 2. attention is all you need) + gpu_num: GPU device number. default: -1 for CPU Returns: pe [context_len, embed_dim] """ - pe_tensor = torch.zeros([max_dim, embed_dim]) # pylint: disable=no-member + device = gpu_num if gpu_num >= 0 else None + pe_tensor = torch.zeros([max_dim, embed_dim], device=device) # pylint: disable=no-member for pos in range(1, sent_len + 1): for i in range(1, embed_dim+1): if method == 1: @@ -96,7 +106,5 @@ def positional_encoding(sent_len: int, max_dim: int, embed_dim: int, method: int pe_tensor[pos-1, i-1] = math.sin(pos / 10000 ** (2*i / embed_dim)) else: pe_tensor[pos-1, i-1] = math.cos(pos / 10000 ** (2*i / embed_dim)) - if torch.cuda.is_available(): - pe_tensor = pe_tensor.cuda() pe_tensor.detach() return pe_tensor diff --git a/src/main/python/khaiii/train/evaluator.py b/src/main/python/khaiii/train/evaluator.py index 33f5fe3..3596039 100644 --- a/src/main/python/khaiii/train/evaluator.py +++ b/src/main/python/khaiii/train/evaluator.py @@ -38,9 +38,12 @@ def evaluate(self) -> Tuple[float, float, float]: """ char_acc = self.cnt['match_chars'] / self.cnt['total_chars'] word_acc = self.cnt['match_words'] / self.cnt['total_words'] - recall = self.cnt['match_morphs'] / self.cnt['total_gold_morphs'] - precision = self.cnt['match_morphs'] / self.cnt['total_pred_morphs'] - f_score = 2.0 * recall * precision / (recall + precision) + if self.cnt['match_morphs'] == 0: + recall = precision = f_score = 0.0 + else: + recall = self.cnt['match_morphs'] / self.cnt['total_gold_morphs'] + precision = self.cnt['match_morphs'] / self.cnt['total_pred_morphs'] + f_score = 2.0 * recall * precision / (recall + precision) self.cnt.clear() return char_acc, word_acc, f_score @@ -104,13 +107,17 @@ def morphs_to_set(cls, morphs: List[PosMorph]) -> set: def report(self, fout: TextIO): """ report recall/precision to file - :param fout: output file + Args: + fout: output file """ print('word accuracy: %d / %d = %.4f' % (self.cnt['match_words'], self.cnt['total_words'], self.cnt['match_words'] / self.cnt['total_words']), file=fout) - recall = self.cnt['match_morphs'] / self.cnt['total_gold_morphs'] - precision = self.cnt['match_morphs'] / self.cnt['total_pred_morphs'] - f_score = 2.0 * recall * precision / (recall + precision) + if self.cnt['match_morphs'] == 0: + recall = precision = f_score = 0.0 + else: + recall = self.cnt['match_morphs'] / self.cnt['total_gold_morphs'] + precision = self.cnt['match_morphs'] / self.cnt['total_pred_morphs'] + f_score = 2.0 * recall * precision / (recall + precision) print('f-score / (recall, precision): %.4f / (%.4f, %.4f)' % (f_score, recall, precision), file=fout) diff --git a/src/main/python/khaiii/train/models.py b/src/main/python/khaiii/train/models.py index c1b3222..a27135c 100644 --- a/src/main/python/khaiii/train/models.py +++ b/src/main/python/khaiii/train/models.py @@ -24,92 +24,104 @@ ######### # types # ######### -class PosModel(nn.Module): +class ConvLayer(nn.Module): """ - part-of-speech tagger pytorch model + 형태소 태깅 모델과 띄어쓰기 모델이 공유하는 컨볼루션 레이어 """ def __init__(self, cfg: Namespace, rsc: Resource): """ Args: - cfg (Namespace): config - rsc (Resource): Resource object + cfg: config + rsc: Resource object """ super().__init__() - self.cfg = cfg - self.rsc = rsc self.embedder = Embedder(cfg, rsc) + ngram = min(5, cfg.window * 2 + 1) + self.convs = nn.ModuleList([nn.Conv1d(cfg.embed_dim, cfg.embed_dim, kernel_size) + for kernel_size in range(2, ngram+1)]) def forward(self, *inputs): - raise NotImplementedError + embeds = self.embedder(*inputs) + embeds_t = embeds.transpose(1, 2) + pool_outs = [] + for conv in self.convs: + conv_out = F.relu(conv(embeds_t)) + pool_outs.append(F.max_pool1d(conv_out, conv_out.size(2))) + features = torch.cat([p.view(embeds.size(0), -1) for p in pool_outs], dim=1) # pylint: disable=no-member + return features - def save(self, path: str): - """ - 모델을 저장하는 메소드 - Args: - path (str): 경로 - """ - torch.save(self.state_dict(), path) - def load(self, path: str): +class HiddenLayer(nn.Module): + """ + 형태소 태깅 모델과 띄어쓰기 모델이 각각 학습하는 히든 레이어 + """ + def __init__(self, cfg: Namespace, rsc: Resource, conv_layer_len: int, is_spc: bool): """ - 저장된 모델을 로드하는 메소드 Args: - path (str): 경로 + cfg: config + rsc: Resource object + conv_layer_len: convolution 레이어의 n-gram 타입 갯수 + is_spc: 띄어쓰기 모델 여부 """ - if torch.cuda.is_available(): - state_dict = torch.load(path) - else: - state_dict = torch.load(path, map_location=lambda storage, loc: storage) - self.load_state_dict(state_dict) - if torch.cuda.is_available(): - self.cuda() + super().__init__() + setattr(cfg, 'hidden_dim', + (cfg.embed_dim * conv_layer_len + len(rsc.vocab_out)) // 2) + feature_dim = cfg.embed_dim * conv_layer_len + tag_dim = 2 if is_spc else len(rsc.vocab_out) + self.layers = nn.ModuleList([nn.Linear(feature_dim, cfg.hidden_dim), + nn.Linear(cfg.hidden_dim, tag_dim)]) + + def forward(self, features): # pylint: disable=arguments-differ + # feature => hidden + features_drop = F.dropout(features) + hidden_out = F.relu(self.layers[0](features_drop)) + # hidden => tag + hidden_out_drop = F.dropout(hidden_out) + tag_out = self.layers[1](hidden_out_drop) + return tag_out -class CnnModel(PosModel): +class Model(nn.Module): """ - convolutional neural network based part-of-speech tagger + 형태소 태깅 모델, 띄어쓰기 모델 """ def __init__(self, cfg: Namespace, rsc: Resource): """ Args: - cfg (Namespace): config - rsc (Resource): Resource object + cfg: config + rsc: Resource object """ - super().__init__(cfg, rsc) - - ngram = min(5, cfg.window * 2 + 1) - self.convs = nn.ModuleList([nn.Conv1d(cfg.embed_dim, cfg.embed_dim, kernel_size) - for kernel_size in range(2, ngram+1)]) - - # conv => hidden - setattr(cfg, 'hidden_dim', (cfg.embed_dim * len(self.convs) + len(rsc.vocab_out)) // 2) - self.conv2hidden = nn.Linear(cfg.embed_dim * len(self.convs), cfg.hidden_dim) + super().__init__() + self.cfg = cfg + self.rsc = rsc + self.conv_layer = ConvLayer(cfg, rsc) + self.hidden_layer_pos = HiddenLayer(cfg, rsc, len(self.conv_layer.convs), is_spc=False) + self.hidden_layer_spc = HiddenLayer(cfg, rsc, len(self.conv_layer.convs), is_spc=True) - # hidden => tag - self.hidden2tag = nn.Linear(cfg.hidden_dim, len(rsc.vocab_out)) + def forward(self, *inputs): + contexts, left_spc_masks, right_spc_masks = inputs + features_pos = self.conv_layer(contexts, left_spc_masks, right_spc_masks) + features_spc = self.conv_layer(contexts, None, None) + logits_pos = self.hidden_layer_pos(features_pos) + logits_spc = self.hidden_layer_spc(features_spc) + return logits_pos, logits_spc - def forward(self, contexts): # pylint: disable=arguments-differ + def save(self, path: str): """ - forward path + 모델을 저장하는 메소드 Args: - contexts: batch size list of character and context - Returns: - output score + path: 경로 """ - embeds = self.embedder(contexts) - embeds_t = embeds.transpose(1, 2) - - pool_outs = [] - for conv in self.convs: - conv_out = F.relu(conv(embeds_t)) - pool_outs.append(F.max_pool1d(conv_out, conv_out.size(2))) - - # conv => hidden - features = torch.cat([p.view(contexts.size(0), -1) for p in pool_outs], dim=1) # pylint: disable=no-member - features_drop = F.dropout(features) - hidden_out = F.relu(self.conv2hidden(features_drop)) + torch.save(self.state_dict(), path) - # hidden => tag - hidden_out_drop = F.dropout(hidden_out) - tag_out = self.hidden2tag(hidden_out_drop) - return tag_out + def load(self, path: str): + """ + 저장된 모델을 로드하는 메소드 + Args: + path: 경로 + conv_layer: convolution layer + """ + state_dict = torch.load(path, map_location=lambda storage, loc: storage) + self.load_state_dict(state_dict) + if torch.cuda.is_available() and self.cfg.gpu_num >= 0: + self.cuda(device=self.cfg.gpu_num) diff --git a/src/main/python/khaiii/train/sentence.py b/src/main/python/khaiii/train/sentence.py index c36113c..e8e9d64 100644 --- a/src/main/python/khaiii/train/sentence.py +++ b/src/main/python/khaiii/train/sentence.py @@ -14,7 +14,7 @@ import logging import re -from typing import List, Tuple +from typing import Dict, List, Tuple ######### @@ -112,7 +112,7 @@ def __eq__(self, other: 'PosWord'): """ return self.res_chrs == other.res_chrs and self.res_tags == other.res_tags - def set_pos_result(self, tags: List[str], restore_dic: dict = None): + def set_pos_result(self, tags: List[str], restore_dic: Dict[str, str] = None): """ 외부에서 생성된 PosWord객체의 정보를 현재 인스턴스에 설정합니다. Args: @@ -125,7 +125,7 @@ def set_pos_result(self, tags: List[str], restore_dic: dict = None): assert len(self.raw) == len(self.tags) # 음절수와 태그수는 동일해야 한다. self.pos_tagged_morphs = self._make_pos_morphs(restore_dic) - def _make_pos_morphs(self, restore_dic: dict = None): + def _make_pos_morphs(self, restore_dic: Dict[str, str] = None): """ 형태소 태그리스트를 대상으로 B/I 로 병합되는 위치를 구합니다. Args: @@ -159,7 +159,7 @@ def _make_pos_morphs(self, restore_dic: dict = None): (lex, iob_tag, self.res_chrs, self.res_tags)) return pos_morphs - def _restore(self, restore_dic: dict): + def _restore(self, restore_dic: Dict[str, str]): """ 원형 복원 사전을 이용하여 형태소의 원형을 복원한다. Args: @@ -234,7 +234,7 @@ def init_pos_tags(self): for word in self.words: self.pos_tagged_words.append(PosWord(word)) - def set_pos_result(self, tags: List[str], restore_dic: dict = None): + def set_pos_result(self, tags: List[str], restore_dic: Dict[str, str] = None): """ 문장 전체에 대한 형태소 태그 출력 레이블 정보를 세팅하고 형태소를 복원한다. Args: diff --git a/src/main/python/khaiii/train/tagger.py b/src/main/python/khaiii/train/tagger.py index c0997d8..0d54823 100644 --- a/src/main/python/khaiii/train/tagger.py +++ b/src/main/python/khaiii/train/tagger.py @@ -16,12 +16,11 @@ import logging import re -import torch import torch.nn.functional as F from khaiii.resource.resource import Resource from khaiii.train.dataset import PosSentTensor -from khaiii.train.models import CnnModel +from khaiii.train.models import Model ######### @@ -31,17 +30,19 @@ class PosTagger: """ part-of-speech tagger """ - def __init__(self, model_dir: str): + def __init__(self, model_dir: str, gpu_num: int = -1): """ Args: model_dir: model dir + gpu_num: GPU number to override """ cfg_dict = json.load(open('{}/config.json'.format(model_dir), 'r', encoding='UTF-8')) self.cfg = Namespace() for key, val in cfg_dict.items(): setattr(self.cfg, key, val) + setattr(self.cfg, 'gpu_num', gpu_num) self.rsc = Resource(self.cfg) - self.model = CnnModel(self.cfg, self.rsc) + self.model = Model(self.cfg, self.rsc) self.model.load('{}/model.state'.format(model_dir)) self.model.eval() @@ -54,10 +55,11 @@ def tag_raw(self, raw_sent: str, enable_restore: bool = True) -> PosSentTensor: PosSentTensor object """ pos_sent = PosSentTensor(raw_sent) - _, contexts = pos_sent.to_tensor(self.cfg, self.rsc, False) - if torch.cuda.is_available(): - contexts = contexts.cuda() - outputs = self.model(contexts) + contexts = pos_sent.get_contexts(self.cfg, self.rsc) + left_spc_masks, right_spc_masks = pos_sent.get_spc_masks(self.cfg, self.rsc, False) + outputs, _ = self.model(PosSentTensor.to_tensor(contexts, self.cfg.gpu_num), # pylint: disable=no-member + PosSentTensor.to_tensor(left_spc_masks, self.cfg.gpu_num), # pylint: disable=no-member + PosSentTensor.to_tensor(right_spc_masks, self.cfg.gpu_num)) # pylint: disable=no-member _, predicts = F.softmax(outputs, dim=1).max(1) tags = [self.rsc.vocab_out[t.item()] for t in predicts] pos_sent.set_pos_result(tags, self.rsc.restore_dic if enable_restore else None) diff --git a/src/main/python/khaiii/train/trainer.py b/src/main/python/khaiii/train/trainer.py index 6bc2f37..1c700cc 100644 --- a/src/main/python/khaiii/train/trainer.py +++ b/src/main/python/khaiii/train/trainer.py @@ -27,9 +27,9 @@ import torch.nn.functional as F from tqdm import tqdm -from khaiii.train.dataset import PosDataset +from khaiii.train.dataset import PosDataset, PosSentTensor from khaiii.train.evaluator import Evaluator -from khaiii.train.models import CnnModel +from khaiii.train.models import Model from khaiii.resource.resource import Resource @@ -50,7 +50,9 @@ def __init__(self, cfg: Namespace): setattr(cfg, 'out_dir', '{}/{}'.format(cfg.logdir, cfg.model_id)) setattr(cfg, 'context_len', 2 * cfg.window + 1) self.rsc = Resource(cfg) - self.model = CnnModel(cfg, self.rsc) + self.model = Model(cfg, self.rsc) + if torch.cuda.is_available() and cfg.gpu_num >= 0: + self.model.cuda(device=cfg.gpu_num) self.optimizer = torch.optim.Adam(self.model.parameters(), cfg.learning_rate) self.criterion = nn.CrossEntropyLoss() self.evaler = Evaluator() @@ -141,8 +143,10 @@ def _restore_prev_train(self): return logging.info('==== continue training: %s ====', self.cfg.model_id) cfg = json.load(open(cfg_path, 'r', encoding='UTF-8')) + gpu_num = self.cfg.gpu_num for key, val in cfg.items(): setattr(self.cfg, key, val) + setattr(self.cfg, 'gpu_num', gpu_num) self._revert_to_best(False) f_score_best = 0.0 @@ -151,8 +155,8 @@ def _restore_prev_train(self): line = line.rstrip('\r\n') if not line: continue - (epoch, loss_train, loss_dev, acc_char, acc_word, f_score, learning_rate) = \ - line.split('\t') + (epoch, loss_train, loss_dev, acc_char, acc_word, f_score, learning_rate) \ + = line.split('\t') self.cfg.epoch = int(epoch) + 1 self.cfg.best_epoch = self.cfg.epoch self.loss_trains.append(float(loss_train)) @@ -178,13 +182,11 @@ def train(self): train_begin = datetime.now() logging.info('{{{{ training begin: %s {{{{', self._dt_str(train_begin)) - if torch.cuda.is_available(): - self.model.cuda() pathlib.Path(self.cfg.out_dir).mkdir(parents=True, exist_ok=True) self.log_file = open('{}/log.tsv'.format(self.cfg.out_dir), 'at') self.sum_wrt = SummaryWriter(self.cfg.out_dir) patience = self.cfg.patience - for _ in range(1000000): + for _ in range(100000): is_best = self._train_epoch() if is_best: patience = self.cfg.patience @@ -214,7 +216,7 @@ def _revert_to_best(self, is_decay_lr: bool): self.model.load('{}/model.state'.format(self.cfg.out_dir)) if is_decay_lr: self.cfg.learning_rate *= self.cfg.lr_decay - self._load_optim('{}/optim.state'.format(self.cfg.out_dir), self.cfg.learning_rate) + self._load_optim(self.cfg.learning_rate) def _train_epoch(self) -> bool: """ @@ -222,39 +224,56 @@ def _train_epoch(self) -> bool: Returns: 현재 epoch이 best 성능을 나타냈는 지 여부 """ - batches = [] + batch_contexts = [] + batch_left_spc_masks = [] + batch_right_spc_masks = [] + batch_labels = [] + batch_spaces = [] loss_trains = [] for train_sent in tqdm(self.dataset_train, 'EPOCH[{}]'.format(self.cfg.epoch), len(self.dataset_train), mininterval=1, ncols=100): - train_labels, train_contexts = train_sent.to_tensor(self.cfg, self.rsc, True) - if torch.cuda.is_available(): - train_labels = train_labels.cuda() - train_contexts = train_contexts.cuda() - - self.model.train() - train_outputs = self.model(train_contexts) - batches.append((train_labels, train_outputs)) - if sum([batch[0].size(0) for batch in batches]) < self.cfg.batch_size: + # 배치 크기만큼 찰 때까지 문장을 추가 + batch_contexts.extend(train_sent.get_contexts(self.cfg, self.rsc)) + left_spc_masks, right_spc_masks = train_sent.get_spc_masks(self.cfg, self.rsc, True) + batch_left_spc_masks.extend(left_spc_masks) + batch_right_spc_masks.extend(right_spc_masks) + batch_labels.extend(train_sent.get_labels(self.rsc)) + batch_spaces.extend(train_sent.get_spaces()) + if len(batch_labels) < self.cfg.batch_size: continue - batch_label = torch.cat([x[0] for x in batches], 0) # pylint: disable=no-member - batch_output = torch.cat([x[1] for x in batches], 0) # pylint: disable=no-member - batches = [] - - batch_output.requires_grad_() - loss_train = self.criterion(batch_output, batch_label) + # 형태소 태깅 모델 학습 + self.model.train() + batch_outputs_pos, batch_outputs_spc = \ + self.model(PosSentTensor.to_tensor(batch_contexts, self.cfg.gpu_num), + PosSentTensor.to_tensor(batch_left_spc_masks, self.cfg.gpu_num), + PosSentTensor.to_tensor(batch_right_spc_masks, self.cfg.gpu_num)) + batch_outputs_pos.requires_grad_() + batch_outputs_spc.requires_grad_() + loss_train_pos = self.criterion(batch_outputs_pos, + PosSentTensor.to_tensor(batch_labels, self.cfg.gpu_num)) + loss_train_spc = self.criterion(batch_outputs_spc, + PosSentTensor.to_tensor(batch_spaces, self.cfg.gpu_num)) + loss_train = loss_train_pos + loss_train_spc loss_trains.append(loss_train.item()) loss_train.backward() self.optimizer.step() self.optimizer.zero_grad() + # 배치 데이터 초기화 + batch_contexts = [] + batch_left_spc_masks = [] + batch_right_spc_masks = [] + batch_labels = [] + batch_spaces = [] + avg_loss_dev, acc_char, acc_word, f_score = self.evaluate(True) is_best = self._check_epoch(loss_trains, avg_loss_dev, acc_char, acc_word, f_score) self.cfg.epoch += 1 return is_best - def _check_epoch(self, loss_trains: List[float], avg_loss_dev: float, acc_char: float, - acc_word: float, f_score: float) -> bool: + def _check_epoch(self, loss_trains: List[float], avg_loss_dev: float, + acc_char: float, acc_word: float, f_score: float) -> bool: """ 매 epoch마다 수행하는 체크 Args: @@ -276,13 +295,15 @@ def _check_epoch(self, loss_trains: List[float], avg_loss_dev: float, acc_char: self.learning_rates.append(self.cfg.learning_rate) is_best = self._is_best() is_best_str = 'BEST' if is_best else '< {:.4f}'.format(max(self.f_scores)) - logging.info('[Los trn] [Los dev] [Acc chr] [Acc wrd] [F-score] [LR]') + logging.info('[Los trn] [Los dev] [Acc chr] [Acc wrd] [F-score]' \ + ' [LR]') logging.info('{:9.4f} {:9.4f} {:9.4f} {:9.4f} {:9.4f} {:8} {:.8f}' \ .format(avg_loss_train, avg_loss_dev, acc_char, acc_word, f_score, is_best_str, self.cfg.learning_rate)) print('{}\t{}\t{}\t{}\t{}\t{}\t{}'.format(self.cfg.epoch, avg_loss_train, avg_loss_dev, acc_char, acc_word, f_score, - self.cfg.learning_rate), file=self.log_file) + self.cfg.learning_rate), + file=self.log_file) self.log_file.flush() self.sum_wrt.add_scalar('loss-train', avg_loss_train, self.cfg.epoch) self.sum_wrt.add_scalar('loss-dev', avg_loss_dev, self.cfg.epoch) @@ -303,30 +324,19 @@ def _is_best(self) -> bool: # this epoch hits new max value self.cfg.best_epoch = self.cfg.epoch self.model.save('{}/model.state'.format(self.cfg.out_dir)) - self._save_optim('{}/optim.state'.format(self.cfg.out_dir)) + torch.save(self.optimizer.state_dict(), '{}/optimizer.state'.format(self.cfg.out_dir)) with open('{}/config.json'.format(self.cfg.out_dir), 'w', encoding='UTF-8') as fout: json.dump(vars(self.cfg), fout, indent=2, sort_keys=True) return True - def _save_optim(self, path: str): - """ - save optimizer parameters - Args: - path: path - """ - torch.save(self.optimizer.state_dict(), path) - - def _load_optim(self, path: str, learning_rate: float): + def _load_optim(self, learning_rate: float): """ load optimizer parameters Args: - path: path learning_rate: learning rate """ - if torch.cuda.is_available(): - state_dict = torch.load(path) - else: - state_dict = torch.load(path, map_location=lambda storage, loc: storage) + path = '{}/optimizer.state'.format(self.cfg.out_dir) + state_dict = torch.load(path, map_location=lambda storage, loc: storage) self.optimizer = torch.optim.Adam(self.model.parameters(), learning_rate) self.optimizer.load_state_dict(state_dict) self.optimizer.param_groups[0]['lr'] = learning_rate @@ -346,18 +356,25 @@ def evaluate(self, is_dev: bool) -> Tuple[float, float, float, float]: self.model.eval() losses = [] for sent in dataset: + contexts = sent.get_contexts(self.cfg, self.rsc) # 만약 spc_dropout이 1.0 이상이면 공백을 전혀 쓰지 않는 것이므로 평가 시에도 적용한다. - labels, contexts = sent.to_tensor(self.cfg, self.rsc, self.cfg.spc_dropout >= 1.0) - if torch.cuda.is_available(): - labels = labels.cuda() - contexts = contexts.cuda() - outputs = self.model(contexts) - loss = self.criterion(outputs, labels) + left_spc_masks, right_spc_masks = sent.get_spc_masks(self.cfg, self.rsc, + self.cfg.spc_dropout >= 1.0) + gpu_num = self.cfg.gpu_num + outputs_pos, outputs_spc = self.model(PosSentTensor.to_tensor(contexts, gpu_num), + PosSentTensor.to_tensor(left_spc_masks, gpu_num), + PosSentTensor.to_tensor(right_spc_masks, gpu_num)) + labels = PosSentTensor.to_tensor(sent.get_labels(self.rsc), self.cfg.gpu_num) + spaces = PosSentTensor.to_tensor(sent.get_spaces(), self.cfg.gpu_num) + loss_pos = self.criterion(outputs_pos, labels) + loss_spc = self.criterion(outputs_spc, spaces) + loss = loss_pos + loss_spc losses.append(loss.item()) - _, predicts = F.softmax(outputs, dim=1).max(1) + _, predicts = F.softmax(outputs_pos, dim=1).max(1) pred_tags = [self.rsc.vocab_out[t.item()] for t in predicts] pred_sent = copy.deepcopy(sent) pred_sent.set_pos_result(pred_tags, self.rsc.restore_dic) self.evaler.count(sent, pred_sent) avg_loss = sum(losses) / len(losses) - return (avg_loss, ) + self.evaler.evaluate() + char_acc, word_acc, f_score = self.evaler.evaluate() + return avg_loss, char_acc, word_acc, f_score diff --git a/src/main/python/setup.py.in b/src/main/python/setup.py.in index e11b1ff..7119342 100644 --- a/src/main/python/setup.py.in +++ b/src/main/python/setup.py.in @@ -42,16 +42,16 @@ class CustomBuild(build): """ run build command """ - with zipfile.ZipFile(f'{_SRC_NAME}.zip', 'r') as src_zip: + with zipfile.ZipFile('{}.zip'.format(_SRC_NAME), 'r') as src_zip: src_zip.extractall() - build_dir = f'{_SRC_NAME}/build' + build_dir = '{}/build'.format(_SRC_NAME) os.makedirs(build_dir, exist_ok=True) subprocess.check_call('cmake ..', cwd=build_dir, shell=True) subprocess.check_call('make all resource', cwd=build_dir, shell=True) shutil.rmtree('khaiii/lib', ignore_errors=True) - shutil.copytree(f'{build_dir}/lib', 'khaiii/lib') + shutil.copytree('{}/lib'.format(build_dir), 'khaiii/lib') shutil.rmtree('khaiii/share', ignore_errors=True) - shutil.copytree(f'{build_dir}/share', 'khaiii/share') + shutil.copytree('{}/share'.format(build_dir), 'khaiii/share') shutil.rmtree(_SRC_NAME) build.run(self) @@ -83,13 +83,12 @@ setup( 'Development Status :: 5 - Stable', 'License :: OSI Approved :: Apache 2.0', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', ], license='Apache 2.0', packages=['khaiii', ], include_package_data=True, install_requires=[], - setup_requires=['cmake>=3.10', 'pytest-runner'], + setup_requires=['pytest-runner', ], tests_require=['pytest', ], zip_safe=False, cmdclass={'build': CustomBuild} diff --git a/src/test/cpp/khaiii/ErrPatchTest.cpp b/src/test/cpp/khaiii/ErrPatchTest.cpp index 3a8cace..bd2365e 100644 --- a/src/test/cpp/khaiii/ErrPatchTest.cpp +++ b/src/test/cpp/khaiii/ErrPatchTest.cpp @@ -93,20 +93,11 @@ shared_ptr ErrPatchTest::_log = spdlog::stderr_color_mt("ErrPatc //////////////// TEST_F(ErrPatchTest, apply) { // for base model - _check("지저스크라이스트", "지저스/NNG + 크라이스트/NNP", "지저스/NNP + 크라이스트/NNP"); - _check("지저스 크라이스트", "지저스/NNG + _ + 크라이스트/NNP", - "지저스/NNP + _ + 크라이스트/NNP"); - _check("고타마싯다르타", "고타마싯다르타/NNP", "고타마/NNP + 싯다르타/NNP"); - _check("무함마드압둘라", "무함마드/NNP + 압/NNG + 둘/NNP + 라/EC", "무함마드/NNP + 압둘라/NNP"); - - /* - // for large model - _check("지저스크라이스트", "지/NNG + 저스크라이스/NNP + 트/NNG", "지저스/NNP + 크라이스트/NNP"); + _check("지저스크라이스트", "지저스크라이스/NNP + 트/NNG", "지저스/NNP + 크라이스트/NNP"); _check("지저스 크라이스트", "지저스/NNP + _ + 크라이스/NNP + 트/NNG", "지저스/NNP + _ + 크라이스트/NNP"); _check("고타마싯다르타", "고타마싯다르타/NNP", "고타마/NNP + 싯다르타/NNP"); - _check("무함마드압둘라", "무함마드압둘라/NNP", "무함마드/NNP + 압둘라/NNP"); - */ + _check("무함마드압둘라", "무함마드압/NNP + 둘/NR + 라/NNP", "무함마드/NNP + 압둘라/NNP"); } diff --git a/train/eval.py b/train/eval.py new file mode 100755 index 0000000..411b851 --- /dev/null +++ b/train/eval.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +khaiii 출력 형태의 두 파일을 읽어들여 f-score를 측정 +__author__ = 'Jamie (jamie.lim@kakaocorp.com)' +__copyright__ = 'Copyright (C) 2019-, Kakao Corp. All rights reserved.' +""" + + +########### +# imports # +########### +from argparse import ArgumentParser, Namespace +from collections import Counter +import logging +import sys +from typing import Iterator, Set, Tuple + + +############# +# functions # +############# +def _load(path: str) -> Iterator[Tuple[str, str]]: + """ + 파일을 읽어들여 (어절, 형태소)를 리턴하는 제너레이터 + Args: + path: file path + Yields: + word + morphs + """ + for line in open(path, 'r', encoding='UTF-8'): + line = line.rstrip('\r\n') + if not line: + yield '', '' + continue + word, morphs = line.split('\t') + yield word, morphs + + +def _morphs_to_set(morphs: str) -> Set[Tuple[str, int]]: + """ + make set from morpheme string + Args: + morphs: morpheme string + Returns: + morphemes set + """ + morph_cnt = Counter([m for m in morphs.split(' + ')]) + morph_set = set() + for morph, freq in morph_cnt.items(): + if freq == 1: + morph_set.add(morph) + else: + morph_set.update([(morph, i) for i in range(freq)]) + return morph_set + + +def _count(cnt: Counter, gold: str, pred: str): + """ + count gold and pred morphemes + Args: + cnt: Counter object + gold: gold standard morphemes + pred: prediction morphemes + """ + gold_set = _morphs_to_set(gold) + pred_set = _morphs_to_set(pred) + cnt['gold'] += len(gold_set) + cnt['pred'] += len(pred_set) + cnt['match'] += len(gold_set & pred_set) + + +def _report(cnt: Counter): + """ + report metric + Args: + cnt: Counter object + """ + precision = 100 * cnt['match'] / cnt['pred'] + recall = 100 * cnt['match'] / cnt['gold'] + f_score = 2 * precision * recall / (precision + recall) + print(f'precision: {precision:.2f}') + print(f'recall: {recall:.2f}') + print(f'f-score: {f_score:.2f}') + + +def run(args: Namespace): + """ + run function which is the start point of program + Args: + args: program arguments + """ + cnt = Counter() + for line_num, (gold, pred) in enumerate(zip(_load(args.gold), _load(args.pred)), start=1): + word_gold, morphs_gold = gold + word_pred, morphs_pred = pred + if word_gold != word_pred: + raise ValueError(f'invalid align at {line_num}: {word_gold} vs {word_pred}') + if not word_gold or not word_pred: + continue + _count(cnt, morphs_gold, morphs_pred) + _report(cnt) + + +######## +# main # +######## +def main(): + """ + main function processes only argument parsing + """ + parser = ArgumentParser(description='command line part-of-speech tagger demo') + parser.add_argument('-g', '--gold', help='gold standard file', metavar='FILE', required=True) + parser.add_argument('-p', '--pred', help='prediction file', metavar='FILE', required=True) + parser.add_argument('--output', help='output file ', metavar='FILE') + parser.add_argument('--debug', help='enable debug', action='store_true') + args = parser.parse_args() + + if args.output: + sys.stdout = open(args.output, 'w', encoding='UTF-8') + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + run(args) + + +if __name__ == '__main__': + main() diff --git a/train/pickle_model.py b/train/pickle_model.py index 11bcddc..ae75d9f 100755 --- a/train/pickle_model.py +++ b/train/pickle_model.py @@ -67,14 +67,16 @@ def _get_embedding(rsc: Resource, state_dict: dict) -> dict: embedding data """ data = {} - chars = [0, ] * 5 # 5 special characters - for idx in range(5, len(rsc.vocab_in)): + chars = [0, ] * 4 # 4 special characters (0: padding, 1: unknown, 2: left word boundary, + # 3: right word boundary) + for idx in range(4, len(rsc.vocab_in)): chars.append(ord(rsc.vocab_in[idx])) data['chars'] = array('i', chars) # [input vocab(char)] * 4(wchar_t) - embedding = state_dict['embedder.embedding.weight'] - data['weights'] = [] - for row in embedding: + embedding = state_dict['conv_layer.embedder.embedding.weight'] + padding = array('f', [0.0, ] * len(embedding[0])) # first embedding is always padding + data['weights'] = [padding, ] + for row in embedding[1:]: data['weights'].append(array('f', row)) # [input vocab(char)] * embed_dim * 4(float) return data @@ -142,15 +144,15 @@ def _get_data(rsc: Resource, state_dict: dict) -> dict: for kernel in range(2, 6): # weight: [output chan(embed_dim)] * kernel * [input chan(embed_dim)] * 4 # bias: [output chan] * 4 - data['convs'][kernel] = _get_conv('convs', kernel, state_dict) + data['convs'][kernel] = _get_conv('conv_layer.convs', kernel, state_dict) # weight: hidden_dim * [cnn layers * output chan(embed_dim)] * 4 # bias: hidden_dim * 4 - data['conv2hidden'] = _get_linear('conv2hidden', state_dict) + data['conv2hidden'] = _get_linear('hidden_layer_pos.layers.0', state_dict) # weight: [output vocab(tag)] * hidden_dim * 4 # bias: [output vocab(tag)] * 4 - data['hidden2tag'] = _get_linear('hidden2tag', state_dict) + data['hidden2tag'] = _get_linear('hidden_layer_pos.layers.1', state_dict) return data diff --git a/train/tag.py b/train/tag.py index 60bb2e9..3e60d07 100755 --- a/train/tag.py +++ b/train/tag.py @@ -14,7 +14,6 @@ ########### from argparse import ArgumentParser, Namespace import logging -import os import sys from khaiii.train.tagger import PosTagger @@ -29,7 +28,7 @@ def run(args: Namespace): Args: args: program arguments """ - tgr = PosTagger(args.model_dir) + tgr = PosTagger(args.model_dir, args.gpu_num) for line_num, line in enumerate(sys.stdin, start=1): if line_num % 100000 == 0: logging.info('%d00k-th line..', (line_num // 100000)) @@ -55,11 +54,11 @@ def main(): parser.add_argument('-m', '--model-dir', help='model dir', metavar='DIR', required=True) parser.add_argument('--input', help='input file ', metavar='FILE') parser.add_argument('--output', help='output file ', metavar='FILE') - parser.add_argument('--gpu-num', help='GPU number to use', metavar='INT', type=int, default=0) + parser.add_argument('--gpu-num', help='GPU number to use ', metavar='INT', + type=int, default=-1) parser.add_argument('--debug', help='enable debug', action='store_true') args = parser.parse_args() - os.environ['CUDA_VISIBLE_DEVICES'] = str(args.gpu_num) if args.input: sys.stdin = open(args.input, 'r', encoding='UTF-8') if args.output: diff --git a/train/train.py b/train/train.py index 8943041..ad3bf3b 100755 --- a/train/train.py +++ b/train/train.py @@ -14,7 +14,6 @@ ########### from argparse import ArgumentParser, Namespace import logging -import os from khaiii.train.trainer import Trainer @@ -45,13 +44,13 @@ def main(): metavar='DIR', default='../rsc/src') parser.add_argument('--logdir', help='tensorboard log dir ', metavar='DIR', default='./logdir') - parser.add_argument('--window', help='left/right character window length ', - metavar='INT', type=int, default=3) - parser.add_argument('--spc-dropout', help='space(word delimiter) dropout rate ', - metavar='REAL', type=float, default=0.0) - parser.add_argument('--cutoff', help='cutoff ', metavar='INT', type=int, default=2) - parser.add_argument('--embed-dim', help='embedding dimension ', metavar='INT', - type=int, default=30) + parser.add_argument('--window', help='left/right character window length ', + metavar='INT', type=int, default=4) + parser.add_argument('--spc-dropout', help='space(word delimiter) dropout rate ', + metavar='REAL', type=float, default=0.1) + parser.add_argument('--cutoff', help='cutoff ', metavar='INT', type=int, default=1) + parser.add_argument('--embed-dim', help='embedding dimension ', metavar='INT', + type=int, default=35) parser.add_argument('--learning-rate', help='learning rate ', metavar='REAL', type=float, default=0.001) parser.add_argument('--lr-decay', help='learning rate decay ', metavar='REAL', @@ -60,12 +59,11 @@ def main(): default=500) parser.add_argument('--patience', help='maximum patience count to revert model ', metavar='INT', type=int, default=10) - parser.add_argument('--gpu-num', help='GPU number to use ', metavar='INT', type=int, - default=0) + parser.add_argument('--gpu-num', help='GPU number to use ', metavar='INT', + type=int, default=-1) parser.add_argument('--debug', help='enable debug', action='store_true') args = parser.parse_args() - os.environ['CUDA_VISIBLE_DEVICES'] = str(args.gpu_num) if args.debug: logging.basicConfig(level=logging.DEBUG) else: diff --git a/train/transform_corpus.py b/train/transform_corpus.py new file mode 100755 index 0000000..307cf16 --- /dev/null +++ b/train/transform_corpus.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +""" +세종 코퍼스와 khaiii 학습 코퍼스를 원하는 형태로 변환하는 스크립트. +- 입력: sejong: 세종 코퍼스, train: khaiii 학습 코퍼스 +- 출력: raw: 원문, khaiii: khaiii 바이너리 프로그램의 출력 포맷 +__author__ = 'Jamie (jamie.lim@kakaocorp.com)' +__copyright__ = 'Copyright (C) 2019-, Kakao Corp. All rights reserved.' +""" + + +########### +# imports # +########### +from argparse import ArgumentParser, Namespace +import logging +import random +import sys +from typing import List + +from khaiii.munjong.sejong_corpus import sents +from khaiii.resource.resource import Resource +from khaiii.train.dataset import PosDataset + + +######### +# types # +######### +class Sentence: + """ + sentence object + """ + def __init__(self): + self.words = [] + self.morphs = [] + + def merge_words(self, rate: float = 0.0): + """ + 어절을 무작위로 합친다. + Args: + rate: 비율 + """ + if rate <= 0.0: + return + idx = 0 + while idx < len(self.words)-1: + if random.random() >= rate: + idx += 1 + continue + self.words[idx] += self.words[idx+1] + self.morphs[idx] += ' + ' + self.morphs[idx+1] + del self.words[idx+1] + del self.morphs[idx+1] + + def __str__(self): + words_str = [f'{w}\t{m}' for w, m in zip(self.words, self.morphs)] + return '\n'.join(words_str) + '\n' + + def raw(self): + """ + raw text + Returns: + raw text in sentence + """ + return ' '.join(self.words) + + @classmethod + def load_sejong(cls) -> List['Sentence']: + """ + load from Sejong corpus + Returns: + list of sentences + """ + sentences = [] + for sent in sents(sys.stdin): + sentence = Sentence() + for word in sent.words: + sentence.words.append(word.raw) + sentence.morphs.append(' + '.join([str(m) for m in word.morphs])) + sentences.append(sentence) + return sentences + + @classmethod + def load_train(cls, rsc_src: str) -> List['Sentence']: + """ + load from khaiii training set + Returns: + list of sentences + """ + restore_dic = Resource.load_restore_dic(f'{rsc_src}/restore.dic') + sentences = [] + for sent in PosDataset(None, restore_dic, sys.stdin): + sentence = Sentence() + for word in sent.pos_tagged_words: + sentence.words.append(word.raw) + sentence.morphs.append(' + '.join([str(m) for m in word.pos_tagged_morphs])) + sentences.append(sentence) + return sentences + + +############# +# functions # +############# +def run(args: Namespace): + """ + run function which is the start point of program + Args: + args: program arguments + """ + random.seed(args.seed) + + sentences = [] + if args.input_format == 'sejong': + sentences = Sentence.load_sejong() + elif args.input_format == 'train': + sentences = Sentence.load_train(args.rsc_src) + else: + raise ValueError(f'invalid input format: {args.input_format}') + + for sentence in sentences: + sentence.merge_words(args.merge_rate) + if args.output_format == 'raw': + print(sentence.raw()) + elif args.output_format == 'khaiii': + print(str(sentence)) + else: + raise ValueError(f'invalid output format: {args.output_format}') + + +######## +# main # +######## +def main(): + """ + main function processes only argument parsing + """ + parser = ArgumentParser(description='세종 코퍼스와 khaiii 학습 코퍼스를 원하는 형태로 변환하는 스크립트') + parser.add_argument('-i', '--input-format', help='input format (sejong, train)', metavar='FMT', + required=True) + parser.add_argument('-o', '--output-format', help='output format (raw, khaiii)', metavar='FMT', + required=True) + parser.add_argument('--rsc-src', help='resource source dir ', + metavar='DIR', default='../rsc/src') + parser.add_argument('--input', help='input file ', metavar='FILE', ) + parser.add_argument('--output', help='output file ', metavar='FILE') + parser.add_argument('--seed', help='random seed ', metavar='NUM', type=int, + default=1234) + parser.add_argument('--merge-rate', help='word merge rate', metavar='REAL', type=float, + default=0.0) + parser.add_argument('--debug', help='enable debug', action='store_true') + args = parser.parse_args() + + if args.input: + sys.stdin = open(args.input, 'r', encoding='UTF-8') + if args.output: + sys.stdout = open(args.output, 'w', encoding='UTF-8') + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + run(args) + + +if __name__ == '__main__': + main() diff --git a/train/validate_errpatch.py b/train/validate_errpatch.py index ed73271..d81fe18 100755 --- a/train/validate_errpatch.py +++ b/train/validate_errpatch.py @@ -25,7 +25,7 @@ from khaiii.munjong.sejong_corpus import Sentence, sents from khaiii.resource.char_align import Aligner, AlignError, align_patch, align_to_tag from khaiii.resource.morphs import mix_char_tag, WORD_DELIM_NUM, SENT_DELIM_NUM -from khaiii.resource.resource import load_restore_dic, load_vocab_out +from khaiii.resource.resource import load_vocab_out, parse_restore_dic ######### @@ -198,7 +198,7 @@ def run(args: Namespace): args: program arguments """ aligner = Aligner(args.rsc_src) - restore_dic = load_restore_dic('{}/restore.dic'.format(args.rsc_src)) + restore_dic = parse_restore_dic('{}/restore.dic'.format(args.rsc_src)) if not restore_dic: sys.exit(1) vocab_out = load_vocab_out(args.rsc_src)