基于Python和BERT的文本相似度检测系统开发实战

发布时间:2026/10/7 21:54:18
基于Python和BERT的文本相似度检测系统开发实战 简介一份基于BERT的深度学习文本相似度检测系统毕业设计源码包面向计算机相关专业学生的毕业设计与课程设计亦可供NLP初学者研究语义匹配算法参考。系统以Python 3.6.8实现核心模型搭配MySQL 5.7数据库及完整前端页面覆盖文本预处理、模型构建、训练评估、相似度比对等关键环节。压缩包共389个文件主要包含Python源码、HTML/CSS/JS前端资源、SQL数据库脚本、说明文档docx/pdf以及演示动图gif等整体大小约52.19MB目录按源码、数据库、说明文档、成品划分结构清晰明确。目前已有78人学习浏览。借助该项目读者可快速获得一套可运行的完整系统并能从说明文档与源码中了解BERT文本相似度检测的实现细节为毕业设计答辩或进一步功能扩展提供扎实基础。1. BERT文本相似度检测系统毕设选题到底在做什么每到毕业设计季节文本相似度检测都是最容易撞车的选题但很多人只是交个模型演示真正把前后端、MySQL和深度学习模型完整串起来的很少。这套基于 Python BERT MySQL 的系统核心任务很清晰输入两段文本后台用 BERT 生成句向量再算余弦相似度最后把分数展示到前端并落库。它既能当查重工具的简化版也能直接改造成 FAQ 问答匹配、客服工单分诊这类小工具。如果你正在挑毕设方向或者刚接触 NLP 想找一个能跑通完整工程链路的练手项目这个标题值得认真读一遍。2. 系统架构与选型为什么是 BERT MySQL2.1 BERT 为什么能扛住文本相似度这个任务文本相似度的传统解法是 TF-IDF 加词向量问题在哪儿呢TF-IDF 只看词面重合把句子拆成词频向量“西红柿”和“番茄”这两个词完全不同就算语义一模一样得分也趋近于零。而 Word2Vec 这类静态词向量虽然能捕捉一部分词义但它不认识上下文——“苹果”在“苹果手机”和“苹果真好吃”里永远是一份固定向量。BERT 用双向 Transformer 做预训练每个 token 的表征都是上下文感知的所以同一个词在不同句子里可以得到不同的语义表示这正是文本相似度最需要的特性。实际选型时常见做法是直接用 Hugging Face 的 transformers 库加载bert-base-chinese。这个模型是 BERT 的中文基础版权重体积大约 400 MBCPU 上跑一次推理在几百毫秒到一两秒之间对毕业设计演示来说完全够用。如果你的机器配置较差导师也没有硬性要求可以换成distilbert-base-chinese速度快一倍准确率只掉两三个点。我在搭这类系统时习惯先把bert-base-chinese跑通再做模型替换对比否则一开始就上小模型出了问题反而分不清是模型问题还是代码问题。除了模型选型池化策略也是容易踩坑的地方。BERT 输出的last_hidden_state是每个 token 一个向量要把整句话压缩成一个固定长度的句向量常见的有三种取[CLS]token 的输出、直接平均所有 token 的向量、按 attention mask 加权平均。三种里平均池化最稳定[CLS]在某些句子对任务上表现不错但它在预训练时是用来做下一句预测的直接拿来做相似度不总是最优。我一般会写一个对比脚本在验证集上跑一下三种方式选分数最高的那个这在论文里也能当实验数据写。2.2 存储层选 MySQL毕设评分老师最认的“完整系统”NLP 项目做到工程化的时候很多人会跑去用 FAISS、Milvus 这类向量数据库因为相似度检索快。但毕业设计场景我强烈建议老实使用 MySQL原因很直接向量数据库只解决“找最近邻”一个问题而毕设要展示的是完整的业务闭环。用户登录、提交句子对、查看历史检测记录、删除记录这些都属于结构化数据用 MySQL 建两张表就能讲清楚答辩时老师问“数据存在哪里”你可以打开 Navicat 给他看表结构比对着向量数据库讲索引机制直观得多。还有一个现实因素标题里已经写死了 mysql如果自己擅自换库不符合选题预期。MySQL 的生态也最成熟导师电脑上十有八九装了部署演示不会因为环境问题翻车。表设计不需要复杂常见的做法是这样的CREATE DATABASE IF NOT EXISTS simi_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE simi_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE detect_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, text_a TEXT NOT NULL, text_b TEXT NOT NULL, score FLOAT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) );这里的score字段存相似度分数类型用 FLOAT 就够不用双精度。text_a和text_b用 TEXT因为输入的句子可能超过 255 个字符。外键user_id是为演示“用户登录后只能看自己的记录”这个功能服务的加了外键也能体现数据库设计的完整性答辩时能加分。另外一个容易被忽略的点是utf8mb4。MySQL 里的utf8实际上是 utf8mb3三个字节存 emoji 或生僻汉字会报错“Incorrect string value”所以建库时一定要显式写utf8mb4。如果你的源 SQL 文件里没写自己导入前先改掉这个教训我见过太多次了。2.3 毕业设计包里到底该有什么源码、说明文档、LW标题里的 LW 在毕设语境下通常指论文文档。一套能拿得出手的毕业设计包内容应该是前端工程、后端 Flask 服务、模型加载与推理代码、SQL 初始化脚本、requirements.txt、说明文档以及 LW 论文。缺少任何一项答辩评分表上都会扣分。我拆解这类源码包的顺序是先看 SQL 文件把数据库建好再跑后端服务最后启动前端页面。这个顺序能帮你快速排查问题因为后端依赖模型和数据库前端只依赖后端接口先把地基打好后面才不会一头雾水。论文文档里的核心不是抄 BERT 原理而是要有对比实验。比如在 ATEC 或 BQ 数据集上分别用 TF-IDF、Word2Vec、BERT 跑一版计算出准确率和 F1做成柱状图。哪怕结果就是 BERT 最高这个实验过程也能证明你“动手深度学习”了而不是只会调包。说明文档则要写清楚环境变量和模型路径最容易导致系统跑不起来的坑是说明里没写模型缓存在哪别人拿到代码后 transformers 自动去网上下载网一断整个演示就完了。所以一个成熟项目里应该提供离线模型下载说明或者直接放一个下载好权重的压缩包这才是完整交付物。3. 本地部署把环境跑起来的完整步骤3.1 Python 与 PyTorch 环境安装部署这套系统第一道坎是 Python 环境。很多电脑上自带 Python 3.8但系统 PATH 里可能同时存在 Anaconda、Windows Store 安装的 Python、以及 Visual Studio 自带的解释器pip 装包时最容易装错地方。我一般用 Anaconda 建独立环境命令如下conda create -n simi python3.9 -y conda activate simi pip install torch2.1.0 pip install transformers pandas flask flask-cors pymysqlPython 3.9 是兼容性最好的版本后续安装不会有依赖冲突。torch2.1.0是目前 CPU 和 GPU 版本都比较稳定的一个版本pip 会根据你的机器自动选择带 CUDA 的版本或纯 CPU 版本不需要手动指定。如果你在 macOS 上运行要把 torch 安装命令换成官方推荐的pip install torch --index-url https://download.pytorch.org/whl/cpu避免拉取到 CUDA 版本导致运行时报错。三段 pip 命令里transformers负责加载 BERTflask写后端接口flask-cors解决前端跨域请求问题pymysql是 Python 操作 MySQL 的驱动。装完后做一次环境自检python -c import torch, transformers, pymysql; print(ok)如果输出 ok说明基础依赖没问题这里pymysql导入成功同时验证了 Python 环境完整。需要注意的是torch默认会下载约 200 MB 的 CPU 运行库网络不好的时候容易超时可以先配置pip install -i https://pypi.tuna.tsinghua.edu.cn/simple来加速如果你在校园网清华源通常稳定得多。3.2 MySQL 安装配置与数据库初始化MySQL 安装教程看着简单但版本选择是个玄学。Windows 上常见的安装方式有两种官方 MSI 安装包和免安装的 ZIP 解压版。MSI 版一路点下一步就能跑起来容易把服务注册成 Windows 服务ZIP 免装版需要自己初始化对新手不友好。我推荐用 MSI 安装 MySQL 8.0安装过程中记住 root 密码卸载也干净后悔药好找。安装好后先用命令行登录一次确认服务状态mysql -uroot -p登录后导入项目里的 SQL 文件。假设源码包里有一个init.sql命令是mysql -uroot -p simi_db init.sql注意simi_db这个数据库名要提前在 SQL 文件里写创建语句否则导入时会提示数据库不存在。如果你的 SQL 文件里有CREATE DATABASE也可以直接执行mysql -uroot -p init.sql我一般会建议你在 MySQL 中新建一个专用账号代替 root 作为后端连接凭证因为 root 密码一旦写死在代码里换台机器跑就暴露出密码不同的风险。专用账号的创建语句是CREATE USER simi_userlocalhost IDENTIFIED WITH mysql_native_password BY 123456; GRANT ALL PRIVILEGES ON simi_db.* TO simi_userlocalhost; FLUSH PRIVILEGES;这里用mysql_native_password是为了兼容 pymysql 1.x 的认证方式如果你用 pymysql 2.0MySQL 8.0 默认的caching_sha2_password也能用但为了避免版本问题统一指定老认证插件更省事。FLUSH PRIVILEGES刷新权限表不然新账号可能起效不及时。数据库配好后后端连接串的常见写法是这样的DB_CONFIG { host: localhost, port: 3306, user: simi_user, password: 123456, database: simi_db, charset: utf8mb4 }这个字典里的charset必须写utf8mb4配合建库时的字符集才能保证中英文以及表情符号都能正确存储和读取。端口号默认 3306如果你安装时改过端口这里同步修改。3.3 启动前后端与模型缓存后端启动前BERT 模型必须先下载到本地。transformers 库第一次调用BertModel.from_pretrained时会访问 Hugging Face 官网下载权重常见做法是手动先跑一次下载脚本python -c from transformers import BertTokenizer, BertModel; BertTokenizer.from_pretrained(bert-base-chinese); BertModel.from_pretrained(bert-base-chinese)这条命令会把模型缓存到用户目录下的.cache/huggingface里之后所有脚本调用都直接用缓存。如果你在服务器环境无法访问外网就去手动下载模型压缩包放到项目目录里再把from_pretrained的参数改成本地路径。这在比赛或答辩现场是保命操作。后端服务用 Flask 写启动命令很简单python app.pyFlask 默认监听 5000 端口如果你 8080 被占用可以指定python app.py --port5001启动后看到* Running on http://127.0.0.1:5000就说明服务起来了。这时用 curl 做一次接口冒烟测试curl -X POST http://127.0.0.1:5000/api/similarity -H Content-Type: application/json -d {text_a: 今天天气不错, text_b: 今天天气很好}返回 JSON 字符串里带score就说明 BERT 推理链路通了。前端方面如果项目是静态 HTML直接用浏览器打开如果是 Vue 工程先npm install再npm run serve默认端口 8080。前端和后端如果不在同一台机器会触发跨域后端在 Flask 里启用CORS(app)就能解决别在前端配置代理这样演示灵活性更高。4. 核心实现BERT 模型实操与相似度计算4.1 数据预处理与文本清洗别让脏文本拖后腿文本相似度系统的指标好坏一半取决于模型一半取决于预处理。直接拿原始字符串去喂 BERT会遇到很多边界情况HTML 标签、多余换行、全角半角符号混用都会让句向量发生偏移。我一般的预处理函数只做三件事import re def clean_text(text: str) - str: # 去掉 HTML 标签比如 br、p 等 text re.sub(r[^], , text) # 把多个空白字符替换成一个空格保留单词间分隔 text re.sub(r\s, , text) # 去掉首尾空格统一为小写只针对英文 text text.strip().lower() return text代码里的re.sub(r[^], , text)是正则替换匹配形如...的标签并删除\s匹配所有空白字符包括换行、制表符、多个空格把它们归并成一个空格.lower()用于英文数据集中文不受影响。这套清洗对后续相似度分数的稳定性作用很大特别是从网页爬来的文本如果不清理同一句话加上不同标签后相似度会从 0.95 掉到 0.7。有些同学喜欢在预处理里加入去停用词比如去掉“的、了、是”等常见词但 BERT 分词器自己会处理这些删掉反而会改变句子的语法结构干扰注意力计算。我做这个项目时吃过亏停用词表一去短文本的相似度就明显波动。经验之谈是如果要做关键词抽取可以预处理去停用词如果是 BERT 全句编码不要动停用词。4.2 加载 BERT 模型生成句向量关键参数一览模型加载和推理是整个系统的核心常见做法是封装一个类避免每次请求都重复加载模型。代码长这样from transformers import BertTokenizer, BertModel import torch class BertEmbedder: def __init__(self, model_namebert-base-chinese): self.tokenizer BertTokenizer.from_pretrained(model_name) self.model BertModel.from_pretrained(model_name) self.model.eval() # 切到评估模式关闭 dropout def encode(self, text: str) - torch.Tensor: # 分词、截断、填充到固定长度 encoded self.tokenizer( text, max_length512, truncationTrue, paddingmax_length, return_tensorspt ) with torch.no_grad(): # 不计算梯度推理更快 outputs self.model(**encoded) # 取 last_hidden_state对非 padding 位置做平均池化 token_vecs outputs.last_hidden_state # [1, 512, 768] attention_mask encoded[attention_mask] masked token_vecs * attention_mask.unsqueeze(-1) summed masked.sum(dim1) counts attention_mask.sum(dim1, keepdimTrue) return (summed / counts).squeeze(0)这段代码的关键参数有四个。max_length512是 BERT 的硬上限输入超过 512 个 token 时truncationTrue会把后面的内容截掉保住前 512 个词。为什么截前面不截后面因为自然语言的语义重点往往在开头居中向量的预训练也没有特别处理尾部。paddingmax_length的意义在同批次对齐当一次性编码多句话时必须让所有句子长度一致才能放进 tensor。attention_mask则是用于区分真实 token 和 padding 的位置我在池化时用掩码把 padding 的贡献清零这样就避免了 5.4 节要讲的“相似度全部偏高”的坑。这里用的是平均池化也就是把所有真实 token 的向量取均值。为什么不直接取outputs.pooler_output它的输出在预训练时是配合下一句预测任务设计的做语义相似度时平均池化通常更稳。如果你希望句向量能体现句子里的重点词可以尝试注意力加权池化但大多数情况下平均池化已经够毕业设计了。4.3 相似度度量余弦相似度与阈值设置句向量生成后度量相似度最常用的就是余弦相似度import torch.nn.functional as F def cosine_sim(vec_a: torch.Tensor, vec_b: torch.Tensor) - float: # 两个向量都已经是 768 维需要扩维成 [1, 768] 才能调用函数 sim F.cosine_similarity(vec_a.unsqueeze(0), vec_b.unsqueeze(0)) return float(sim.item())余弦相似度计算与向量长度无关只考虑方向夹角适合 BERT 这种 768 维的高维向量。为什么不选欧氏距离因为句向量的模长受句子长度影响很大长句子平行向量模长更大直接用欧氏距离会让“相似”的定义偏向文本长度不纯粹。阈值怎么确定常见做法是先收集 200 对“相似/不相似”的样本跑一遍系统画出 ROC 曲线选约登指数最大的点。但在毕设演示中更实用的做法是设置 0.8 作为默认相似阈值大于等于阈值显示“相似”否则显示“不相似”。0.8 这个值在大多数中文语义数据集上处于召回和精度的平衡点。你也可以把阈值做成可配置参数后端接口接收一个threshold字段前端放一个滑杆答辩时现场调节能直观展示不同阈值下的判断变化这一招比较讨巧。要注意阈值只对“判断是否相似”有用前端展示的相似度数值本身不应该被篡改。我在写后端时会把原始score和经过阈值判断的is_similar两个字段都返回这样前端既显示百分比又显示文字结论数据也能做后续分析。5. 避坑指南从调包到前端显示的五个常见问题5.1 现象加载 BERT 时内存爆掉或直接进程被杀原因你安装的 torch 是 GPU 版但机器没有 NVIDIA 显卡或者显卡显存不足 2G。GPU 版 torch 初始化时会尝试分配显卡显存失败后回退 CPU 有可能出问题。另外一个原因是虚拟内存不足BERT 模型加载时会把权重读入内存大约占用 400 MB 到 1 GB。解决先确认 torch 是不是 CPU 版本执行python -c import torch; print(torch.cuda.is_available())看看输出。如果当前环境一直输出 False但加载模型时报的是 CUDA 错误就强制指定设备为 CPU在模型加载前加torch.device(cpu)。同时给系统设置一个较大的交换分区Windows 上把虚拟内存调到 8GB 以上可以缓解闪退。此外在启动代码里写上os.environ[TOKENIZERS_PARALLELISM] false可以避免分词器在多线程下引发偶发崩溃。5.2 现象MySQL 连接失败但密码和账号都对原因MySQL 8.0 默认使用caching_sha2_password认证插件而 pymysql 1.0 版本默认只支持mysql_native_password握手协议不匹配就会报Authentication plugin caching_sha2_password cannot be loaded。解决三种方案任选。升级 pymysql 到 2.0 以上兼容新认证或者在 MySQL 里改回老插件命令是ALTER USER simi_userlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;。第三种方案是在后端连接配置中使用 SSL 连接但本地开发完全没必要。我推荐第一种升级依赖最干净不用动数据库账号安全策略。5.3 现象写入中文变成乱码前端显示问号或方块原因数据库连接没有指定charsetutf8mb4或者表本身建在latin1上。MySQL 的安装默认字符集在 Windows 上经常是latin1直接存中文就乱码。解决建库时用CREATE DATABASE simi_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;后端连接参数里加charset: utf8mb4。如果你已经建好了表执行ALTER TABLE detect_record CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;也能补救。注意这个 ALTER 操作会把现有数据重新编码乱码已经存在的话先备份再改不然数据会再丢一次。5.4 现象所有相似度都集中在 0.9 以上区分度极差原因池化时把 padding 部分的向量也平均进去了。BERT 对统一长度的输入不足 512 的位置会被 padding token 填满在平均池化时如果直接last_hidden_state.mean(dim1)padding 位置贡献的向量值非常小会把所有句子都往坐标系原点方向拉从而造成相似度虚高。解决按 attention_mask 做加权平均只有真实 token 才参与求和。具体代码在 4.2 节已经给出核心是masked token_vecs * attention_mask.unsqueeze(-1)把 padding 位置的向量乘 0再除以真实 token 数量。这一个小小的改动通常能让相似度分布从 0.9 以上拉开到 0.5 到 0.95 的合理区间。5.5 现象模型加载时间太长前端请求一直超时原因后端代码在请求处理函数里每次都调用BertModel.from_pretrained等于每条文本重新加载一次 400 MB 的权重。即便服务器内存够也扛不住并发何况 Flask 开发服务器本身是单线程。解决把模型加载放到模块加载时做一次比如在 Flask 的全局变量里初始化类的实例embedder BertEmbedder() # 模块顶层只加载一次 app.route(/api/similarity, methods[POST]) def similarity(): # 直接用全局 embedder不要在这里重新加载 ...另一个坑是 Flask 开启 debug 模式时会启动两个进程两个进程都会执行模块加载导致内存翻倍。如果你只求演示稳定直接app.run(debugFalse, host0.0.0.0, port5000)前端跨域通过CORS(app)解决不需要 debug 模式。6. 一个进阶技巧用自建测试集验证阈值而不是拍脑袋定 0.8阈值设置不能靠猜我一般会写一个回测脚本来选。先准备 100 对正样本语义相似和 100 对负样本语义不相似放在一个 JSON 文件里然后循环测试不同阈值的准确率。一个小技巧是借用分类任务里的 F1 指标对每一个候选阈值计算精确率和召回率取 F1 最高点作为线上默认值。下面是回测的核心代码import json with open(test_pairs.json, r, encodingutf8) as f: pairs json.load(f) best_threshold 0.8 best_f1 0.0 for threshold in [i / 100 for i in range(60, 96, 5)]: tp fp fn tn 0 for p in pairs: sim cosine_sim(embedder.encode(p[a]), embedder.encode(p[b])) pred sim threshold if p[label] 1 and pred: tp 1 elif p[label] 0 and pred: fp 1 elif p[label] 1 and not pred: fn 1 else: tn 1 precision tp / (tp fp 1e-9) recall tp / (tp fn 1e-9) f1 2 * precision * recall / (precision recall 1e-9) if f1 best_f1: best_f1 f1 best_threshold threshold print(fbest threshold: {best_threshold}, F1: {best_f1:.4f})这个脚本里的1e-9是平滑项防止分母为零。跑完你会得到一个比 0.8 更贴合你数据集的阈值把它写进后端配置系统判断结果立刻有了依据。如果你的测试集里负样本特别少F1 会虚高建议每个类别至少 50 条。我自己做类似项目时还有一个习惯在说明文档里专门加一页“如何复现实验结果”把回测脚本和数据集的格式写清楚。这样答辩时老师不管问“阈值为什么是 0.82”还是“准确率怎么统计的”都能立刻翻到对应章节讲明白。这种细节比堆模型复杂度更能体现工程能力希望帮到你。本文还有配套的精品资源点击获取