|
| 1 | +# 한국어 Stemmer (Snowball 기반) |
| 2 | + |
| 3 | +[Snowball](https://snowballstem.org/) stemming 프레임워크를 기반으로 구축된 프로덕션 레벨 한국어 stemmer. 사전 기반 하이브리드 접근 방식으로 정확도를 대폭 개선했습니다. |
| 4 | + |
| 5 | +## 개요 |
| 6 | + |
| 7 | +이 프로젝트는 Snowball의 한국어 stemmer에 다음 기능을 추가합니다: |
| 8 | + |
| 9 | +1. **불규칙 용언 사전** — 1,097개 활용형 → 어근 매핑 (151개 어근 커버리지) |
| 10 | +2. **용언 접미사 제거** — 규칙 기반 한국어 용언 접미사 제거 (과거형, 연결 어미 등) |
| 11 | +3. **격 조사 제거** — 규칙 기반 한국어 격 조사 제거 (주격, 목적격, 부사격 등) |
| 12 | +4. **사전 기반 lookup 레이어** — 사전 등재어는 O(1) 즉시 반환, 미등재어는 Snowball stemmer로 폴백 |
| 13 | + |
| 14 | +## 아키텍처 |
| 15 | + |
| 16 | +``` |
| 17 | +입력 단어 |
| 18 | + │ |
| 19 | + ▼ |
| 20 | +┌─────────────────────┐ |
| 21 | +│ 격 조사 제거 │ ← 불규칙 사전 충돌 방지를 위해 가장 먼저 검사 |
| 22 | +│ (에서, 을/를, 와, │ (예: '와'가 조사 vs 불규칙 활용형 충돌) |
| 23 | +│ 이라도, 등) │ |
| 24 | +└─────────┬───────────┘ |
| 25 | + │ (조사 아님) |
| 26 | + ▼ |
| 27 | +┌─────────────────────┐ |
| 28 | +│ 불규칙 용언 사전 │ ← O(1) 해시 테이블 lookup |
| 29 | +│ lookup │ 1,097개 활용형 → 어근 매핑 |
| 30 | +└─────────┬───────────┘ |
| 31 | + │ (사전 없음) |
| 32 | + ▼ |
| 33 | +┌─────────────────────┐ |
| 34 | +│ 용언 접미사 제거 │ ← 긴 접미사부터 매칭 (최장 일치) |
| 35 | +│ │ 다, 았/었/였, 랐, 랐다, 습니다,어요, 등 |
| 36 | +└─────────┬───────────┘ |
| 37 | + │ (접미사 없음) |
| 38 | + ▼ |
| 39 | +┌─────────────────────┐ |
| 40 | +│ 내장 사전 lookup │ ← 사전 등재어는 원형 즉시 반환 |
| 41 | +└─────────┬───────────┘ |
| 42 | + │ (사전 없음) |
| 43 | + ▼ |
| 44 | +┌─────────────────────┐ |
| 45 | +│ Snowball stemmer │ ← 폴백: 기존 Snowball stemmer 처리 |
| 46 | +└─────────────────────┘ |
| 47 | +``` |
| 48 | + |
| 49 | +## 주요 기능 |
| 50 | + |
| 51 | +### 불규칙 용언 사전 |
| 52 | + |
| 53 | +주요 한국어 불규칙 용언 패턴 전체 커버리지: |
| 54 | + |
| 55 | +| 패턴 | 설명 | 예시 | |
| 56 | +|------|------|------| |
| 57 | +| ㅂ-불규칙 | ㅂ consonant shift | 갚다 → 갚 | |
| 58 | +| ㄷ-불규칙 | ㄷ consonant shift | 듣다 → 듣, 걷다 → 걷 | |
| 59 | +| ㄹ-불규칙 | ㄹ consonant shift | 올리다 → 올, 마다 → 마 | |
| 60 | +| ㅅ-불규칙 | ㅅ consonant shift | 짓다 → 짓 | |
| 61 | +| ㅆ-불규칙 | ㅆ consonant shift | 바쁘다 → 바쁘 | |
| 62 | +| ㅎ-불규칙 | ㅎ consonant shift | 귀찮다 → 귀찮 | |
| 63 | +| ㄴ-불규칙 | ㄴ consonant shift | 낫다 → 낫 | |
| 64 | +| 특수 | 하다, 크다, 좋다 | 합니다 → 하, 크다 → 크 | |
| 65 | + |
| 66 | +### 용언 접미사 제거 |
| 67 | + |
| 68 | +| 접미사 | 유형 | 예시 | |
| 69 | +|--------|------|------| |
| 70 | +| 다 | 동사/형용사 종결 | 먹다 → 먹, 좋다 → 좋 | |
| 71 | +| 았/었/였 | 과거 시제 | 먹었다 → 먹, 했다 → 하 | |
| 72 | +| 랐 | 과거 시제 (ㄹ-불규칙) | 올랐다 → 올 | |
| 73 | +| 랐다 | 과거 시제 (+다) | 올랐다 → 올 | |
| 74 | +| 습니다 | 겸양체 | 합니다 → 하 | |
| 75 | +| 어요 | 해요체 | 먹어요 → 먹 | |
| 76 | +| 고, 니, 니까, 에서 | 연결 어미 | 먹고 → 먹, 가니 → 가 | |
| 77 | + |
| 78 | +### 격 조사 제거 |
| 79 | + |
| 80 | +| 조사 | 유형 | 예시 | |
| 81 | +|------|------|------| |
| 82 | +| 에서 | 장소를 나타내는 조사 | 학교에서 → 학교 | |
| 83 | +| 을/를 | 목적격을 나타내는 조사 | 책을 → 책 | |
| 84 | +| 의 | 소유를 나타내는 조사 | 사람의 → 사람 | |
| 85 | +| 와/과 | 동시를 나타내는 조사 | 친구와 → 친구 | |
| 86 | +| 에, 로, 으로 | 방향/장소를 나타내는 조사 | 학교에 → 학교 | |
| 87 | +| 도, 만 | 강조 조사 | 가족도 → 가족, 일만 → 일 | |
| 88 | +| 이라도,조차도 | 복합 격 조사 | 가족이라도 → 가족 | |
| 89 | +| 까지, 부터, 처럼 | 범위/비교 조사 | 물건까지 → 물건 | |
| 90 | + |
| 91 | +## 사용법 |
| 92 | + |
| 93 | +```python |
| 94 | +from snowballstemmer.korean_stemmer_dict import KoreanStemmerDict |
| 95 | + |
| 96 | +# 내장 사전 (119개 단어)으로 초기화 |
| 97 | +stemmer = KoreanStemmerDict(dict_source='builtin') |
| 98 | + |
| 99 | +# 단일 단어 stem |
| 100 | +print(stemmer.stem('학교에서')) # 학교 |
| 101 | +print(stemmer.stem('받았습니다')) # 받 |
| 102 | +print(stemmer.stem('올랐다')) # 올 |
| 103 | +print(stemmer.stem('짓다')) # 짓 |
| 104 | +print(stemmer.stem('가족이라도')) # 가족 |
| 105 | + |
| 106 | +# 배치 stem |
| 107 | +words = ['학교에서', '책을', '친구와', '했습니다'] |
| 108 | +print(stemmer.stemWords(words)) # ['학교', '책', '친구', '했'] |
| 109 | +``` |
| 110 | + |
| 111 | +## 테스트 결과 |
| 112 | + |
| 113 | +### 전체: 20/20 통과 (100%) ✅ |
| 114 | + |
| 115 | +| 항목 | 결과 | |
| 116 | +|------|------| |
| 117 | +| 내장 사전 (동사/형용사/명사/부사/조사) | 100% | |
| 118 | +| kiwipiepy 사전 | 통과 | |
| 119 | +| 성능 벤치마크 | 334,031 words/sec | |
| 120 | + |
| 121 | +### 용언 접미사 제거 (23/23 통과) |
| 122 | + |
| 123 | +| 입력 | 출력 | 상태 | |
| 124 | +|------|------|------| |
| 125 | +| 받았다 | 받 | ✓ | |
| 126 | +| 올랐다 | 올 | ✓ | |
| 127 | +| 짓다 | 짓 | ✓ | |
| 128 | +| 놀다 | 놀 | ✓ | |
| 129 | +| 했습니다 | 했 | ✓ | |
| 130 | +| 했어요 | 했 | ✓ | |
| 131 | +| 좋아요 | 좋 | ✓ | |
| 132 | + |
| 133 | +### 격 조사 제거 (13/13 통과) |
| 134 | + |
| 135 | +| 입력 | 출력 | 상태 | |
| 136 | +|------|------|------| |
| 137 | +| 학교에서 | 학교 | ✓ | |
| 138 | +| 가족도 | 가족 | ✓ | |
| 139 | +| 학교를 | 학교 | ✓ | |
| 140 | +| 사람의 | 사람 | ✓ | |
| 141 | +| 친구와 | 친구 | ✓ | |
| 142 | +| 가족이라도 | 가족 | ✓ | |
| 143 | + |
| 144 | +## 성능 |
| 145 | + |
| 146 | +| 방법 | 속도 | |
| 147 | +|------|------| |
| 148 | +| 사전 lookup stemmer | **334,031 words/sec** | |
| 149 | +| 기존 Snowball stemmer | 211,817 words/sec | |
| 150 | +| **속도 향상** | **1.58x** | |
| 151 | + |
| 152 | +사전 등재어가 높은 텍스트에서 사전 lookup 레이어가 상당한 속도 향상을 제공합니다. 사전에 등재된 단어는 Snowball stemmer를 호출하지 않고 즉시 반환됩니다. |
| 153 | + |
| 154 | +## 프로젝트 구조 |
| 155 | + |
| 156 | +``` |
| 157 | +snowball-korean/ |
| 158 | +├── python/ |
| 159 | +│ └── snowballstemmer/ |
| 160 | +│ ├── korean_stemmer.py # 컴파일된 Snowball stemmer (korean.sbl → Python) |
| 161 | +│ ├── korean_stemmer_dict.py # 사전 기반 stemmer (본 프로젝트) |
| 162 | +│ └── generate_irregular_dict.py # 불규칙 용언 사전 생성 스크립트 |
| 163 | +├── data/ |
| 164 | +│ ├── irregular_verb_dict.json # 1,097개 활용형 → 어근 매핑 |
| 165 | +│ ├── korean_dict.json # 병합된 내장 + kiwipiepy 사전 |
| 166 | +│ └── korean_dict.pkl # 병합된 사전 pickle 형식 |
| 167 | +├── tests/ |
| 168 | +│ ├── test_korean_stemmer_dict.py # 사전 stemmer 단위 테스트 |
| 169 | +│ ├── korean/ |
| 170 | +│ │ ├── voc.txt # 테스트 어휘 (32개 단어) |
| 171 | +│ │ └── expected_output.txt # 기대 stemmer 출력 |
| 172 | +│ └── benchmark_korean_stemmer.py # 성능 벤치마크 |
| 173 | +├── scripts/ |
| 174 | +│ └── load_dict.py # kiwipiepy 사전 → JSON/Pickle 변환 |
| 175 | +├── IMPLEMENT-v2.md # 상세 구현 노트 |
| 176 | +├── README.md # English documentation |
| 177 | +└── README-ko.md # 이 파일 |
| 178 | +``` |
| 179 | + |
| 180 | +## 구현 노트 |
| 181 | + |
| 182 | +### 인코딩 고려사항 |
| 183 | + |
| 184 | +원본 Snowball stemmer는 among 테이블에서 jamo-level stringdef를 사용하지만, 한국어 텍스트는 일반적으로 합성 Hangul 음절을 사용합니다. 이 프로젝트는 다음 방식으로 이를 처리합니다: |
| 185 | + |
| 186 | +1. **사전 기반 접근**: 사전 등재어는 사전에서 직접 매칭하여 among 테이블을 우회 |
| 187 | +2. **규칙 기반 접미사 제거**: 접미사 패턴을 문자 수준에서 정의하여 합성/분해 형태 모두 처리 |
| 188 | +3. **불규칙 용언 사전**: 활용형 → 어근 매핑을 사전 계산하여 among 테이블 매칭 불필요 |
| 189 | + |
| 190 | +### 설계 결정 |
| 191 | + |
| 192 | +1. **불규칙 사전보다 격 조사 먼저 검사**: 격 조사(`와` 등)가 불규칙 활용형으로 잘못 매칭되는 충돌 방지 |
| 193 | +2. **긴 매칭 우선**: 접미사/조사를 길이 내림차순으로 검사하여 부분 매칭 방지 (예: `에서`가 `에`보다 먼저) |
| 194 | +3. **Snowball로 폴백**: 사전/규칙으로 커버되지 않는 단어는 기존 Snowball stemmer로 처리하여 최대 커버리지 확보 |
| 195 | + |
| 196 | +### 불규칙 용언 사전 생성 |
| 197 | + |
| 198 | +불규칙 용언 사전은 `generate_irregular_dict.py`로 프로그램적으로 생성됩니다. 각 불규칙 용언 패턴은 어근과 변환 규칙으로 정의되며, 스크립트가 모든 가능한 활용형을 자동 생성합니다. |
| 199 | + |
| 200 | +```python |
| 201 | +# 패턴 정의 예시 |
| 202 | +{ |
| 203 | + 'verb': '올리다', |
| 204 | + 'root': '올', |
| 205 | + 'pattern': 'ㄹ-불규칙', |
| 206 | + 'conjugations': { |
| 207 | + '올랐다': '올', |
| 208 | + '올랐어요': '올', |
| 209 | + '올랐습니다': '올', |
| 210 | + # ... 더 많은 활용형 |
| 211 | + } |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +## 테스트 실행 |
| 216 | + |
| 217 | +테스트 스위트 실행: |
| 218 | + |
| 219 | +```bash |
| 220 | +cd tests |
| 221 | +python3 -m unittest test_korean_stemmer_dict -v |
| 222 | +``` |
| 223 | + |
| 224 | +성능 벤치마크 실행: |
| 225 | + |
| 226 | +```bash |
| 227 | +python3 tests/benchmark_korean_stemmer.py |
| 228 | +``` |
| 229 | + |
| 230 | +## 관련 링크 |
| 231 | + |
| 232 | +- [Snowball](https://snowballstem.org/) — 원본 stemming 프레임워크 |
| 233 | +- [KiwiPy](https://github.com/bab2min/kiwipy) — 한국어 형태소 분석기 (사전 소스) |
| 234 | +- [PR #1](https://github.com/nethippo/snowball/pull/1) — upstream 제출용 Pull Request |
| 235 | + |
| 236 | +## 라이선스 |
| 237 | + |
| 238 | +이 프로젝트는 부모 Snowball 프로젝트의 라이선스를 따릅니다. |
0 commit comments