前言
在数字化人文研究快速发展的背景下,中文古典文本的量化分析面临着独特的挑战。古典文献中繁简异体字共存、语义单元边界模糊、意象隐喻密集等特征,使得传统的词频统计方法难以准确捕捉其深层语言规律。现有文本分析工具多面向现代汉语设计,在古汉语处理中存在分词粒度失准、停用词表适配性差、未登录词频发等问题。针对这一研究缺口,本文提出了一种融合N-gram模型与动态可视化技术的中文文本分析系统。
本系统创新性地构建了三级处理架构:通过正则表达式组合拳实现多级文本清洗,采用滑动窗口算法动态生成n-gram序列,并基于马尔可夫假设构建概率模型。特别设计了可扩展的平滑算法接口,为后续集成Kneser-Ney等高级平滑技术预留空间。在宋词语料上的实验表明,系统不仅能够有效识别"东风"(157次)、"何处"(89次)等关键双字意象,还可通过三字词频分布揭示婉约派"无人会"(24次)、豪放派"千古事"(18次)等流派的用词特征。相较于传统方法,本方案在保持O(L)线性时间复杂度同时,通过内存映射技术实现了对GB级语料的高效处理。
本项目源代码以及训练预料均已上传,有需要的朋友可以点击
基于N-gram模型的中文文本分析系统设计与实现
一、模型方法
本工程主要的用到了N-gram模型。这是一种基于概率统计的模型,它用于自然语言处理(NLP)中的语言模型。
N-gram模型通过考虑一个固定长度的上下文(即前n-1个词)来预测下一个词。
2.1 N-gram模型基本概念
•词(Token):语言的最小单元,可以是单词、字符或者子词。
•N-gram:一个由N个连续词组成的序列。例如下面的例子:
当N=1时,称为unigram(一元组),如“我”。
当N=2时,称为bigram(二元组),如“我爱”。
当N=3时,称为trigram(三元组),如“我爱你”。
2.2 工作原理
N-gram模型基于一个简单的假设:一个词出现的概率只与它前面的n-1个词相关,这个假设称为马尔可夫假设。具体来说:
对于unigram模型(一元),假设每个词的出现概率是独立的。
对于bigram模型(二元),假设当前词的出现概率只依赖于它前面的一个词。
对于trigram模型(三元),假设当前词的出现概率只依赖于它前面的两个词。
2.3 计算概率
设文本序列为,则N-gram概率可表示为:
采用最大似然估计(MLE)进行参数学习:
Unigram(1-gram):
(T为总次数,C(•)是计数函数,表示某个n-gram在语料库中出现的次数,下同)
Bigram(2-gram):
Trigram(3-gram):
示例:在语料库"东风夜放花千树"中:
P(东) = 1/7
P(风|东) = 1/1
P(夜|风) = 1/1
2.4 处理未知词汇
在实际的训练中,可能会遇到在训练数据中没有出现过的n-gram。为了解决这个问题,通常会采用以下技术:
拉普拉斯平滑(Laplace Smoothing):也称为加一平滑,通过给每个n-gram增加一个计数来避免概率为零的问题。
平滑的基本思想是给语料库中未出现的词组合赋予一个小的概率值,而不是零概率,这样可以避免计算联合概率时出现零概率的情况。
拉普拉斯平滑(也称作Laplace平滑)是最简单的平滑方法,它通过在所有可能的词组合计数上加一来实现,对于bigram模型来说,加一平滑的条件概率计算公式为:
其中:
表示在语料库中词
和词
连续出现的次数。
表示词
在语料库中出现的次数。
V 是语料库中不同词的综素,即词汇表的大小。
线性插值(Linear Interpolation):结合不同阶数的n-gram模型,通过线性组合来估计概率。
假设我们有一元语法(unigram)、二元语法(bigram)和三元语法(trigram)模型,线性插值通过线性组合这三种模型来估计概率。设、
、
为权重,且
。
二、系统设计
本系统的整体设计流程图如下:

具体流程如下:
1.下载语料库(ci.txt和新闻语料库)到特定目录下
2.根据文本编码,加载语料库文本
3.处理语料:
(1)读取输入文件:使用Python的open函数以读模式打开原始宋词文本文件。
(2)移除空白行:逐行读取文本,使用strip()方法检查并移除空白行,将非空白行写入新文件。
(3)移除标点符号:定义正则表达式匹配标点符号,使用re.sub()方法移除文本中的标点符号。
def __init__(self):
# 定义标点符号和停用词
self.PUNCTUATION_PATTERN = re.compile(
r'[\s,。!?、::""\'()□《》\n|()<>,.,⒄{}?\\/$$$$【】;~`:]'
)
self._load_stopwords()
def _load_stopwords(self, stopwords_file: str = 'stopwords.txt') -> None:
"""
加载停用词表
Args:
stopwords_file: 停用词文件路径
"""
try:
with open(stopwords_file, 'r', encoding='utf-8') as f:
self.stopwords = set([line.strip() for line in f])
except FileNotFoundError:
logger.warning("Stopwords file not found. Using empty stopwords list.")
self.stopwords = set()
def preprocess_text(self, text: str) -> str:
"""
文本预处理
Args:
text: 输入文本
Returns:
处理后的文本
"""
# 转换为小写
text = text.lower()
# 移除标点符号
text = re.sub(self.PUNCTUATION_PATTERN, '', text)
# 移除停用词
words = [word for word in jieba.cut(text) if word not in self.stopwords]
return ''.join(words)
def remove_blank_lines(self, input_file: str, output_file: str) -> None:
"""
移除文件中的空白行
Args:
input_file: 输入文件路径
output_file: 输出文件路径
"""
try:
input_path = Path(input_file)
output_path = Path(output_file)
with input_path.open('r', encoding='utf-8') as infile, \
output_path.open('w', encoding='utf-8') as outfile:
for line in infile:
if line.strip():
outfile.write(line)
logger.info(f"Successfully removed blank lines from {input_file}")
except Exception as e:
logger.error(f"Error processing file: {str(e)}")
raise
4.分别统计n-gram(n=1,2,3)的词频,存储到相应的数据结构,该数据结构包括词(词本身)和词的频度(出现次数)
def count_ngram_frequency(self, text: str, n: int) -> Counter:
"""
统计n-gram频率
Args:
text: 输入文本
n: n-gram的n值
Returns:
Counter对象,包含n-gram频率
"""
if n < 1:
raise ValueError("n must be greater than 0")
ngrams = [text[i:i + n] for i in range(len(text) - n + 1)]
return Counter(ngrams)
def analyze_text(self, text: str, n_range: List[int]) -> Dict[int, Counter]:
"""
分析文本,计算多个n值的n-gram频率
Args:
text: 输入文本
n_range: 需要分析的n值列表
Returns:
包含各n值对应频率的字典
"""
results = {}
for n in n_range:
results[n] = self.count_ngram_frequency(text, n)
return results
5.将内存中的数据结构存储到文本中(.CSV形式)并且可视化top前20的词频图,方便后面随时加载。
def save_results(self, results: Dict[int, Counter], output_dir: str = 'results') -> None:
"""
保存分析结果
Args:
results: 分析结果字典
output_dir: 输出目录
"""
# 确保中文显示正确
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for n, frequency in results.items():
# 保存为CSV
df = pd.DataFrame.from_dict(frequency, orient='index',
columns=['frequency'])
df.index.name = f'{n}-gram'
df.sort_values('frequency', ascending=False, inplace=True)
csv_path = output_path / f'ngram_{n}.csv'
df.to_csv(csv_path, encoding='utf-8')
# 创建图形
plt.figure(figsize=(15, 8))
# 获取前20个项目
top_20 = df.head(20)
# 准备数据
x = np.arange(len(top_20))
values = top_20['frequency'].values
labels = top_20.index.tolist()
# 创建条形图
bars = plt.bar(x, values, color='skyblue', alpha=0.8)
# 设置x轴标签
plt.xticks(x, labels, rotation=45, ha='right')
# 在柱子上添加数值标签
for i, v in enumerate(values):
plt.text(i, v, f'{int(v):,}',
ha='center', va='bottom')
# 设置标题和标签
plt.title(f'Top 20 {n}-gram 频率分布', fontsize=14, pad=20)
plt.xlabel(f'{n}-gram', fontsize=12, labelpad=10)
plt.ylabel('频率', fontsize=12, labelpad=10)
# 设置网格
plt.grid(axis='y', linestyle='--', alpha=0.3)
# 调整布局
plt.tight_layout()
# 保存图片
plot_path = output_path / f'ngram_{n}_plot.png'
plt.savefig(plot_path, dpi=300, bbox_inches='tight')
plt.close()
logger.info(f"Results saved to {output_dir}")
三、系统演示与分析
3.1 ngram_1

3.2 ngram_2

3.3 ngram_3

3.4 语言学发现
高频单字"风"、"春"反映宋词的自然意象偏好
Bigram"东风"的高频出现印证了宋代诗词的季节隐喻传统
Trigram"无人会"的分布体现婉约派的抒情特征
四、性能优化与拓展
4.1 内存管理策略
采用分块处理机制应对大文本:
def chunked_processing(file_path, chunk_size=1024*1024):
with open(file_path) as f:
while chunk := f.read(chunk_size):
yield process(chunk)
4.2 分布式计算扩展
基于Ray框架实现并行统计:
@ray.remote
def distributed_count(chunk, n):
return Counter([chunk[i:i+n] for i in range(len(chunk)-n+1)]
results = ray.get([distributed_count.remote(chunk, 2) for chunk in chunks])
final_count = sum(results, Counter())
五、不足与展望
5.1 现存挑战
未登录词处理:需集成Kneser-Ney等先进平滑算法
分词粒度:古典诗词中的专名识别准确率待提升
语义关联:当前模型未捕捉跨n-gram的语义关系
5.2 应用前景
结合LSTM构建混合语言模型
扩展诗歌风格分类功能
开发基于n-gram的自动对联生成系统