From 729566b80e044e95feed4d38cee5762bf5afb955 Mon Sep 17 00:00:00 2001 From: bab2min Date: Mon, 8 Apr 2024 01:23:33 +0900 Subject: [PATCH 1/8] implemented continual typo tolerance --- include/kiwi/Kiwi.h | 8 +- include/kiwi/TypoTransformer.h | 50 ++- src/KTrie.cpp | 649 ++++++++++++++++++++++++++------- src/KTrie.h | 18 +- src/Kiwi.cpp | 6 +- src/KiwiBuilder.cpp | 5 +- src/TypoTransformer.cpp | 259 ++++++++----- 7 files changed, 747 insertions(+), 248 deletions(-) diff --git a/include/kiwi/Kiwi.h b/include/kiwi/Kiwi.h index 58248402..e052cbaf 100644 --- a/include/kiwi/Kiwi.h +++ b/include/kiwi/Kiwi.h @@ -68,6 +68,7 @@ namespace kiwi float unkFormScoreBias = 5; float spacePenalty = 7; float typoCostWeight = 6; + float continualTypoCost = INFINITY; size_t maxUnkFormSize = 6; size_t spaceTolerance = 0; @@ -126,7 +127,7 @@ namespace kiwi * @note 이 생성자는 기본 생성자로 이를 통해 생성된 객체는 바로 형태소 분석에 사용할 수 없다. * kiwi::KiwiBuilder 를 통해 생성된 객체만이 형태소 분석에 사용할 수 있다. */ - Kiwi(ArchType arch = ArchType::default_, LangModel _langMdl = {}, bool typoTolerant = false); + Kiwi(ArchType arch = ArchType::default_, LangModel _langMdl = {}, bool typoTolerant = false, bool continualTypoTolerant = false); ~Kiwi(); @@ -781,6 +782,11 @@ namespace kiwi */ Kiwi build(const TypoTransformer& typos = {}, float typoCostThreshold = 2.5f) const; + Kiwi build(DefaultTypoSet typos, float typoCostThreshold = 2.5f) const + { + return build(getDefaultTypoSet(typos), typoCostThreshold); + } + using TokenFilter = std::function; HSDataset makeHSDataset(const std::vector& inputPathes, diff --git a/include/kiwi/TypoTransformer.h b/include/kiwi/TypoTransformer.h index 1650c62b..550eaa06 100644 --- a/include/kiwi/TypoTransformer.h +++ b/include/kiwi/TypoTransformer.h @@ -164,6 +164,7 @@ namespace kiwi utils::FrozenTrie patTrie; KString strPool; Vector replacements; + float continualTypoThreshold = INFINITY; template TypoCandidates _generate(const KString& orig, float costThreshold = 2.5f) const; @@ -178,6 +179,11 @@ namespace kiwi PreparedTypoTransformer& operator=(PreparedTypoTransformer&&); bool ready() const { return !replacements.empty(); } + + float getContinualTypoCost() const + { + return continualTypoThreshold; + } TypoCandidates generate(const std::u16string& orig, float costThreshold = 2.5f) const; }; @@ -203,6 +209,7 @@ namespace kiwi utils::ContinuousTrie patTrie; KString strPool; Vector> replacements; + float continualTypoThreshold = INFINITY; void addTypoImpl(const KString& orig, const KString& error, float cost, CondVowel leftCond = CondVowel::none); void addTypoWithCond(const KString& orig, const KString& error, float cost, CondVowel leftCond = CondVowel::none); @@ -212,19 +219,11 @@ namespace kiwi using TypoDef = std::tuple, std::initializer_list, float, CondVowel>; TypoTransformer(); + TypoTransformer(std::initializer_list lst) : TypoTransformer() { - for (auto& l : lst) - { - for (auto i : std::get<0>(l)) - { - for (auto o : std::get<1>(l)) - { - addTypo(i, o, std::get<2>(l), std::get<3>(l)); - } - } - } + addTypos(lst); } ~TypoTransformer(); @@ -233,18 +232,45 @@ namespace kiwi TypoTransformer& operator=(const TypoTransformer&); TypoTransformer& operator=(TypoTransformer&&); + bool isContinualTypoEnabled() const; + bool empty() const { - return replacements.empty(); + return replacements.empty() && !isContinualTypoEnabled(); } void addTypo(const std::u16string& orig, const std::u16string& error, float cost = 1, CondVowel leftCond = CondVowel::none); + TypoTransformer& addTypos(std::initializer_list lst) + { + for (auto& l : lst) + { + for (auto i : std::get<0>(l)) + { + for (auto o : std::get<1>(l)) + { + addTypo(i, o, std::get<2>(l), std::get<3>(l)); + } + } + } + return *this; + } + + TypoTransformer copyWithNewContinualTypoCost(float threshold) const; + PreparedTypoTransformer prepare() const { return { *this }; } }; - extern const TypoTransformer withoutTypo, basicTypoSet; + enum class DefaultTypoSet + { + withoutTypo, + basicTypoSet, + continualTypoSet, + basicTypoSetWithContinual, + }; + + const TypoTransformer& getDefaultTypoSet(DefaultTypoSet set); } diff --git a/src/KTrie.cpp b/src/KTrie.cpp index 4b12b307..b7a67bb4 100644 --- a/src/KTrie.cpp +++ b/src/KTrie.cpp @@ -13,7 +13,7 @@ using namespace kiwi; namespace kiwi { template - inline bool appendNewNode(Vector& nodes, Vector>& endPosMap, size_t startPos, Args&&... args) + inline bool appendNewNode(Vector& nodes, Vector>& endPosMap, size_t startPos, size_t endPos, Args&&... args) { static constexpr uint32_t npos = -1; @@ -23,9 +23,8 @@ namespace kiwi } size_t newId = nodes.size(); - nodes.emplace_back(forward(args)...); + nodes.emplace_back(startPos, endPos, forward(args)...); auto& nnode = nodes.back(); - nnode.startPos = startPos; nnode.prev = newId - endPosMap[startPos].first; if (nnode.endPos >= endPosMap.size()) return true; @@ -43,15 +42,88 @@ namespace kiwi return true; } - struct TypoCostInfo + template + struct FormCandidate { - float cost; - uint32_t start; - uint32_t typoId; + const Form* form = nullptr; + float cost = 0; + uint32_t start = 0; + uint32_t typoId = 0; + uint32_t end = 0; // only used in continual typo tolerant mode - TypoCostInfo(float _cost = 0, uint32_t _start = 0, uint32_t _typoId = 0) - : cost{ _cost }, start{ _start }, typoId{ _typoId } + FormCandidate(const Form* _form = nullptr, float _cost = 0, uint32_t _start = 0, uint32_t _typoId = 0, uint32_t _end = 0) + : form{ _form }, cost{ _cost }, start{ _start }, typoId{ _typoId }, end{ _end } {} + + size_t getStartPos(size_t ) const + { + return start; + } + + size_t getEndPos(size_t currentPos) const + { + return end ? end : currentPos; + } + + float getTypoCost() const + { + return cost; + } + + uint32_t getTypoId() const + { + return typoId; + } + + size_t getFormSizeWithTypos(const size_t* typoPtrs) const + { + return typoPtrs[typoId + 1] - typoPtrs[typoId]; + } + + bool operator==(const Form* f) const + { + return form == f; + } + }; + + template<> + struct FormCandidate + { + const Form* form = nullptr; + + FormCandidate(const Form* _form = nullptr, float = 0, uint32_t = 0, uint32_t = 0, uint32_t = 0) + : form{ _form } + {} + + size_t getStartPos(size_t currentPos) const + { + return currentPos - form->sizeWithoutSpace(); + } + + size_t getEndPos(size_t currentPos) const + { + return currentPos; + } + + float getTypoCost() const + { + return 0; + } + + uint32_t getTypoId() const + { + return 0; + } + + size_t getFormSizeWithTypos(const size_t*) const + { + return form->form.size(); + } + + bool operator==(const Form* f) const + { + return form == f; + } }; template @@ -71,17 +143,20 @@ namespace kiwi } } - template + template bool insertCandidates( - Vector& candidates, - Vector& candTypoCostStarts, + Vector>& candidates, const Form* foundCand, const Form* formBase, const size_t* typoPtrs, U16StringView str, - const Vector& nonSpaces + const Vector& nonSpaces, + uint32_t startPosition = 0, + uint32_t endPosition = 0, + float cost = 0 ) { + static constexpr size_t posMultiplier = continualTypoTolerant ? 4 : 1; if (typoTolerant) { auto tCand = reinterpret_cast(foundCand); @@ -89,13 +164,12 @@ namespace kiwi while (1) { - auto typoFormSize = typoPtrs[tCand->typoId + 1] - typoPtrs[tCand->typoId]; + const auto typoFormSize = typoPtrs[tCand->typoId + 1] - typoPtrs[tCand->typoId]; auto cand = &tCand->form(formBase); if (FeatureTestor::isMatched(&str[0], &str[nonSpaces[nonSpaces.size() - typoFormSize]], tCand->leftCond) && FeatureTestor::isMatchedApprox(&str[0], &str[nonSpaces[nonSpaces.size() - typoFormSize]], cand->vowel, cand->polar)) { - candidates.emplace_back(cand); - candTypoCostStarts.emplace_back(tCand->score(), nonSpaces.size() - typoFormSize, tCand->typoId); + candidates.emplace_back(cand, tCand->score() + cost, startPosition ? startPosition : ((nonSpaces.size() - typoFormSize) * posMultiplier), tCand->typoId, endPosition); } if (tCand[0].hash() != tCand[1].hash()) break; ++tCand; @@ -118,6 +192,23 @@ namespace kiwi return true; } + template + size_t getFormLength( + const Form* form, + const Form* formBase + ) + { + if (typoTolerant) + { + auto tCand = reinterpret_cast(form); + return tCand->form(formBase).form.size(); + } + else + { + return form->form.size(); + } + } + inline void removeUnconnected(Vector& ret, const Vector& graph, const Vector>& endPosMap) { thread_local Vector connectedList; @@ -142,8 +233,8 @@ namespace kiwi if (graph[i].endPos != node.startPos) continue; if (connectedList[i]) continue; updateList.emplace_back(i); + connectedList[i] = 1; } - fill(connectedList.begin() + scanStart, connectedList.begin() + scanEnd, 1); } size_t connectedCnt = accumulate(connectedList.begin(), connectedList.end(), 0); @@ -172,38 +263,271 @@ namespace kiwi } } -} + // nonSpaces idx 데이터로부터 글자 수 + 공백 블록 수를 계산한다. + template + inline size_t countChrWithNormalizedSpace(It first, It last) + { + size_t n = std::distance(first, last); + auto prevIdx = *first++; + for (; first != last; ++first) + { + if (*first != prevIdx + 1) ++n; + prevIdx = *first; + } + return n; + } -// nonSpaces idx 데이터로부터 글자 수 + 공백 블록 수를 계산한다. -template -inline size_t countChrWithNormalizedSpace(It first, It last) -{ - size_t n = std::distance(first, last); - auto prevIdx = *first++; - for (; first != last; ++first) + // 공백 문자의 위치가 형태소의 공백 위치와 불일치하는 개수를 센다. + inline size_t countSpaceErrors(const KString& form, const uint32_t* spaceIdxFirst, const uint32_t* spaceIdxLast) { - if (*first != prevIdx + 1) ++n; - prevIdx = *first; + size_t n = 0; + size_t spaceOffset = 0; + const size_t size = std::distance(spaceIdxFirst, spaceIdxLast); + for (size_t i = 1; i < size; ++i) + { + const bool hasSpace = spaceIdxFirst[i] - spaceIdxFirst[i - 1] > 1; + if (hasSpace && form[i + spaceOffset] != u' ') ++n; + spaceOffset += form[i + spaceOffset] == u' ' ? 1 : 0; + } + return n; } - return n; -} -// 공백 문자의 위치가 형태소의 공백 위치와 불일치하는 개수를 센다. -inline size_t countSpaceErrors(const KString& form, const uint32_t* spaceIdxFirst, const uint32_t* spaceIdxLast) -{ - size_t n = 0; - size_t spaceOffset = 0; - const size_t size = std::distance(spaceIdxFirst, spaceIdxLast); - for (size_t i = 1; i < size; ++i) + // onset: ㅇ=11, ㅎ=18 + inline char16_t overrideOnset(char16_t c, const int onset = 11) + { + if (!isHangulSyllable(c)) return 0; + const int vowel = (c - 0xAC00) / 28 % 21; + const int coda = (c - 0xAC00) % 28; + return 0xAC00 + onset * 28 * 21 + vowel * 28 + coda; + } + + // 받침 + 초성 ㅇ이 연철된 경우 + struct ContinualIeungDecomposer + { + static constexpr size_t boundaryId = 1; + char16_t onsetToCoda(char16_t c) + { + static constexpr char16_t onsetToCoda[] = { + 0x11A8, // ㄱ + 0x11A9, // ㄲ + 0x11AB, // ㄴ + 0x11AE, // ㄷ + 0, // ㄸ + 0x11AF, // ㄹ + 0x11B7, // ㅁ + 0x11B8, // ㅂ + 0, // ㅃ + 0x11BA, // ㅅ + 0x11BB, // ㅆ + 0, // ㅇ + 0x11BD, // ㅈ + 0, // ㅉ + 0x11BE, // ㅊ + 0x11BF, // ㅋ + 0x11C0, // ㅌ + 0x11C1, // ㅍ + 0x11C2, // ㅎ + }; + + if (isHangulSyllable(c)) + { + int onset = (c - 0xAC00) / 28 / 21; + return onsetToCoda[onset]; + } + + switch (c) + { + case u'ㄱ': return 0x11A8; + case u'ㄲ': return 0x11A9; + case u'ㄴ': return 0x11AB; + case u'ㄷ': return 0x11AE; + case u'ㄹ': return 0x11AF; + case u'ㅁ': return 0x11B7; + case u'ㅂ': return 0x11B8; + case u'ㅅ': return 0x11BA; + case u'ㅆ': return 0x11BB; + case u'ㅈ': return 0x11BD; + case u'ㅊ': return 0x11BE; + case u'ㅋ': return 0x11BF; + case u'ㅌ': return 0x11C0; + case u'ㅍ': return 0x11C1; + case u'ㅎ': return 0x11C2; + default: return 0; + } + + return 0; + } + + char16_t dropRightSyllable(char16_t c) + { + return overrideOnset(c, 11); + } + }; + + // 받침 + 초성 ㅎ이 연철된 경우 + struct ContinualHieutDecomposer + { + static constexpr size_t boundaryId = 2; + char16_t onsetToCoda(char16_t c) + { + static constexpr char16_t onsetToCoda[] = { + 0, // ㄱ + 0, // ㄲ + 0x11AB, // ㄴ + 0, // ㄷ + 0, // ㄸ + 0x11AF, // ㄹ + 0x11B7, // ㅁ + 0, // ㅂ + 0, // ㅃ + 0x11BA, // ㅅ + 0, // ㅆ + 0, // ㅇ + 0, // ㅈ + 0, // ㅉ + 0x11BD, // ㅊ->ㅈ + 0x11A8, // ㅋ->ㄱ + 0x11AE, // ㅌ->ㄷ + 0x11B8, // ㅍ->ㅂ + 0, // ㅎ + }; + + if (isHangulSyllable(c)) + { + int onset = (c - 0xAC00) / 28 / 21; + return onsetToCoda[onset]; + } + return 0; + } + + char16_t dropRightSyllable(char16_t c) + { + return overrideOnset(c, 18); + } + }; + + // 받침 ㅎ + ㅎ이 아닌 초성이 연철된 경우 + struct ContinualCodaDecomposer { - const bool hasSpace = spaceIdxFirst[i] - spaceIdxFirst[i - 1] > 1; - if (hasSpace && form[i + spaceOffset] != u' ') ++n; - spaceOffset += form[i + spaceOffset] == u' ' ? 1 : 0; + static constexpr size_t boundaryId = 3; + char16_t onsetToCoda(char16_t c) + { + static constexpr char16_t onsetToCoda[] = { + 0, // ㄱ + 0, // ㄲ + 0, // ㄴ + 0, // ㄷ + 0, // ㄸ + 0, // ㄹ + 0, // ㅁ + 0, // ㅂ + 0, // ㅃ + 0, // ㅅ + 0, // ㅆ + 0, // ㅇ + 0, // ㅈ + 0, // ㅉ + 0x11C2, // ㅊ->ㅎ + 0x11C2, // ㅋ->ㅎ + 0x11C2, // ㅌ->ㅎ + 0x11C2, // ㅍ->ㅎ + 0, // ㅎ + }; + + if (isHangulSyllable(c)) + { + int onset = (c - 0xAC00) / 28 / 21; + return onsetToCoda[onset]; + } + return 0; + } + + char16_t dropRightSyllable(char16_t c) + { + const int onset = (c - 0xAC00) / 28 / 21; + const int vowel = (c - 0xAC00) / 28 % 21; + const int coda = (c - 0xAC00) % 28; + static constexpr char16_t onsetMap[] = { + 0, // ㄱ + 0, // ㄲ + 0, // ㄴ + 0, // ㄷ + 0, // ㄸ + 0, // ㄹ + 0, // ㅁ + 0, // ㅂ + 0, // ㅃ + 0, // ㅅ + 0, // ㅆ + 0, // ㅇ + 0, // ㅈ + 0, // ㅉ + 12, // ㅊ->ㅈ + 0, // ㅋ->ㄱ + 3, // ㅌ->ㄷ + 7, // ㅍ->ㅂ + 0, // ㅎ + }; + return 0xAC00 + (onsetMap[onset] * 21 + vowel) * 28 + coda; + } + }; + + template + inline void insertContinualTypoNode( + Vector>& candidates, + Vector::Node*>>& continualTypoRightNodes, + Decomposer decomposer, + float continualTypoCost, + char16_t c, + const Form* formBase, + const size_t* typoPtrs, + const utils::FrozenTrie& trie, + U16StringView str, + const Vector& nonSpaces, + const utils::FrozenTrie::Node* curNode + ) + { + if (!continualTypoTolerant) return; + static constexpr size_t posMultiplier = continualTypoTolerant ? 4 : 1; + + char16_t codaFromContinual = decomposer.onsetToCoda(c), + droppedSyllable = decomposer.dropRightSyllable(c); + if (!codaFromContinual || !droppedSyllable) return; + + const auto boundary = (nonSpaces.size() - 1) * posMultiplier + Decomposer::boundaryId; + bool inserted = false; + auto* contNode = curNode->template nextOpt(trie, codaFromContinual); + while (!contNode) + { + curNode = curNode->fail(); + if (!curNode) break; + contNode = curNode->template nextOpt(trie, codaFromContinual); + } + + if (!contNode) return; + + for (auto submatcher = contNode; submatcher; submatcher = submatcher->fail()) + { + const Form* cand = submatcher->val(trie); + if (!cand) break; + else if (!trie.hasSubmatch(cand)) + { + if (getFormLength(cand, formBase) <= 1) break; + inserted = true; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces, 0, boundary, continualTypoCost / 2)) break; + } + } + + if (!inserted) return; + + if (auto* dropNode = trie.root()->template nextOpt(trie, droppedSyllable)) + { + continualTypoRightNodes.emplace_back(boundary, dropNode); + } } - return n; } -template +template size_t kiwi::splitByTrie( Vector& ret, const Form* formBase, @@ -214,11 +538,27 @@ size_t kiwi::splitByTrie( Match matchOptions, size_t maxUnkFormSize, size_t spaceTolerance, - float typoCostWeight, + float continualTypoCost, const PretokenizedSpanGroup::Span*& pretokenizedFirst, const PretokenizedSpanGroup::Span* pretokenizedLast ) { + /* + * posMultiplier는 연철 교정 모드(continualTypoTolerant)에서 사용된다. + * 이 경우 음절 경계로 분할되는 형태소들은 모두 4의 배수로 인덱싱되고 + * 연철되어 두 음절에 걸쳐 있는 형태소들은 4n + 1, 2, 3으로 인덱싱된다. + * 4n + 1: 받침 + 초성 ㅇ이 연철되어 결합한 경우(ex: 사람이 -> 사라미) + * 4n + 2: 받침 + 초성 ㅎ이 연철되어 결합한 경우(ex: 급하다 -> 그파다) + * 4n + 3: ㅎ 받침 + ㅎ이 아닌 초성이 연철되어 결합한 경우(ex: 않다 -> 안타) + * + * 연철 교정 모드가 사용되지 않을 경우 + * '사라ㅁ이'에서 형태소 '사라ㅁ'과 '이'의 (시작, 끝지점)은 각각 (0, 3), (3, 4)가 된다. + * 연철 교정 모드가 사용될 경우 + * '사라ㅁ이'에서 형태소 '사라ㅁ'과 '이'의 (시작, 끝지점)은 각각 (0, 12), (12, 16)가 된다. + * 그리고 '사라미'에서는 형태소 '사라ㅁ'은 (0, 9), '이'는 (9, 12)가 된다. + */ + static constexpr size_t posMultiplier = continualTypoTolerant ? 4 : 1; + /* * endPosMap[i]에는 out[x].endPos == i를 만족하는 첫번째 x(first)와 마지막 x + 1(second)가 들어 있다. * first == second인 경우 endPos가 i인 노드가 없다는 것을 의미한다. @@ -226,41 +566,45 @@ size_t kiwi::splitByTrie( */ thread_local Vector> endPosMap; endPosMap.clear(); - endPosMap.resize(str.size() + 1, make_pair(-1, -1)); + endPosMap.resize(str.size() * posMultiplier + 1, make_pair(-1, -1)); endPosMap[0] = make_pair(0, 1); thread_local Vector nonSpaces; nonSpaces.clear(); - nonSpaces.reserve(str.size()); + nonSpaces.reserve(str.size() * posMultiplier); thread_local Vector out; out.clear(); out.emplace_back(); size_t n = 0; - Vector candidates; - Vector candTypoCostStarts; + Vector> candidates; auto* curNode = trie.root(); auto* nextNode = trie.root(); - + Vector> continualTypoRightNodes; + size_t lastSpecialEndPos = 0, specialStartPos = 0; POSTag chrType, lastChrType = POSTag::unknown, lastMatchedPattern = POSTag::unknown; - auto branchOut = [&](size_t unkFormEndPos = 0, size_t unkFormEndPosWithSpace = 0, bool specialMatched = false) + auto flushBranch = [&](size_t unkFormEndPos = 0, size_t unkFormEndPosWithSpace = 0, bool specialMatched = false) { if (!candidates.empty()) { bool alreadySpecialChrProcessed = false; for (auto& cand : candidates) { - const size_t nBegin = typoTolerant ? candTypoCostStarts[&cand - candidates.data()].start : (nonSpaces.size() - cand->sizeWithoutSpace()); - const auto scanStart = max(endPosMap[nBegin].first, (uint32_t)1), scanEnd = endPosMap[nBegin].second; + const size_t nBegin = cand.getStartPos(nonSpaces.size() * posMultiplier) / posMultiplier, + nBeginWithMultiplier = cand.getStartPos(nonSpaces.size() * posMultiplier), + nEndWithMultiplier = cand.getEndPos(nonSpaces.size() * posMultiplier); + const auto scanStart = max(endPosMap[nBeginWithMultiplier].first, (uint32_t)1), scanEnd = endPosMap[nBeginWithMultiplier].second; const bool longestMatched = scanStart < scanEnd && any_of(out.begin() + scanStart, out.begin() + scanEnd, [&](const KGraphNode& g) { - return nBegin == g.endPos && lastSpecialEndPos == g.endPos - (g.uform.empty() ? g.form->sizeWithoutSpace() : g.uform.size()); + return nBeginWithMultiplier == g.endPos && lastSpecialEndPos == g.endPos - (g.uform.empty() ? g.form->sizeWithoutSpace() : g.uform.size()) * posMultiplier; }); // insert unknown form - if (nBegin > lastSpecialEndPos && !longestMatched - && !isHangulCoda(cand->form[0])) + if (nBeginWithMultiplier % posMultiplier == 0 + && nEndWithMultiplier % posMultiplier == 0 + && nBegin > lastSpecialEndPos && !longestMatched + && !isHangulCoda(cand.form->form[0])) { { size_t lastPos = out.back().endPos; @@ -270,7 +614,9 @@ size_t kiwi::splitByTrie( if (lastPos && isHangulCoda(str[nonSpaces[lastPos]])) lastPos--; // prevent coda to be matched alone. if (lastPos != lastSpecialEndPos) { - appendNewNode(out, endPosMap, lastPos, str.substr(nonSpaces[lastPos], nonSpaces[nBegin] - nonSpaces[lastPos]), (uint16_t)nBegin); + appendNewNode(out, endPosMap, + lastPos * posMultiplier, nBegin * posMultiplier, + str.substr(nonSpaces[lastPos], nonSpaces[nBegin] - nonSpaces[lastPos])); } } } @@ -278,19 +624,23 @@ size_t kiwi::splitByTrie( const size_t newNodeLength = nBegin - lastSpecialEndPos; if (maxUnkFormSize && newNodeLength <= maxUnkFormSize) { - appendNewNode(out, endPosMap, lastSpecialEndPos, str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[nBegin] - nonSpaces[lastSpecialEndPos]), (uint16_t)nBegin); + appendNewNode(out, endPosMap, + lastSpecialEndPos * posMultiplier, nBegin * posMultiplier, + str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[nBegin] - nonSpaces[lastSpecialEndPos])); } } // if special character - if (cand->candidate[0] <= trie.value((size_t)POSTag::sn)->candidate[0]) + if (cand.form->candidate[0] <= trie.value((size_t)POSTag::sn)->candidate[0]) { // special character should be processed one by one chr. if (!alreadySpecialChrProcessed) { - if (appendNewNode(out, endPosMap, nonSpaces.size() - 1, U16StringView{ cand->form.data() + cand->form.size() - 1, 1 }, (uint16_t)nonSpaces.size())) + if (appendNewNode(out, endPosMap, + (nonSpaces.size() - 1) * posMultiplier, nEndWithMultiplier, + U16StringView{ cand.form->form.data() + cand.form->form.size() - 1, 1 })) { - out.back().form = trie.value((size_t)cand->candidate[0]->tag); + out.back().form = trie.value((size_t)cand.form->candidate[0]->tag); } lastSpecialEndPos = nonSpaces.size(); alreadySpecialChrProcessed = true; @@ -298,46 +648,51 @@ size_t kiwi::splitByTrie( } else { - // TO DO: 아래의 spaceErrors 계산방식은 오타 교정 모드에서는 부정확한 값을 낼 수 있음. 더 정교한 방식으로 개선 필요 const size_t lengthWithSpaces = countChrWithNormalizedSpace(nonSpaces.begin() + nBegin, nonSpaces.end()); + const size_t formSizeWithTypos = cand.getFormSizeWithTypos(typoPtrs); size_t spaceErrors = 0; - if (lengthWithSpaces <= cand->form.size() + spaceTolerance - && (!cand->numSpaces || (spaceErrors = countSpaceErrors(cand->form, nonSpaces.data() + nBegin, nonSpaces.data() + nonSpaces.size())) <= spaceTolerance)) + if (lengthWithSpaces <= formSizeWithTypos + spaceTolerance + && (!cand.form->numSpaces || (spaceErrors = countSpaceErrors(cand.form->form, nonSpaces.data() + nBegin, nonSpaces.data() + nonSpaces.size())) <= spaceTolerance)) { - if (!cand->numSpaces && lengthWithSpaces > cand->form.size()) spaceErrors = lengthWithSpaces - cand->form.size(); - const float typoCost = typoTolerant ? candTypoCostStarts[&cand - candidates.data()].cost : 0.f; - if (appendNewNode(out, endPosMap, nBegin, cand, (uint16_t)nonSpaces.size(), typoCost)) + if (!cand.form->numSpaces && lengthWithSpaces > formSizeWithTypos) spaceErrors = lengthWithSpaces - formSizeWithTypos; + const float typoCost = cand.getTypoCost(); + if (appendNewNode(out, endPosMap, + nBeginWithMultiplier, nEndWithMultiplier, + cand.form, typoCost)) { out.back().spaceErrors = spaceErrors; if (typoTolerant) { - out.back().typoFormId = candTypoCostStarts[&cand - candidates.data()].typoId; + out.back().typoFormId = cand.getTypoId(); } } } } } candidates.clear(); - if (typoTolerant) candTypoCostStarts.clear(); } else if (out.size() > 1 && !specialMatched) { size_t lastPos = out.back().endPos; if (lastPos < unkFormEndPos && !isHangulCoda(str[nonSpaces[lastPos]])) { - appendNewNode(out, endPosMap, lastPos, str.substr(nonSpaces[lastPos], unkFormEndPosWithSpace - nonSpaces[lastPos]), (uint16_t)unkFormEndPos); + appendNewNode(out, endPosMap, + lastPos * posMultiplier, unkFormEndPos * posMultiplier, + str.substr(nonSpaces[lastPos], unkFormEndPosWithSpace - nonSpaces[lastPos])); } } - const auto scanStart = max(endPosMap[unkFormEndPos].first, (uint32_t)1), scanEnd = endPosMap[unkFormEndPos].second; + const auto scanStart = max(endPosMap[unkFormEndPos * posMultiplier].first, (uint32_t)1), scanEnd = endPosMap[unkFormEndPos * posMultiplier].second; const bool duplicated = scanStart < scanEnd && any_of(out.begin() + scanStart, out.begin() + scanEnd, [&](const KGraphNode& g) { - size_t startPos = g.endPos - (g.uform.empty() ? g.form->sizeWithoutSpace() : g.uform.size()); - return startPos == lastSpecialEndPos && g.endPos == unkFormEndPos; + size_t startPos = g.endPos - (g.uform.empty() ? g.form->sizeWithoutSpace() : g.uform.size()) * posMultiplier; + return startPos == lastSpecialEndPos * posMultiplier && g.endPos == unkFormEndPos * posMultiplier; }); if (unkFormEndPos > lastSpecialEndPos && !duplicated) { - appendNewNode(out, endPosMap, lastSpecialEndPos, str.substr(nonSpaces[lastSpecialEndPos], unkFormEndPosWithSpace - nonSpaces[lastSpecialEndPos]), (uint16_t)unkFormEndPos); + appendNewNode(out, endPosMap, + lastSpecialEndPos * posMultiplier, unkFormEndPos * posMultiplier, + str.substr(nonSpaces[lastSpecialEndPos], unkFormEndPosWithSpace - nonSpaces[lastSpecialEndPos])); } }; @@ -361,7 +716,9 @@ size_t kiwi::splitByTrie( // sequence of speical characters found if (lastChrType != POSTag::max && !isWebTag(lastChrType)) { - if (appendNewNode(out, endPosMap, specialStartPos, U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] }, (uint16_t)nonSpaces.size())) + if (appendNewNode(out, endPosMap, + specialStartPos * posMultiplier, nonSpaces.size() * posMultiplier, + U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] })) { out.back().form = trie.value((size_t)lastChrType); } @@ -371,8 +728,10 @@ size_t kiwi::splitByTrie( } uint32_t length = pretokenizedFirst->end - pretokenizedFirst->begin; - branchOut(nonSpaces.size(), n); - if (appendNewNode(out, endPosMap, nonSpaces.size(), pretokenizedFirst->form, nonSpaces.size() + length)) + flushBranch(nonSpaces.size(), n); + if (appendNewNode(out, endPosMap, + nonSpaces.size() * posMultiplier, (nonSpaces.size() + length) * posMultiplier, + pretokenizedFirst->form)) { if (within(pretokenizedFirst->form, fallbackFormBegin, fallbackFormEnd)) { @@ -401,7 +760,9 @@ size_t kiwi::splitByTrie( // sequence of speical characters found if (lastChrType != POSTag::max && !isWebTag(lastChrType)) { - if (appendNewNode(out, endPosMap, specialStartPos, U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] }, (uint16_t)nonSpaces.size())) + if (appendNewNode(out, endPosMap, + specialStartPos * posMultiplier, nonSpaces.size() * posMultiplier, + U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] })) { out.back().form = trie.value((size_t)lastChrType); } @@ -413,7 +774,7 @@ size_t kiwi::splitByTrie( size_t patStart = nonSpaces.size(); for (size_t i = 0; i < m.first; ++i) { - branchOut(nonSpaces.size(), n + i, i > 0); + flushBranch(nonSpaces.size(), n + i, i > 0); nextNode = curNode->template nextOpt(trie, str[n + i]); while (!nextNode) // if curNode has no exact next node, goto fail { @@ -426,7 +787,7 @@ size_t kiwi::splitByTrie( if (!cand) break; else if (!trie.hasSubmatch(cand)) { - if (!insertCandidates(candidates, candTypoCostStarts, cand, formBase, typoPtrs, str, nonSpaces)) break; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces)) break; } } nextNode = curNode->template nextOpt(trie, str[n + i]); @@ -447,14 +808,16 @@ size_t kiwi::splitByTrie( if (!cand) break; else if (!trie.hasSubmatch(cand)) { - if (!insertCandidates(candidates, candTypoCostStarts, cand, formBase, typoPtrs, str, nonSpaces)) break; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces)) break; } } continuePatternFor:; } - branchOut(nonSpaces.size(), n + m.first, true); + flushBranch(nonSpaces.size(), n + m.first, true); - if (appendNewNode(out, endPosMap, patStart, U16StringView{ &str[n], m.first }, (uint16_t)(patStart + m.first))) + if (appendNewNode(out, endPosMap, + patStart * posMultiplier, (patStart + m.first) * posMultiplier, + U16StringView{ &str[n], m.first })) { out.back().form = trie.value((size_t)chrType); } @@ -478,19 +841,23 @@ size_t kiwi::splitByTrie( // sequence of speical characters found if (lastChrType != POSTag::max && lastChrType != POSTag::unknown && lastChrType != lastMatchedPattern) { - const auto scanStart = max(endPosMap[nonSpaces.size()].first, (uint32_t)1), scanEnd = endPosMap[nonSpaces.size()].second; + const auto scanStart = max(endPosMap[specialStartPos * posMultiplier].first, (uint32_t)1), scanEnd = endPosMap[specialStartPos * posMultiplier].second; const bool duplicated = scanStart < scanEnd && any_of(out.begin() + scanStart, out.begin() + scanEnd, [&](const KGraphNode& g) { - return nonSpaces.size() == g.endPos; + return specialStartPos * posMultiplier == g.endPos; }); if (nonSpaces.size() > lastSpecialEndPos && specialStartPos > lastSpecialEndPos && !duplicated) { - appendNewNode(out, endPosMap, lastSpecialEndPos, str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[specialStartPos] - nonSpaces[lastSpecialEndPos]), (uint16_t)specialStartPos); + appendNewNode(out, endPosMap, + lastSpecialEndPos * posMultiplier, specialStartPos * posMultiplier, + str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[specialStartPos] - nonSpaces[lastSpecialEndPos])); } if (lastChrType != POSTag::ss) // ss 태그는 morpheme 내에 등록된 후보에서 직접 탐색하도록 한다 { - if (appendNewNode(out, endPosMap, specialStartPos, U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] }, (uint16_t)nonSpaces.size())) + if (appendNewNode(out, endPosMap, + specialStartPos * posMultiplier, nonSpaces.size() * posMultiplier, + U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] })) { out.back().form = trie.value((size_t)lastChrType); } @@ -520,17 +887,18 @@ size_t kiwi::splitByTrie( // 공백문자를 무시하고 분할 진행 if (chrType == POSTag::unknown) { - branchOut(nonSpaces.size(), n); + flushBranch(nonSpaces.size(), n); lastSpecialEndPos = nonSpaces.size(); goto continueFor; } if (isOldHangulToneMark(c)) { - branchOut(nonSpaces.size(), n); + flushBranch(nonSpaces.size(), n); goto continueFor; } + auto* curNodeForContinualTypo = curNode; nextNode = curNode->template nextOpt(trie, c); while (!nextNode) // if curNode has no exact next node, goto fail { @@ -544,7 +912,7 @@ size_t kiwi::splitByTrie( else if (!trie.hasSubmatch(cand)) { zCodaFollowable = zCodaFollowable || getZCodaAppendable(cand, formBase); - if (!insertCandidates(candidates, candTypoCostStarts, cand, formBase, typoPtrs, str, nonSpaces)) break; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces)) break; } } nextNode = curNode->template nextOpt(trie, c); @@ -553,11 +921,11 @@ size_t kiwi::splitByTrie( { if (chrType != POSTag::max) { - branchOut(specialStartPos, specialStartPos < nonSpaces.size() ? nonSpaces[specialStartPos] : n); + flushBranch(specialStartPos, specialStartPos < nonSpaces.size() ? nonSpaces[specialStartPos] : n); } else { - branchOut(); + flushBranch(); } // spaceTolerance == 0이고 공백 문자인 경우 @@ -578,11 +946,7 @@ size_t kiwi::splitByTrie( if (!!(matchOptions & Match::zCoda) && zCodaFollowable && isHangulCoda(c) && (n + 1 >= str.size() || !isHangulSyllable(str[n + 1]))) { - candidates.emplace_back(formBase + defaultTagSize + (c - 0x11A8) - 1); - if (typoTolerant) - { - candTypoCostStarts.emplace_back(0, nonSpaces.size() - 1); - } + candidates.emplace_back(formBase + defaultTagSize + (c - 0x11A8) - 1, 0, nonSpaces.size() - 1); } zCodaFollowable = false; @@ -590,27 +954,45 @@ size_t kiwi::splitByTrie( } } + if (continualTypoTolerant) + { + size_t outputIdx = 0; + for (auto& rn : continualTypoRightNodes) + { + rn.second = rn.second->template nextOpt(trie, c); + if (!rn.second) continue; + continualTypoRightNodes[outputIdx++] = rn; + } + continualTypoRightNodes.resize(outputIdx); + } + if (chrType != POSTag::max) { - branchOut(specialStartPos, specialStartPos < nonSpaces.size() ? nonSpaces[specialStartPos] : n); + flushBranch(specialStartPos, specialStartPos < nonSpaces.size() ? nonSpaces[specialStartPos] : n); } else { - branchOut(); + flushBranch(); } nonSpaces.emplace_back(n); if (!!(matchOptions & Match::zCoda) && zCodaFollowable && isHangulCoda(c) && (n + 1 >= str.size() || !isHangulSyllable(str[n + 1]))) { - candidates.emplace_back(formBase + defaultTagSize + (c - 0x11A8) - 1); - if (typoTolerant) - { - candTypoCostStarts.emplace_back(0, nonSpaces.size() - 1); - } + candidates.emplace_back(formBase + defaultTagSize + (c - 0x11A8) - 1, 0, nonSpaces.size() - 1); } zCodaFollowable = false; + if (continualTypoTolerant && lastChrType == POSTag::max) + { + insertContinualTypoNode(candidates, continualTypoRightNodes, ContinualIeungDecomposer{}, + continualTypoCost, c, formBase, typoPtrs, trie, str, nonSpaces, curNodeForContinualTypo); + insertContinualTypoNode(candidates, continualTypoRightNodes, ContinualHieutDecomposer{}, + continualTypoCost, c, formBase, typoPtrs, trie, str, nonSpaces, curNodeForContinualTypo); + insertContinualTypoNode(candidates, continualTypoRightNodes, ContinualCodaDecomposer{}, + continualTypoCost, c, formBase, typoPtrs, trie, str, nonSpaces, curNodeForContinualTypo); + } + // from this, curNode has the exact next node curNode = nextNode; // if it has exit node, patterns have been found @@ -621,7 +1003,19 @@ size_t kiwi::splitByTrie( else if (!trie.hasSubmatch(cand)) { zCodaFollowable = zCodaFollowable || getZCodaAppendable(cand, formBase); - if (!insertCandidates(candidates, candTypoCostStarts, cand, formBase, typoPtrs, str, nonSpaces)) break; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces)) break; + } + } + + if (continualTypoTolerant) + { + for (auto& rn : continualTypoRightNodes) + { + const Form* cand = rn.second->val(trie); + if (cand && !trie.hasSubmatch(cand)) + { + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces, rn.first, 0, continualTypoCost / 2)) break; + } } } continueFor: @@ -631,16 +1025,20 @@ size_t kiwi::splitByTrie( // sequence of speical characters found if (lastChrType != POSTag::max && lastChrType != POSTag::unknown && !isWebTag(lastChrType)) { - const auto scanStart = max(endPosMap[nonSpaces.size()].first, (uint32_t)1), scanEnd = endPosMap[nonSpaces.size()].second; + const auto scanStart = max(endPosMap[specialStartPos * posMultiplier].first, (uint32_t)1), scanEnd = endPosMap[specialStartPos * posMultiplier].second; const bool duplicated = scanStart < scanEnd && any_of(out.begin() + scanStart, out.begin() + scanEnd, [&](const KGraphNode& g) { - return nonSpaces.size() == g.endPos; + return specialStartPos * posMultiplier == g.endPos; }); if (nonSpaces.size() > lastSpecialEndPos && specialStartPos > lastSpecialEndPos && !duplicated) { - appendNewNode(out, endPosMap, lastSpecialEndPos, str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[specialStartPos] - nonSpaces[lastSpecialEndPos]), (uint16_t)specialStartPos); + appendNewNode(out, endPosMap, + lastSpecialEndPos * posMultiplier, specialStartPos * posMultiplier, + str.substr(nonSpaces[lastSpecialEndPos], nonSpaces[specialStartPos] - nonSpaces[lastSpecialEndPos])); } - if (appendNewNode(out, endPosMap, specialStartPos, U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] }, (uint16_t)nonSpaces.size())) + if (appendNewNode(out, endPosMap, + specialStartPos * posMultiplier, nonSpaces.size() * posMultiplier, + U16StringView{ &str[nonSpaces[specialStartPos]], n - nonSpaces[specialStartPos] })) { out.back().form = trie.value((size_t)lastChrType); } @@ -653,14 +1051,15 @@ size_t kiwi::splitByTrie( if (curNode->val(trie) && !trie.hasSubmatch(curNode->val(trie))) { const Form* cand = curNode->val(trie); - if (!insertCandidates(candidates, candTypoCostStarts, cand, formBase, typoPtrs, str, nonSpaces)) break; + if (!insertCandidates(candidates, cand, formBase, typoPtrs, str, nonSpaces)) break; } curNode = curNode->fail(); } - branchOut(nonSpaces.size(), n); + flushBranch(nonSpaces.size(), n); - appendNewNode(out, endPosMap, nonSpaces.size(), nullptr, nonSpaces.size() + 1); - out.back().endPos = nonSpaces.size(); + appendNewNode(out, endPosMap, + nonSpaces.size() * posMultiplier, (nonSpaces.size() + 1) * posMultiplier, nullptr); + out.back().endPos = nonSpaces.size() * posMultiplier; nonSpaces.emplace_back(n); @@ -668,8 +1067,8 @@ size_t kiwi::splitByTrie( for (size_t i = 1; i < ret.size() - 1; ++i) { auto& r = ret[i]; - r.startPos = nonSpaces[r.startPos] + startOffset; - r.endPos = nonSpaces[r.endPos - 1] + 1 + startOffset; + r.startPos = nonSpaces[r.startPos / posMultiplier] + startOffset; + r.endPos = nonSpaces[(r.endPos + posMultiplier - 1) / posMultiplier - 1] + 1 + startOffset; } ret.back().startPos = ret.back().endPos = str.size() + startOffset; while (n < str.size() && isSpace(str[n])) ++n; @@ -694,30 +1093,30 @@ const Form* kiwi::findForm( namespace kiwi { - template + template struct SplitByTrieGetter { template struct Wrapper { - static constexpr FnSplitByTrie value = &splitByTrie(i), typoTolerant>; + static constexpr FnSplitByTrie value = &splitByTrie(i), typoTolerant, continualTypoTolerant>; }; }; } -FnSplitByTrie kiwi::getSplitByTrieFn(ArchType arch, bool typoTolerant) +FnSplitByTrie kiwi::getSplitByTrieFn(ArchType arch, bool typoTolerant, bool continualTypoTolerant) { - static tp::Table table{ SplitByTrieGetter{} }; - static tp::Table tableTT{ SplitByTrieGetter{} }; + static std::array, 4> table{ + SplitByTrieGetter{}, + SplitByTrieGetter{}, + SplitByTrieGetter{}, + SplitByTrieGetter{} + }; - if (typoTolerant) - { - return tableTT[static_cast(arch)]; - } - else - { - return table[static_cast(arch)]; - } + size_t idx = 0; + if (typoTolerant) idx += 1; + if (continualTypoTolerant) idx += 2; + return table[idx][static_cast(arch)]; } namespace kiwi diff --git a/src/KTrie.h b/src/KTrie.h index 4bf0907a..71bd9e5a 100644 --- a/src/KTrie.h +++ b/src/KTrie.h @@ -63,8 +63,10 @@ namespace kiwi uint32_t typoFormId = 0; uint32_t spaceErrors = 0; - KGraphNode(const Form* _form = nullptr, uint16_t _endPos = 0, float _typoCost = 0) : form(_form), endPos(_endPos), typoCost(_typoCost) {} - KGraphNode(U16StringView _uform, uint16_t _endPos, float _typoCost = 0) : uform(_uform), endPos(_endPos), typoCost(_typoCost) {} + KGraphNode(uint16_t _startPos = 0, uint16_t _endPos = 0, const Form* _form = nullptr, float _typoCost = 0) + : form(_form), startPos(_startPos), endPos(_endPos), typoCost(_typoCost) {} + KGraphNode(uint16_t _startPos, uint16_t _endPos, U16StringView _uform, float _typoCost = 0) + : uform(_uform), startPos(_startPos), endPos(_endPos), typoCost(_typoCost) {} KGraphNode* getPrev() { return prev ? this - prev : nullptr; } const KGraphNode* getPrev() const { return prev ? this - prev : nullptr; } @@ -73,7 +75,13 @@ namespace kiwi const KGraphNode* getSibling() const { return sibling ? this + sibling : nullptr; } }; - template + /** + * @brief string을 분할하여 Form으로 구성된 그래프를 생성한다. + * @tparam arch Trie탐색에 사용할 CPU 아키텍처 타입 + * @tparam typoTolerant 오타가 포함된 형태를 탐색할지 여부 + * @tparam continualTypoTolerant 연철된 오타를 탐색할지 여부 + */ + template size_t splitByTrie( Vector& out, const Form* formBase, @@ -84,7 +92,7 @@ namespace kiwi Match matchOptions, size_t maxUnkFormSize, size_t spaceTolerance, - float typoCostWeight, + float continualTypoCost, const PretokenizedSpanGroup::Span*& pretokenizedFirst, const PretokenizedSpanGroup::Span* pretokenizedLast ); @@ -96,7 +104,7 @@ namespace kiwi ); using FnSplitByTrie = decltype(&splitByTrie); - FnSplitByTrie getSplitByTrieFn(ArchType arch, bool typoTolerant); + FnSplitByTrie getSplitByTrieFn(ArchType arch, bool typoTolerant, bool continualTypoTolerant); using FnFindForm = decltype(&findForm); FnFindForm getFindFormFn(ArchType arch); diff --git a/src/Kiwi.cpp b/src/Kiwi.cpp index ef635f46..a24d877b 100644 --- a/src/Kiwi.cpp +++ b/src/Kiwi.cpp @@ -41,11 +41,11 @@ namespace kiwi return buf; } - Kiwi::Kiwi(ArchType arch, LangModel _langMdl, bool typoTolerant) + Kiwi::Kiwi(ArchType arch, LangModel _langMdl, bool typoTolerant, bool continualTypoTolerant) : langMdl(_langMdl) { selectedArch = arch; - dfSplitByTrie = (void*)getSplitByTrieFn(selectedArch, typoTolerant); + dfSplitByTrie = (void*)getSplitByTrieFn(selectedArch, typoTolerant, continualTypoTolerant); dfFindForm = (void*)getFindFormFn(selectedArch); static tp::Table lmKnLM_8{ FindBestPathGetter::type>{} }; @@ -961,7 +961,7 @@ namespace kiwi matchOptions, maxUnkFormSize, spaceTolerance, - typoCostWeight, + continualTypoCost, pretokenizedFirst, pretokenizedLast ); diff --git a/src/KiwiBuilder.cpp b/src/KiwiBuilder.cpp index 93930f90..3c072843 100644 --- a/src/KiwiBuilder.cpp +++ b/src/KiwiBuilder.cpp @@ -1682,7 +1682,7 @@ namespace kiwi Kiwi KiwiBuilder::build(const TypoTransformer& typos, float typoCostThreshold) const { - Kiwi ret{ archType, langMdl, !typos.empty()}; + Kiwi ret{ archType, langMdl, !typos.empty(), typos.isContinualTypoEnabled() }; Vector combinedForms; Vector combinedMorphemes; @@ -1806,9 +1806,10 @@ Kiwi KiwiBuilder::build(const TypoTransformer& typos, float typoCostThreshold) c using TypoInfo = tuple; UnorderedMap> typoGroup; auto ptypos = typos.prepare(); + ret.continualTypoCost = ptypos.getContinualTypoCost(); for (auto f : sortedForms) { - // 현재는 공백이 없는 단일 단어에 대해서만 오타 교정을 수행 + // 현재는 공백이 없는 단일 단어에 대해서만 오타 교정을 수행. // 공백이 포함된 복합 명사류의 경우 오타 후보가 지나치게 많아져 // 메모리 요구량이 급격히 증가하기 때문. if (f->numSpaces == 0) diff --git a/src/TypoTransformer.cpp b/src/TypoTransformer.cpp index 10bb3bc8..6b6076e3 100644 --- a/src/TypoTransformer.cpp +++ b/src/TypoTransformer.cpp @@ -307,10 +307,22 @@ void TypoTransformer::addTypo(const u16string& orig, const u16string& error, flo return addTypoNormalized(normalizeHangul(orig), normalizeHangul(error), cost, leftCond); } +bool TypoTransformer::isContinualTypoEnabled() const +{ + return isfinite(continualTypoThreshold); +} + +TypoTransformer TypoTransformer::copyWithNewContinualTypoCost(float threshold) const +{ + TypoTransformer ret = *this; + ret.continualTypoThreshold = threshold; + return ret; +} + PreparedTypoTransformer::PreparedTypoTransformer() = default; PreparedTypoTransformer::PreparedTypoTransformer(const TypoTransformer& tt) - : strPool{ tt.strPool } + : strPool{ tt.strPool }, continualTypoThreshold{ tt.continualTypoThreshold } { size_t tot = 0; for (auto& rs : tt.replacements) tot += rs.size(); @@ -439,7 +451,6 @@ TypoCandidates PreparedTypoTransformer::generate(const u16string& orig, fl return _generate(normalizeHangul(orig), costThreshold); } - namespace kiwi { template class TypoCandidates; @@ -450,102 +461,150 @@ namespace kiwi template TypoCandidates PreparedTypoTransformer::_generate(const KString&, float) const; template TypoCandidates PreparedTypoTransformer::_generate(const KString&, float) const; - const TypoTransformer withoutTypo; - - using TypoDef = TypoTransformer::TypoDef; - - const TypoTransformer basicTypoSet { - TypoDef{ {u"ㅐ", u"ㅔ"}, {u"ㅐ", u"ㅔ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ㅐ", u"ㅔ"}, {u"ㅒ", u"ㅖ"}, 1.5f, CondVowel::none }, - TypoDef{ {u"ㅒ", u"ㅖ"}, {u"ㅐ", u"ㅔ"}, 1.5f, CondVowel::none }, - TypoDef{ {u"ㅒ", u"ㅖ"}, {u"ㅒ", u"ㅖ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ㅚ", u"ㅙ", u"ㅞ"}, {u"ㅚ", u"ㅙ", u"ㅞ", u"ㅐ", u"ㅔ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ㅝ"}, {u"ㅗ", u"ㅓ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ㅟ", u"ㅢ"}, {u"ㅣ"}, 1.f, CondVowel::none}, - TypoDef{ {u"위", u"의"}, {u"이"}, INFINITY, CondVowel::none}, // 이->위, 이->의 과도교정 배제 - TypoDef{ {u"위", u"의"}, {u"이"}, 1.f, CondVowel::any}, // 이->위, 이->의 과도교정 배제 - TypoDef{ {u"자", u"쟈"}, {u"자", u"쟈"}, 1.f, CondVowel::none }, - TypoDef{ {u"재", u"쟤"}, {u"재", u"쟤"}, 1.f, CondVowel::none }, - TypoDef{ {u"저", u"져"}, {u"저", u"져"}, 1.f, CondVowel::none }, - TypoDef{ {u"제", u"졔"}, {u"제", u"졔"}, 1.f, CondVowel::none }, - TypoDef{ {u"조", u"죠", u"줘"}, {u"조", u"죠", u"줘"}, 1.f, CondVowel::none }, - TypoDef{ {u"주", u"쥬"}, {u"주", u"쥬"}, 1.f, CondVowel::none }, - TypoDef{ {u"차", u"챠"}, {u"차", u"챠"}, 1.f, CondVowel::none }, - TypoDef{ {u"채", u"챼"}, {u"채", u"챼"}, 1.f, CondVowel::none }, - TypoDef{ {u"처", u"쳐"}, {u"처", u"쳐"}, 1.f, CondVowel::none }, - TypoDef{ {u"체", u"쳬"}, {u"체", u"쳬"}, 1.f, CondVowel::none }, - TypoDef{ {u"초", u"쵸", u"춰"}, {u"초", u"쵸", u"춰"}, 1.f, CondVowel::none }, - TypoDef{ {u"추", u"츄"}, {u"추", u"츄"}, 1.f, CondVowel::none }, - TypoDef{ {u"유", u"류"}, {u"유", u"류"}, 1.f, CondVowel::none }, - TypoDef{ {u"므", u"무"}, {u"므", u"무"}, 1.f, CondVowel::none }, - TypoDef{ {u"브", u"부"}, {u"브", u"부"}, 1.f, CondVowel::none }, - TypoDef{ {u"프", u"푸"}, {u"프", u"푸"}, 1.f, CondVowel::none }, - TypoDef{ {u"르", u"루"}, {u"르", u"루"}, 1.f, CondVowel::none }, - TypoDef{ {u"러", u"뤄"}, {u"러", u"뤄"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆩ", u"ᆪ"}, {u"ᆨ", u"ᆩ", u"ᆪ"}, 1.5f, CondVowel::none }, - TypoDef{ {u"ᆬ", u"ᆭ"}, {u"ᆫ", u"ᆬ", u"ᆭ"}, 1.5f, CondVowel::none }, - TypoDef{ {u"ᆰ", u"ᆱ", u"ᆲ", u"ᆳ", u"ᆴ", u"ᆵ", u"ᆶ"}, {u"ᆯ", u"ᆰ", u"ᆱ", u"ᆲ", u"ᆳ", u"ᆴ", u"ᆵ", u"ᆶ"}, 1.5f, CondVowel::none }, - TypoDef{ {u"ᆺ", u"ᆻ"}, {u"ᆺ", u"ᆻ"}, 1.f, CondVowel::none }, - - TypoDef{ {u"안"}, {u"않"}, 1.5f, CondVowel::none }, - TypoDef{ {u"맞추", u"맞히"}, {u"맞추", u"맞히"}, 1.5f, CondVowel::none }, - TypoDef{ {u"맞춰", u"맞혀"}, {u"맞춰", u"맞혀"}, 1.5f, CondVowel::none }, - TypoDef{ {u"받치", u"바치", u"받히"}, {u"받치", u"바치", u"받히"}, 1.5f, CondVowel::none }, - TypoDef{ {u"받쳐", u"바쳐", u"받혀"}, {u"받쳐", u"바쳐", u"받혀"}, 1.5f, CondVowel::none }, - TypoDef{ {u"던", u"든"}, {u"던", u"든"}, 1.f, CondVowel::none }, - TypoDef{ {u"때", u"데"}, {u"때", u"데"}, 1.5f, CondVowel::none }, - TypoDef{ {u"빛", u"빚"}, {u"빛", u"빚"}, 1.f, CondVowel::none }, - - TypoDef{ {u"ᆮ이", u"지"}, {u"ᆮ이", u"지"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆮ여", u"져"}, {u"ᆮ여", u"져"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᇀ이", u"치"}, {u"ᇀ이", u"치"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᇀ여", u"쳐"}, {u"ᇀ여", u"쳐"}, 1.f, CondVowel::none }, - - TypoDef{ {u"ᄀ", u"ᄁ"}, {u"ᄀ", u"ᄁ"}, 1.f, CondVowel::applosive }, - TypoDef{ {u"ᄃ", u"ᄄ"}, {u"ᄃ", u"ᄄ"}, 1.f, CondVowel::applosive }, - TypoDef{ {u"ᄇ", u"ᄈ"}, {u"ᄇ", u"ᄈ"}, 1.f, CondVowel::applosive }, - TypoDef{ {u"ᄉ", u"ᄊ"}, {u"ᄉ", u"ᄊ"}, 1.f, CondVowel::applosive }, - TypoDef{ {u"ᄌ", u"ᄍ"}, {u"ᄌ", u"ᄍ"}, 1.f, CondVowel::applosive }, - - TypoDef{ {u"ᇂᄒ", u"ᆨᄒ", u"ᇂᄀ"}, {u"ᇂᄒ", u"ᆨᄒ", u"ᇂᄀ"}, 1.f, CondVowel::none}, - - TypoDef{ {u"ᆨᄂ", u"ᆩᄂ", u"ᆪᄂ", u"ᆿᄂ", u"ᆼᄂ"}, {u"ᆨᄂ", u"ᆩᄂ", u"ᆪᄂ", u"ᆿᄂ", u"ᆼᄂ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆨᄆ", u"ᆩᄆ", u"ᆪᄆ", u"ᆿᄆ", u"ᆼᄆ"}, {u"ᆨᄆ", u"ᆩᄆ", u"ᆪᄆ", u"ᆿᄆ", u"ᆼᄆ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆨᄅ", u"ᆩᄅ", u"ᆪᄅ", u"ᆿᄅ", u"ᆼᄅ", u"ᆼᄂ",}, {u"ᆨᄅ", u"ᆩᄅ", u"ᆪᄅ", u"ᆿᄅ", u"ᆼᄅ", u"ᆼᄂ",}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆮᄂ", u"ᆺᄂ", u"ᆻᄂ", u"ᆽᄂ", u"ᆾᄂ", u"ᇀᄂ", u"ᆫᄂ"}, {u"ᆮᄂ", u"ᆺᄂ", u"ᆻᄂ", u"ᆽᄂ", u"ᆾᄂ", u"ᇀᄂ", u"ᆫᄂ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆮᄆ", u"ᆺᄆ", u"ᆻᄆ", u"ᆽᄆ", u"ᆾᄆ", u"ᇀᄆ", u"ᆫᄆ"}, {u"ᆮᄆ", u"ᆺᄆ", u"ᆻᄆ", u"ᆽᄆ", u"ᆾᄆ", u"ᇀᄆ", u"ᆫᄆ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆮᄅ", u"ᆺᄅ", u"ᆻᄅ", u"ᆽᄅ", u"ᆾᄅ", u"ᇀᄅ", u"ᆫᄅ", u"ᆫᄂ",}, {u"ᆮᄅ", u"ᆺᄅ", u"ᆻᄅ", u"ᆽᄅ", u"ᆾᄅ", u"ᇀᄅ", u"ᆫᄅ", u"ᆫᄂ",}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆸᄂ", u"ᆹᄂ", u"ᇁᄂ", u"ᆷᄂ"}, {u"ᆸᄂ", u"ᆹᄂ", u"ᇁᄂ", u"ᆷᄂ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆸᄆ", u"ᆹᄆ", u"ᇁᄆ", u"ᆷᄆ"}, {u"ᆸᄆ", u"ᆹᄆ", u"ᇁᄆ", u"ᆷᄆ"}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆸᄅ", u"ᆹᄅ", u"ᇁᄅ", u"ᆷᄅ", u"ᆷᄂ",}, {u"ᆸᄅ", u"ᆹᄅ", u"ᇁᄅ", u"ᆷᄅ", u"ᆷᄂ",}, 1.f, CondVowel::none }, - TypoDef{ {u"ᆫᄅ", u"ᆫᄂ", u"ᆯᄅ", u"ᆯᄂ"}, {u"ᆫᄅ", u"ᆫᄂ", u"ᆯᄅ", u"ᆯᄂ"}, 1.f, CondVowel::none }, - - TypoDef{ {u"ᆨᄋ", u"ᄀ"}, {u"ᆨᄋ", u"ᄀ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆩᄋ", u"ᄁ"}, {u"ᆩᄋ", u"ᄁ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆫᄋ", u"ᆫᄒ", u"ᄂ"}, {u"ᆫᄋ", u"ᆫᄒ", u"ᄂ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆬᄋ", u"ᆫᄌ"}, {u"ᆬᄋ", u"ᆫᄌ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆭᄋ", u"ᄂ"}, {u"ᆭᄋ", u"ᄂ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆮᄋ", u"ᄃ"}, {u"ᆮᄋ", u"ᄃ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆯᄋ", u"ᆯᄒ", u"ᄅ"}, {u"ᆯᄋ", u"ᆯᄒ", u"ᄅ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆰᄋ", u"ᆯᄀ"}, {u"ᆰᄋ", u"ᆯᄀ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆰᄒ", u"ᆯᄏ"}, {u"ᆰᄒ", u"ᆯᄏ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆷᄋ", u"ᄆ"}, {u"ᆷᄋ", u"ᄆ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆸᄋ", u"ᄇ"}, {u"ᆸᄋ", u"ᄇ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆺᄋ", u"ᄉ"}, {u"ᆺᄋ", u"ᄉ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆻᄋ", u"ᆺᄉ", u"ᄊ"}, {u"ᆻᄋ", u"ᆺᄉ", u"ᄊ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆽᄋ", u"ᄌ"}, {u"ᆽᄋ", u"ᄌ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆾᄋ", u"ᆾᄒ", u"ᆽᄒ", u"ᄎ"}, {u"ᆾᄋ", u"ᆾᄒ", u"ᆽᄒ", u"ᄎ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᆿᄋ", u"ᆿᄒ", u"ᆨᄒ", u"ᄏ"}, {u"ᆿᄋ", u"ᆿᄒ", u"ᆨᄒ", u"ᄏ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᇀᄋ", u"ᇀᄒ", u"ᆮᄒ", u"ᄐ"}, {u"ᇀᄋ", u"ᇀᄒ", u"ᆮᄒ", u"ᄐ"}, 1.f, CondVowel::vowel }, - TypoDef{ {u"ᇁᄋ", u"ᇁᄒ", u"ᆸᄒ", u"ᄑ"}, {u"ᇁᄋ", u"ᇁᄒ", u"ᆸᄒ", u"ᄑ"}, 1.f, CondVowel::vowel }, - - TypoDef{ {u"은", u"는"}, {u"은", u"는"}, 2.f, CondVowel::none }, - TypoDef{ {u"을", u"를"}, {u"을", u"를"}, 2.f, CondVowel::none }, - - TypoDef{ {u"ㅣ워", u"ㅣ어", u"ㅕ"}, {u"ㅣ워", u"ㅣ어", u"ㅕ"}, 1.5f, CondVowel::none}, - //TypoDef{ {u"ㅡ아", u"ㅏ"}, {u"ㅡ아", u"ㅏ"}, 1.5f, CondVowel::none}, - //TypoDef{ {u"ㅡ어", u"ㅓ"}, {u"ㅡ어", u"ㅓ"}, 1.5f, CondVowel::none}, - //TypoDef{ {u"ㅡ오", u"ㅗ"}, {u"ㅡ오", u"ㅗ"}, 1.5f, CondVowel::none}, - //TypoDef{ {u"ㅡ우", u"ㅜ"}, {u"ㅡ우", u"ㅜ"}, 1.5f, CondVowel::none}, - }; + + const TypoTransformer& getDefaultTypoSet(DefaultTypoSet set) + { + using TypoDef = TypoTransformer::TypoDef; + static const TypoTransformer withoutTypo; + + static const TypoTransformer basicTypoSet{ + TypoDef{ {u"ㅐ", u"ㅔ"}, {u"ㅐ", u"ㅔ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ㅐ", u"ㅔ"}, {u"ㅒ", u"ㅖ"}, 1.5f, CondVowel::none }, + TypoDef{ {u"ㅒ", u"ㅖ"}, {u"ㅐ", u"ㅔ"}, 1.5f, CondVowel::none }, + TypoDef{ {u"ㅒ", u"ㅖ"}, {u"ㅒ", u"ㅖ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ㅚ", u"ㅙ", u"ㅞ"}, {u"ㅚ", u"ㅙ", u"ㅞ", u"ㅐ", u"ㅔ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ㅝ"}, {u"ㅗ", u"ㅓ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ㅟ", u"ㅢ"}, {u"ㅣ"}, 1.f, CondVowel::none}, + TypoDef{ {u"위", u"의"}, {u"이"}, INFINITY, CondVowel::none}, // 이->위, 이->의 과도교정 배제 + TypoDef{ {u"위", u"의"}, {u"이"}, 1.f, CondVowel::any}, // 이->위, 이->의 과도교정 배제 + TypoDef{ {u"자", u"쟈"}, {u"자", u"쟈"}, 1.f, CondVowel::none }, + TypoDef{ {u"재", u"쟤"}, {u"재", u"쟤"}, 1.f, CondVowel::none }, + TypoDef{ {u"저", u"져"}, {u"저", u"져"}, 1.f, CondVowel::none }, + TypoDef{ {u"제", u"졔"}, {u"제", u"졔"}, 1.f, CondVowel::none }, + TypoDef{ {u"조", u"죠", u"줘"}, {u"조", u"죠", u"줘"}, 1.f, CondVowel::none }, + TypoDef{ {u"주", u"쥬"}, {u"주", u"쥬"}, 1.f, CondVowel::none }, + TypoDef{ {u"차", u"챠"}, {u"차", u"챠"}, 1.f, CondVowel::none }, + TypoDef{ {u"채", u"챼"}, {u"채", u"챼"}, 1.f, CondVowel::none }, + TypoDef{ {u"처", u"쳐"}, {u"처", u"쳐"}, 1.f, CondVowel::none }, + TypoDef{ {u"체", u"쳬"}, {u"체", u"쳬"}, 1.f, CondVowel::none }, + TypoDef{ {u"초", u"쵸", u"춰"}, {u"초", u"쵸", u"춰"}, 1.f, CondVowel::none }, + TypoDef{ {u"추", u"츄"}, {u"추", u"츄"}, 1.f, CondVowel::none }, + TypoDef{ {u"유", u"류"}, {u"유", u"류"}, 1.f, CondVowel::none }, + TypoDef{ {u"므", u"무"}, {u"므", u"무"}, 1.f, CondVowel::none }, + TypoDef{ {u"브", u"부"}, {u"브", u"부"}, 1.f, CondVowel::none }, + TypoDef{ {u"프", u"푸"}, {u"프", u"푸"}, 1.f, CondVowel::none }, + TypoDef{ {u"르", u"루"}, {u"르", u"루"}, 1.f, CondVowel::none }, + TypoDef{ {u"러", u"뤄"}, {u"러", u"뤄"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆩ", u"ᆪ"}, {u"ᆨ", u"ᆩ", u"ᆪ"}, 1.5f, CondVowel::none }, + TypoDef{ {u"ᆬ", u"ᆭ"}, {u"ᆫ", u"ᆬ", u"ᆭ"}, 1.5f, CondVowel::none }, + TypoDef{ {u"ᆰ", u"ᆱ", u"ᆲ", u"ᆳ", u"ᆴ", u"ᆵ", u"ᆶ"}, {u"ᆯ", u"ᆰ", u"ᆱ", u"ᆲ", u"ᆳ", u"ᆴ", u"ᆵ", u"ᆶ"}, 1.5f, CondVowel::none }, + TypoDef{ {u"ᆺ", u"ᆻ"}, {u"ᆺ", u"ᆻ"}, 1.f, CondVowel::none }, + + TypoDef{ {u"안"}, {u"않"}, 1.5f, CondVowel::none }, + TypoDef{ {u"맞추", u"맞히"}, {u"맞추", u"맞히"}, 1.5f, CondVowel::none }, + TypoDef{ {u"맞춰", u"맞혀"}, {u"맞춰", u"맞혀"}, 1.5f, CondVowel::none }, + TypoDef{ {u"받치", u"바치", u"받히"}, {u"받치", u"바치", u"받히"}, 1.5f, CondVowel::none }, + TypoDef{ {u"받쳐", u"바쳐", u"받혀"}, {u"받쳐", u"바쳐", u"받혀"}, 1.5f, CondVowel::none }, + TypoDef{ {u"던", u"든"}, {u"던", u"든"}, 1.f, CondVowel::none }, + TypoDef{ {u"때", u"데"}, {u"때", u"데"}, 1.5f, CondVowel::none }, + TypoDef{ {u"빛", u"빚"}, {u"빛", u"빚"}, 1.f, CondVowel::none }, + + TypoDef{ {u"ᆮ이", u"지"}, {u"ᆮ이", u"지"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆮ여", u"져"}, {u"ᆮ여", u"져"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᇀ이", u"치"}, {u"ᇀ이", u"치"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᇀ여", u"쳐"}, {u"ᇀ여", u"쳐"}, 1.f, CondVowel::none }, + + TypoDef{ {u"ᄀ", u"ᄁ"}, {u"ᄀ", u"ᄁ"}, 1.f, CondVowel::applosive }, + TypoDef{ {u"ᄃ", u"ᄄ"}, {u"ᄃ", u"ᄄ"}, 1.f, CondVowel::applosive }, + TypoDef{ {u"ᄇ", u"ᄈ"}, {u"ᄇ", u"ᄈ"}, 1.f, CondVowel::applosive }, + TypoDef{ {u"ᄉ", u"ᄊ"}, {u"ᄉ", u"ᄊ"}, 1.f, CondVowel::applosive }, + TypoDef{ {u"ᄌ", u"ᄍ"}, {u"ᄌ", u"ᄍ"}, 1.f, CondVowel::applosive }, + + TypoDef{ {u"ᇂᄒ", u"ᆨᄒ", u"ᇂᄀ"}, {u"ᇂᄒ", u"ᆨᄒ", u"ᇂᄀ"}, 1.f, CondVowel::none}, + + TypoDef{ {u"ᆨᄂ", u"ᆩᄂ", u"ᆪᄂ", u"ᆿᄂ", u"ᆼᄂ"}, {u"ᆨᄂ", u"ᆩᄂ", u"ᆪᄂ", u"ᆿᄂ", u"ᆼᄂ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆨᄆ", u"ᆩᄆ", u"ᆪᄆ", u"ᆿᄆ", u"ᆼᄆ"}, {u"ᆨᄆ", u"ᆩᄆ", u"ᆪᄆ", u"ᆿᄆ", u"ᆼᄆ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆨᄅ", u"ᆩᄅ", u"ᆪᄅ", u"ᆿᄅ", u"ᆼᄅ", u"ᆼᄂ",}, {u"ᆨᄅ", u"ᆩᄅ", u"ᆪᄅ", u"ᆿᄅ", u"ᆼᄅ", u"ᆼᄂ",}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆮᄂ", u"ᆺᄂ", u"ᆻᄂ", u"ᆽᄂ", u"ᆾᄂ", u"ᇀᄂ", u"ᆫᄂ"}, {u"ᆮᄂ", u"ᆺᄂ", u"ᆻᄂ", u"ᆽᄂ", u"ᆾᄂ", u"ᇀᄂ", u"ᆫᄂ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆮᄆ", u"ᆺᄆ", u"ᆻᄆ", u"ᆽᄆ", u"ᆾᄆ", u"ᇀᄆ", u"ᆫᄆ"}, {u"ᆮᄆ", u"ᆺᄆ", u"ᆻᄆ", u"ᆽᄆ", u"ᆾᄆ", u"ᇀᄆ", u"ᆫᄆ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆮᄅ", u"ᆺᄅ", u"ᆻᄅ", u"ᆽᄅ", u"ᆾᄅ", u"ᇀᄅ", u"ᆫᄅ", u"ᆫᄂ",}, {u"ᆮᄅ", u"ᆺᄅ", u"ᆻᄅ", u"ᆽᄅ", u"ᆾᄅ", u"ᇀᄅ", u"ᆫᄅ", u"ᆫᄂ",}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆸᄂ", u"ᆹᄂ", u"ᇁᄂ", u"ᆷᄂ"}, {u"ᆸᄂ", u"ᆹᄂ", u"ᇁᄂ", u"ᆷᄂ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆸᄆ", u"ᆹᄆ", u"ᇁᄆ", u"ᆷᄆ"}, {u"ᆸᄆ", u"ᆹᄆ", u"ᇁᄆ", u"ᆷᄆ"}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆸᄅ", u"ᆹᄅ", u"ᇁᄅ", u"ᆷᄅ", u"ᆷᄂ",}, {u"ᆸᄅ", u"ᆹᄅ", u"ᇁᄅ", u"ᆷᄅ", u"ᆷᄂ",}, 1.f, CondVowel::none }, + TypoDef{ {u"ᆫᄅ", u"ᆫᄂ", u"ᆯᄅ", u"ᆯᄂ"}, {u"ᆫᄅ", u"ᆫᄂ", u"ᆯᄅ", u"ᆯᄂ"}, 1.f, CondVowel::none }, + + TypoDef{ {u"ᆨᄋ", u"ᄀ"}, {u"ᆨᄋ", u"ᄀ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆩᄋ", u"ᄁ"}, {u"ᆩᄋ", u"ᄁ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆫᄋ", u"ᆫᄒ", u"ᄂ"}, {u"ᆫᄋ", u"ᆫᄒ", u"ᄂ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆬᄋ", u"ᆫᄌ"}, {u"ᆬᄋ", u"ᆫᄌ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆭᄋ", u"ᄂ"}, {u"ᆭᄋ", u"ᄂ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆮᄋ", u"ᄃ"}, {u"ᆮᄋ", u"ᄃ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆯᄋ", u"ᆯᄒ", u"ᄅ"}, {u"ᆯᄋ", u"ᆯᄒ", u"ᄅ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆰᄋ", u"ᆯᄀ"}, {u"ᆰᄋ", u"ᆯᄀ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆰᄒ", u"ᆯᄏ"}, {u"ᆰᄒ", u"ᆯᄏ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆷᄋ", u"ᄆ"}, {u"ᆷᄋ", u"ᄆ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆸᄋ", u"ᄇ"}, {u"ᆸᄋ", u"ᄇ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆺᄋ", u"ᄉ"}, {u"ᆺᄋ", u"ᄉ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆻᄋ", u"ᆺᄉ", u"ᄊ"}, {u"ᆻᄋ", u"ᆺᄉ", u"ᄊ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆽᄋ", u"ᄌ"}, {u"ᆽᄋ", u"ᄌ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆾᄋ", u"ᆾᄒ", u"ᆽᄒ", u"ᄎ"}, {u"ᆾᄋ", u"ᆾᄒ", u"ᆽᄒ", u"ᄎ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᆿᄋ", u"ᆿᄒ", u"ᆨᄒ", u"ᄏ"}, {u"ᆿᄋ", u"ᆿᄒ", u"ᆨᄒ", u"ᄏ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᇀᄋ", u"ᇀᄒ", u"ᆮᄒ", u"ᄐ"}, {u"ᇀᄋ", u"ᇀᄒ", u"ᆮᄒ", u"ᄐ"}, 1.f, CondVowel::vowel }, + TypoDef{ {u"ᇁᄋ", u"ᇁᄒ", u"ᆸᄒ", u"ᄑ"}, {u"ᇁᄋ", u"ᇁᄒ", u"ᆸᄒ", u"ᄑ"}, 1.f, CondVowel::vowel }, + + TypoDef{ {u"은", u"는"}, {u"은", u"는"}, 2.f, CondVowel::none }, + TypoDef{ {u"을", u"를"}, {u"을", u"를"}, 2.f, CondVowel::none }, + + TypoDef{ {u"ㅣ워", u"ㅣ어", u"ㅕ"}, {u"ㅣ워", u"ㅣ어", u"ㅕ"}, 1.5f, CondVowel::none}, + }; + static const TypoTransformer continualTypoSet = withoutTypo.copyWithNewContinualTypoCost(1.f).addTypos({ + TypoDef{ {u"ᆪ"}, {u"ᆨᆺ", u"ᆨᆻ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆬ"}, {u"ᆫᆽ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆭ"}, {u"ᆫᇂ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆰ"}, {u"ᆯᆨ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆱ"}, {u"ᆯᆷ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆲ"}, {u"ᆯᆸ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆳ"}, {u"ᆯᆺ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆴ"}, {u"ᆯᇀ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆵ"}, {u"ᆯᇁ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆶ"}, {u"ᆯᇂ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆹ"}, {u"ᆸᆺ", u"ᆸᆻ"}, 1e-12f, CondVowel::none }, + }); + + static const TypoTransformer basicTypoSetWithContinual = basicTypoSet.copyWithNewContinualTypoCost(1.f).addTypos({ + TypoDef{ {u"ᆪ"}, {u"ᆨᆺ", u"ᆨᆻ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆬ"}, {u"ᆫᆽ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆭ"}, {u"ᆫᇂ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆰ"}, {u"ᆯᆨ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆱ"}, {u"ᆯᆷ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆲ"}, {u"ᆯᆸ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆳ"}, {u"ᆯᆺ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆴ"}, {u"ᆯᇀ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆵ"}, {u"ᆯᇁ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆶ"}, {u"ᆯᇂ"}, 1e-12f, CondVowel::none }, + TypoDef{ {u"ᆹ"}, {u"ᆸᆺ", u"ᆸᆻ"}, 1e-12f, CondVowel::none }, + }); + + if (set == DefaultTypoSet::withoutTypo) + { + return withoutTypo; + } + else if (set == DefaultTypoSet::basicTypoSet) + { + return basicTypoSet; + } + else if (set == DefaultTypoSet::continualTypoSet) + { + return continualTypoSet; + } + else if (set == DefaultTypoSet::basicTypoSetWithContinual) + { + return basicTypoSetWithContinual; + } + else + { + throw invalid_argument{ "Invalid `DefaultTypoSet`" }; + } + } + } From fc82d1a1f39c58d12a4c2d00a42c6748d8dd3529 Mon Sep 17 00:00:00 2001 From: bab2min Date: Mon, 8 Apr 2024 01:25:50 +0900 Subject: [PATCH 2/8] added `kiwi_typo_get_default` to C API --- include/kiwi/capi.h | 28 +++++++++++++++++++++------- src/capi/kiwi_c.cpp | 19 +++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/include/kiwi/capi.h b/include/kiwi/capi.h index 30b4d70d..d70a36b5 100644 --- a/include/kiwi/capi.h +++ b/include/kiwi/capi.h @@ -325,12 +325,6 @@ DECL_DLL kiwi_ws_h kiwi_builder_extract_add_words_w(kiwi_builder_h handle, kiwi_ */ DECL_DLL kiwi_h kiwi_builder_build(kiwi_builder_h handle, kiwi_typo_h typos, float typo_cost_threshold); -/** - * @brief Kiwi 기본 내장 오타 교정기의 핸들 - * - * @note 이 핸들은 kiwi_typo_close에 사용할 수 없음. - */ -extern DECL_DLL const kiwi_typo_h kiwi_basic_typo; /** * @brief @@ -346,10 +340,30 @@ DECL_DLL kiwi_typo_h kiwi_typo_init(); * * @return * - * @note 이 핸들은 kiwi_typo_close에 사용할 수 없음. + * @note 이 핸들은 kiwi_typo_close에 사용할 수 없음. + * 이 함수의 반환값은 kiwi_typo_get_default(KIWI_TYPO_BASIC_TYPO_SET)과 동일합니다. + * 이 함수보다 더 다양한 기능을 제공하는 kiwi_typo_get_default를 사용하는 것을 권장합니다. */ DECL_DLL kiwi_typo_h kiwi_typo_get_basic(); + +enum +{ + KIWI_TYPO_WITHOUT_TYPO = 0, + KIWI_TYPO_BASIC_TYPO_SET = 1, + KIWI_TYPO_CONTINUAL_TYPO_SET = 2, + KIWI_TYPO_BASIC_TYPO_SET_WITH_CONTINUAL = 3, +}; + +/** + * @brief Kiwi에 기본적으로 내장된 오타 교정기의 핸들을 반환합니다. + * + * @return + * + * @note 이 핸들은 kiwi_typo_close에 사용할 수 없음. + */ +DECL_DLL kiwi_typo_h kiwi_typo_get_default(int kiwi_typo_set); + /** * @brief * diff --git a/src/capi/kiwi_c.cpp b/src/capi/kiwi_c.cpp index e2070b18..74e760e3 100644 --- a/src/capi/kiwi_c.cpp +++ b/src/capi/kiwi_c.cpp @@ -368,7 +368,7 @@ kiwi_h kiwi_builder_build(kiwi_builder_h handle, kiwi_typo_h typos, float typo_c auto* kb = (KiwiBuilder*)handle; try { - const TypoTransformer* tt = &withoutTypo; + const TypoTransformer* tt = &getDefaultTypoSet(DefaultTypoSet::withoutTypo); if (typos) { tt = typos; @@ -382,8 +382,6 @@ kiwi_h kiwi_builder_build(kiwi_builder_h handle, kiwi_typo_h typos, float typo_c } } -const kiwi_typo_h kiwi_basic_typo = (kiwi_typo_h)&basicTypoSet; - kiwi_typo_h kiwi_typo_init() { try @@ -399,7 +397,20 @@ kiwi_typo_h kiwi_typo_init() kiwi_typo_h kiwi_typo_get_basic() { - return (kiwi_typo_h)&basicTypoSet; + return kiwi_typo_get_default(KIWI_TYPO_BASIC_TYPO_SET); +} + +kiwi_typo_h kiwi_typo_get_default(int kiwi_typo_set) +{ + try + { + return (kiwi_typo_h)&getDefaultTypoSet((DefaultTypoSet)kiwi_typo_set); + } + catch (...) + { + currentError = current_exception(); + return nullptr; + } } int kiwi_typo_add(kiwi_typo_h handle, const char** orig, int orig_size, const char** error, int error_size, float cost, int condition) From 6c98ac305c876014a8db472104fd103cfc9bdf19 Mon Sep 17 00:00:00 2001 From: bab2min Date: Mon, 8 Apr 2024 01:26:07 +0900 Subject: [PATCH 3/8] updated test cases for C API --- test/test_c.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_c.cpp b/test/test_c.cpp index 8847169b..f91c73da 100644 --- a/test/test_c.cpp +++ b/test/test_c.cpp @@ -251,7 +251,7 @@ TEST(KiwiC, AnalyzeBasicTypoSet) { kiwi_h okw = reuse_kiwi_instance(), typo_kw; kiwi_builder_h builder = kiwi_builder_init(MODEL_PATH, 0, KIWI_BUILD_DEFAULT); - typo_kw = kiwi_builder_build(builder, kiwi_basic_typo, 2.5f); + typo_kw = kiwi_builder_build(builder, kiwi_typo_get_default(KIWI_TYPO_BASIC_TYPO_SET), 2.5f); kiwi_set_option_f(typo_kw, KIWI_TYPO_COST_WEIGHT, 5); kiwi_res_h o, c; From ae606f77c86c863ad2314c7845b57e5c844c732e Mon Sep 17 00:00:00 2001 From: bab2min Date: Mon, 8 Apr 2024 01:26:34 +0900 Subject: [PATCH 4/8] added test cases for `continualTypoSet` --- test/test_typo.cpp | 87 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/test/test_typo.cpp b/test/test_typo.cpp index bf357738..7f662631 100644 --- a/test/test_typo.cpp +++ b/test/test_typo.cpp @@ -55,7 +55,7 @@ TEST(KiwiTypo, Generate) TEST(KiwiTypo, BasicTypoSet) { - auto ptt = basicTypoSet.prepare(); + auto ptt = getDefaultTypoSet(DefaultTypoSet::basicTypoSet).prepare(); for (auto t : ptt.generate(u"의")) { @@ -93,7 +93,7 @@ TEST(KiwiTypo, AnalyzeBasicTypoSet) KiwiBuilder builder{ MODEL_PATH, 0, BuildOption::default_, }; Kiwi kiwi = builder.build(); - Kiwi typoKiwi = builder.build(basicTypoSet); + Kiwi typoKiwi = builder.build(DefaultTypoSet::basicTypoSet); typoKiwi.setTypoCostWeight(5); TokenResult o = kiwi.analyze(u"외않됀데?", Match::allWithNormalizing); @@ -121,3 +121,86 @@ TEST(KiwiTypo, AnalyzeBasicTypoSet) c = typoKiwi.analyze(u"Wertheimer)가 자신의 논문 <운동지각에 관한 실험연구>(Experimental studies on the perception of movement)을 통해 일상적인 지각 현상에 대한 새로운 시각을 제시한 시기이다.", Match::allWithNormalizing); } + +TEST(KiwiTypo, ContinualTypoSet) +{ + KiwiBuilder builder{ MODEL_PATH, 0, BuildOption::default_, }; + Kiwi typoKiwi = builder.build(DefaultTypoSet::continualTypoSet); + + auto res = typoKiwi.analyze(u"프로그래미", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 2); + EXPECT_EQ(res[0].str, u"프로그램"); + EXPECT_EQ(res[1].str, u"이"); + + res = typoKiwi.analyze(u"프로그래믈", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 2); + EXPECT_EQ(res[0].str, u"프로그램"); + EXPECT_EQ(res[1].str, u"을"); + + res = typoKiwi.analyze(u"오늘사무시레서", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 3); + EXPECT_EQ(res[1].str, u"사무실"); + EXPECT_EQ(res[2].str, u"에서"); + + res = typoKiwi.analyze(u"법원이 기가캤다.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 7); + EXPECT_EQ(res[2].str, u"기각"); + EXPECT_EQ(res[3].str, u"하"); + + res = typoKiwi.analyze(u"하나도 업써.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 5); + EXPECT_EQ(res[2].str, u"없"); + EXPECT_EQ(res[3].str, u"어"); + + res = typoKiwi.analyze(u"말근 하늘", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 3); + EXPECT_EQ(res[0].str, u"맑"); + EXPECT_EQ(res[1].str, u"은"); + + res = typoKiwi.analyze(u"아주 만타.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 4); + EXPECT_EQ(res[1].str, u"많"); + EXPECT_EQ(res[2].str, u"다"); +} + + +TEST(KiwiTypo, BasicTypoSetWithContinual) +{ + KiwiBuilder builder{ MODEL_PATH, 0, BuildOption::default_, }; + Kiwi typoKiwi = builder.build(DefaultTypoSet::basicTypoSetWithContinual); + + auto res = typoKiwi.analyze(u"프로그레미", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 2); + EXPECT_EQ(res[0].str, u"프로그램"); + EXPECT_EQ(res[1].str, u"이"); + + res = typoKiwi.analyze(u"프로그레믈", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 2); + EXPECT_EQ(res[0].str, u"프로그램"); + EXPECT_EQ(res[1].str, u"을"); + + res = typoKiwi.analyze(u"오늘사므시레서", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 3); + EXPECT_EQ(res[1].str, u"사무실"); + EXPECT_EQ(res[2].str, u"에서"); + + res = typoKiwi.analyze(u"버붠이 기가캤다.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 7); + EXPECT_EQ(res[2].str, u"기각"); + EXPECT_EQ(res[3].str, u"하"); + + res = typoKiwi.analyze(u"하나도 업써.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 5); + EXPECT_EQ(res[2].str, u"없"); + EXPECT_EQ(res[3].str, u"어"); + + res = typoKiwi.analyze(u"말근 하늘", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 3); + EXPECT_EQ(res[0].str, u"맑"); + EXPECT_EQ(res[1].str, u"은"); + + res = typoKiwi.analyze(u"아주 만타.", Match::allWithNormalizing).first; + EXPECT_EQ(res.size(), 4); + EXPECT_EQ(res[1].str, u"많"); + EXPECT_EQ(res[2].str, u"다"); +} From ef004d5a012e26494984aafa3015cd9fa29d3b1f Mon Sep 17 00:00:00 2001 From: bab2min Date: Sat, 13 Apr 2024 15:52:35 +0900 Subject: [PATCH 5/8] minor fixes & bump to 0.17.1 --- CMakeLists.txt | 2 +- include/kiwi/Macro.h | 2 +- include/kiwi/TypoTransformer.h | 5 +++++ src/KTrie.cpp | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 063dc850..6822d749 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) -project(kiwi VERSION 0.17.0 DESCRIPTION "Kiwi, Korean Intelligent Word Identifier") +project(kiwi VERSION 0.17.1 DESCRIPTION "Kiwi, Korean Intelligent Word Identifier") set ( CMAKE_CXX_STANDARD 14 ) set ( CMAKE_VERBOSE_MAKEFILE true ) diff --git a/include/kiwi/Macro.h b/include/kiwi/Macro.h index 36809698..25b0c393 100644 --- a/include/kiwi/Macro.h +++ b/include/kiwi/Macro.h @@ -4,7 +4,7 @@ #define KIWI_STR(x) KIWI_STR_HELPER(x) #define KIWI_VERSION_MAJOR 0 -#define KIWI_VERSION_MINOR 16 +#define KIWI_VERSION_MINOR 17 #define KIWI_VERSION_PATCH 1 #define KIWI_VERSION_STRING KIWI_STR(KIWI_VERSION_MAJOR) "." KIWI_STR(KIWI_VERSION_MINOR) "." KIWI_STR(KIWI_VERSION_PATCH) diff --git a/include/kiwi/TypoTransformer.h b/include/kiwi/TypoTransformer.h index 550eaa06..f13576be 100644 --- a/include/kiwi/TypoTransformer.h +++ b/include/kiwi/TypoTransformer.h @@ -256,6 +256,11 @@ namespace kiwi return *this; } + void setContinualTypoCost(float threshold) + { + continualTypoThreshold = threshold; + } + TypoTransformer copyWithNewContinualTypoCost(float threshold) const; PreparedTypoTransformer prepare() const diff --git a/src/KTrie.cpp b/src/KTrie.cpp index b7a67bb4..08cfbf7f 100644 --- a/src/KTrie.cpp +++ b/src/KTrie.cpp @@ -579,6 +579,7 @@ size_t kiwi::splitByTrie( size_t n = 0; Vector> candidates; auto* curNode = trie.root(); + auto* curNodeForContinualTypo = trie.root(); auto* nextNode = trie.root(); Vector> continualTypoRightNodes; @@ -898,7 +899,7 @@ size_t kiwi::splitByTrie( goto continueFor; } - auto* curNodeForContinualTypo = curNode; + curNodeForContinualTypo = curNode; nextNode = curNode->template nextOpt(trie, c); while (!nextNode) // if curNode has no exact next node, goto fail { From 2b4b707aa2cbde7177794a6745b84e312648d05e Mon Sep 17 00:00:00 2001 From: bab2min Date: Sat, 13 Apr 2024 15:54:14 +0900 Subject: [PATCH 6/8] updated evaluator & runner --- tools/evaluator_main.cpp | 8 +++++--- tools/runner.cpp | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/evaluator_main.cpp b/tools/evaluator_main.cpp index 4eeaa5d3..fb7130c9 100644 --- a/tools/evaluator_main.cpp +++ b/tools/evaluator_main.cpp @@ -12,13 +12,13 @@ using namespace std; using namespace kiwi; int doEvaluate(const string& modelPath, const string& output, const vector& input, - bool normCoda, bool zCoda, bool useSBG, float typoCostWeight) + bool normCoda, bool zCoda, bool useSBG, float typoCostWeight, bool cTypo) { try { tutils::Timer timer; Kiwi kw = KiwiBuilder{ modelPath, 1, BuildOption::default_, useSBG }.build( - typoCostWeight > 0 ? basicTypoSet : withoutTypo + typoCostWeight > 0 ? (cTypo ? DefaultTypoSet::basicTypoSetWithContinual : DefaultTypoSet::basicTypoSet) : DefaultTypoSet::withoutTypo ); if (typoCostWeight > 0) kw.setTypoCostWeight(typoCostWeight); @@ -97,6 +97,7 @@ int main(int argc, const char* argv[]) SwitchArg withoutZCoda{ "", "wzcoda", "without z-coda", false }; SwitchArg useSBG{ "", "sbg", "use SkipBigram", false }; ValueArg typoTolerant{ "", "typo", "make typo-tolerant model", false, 0.f, "float"}; + SwitchArg cTypo{ "", "ctypo", "make continual-typo-tolerant model", false }; UnlabeledMultiArg files{ "files", "evaluation set files", true, "string" }; cmd.add(model); @@ -106,6 +107,7 @@ int main(int argc, const char* argv[]) cmd.add(withoutZCoda); cmd.add(useSBG); cmd.add(typoTolerant); + cmd.add(cTypo); try { @@ -116,6 +118,6 @@ int main(int argc, const char* argv[]) cerr << "error: " << e.error() << " for arg " << e.argId() << endl; return -1; } - return doEvaluate(model, output, files.getValue(), !withoutNormCoda, !withoutZCoda, useSBG, typoTolerant); + return doEvaluate(model, output, files.getValue(), !withoutNormCoda, !withoutZCoda, useSBG, typoTolerant, cTypo); } diff --git a/tools/runner.cpp b/tools/runner.cpp index ea6c3414..78954de2 100644 --- a/tools/runner.cpp +++ b/tools/runner.cpp @@ -28,7 +28,7 @@ int run(const string& modelPath, bool benchmark, const string& output, const str { tutils::Timer timer; size_t lines = 0, bytes = 0; - Kiwi kw = KiwiBuilder{ modelPath, 1, BuildOption::default_, sbg }.build(typos > 0 ? basicTypoSet : withoutTypo); + Kiwi kw = KiwiBuilder{ modelPath, 1, BuildOption::default_, sbg }.build(typos > 0 ? DefaultTypoSet::basicTypoSet : DefaultTypoSet::withoutTypo); cout << "Kiwi v" << KIWI_VERSION_STRING << endl; if (tolerance) From 48982474a601d8a48b8499215d88bb2421d2cb8f Mon Sep 17 00:00:00 2001 From: bab2min Date: Sat, 13 Apr 2024 15:54:28 +0900 Subject: [PATCH 7/8] updated KiwiJava binding --- bindings/java/kiwi_java.cpp | 3 +- bindings/java/kr/pe/bab2min/Kiwi.java | 2 +- bindings/java/kr/pe/bab2min/KiwiBuilder.java | 123 +++++++++++++++++++ bindings/java/kr/pe/bab2min/KiwiTest.java | 32 +++++ 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/bindings/java/kiwi_java.cpp b/bindings/java/kiwi_java.cpp index d84a9586..5db1f758 100644 --- a/bindings/java/kiwi_java.cpp +++ b/bindings/java/kiwi_java.cpp @@ -546,7 +546,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) jni::define() .template ctor<>() - .template method<&JTypoTransformer::addTypo>("_addTypo"), + .template method<&JTypoTransformer::addTypo>("_addTypo") + .template method<&JTypoTransformer::setContinualTypoCost>("_setContinualTypoCost"), jni::define() .template ctor() diff --git a/bindings/java/kr/pe/bab2min/Kiwi.java b/bindings/java/kr/pe/bab2min/Kiwi.java index 895d224f..5aa1b54c 100644 --- a/bindings/java/kr/pe/bab2min/Kiwi.java +++ b/bindings/java/kr/pe/bab2min/Kiwi.java @@ -12,7 +12,7 @@ public class Kiwi implements AutoCloseable { private long _inst; - final private static String _version = "0.17.0"; + final private static String _version = "0.17.1"; public static class Match { final static public int none = 0, diff --git a/bindings/java/kr/pe/bab2min/KiwiBuilder.java b/bindings/java/kr/pe/bab2min/KiwiBuilder.java index bafb84a9..29d1c4e0 100644 --- a/bindings/java/kr/pe/bab2min/KiwiBuilder.java +++ b/bindings/java/kr/pe/bab2min/KiwiBuilder.java @@ -63,6 +63,7 @@ public boolean isAlive() { @Override public native void close() throws Exception; public native void _addTypo(String orig, String error, float cost, byte convVowel); + public native void _setContinualTypoCost(float cost); public TypoTransformer addTypo(String orig, String error, float cost, byte convVowel) { _addTypo(orig, error, cost, convVowel); @@ -77,6 +78,11 @@ public TypoTransformer addTypo(String[] orig, String[] error, float cost, byte c } return this; } + + public TypoTransformer setContinualTypoCost(float cost) { + _setContinualTypoCost(cost); + return this; + } } public KiwiBuilder(long _inst) { @@ -219,4 +225,121 @@ public Kiwi build(TypoTransformer typos) { .addTypo(new String[]{"을", "를"}, new String[]{"을", "를"}, 2.f, CondVowel.none) .addTypo(new String[]{"ㅣ워", "ㅣ어", "ㅕ"}, new String[]{"ㅣ워", "ㅣ어", "ㅕ"}, 1.5f, CondVowel.none); + + final public static TypoTransformer continualTypoSet = new TypoTransformer() + .setContinualTypoCost(1.f) + .addTypo(new String[]{"ᆪ"}, new String[]{"ᆨᆺ", "ᆨᆻ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆬ"}, new String[]{"ᆫᆽ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆭ"}, new String[]{"ᆫᇂ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆰ"}, new String[]{"ᆯᆨ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆱ"}, new String[]{"ᆯᆷ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆲ"}, new String[]{"ᆯᆸ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆳ"}, new String[]{"ᆯᆺ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆴ"}, new String[]{"ᆯᇀ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆵ"}, new String[]{"ᆯᇁ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆶ"}, new String[]{"ᆯᇂ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆹ"}, new String[]{"ᆸᆺ", "ᆸᆻ"}, 1e-12f, CondVowel.none); + + final public static TypoTransformer basicTypoSetWithContinual = new TypoTransformer() + .addTypo(new String[]{"ㅐ", "ㅔ"}, new String[]{"ㅐ", "ㅔ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ㅐ", "ㅔ"}, new String[]{"ㅒ", "ㅖ"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"ㅒ", "ㅖ"}, new String[]{"ㅐ", "ㅔ"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"ㅒ", "ㅖ"}, new String[]{"ㅒ", "ㅖ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ㅚ", "ㅙ", "ㅞ"}, new String[]{"ㅚ", "ㅙ", "ㅞ", "ㅐ", "ㅔ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ㅝ"}, new String[]{"ㅗ", "ㅓ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ㅟ", "ㅢ"}, new String[]{"ㅣ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"위", "의"}, new String[]{"이"}, Float.POSITIVE_INFINITY, CondVowel.none) + .addTypo(new String[]{"위", "의"}, new String[]{"이"}, 1.f, CondVowel.any) + .addTypo(new String[]{"자", "쟈"}, new String[]{"자", "쟈"}, 1.f, CondVowel.none) + .addTypo(new String[]{"재", "쟤"}, new String[]{"재", "쟤"}, 1.f, CondVowel.none) + .addTypo(new String[]{"저", "져"}, new String[]{"저", "져"}, 1.f, CondVowel.none) + .addTypo(new String[]{"제", "졔"}, new String[]{"제", "졔"}, 1.f, CondVowel.none) + .addTypo(new String[]{"조", "죠", "줘"}, new String[]{"조", "죠", "줘"}, 1.f, CondVowel.none) + .addTypo(new String[]{"주", "쥬"}, new String[]{"주", "쥬"}, 1.f, CondVowel.none) + .addTypo(new String[]{"차", "챠"}, new String[]{"차", "챠"}, 1.f, CondVowel.none) + .addTypo(new String[]{"채", "챼"}, new String[]{"채", "챼"}, 1.f, CondVowel.none) + .addTypo(new String[]{"처", "쳐"}, new String[]{"처", "쳐"}, 1.f, CondVowel.none) + .addTypo(new String[]{"체", "쳬"}, new String[]{"체", "쳬"}, 1.f, CondVowel.none) + .addTypo(new String[]{"초", "쵸", "춰"}, new String[]{"초", "쵸", "춰"}, 1.f, CondVowel.none) + .addTypo(new String[]{"추", "츄"}, new String[]{"추", "츄"}, 1.f, CondVowel.none) + .addTypo(new String[]{"유", "류"}, new String[]{"유", "류"}, 1.f, CondVowel.none) + .addTypo(new String[]{"므", "무"}, new String[]{"므", "무"}, 1.f, CondVowel.none) + .addTypo(new String[]{"브", "부"}, new String[]{"브", "부"}, 1.f, CondVowel.none) + .addTypo(new String[]{"프", "푸"}, new String[]{"프", "푸"}, 1.f, CondVowel.none) + .addTypo(new String[]{"르", "루"}, new String[]{"르", "루"}, 1.f, CondVowel.none) + .addTypo(new String[]{"러", "뤄"}, new String[]{"러", "뤄"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆩ", "ᆪ"}, new String[]{"ᆨ", "ᆩ", "ᆪ"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"ᆬ", "ᆭ"}, new String[]{"ᆫ", "ᆬ", "ᆭ"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"ᆰ", "ᆱ", "ᆲ", "ᆳ", "ᆴ", "ᆵ", "ᆶ"}, new String[]{"ᆯ", "ᆰ", "ᆱ", "ᆲ", "ᆳ", "ᆴ", "ᆵ", "ᆶ"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"ᆺ", "ᆻ"}, new String[]{"ᆺ", "ᆻ"}, 1.f, CondVowel.none) + + .addTypo(new String[]{"안"}, new String[]{"않"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"맞추", "맞히"}, new String[]{"맞추", "맞히"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"맞춰", "맞혀"}, new String[]{"맞춰", "맞혀"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"받치", "바치", "받히"}, new String[]{"받치", "바치", "받히"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"받쳐", "바쳐", "받혀"}, new String[]{"받쳐", "바쳐", "받혀"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"던", "든"}, new String[]{"던", "든"}, 1.f, CondVowel.none) + .addTypo(new String[]{"때", "데"}, new String[]{"때", "데"}, 1.5f, CondVowel.none) + .addTypo(new String[]{"빛", "빚"}, new String[]{"빛", "빚"}, 1.f, CondVowel.none) + + .addTypo(new String[]{"ᆮ이", "지"}, new String[]{"ᆮ이", "지"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆮ여", "져"}, new String[]{"ᆮ여", "져"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᇀ이", "치"}, new String[]{"ᇀ이", "치"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᇀ여", "쳐"}, new String[]{"ᇀ여", "쳐"}, 1.f, CondVowel.none) + + .addTypo(new String[]{"ᄀ", "ᄁ"}, new String[]{"ᄀ", "ᄁ"}, 1.f, CondVowel.applosive) + .addTypo(new String[]{"ᄃ", "ᄄ"}, new String[]{"ᄃ", "ᄄ"}, 1.f, CondVowel.applosive) + .addTypo(new String[]{"ᄇ", "ᄈ"}, new String[]{"ᄇ", "ᄈ"}, 1.f, CondVowel.applosive) + .addTypo(new String[]{"ᄉ", "ᄊ"}, new String[]{"ᄉ", "ᄊ"}, 1.f, CondVowel.applosive) + .addTypo(new String[]{"ᄌ", "ᄍ"}, new String[]{"ᄌ", "ᄍ"}, 1.f, CondVowel.applosive) + + .addTypo(new String[]{"ᇂᄒ", "ᆨᄒ", "ᇂᄀ"}, new String[]{"ᇂᄒ", "ᆨᄒ", "ᇂᄀ"}, 1.f, CondVowel.none) + + .addTypo(new String[]{"ᆨᄂ", "ᆩᄂ", "ᆪᄂ", "ᆿᄂ", "ᆼᄂ"}, new String[]{"ᆨᄂ", "ᆩᄂ", "ᆪᄂ", "ᆿᄂ", "ᆼᄂ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆨᄆ", "ᆩᄆ", "ᆪᄆ", "ᆿᄆ", "ᆼᄆ"}, new String[]{"ᆨᄆ", "ᆩᄆ", "ᆪᄆ", "ᆿᄆ", "ᆼᄆ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆨᄅ", "ᆩᄅ", "ᆪᄅ", "ᆿᄅ", "ᆼᄅ", "ᆼᄂ",}, new String[]{"ᆨᄅ", "ᆩᄅ", "ᆪᄅ", "ᆿᄅ", "ᆼᄅ", "ᆼᄂ",}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆮᄂ", "ᆺᄂ", "ᆻᄂ", "ᆽᄂ", "ᆾᄂ", "ᇀᄂ", "ᆫᄂ"}, new String[]{"ᆮᄂ", "ᆺᄂ", "ᆻᄂ", "ᆽᄂ", "ᆾᄂ", "ᇀᄂ", "ᆫᄂ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆮᄆ", "ᆺᄆ", "ᆻᄆ", "ᆽᄆ", "ᆾᄆ", "ᇀᄆ", "ᆫᄆ"}, new String[]{"ᆮᄆ", "ᆺᄆ", "ᆻᄆ", "ᆽᄆ", "ᆾᄆ", "ᇀᄆ", "ᆫᄆ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆮᄅ", "ᆺᄅ", "ᆻᄅ", "ᆽᄅ", "ᆾᄅ", "ᇀᄅ", "ᆫᄅ", "ᆫᄂ",}, new String[]{"ᆮᄅ", "ᆺᄅ", "ᆻᄅ", "ᆽᄅ", "ᆾᄅ", "ᇀᄅ", "ᆫᄅ", "ᆫᄂ",}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆸᄂ", "ᆹᄂ", "ᇁᄂ", "ᆷᄂ"}, new String[]{"ᆸᄂ", "ᆹᄂ", "ᇁᄂ", "ᆷᄂ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆸᄆ", "ᆹᄆ", "ᇁᄆ", "ᆷᄆ"}, new String[]{"ᆸᄆ", "ᆹᄆ", "ᇁᄆ", "ᆷᄆ"}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆸᄅ", "ᆹᄅ", "ᇁᄅ", "ᆷᄅ", "ᆷᄂ",}, new String[]{"ᆸᄅ", "ᆹᄅ", "ᇁᄅ", "ᆷᄅ", "ᆷᄂ",}, 1.f, CondVowel.none) + .addTypo(new String[]{"ᆫᄅ", "ᆫᄂ", "ᆯᄅ", "ᆯᄂ"}, new String[]{"ᆫᄅ", "ᆫᄂ", "ᆯᄅ", "ᆯᄂ"}, 1.f, CondVowel.none) + + .addTypo(new String[]{"ᆨᄋ", "ᄀ"}, new String[]{"ᆨᄋ", "ᄀ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆩᄋ", "ᄁ"}, new String[]{"ᆩᄋ", "ᄁ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆫᄋ", "ᆫᄒ", "ᄂ"}, new String[]{"ᆫᄋ", "ᆫᄒ", "ᄂ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆬᄋ", "ᆫᄌ"}, new String[]{"ᆬᄋ", "ᆫᄌ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆭᄋ", "ᄂ"}, new String[]{"ᆭᄋ", "ᄂ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆮᄋ", "ᄃ"}, new String[]{"ᆮᄋ", "ᄃ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆯᄋ", "ᆯᄒ", "ᄅ"}, new String[]{"ᆯᄋ", "ᆯᄒ", "ᄅ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆰᄋ", "ᆯᄀ"}, new String[]{"ᆰᄋ", "ᆯᄀ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆰᄒ", "ᆯᄏ"}, new String[]{"ᆰᄒ", "ᆯᄏ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆷᄋ", "ᄆ"}, new String[]{"ᆷᄋ", "ᄆ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆸᄋ", "ᄇ"}, new String[]{"ᆸᄋ", "ᄇ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆺᄋ", "ᄉ"}, new String[]{"ᆺᄋ", "ᄉ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆻᄋ", "ᆺᄉ", "ᄊ"}, new String[]{"ᆻᄋ", "ᆺᄉ", "ᄊ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆽᄋ", "ᄌ"}, new String[]{"ᆽᄋ", "ᄌ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆾᄋ", "ᆾᄒ", "ᆽᄒ", "ᄎ"}, new String[]{"ᆾᄋ", "ᆾᄒ", "ᆽᄒ", "ᄎ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᆿᄋ", "ᆿᄒ", "ᆨᄒ", "ᄏ"}, new String[]{"ᆿᄋ", "ᆿᄒ", "ᆨᄒ", "ᄏ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᇀᄋ", "ᇀᄒ", "ᆮᄒ", "ᄐ"}, new String[]{"ᇀᄋ", "ᇀᄒ", "ᆮᄒ", "ᄐ"}, 1.f, CondVowel.vowel) + .addTypo(new String[]{"ᇁᄋ", "ᇁᄒ", "ᆸᄒ", "ᄑ"}, new String[]{"ᇁᄋ", "ᇁᄒ", "ᆸᄒ", "ᄑ"}, 1.f, CondVowel.vowel) + + .addTypo(new String[]{"은", "는"}, new String[]{"은", "는"}, 2.f, CondVowel.none) + .addTypo(new String[]{"을", "를"}, new String[]{"을", "를"}, 2.f, CondVowel.none) + + .addTypo(new String[]{"ㅣ워", "ㅣ어", "ㅕ"}, new String[]{"ㅣ워", "ㅣ어", "ㅕ"}, 1.5f, CondVowel.none) + .setContinualTypoCost(1.f) + .addTypo(new String[]{"ᆪ"}, new String[]{"ᆨᆺ", "ᆨᆻ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆬ"}, new String[]{"ᆫᆽ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆭ"}, new String[]{"ᆫᇂ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆰ"}, new String[]{"ᆯᆨ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆱ"}, new String[]{"ᆯᆷ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆲ"}, new String[]{"ᆯᆸ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆳ"}, new String[]{"ᆯᆺ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆴ"}, new String[]{"ᆯᇀ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆵ"}, new String[]{"ᆯᇁ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆶ"}, new String[]{"ᆯᇂ"}, 1e-12f, CondVowel.none) + .addTypo(new String[]{"ᆹ"}, new String[]{"ᆸᆺ", "ᆸᆻ"}, 1e-12f, CondVowel.none); + } diff --git a/bindings/java/kr/pe/bab2min/KiwiTest.java b/bindings/java/kr/pe/bab2min/KiwiTest.java index 39156662..326ff831 100644 --- a/bindings/java/kr/pe/bab2min/KiwiTest.java +++ b/bindings/java/kr/pe/bab2min/KiwiTest.java @@ -123,6 +123,38 @@ public void testTypos() throws Exception { assertEquals(tokens[5].form, "어"); } + @Test + public void testContinualTypos() throws Exception { + System.gc(); + KiwiBuilder builder = new KiwiBuilder(modelPath); + Kiwi kiwi = builder.build(KiwiBuilder.continualTypoSet); + + Kiwi.Token[] tokens = kiwi.tokenize("프로그래미", Kiwi.Match.allWithNormalizing); + System.out.println(Arrays.deepToString(tokens)); + assertEquals(tokens[0].form, "프로그램"); + assertEquals(tokens[1].form, "이"); + + tokens = kiwi.tokenize("프로그래믈", Kiwi.Match.allWithNormalizing); + System.out.println(Arrays.deepToString(tokens)); + assertEquals(tokens[0].form, "프로그램"); + assertEquals(tokens[1].form, "을"); + + tokens = kiwi.tokenize("오늘사무시레서", Kiwi.Match.allWithNormalizing); + System.out.println(Arrays.deepToString(tokens)); + assertEquals(tokens[1].form, "사무실"); + assertEquals(tokens[2].form, "에서"); + + tokens = kiwi.tokenize("법원이 기가캤다.", Kiwi.Match.allWithNormalizing); + System.out.println(Arrays.deepToString(tokens)); + assertEquals(tokens[2].form, "기각"); + assertEquals(tokens[3].form, "하"); + + tokens = kiwi.tokenize("하나도 업써.", Kiwi.Match.allWithNormalizing); + System.out.println(Arrays.deepToString(tokens)); + assertEquals(tokens[2].form, "없"); + assertEquals(tokens[3].form, "어"); + } + @Test public void testBlocklist() throws Exception { System.gc(); From 4d4a5e333f8ec478f524f29affc2eff96cd9b6f1 Mon Sep 17 00:00:00 2001 From: bab2min Date: Sat, 13 Apr 2024 15:54:46 +0900 Subject: [PATCH 8/8] added eval data for continual typos --- eval_data/web_with_cont_typos.txt | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 eval_data/web_with_cont_typos.txt diff --git a/eval_data/web_with_cont_typos.txt b/eval_data/web_with_cont_typos.txt new file mode 100644 index 00000000..f8b3f061 --- /dev/null +++ b/eval_data/web_with_cont_typos.txt @@ -0,0 +1,97 @@ +좀 아쉽지만 같이 할만한 취미 새로 찾아보는것도 재미임. 아니면 나가서 놀다와라 시저내야지머. 좀/MAG 아쉽/VA-I 지만/EC 같이/MAG 하/VV ᆯ/ETM 만/NNB 하/XSA ᆫ/ETM 취미/NNG 새로/MAG 찾아보/VV 는/ETM 것/NNB 도/JX 재미/NNG 이/VCP ᆷ/EF ./SF 아니/VCN 면/EC 나가/VV 어서/EC 놀/VV 다/EC 오/VV 어라/EC 시전/NNG 하/VV 어야지/EF 뭐/IC ./SF +애초에 카프리 될까?? 애초/NNG 에/JKB 카풀/NNG 이/JKC 되/VV ᆯ까/EF ??/SF +사내에서 서로 호가미 있으면 어떻하든 엮이게 되어이씀 회식 일찍 끝나서 아쉬운 마으메 두리서 2차 갔다가 진짜 애휴. 사내/NNG 에서/JKB 서로/MAG 호감/NNG 이/JKS 있/VA 으면/EC 어떡하/VV 든/EC 엮이/VV 게/EC 되/VV 어/EC 있/VX 음/EF 회식/NNG 일찍/MAG 끝나/VV 어서/EC 아쉽/VA-I 은/ETM 마음/NNG 에/JKB 둘/NR 이서/JKS 2/SN 차/NNB 가/VV 었/EP 다가/EC 진짜/MAG 에휴/IC ./SF +결혼하면 경제권 다 뺐기고 우울하다는 말만 드렀는데 난 아주 많이 행우나 인가 봄... 결혼/NNG 하/XSV 면/EC 경제/NNG 권/XSN 다/MAG 뺏기/VV 고/EC 우울/NNG 하/XSA 다는/ETM 말/NNG 만/JX 듣/VV-I 었/EP 는데/EC 나/NP ᆫ/JX 아주/MAG 많이/MAG 행운아/NNG 이/VCP ᆫ가/EC 보/VX ᆷ/EF .../SF +빨간새기 성능도 굉장히 조흔데 초로기가 난이도가 쉬워서 가려짐. 난무스킬이 커멘드에따라 막타가 두종류로 나뉘고 돌진스키레 연계까지 되서 확정딜이 제일 쎔. 빨간색/NNG 이/JKS 성능/NNG 도/JX 굉장히/MAG 좋/VA 은데/EC 초록이/NNG 가/JKS 난이도/NNG 가/JKS 쉽/VA-I 어서/EC 가리/VV 어/EC 지/VX ᆷ/EF ./SF 난무/NNG 스킬/NNG 이/JKS 커맨드/NNG 에/JKB 따르/VV 어/EC 막타/NNG 가/JKS 두/MM 종류/NNG 로/JKB 나뉘/VV 고/EC 돌진/NNG 스킬/NNG 에/JKB 연계/NNG 까지/JX 되/VV 어서/EC 확정/NNG 딜/NNG 이/JKS 제일/MAG 쎄/VA ᆷ/EF ./SF +뭔가 하고는 있는데 개임개발자는 아니다 보니, 뭐/NP 이/VCP ᆫ가/EC 하/VV 고/EC 는/JX 있/VX 는데/EC 게임/NNG 개발자/NNG 는/JX 아니/VCN 다/EC 보/VX 니/EC ,/SP +원래 스마트한 엘리트 근육맨 케릭터인데 약때문에 멍청해지는거라서 에매함.. 원래/MAG 스마트/NNG 하/XSV ᆫ/ETM 엘리트/NNG 근육맨/NNG 캐릭터/NNG 이/VCP ᆫ데/EC 약/NNG 때문/NNB 에/JKB 멍청하/VA 어/EC 지/VX 는/ETM 거/NNB 이/VCP 라서/EC 애매/XR 하/XSA ᆷ/EF ../SF +어차피 실리콘은 안쪼그로 삽입되서 핏줄 잘보인다 목에 스카프나 초커같은거 두르고 있으면 의심해야댐. 어차피/MAG 실리콘/NNG 은/JX 안쪽/NNG 으로/JKB 삽입/NNG 되/XSV 어서/EC 핏줄/NNG 잘/MAG 보이/VV ᆫ다/EF 목/NNG 에/JKB 스카프/NNG 나/JC 초커/NNG 같/VA 은/ETM 거/NNB 두르/VV 고/EC 있/VX 으면/EC 의심/NNG 하/XSV 어야/EC 되/VV ᆷ/EF ./SF +요즈멘 두발 자유화라 염색도 되나보던대요 나땐 2샌치 스포츠에 남중 남고 ㅠㅠ 요즘 학생들 부럼네요. 요즘/NNG 에/JKB ᆫ/JX 두발/NNG 자유/NNG 화/XSN 이/VCP 라/EC 염색/NNG 도/JX 되/VV 나/EC 보/VX 던데요/EC 나/NP 때/NNG ᆫ/JX 2/SN 센치/NNB 스포츠/NNG 에/JKB 남중/NNG 남고/NNG ㅠㅠ/SW 요즘/NNG 학생/NNG 들/XSN 부럽/VA-I 네요/EF ./SF +그랜져로 업글함 그랜저/NNG 로/JKB 업글/NNG 하/XSV ᆷ/EF +중소의 특징은 위쪼근 가족 및 친족으로 체워져 이써서 밑층의 계급 구조가 야타. 규모도 잦 같은 데가 전무, 상무, 이사,부장 이런 나부랭이급이 만코 실무 계급인 과장, 대리, 계장 배정인원 구조는 또 저거서 역피라미드인 경우도 흔함. 그러다보니, 과장 하나 올라가는 계다늘 더 가파르게 만든 곳도 많다.. 중소/NNG 의/JKG 특징/NNG 은/JX 위/NNG 쪽/NNB 은/JX 가족/NNG 및/MAJ 친족/NNG 으로/JKB 채우/VV 어/EC 지/VX 어/EC 있/VX 어서/EC 밑/NNG 층/XSN 의/JKG 계급/NNG 구조/NNG 가/JKS 얇/VA 어/EF ./SF 규모/NNG 도/JX 좆/NNG 같/VA 은/ETM 데/NNB 가/JKS 전무/NNG ,/SP 상무/NNG ,/SP 이사/NNG ,/SP 부장/NNG 이런/MM 나부랭이/NNG 급/NNG 이/JKS 많/VA 고/EC 실무/NNG 계급/NNG 이/VCP ᆫ/ETM 과장/NNG ,/SP 대리/NNG ,/SP 계장/NNG 배정/NNG 인원/NNG 구조/NNG 는/JX 또/MAG 적/VA 어서/EC 역/XPN 피라미드/NNG 이/VCP ᆫ/ETM 경우/NNG 도/JX 흔하/VA ᆷ/EF ./SF 그러/VV 다/EC 보/VX 니/EC ,/SP 과장/NNG 하나/NR 올라가/VV 는/ETM 계단/NNG 을/JKO 더/MAG 가파르/VA 게/EC 만들/VV ᆫ/ETM 곳/NNG 도/JX 많/VA 다/EF ../SF +저게 주작같아보이면 중소를 아얘 경험안해본거 아프로도 이쪽으로 오지말고 그쪽에서 행보캐라 저것/NP 이/JKS 주작/NNG 같/VA 어/EC 보이/VV 면/EC 중소/NNG 를/JKO 아예/MAG 경험/NNG 안/MAG 하/VV 어/EC 보/VX ᆫ/ETM 거/NNB 앞/NNG 으로/JKB 도/JX 이/MM 쪽/NNB 으로/JKB 오/VV 지/EC 말/VX 고/EC 그쪽/NP 에서/JKB 행복/NNG 하/XSA 어라/EF +그럼 욕좀 할께. 정신차리고 꺼저라 제발. 그럼/IC 욕/NNG 좀/MAG 하/VV ᆯ께/EF ./SF 정신/NNG 차리/VV 고/EC 꺼지/VV 어라/EC 제발/MAG ./SF +이런상황에선 문제를 명화카게 정의하고 해결채글 재시 할 수 있는 사람이 인정을 받겠죠.. 매뉴얼 없다고 아몰랑 하는거보단. 이런/MM 상황/NNG 에서/JKB ᆫ/JX 문제/NNG 를/JKO 명확/XR 하/XSA 게/EC 정의/NNG 하/XSV 고/EC 해결책/NNG 을/JKO 제시/NNG 하/XSV ᆯ/ETM 수/NNB 있/VA 는/ETM 사람/NNG 이/JKS 인정/NNG 을/JKO 받/VV-R 겠/EP 죠/EF ../SF 매뉴얼/NNG 없/VA 다고/EC 아몰랑/NNP 하/VV 는/ETM 거/NNB 보다/JKB ᆫ/JX ./SF +얘를들면 사고를 쳐쓸때 내 선에서 처리해씀. 상사가 잘 되가냐? 물어보면 예/NNG 를/JKO 들/VV 면/EC 사고/NNG 를/JKO 치/VV 었/EP 을/ETM 때/NNG 나/NP 의/JKG 선/NNG 에서/JKB 처리/NNG 하/XSV 었/EP 음/EF ./SF 상사/NNG 가/JKS 잘/MAG 되/VV 어/EC 가/VX 냐/EF ?/SF 묻/VV-I 어/EC 보/VX 면/EC +솔지키 큰 업무들을 일개사워니 주먹구구식이다 나라면 이러케할텐대 평가하는것도 웃낀일임 솔직히/MAG 크/VA ᆫ/ETM 업무/NNG 들/XSN 을/JKO 일개/NNG 사원/NNG 이/JKS 주먹구구식/NNG 이/VCP 다/EF 나/NP 이/VCP 라면/EC 이렇/VA 게/EC 하/VV ᆯ/ETM 터/NNB 이/VCP ᆫ데/EC 평가/NNG 하/XSV 는/ETM 것/NNB 도/JX 웃기/VV ᆫ/ETM 일임/NNG +아안되!!! 그럼 진화해서 달기된다고!!!!! 아/IC 안/MAG 되/VV 어/EF !!!/SF 그럼/MAJ 진화/NNG 하/XSV 어서/EC 닭/NNG 이/JKC 되/VV ᆫ다고/EC !!!!!/SF +어차피 점잔케 말해도 못 아라 들을탠데 무슨 상과니 있을까 어차피/MAG 점잖/VA 게/EC 말/NNG 하/XSV 어도/EC 못/MAG 알/VV 어/EC 듣/VV-I 을/ETM 터/NNB 이/VCP ᆫ데/EC 무슨/MM 상관/NNG 이/JKS 있/VV 을까/EF +이거 월래 뒤펴니 이리 엑티브했냐? 이거/NP 원래/MAG 뒤편/NNG 이/JKS 이리/MAG 액티브/NNG 하/XSV 었/EP 냐/EF ?/SF +두가지 요소를 이야기하고 시픈데 첫쨰로 피로골저릐 시대라고 생가캄 두/MM 가지/NNB 요소/NNG 를/JKO 이야기/NNG 하/XSV 고/EC 싶/VX 은데/EC 첫째/NR 로/JKB 피로/NNG 골절/NNG 의/JKG 시대/NNG 이/VCP 라고/EC 생각/NNG 하/XSV ᆷ/EF +물론 거기에 휩슬린 네가 말하는 민감한 중간층도 있겠지만 물론/MAG 거기/NP 에/JKB 휩쓸리/VV ᆫ/ETM 너/NP 가/JKS 말/NNG 하/XSV 는/ETM 민감/XR 하/XSA ᆫ/ETM 중간/NNG 층/XSN 도/JX 있/VV 겠/EP 지만/EC +반응이 즉각 오는게 아니라 자기객관화 안돼면 고생만 오지게 하다가 욕보는 요시겁.. 반응/NNG 이/JKS 즉각/MAG 오/VV 는/ETM 것/NNB 이/JKC 아니/VCN 라/EC 자기/NP 객관/NNG 화/XSN 안/MAG 되/VV 면/EC 고생/NNG 만/JX 오지/VA 게/EC 하/VV 다가/EC 욕보/VV 는/ETM 요식업/NNG ../SF +근데 저기는 직원수도 더 만코 프렌차이즈치킨과 다르게 직접준비하는게 만흘거라 좀 아슬아슬할듯 근데/MAJ 저기/NP 는/JX 직원/NNG 수/NNG 도/JX 더/MAG 많/VA 고/EC 프랜차이즈/NNG 치킨/NNG 과/JKB 다르/VA 게/EC 직접/MAG 준비/NNG 하/XSV 는/ETM 것/NNB 이/JKS 많/VA 을/ETM 거/NNB 이/VCP 라/EC 좀/MAG 아슬아슬/MAG 하/XSA ᆯ/ETM 듯/NNB +이거 옜날에 다른 탄산보다 싸서 자주 맛나게 머거썼는데 이거/NP 옛날/NNG 에/JKB 다른/MM 탄산/NNG 보다/JKB 싸/VV 어서/EC 자주/MAG 맛나/VA 게/EC 먹/VV 었었/EP 는데/EC +롤러코스터 재대로 태우시네 ㅋㅋㅋㅋ 롤러코스터/NNG 제대로/MAG 태우/VV 시/EP 네/EF ㅋㅋㅋㅋ/SW +다들 생각한거랑틀려서 실망해찌 나도야 ㅋㅋㅋㅋㅋㅋㅋ 줜나웃기네 다/MAG 들/XSN 생각/NNG 하/XSV ᆫ/ETM 거/NNB 랑/JKB 틀리/VA 어서/EC 실망/NNG 하/XSV 었/EP 지/EF 나/NP 도/JX 야/JX ㅋㅋㅋㅋㅋㅋㅋ/SW 존나/MAG 웃기/VV 네/EF +어째 불나면 유리라도 깨서 연기라도 빼던가 구추를하던가해야할텐데 질시캐서 죽을것같이 불안하네 어째/MAG 불/NNG 나/VV 면/EC 유리/NNG 이/VCP 라도/EC 깨/VV 어서/EC 연기/NNG 이/VCP 라도/EC 빼/VV 든가/EC 구출/NNG 을/JKO 하/VV 든가/EC 하/VV 어야/EC 하/VX ᆯ/ETM 터/NNB 이/VCP ᆫ데/EC 질식/NNG 하/XSV 어서/EC 죽/VV 을/ETM 것/NNB 같이/JKB 불안/NNG 하/XSA 네/EF +실제로는 해상도 더 낮고 텍스쳐도 더 뭉게보일껄 실제로/MAG 는/JX 해상도/NNG 더/MAG 낮/VA 고/EC 텍스처/NNP 도/JX 더/MAG 뭉개/VV 어/EC 보이/VV ᆯ걸/EF +그걸 가만해도 그냥 몸치임. 무술 경려근 고사하고 건강용 체조만 했어도 저따위론 안함. 그거/NP ᆯ/JKO 감안/NNG 하/XSV 어도/EC 그냥/MAG 몸치/NNG 이/VCP ᆷ/EF ./SF 무술/NNG 경력/NNG 은/JX 고사하고/MAG 건강/NNG 용/XSN 체조/NNG 만/JX 하/VV 었/EP 어도/EC 저/MM 따위/NNB 로/JKB ᆫ/JX 안/MAG 하/VV ᆷ/EF ./SF +이걸 증명하면 모든 컴퓨터 보안쳬계가 바뀜. P=NP를 증명하는 순간 거이 모든 암호체계가 무너질텐데 이거/NP ᆯ/JKO 증명/NNG 하/XSV 면/EC 모든/MM 컴퓨터/NNG 보안/NNG 체계/NNG 가/JKS 바뀌/VV ᆷ/EF ./SF P/SL =/SW NP/SL 를/JKO 증명/NNG 하/XSV 는/ETM 순간/NNG 거의/MAG 모든/MM 암호/NNG 체계/NNG 가/JKS 무너지/VV ᆯ/ETM 터/NNB 이/VCP ᆫ데/EC +넘버 조회 해봐야 체굴인지 아닌지 알수잇다는데 넘버까지 조자카면 그냥 새거 없다고 보면 되겠네 넘버/NNG 조회/NNG 하/XSV 어/EC 보/VX 어야/EC 채굴/NNG 이/VCP ᆫ지/EC 아니/VCN ᆫ지/EC 알/VV ᆯ/ETM 수/NNB 있/VA 다는데/EC 넘버/NNG 까지/JX 조작/NNG 하/XSV 면/EC 그냥/MAG 새/MM 거/NNB 없/VA 다고/EC 보/VV 면/EC 되/VV 겠/EP 네/EF +물로는씨서봤는대 초음파세처캐도괜찮나보넹 ㅋㅋㅋ 물/NNG 로/JKB 는/JX 씻/VV-R 어/EC 보/VX 었/EP 는데/EC 초음파/NNG 세척/NNG 하/XSV 어도/EC 괜찮/VA 나/EC 보/VX 넹/EF ㅋㅋㅋ/SW +걍 사기꾼 색기들 집다닌데 못자바 쳐넣나 걍/MAG 사기꾼/NNG 새끼/NNG 들/XSN 집단/NNG 이/VCP ᆫ데/EC 못/MAG 잡/VV-R 어/EC 처넣/VV 나/EC +이러다 나 진짜 하라버지가 되버려용 이러/VV 다/EC 나/NP 진짜/MAG 할아버지/NNG 가/JKC 되/VV 어/EC 버리/VX 어용/EF +기자드리 이런거 파해치는게 일 아닌가 기자/NNG 들/XSN 이/JKS 이런/MM 거/NNB 파헤치/VV 는/ETM 것/NNB 이/JKS 일/NNG 아니/VCN ᆫ가/EF +마! 이쁜이센드위치 모르나! 저거 왜 다 노갓냐ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 마/NNG !/SF 이쁜이/NNP 샌드위치/NNG 모르/VV 나/EF !/SF 저거/NP 왜/MAG 다/MAG 녹/VV 었/EP 냐/EF ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ/SW +좀 꾸미면 왜 애꺼 뻇어 입었냐 소리 나옴. 지금쯤 머하고 이쓸까 좀/MAG 꾸미/VV 면/EC 왜/MAG 애/NNG 꺼/NNB 뺏/VV-R 어/EC 입/VV-R 었/EP 냐/EC 소리/NNG 나오/VV ᆷ/EF ./SF 지금/NNG 쯤/XSN 뭐/NP 하/VV 고/EC 있/VX 을까/EF +마이클 젝슨 소환영 촉메제로 봉인지정? 마이클/NNP 잭슨/NNP 소환/NNG 용/XSN 촉매제/NNG 로/JKB 봉인/NNG 지정/NNG ?/SF +정말 자기관리를 잘해서 피부도 몸매도 조흐면.... 저기에 글쓸떄까지 감안히 놔둘리 없다.... 정말/MAG 자기/NNG 관리/NNG 를/JKO 잘/MAG 하/XSV 어서/EC 피부/NNG 도/JX 몸매/NNG 도/JX 좋/VA 으면/EC ..../SF 저기/NP 에/JKB 글/NNG 쓰/VV ᆯ/ETM 때/NNG 까지/JX 가만히/MAG 놔두/VV ᆯ/ETM 리/NNB 없/VA 다/EF ..../SF +20대까진 내구도가 달아도 수리하면 다 수리되눈 느낌임 30/100까지 달아도 휴식하면 100/100이 되는데 30대부턴 휴식을 해도 최대 내구도가 닳는 느끼미야 운동은 그 차감치를 줄여주는거고 때에 따라선 회복되기도 하는거 같아 20/SN 대/NNB 까지/JX ᆫ/JX 내구도/NNG 가/JKS 닳/VV 어도/EC 수리/NNG 하/XSV 면/EC 다/MAG 수리/NNG 되/XSV 는/ETM 느낌/NNG 이/VCP ᆷ/EF 30/100/W_SERIAL 까지/JX 닳/VV 어도/EC 휴식/NNG 하/XSV 면/EC 100/100/W_SERIAL 이/JKC 되/VV 는데/EC 30/SN 대/NNB 부터/JX ᆫ/JX 휴식/NNG 을/JKO 하/VV 어도/EC 최대/NNG 내구도/NNG 가/JKS 닳/VV 는/ETM 느낌/NNG 이/VCP 야/EC 운동/NNG 은/JX 그/MM 차감/NNG 치/XSN 를/JKO 줄이/VV 어/EC 주/VX 는/ETM 거/NNB 이/VCP 고/EC 때/NNG 에/JKB 따르/VV 어서/EC ᆫ/JX 회복/NNG 되/XSV 기/ETN 도/JX 하/VX 는/ETM 거/NNB 같/VA 어/EF +참고로 박준형 학력 캘리포니아 주립대 그래픽디자인 학사 참고/NNG 로/JKB 박준형/NNP 학력/NNG 캘리포니아/NNP 주립대/NNG 그래픽/NNG 디자인/NNG 학사/NNG +인구수 많으면 뭐하는데 도시는 빛더미지, 코로나때도 제대로된 지원도 거의 업썼지 인구/NNG 수/NNG 많/VA 으면/EC 뭐/NP 하/VV 는데/EF 도시/NNG 는/JX 빚더미/NNG 이/VCP 지/EC ,/SP 코로나/NNP 때/NNG 도/JX 제대로/MAG 되/VV ᆫ/ETM 지원/NNG 도/JX 거의/MAG 없/VA 었/EP 지/EF +부평공장 빠지면 역근처 재에하고는 부평 한동안 진짜 휑하겠내요 서구가는 길목은 진짜 건물들 다오래됬던데 부평/NNP 공장/NNG 빠지/VV 면/EC 역/NNG 근처/NNG 제외/NNG 하고/JKB 는/JX 부평/NNP 한/MM 동안/NNG 진짜/MAG 휑하/VA 겠/EP 네요/EF 서구/NNP 가/VV 는/ETM 길목/NNG 은/JX 진짜/MAG 건물/NNG 들/XSN 다/MAG 오래되/VA 었/EP 던/ETM 데/NNB +다른 기관의 도움을 받는다는게 툭까놓고 말해서 협조요청에 일정조저레 안전대채게 분배방법에 그러면 그 도움받는동안 그 다른기관이 하던일은 어떠케 될것인가부터 어떻게 얼마나 누가 일을 할건가까지 전부 다 결정해야하는데 회의에 회의를 거져 다 정해져도 최소 한두달 이상 걸림 진짜. 관료제라는게 정말로 비리를 방지하기 위해 권하늘 쪼개고 쪼개고 또 쪼개버렸기 때무네 말처럼 그렇게 쉽게 협조를 할 수 있는게 아니라.. 다른/MM 기관/NNG 의/JKG 도움/NNG 을/JKO 받/VV-R 는다는/ETM 것/NNB 이/JKS 툭/MAG 까놓/VV 고/EC 말/NNG 하/XSV 어서/EC 협조/NNG 요청/NNG 에/JKB 일정/NNG 조절/NNG 에/JKB 안전/NNG 대책/NNG 에/JKB 분배/NNG 방법/NNG 에/JKB 그러면/MAJ 그/MM 도움/NNG 받/VV-R 는/ETM 동안/NNG 그/MM 다른/MM 기관/NNG 이/JKS 하/VV 던/ETM 일/NNG 은/JX 어떻/VA 게/EC 되/VV ᆯ/ETM 것/NNB 이/VCP ᆫ가/EF 부터/JX 어떻/VA 게/EC 얼마나/MAG 누/NP 가/JKS 일/NNG 을/JKO 하/VV ᆯ/ETM 거/NNB 이/VCP ᆫ가/EC 까지/JX 전부/MAG 다/MAG 결정/NNG 하/XSV 어야/EC 하/VX 는데/EC 회의/NNG 에/JKB 회의/NNG 를/JKO 거치/VV 어/EC 다/MAG 정하/VV 어/EC 지/VX 어도/EC 최소/NNG 한두/MM 달/NNG 이상/NNG 걸리/VV ᆷ/ETN 진짜/MAG ./SF 관료/NNG 제/XSN 이/VCP 라는/ETM 것/NNB 이/JKS 정말로/MAG 비리/NNG 를/JKO 방지/NNG 하/XSV 기/ETN 위하/VV 어/EC 권한/NNG 을/JKO 쪼개/VV 고/EC 쪼개/VV 고/EC 또/MAG 쪼개/VV 어/EC 버리/VX 었/EP 기/ETN 때문/NNB 에/JKB 말/NNG 처럼/JKB 그렇/VA 게/EC 쉽/VA-I 게/EC 협조/NNG 를/JKO 하/VV ᆯ/ETM 수/NNB 있/VA 는/ETM 것/NNB 이/JKC 아니/VCN 라/EC ../SF +공대 매커니즈므로는 정상이지. 깔아뭉게는걸 즐겨함.... 공대/NNG 메커니즘/NNG 으로/JKB 는/JX 정상/NNG 이/VCP 지/EF ./SF 깔아뭉개/VV 는/ETM 거/NNB ᆯ/JKO 즐기/VV 어/EC 하/VX ᆷ/EF ..../SF +살려죠 살리/VV 어/EC 주/VX 어/EF +존나 빡치는 일 있었나보다 싶음.. 사회 생활하면서 빡치는 일 이써서 왠만하면 참는데 저정도면 뭐.. 존나/MAG 빡/MAG 치/VV 는/ETM 일/NNG 있/VA 었/EP 나/EC 보/VX 다/EF 싶/VX 음/EF ../SF 사회/NNG 생활/NNG 하/VV 면서/EC 빡/MAG 치/VV 는/ETM 일/NNG 있/VA 어서/EC 웬만/XR 하/XSA 면/EC 참/VV 는데/EC 저/MM 정도/NNG 이/VCP 면/EC 뭐/IC ../SF +구매팀 한명이 총대매고 공론화시킬려고 저럴수도 이씀. 구매/NNG 팀/NNG 한/MM 명/NNB 이/JKS 총대/NNG 메/VV 고/EC 공론/NNG 화/XSN 시키/VV ᆯ려고/EC 저러/VV ᆯ/ETM 수/NNB 도/JX 있/VA 음/EF ./SF +내 커리어를 재물로 바쳐서 신이블 갈군다! 나/NP 의/JKG 커리어/NNG 를/JKO 제물/NNG 로/JKB 바치/VV 어서/EC 신입/NNG 을/JKO 갈구/NNG ᆫ다/EF !/SF +저거는 혼바바니면 무례한개 맞다 저거/NP 는/JX 혼밥/NNG 아니/VCN 면/EC 무례/NNG 하/XSA ᆫ/ETM 것/NNB 이/JKS 맞/VV 다/EF +나 첨에 입대할때 자대 양주?로 배치받앗는데 와 경기도네 ㅋㅋ 개꿀 햇는데 체전방이던데 ㅋㅋㅋㅋㅋㅋ 나/NP 첨/NNG 에/JKB 입대/NNG 하/XSV ᆯ/ETM 때/NNG 자대/NNG 양주/NNP ?/SF 로/NNG 배치/NNG 받/VV-R 었/EP 는데/EC 와/IC 경기도/NNP 이/VCP 네/XSN ㅋㅋ/SW 개/XPN 꿀/NNG 하/XSV 었/EP 는데/EC 최/XPN 전방/NNG 이/VCP 던데/EF ㅋㅋㅋㅋㅋㅋ/SW +다만 당시니 제시한 초단기간 근로자가 많은 직군 중에 카페, 편의점를 제외하면 본문에 언급되는 20대 초반이 주로 하는 직군이 아니자너. 다만/MAG 당신/NP 이/JKS 제시/NNG 하/XSV ᆫ/ETM 초/XPN 단기간/NNG 근로자/NNG 가/JKS 많/VA 은/ETM 직군/NNG 중/NNB 에/JKB 카페/NNG ,/SP 편의점/NNG 을/JKO 제외/NNG 하/XSV 면/EC 본문/NNG 에/JKB 언급/NNG 되/XSV 는/ETM 20/SN 대/NNB 초반/NNG 이/JKS 주로/MAG 하/VV 는/ETM 직군/NNG 이/JKS 아니/VCN 잖어/EF ./SF +그냥 기보니 안되있따는거지 몬 호구야 직장도 잘하고 못하고를 떠나 일 배울생각도 업시 월급루팡짓 할려면 짤려 그냥/MAG 기본/NNG 이/JKC 안/MAG 되/VV 어/EC 있/VX 다는/ETM 거/NNB 이/VCP 지/EC 뭔/MM 호구/NNG 야/JX 직장/NNG 도/JX 잘/MAG 하/XSV 고/EC 못하/VX 고/EC 를/JKO 떠나/VV 어/EC 일/NNG 배우/VV ᆯ/ETM 생각/NNG 도/JX 없이/MAG 월급루팡/NNG 짓/XSN 하/VV ᆯ려면/EC 짤리/VV 어/EF +그리고 집 화장시른 자주 청소하는거 아니면 그렇게 튄 오줌 떄문에 화장실에서 냄세도 나게되고 그리고/MAJ 집/NNG 화장실/NNG 은/JX 자주/MAG 청소/NNG 하/XSV 는/ETM 거/NNB 아니/VCN 면/EC 그렇/VA 게/EC 튀/VV ᆫ/ETM 오줌/NNG 때문/NNB 에/JKB 화장실/NNG 에서/JKB 냄새/NNG 도/JX 나/VV 게/EC 되/VV 고/EC +내가 알고 있는 회사만해도 지방에 있던 공장들은 업샐수 있으면 업쌔고 해외로 뺴버리고 연구인력, 새로 국내에 지을 생산라인은 죄다 경기도로 옯겨버림 나/NP 가/JKS 알/VV 고/EC 있/VX 는/ETM 회사/NNG 만/JX 하/VV 어도/EC 지방/NNG 에/JKB 있/VA 던/ETM 공장/NNG 들/XSN 은/JX 없애/VV ᆯ/ETM 수/NNB 있/VA 으면/EC 없애/VV 고/EC 해외/NNG 로/JKB 빼/VV 어/EC 버리/VX 고/EC 연구/NNG 인력/NNG ,/SP 새로/MAG 국내/NNG 에/JKB 짓/VV-I 을/ETM 생산/NNG 라인/NNG 은/JX 죄다/MAG 경기도/NNP 로/JKB 옮기/VV 어/EC 버리/VX ᆷ/EF +자신들은 안죶된다고 생각하은데 왜 그러게써 자신/NNG 들/XSN 은/JX 안/MAG 좆/NNG 되/XSV ᆫ다고/EC 생각/NNG 하/XSV 는데/EC 왜/MAG 그러/VV 겠/EP 어/EF +실제론 독재자가 수도권에 이권을 많이 가진 사라밀 확율이 더 높겠지 실제로/MAG ᆫ/JX 독재자/NNG 가/JKS 수도/NNG 권/XSN 에/JKB 이권/NNG 을/JKO 많이/MAG 가지/VV ᆫ/ETM 사람/NNG 이/VCP ᆯ/ETM 확률/NNG 이/JKS 더/MAG 높/VA 겠/EP 지/EF +없에려면 기간을 좀 두고해야겠지 막없에고 땡인게 아니니까 없애/VV 려면/EC 기간/NNG 을/JKO 좀/MAG 두/VV 고/EC 하/VV 어야/EC 겠/EP 지/EF 막/MAG 없애/VV 고/EC 땡/NNG 이/VCP ᆫ/ETM 것/NNB 이/JKC 아니/VCN 니까/EC +소리를 좀 노펴봐 소리/NNG 를/JKO 좀/MAG 높이/VV 어/EC 보/VX 어/EF +문디새끼 니 속꼬만 사랐나? 문디/NNG 새끼/NNG 니/NP 속/VV 고/EC 만/JX 살/VV 었/EP 나/EC ?/SF +물론 드래곤보리 해마다 게임이 나와재끼고 굿즈도 겁나 파라재끼는 컨탠츠인건 감안하고.. 물론/MAG 드래곤볼/NNP 이/JKS 해/NNG 마다/JX 게임/NNG 이/JKS 나오/VV 어/EC 제끼/VX 고/EC 굿즈/NNG 도/JX 겁나/MAG 팔/VV 어/EC 제끼/VV 는/ETM 컨텐츠/NNG 이/VCP ᆫ/ETM 거/NNB ᆫ/JX 감안/NNG 하/XSV 고/EC ../SF +존나 시원하게 까네요 소기 뚤리는 느낌 ㅋㅋㅋ 존나/MAG 시원/XR 하/XSA 게/EC 까/VV 네/EF 요/JX 속/NNG 이/JKS 뚫리/VV 는/ETM 느낌/NNG ㅋㅋㅋ/SW +버스랑 왼쪽에 잇는 차 위치를 봐라 은행중인가 버스/NNG 랑/JC 왼쪽/NNG 에/JKB 있/VA 는/ETM 차/NNG 위치/NNG 를/JKO 보/VV 어라/EC 운행/NNG 중/NNB 이/VCP ᆫ가/EF +면허는 따고 지껄이는 거냐. 블박차량이 파란불 직진 신호에 직진하고 있는데 면허/NNG 는/JX 따/VV 고/EC 지껄이/VV 는/ETM 거/NNB 이/VCP 냐/EF ./SF 블박/NNG 차량/NNG 이/JKS 파란불/NNG 직진/NNG 신호/NNG 에/JKB 직진/NNG 하/XSV 고/EC 있/VX 는데/EC +막판에 헨들도 꺾어서 저 상황에서 사거리 전방주시 미흡이 과실로 저굥댈 수 있나.. 막판/NNG 에/JKB 핸들/NNG 도/JX 꺾/VV 어서/EC 저/MM 상황/NNG 에서/JKB 사거리/NNG 전방/NNG 주시/NNG 미흡/NNG 이/JKS 과실/NNG 로/JKB 적용/NNG 되/XSV ᆯ/ETM 수/NNB 있/VA 나/EC ../SF +군의관 뽑는기준이 어떠킬레 저러는건가요? 군의관/NNG 뽑/VV-R 는/ETM 기준/NNG 이/JKS 어떻/VA 길래/EC 저러/VV 는/ETM 거/NNB 이/VCP ᆫ가/EF 요/JX ?/SF +애초에 직업군인형 의사를 꼿을수없으니 더더욱 그렇고 애초/NNG 에/JKB 직업/NNG 군인/NNG 형/XSN 의사/NNG 를/JKO 꽂/VV 을/ETM 수/NNB 없/VA 으니/EC 더더욱/MAG 그렇/VA 고/EC +하지만 우리나라에서는 그런 생가글 가질수있는 환경이 충부니 조성되있다는걸 아라야한다는겁니다. 그러므로 원글에 나오는 디자인한 디자이너의 태도가 잘못됬다는거죠. 님한테 뭘 바라는건 없습니다. 다만 폭 넖게 생가카시라는거죠. 하지만/MAJ 우리나라/NNG 에서/JKB 는/JX 그런/MM 생각/NNG 을/JKO 가지/VV ᆯ/ETM 수/NNB 있/VA 는/ETM 환경/NNG 이/JKS 충분히/MAG 조성/NNG 되/XSV 어/EC 있/VX 다는/ETM 거/NNB ᆯ/JKO 알/VV 어야/EC 하/VX ᆫ다는/ETM 거/NNB 이/VCP ᆸ니다/EF ./SF 그러므로/MAJ 원/XPN 글/NNG 에/JKB 나오/VV 는/ETM 디자인/NNG 하/XSV ᆫ/ETM 디자이너/NNG 의/JKG 태도/NNG 가/JKS 잘못/MAG 되/XSV 었/EP 다는/ETM 거/NNB 이/VCP 죠/EF ./SF 님/NNG 한테/JKB 뭐/NP ᆯ/JKO 바라/VV 는/ETM 거/NNB ᆫ/JX 없/VA 습니다/EF ./SF 다만/MAG 폭/NNG 넓/VA 게/EC 생각/NNG 하/XSV 시/EP 라는/ETM 거/NNB 이/VCP 죠/EF ./SF +이러니까 사적재재에 사람들이 열광하지 그냥 조하하겠냐 이러/VV 니까/EC 사/NNG 적/XSN 제재/NNG 에/JKB 사람/NNG 들/XSN 이/JKS 열광/NNG 하/XSV 지/EF 그냥/MAG 좋아하/VV 겠/EP 냐/EF +보복을 위해 있는게 교도소가 아니니까 거기다 고통을 주는.방버블 저런 원시저긴 수단을 쓸 피료도 없고 관리가 안되는 환경일수록 좋은 일더 없다고 생각함. 내부 치안이단 교도소내의 계급사회던 간에 악영할게.늘어난다고 봄 보복/NNG 을/JKO 위하/VV 어/EC 있/VX 는/ETM 것/NNB 이/JKS 교도소/NNG 가/JKC 아니/VCN 니까/EC 거기/NP 다/JX 고통/NNG 을/JKO 주/VV 는/ETM ./SF 방법/NNG 을/JKO 저런/MM 원시/NNG 적/XSN 이/VCP ᆫ/ETM 수단/NNG 을/JKO 쓰/VV ᆯ/ETM 필요/NNG 도/JX 없/VA 고/EC 관리/NNG 가/JKS 안/MAG 되/VV 는/ETM 환경/NNG 이/VCP ᆯ수록/EC 좋/VA 은/ETM 일/NNG 도/JX 없/VA 다고/EC 생각/NNG 하/XSV ᆷ/ETN ./SF 내부/NNG 치안/NNG 이/VCP 든/EC 교도소/NNG 내/NNB 의/JKG 계급/NNG 사회/NNG 이/VCP 든/EC 간/NNB 에/JKB 악용/NNG 하/XSV ᆯ게/EF ./SF 늘어나/VV ᆫ다고/EC 보/VV ᆷ/ETN +문제는 더빙이 무슨 매화마다 드믄드믄 되어있는게 있는거보고 어이업서서 ㅋㅋㅋ 문제/NNG 는/JX 더빙/NNG 이/JKS 무슨/MM 매/MM 화/NNG 마다/JX 드문드문/MAG 되/VV 어/EC 있/VX 는/ETM 것/NNB 이/JKS 있/VV 는/ETM 거/NNB 보/VV 고/EC 어이없/VA 어서/EC ㅋㅋㅋ/SW +솔직히 디즈니+ 들어오고 나서 기묘한 이야기나 러브데스로봇처럼 하나라도 머 어디 약파리할 물건이 당췌 안보임. 솔직히/MAG 디즈니+/NNP 들어오/VV 고/EC 나/VX 어서/EC 기묘/XR 하/XSA ᆫ/ETM 이야기/NNG 나/JC 러브/NNG 데스/NNG 로봇/NNG 처럼/JKB 하나/NR 이/VCP 라도/EC 뭐/IC 어디/IC 약팔이/NNG 하/XSV ᆯ/ETM 물건/NNG 이/JKS 당최/MAG 안/MAG 보이/VV ᆷ/EF ./SF +그건 아는데 지들끼리 짝짜꿍 하더라도 개같지만 말은 되는 똥딱개 논리가 법적으로 뭔가 있을거 아냐 저새끼가 똥먹었습니다. 똥 무든 빵이랑 그게 입으로 들어가는 사진 다 찌겄습니다 했는데 안먹었다고 잡아때려면 그냥 우기는게 아니라 그거/NP ᆫ/JX 알/VV 는데/EC 지/NP 들/XSN 끼리/XSN 짝자꿍/NNG 하/VV 더라도/EC 개/NNG 같/VA 지만/EC 말/NNG 은/JX 되/VV 는/ETM 똥/NNG 닦개/NNG 논리/NNG 가/JKS 법/NNG 적/XSN 으로/JKB 뭐/NP 이/VCP ᆫ가/EF 있/VA 을/ETM 거/NNB 아니/VCN 야/EF 저/MM 새끼/NNG 가/JKS 똥/NNG 먹/VV 었/EP 습니다/EF ./SF 똥/NNG 묻/VV-R 은/ETM 빵/NNG 이랑/JC 그것/NP 이/JKS 입/NNG 으로/JKB 들어가/VV 는/ETM 사진/NNG 다/MAG 찍/VV 었/EP 습니다/EF 하/VV 었/EP 는데/EC 안/MAG 먹/VV 었/EP 다고/EC 잡아떼/VV 려면/EC 그냥/MAG 우기/VV 는/ETM 것/NNB 이/JKC 아니/VCN 라/EC +거 아들래미라는 놈이 인두껍을 뒤지버쓴 새끼구만 거/IC 아들내미/NNG 이/VCP 라는/ETM 놈/NNB 이/JKS 인두겁/NNG 을/JKO 뒤집어쓰/VV ᆫ/ETM 새끼/NNG 구만/EF +저 경우에는 반대로 안전밸트를 메서 살았으면 산채로 불타 주근거잖아 저/MM 경우/NNG 에/JKB 는/JX 반대/NNG 로/JKB 안전벨트/NNG 를/JKO 매/VV 어서/EC 살/VV 었/EP 으면/EC 살/VV ᆫ/ETM 채/NNB 로/JKB 불타/VV 어/EC 죽/VV 은/ETM 거/NNB 잖아/EF +자리바꾸다 여자짝궁 울려본적이씀 자리/NNG 바꾸/VV 다/EC 여자/NNG 짝꿍/NNG 울리/VV 어/EC 보/VX ᆫ/ETM 적/NNB 있/VA 음/EF +못생겨도 성격만 좋으면 왠만하면 자기 짝 차자서 결혼한다 못생기/VV 어도/EC 성격/NNG 만/JX 좋/VA 으면/EC 웬만/XR 하/XSA 면/EC 자기/NP 짝/NNG 찾/VV 어서/EC 결혼/NNG 하/XSV ᆫ다/EF +기업이라는 전재 자체가 집단 이기주의를 형성하기 아주 좋은 집단임. 이익을 추구하기 위해 모인 집단이고 특히 경제적 이익에 모든게 집중되어 있기 때문에 최대한 손해를 적게 보기 위한 의사결정의 경우에는 아주 쉽게 하븨을 도출 할 수 있음. 기업/NNG 이/VCP 라는/ETM 전제/NNG 자체/NNG 가/JKS 집단/NNG 이기주의/NNG 를/JKO 형성/NNG 하/XSV 기/ETN 아주/MAG 좋/VA 은/ETM 집단/NNG 이/VCP ᆷ/EF ./SF 이익/NNG 을/JKO 추구/NNG 하/XSV 기/ETN 위하/VV 어/EC 모이/VV ᆫ/ETM 집단/NNG 이/VCP 고/EC 특히/MAG 경제/NNG 적/XSN 이익/NNG 에/JKB 모든/MM 것/NNB 이/JKS 집중/NNG 되/XSV 어/EC 있/VX 기/ETN 때문/NNB 에/JKB 최대한/MAG 손해/NNG 를/JKO 적/VA 게/EC 보/VV 기/ETN 위하/VV ᆫ/ETM 의사/NNG 결정/NNG 의/JKG 경우/NNG 에/JKB 는/JX 아주/MAG 쉽/VA-I 게/EC 합의/NNG 를/JKO 도출/NNG 하/XSV ᆯ/ETM 수/NNB 있/VA 음/EF ./SF +잡아 뗼 수 있으면 사과 안하고 더퍼버리는 거고, 명백한 증거가 퍼져있으면 바로 사과 박고 이후에 이미지 개선하는 게 좋지. 잡/VV-R 어/EC 떼/VV ᆯ/ETM 수/NNB 있/VA 으면/EC 사과/NNG 안/MAG 하/VV 고/EC 덮/VV 어/EC 버리/VX 는/ETM 거/NNB 이/VCP 고/EC ,/SP 명백/XR 하/XSA ᆫ/ETM 증거/NNG 가/JKS 퍼지/VV 어/EC 있/VX 으면/EC 바로/MAG 사과/NNG 박/VV 고/EC 이후/NNG 에/JKB 이미지/NNG 개선/NNG 하/XSV 는/ETM 것/NNB 이/JKS 좋/VA 지/EF ./SF +대충 복재인격은 나인가 고찰 대충/MAG 복제/NNG 인격/NNG 은/JX 나/NP 이/VCP ᆫ가/EC 고찰/NNG +베스킨라빈스 경쟁 업체 나뚜르도 롯데고. 그나마 뚜레주르 정도나 선택지 정도 배스킨라빈스/NNP 경쟁/NNG 업체/NNG 나뚜루/NNP 도/JX 롯데/NNP 이/VCP 고/EC ./SF 그나마/MAG 뚜레쥬르/NNP 정도/NNG 나/JC 선택지/NNG 정도/NNG +빵공장 직원들... 재분은 저기서 다해먹는다고 됨 재분계에선 크나큰악임 문제는 차선택도 없다고 보면됨... 빵/NNG 공장/NNG 직원/NNG 들/XSN .../SF 제분/NNG 은/JX 저기/NP 서/JKB 다/MAG 하/VV 어/EC 먹/VX 는다고/EC 되/VV ᆷ/ETN 제분/NNG 계/XSN 에서/JKB ᆫ/JX 크나크/VA ᆫ/ETM 악/NNG 이/VCP ᆷ/EF 문제/NNG 는/JX 차선책/NNG 도/JX 없/VA 다고/EC 보/VV 면/EC 되/VV ᆷ/EF .../SF +안티벡신 시위 나가서 연설하기 안티/NNG 백신/NNG 시위/NNG 나가/VV 어서/EC 연설/NNG 하/XSV 기/ETN +나는 급여는 나자도 워라벨만 지켜주면 다니는데 어지간한 코스탁 기없도 안지켜주더라 나/NP 는/JX 급여/NNG 는/JX 낮/VA 어도/EC 워라밸/NNG 만/JX 지키/VV 어/EC 주/VX 면/EC 다니/VV 는데/EC 어지간/XR 하/XSA ᆫ/ETM 코스닥/NNG 기업/NNG 도/JX 안/MAG 지키/VV 어/EC 주/VX 더라/EF +근데 순수 시장놀리로만 접근하면 안돼는게.. 예를들어 우리나라 농업도 단가 안맞는다고 다 작살내고 외국에 의존했다간 근데/MAJ 순수/NNG 시장/NNG 논리/NNG 로/JKB 만/JX 접근/NNG 하/XSV 면/EC 안/MAG 되/VV 는/ETM 것/NNB 이/JKS ../SF 예/NNG 를/JKO 들/VV 어/EC 우리/NP 나라/NNG 농업/NNG 도/JX 단가/NNG 안/MAG 맞/VV 는다고/EC 다/MAG 작살내/VV 고/EC 외국/NNG 에/JKB 의존/NNG 하/XSV 었/EP 다가/EC ᆫ/JX +내가 지금 친척들이랑 다 인연끈코 사는데 저거 절대로 해주면 안됨 나/NP 가/JKS 지금/MAG 친척/NNG 들/XSN 이랑/JKB 다/MAG 인연/NNG 끊/VV 고/EC 살/VV 는데/EC 저거/NP 절대로/MAG 하/VV 어/EC 주/VX 면/EC 안/MAG 되/XSV ᆷ/EF +뭔가 집히는게 있어? 뭐/NP 이/VCP ᆫ가/EC 짚이/VV 는/ETM 것/NNB 이/JKS 있/VA 어/EF ?/SF +눈속에 뭍힌 동백꽃봉오리 눈/NNG 속/NNG 에/JKB 묻히/VV ᆫ/ETM 동백꽃/NNG 봉오리/NNG +엉덩이가 덮히는 캐쥬얼코디용 여성 하의다. 엉덩이/NNG 가/JKS 덮이/VV 는/ETM 캐주얼/NNG 코디/NNG 용/XSN 여성/NNG 하의/NNG 이/VCP 다/EF ./SF +그는 이내 짚히는 것이 있는 듯 눈빛을 반짝였다 그/NP 는/JX 이내/MAG 짚이/VV 는/ETM 것/NNB 이/JKS 있/VV 는/ETM 듯/NNB 눈/NNG 빛/NNG 을/JKO 반짝이/VV 었/EP 다/EF +그의 방문이 굳게 닫쳤다. 그/NP 의/JKG 방문/NNG 이/JKS 굳/VA-R 게/EC 닫히/VV 었/EP 다/EF ./SF +둘째, 눈꺼풀이 닽혔다 열렸다 하면서 눈동자의 윤활을 돕기위한 작은량의 눈물이 있으며 둘째/NR ,/SP 눈꺼풀/NNG 이/JKS 닫히/VV 었/EP 다/EC 열리/VV 었/EP 다/EC 하/VV 면서/EC 눈동자/NNG 의/JKG 윤활/NNG 을/JKO 돕/VV-I 기/ETN 위하/VV ᆫ/ETM 작/VA 은/ETM 량/NNG 의/JKG 눈물/NNG 이/JKS 있/VV 으며/EC +섬의 노래에서는 그를 「달빛과 조석의 잋힌 아이」, 또는 「달빛에 잋힌 아이」라 부르고 있다. 섬/NNG 의/JKG 노래/NNG 에서/JKB 는/JX 그/NP 를/JKO 「/SSO 달빛/NNG 과/JC 조석/NNG 의/JKG 잊히/VV ᆫ/ETM 아이/NNG 」/SSC ,/SP 또는/MAJ 「/SSO 달빛/NNG 에/JKB 잊히/VV ᆫ/ETM 아이/NNG 」/SSC 이/VCP 라/EC 부르/VV 고/EC 있/VX 다/EF ./SF +새로 방영되는 일일드라마는 얼키고설킨 가족의 사랑 이야기에 관한 것이다. 새로/MAG 방영/NNG 되/XSV 는/ETM 일일/NNG 드라마/NNG 는/JX 얽히고설키/VV ᆫ/ETM 가족/NNG 의/JKG 사랑/NNG 이야기/NNG 에/JKB 관하/VV ᆫ/ETM 것/NNB 이/VCP 다/EF ./SF +문에 부딛혀서 코피가 났다. 문/NNG 에/JKB 부딪히/VV 어서/EC 코피/NNG 가/JKS 나/VV 었/EP 다/EF ./SF +넘어지며 엉덩이를 바닥에 부디쳐 다쳤어. 넘어지/VV 며/EC 엉덩이/NNG 를/JKO 바닥/NNG 에/JKB 부딪히/VV 어/EC 다치/VV 었/EP 어/EF ./SF +요즘 중학생들의 장례희망... 나 때는 연예인, 운동선수였는데 다들 유투버네 요즘/NNG 중학생/NNG 들/XSN 의/JKG 장래/NNG 희망/NNG .../SF 나/NP 때/NNG 는/JX 연예인/NNG ,/SP 운동/NNG 선수/NNG 이/VCP 었/EP 는데/EC 다/MAG 들/XSN 유투버/NNG 이/VCP 네/EF \ No newline at end of file