简介:这份韩语词汇表资源面向韩语学习者、语言研究者及需要构建韩语词典数据的开发者,提供从原始词条到结构化词典的完整数据集合。资源包共5个文件,以json词典数据、txt单词列表和md说明文档为主,压缩包约28.37MB,json文件分别对应词条与描述的前后版本,txt则保留含重复的原始单词列表及去重版本,便于对比与清洗。数据来源参考国立韩国语标准韩语词典的检索结果,覆盖单词与释义的对应关系,适合用于词频统计、词典构建、记忆卡片生成或自然语言处理实验。已有397人学习下载,读者可借此快速获得可复用的韩语词库基础,省去逐条抓取与整理的繁琐过程,直接投入学习或二次开发。
1. 韩语词汇表:从一份词表到可检索、可评测的语言资源
做韩语 NLP 的同行大概都遇到过这个场景:模型训练脚本跑通了,数据加载器也没报错,但分词结果里먹었습니다被切成了먹었습니다三段,词性标注却给整段打上了一个动词标签。排查半天,问题不在模型,而在词表——手里那份korean_wordlist要么是网上随手抓的,要么是某个教程里附带的几百行示例,既没有词性字段,也没有变形还原规则,更谈不上覆盖率统计。韩语词汇表这件事,看起来只是「一份单词列表」,实际决定了分词、形态还原、拼写检查、检索召回这几条链路的上限。它适合两类人:一是正在搭韩语文本预处理管线、需要一份可控词表做基线的人;二是已经有一份词表、但不确定它够不够用、该怎么验证和扩充的人。下面按「词表长什么样 → 怎么构建 → 怎么用起来 → 坑在哪」的顺序讲清楚。
2. 韩语词汇表到底该装什么:字段设计与形态学约束
2.1 为什么不能只存「单词 + 释义」
很多人对词汇表的第一反应是「一个词一行,后面跟中文意思」,这是词典思维,不是语言资源思维。韩语是黏着语,一个动词词干可以派生出几十甚至上百种形态:가다(去)会变成가고가서갔습니다가겠다가시는……如果词表里只存原形,任何基于精确匹配的下游任务都会大面积漏召回;如果只存变形,词表体积会爆炸且无法回溯词根。常见做法是双表结构:一张词根表(lemma)存原形和词性,一张变形表(surface form)存实际出现的形态并指回词根。这样分词器查变形表拿到词根,形态分析器查词根表拿到词性,两边各取所需。
字段设计上,我一般会保留这几列:surface(实际书写形式)、lemma(原形)、pos(词性,用世宗标签体系的粗粒度即可)、type(固有词/汉字词/外来词)、frequency(语料频次)、source(来源标记)。source这一列经常被忽略,但当你把多个来源的词表合并时,它是唯一能让你回溯「这个词到底哪来的、要不要信」的后悔药。词性标签不要一上来就用细粒度,韩语形态标注体系里细标签有几十个,初期用NNG(普通名词)、VV(动词)、VA(形容词)、MAG(普通副词)、JKS(主格助词)这类粗标签就够跑通管线,细标签等你有标注数据再说。
2.2 覆盖率、歧义率、变形还原率:三个必须先算的指标
拿到或做完一份词表,别急着往模型里灌,先算三个数。覆盖率:拿一份真实语料(新闻、评论、对话各取一部分),统计语料中 token 有多少比例能在词表里找到。韩语日常文本里,一份 5 万词根 + 30 万变形的词表,覆盖率通常能到 95% 上下;如果只有 1 万词,覆盖率可能掉到 80% 以下,剩下的全是人名、新造词、外来语。歧义率:同一个 surface 对应多个 lemma 或词性的比例,韩语里이可以是主格助词、指示代词、名词「牙齿」,歧义率不高的词表反而说明标注太粗。变形还原率:随机抽 200 个变形,看有多少能正确还原到词根,这个指标直接决定形态分析器的可用性。
这三个指标不需要复杂工具,写个脚本跑一遍就行。下面这段代码演示怎么用 Python 算覆盖率和歧义率,输入是一份 TSV 格式的词表(surface\tlemma\tpos)和一份纯文本语料。
# coverage_check.py # 输入:wordlist.tsv(surface, lemma, pos 三列,制表符分隔) # corpus.txt(每行一句韩语文本) # 输出:覆盖率、歧义率、未登录词样例 import collections def load_wordlist(path): surface_to_lemmas = collections.defaultdict(set) with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) < 3: continue # 跳过残缺行,别让脏数据污染统计 surface, lemma, pos = parts[0], parts[1], parts[2] surface_to_lemmas[surface].add((lemma, pos)) return surface_to_lemmas def tokenize_naive(line): # 极简切分:按空白切,真实场景应换成形态分析器 return [t for t in line.strip().split() if t] def main(): wl = load_wordlist("wordlist.tsv") total, hit = 0, 0 oov_counter = collections.Counter() with open("corpus.txt", encoding="utf-8") as f: for line in f: for tok in tokenize_naive(line): total += 1 if tok in wl: hit += 1 else: oov_counter[tok] += 1 print(f"覆盖率: {hit/total:.4f} ({hit}/{total})") ambiguous = sum(1 for s, s2 in wl.items() if len(s2) > 1) print(f"歧义率: {ambiguous/len(wl):.4f} ({ambiguous}/{len(wl)})") print("高频未登录词 Top20:") for w, c in oov_counter.most_common(20): print(f" {w}\t{c}") if __name__ == "__main__": main()这段代码的关键点在于surface_to_lemmas用set存(lemma, pos)元组,而不是只存 lemma——因为同一个 surface 可能对应不同词性,只存 lemma 会低估歧义率。tokenize_naive按空白切分只是占位,真实语料里韩语助词常和名词连写(학교에서),空白切分会把整个학교에서当成一个 token,导致覆盖率虚低。所以跑正式统计前,要么先用形态分析器切好,要么接受这个偏差并把它写进报告里。oov_counter统计未登录词频次,Top20 里如果出现大量XX입니다XX했다这种带尾缀的形式,说明你的变形表没覆盖到谓词活用,需要补。
提示:覆盖率不是越高越好。如果一份词表覆盖率 99.9%,先怀疑它是不是把语料本身也收进去了,这种「作弊词表」在测试集上会给你虚假的安全感。
3. 从零构建一份可用的韩语词表:采集、清洗、合并
3.1 语料采集与词频统计的最小流程
构建词表的第一步不是找现成词表,而是从你自己的目标语料里统计词频。原因很简单:通用词表覆盖的是「平均语言」,而你的任务可能是电商评论、客服对话或法律文书,领域词分布完全不同。采集流程分四步:收集原始文本、做基础清洗、分词、统计频次。清洗阶段要处理的是 HTML 标签、重复空格、全角半角混用、以及韩语里常见的错别字(比如되和돼混用)。分词阶段如果暂时没有形态分析器,可以先用空白 + 标点切分做粗统计,但要知道这会高估长尾。
# build_freq.py # 从原始文本统计词频,输出候选词表 import re import collections # 只保留韩文音节、汉字、字母和数字,其余当分隔符 TOKEN_RE = re.compile(r"[가-힣]+|[a-zA-Z]+|[0-9]+") def clean(line): line = re.sub(r"<[^>]+>", " ", line) # 去 HTML 标签 line = line.replace("\u00a0", " ") # 不换行空格 line = re.sub(r"\s+", " ", line) # 压缩空白 return line.strip() def main(): freq = collections.Counter() with open("raw_corpus.txt", encoding="utf-8") as f: for line in f: for tok in TOKEN_RE.findall(clean(line)): if len(tok) >= 2: # 单字噪声大,先过滤 freq[tok] += 1 with open("candidates.tsv", "w", encoding="utf-8") as out: for tok, c in freq.most_common(): if c >= 3: # 频次阈值,低于 3 的先不进候选 out.write(f"{tok}\t{tok}\tUNK\t{c}\n") print(f"候选词数: {sum(1 for c in freq.values() if c >= 3)}") if __name__ == "__main__": main()TOKEN_RE用[가-힣]+匹配连续韩文音节,这是 Unicode 韩文音节区,覆盖了绝大部分现代韩语用字。len(tok) >= 2过滤单字,因为韩语里单字 token 大量是助词碎片或噪声,但注意这会漏掉물(水)、불(火)这类单字名词,如果你的领域里单字词重要,把阈值改成 1 并单独处理。c >= 3是频次阈值,低于 3 的词大概率是拼写错误或极低频专名,先不进候选,等后面人工审核再决定。输出的candidates.tsv里 lemma 和 pos 先填占位符,因为这一步只做频次统计,形态还原是下一步的事。
3.2 多来源词表合并:去重、冲突消解与来源标记
实际项目里,你很少只用一个来源。常见组合是:一份公开基础词表 + 从自己语料统计的候选词 + 一份领域术语表。合并时三个问题会同时冒出来:完全重复(surface 和 lemma 都一样)、部分冲突(同一个 surface 在不同来源里 lemma 或 pos 不一致)、粒度不一致(有的来源用细词性标签,有的用粗标签)。处理顺序建议是:先统一词性标签到粗粒度,再按 surface 分组,组内如果 lemma 一致就合并频次,lemma 不一致就保留多条并标记conflict,最后人工抽查冲突项。
# merge_wordlists.py # 合并多个 TSV 词表,统一词性,标记冲突 import collections # 细标签到粗标签的映射,按需扩充 POS_MAP = { "NNG": "N", "NNP": "N", "NNB": "N", "VV": "V", "VA": "V", "VX": "V", "MAG": "M", "MAJ": "M", "JKS": "J", "JKC": "J", "JKG": "J", "JKO": "J", "EP": "E", "EF": "E", "EC": "E", "ETN": "E", "ETM": "E", } def norm_pos(pos): return POS_MAP.get(pos, "X") # 未知标签归到 X,别丢 def load(path, source): rows = [] with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) < 3: continue surface, lemma, pos = parts[0], parts[1], norm_pos(parts[2]) freq = int(parts[3]) if len(parts) > 3 and parts[3].isdigit() else 0 rows.append((surface, lemma, pos, freq, source)) return rows def main(): all_rows = [] all_rows += load("base_wordlist.tsv", "base") all_rows += load("candidates.tsv", "corpus") all_rows += load("domain_terms.tsv", "domain") grouped = collections.defaultdict(list) for r in all_rows: grouped[r[0]].append(r) with open("merged.tsv", "w", encoding="utf-8") as out: for surface, rows in grouped.items(): lemmas = {r[1] for r in rows} poss = {r[2] for r in rows} total_freq = sum(r[3] for r in rows) sources = "|".join(sorted({r[4] for r in rows})) if len(lemmas) == 1 and len(poss) == 1: lemma, pos = rows[0][1], rows[0][2] out.write(f"{surface}\t{lemma}\t{pos}\t{total_freq}\t{sources}\n") else: # 冲突项:每个 lemma/pos 组合单独一行,标记 conflict for lemma, pos in sorted({(r[1], r[2]) for r in rows}): out.write(f"{surface}\t{lemma}\t{pos}\t{total_freq}\t{sources}|conflict\n") print("合并完成,冲突项已标记") if __name__ == "__main__": main()POS_MAP是合并能否成功的关键。如果两个来源一个标NNG一个标NNP,不统一就会被误判成冲突,实际上它们都是名词,只是普通名词和专有名词的区别。映射到粗标签后,这类假冲突消失,真正的冲突(比如一个标名词一个标动词)才会浮出来。sources字段用|拼接来源,冲突项额外加conflict标记,方便后续用grep conflict merged.tsv一次性捞出所有待人工确认的行。total_freq跨来源累加,但要注意不同来源的频次口径可能不同——如果一个是语料频次、一个是词典权重,直接相加没有意义,这种情况我一般只保留语料来源的频次,其余置 0。
3.3 词表落地前的抽样验证
合并完不要直接上线,抽三组样本验证:高频词(Top 1000)看词性和 lemma 是否正确,中频词(1000~5000)看有没有系统性错误,低频词随机抽 200 个看来源是否可信。验证时重点盯三类问题:汉字词还原错误(학교应该还原到학교而不是학)、外来语转写不一致(컴퓨터和콤퓨터并存)、以及助词被误收为独立词条。这一步没有自动化捷径,但抽 500 条大概半小时能过一遍,比后面模型训完发现词表有问题再回头改要划算得多。
4. 把词表接进分词与形态还原管线
4.1 词表驱动的最大匹配分词:参数与边界
有了词表,最直接的下游用法就是分词。韩语分词里,词表驱动的最大匹配(Max Matching)虽然简单,但在领域文本上经常比通用形态分析器更稳,因为你可以把领域术语直接塞进词表,让它们不被切碎。实现上分前向最大匹配和后向最大匹配,韩语里后向匹配通常略好,因为韩语的修饰成分在前、中心词在后,从右往左切能减少把词尾误当独立词的错误。关键参数是max_len,设成词表里最长词的长度,一般 8~12 个音节够用,设太大只是浪费匹配次数。
# max_match.py # 后向最大匹配分词,词表来自 merged.tsv def load_dict(path): vocab = set() max_len = 0 with open(path, encoding="utf-8") as f: for line in f: surface = line.split("\t")[0] vocab.add(surface) max_len = max(max_len, len(surface)) return vocab, max_len def backward_max_match(text, vocab, max_len): result = [] i = len(text) while i > 0: matched = False for j in range(max(0, i - max_len), i): piece = text[j:i] if piece in vocab: result.append(piece) i = j matched = True break if not matched: # 未登录:单字切出,保证不丢字符 result.append(text[i-1]) i -= 1 return list(reversed(result)) if __name__ == "__main__": vocab, max_len = load_dict("merged.tsv") sent = "학교에서친구를만났습니다" print(backward_max_match(sent, vocab, max_len))backward_max_match从右往左扫描,每次尝试匹配最长片段,匹配不到就单字切出。max_len从词表动态算出来,避免硬编码。注意range(max(0, i - max_len), i)这个区间是左闭右开,piece = text[j:i]取的是j到i-1的字符,逻辑上没问题但容易写错边界,测试时用학교에서这种已知词验证一下。未登录词单字切出是保底策略,实际用的时候可以改成「单字 + 标记」,让下游知道这里没匹配上。这个分词器对词表里有的词效果很好,但词表没覆盖的变形会切得很碎,所以它适合和形态分析器配合:词表命中的直接用,没命中的交给分析器。
4.2 用词表做拼写检查:编辑距离 + 频次阈值
词表另一个高频用途是拼写检查。韩语拼写错误常见两类:一是辅音/元音混淆(되/돼、안/않),二是空格错误。基于词表的检查逻辑是:对每个 token,如果不在词表里,就在编辑距离 1 的候选里找词表内且频次高于阈值的词,作为建议。编辑距离 1 对韩文音节来说,一次替换就能覆盖很多常见错字,因为韩文音节是组合式的,一个音节的辅音或元音改一个字母就是另一个音节。
# spell_check.py # 基于词表的拼写建议,编辑距离 1 def load_vocab_with_freq(path): vocab = {} with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) >= 4 and parts[3].isdigit(): vocab[parts[0]] = int(parts[3]) return vocab def edit_distance_one(word): # 生成编辑距离为 1 的候选:删除、替换、相邻交换 letters = list(word) candidates = set() for i in range(len(letters)): candidates.add("".join(letters[:i] + letters[i+1:])) # 删除 for c in "가나다라마바사아자차카타파하거너더러머버서어저처커터퍼허": candidates.add("".join(letters[:i] + [c] + letters[i+1:])) # 替换 for i in range(len(letters) - 1): swapped = letters[:] swapped[i], swapped[i+1] = swapped[i+1], swapped[i] candidates.add("".join(swapped)) # 交换 return candidates def suggest(word, vocab, min_freq=5): if word in vocab: return [] hits = [(c, vocab[c]) for c in edit_distance_one(word) if c in vocab and vocab[c] >= min_freq] return sorted(hits, key=lambda x: -x[1])[:5] if __name__ == "__main__": vocab = load_vocab_with_freq("merged.tsv") print(suggest("학교에서", vocab)) # 假设词表里是 학교에서 print(suggest("되요", vocab)) # 常见错写,应建议 돼요edit_distance_one里的替换字符集只列了部分韩文音节,实际用的时候应该用完整的韩文音节表(가到힣,共 11172 个),但那样候选集会很大,所以常见做法是只对错误 token 里每个位置尝试有限的高频音节,或者用音素级编辑距离而不是音节级。min_freq=5是频次阈值,防止建议出一个词表里只出现一次的怪词。这个方案对되요→돼요这类错误有效,但对整句空格错误无能为力,空格问题需要另外的规则或模型。
4.3 词表在检索召回里的用法:同义扩展与变形归一
搜索场景里,用户输入먹었어,文档里写的是먹다,精确匹配召不回。词表在这里的作用是变形归一:把查询和文档都先过一遍形态还原,统一到 lemma 再建索引。如果词表里有变形到 lemma 的映射,这一步就是查表;如果没有,就得靠形态分析器。另一个用法是同义扩展:词表里如果标了汉字词和固有词的对应关系(比如식사和밥都表示「饭」),可以在查询时扩展召回。这部分依赖词表的type字段和额外维护的同义词典,不是词表本身能解决的,但词表提供了扩展的锚点。
5. 避坑与排查:词表项目里最容易翻车的五件事
5.1 现象:覆盖率很高,但模型效果很差
原因通常是词表把测试集语料也统计进去了,或者词表来源和训练语料同分布但和真实线上分布差很远。解决方法是把词表构建语料和评测语料严格分开,构建词表只用训练侧数据,评测时单独算覆盖率。如果线上分布未知,至少留一份「从未参与词表构建」的语料做验证。
5.2 现象:同一个词在词表里出现多次,lemma 还不一样
原因是多来源合并时没做冲突消解,或者合并脚本的去重键选错了(比如只按 surface 去重,没管 lemma)。解决方法是合并时按(surface, lemma, pos)三元组去重,冲突项单独标记,人工确认后再决定保留哪条。别用set直接去重整个行,那样会把频次和来源信息也丢掉。
5.3 现象:分词结果里助词被单独切出来,名词被切碎
原因是词表里收了助词作为独立词条,最大匹配时优先匹配到了助词。解决方法是分词用的词表和形态分析用的词表分开:分词词表里不放助词、词尾这类功能词,让它们附着在实词上;形态分析词表里才需要功能词来还原。如果必须共用一份,在匹配时给功能词加惩罚权重,优先匹配实词。
5.4 现象:汉字词还原错误,比如학교还原成학
原因是词表里학교的 lemma 字段填错了,或者构建时用了按音节切分的错误逻辑。韩语汉字词大多是双音节以上,还原时应该整词还原,不要按汉字逐字拆。排查方法是抽 100 个汉字词,看 lemma 长度分布,如果大量 lemma 长度是 1,基本可以确定还原逻辑有问题。
5.5 现象:词表文件越来越大,加载慢、内存高
原因是把低频变形也全量加载了,或者用了低效的存储格式(比如每行 JSON)。解决方法是按频次分层:高频词根和变形常驻内存,低频词放磁盘用的时候再查;存储格式用 TSV 或二进制,别用 JSON。如果词表超过百万行,考虑用前缀树或 DAWG 压缩,加载时间和内存都能降一个量级。
6. 词表质量怎么持续验证:一个可复用的评测脚本
词表不是做完就完了,语料在变、领域在变,词表得跟着迭代。我一般会留一个评测脚本,每次词表更新后跑一遍,看三个指标有没有退化:覆盖率、歧义率、以及一个我称之为「还原准确率」的指标——从语料里抽 200 个变形,人工标注正确 lemma,看词表能还原对多少。这个脚本不需要复杂框架,核心是把上一节的覆盖率统计和还原检查串起来,输出一份可对比的报告。
# eval_wordlist.py # 词表质量评测:覆盖率 + 还原准确率(需人工标注样本) import collections def load_wordlist(path): surface_to_lemma = {} with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) >= 3: surface_to_lemma[parts[0]] = parts[1] return surface_to_lemma def coverage(wordlist, corpus_path): total, hit = 0, 0 with open(corpus_path, encoding="utf-8") as f: for line in f: for tok in line.strip().split(): total += 1 if tok in wordlist: hit += 1 return hit / total if total else 0.0 def lemma_accuracy(wordlist, gold_path): # gold_path 每行:surface\tgold_lemma correct, total = 0, 0 with open(gold_path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) < 2: continue total += 1 pred = wordlist.get(parts[0]) if pred == parts[1]: correct += 1 return correct / total if total else 0.0 if __name__ == "__main__": wl = load_wordlist("merged.tsv") print(f"覆盖率: {coverage(wl, 'eval_corpus.txt'):.4f}") print(f"还原准确率: {lemma_accuracy(wl, 'gold_lemmas.tsv'):.4f}")这个脚本的价值在于可对比:每次词表更新,把输出存成带日期的文件,下次跑完 diff 一下,覆盖率掉了还是还原准确率掉了,一目了然。gold_lemmas.tsv需要人工标注,200 条大概一小时能标完,标一次可以复用很久,除非词表结构大改。我自己的习惯是词表每次大改(合并新来源、调整词性体系)必跑一次,小改(补几十个领域词)就只跑覆盖率,还原准确率一季度抽一次。踩过的坑是早期不做这个对比,词表改着改着覆盖率从 96% 掉到 91%,直到线上召回出问题才发现,回头查是哪次合并引入的脏数据花了整整两天。所以现在宁可多花十分钟跑脚本,也不省这一步。希望帮到你。
本文还有配套的精品资源,点击获取