ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

BERT文本相似度检测系统:毕设级实现与踩坑记录

BERT文本相似度检测系统:毕设级实现与踩坑记录 简介这套基于BERT的深度学习文本相似度检测系统设计源码适合作为计算机相关专业毕业设计、课程设计的完整参考项目也方便NLP初学者理解从数据到模型部署的流程。系统使用Python 3.6.8搭建后端借助BERT预训练模型完成语义特征提取与相似度计算并配套MySQL 5.7数据库及Navicat管理工具整体包含前端界面、后端接口、数据库脚本与说明文档。资源包共389个文件、52.19MB以Python源码py/pyc为核心辅以JavaScript/CSS/HTML前端资源、gif/png演示素材以及docx/pdf/txt说明文档SQL文件保存了建库建表数据目录按源码、数据库、成品归档结构清晰。目前已有78人学习浏览。通过这套资源既能直接运行体验完整的前后端联动效果也可参考项目架构、接口设计和文档撰写毕业设计对深度学习与Web系统结合的场景有较强的实践指导意义。1. BERT文本相似度检测毕设级完整系统到底能给你什么如果你拿到这套「深度学习文本相似度检测系统」的毕设源码第一反应多半是找 README、跑 demo、看前端页面长什么样。但更值得先弄清楚的问题不是“代码能不能跑”而是这个系统凭什么用 BERTMySQL 在里边是不是可有可无。答案是BERT 负责语义相似度的实时计算MySQL 负责把每一次比对的历史记录落库前端负责把模型能力包装成可操作的页面三者的取舍直接决定你答辩时能不能把话说圆。这个方向适合两类人一类是 NLP 毕业设计选了文本相似度、需要完整可复现系统的人另一类是求职想往 NLP 工程方向靠、需要一个能讲清前后端联调的项目的人。整套源码里附带前后端、MySQL 建库脚本、说明文档和 LW论文文档但拿到手先别急着逐行读代码。我习惯先把系统拆成三个问题模型怎么算相似度数据库存什么前端调什么接口。这三件事想清楚了后面改阈值、换模型、接真实数据都是顺水推舟的事。2. 系统拆解BERT为什么能算相似度MySQL和前后端各管什么事2.1 BERT算相似度的两种结构CLS向量与双塔编码BERT 本身不是为文本相似度设计的它训练时用的是掩码语言模型和下一句预测。毕业设计里把它拿来做相似度检测最常见做法是直接取模型输出的一个固定维度向量来代表整句话然后算两个向量之间的余弦相似度。这就会牵出一个关键选择用哪个向量代表句子。第一种是直接取每个句子送入 BERT 后 [CLS] 位置最后一层的输出。BERT 的 [CLS] 在设计之初就承担了聚合整句语义信息的角色所以很多教材里默认“拿 CLS 就行”。但实际跑过就会知道未经过相似度任务微调的 BERT其 CLS 向量里包含的语义信息并不稳定直接算余弦相似度会出现大量“看起来该相似却不相似”的情况。第二种是双塔结构也叫 Siamese 结构两个句子分别过同一个 BERT 模型两个输出向量再进到一个距离计算层。这种方式在 Sentence-BERT 里被验证过效果好很多代价是训练时要构造句子对数据。毕设系统一般都采用第一种“CLS 或池化向量 余弦相似度”的路径因为它不需要训练数据也能出结果改造成本低。这里有一个常被忽略的参数池化方式。我自己调系统时发现mean pooling 通常比直接取 CLS 更稳。原因是 CLS 位置只占一个 token 的表示而 mean pooling 会把整句话所有 token 的向量做平均受特殊符号影响小。代码里实现时就是拿 attention_mask 过滤掉 padding 位再对有效 token 向量求均值。2.2 前后端与MySQL的职责边界模型不做持久化库只存记录这个系统的架构层级并不复杂但职责边界必须清楚。文本相似度推理本身是无状态的输入两个句子BERT 前向传播一次输出一个 0 到 1 之间的分数这个过程不需要查数据库。所以把 BERT 推理逻辑放在后端服务里提供 HTTP 接口给前端调用这是最顺的组装方式。MySQL 在这里不是用来存模型参数的。BERT 的权重文件通常是几百 MB 的本地文件不应该也不适合放进数据库。MySQL 存的是业务数据哪两个句子在什么时间被比对过、分数是多少、当时用的阈值是什么、用户是谁。这些数据是答辩时最值钱的东西因为可以导出统计结果比如“本系统共处理了 1500 次比对请求平均响应时间 800ms”。另外一个容易踩的认知误区是把 BERT 推理结果缓存到数据库里。常见做法是每次请求都实时推理除非你有明确的句子对重复出现场景。毕设系统规模不大实时推理完全可以承受别为了“优化”把缓存逻辑做复杂。前端在这个系统里的任务是提供两个输入框、一个提交按钮、一个结果展示区域。它通过 AJAX 请求把两个句子 POST 给后端后端返回 score、label、耗时等字段前端把结果渲染出来。整个链路里数据和逻辑的边界就是“模型在后端记录在 MySQL展示在前端”。2.3 为什么不用TF-IDF和Word2Vec语义相似度的分水岭如果你在答辩时被问“为什么要用深度学习而不是传统方法”这个问题必须在选型阶段就准备好。TF-IDF 配合余弦相似度只能衡量字面重合程度它会把文本拆成词后按词频加权两个句子只要用词不同哪怕语义完全相同得分也会很低。举个实际例子“这个价格真便宜”和“这个价位很实惠”在语义上是同一个意思但 TF-IDF 的向量空间里除了“这个”以外没有任何一个词是重合的算出来相似度可能只有 0.2 不到。Word2Vec 能解决一部分问题因为它把词映射成了向量可以用来算词之间的相似度但它是静态的一个词只有一个向量无法根据上下文动态调整。多义词“苹果”在“苹果很好吃”和“苹果发布新手机”里应该指向不同语义Word2Vec 做不到。BERT 是上下文相关的同一个词在不同句子里得到的向量不一样。这是它和 Word2Vec 最本质的区别也是一个很好的答辩切入点。所以 BERT 在相似度任务上的优势不是“深度学习四个字听起来高级”而是它能理解语义层面的相似而不是停留在字符层面的匹配。3. 从零跑通Python环境、MySQL建表和BERT权重准备3.1 先把环境钉死Python版本、依赖清单与安装命令很多时候代码跑不起来不是因为代码有问题而是环境版本太新。BERT 相关的依赖对 Python 版本有隐性要求尤其是 torch 在较新版本上的编译支持。我一般会把 Python 环境锁定在 3.8 到 3.10 之间不要一上来就装最新的 3.12。python 安装教程里总劝你勾选 Add Python to PATH这一步做对了后面能少很多事。推荐用虚拟环境隔离项目避免污染系统 Python。下面是一套可以直接执行的安装命令python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install --upgrade pip pip install torch --index-url https://download.pytorch.org/whl/cpu pip install transformers4.30.2 pip install flask pymysql numpy这里把 torch 单独安装并且指定了 CPU 版本。很多人安装 numpy 时会遇到版本冲突常见原因是系统中已经存在一个旧版 numpy而 transformers 对它有过高要求。建议在虚拟环境里全新安装不要用手动下载的包去覆盖。transformers 指定 4.30.2 是稳妥选择新版 API 变动较大很多旧教程代码在新版下会报错。安装完后建议运行一次 python -c import torch, transformers; print(torch.version, transformers.version) 确认版本。这一步能排查 80% 的导入失败问题。3.2 数据库设计比对记录表结构与MySQL配置要点MySQL 在这个系统里的核心表只有一张叫相似度比对记录表。表设计不需要复杂但字段类型、字符集、索引这几个点要一次到位否则后面写进中文数据就会遇到乱码或者排序问题。CREATE DATABASE sim_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE sim_system; CREATE TABLE t_similarity_record ( id INT AUTO_INCREMENT PRIMARY KEY, text_a VARCHAR(500) NOT NULL COMMENT 待比对文本A, text_b VARCHAR(500) NOT NULL COMMENT 待比对文本B, sim_score DECIMAL(5,4) NOT NULL COMMENT BERT计算出的余弦相似度, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 比对时间, KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文本相似度比对记录表;这里有两个细节值得说明。第一字符集必须用 utf8mb4而不是 utf8因为 MySQL 的 utf8 实际只支持最多 3 字节的字符遇到生僻字或 emoji 会报错或乱码。第二sim_score 用 DECIMAL(5,4)表示最多 5 位数字、小数占 4 位可以存 0.0000 到 9.9999余弦相似度的范围在 -1 到 1 之间完全够用。MySQL 配置方面最常见的坑是安装完成后 root 密码没记住、或者服务启动失败。mysql 5.7 与 8.0 在安装流程上差异较大如果照着网上的 mysql 安装教程操作先确认教程对应的版本是否和你本地一致。数据库启动后用 mysql -u root -p 登录执行上面的 SQL 即可。3.3 加载BERT前的自检脚本模型、分词器、设备一次确认transformers 库首次加载模型时会从 Hugging Face 下载权重文件如果网络不稳定或者模型名称写错会卡在下载阶段很久。稳妥做法是先把模型下载到本地目录然后用 local_files_only 参数强制从本地加载。这样后面每次启动都不会有网络依赖。from transformers import AutoTokenizer, AutoModel model_dir ./bert-base-chinese-local # 首次运行去掉 local_files_only让它自动下载到 model_dir tokenizer AutoTokenizer.from_pretrained(model_dir, local_files_onlyTrue) model AutoModel.from_pretrained(model_dir, local_files_onlyTrue) model.eval() print(f模型已加载参数量约 {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M)参数说明local_files_onlyTrue 表示只读取本地文件不发起任何网络请求如果目录为空会直接报错这样能立刻发现问题而不是卡住。model.eval() 把模型切换到推理模式它会关闭 dropout 等训练专用行为这一点很多人会忘记导致同样的输入两次推理结果不一致看着像玄学其实就是没切 eval 模式。这段脚本跑通后BERT 这块的黑匣子就算打开了。设备方面CPU 也能跑只是单次推理慢一些如果机器有 NVIDIA 显卡可以加一行 model.to(cuda)但要注意把输入张量也移到同一设备。4. 把检测做成接口后端推理、前端页面与MySQL落库的完整链路4.1 模型推理封装向量计算与余弦相似度的参数设置在写接口之前先把推理逻辑封装成一个独立函数输入两个字符串输出一个浮点数。这一步做得好后面接 Flask 接口就只是加一层网络协议的问题。import torch import torch.nn.functional as F from transformers import AutoTokenizer, AutoModel model_dir ./bert-base-chinese-local tokenizer AutoTokenizer.from_pretrained(model_dir, local_files_onlyTrue) model AutoModel.from_pretrained(model_dir, local_files_onlyTrue) model.eval() def encode_text(text: str): encoded tokenizer( text, max_length128, paddingmax_length, truncationTrue, return_tensorspt ) return encoded def mean_pooling(token_embeddings, attention_mask): input_mask_expanded attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings torch.sum(token_embeddings * input_mask_expanded, dim1) sum_mask torch.clamp(input_mask_expanded.sum(dim1), min1e-9) return sum_embeddings / sum_mask def compute_similarity(text_a: str, text_b: str) - float: with torch.no_grad(): enc_a encode_text(text_a) enc_b encode_text(text_b) out_a model(**enc_a) out_b model(**enc_b) emb_a mean_pooling(out_a.last_hidden_state, enc_a[attention_mask]) emb_b mean_pooling(out_b.last_hidden_state, enc_b[attention_mask]) cos_score F.cosine_similarity(emb_a, emb_b, dim-1) return round(float(cos_score[0]), 4)这段代码有几个参数是实践出来的不是默认值就能用。max_length128 对短文本比对足够超过 128 的部分会被截断如果做长文档比对可以调到 256但推理时间几乎翻倍。paddingmax_length 会把所有句子都补到 128 个 token好处是 batch 内形状统一坏处是短句子浪费计算。truncationTrue 必须和 max_length 同时使用否则超长文本会报错。emb_a 和 emb_b 的 shape 都是 (1, 768)768 是 BERT-base 的隐藏层维度。mean_pooling 利用 attention_mask 屏蔽 padding 位避免无效 token 拉低向量的平均语义这是比直接取 CLS 更稳的池化方法。4.2 接口与落库POST请求里同时完成打分和记录推理函数写好后用 Flask 包一层 HTTP 接口。模型加载必须放在模块级别也就是只加载一次。如果写进请求处理函数里每个请求都会重新读权重文件响应时间会从几百毫秒恶化到几十秒。from flask import Flask, request, jsonify import pymysql app Flask(__name__) def save_record(text_a: str, text_b: str, score: float): conn pymysql.connect( hostlocalhost, userroot, password123456, databasesim_system, charsetutf8mb4 ) try: with conn.cursor() as cursor: sql INSERT INTO t_similarity_record (text_a, text_b, sim_score) VALUES (%s, %s, %s) cursor.execute(sql, (text_a, text_b, score)) conn.commit() finally: conn.close() app.route(/api/similarity, methods[POST]) def similarity(): data request.get_json(forceTrue) text_a data.get(text_a, ).strip() text_b data.get(text_b, ).strip() if not text_a or not text_b: return jsonify({error: text_a and text_b cannot be empty}), 400 score compute_similarity(text_a, text_b) threshold float(data.get(threshold, 0.7)) label 相似 if score threshold else 不相似 save_record(text_a, text_b, score) return jsonify({ text_a: text_a, text_b: text_b, score: score, label: label, threshold: threshold }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)request.get_json(forceTrue) 会强制把请求体按 JSON 解析即使请求头没写 Content-Type。这么做是为了兼容前端的 fetch 调用。threshold 允许前端传入这比写死在代码里更灵活方便后面做阈值对比实验。save_record 每次请求都新建连接在毕设量级下没有问题没必要引入连接池。注意 pymysql.connect 里的 charsetutf8mb4 必须和建表时的字符集一致否则中文文本写进库后可能仍是乱码。conn.commit() 在 execute 之后调用忘记提交会导致数据查不到这个问题经常被漏掉。启动后可以先在命令行里用 curl 验证接口curl -X POST http://127.0.0.1:5000/api/similarity \ -H Content-Type: application/json \ -d {text_a:这个价格真便宜,text_b:这个价位很实惠}返回的 JSON 里包含 score、label、threshold 三个字段前端直接拿这些字段渲染即可。4.3 前端联调两个输入框、一个按钮、三分区展示结果前端尽量用纯 HTML 加原生 JavaScript不引框架这样项目在任意浏览器打开都能跑教师机、机房电脑都能演示避免答辩现场因为没装依赖翻车。!DOCTYPE html html langzh-CN head meta charsetutf-8 title文本相似度检测系统/title /head body div textarea idtext_a placeholder输入第一段文本/textarea textarea idtext_b placeholder输入第二段文本/textarea button idbtn_submit开始比对/button div idresult/div /div script document.getElementById(btn_submit).addEventListener(click, async () { const textA document.getElementById(text_a).value.trim(); const textB document.getElementById(text_b).value.trim(); if (!textA || !textB) { document.getElementById(result).innerText 两个文本框都要填写; return; } const resp await fetch(http://127.0.0.1:5000/api/similarity, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ text_a: textA, text_b: textB }) }); const data await resp.json(); document.getElementById(result).innerText 相似度得分 data.score 判定结果 data.label; }); /script /body /htmlfetch 请求默认是异步的用了 async/await 后代码更接近顺序执行。这里的 URL 写死为 127.0.0.1:5000适用于前后端都在本机开发的情况。如果前端页面和后端不在同一个域名下浏览器会触发跨域限制此时最简单做法是在 Flask 里加 CORS 响应头不是改前端。text_a 和 text_b 在发送前做 trim 去首尾空格避免用户手滑多打一个空格导致相似度无意义地下降。这个细节看起来小实际演示时很关键一段文本末尾多个空格得分可能从 0.85 掉到 0.8。5. 踩坑记录BERT部署和毕设答辩前最容易翻车的五个点5.1 首次推理卡到怀疑人生速度与内存的双重失控现象接口首次调用时等了十几秒才返回第二次也并没有快多少。用 CPU 跑一段 100 字的文本单次推理耗时超过 3 秒。原因很多人把 max_length 设成了 512并且没有开 torch.no_grad()。BERT 的 self-attention 计算复杂度是 O(n²)序列长度从 128 涨到 512耗时不是翻 4 倍而是接近 16 倍。另一个常见原因是模型每次请求都重新加载了。解决把 max_length 压到 64 或 128推理代码包在 with torch.no_grad() 里关闭梯度计算模型加载移到模块顶层只初始化一次。做完这三步CPU 上单次推理通常能压到 1 秒以内。5.2 中文句子被割成乱码分词器与预训练权重不配套现象英文文本相似度正常中文文本算出来的分数完全不对甚至分词后出现 [UNK]。原因使用了 bert-base-uncased 这个英文模型来处理中文。英文分词器压根不认识中文字符也没有对应的词表所有汉字都被映射成未知字符。解决无条件选择 bert-base-chinese或者改用 chinese-roberta-wwm-ext 之类的中文预训练模型。这里的血泪经验是不要在代码里把模型名称写成缩写比如写 bert-chinese 会直接报错模型名必须和 Hugging Face 上的完整名称一致。模型加载后也可以用 tokenizer.tokenize(你好) 快速验证如果输出成了 [UNK]赶紧换分词器。5.3 相似度普遍0.99输出层与池化方式选错了现象任意两段文本哪怕完全不相关算出的相似度都在 0.98 以上系统失去区分度。原因直接把 model 的 pooler_output 用来相似度计算。BERT 在预训练时会优化 Next Sentence Prediction 任务pooler_output 是专门为二分类设计的一层变换它输出的向量会被压缩到一个高相似度的空间里。另一个常见失误是没做归一化直接用原始向量点积当相似度。解决改用 last_hidden_state 配合 mean_pooling也就是第 4.1 节里的写法。验证方法很简单拿“今天天气很好”和“今天天气很糟糕”去测如果得分还是 0.99就说明向量提取路径有问题。正常模型这两句话的相似度应该在 0.8 到 0.9 之间。5.4 页面中文乱码MySQL编码与Flask响应头没对齐现象前端页面显示中文正常但存入 MySQL 后通过命令行查询变成问号或者 Flask 返回的 JSON 中中文显示成 \uXXXX。原因第一类是因为 MySQL 连接时没有指定 charsetutf8mb4第二类是因为 Flask 的 jsonify 默认使用 ensure_asciiTrue把中文转成了 ASCII 转义序列。解决pymysql.connect 中显式写 charsetutf8mb4如果想让返回结果直接显示中文可以在 Flask 配置里设置 app.config[JSON_AS_ASCII] False。建表时 DEFAULT CHARSET 也必须是 utf8mb4三个地方保持一致乱码才能根治。5.5 显存不够也想跑CPU推理的降级方案现象在 8GB 显存的显卡上加载模型推理几个请求后报 CUDA out of memory。原因模型默认以 float32 精度加载BERT-base 参数量约 110M仅权重就占 440MB加上中间激活值和 attention 矩阵长文本下显存占用快速膨胀。解决一是把 batch_size 降为 1二是改用半精度推理。这相当于裁剪模型精度算力允许时可以用。实际验证时发现float16 推理的相似度结果和 float32 的差异通常只有千分之一不影响判定结果。如果机器连 CPU 都吃力就把项目定位成“可演示的完整系统”答辩时重点讲架构设计而不是压榨性能。6. 把作品做成能答辩的项目阈值标定与微调实验设计6.1 用LCQMC思路造自己的微调数据集原始 BERT 直接算余弦相似度效果是“能区分完全不相关和高度相似”但对“有点相关但不同义”的句子判断不稳定。想要在答辩时有亮点可以做一个轻量级微调构造正负样本对把文本相似度转成二分类任务。from transformers import BertForSequenceClassification, Trainer, TrainingArguments model_cls BertForSequenceClassification.from_pretrained( ./bert-base-chinese-local, num_labels2 ) training_args TrainingArguments( output_dir./checkpoints, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size16, learning_rate2e-5, weight_decay0.01, logging_steps50, save_strategyepoch )learning_rate2e-5 是 BERT 微调的经典默认值不要改成 1e-3那会让预训练权重在几步之内被毁掉。weight_decay0.01 起到类似 L2 正则化的作用防止在小数据集上过拟合。如果训练集不到 2000 条把 num_train_epochs 降到 2并且 batch_size 调到 8。6.2 怎么向导师证明“调过参”阈值对比实验答辩最怕被问“阈值 0.7 是怎么来的”“准确率多少”。最扎实的回应是拿出一张阈值对比表证明自己做了实验而不是拍脑袋。构造 50 条“语义相似/不相似”的测试样本统计不同阈值下的判定结果。阈值准确率召回率F10.50待填入待填入待填入0.60待填入待填入待填入0.70待填入待填入待填入0.80待填入待填入待填入把这张表填进 LW 的实验章节里比写一百行代码更能说明你会做效果评估。计算脚本可以用 Python 写一个简单的循环遍历测试集每一对样本经过 compute_similarity 得到分数再和阈值做比较。我做这个方向时最后悔的一件事就是把阈值写死在代码里答辩现场被问“阈值为什么是 0.7”时只能说“感觉”。后来花了半天做了这组对比实验才知道自己用的 0.7 并不是 F1 最高的点。如果你也正在做这套系统建议从第一天起就保留好每一版实验结果哪怕只是记录一个分数、一条命令、一次手动标注这些碎片在写论文时都是后悔药。希望这篇笔记能帮你少走几段弯路。本文还有配套的精品资源点击获取
返回列表