ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

兰卡斯特汉语语料库LCMC:设计原理与Python处理实战指南

兰卡斯特汉语语料库LCMC:设计原理与Python处理实战指南

简介:兰卡斯特汉语语料库(LCMC)是一份百万词级现代汉语书面语通用平衡语料库,面向语言学研究者、汉语教师及自然语言处理爱好者,为其提供免费开放的语料数据支撑。该语料库在构建时参考FLOB/FROWN的设计思路,并作为英汉时体对比研究项目的成果之一,适合用于语体分布考察、词频统计、英汉平行对比及汉语教学研究等场景。包内共71个文件,核心为39个htm格式的语料分卷文本、31个xml结构标注文件及1个doc格式的语料库使用说明文档,文件组织清晰,可方便检索与按需提取;压缩包整体仅6.34MB,体积小巧易于获取。目前已有3466人学习/下载,受到相关领域学习者关注。通过这套语料资源,用户可快速获得近百万词的现代汉语平衡语料样本,用于学术研究中的语料检索、标注分析与语言特征统计,是开展汉语语料库相关课题的实用基础数据。

1. 兰卡斯特汉语语料库LCMC语料库:先搞清楚它到底是什么

想做汉语语域差异研究,第一反应往往是去翻现成的通用语料库,但真要对比“新闻、小说、学术论文”之间的词汇与词性分布时,手里缺一个结构透明的成品。兰卡斯特汉语语料库(LCMC)就是为此设计的:它有约 100 万词次的现代汉语书面语,采样于 1990 年前后的正式出版物,并且全部经过分词和词性标注。学习自然语言处理的人不需要再费劲洗菜一样清理原始文本,可以直接拿它做频率统计、搭配检索、语域对比,甚至试跑一个有监督的词性标注器。

但很多朋友拿到 LCMC 后第一个反应是“这文件格式好怪”,这其实是它最优秀的起点。LCMC 直接模仿了英语 Brown 语料库的抽样框架,每一段文本来自什么文体、抽取多少词,都被严格规定过,而不是随手攒的网页文本。对从事语料库语言学、计算语言学入门、社会语言学语体分析的人来说,它是一份可以重复验证的小而完备的基准语料。本文就从语料库设计、读取逻辑、统计模式,一直到你实际使用时容易翻车的五个细节,按动手路线展开。

2. LCMC 的语料设计:为什么说它是“平衡”语料库

2.1 模仿 Brown 的抽样框架:15 类文体不是随便分的

LCMC 的可贵之处,不在于语料总量多,而在于抽样设计有明确依据。它沿用 Brown 语料库的抽样思路,把现代汉语书面语按使用领域拆成 15 个类别,每个类别对应一组代码。常见类别如下表所示:

代码文体范围典型篇章
A新闻报道报纸要闻、本地新闻
B社论与立场文章报纸评论、编者按
C新闻评论与专栏专栏作家文章
D宗教类布道稿、宗教刊物
E技术与操作说明技能手册、使用指南
F通俗社科科普文章、社会分析
G传记与散文人物传、杂文随笔
H公文与报告政府公报、会议报告
J学术论文学报、研究论文
K一般小说现代长中篇
L侦探小说推理故事
M科幻小说幻想题材
N武侠小说武打叙事
P爱情小说言情叙事
R幽默小品笑话、讽刺短文

这个分类让你在做语域对比时,不用自己再发明一套“新闻 vs 文学”的粗糙标签,直接用现成的代码就能聚合。类别之间彼此独立,但又在性别、地域、主题上做了基础平衡,所以它叫平衡语料库。每条语料按段落和句子切分,尽量保证一个类内文本长度接近,方便统计时不用反复做长度归一化。

2.2 分词与词性标注:数据长什么样

LCMC 在发布时就已经完成了基础加工。常见发行版本会把一篇文本拆成一行一行的短句,每一行中词语和词性标注连在一起,不同来源的格式略有差异。最典型的是“词语/标注”用斜杠分隔,例如:

他/r 站/v 在/p 门口/n 的/u 位置/n 上/f

这里的r是代词,v是动词,p是介词,n是名词,u是助词,f是方位词。标点符号一般也保留,但会作为独立 item。另有些版本把@当作换行标记,整篇文本压缩成一个长行。你拿到手后第一件事不是急着统计,而是先看一套文件里的一致格式,否则后面代码会一跑就崩。

还需要注意,LCMC 使用的标注集并不等于国内常见的“北大现代汉语语料库标注集”,它更像标注粒度固定、标记数量较少的折中方案,名词不再细分“普通名词/专有名词”时会占用多个标志位,动词也只区分及物与不及物等少数属性。实际使用时,你只需要认全名词、动词、形容词、助词、代词这些大类就能完成大部分统计工作,太细的属性反而容易带来噪音。

2.3 和现代通用语料库的差异:为什么 100 万词仍然值得用

很多人会质疑,100 万词是不是太小了?对比动辄几十亿词的现代汉语语料,LCMC 确实不算大,但它解决的是“结构已知、类别可控”的问题。现代互联网语料看似规模大,却有难以避免的领域倾斜,新闻和百科占了绝大多数,口语与文言混杂,而且清洗过程常常丢失原创文本信息。LCMC 从设计之初就把文体的比例和抽样来源写进框架,每一类文本的质量都被人工检查过,这让它在统计意义上更可信。

另一个不可替代点是时间截面。LCMC 采样于 1990 年前后,正好处于改革开放后、互联网普及前的书面语阶段。如果你想研究“语言演变”,对比九十年代书面语和现代网络语料之间的词汇差异,LCMC 是不可多得的对照组。它可以与同时代的现代汉语语料库互补,但不适合直接用来训练当前的大语言模型,因为数据量和领域新鲜度都不够。把它当作规格明确的评测集、验证集,比当作训练集更明智。

3. 把 LCMC 用起来:下载、检索和最小处理流程

3.1 先确认你拿到的版本形态

LCMC 在流传中产生了不少“民间版本”,最常见的是压缩包里有按文体代码命名的多个文本文件,比如lcmc-a.txt、lcmc-b.txt,也有的版本把所有文件平铺在一层目录里,文件名包含A01这样的编号。还有一些整合版直接做成一个大的 utf-8 纯文本文件,每个句子一行。拿到之后我建议先建一个干净的目录,把原始文件单独存放,避免后续脚本修改源文件导致不可逆损坏。

打开文件前,先用文本编辑器或命令行预览前几行,确认编码。旧版流转的 LCMC 可能来自 GBK 或 GB2312 环境,如果你的操作系统默认是 UTF-8,直接读取就会乱码。为了一劳永逸,可以把原始文件统一转成 UTF-8 格式,再保存为副本,后续所有 Python 代码都用 UTF-8 处理。

3.2 用 Python 批量读取并生成词性分布

下面这个脚本是我处理 LCMC 时最常用的一段,适合从多文件语料库中提取每个词性标签的全局频次。它会遍历当前目录下的所有lcmc-*.txt文件,并逐行拆分。

import os import re from collections import Counter def process_lcmc_file(path): pos_counter = Counter() word_counter = Counter() # 以 utf-8 读取,忽略解码错误行 with open(path, 'r', encoding='utf-8', errors='ignore') as f: for line in f: line = line.strip() if not line: continue # 假设每个词与标注用斜杠分隔 items = re.split(r'[\s/]+', line) # items 可能是 [词, 标注] 的平铺序列 for item in items: if not item: continue if '/' in item: w, p = item.rsplit('/', 1) word_counter[w] += 1 pos_counter[p] += 1 else: # 某些版本没有斜杠,单独处理做词频 word_counter[item] += 1 return word_counter, pos_counter if __name__ == "__main__": total_word = Counter() total_pos = Counter() folder = "./lcmc_files" for filename in os.listdir(folder): if not filename.startswith("lcmc-"): continue if not filename.endswith(".txt"): continue wc, pc = process_lcmc_file(os.path.join(folder, filename)) total_word.update(wc) total_pos.update(pc) print(total_pos.most_common(30))

这段代码的核心是正则切分和Counter累加。正则re.split(r'[\s/]+', line)会把空格、制表符和斜杠统一作为分隔符,这样能同时兼容“词 标注”和“词/标注”两种常见格式。但需要注意,rsplit('/', 1)只拆最后一个斜杠,避免词中含有斜杠时出错。中文粒子中偶尔会有英文标点,但分词后一般不会有界符混入。

如果某个文本里出现连续两个斜杠,或标注属性带前缀后缀,你需要根据实际数据微调正则。一个快速验证方法是统计total_pos中是否出现无法识别的标记,比如nx、qq等,这类标记一般来自版本差异,可以在后续统一映射到大类。

3.3 检索特定结构:用正则匹配“形容词+名词”

LCMC 的另一类常规应用是搭配检索,比如找出语料中所有“形容词+名词”序列。由于文本已经分词和标注,不需要再跑分词模型,直接用行级正则匹配即可。

import os import re def find_adj_noun(folder): pattern = re.compile(r'(\w+)/a\s+(\w+)/n') results = [] for filename in os.listdir(folder): if not filename.endswith('.txt'): continue with open(os.path.join(folder, filename), 'r', encoding='utf-8', errors='ignore') as f: for line in f: # 去除行内其他标点 clean_line = line.strip() for m in pattern.finditer(clean_line): adj, noun = m.group(1), m.group(2) results.append((adj, noun, filename)) return results if __name__ == "__main__": folder = "./lcmc_files" pairs = find_adj_noun(folder) for adj, noun, fname in pairs[:20]: print(adj, noun, fname)

注意这里我用\w+匹配中文词,在 Python 的re中\w默认只能匹配英文、数字和下划线,不能匹配中文。如果你直接跑上面的代码,可能会完美跳过所有中文。正确写法应该用[\u4e00-\u9fff]+或者开启re.UNICODE标志。在 Python 3 中,re模块默认 Unicode 匹配,\w对中文并不生效,所以更稳妥的是自定义字符集。

带标注的正则容易受空格干扰,如果某一版本里标注和词之间夹着多个空格,模式就要改成([\u4e00-\u9fff]+)\s*/\s*a\s+([\u4e00-\u9fff]+)\s*/\s*n。这种参数差异,正是你在处理不同发行版本时必须先做“格式侦查”的原因。

4. LCMC 能做什么:语域对比和词性分布分析的落地场景

4.1 同一词在不同文体中的频率差异

语域对比是 LCMC 最擅长的事。比如“进行”这个词,在学术论文和新闻评论里很常见,但武侠小说里可能几乎不出现。你可以按文体代码拆开统计,计算归一化频率:

# 统计特定词在不同文体中的每十万词频次 from collections import defaultdict import os, re word = "进行" freq = defaultdict(int) total = defaultdict(int) for fname in os.listdir("./lcmc_files"): if not fname.endswith(".txt"): continue style = fname.split("-")[1][0] # 从文件名提取文体代码 with open(os.path.join("./lcmc_files", fname), 'r', encoding='utf-8', errors='ignore') as f: text = f.read() wc = text.count(word) freq[style] += wc # 粗略统计总词数 total[style] += len(text.split()) for style, count in freq.items(): rate = count / (total[style] / 100000) print(style, count, round(rate, 2))

这段代码里style = fname.split("-")[1][0]对文件名lcmc-j01.txt有效,它会取到j。但如果你的文件不是这种命名,就需要换成其他解析方式。这种统计的价值不是数字本身,而是拿不同文体横向比较后,能看出词汇的语域偏好,这正是语料库语言学的经典分析路径。

4.2 作为词性标注评测基准

LCMC 的标注经过了多轮人工校对,比市场上许多中文标注语料稳定。因此做词性标注实验时,可以把 LCMC 切出一个小测试集,用你自己的标注器去预测,再和原始标注比较,计算准确率。规模不大,跑起来很快,方便反复调参。

评测时要注意,LCMC 的标注集和官方常用标注集存在映射关系。你必须先建立映射表,例如把 LCMC 的v映射成你模型输出的动词,把a映射成形容词,否则两端标签名都对不上,准确率会莫名其妙地低。这个映射表务必单独保存成 JSON 文件,以后每次实验复用同一份,保证评测可比。

4.3 与另一个语料库做平行对比的小思路

如果你手上还有一个其他来源的语料,想把它们放到同一个框架下比较,建议不要直接混合后去统计。更好的做法是:先分别计算两个语料的词性分布向量,再用卡方或对数似然比找出差异显著的词。LCMC 的平衡结构能作为基线,另一个语料作为目标组。这样能避免语料总量差异造成的假象。

比如你想比较“网络评论 vs LCMC 新闻类”,可以先随机从网络上抽取 10 万词,再用 LCMC 的 A 类作为新闻代表。对比时把所有词频归一化到每十万词,再计算 log-likelihood 值,筛出 D 值最大的前 50 个词作为“特征词”。这个方法在语域分析里相当成熟,难点不在统计上,而在人为判断哪些差异词有解释价值。

5. 用 LCMC 的 5 个常见坑:从标注集到文本编码

5.1 文件层:编码不一致导致读出来乱码

现象:打开文件后看到���t��之类的符号,Python 读取时报UnicodeDecodeError。

原因:网上流传的文件一部分是 GBK 编码,一部分是 UTF-8 编码,没有统一规范。你的代码用 UTF-8 去读 GBK 文本,中文部分全部变成乱码,甚至中断。

解决:不要手动猜测每个文件编码。用chardet或langdetect快速检测,再统一转码。我已经习惯在脚本里加上一层errors='ignore'兜底,但最好还是先转成 UTF-8 的副本,之后所有脚本都用同一编码。

5.2 标注集认不准

现象:统计出的词性标签里出现奇怪标记,比如qr、nz、vg,和资料里列出的词性代码表对不上。

原因:不同版本的 LCMC 可能改写过标注集,或者在原版基础上补充标注。LCMC 基础标注集比较简洁,但民间追加版本的制作者会引入自己的扩展标记。

解决:不要编代码表,直接在语料里抽出所有不认识的标签,建立一个扩展映射表。宁可把未知的标签全部归入OTHER类,也不要在统计时强行拆开,否则结果不稳定。

5.3 简繁体混用导致检索漏词

现象:检索“定义”的时候,统计结果远低于预期,因为部分文件中写的是“定義”。

原因:1990 年代的出版物本身就存在简繁体混排,尤其港台来源的文本没有全部转成简体;网络转版时也常常只转一部分。

解决:先做一次整体繁体转简体,但转换后不要直接覆盖原始文件。我通常会对每个文本输出两个副本:原始版和“标准化简体版”,检索用简体版,文本内容研究用原始版,两不误。

5.4 语料规模小,统计结果容易波动

现象:在小说类文本里统计某个低频词,发现频率在不同章节之间的差异达到数倍,甚至同义词排序不稳定。

原因:每个文体类别的文本量并不大,低频词可能只出现一次,频率受单篇主题影响非常大,统计误差远大于高频词。

解决:做频率对比时设置最低出现次数阈值,比如只统计至少出现 5 次的词,并对频数做平滑处理。更重要的是,不要因为一次统计差异就下结论,对抽样区间做几次折半重测,稳定下来再解释。

5.5 民间“改写版”标注错误被当成基准

现象:你在验证标注器时,发现测试集上准确率始终上不了 90%,检查之后才知道有一批文件的标注明显错位,比如动词标成名词。

原因:某些转贴者对文件乱改,没有保留原始校对结果,甚至把原始文件里多音字全部替换了。

解决:把 LCMC 当作基准之前,先随机抽样 5 到 10 个文件,人工标注一小段,比较你拿到的版本和官方样例描述是否一致。发现异常就换一个版本来源,或者只选用已知可信的那几个文体子集。

6. 进阶:用 Python 做一个 LCMC 语域对比小工具

最后分享一个我平时最常用的进阶技巧:做一个极简的语域对比工具,让“学术类 vs 武侠小说类”这种跨域比较可以一键出结果。核心思路是先按文体聚合词性频次,再把频次转化为每十万词分布,最后输出差异最显著的词。

from collections import defaultdict import math import os def load_style(folder, style_code): words = [] for fname in os.listdir(folder): if not fname.endswith('.txt'): continue if style_code not in fname: continue with open(os.path.join(folder, fname), 'r', encoding='utf-8', errors='ignore') as f: text = f.read() # 简单分词假设:横杠分隔 words.extend([w for w in text.replace('/', ' ').split()]) return words def ll_score(freq_a, total_a, freq_b, total_b): # 将对数似然比计算简化,E 为期望值 a, c = freq_a, freq_b b, d = total_a - a, total_b - c e1 = (a + b) * (a + c) / (total_a + total_b) e2 = (a + d) * (b + d) / (total_a + total_b) return 2 * (a * math.log((a / e1) if a else 1) + c * math.log((c / e2) if c else 1)) academic = load_style('./lcmc_files', 'j') novel = load_style('./lcmc_files', 'k') word_freq_a = defaultdict(int) word_freq_b = defaultdict(int) for w in academic: word_freq_a[w] += 1 for w in novel: word_freq_b[w] += 1 total_a = len(academic) total_b = len(novel) diff = [] for w in set(word_freq_a) | set(word_freq_b): fa = word_freq_a.get(w, 0) fb = word_freq_b.get(w, 0) if fa + fb < 5: # 低频过滤 continue diff.append((ll_score(fa, total_a, fb, total_b), w, fa, fb)) diff.sort(reverse=True) for score, w, fa, fb in diff[:30]: print(round(score, 2), w, fa, fb)

这个工具没有任何外部依赖,核心就是对数似然比排序。对语文研究来说,这种量化结果的价值在于给你一个明确的线索,最终解释权仍然在语言规律那里。我的习惯是跑完数据后,总会挑两个差异显眼的词回原文里看上下文,确认不是被代词或者文本噪声误导。语料库工具最怕的就是,统计响了,却没读过原始片段。

用 LCMC 这类小语料库,最大的收益是你能完全掌控数据生命周期:从格式、编码到统计口径都查得到,相较于黑盒式的大型云端语料,踩坑成本低,得出结论也更踏实。希望这篇笔记能帮你少走几趟弯路,快速把语料库变成可知、可算、可解释的产出。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进