
简介面向毕业设计、课程设计与项目开发场景这份基于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, encodingutf-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, encodingutf-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( inputcorpus/all_text.txt, # 所有原文译文每行一句 model_prefixbpe, # 输出的模型前缀 vocab_size8000, # 词表大小量级参考这里 model_typebpe, # 子词切分方式 character_coverage0.9995, # 覆盖 99.95% 的字符生僻字不会被频繁 UNK num_threads8, # 并行训练线程 )训练完成后会生成bpe.model和bpe.vocab两个文件。后续加载数据时用spm.SentencePieceProcessor读取模型把每句话 encode 成 id 序列即可。这里character_coverage不要直接设成 1.0因为语料里总有些奇怪的生僻字和噪声字符保留千分之五的未知量会让模型学会处理未知字符而不是死记硬背。vocab_size 也别贪大古文语料规模有限八千到一万二之间足够词表再大就容易出现过拟合。4. 训练一个古文到现代文的 Transformer 翻译器超参数与代码落点4.1 模型结构怎么定一个能跑在 CPU 上的最小配置确定词表后模型结构直接照搬 Transformer base不需要改任何架构代码。以 PyTorch 的实现为例关键超参数可以这样设d_model512nhead8num_encoder_layers6num_decoder_layers6dim_feedforward2048dropout0.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_clip1.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_index0, label_smoothing0.1) # Adam Noam 学习率调度先 warmup 再衰减训练更稳定 optimizer torch.optim.Adam(model.parameters(), betas(0.9, 0.98), eps1e-9)训练时我会把学习率峰值设到5e-4warmup 步数4000。如果用默认的固定学习率很容易在训练中期出现 loss 震荡甚至炸掉。标签平滑 0.1 看起来不起眼但在古文翻译这种一对多任务里很关键——同一句古文有多个合法译文如果模型被训练成只认一种BLEU 分数再高人看起来也别扭。损失函数里的ignore_index0对应padtoken 的 id这一步不能省否则 padding 位置也会参与梯度计算白白增加噪声。4.3 推理与第一次翻译验证beam search 的宽度该设多少训练收敛后写一个翻译脚本验证效果。这里有两个选择贪心搜索每次都取概率最高的词或 beam search保留多个候选路径。贪心快但容易丢全局信息常常翻出主语残缺的句子beam search 效果明显更好。下面这段 beam search 是简化版重点看 beam 宽度和长度惩罚的配合def beam_search(model, src_tokens, beam_size5, max_len128, eos_id2, len_penalty1.2): # 初始候选只有一个 bosscore 为 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]], devicemodel.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(keylambda x: x[1] / ((len(x[0]) 5) ** len_penalty), reverseTrue) 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靠前位置保证别人拿到源码按照命令一步步跑就能复现你的训练过程。这个交付习惯我在做过几次项目后觉得比模型本身还重要。你永远不知道答辩前夜会被问出什么奇怪问题但能快速定位到源码文件就成功了一半。希望这些踩过的坑和验证过的参数能帮到你。本文还有配套的精品资源点击获取