
简介面向毕业设计与自然语言处理实践这套基于Python的BERT文本相似度检测系统资源适合需要完成深度学习相关课题的学生直接运行、改造与学习。资源围绕“源码数据库”组织覆盖数据预处理、BERT模型微调、文本向量化、余弦相似度计算以及后端接口封装等关键环节能帮助读者从零理解相似度检测系统的搭建与运行流程。压缩包约52.54MB内部包含程序、数据库与说明文档等模块便于学习者对照代码、数据库结构和说明文档逐步复现项目。目前已有586人学习。借助这套资源读者不仅能掌握BERT在语义匹配项目中的应用思路还可以练习Python编程、深度学习框架使用、数据库表结构设计与Web服务部署等综合技能同时深入理解分词、去停用词、标准化等文本预处理细节对毕业设计答辩和NLP入门均有实用价值。1. 让毕业设计有辨识度的切入点BERT文本相似度检测系统的需求拆解论文查重、智能客服去重、舆情监控中的事件合并核心都在回答同一个问题两段文本说的是不是同一件事。传统做法用 TF-IDF 算余弦相似度但文本一旦经过同义改写、语序调整、被动句换主动句关键词全变了得分立刻失真。基于深度学习的 BERT 模型把文本编码成语义向量再做比对能够覆盖这类改写场景这几年「Python BERT 文本相似度」也顺势成了毕业设计里的高频选题。这篇文章沿着「Python BERT 数据库」这条主线把系统落地的完整链路拆开讲环境怎么装、模型怎么加载、相似度分数怎么算、结果怎么存进数据库、阈值怎么定才不会被答辩老师追问到哑口。默认你已经有基础的 Python 语法能力但对 transformers、PyTorch 和向量化计算还比较陌生。文中的命令和代码可以直接复制到自己的项目里跑通再按你的数据集替换即可。2. BERT为什么能打Transformer编码器与文本相似度的计算链路2.1 从TF-IDF到BERT相似度计算的三代方案先解决一个面试也常问的问题为什么不用 TF-IDF 或者 Word2VecTF-IDF 把文本变成词频向量查询和去重场景下如果用户把人工智能换成了AI词表对不上向量直接偏移余弦相似度断崖式下跌。Word2Vec 解决了词的语义相似但要得到句子向量通常得把词向量平均句子一长关键信息就被高频词冲淡了。BERT 走的是另一条路每个 token 经过多层双向注意力编码后向量携带的是整个句子的上下文信息再做池化得到句子级向量相似度基于语义而非字符重合。下面这张表在答辩 PPT 里可以直接用三种方案的核心差异一目了然方案建模粒度上下文感知同义改写鲁棒性工程成本TF-IDF词频无差极低Word2Vec 平均词向量弱中低BERT整句 上下文强好中高2.2 CLS向量与PoolingBERT输出怎么变成句子向量BERT 的输入经过 12 层 Transformer 编码后输出形状是[batch_size, seq_len, hidden_size]中文 base 模型的 hidden_size 为 768。这个三维矩阵不能直接做余弦相似度需要先压缩成一个向量。压缩方式直接决定相似度质量。第一种是取[CLS]位置的向量。BERT 的第一个 token 是[CLS]它的最后一层输出理论上汇总了全序列信息直接取出来就能当句子向量用。但实际测试里未微调的 CLS 向量在同义改写场景下稳定性不如均值池化。第二种是 mean pooling把最后一层所有 token 向量按 attention mask 加权平均。我一般用这种实现如下def get_sentence_embedding(text, tokenizer, model, max_len128): inputs tokenizer( text, max_lengthmax_len, truncationTrue, paddingTrue, return_tensorspt ) with torch.no_grad(): outputs model(**inputs) # last_hidden_state 形状: (1, seq_len, 768) last_hidden outputs.last_hidden_state # attention_mask 中有效 token 为 1padding 为 0 mask inputs[attention_mask].unsqueeze(-1) # 将 padding 位置的向量清零后按有效 token 数做平均 masked last_hidden * mask emb masked.sum(dim1) / mask.sum(dim1) return emb.squeeze()逻辑说明最后一层输出的每个 token 向量里padding 位置的值是无意义的直接用sum会导致短句子向量凭空变小。乘以 attention_mask 把 padding 位清零再按有效 token 数做平均这样不同长度句子的向量在量纲上是一致的。参数说明max_len128表示长于 128 的部分直接截断短于的补 padding。对短文本相似度场景 128 足够超过 256 后显存占用会明显上涨速度和收益不成正比。2.3 两种相似度口径向量比对与句子对分类拿到句子向量后的落地路线有两种。方案一直接算余弦相似度cos(v1, v2) (v1 · v2) / (|v1| × |v2|)输出范围在 [-1, 1]实际使用中取 0 到 1 这一段做归一化展示。方案二把(text_a, text_b)拼成一个句子对输入 BERT后面接一个全连接分类头输出是否相似的概率。方案二精度上限更高但需要成对的标注数据做微调。毕业设计没有现成标注语料时先用方案一做基线系统把全链路跑通如果导师要求必须有训练环节再对方案二做微调。这两条路线答辩时的讲述逻辑完全不同方案一讲的是预训练语义表征 无监督相似度计算方案二讲的是句子对分类 下游任务微调。不管选哪条余弦相似度、欧氏距离这些度量口径要提前理清别在答辩时被问住。3. Python环境搭建与最小Demo加载BERT模型跑通第一对文本3.1 环境与依赖Python版本、PyTorch与transformers这套代码最常见的运行环境是 Python 3.10 PyTorch 2.x transformers 4.x。Windows 和 Linux 都能跑唯一区别是 PyTorch 装 CPU 版还是 GPU 版。没有独显就装 CPU 版bert-base-chinese 推理一段短文本大约几百毫秒演示完全够用。# 建议先创建虚拟环境避免污染系统 Python python -m venv bert_sim_env source bert_sim_env/bin/activate # Windows 下是 bert_sim_env\Scripts\activate pip install transformers torch accelerate pip install flask pymysqltransformers 负责模型下载与加载torch 负责张量运算accelerate 用来做 GPU 显存管理flask 和 pymysql 是后面接口层和数据库层要用的。第一次加载模型会从 Hugging Face 下载 bert-base-chinese 权重约 400MB耐心等。国内网络下载慢的话可以先设置export HF_ENDPOINThttps://hf-mirror.com再执行代码走镜像源。3.2 最小相似度检测函数从加载模型到输出分数完整的最小 Demo 代码如下from transformers import AutoTokenizer, AutoModel import torch tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModel.from_pretrained(bert-base-chinese) model.eval() def embed(text, max_len128): inputs tokenizer( text, max_lengthmax_len, truncationTrue, paddingTrue, return_tensorspt ) with torch.no_grad(): outputs model(**inputs) mask inputs[attention_mask].unsqueeze(-1) emb (outputs.last_hidden_state * mask).sum(dim1) / mask.sum(dim1) return emb.squeeze() def similarity(text_a, text_b): vec_a embed(text_a) vec_b embed(text_b) return torch.cosine_similarity( vec_a.unsqueeze(0), vec_b.unsqueeze(0) ).item()跑一下看看输出print(similarity(我喜欢编程, 我热爱写代码)) print(similarity(我喜欢编程, 今天天气很好))第一对的分数通常会明显高于第二对说明模型确实捕捉到了语义层面的相似。只要第一对大于第二对链路就是通的后面的数据库和接口工作都可以在本节基础上叠加。3.3 刚装完环境最容易踩的三个坑第一个坑是中文模型选错。bert-base-cased是英文模型直接跑中文输出大量[UNK]相似度结果完全失真。中文一定用bert-base-chinese或者哈工大开源的hfl/chinese-bert-wwm-ext后者在中文语义任务上通常更稳。第二个坑是忘记model.eval()。模型默认处于训练模式Dropout 层会随机丢弃节点导致同样的输入两次输出的向量不同相似度分数忽高忽低。推理前务必调用model.eval()把模型切到评估模式。第三个坑是 tokenizer 的 padding 报 warning。如果没有设置paddingTruebatch 内不同长度的样本会报长度不一致错误。上面代码里paddingTrue配合return_tensorspt是最稳妥的写法transformers 会自动按 batch 内最长序列对齐。3.4 训练还是微调毕业设计做到哪一步算完整如果导师希望系统里有训练环节可以在句子对数据集上做微调。数据格式是 CSV 三列text_a, text_b, label0 表示不相似1 表示相似。1000 条左右的数据量足够微调一个小模型比如hfl/chinese-bert-wwm-ext普通 CPU 也能在半小时内跑完一个 epoch。注意微调后模型不再直接输出语义向量而是输出分类概率。此时相似度判断要改为取[CLS]向量接全连接层比较的是概率值而不是余弦分数阈值含义也不同。如果系统里的历史数据都是用余弦分数存的切换微调模型后分数分布会变第 5 章的评估步骤需要重跑一遍。4. 数据库设计与完整系统落地从文本对存储到查询接口4.1 MySQL表结构把相似度检测结果存成可检索的数据数据库这一层在毕业设计里经常被敷衍掉但答辩时恰恰爱从这里切入。光算出分数不落库系统就不完整。我通常建一张text_pair表把每一对文本、相似度分数、判定标签、推理耗时、模型名称都存下来后续做统计分析都有据可查。CREATE DATABASE IF NOT EXISTS text_sim_db DEFAULT CHARACTER SET utf8mb4; USE text_sim_db; CREATE TABLE text_pair ( id INT AUTO_INCREMENT PRIMARY KEY, text_a TEXT NOT NULL, text_b TEXT NOT NULL, similarity_score FLOAT NOT NULL, is_similar TINYINT NOT NULL DEFAULT 0, model_name VARCHAR(64) NOT NULL DEFAULT bert-base-chinese, latency_ms INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_score (similarity_score), KEY idx_similar (is_similar), KEY idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明text_a、text_b存原文similarity_score存余弦相似度分数is_similar由应用层根据阈值写成 0 或 1便于后续统计相似率latency_ms存单次推理耗时答辩性能曲线就直接从这张表取数。字符集用 utf8mb4避免生僻字和 emoji 写入时报编码错误。阈值不建议写死在 SQL 里否则每次调阈值都要改表结构或改脚本。正确做法是应用层算好分数、判定好标签再写入SQL 只做存储。4.2 Python连接MySQLpymysql的写入与连接复用Python 操作 MySQL 最直接的方式是 pymysql。先建立连接再执行插入import pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasetext_sim_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def insert_result(text_a, text_b, score, is_similar, latency_ms): with conn.cursor() as cursor: sql INSERT INTO text_pair (text_a, text_b, similarity_score, is_similar, latency_ms) VALUES (%s, %s, %s, %s, %s) cursor.execute(sql, (text_a, text_b, score, is_similar, latency_ms)) conn.commit()逻辑说明参数化 SQL 能防止拼接字符串带来的注入问题cursor.execute只负责执行真实的写入由conn.commit()提交到数据库。如果不调 commit数据只是停留在事务里进程退出后丢失。注意点上面这种单连接写法的前提是低并发演示。如果答辩现场连续请求几十次MySQL 的wait_timeout默认 8 小时不会断但max_allowed_packet太小会在插入长文本时报错。稳妥做法是使用dbutils.pooled_db.PooledDB做连接池或者至少保证每次操作完成后在finally里close()。4.3 Flask接口把相似度检测封装成可调用的API有了数据库和模型层用 Flask 把它们串起来系统就从脚本变成了应用from flask import Flask, request, jsonify app Flask(__name__) app.post(/api/similarity) def api_similarity(): payload request.get_json(forceTrue) text_a payload.get(text_a, ).strip() text_b payload.get(text_b, ).strip() if not text_a or not text_b: return jsonify({error: text_a and text_b are required}), 400 score similarity(text_a, text_b) is_similar 1 if score 0.75 else 0 insert_result(text_a, text_b, score, is_similar, 0) return jsonify({score: round(score, 4), is_similar: is_similar}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)调用示例curl -X POST http://127.0.0.1:5000/api/similarity \ -H Content-Type: application/json \ -d {text_a:我喜欢编程,text_b:我热爱写代码}返回结果是 JSON演示时用 Postman 或在浏览器里访问都直观。注意debugFalseFlask 调试模式会启动 reloader导致模型被加载两次启动时间直接翻倍答辩现场很容易因为这个露怯。4.4 批量比对从一对一接口到一对多检索接口版适合演示单次检测但问得深一点的老师会问如果库里有 1 万条文本新来一条怎么找最相似的最直接的做法是遍历每条历史文本分别算相似度但 1 万条文本在 CPU 上可能要几十秒体验很差。改进方案是离线阶段把库中所有文本的向量算好存成一张text_embedding表在线阶段把新文本向量和库里向量做矩阵点积一次算出所有分数再用 NumPy 排序取 Top-Nimport numpy as np # vectors 是从数据库读出的历史向量矩阵形状 (N, 768) def batch_search(query_text, vectors, top_k5): query_vec embed(query_text).numpy() scores np.dot(vectors, query_vec) / ( np.linalg.norm(vectors, axis1) * np.linalg.norm(query_vec) ) top_idx np.argsort(scores)[::-1][:top_k] return [(int(i), float(scores[i])) for i in top_idx]逻辑说明矩阵乘法np.dot等价于对 N 条向量逐一计算点积比 Python 循环快一个数量级。除以范数后得到的就是余弦相似度。argsort返回从小到大的索引加[::-1]反转成从大到小再截取前 5 个就是最相似的记录。这个方法在几百到几万条的规模下都够用也不必给毕业设计引入分布式向量数据库。5. 三个必调参数与验证手法阈值、max_len与F1评估核心系统跑通之后答辩时最容易被追问的就是这个分数为什么这么定。下面是三个每次都要调的参数和对应的验证方法。5.1 相似度阈值0.75 不是标准答案每个数据集的分布都不一样。正确做法是准备 100 条已标注的正负样本全部跑一遍相似度统计正样本和负样本的分数区间。一般会看到负样本集中在 0.5~0.7正样本集中在 0.8 以上中间空隙处就是阈值候选位置。选择使 F1 最大的点比拍脑袋定 0.75 更能说服人。5.2 max_lenTransformer 的注意力复杂度是 O(n²)max_len从 128 调到 256显存占用不是翻倍而是接近四倍。短文本场景维持在 128 就好只有文本普遍超过 200 字时才需要 256。另外模型加载约占用 1GB 内存答辩前不要现场调整max_len或切换模型第一个请求会显著变慢。5.3 F1验证脚本把验证脚本直接放到项目根目录跑完打印指标这个细节在答辩时很加分# 格式: (text_a, text_b, true_label) pairs [ (我喜欢编程, 我热爱写代码, 1), (我喜欢编程, 今天天气很好, 0), # ... 继续补充标注样本 ] scores [similarity(a, b) for a, b, _ in pairs] pred [1 if s threshold else 0 for s in scores] true [label for _, _, label in pairs] tp sum(1 for p, t in zip(pred, true) if p 1 and t 1) fp sum(1 for p, t in zip(pred, true) if p 1 and t 0) fn sum(1 for p, t in zip(pred, true) if p 0 and t 1) precision tp / (tp fp) recall tp / (tp fn) f1 2 * precision * recall / (precision recall) print(fthreshold{threshold:.2f} precision{precision:.2f} recall{recall:.2f} f1{f1:.2f})展示时把接口返回结果按分数降序排列先给老师看分数最高的几对。比如我喜欢编程和我热爱写代码得分 0.86而两段不含任何相同关键词的改写句同样得分很高这就直观说明了 BERT 语义编码比关键词匹配更合理。本文还有配套的精品资源点击获取