☰
古文到现代文机器翻译实战:基于Transformer与BPE的毕设全流程指南
2026/10/7 18:45:50 网站建设 项目流程

简介:面向毕业设计、课程设计与项目开发场景,这份基于Python实现的古文到现代文机器翻译源码,覆盖了从语料预处理、文本向量化、神经网络构建到注意力机制训练与测试的完整流程。工程结构清晰,适合计算机相关专业学生参考、复现与二次拓展,尤其适合需要快速搭建自然语言处理演示项目的开发者。

包体信息方面,资源共包含12个文件,其中以11个Python脚本为核心,分别承担序列编号、输入向量化、TFRecord操作、服务器通信、模型训练与测试等模块功能,另有1个Markdown说明文档辅助阅读与运行指导,压缩包整体仅19KB,轻量易部署。

目前已有199人学习下载。源码经过严格测试,可直接运行并根据自身需求调整语料与模型参数,有助于深入理解基于注意力机制的序列到序列翻译思路,也可作为毕业论文或课程答辩的支撑材料。

1. 古文到现代文的机器翻译:为什么它比普通翻译更值得做成毕设

很多人第一次看到"古文到现代文的机器翻译"这个题目,以为就是做一个查词典的小工具:输入"之乎者也",输出"的、呢、吗"。真上手才发现,文言文里省略主语的句子一抓一大把,"为"字在不同语境里能翻出十几种意思,光靠映射词典根本不工作。这个项目的本质,其实是一个序列到序列的神经机器翻译任务,和英译中、中译英是同一套技术栈,只是语料和预处理更棘手一些。它能锻炼数据清洗、模型训练、推理部署一整条链路,又不像通用翻译那样需要承受大规模算力的压力,非常适合毕业设计、课程设计或个人项目练手。这篇笔记会从语料、模型、训练到答辩演示,把整条路拆开讲清楚。

2. 先拆任务再选路线:这个翻译任务的技术拆解与模型选型

2.1 文言文翻译难在哪:和现代翻译的三个显著差别

文言文到现代文,表面上是"两种语言"之间的转换,实际难点有三个层次。第一是词汇层面的多义性。"兵"既可以指武器也可以指士兵,"走"在古汉语里是"跑"的意思,同一个词在不同句子里对应完全不同的现代词,这对模型的上下文建模能力要求很高。第二是句法层面的省略。文言文的主语、谓语、宾语甚至介词经常全省略,像"(赵王)使(人)往(于)赵"这种句子,机器必须自己从上下文里把省略成分"脑补"出来,而现代汉语很少这么写。第三是语用层面的格式套语。"之乎者也""夫""盖"这类虚词没有实际词汇意义,却在行文逻辑里承担发语、停顿、疑问的功能,翻译时有的要译成现代虚词,有的要直接丢弃,规则几乎说不清。

这三点叠加起来,决定了不能用简单的统计方法或规则模板解决。一句"学而时习之,不亦说乎",有人译成"学习后经常温习,不也愉快吗",有人译成"学了知识然后按时去实践它,不也很高兴吗",两种译文都对,但字面上完全对不上。这意味着模型要学的不只是词汇替换,而是"语义层面的转写"。也正是因为这种多样性,翻译评估不能用单一指标,后面避坑那一章会专门讲这一点。

2.2 规则、统计还是神经机器翻译:三条路线的取舍逻辑

做这个题目前,先回答一个问题:技术路线选哪条?早年做古文翻译有两条老路。一是规则法,人工编写句法规则和词典,比如"看到'其'在人称位置,译为'他的'"。这种做法在小范围测试集上表现稳定,但规则一多就互相冲突,覆盖不了真实语料里千奇百怪的句式,维护成本极高。二是统计机器翻译(SMT),基于短语对齐和语言模型做打分,效果比规则法好不少,但需要大量人工设计的特征,训练流程复杂,现在学术界和工业界都已经很少再单独使用。

当前的主流是神经机器翻译(NMT),用端到端的序列到序列模型直接学习"古文序列 → 现代文序列"的映射。NMT 的优势在于不需要显式设计特征,注意力机制天然处理长句和省略现象。对毕设项目来说,NMT 的代码结构清晰,可视化潜力大(注意力权重能用来解释模型),答辩时"讲原理"这个环节特别好展开。你完全可以从零搭一个 Transformer,不依赖现成翻译 API,源码和原理都能自己说清楚,这样的工作量放在课程设计或毕业设计里,既不会单薄到没内容,也不会难到做不完。

2.3 自训 Transformer 和微调预训练模型:两条路怎么选

确定了用 NMT 之后,还有一次选型:是自己从零训练一个小规模 Transformer,还是基于 mT5、ChatGLM 这类预训练模型做微调?我的建议是,取决于你的环境和答辩策略。如果你的电脑有 12GB 以上显存的显卡,微调一个开源预训练模型,翻译质量会明显更高,尤其对付低频词和生僻句式的能力更强。如果你的环境只是普通笔记本 CPU,或者学校服务器资源紧张,那就从头训练一个小 Transformer,语料控制在几万到十几万句对,词表八千到一万,训练时间控制在一两个小时内,照样能跑出有说服力的结果。

我一般会推荐毕设场景选后者。原因有三:第一,从零实现 Transformer 能覆盖注意力机制、位置编码、beam search 这些高频考点,答辩时老师问什么都有准备;第二,微调预训练模型有"黑匣子"嫌疑,老师问"为什么这里效果不好"时很难深入;第三,CPU 环境下小模型也能完整走通流程。不过,如果你的题目偏向应用型,想做出一个"能用"的翻译工具,那微调路线更合适,效果直观,展示时翻车概率小。两条路不冲突,后面章节的核心流程两条路通用,差异只在加载模型和训练配置上。

3. 语料是项目的命门:平行语料的采集、清洗与对齐

3.1 语料从哪来:公开数据与自建语料的配比策略

古文到现代文的平行语料不像中英翻译那么好找,公开的成规模数据集不多。常见的来源有三类:一是古诗文类网站里的原文配译文,二是古籍白话文对照本,三是教材里的文言文课文和翻译。第一类数量最大,但格式最乱;第二类质量高,但覆盖范围有限;第三类可以通过 OCR 或手动整理获得,适合做小规模测试集。坦白说,凡是爬下来的语料,原文和译文错位、缺行、乱码是常态,这一步做不好,后面训练出来的模型基本就是乱译。很多项目最终效果差,不是因为模型不行,而是语料本身脏。

数据配比上,我建议总量跑到五万到十五万句对。如果公开数据凑不够,可以结合"半自动构造":拿《论语》《孟子》这类经典篇章,把逐句的白话文翻译手工整理,速度虽然慢,但能保证对齐质量。还有一个小技巧,古诗文网站里经常有"每首诗配一段翻译"的格式,这种数据可以按段落切句,再用标点和长度信息做粗对齐,最后人工抽检。语料宁可少而精,不要多而乱,一万句干净数据训练出来的模型,通常比十万句错位数据强得多。

3.2 清洗与对齐脚本:一套可直接改着用的数据清洗流程

拿到原始文本后,第一步不是训练,而是老老实实做清洗。我会先把原始文件丢进一个 Python 脚本里跑一遍,做繁简统一、去空格、去多余标点、过滤畸形长度。下面这段脚本是一个可复用的清洗模板,按你的数据格式改改就能跑:

import re from opencc import OpenCC # 用 OpenCC 把繁体统一成简体,避免词表被同一字的繁简两式重复占用 cc = OpenCC('t2s') def clean_pair(src: str, tgt: str): src = cc.convert(src.strip()) tgt = cc.convert(tgt.strip()) # 去掉书名号、引号等噪声符号 src = re.sub(r'[《》「」『』“”‘’]', '', src) tgt = re.sub(r'[《》「」『』“”‘’]', '', tgt) # 去掉所有空白字符 src = re.sub(r'\s+', '', src) tgt = re.sub(r'\s+', '', tgt) # 过滤空行和畸形短句 if len(src) < 4 or len(tgt) < 4: return None # 过滤超长句子,Transformer 对超长输入不稳定 if len(src) > 256 or len(tgt) > 256: return None # 长度比过滤:文言文通常比译文短,但比例不会超过阈值 if len(src) / len(tgt) > 4 or len(tgt) / len(src) > 4: return None return src, tgt pairs = [] with open('raw_corpus.txt', 'r', encoding='utf-8') as f: lines = f.readlines() # 假设原始文件的格式是"原文行 + 译文行"交替出现 for i in range(0, len(lines) - 1, 2): src_line = lines[i].strip() tgt_line = lines[i + 1].strip() cleaned = clean_pair(src_line, tgt_line) if cleaned: pairs.append(cleaned) with open('corpus_clean.tsv', 'w', encoding='utf-8') as f: for src, tgt in pairs: f.write(f"{src}\t{tgt}\n")

清洗脚本里最容易被忽略的是长度比过滤。我见过一份语料,原文是"学而时习之"五个字,译文却配了一整段三百字的讲解,这种数据喂给模型,模型会学成"加长型扩写器"。把长度比上限压到四倍,基本能滤掉这类问题。OpenCC 这一步也建议保留,很多古诗文网站混排繁体简体,不统一的话同一个字在词表里占两个位置,稀释了模型学到的字形信息。

3.3 分词与词表:别拿 jieba 切古文,用 BPE 或字符级

分词这一步,新手最容易踩坑。jieba 这类现代中文分词器是跑现代汉语的,切古文经常把"之""乎""者""也"这类虚词单独切出来,或者把"不亦说乎"整个切成一个词,结果就是词表又碎又不稳定。做古文到现代文的翻译,不建议做基于词典的切分,两条路更稳。一条是字符级建模:直接把每个汉字当一个 token,词表就是常用汉字集合加标点,大小一般两三万,简单粗暴,对生僻字友好。另一条是子词建模:用 SentencePiece 直接在原文和译文混合语料上训练 BPE 词表,让模型自动学习"之乎者也"这类高频虚词组合。

我的推荐是走 BPE 子词路线。下面这段代码用 SentencePiece 训练一个八千大小的 BPE 词表,注意要把源语言和目标语言文本拼在一起训练,这样推理时源端和目标端共用一套词表,不需要单独处理映射:

import sentencepiece as spm # 先准备一个纯文本文件,每行一句话,原文和译文都放进去 spm.SentencePieceTrainer.train( input='corpus/all_text.txt', # 所有原文+译文,每行一句 model_prefix='bpe', # 输出的模型前缀 vocab_size=8000, # 词表大小,量级参考这里 model_type='bpe', # 子词切分方式 character_coverage=0.9995, # 覆盖 99.95% 的字符,生僻字不会被频繁 UNK num_threads=8, # 并行训练线程 )

训练完成后会生成bpe.model和bpe.vocab两个文件。后续加载数据时,用spm.SentencePieceProcessor读取模型,把每句话 encode 成 id 序列即可。这里character_coverage不要直接设成 1.0,因为语料里总有些奇怪的生僻字和噪声字符,保留千分之五的未知量会让模型学会处理未知字符而不是死记硬背。vocab_size 也别贪大,古文语料规模有限,八千到一万二之间足够,词表再大就容易出现过拟合。

4. 训练一个古文到现代文的 Transformer 翻译器:超参数与代码落点

4.1 模型结构怎么定:一个能跑在 CPU 上的最小配置

确定词表后,模型结构直接照搬 Transformer base,不需要改任何架构代码。以 PyTorch 的实现为例,关键超参数可以这样设:d_model=512,nhead=8,num_encoder_layers=6,num_decoder_layers=6,dim_feedforward=2048,dropout=0.1。这套参数在 CPU 上训练十五万句对大约耗时几小时,可以接受。如果你只想快速验证流程,可以把层数降到四层,feedforward 降到 1024,训练时间能压到一小时以内。

这里有个常被忽视的点:位置编码。Transformer 本身没有顺序信息,位置编码是模型理解"错位翻译"的关键。PyTorch 自带的Transformer模块内置了位置编码,但如果想可视化注意力权重用于答辩展示,我还是建议自己写完整的编码器解码器结构,不要直接调nn.Transformer,因为后者封装程度高,取中间层的注意力权重比较麻烦。自写结构代码量不大,还能在报告里贴核心公式,加分明显。

4.2 训练脚本:学习率调度、标签平滑和梯度裁剪缺一不可

训练循环本身不复杂,但有几个参数直接决定能不能收敛。下面这段是一个经过验证的训练主循环骨架,注意看学习率、标签平滑、梯度裁剪这三处:

import torch from torch.nn.utils import clip_grad_norm_ def train_epoch(model, dataloader, optimizer, criterion, device, grad_clip=1.0): model.train() total_loss = 0 for batch in dataloader: src, tgt = batch src, tgt = src.to(device), tgt.to(device) # 输入 decoder 时去掉最后一个 token,预测时偏移一位 logits = model(src, tgt[:, :-1]) # 预测目标是去掉第一个 token 后的序列 loss = criterion( logits.reshape(-1, logits.size(-1)), tgt[:, 1:].reshape(-1) ) optimizer.zero_grad() loss.backward() # 梯度裁剪是防 NaN 的第一道保险 clip_grad_norm_(model.parameters(), grad_clip) optimizer.step() total_loss += loss.item() return total_loss / len(dataloader) # 参数:标签平滑让模型不要过度自信,对翻译这类多样性问题很有用 criterion = torch.nn.CrossEntropyLoss(ignore_index=0, label_smoothing=0.1) # Adam + Noam 学习率调度:先 warmup 再衰减,训练更稳定 optimizer = torch.optim.Adam(model.parameters(), betas=(0.9, 0.98), eps=1e-9)

训练时我会把学习率峰值设到5e-4,warmup 步数4000。如果用默认的固定学习率,很容易在训练中期出现 loss 震荡甚至炸掉。标签平滑 0.1 看起来不起眼,但在古文翻译这种"一对多"任务里很关键——同一句古文有多个合法译文,如果模型被训练成只认一种,BLEU 分数再高,人看起来也别扭。损失函数里的ignore_index=0对应<pad>token 的 id,这一步不能省,否则 padding 位置也会参与梯度计算,白白增加噪声。

4.3 推理与第一次翻译验证:beam search 的宽度该设多少

训练收敛后,写一个翻译脚本验证效果。这里有两个选择:贪心搜索(每次都取概率最高的词)或 beam search(保留多个候选路径)。贪心快但容易丢全局信息,常常翻出主语残缺的句子;beam search 效果明显更好。下面这段 beam search 是简化版,重点看 beam 宽度和长度惩罚的配合:

def beam_search(model, src_tokens, beam_size=5, max_len=128, eos_id=2, len_penalty=1.2): # 初始候选只有一个 <bos>,score 为 0 beam = [( [], 0.0 )] for _ in range(max_len): new_beam = [] for seq, score in beam: if seq and seq[-1] == eos_id: new_beam.append((seq, score)) continue # 把当前序列拼成 decoder 输入,取最后一步的 log 概率 dec_input = torch.tensor([seq] if seq else [[1]], device=model.device) # 1 是 <bos> with torch.no_grad(): logits = model.decode_step(src_tokens, dec_input) log_probs = torch.log_softmax(logits[-1], dim=-1) # 取 top-k 扩展候选 top_k = log_probs.topk(beam_size) for token, lp in zip(top_k.indices, top_k.values): new_beam.append((seq + [token.item()], score + lp.item())) # 按分数排序,并应用长度惩罚,过滤掉凑长度的劣质译文 new_beam.sort(key=lambda x: x[1] / ((len(x[0]) + 5) ** len_penalty), reverse=True) beam = new_beam[:beam_size] return beam[0][0]

beam 宽度 5 是常用值,原型验证时足够;想现场跑快一点可以降到 3,效果差别不大。长度惩罚len_penalty设 1.2 是为了防止模型生成长而重复的句子,这个值在中文翻译里基本够用。跑完脚本后,选几句经典句子试翻译,比如"学而时习之,不亦说乎",如果输出大致通顺,再试一些语料里没出现过的句子,比如"李白打酒"这类典故句,观察模型会不会因为句式和素材的差异而翻车。这一步的输出效果,直接决定你后续要不要调整数据或模型规模。

5. 避坑:训练古文翻译模型最容易翻车的 5 个地方

5.1 语料错位:模型"学了个寂寞"的元凶

现象:loss 前期下降正常,到中后期开始震荡,训练结束后的译文句子结构混乱,比如"学而时习之"被译成"的的学习温习他"。原因:爬虫抓取的原文和译文并行文本行数不对齐,导致模型输入输出是随机配对的。解决:清洗脚本里加"序号锚点"校验。有不少古诗文网页每段原文旁边带小编号(如原文 [1]、译文 [1]),按序号抽取能有效避免错位。纯文本格式的话,至少做长度比过滤加人工抽检,挑五十对看一眼,错位率超过百分之五就得返工。

5.2 BLEU 分数虚高:参考译文只有一种,机器分数会骗人

现象:验证集 BLEU 值跑到了 35 以上,看起来很漂亮,但把模型译文拿给人看,完全不是人话,要么重复堆词,要么把虚词全部丢掉。原因:BLEU 是 n-gram 重合度计算,古文翻译存在大量合法改写,参考译文只收录了一种,模型只要命中几个片段就能刷高分,即使整体语序和逻辑是乱的。解决:不要只看 BLEU,加一个 chrF 指标(字符级 n-gram 匹配),它更接近人工观感。更重要的是做人工评测,至少准备三十句测试句,按忠实度和流畅度两栏打分,这个结果写进毕业论文里比一个 BLEU 数字有说服力得多。

5.3 生僻字和异体字把词表撑爆,还频繁出 UNK

现象:词表设了八千,训练时发现 UNK 比例高达百分之十以上,很多常见古字被切分成了未知字符,翻译出来的句子满屏<unk>。原因:文言文里存在大量现代汉语不用的异体字、古字和通假字,直接训练 BPE 时低频生僻字占了大量词表名额,常见字反而被挤占。解决:第一,语料清洗阶段把生僻字统一映射成<unk>而不是强行保留;第二,BPE 训练时把character_coverage调到 0.9995 以下,让训练器自动把尾部稀有字符折叠;第三,在推理脚本里加一个"生僻字提示",检测到输入有生僻字时弹一条提示,避免展示现场出现大段 UNK。

5.4 训练中期 loss 突然变成 NaN

现象:epoch 跑到第二十个左右,loss 直接从 3.2 跳到 NaN,后面怎么调都回不来。原因:最典型的是学习率过高导致梯度爆炸,或者是语料里混入了不可见的控制字符(比如从网页爬下来的零宽空格、换行符残留),这些字符进入词表后产生异常梯度。解决:优化器用 Adam 并加 warmup,是防 NaN 的有效手段;如果已经炸了,可以尝试降低学习率重新跑,同时把清洗脚本升级一下,用unicodedata过滤掉不可见控制字符。需要注意的是,label_smoothing不会导致 NaN,但千万不要把 smoothing 设到 0.3 以上,会让损失居高不下。

5.5 分词器与词表不一致:训练正常,推理却报尺寸不匹配

现象:训练脚本跑通,但写推理脚本时一加载词表就报IndexError或者size mismatch。原因:训练时用的 SentencePiece 词表是在"全部古文+现代文混合文本"上训练的,但推理脚本里可能又单独加载了一个别的分词器版本,或者直接用 jieba 切词再查词表,结果词表 id 对不上。解决:训练和推理必须使用同一个spm.model文件,推荐在代码里写一个全局常量指向bpe.model的位置,不要复制到别处再改名。另外,检查两个脚本里encode时的参数是否一致(比如是否加了add_bos),一个加了<bos>一个没加,序列长度就会错位,推理时 Transformer 会直接报错。

6. 从模型到交付:评估、界面封装与源码组织

6.1 多维度评测:一张评分表讲清你的模型效果

答辩时最怕被问"你的翻译效果到底怎么样",只丢一个 BLEU 值是不够的。建议做一个二十到三十句的测试集,每句按三个维度打分:忠实度(原文信息是否完整保留)、流畅度(译文是否通顺)、风格贴合度(是否像现代汉语而不是半文半白)。每个维度一到五分,找三个人打分取平均,把结果做成表格。如果你微调了预训练模型,还可以挑五句做对比展示,同一句古文放上你的模型译文和基线模型译文,这种直接对比在答辩现场比任何指标都有冲击力。

6.2 用一个 Streamlit 脚本把模型变成能演示的 Web 页面

训练脚本写得再好,评委也想亲手输入试试。用 Streamlit 写一个几十行的演示页面,是最快的交付方式。架子很简单:页面放一个输入框,一个"翻译"按钮,点击后调用上面的 beam search 函数,把结果打印在下边。界面不用华丽,重点是把输入、输出、耗时三块信息展示清楚。

import streamlit as st from predict import translate_sentence # 你自己的推理函数 st.set_page_config(page_title="古文翻译 Demo") st.title("古文到现代文机器翻译") src_text = st.text_area("输入古文", "学而时习之,不亦说乎?") if st.button("翻译"): result, time_cost = translate_sentence(src_text) st.markdown(f"**译文**:{result}") st.caption(f"推理耗时:{time_cost:.2f} 秒")

6.3 源码目录这样组织,答辩不用翻文件找半天

最后说源码目录的事。题目里带"源码"二字,交上去的目录结构一定要让别人能直接复现。我建议按六个目录拆开:data/放原始语料和清洗脚本,preprocess/放 SentencePiece 训练和词表构建,model/放 Transformer 定义,train.py放训练入口,predict.py放推理函数,web/放 Streamlit 页面。每个目录配一个README.md写清楚这个目录干什么、跑哪个命令。训练脚本和预测脚本的命令要写在根目录的README.md靠前位置,保证别人拿到源码,按照命令一步步跑就能复现你的训练过程。这个交付习惯,我在做过几次项目后觉得比模型本身还重要。你永远不知道答辩前夜会被问出什么奇怪问题,但能快速定位到源码文件,就成功了一半。希望这些踩过的坑和验证过的参数能帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询