十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python + BERT:文本语义相似度检测完整实战

Python + BERT:文本语义相似度检测完整实战 简介一份基于Python与BERT深度学习的文本相似度检测系统毕业设计源码包面向计算机相关专业学生、毕业设计开发者及自然语言处理入门者。项目以BERT预训练模型为核心提供完整可运行代码与配套数据库覆盖文本预处理、模型微调、句向量抽取、余弦相似度计算以及Flask/Django接口封装的全流程能够解决文本语义匹配、查重、问答推荐等场景中的相似度衡量问题。压缩包约52.54MB包内按程序、数据库、文档等模块化整理程序部分为Python源码与执行入口数据库部分为项目的数据存储文件文档部分涵盖项目背景、需求分析、使用手册和API参考便于从理论到实践完整理解系统。已有586人学习下载适合用于毕业设计、课程设计或NLP项目练手同时可以帮助初学者快速掌握BERT在文本相似度任务中的落地流程以及数据处理、模型调优、Web服务封装等工程细节是构建完整NLP应用的参考范本。1. 当文本相似度检测告别 TF-IDFBERT 为什么能读懂“同一个意思”第一次把 BERT 接进相似度任务时最直观的差异是它不再靠字面重合度判断两句话是否相关。“预计起飞时间”和“ETD”在词袋模型里没有任何共同词但在 BERT 的语义空间里两个向量的夹角非常小。这项能力来自预训练阶段的双向上下文建模每个 token 的表征都经过整个句子的信息融合而不是单独的词向量拼接。这套 Python BERT 毕业设计项目把语义相似度检测的完整链路都放进了源码里文本对数据、微调脚本、推理入口、数据库表结构与写入逻辑都齐备适合拿来做毕业设计或课程设计二次开发。它不像传统查重工具只做字符匹配而是真正学出了“不同说法、同一意思”的判别能力。对刚接触深度学习的人而言这也是观察 BERT 微调、向量存取和接口封装如何协作的极简样本。2. BERT 语义向量与相似度度量从 CLS 到余弦距离2.1 BERT 输出层到底该怎么取向量以 bert-base-chinese 为例输入一段文本后模型输出 hidden states形状是[batch_size, seq_len, 768]。seq_len 是分词后的 token 数768 是基础版 BERT 的隐藏维。句首的[CLS]token 对应的向量常被当作整句表示[SEP]用来分隔句子对分类微调时默认就是取[CLS]位置接一个全连接层输出 logits。但做相似度不一定只能取[CLS]。句子对任务里mean pooling往往比[CLS]稳定它把每个 token 的向量求平均把句子所有词的语义都揉进去。个别实现用 max pooling句子越长max 越容易丢掉频率信息。我的习惯是分类微调用[CLS]相似度检索用 mean pooling两边在验证集上对比后选更优的那个。import torch def mean_pooling(model_output, attention_mask): token_embeddings model_output.last_hidden_state 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这个函数把 padding token 的向量乘 0避免[PAD]位置进入平均。attention_mask形状是[batch_size, seq_len]扩展后与 token 向量逐元素相乘分母只统计真实 token。直接用[CLS]的话取model_output.last_hidden_state[:, 0, :]这里的 0 是每个样本的第一个 token。2.2 余弦相似度为什么是句子对的主角向量出来后常用选择是余弦相似度、欧氏距离和点积。句子向量关心语义方向而不是绝对大小余弦只看夹角天然合适。欧氏距离对向量长度敏感写作风格差异会直接影响距离点积适合向量检索数据库因为有现成索引与近似算法但结果没有上界解释性弱一些。PyTorch 自带实现from torch.nn.functional import cosine_similarity vector_a bert_encode(预计到达时间) vector_b bert_encode(ETD) cos cosine_similarity(vector_a, vector_b, dim1).item()dim1表示按句子对两两比较返回形状[batch_size]分数落在 -1 到 1。实际文本对大多在 0 到 1 之间偶有负值不必意外。max_len 越大[PAD]占比越高某些实现下均值会被轻微拉低所以我一般把 max_len 控制在 64 或 128而不是拉满 512。2.3 向量存哪里MySQL 与向量检索的分工这个项目带了数据库不少读者第一反应是“把向量直接存数据库”。严格说MySQL 不适合做高维向量索引百万条 768 维向量做暴力查询成本非常可观。毕设数据量小我更推荐文本、标签和相似度结果放 MySQL向量若需留档写入 FAISS 索引或按 ID 存成 numpy 文件。存储对象推荐方案适用场景文本对、标签、推理记录MySQL / SQLite历史记录、管理后台向量全量索引FAISS、Milvus上万条语义检索模型权重本地目录推理服务加载Milvus 这类向量数据库在工业界常见但单机毕设引入它部署成本高于收益。先把 MySQL 的表、业务逻辑做完整后面需要扩量时把向量迁移到独立索引接口层不用改。3. Python 深度学习环境与 BERT 数据预处理3.1 从裸机到能跑 BERT 的环境配置先解决运行环境。Python 建议 3.8 到 3.10版本过早与新版 transformers 兼容性差Linux 服务器上装 Python 时注意别覆盖系统自带的 python3。用 venv 隔离项目依赖VSCode 里配置 Python 环境时把解释器指向虚拟环境路径即可。Python 版本transformers 版本备注3.84.x最稳的组合3.104.x新版依赖兼容良好3.12暂不建议部分依赖编译易报错python -m venv bert_sim_env source bert_sim_env/bin/activate # Windows 下用 bert_sim_env\Scripts\activate pip install torch transformers datasets pymysql flask需要 GPU 时PyTorch 安装命令按官网 CUDA 版本选直接pip install torch默认装 CPU 版训练会慢很多。装完验证python -c import torch; print(torch.cuda.is_available())输出 True 才说明 CUDA 可用。只有 CPU 也能跑通流程把 batch size 调到 8、max_len 调到 64。3.2 tokenizer 封装中文不需要 jieba很多参考项目会先做 jieba 分词BERT 不需要。BERT 使用 WordPiece 词表中文按字和词根切分模型自己掌握边界。额外分词反而改变输入分布微调效果容易变差。直接用官方 tokenizerfrom transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def encode_pair(text_a, text_b, max_len128): encoded tokenizer( text_a, text_b, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), token_type_ids: encoded[token_type_ids].squeeze(0) }参数逐个说。max_length128截断过长的文本长文本场景再调大paddingmax_length保证 batch 内每个样本维度一致truncationTrue配合 max_length 生效。返回的 input_ids 是 token 序号attention_mask 标记真实 tokentoken_type_ids 区分第一句与第二句。这三个字段直接作为模型输入。3.3 文本清洗与训练样本构造数据质量决定相似度上限。清洗阶段我做三件事去掉大段空白和不可见字符过滤 URL 与 HTML 标签繁体统一转简体。中文语义相似度不需要去停用词停用词在 BERT 里不是删除对象删了反而可能改变语义。真正值得花时间的是样本构造。正样本来自同义改写比如“预计到达时间”和“ETD”负样本从不同类别里随机抽保持 1:1。负样本建议加入“表面相近、意思不同”的干扰项比如“怎么查航班”和“怎么取消航班”两者只差一个词但语义方向相反模型要学的判别信息更多。整理成(text_a, text_b, label)三元组label 为 0 或 1存成 CSV 或 JSON 均可。数据准备好后按 8:2 划分训练集和验证集。验证集有两个用途一是看模型有没有过拟合二是微调结束后用来选推理阈值。DataLoader 里训练集开shuffleTrue验证集不开保证每次评估顺序一致。4. PyTorch 微调 BERT 与相似度推理实战4.1 分类还是回归模型的输出设计相似度任务有两种建模方式二分类和回归。二分类里 num_labels2模型输出“不相似/相似”两类概率回归里 num_labels1直接输出分数训练用 MSE 逼近人工标注。如果只有 0/1 标签分类更稳定有连续标注分数时回归更自然。下面按回归实现接口阶段直接返回小数分数展示直观。from transformers import BertForSequenceClassification model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels1 )num_labels1会把分类头改成单输出神经元配合回归 loss 使用。如果使用过程中发现输出分布与预期差异大再切回 num_labels2 也不复杂只需要把 loss 换成交叉熵。4.2 自定义 Dataset 与完整训练循环定义 Dataset把 tokenizer 输出与标签封装起来from torch.utils.data import Dataset import torch class TextPairDataset(Dataset): def __init__(self, pairs, labels, tokenizer, max_len128): self.pairs pairs self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.pairs) def __getitem__(self, idx): text_a, text_b self.pairs[idx] encoded self.tokenizer( text_a, text_b, max_lengthself.max_len, paddingmax_length, truncationTrue, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), label: torch.tensor(self.labels[idx], dtypetorch.float) }__getitem__返回单条样本输入形状[max_len]标签是标量DataLoader 会把一批样本堆叠成[batch_size, max_len]。这个阶段最常见的报错是样本长度不一最后一批维度对不上原因就是 tokenizer 没加paddingmax_length。训练循环骨架from torch.optim import AdamW import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels1) model.to(device) optimizer AdamW(model.parameters(), lr2e-5) loss_fn nn.MSELoss() for epoch in range(3): model.train() total_loss 0.0 for batch in train_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) outputs model(input_idsinput_ids, attention_maskattention_mask) logits outputs.logits.squeeze(1) loss loss_fn(logits, labels) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() optimizer.zero_grad() total_loss loss.item() print(fepoch {epoch 1} loss: {total_loss / len(train_loader):.4f})这里没有向分类头传 labels而是手动取 logits 计算 MSEsqueeze(1)把[batch_size, 1]压成[batch_size]与 labels 形状对齐。clip_grad_norm_把梯度范数钳到 1.0防止个别难样本把参数更新带偏BERT 微调里很关键。AdamW 与 Adam 的差别在权重衰减处理上迁移任务几乎都用 AdamW。微调超参建议先用下面的保守组合跑通后再调参数推荐范围说明batch_size8~32显存不够先减这个learning_rate2e-5~5e-5再大容易发散max_len64~128短文本 64 足够warmup_ratio0.1前 10% 步数热启动训练结束后保存模型与 tokenizer方便独立推理model.save_pretrained(./bert_sim_model) tokenizer.save_pretrained(./bert_sim_model)4.3 推理函数输入句子对返回分数推理时切到 eval 模式关闭梯度计算避免显存浪费且不受 Dropout 影响def predict_similarity(text_a, text_b, model, tokenizer, device): model.eval() encoded tokenizer( text_a, text_b, max_length128, paddingmax_length, truncationTrue, return_tensorspt ) encoded {k: v.to(device) for k, v in encoded.items()} with torch.no_grad(): logits model(**encoded).logits score torch.sigmoid(logits).item() return round(score, 4)model.eval()与torch.no_grad()各管一件事前者影响 Dropout 和层状态后者决定是否构建计算图。回归 logits 没有上界根据训练标签范围做一次 sigmoid 压到 0~1。接口单条推理用 batch size 1QPS 不高时足够。5. 数据库表设计、Flask 接口与历史记录管理5.1 表结构设计记录一次相似度检测的完整链路数据库不只有模型参数每次推理的文本、分数、判定结果都可以留档作为后续分析或答辩演示的数据。数据量不大时把文本对与检测结果合并成一张记录表查询更简单CREATE DATABASE bert_sim DEFAULT CHARACTER SET utf8mb4; USE bert_sim; CREATE TABLE similarity_record ( id INT AUTO_INCREMENT PRIMARY KEY, text_a TEXT NOT NULL, text_b TEXT NOT NULL, score FLOAT NOT NULL, label VARCHAR(10) COMMENT similar / dissimilar, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;text_a和text_b用 TEXT因为用户输入长度不可控label用字符串记录判定结果比 0/1 更直观也方便以后扩展多档标签created_at由数据库生成Python 端不用管时间。索引建在created_at上历史查询按时间倒序会很快。若未来数据量涨到几十万条文本元数据继续留在 MySQL向量抽出来放 FAISS 或 Milvus。5.2 Python 写入与查询pymysql 的基本操作连接参数集中在代码开头密码用环境变量避免入库提交时暴露凭据import os import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordos.getenv(MYSQL_PASSWORD), databasebert_sim, charsetutf8mb4 ) def save_record(text_a, text_b, score, label): with conn.cursor() as cursor: sql INSERT INTO similarity_record (text_a, text_b, score, label) VALUES (%s, %s, %s, %s) cursor.execute(sql, (text_a, text_b, score, label)) row_id cursor.lastrowid conn.commit() return row_id参数化 SQL 是必须的f-string 拼接会把带引号的输入变成注入语句批量场景用executemany一次提交一千条写入速度明显提升。查询历史时ORDER BY created_at DESC LIMIT 20接口不要返回全表。数据量小这样最简单直接。5.3 Flask 接口封装与结果返回格式推理函数单独放一个predict.py数据库操作放db.pyFlask 里只做路由编排。接口只需一个 POSTfrom flask import Flask, request, jsonify app Flask(__name__) app.route(/similarity, methods[POST]) def similarity_api(): data request.get_json() 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 are required}), 400 score predict_similarity(text_a, text_b, model, tokenizer, device) label similar if score 0.75 else dissimilar record_id save_record(text_a, text_b, score, label) return jsonify({record_id: record_id, score: score, label: label})返回固定为 record_id、score、label 三件套任何前端都能直接渲染。阈值 0.75 不是拍脑袋验证集上搜索后确定下一章讲自动化方法。模型加载放在服务启动阶段model.eval()执行一次即可不要在每次请求里反复切换状态。6. 工程落地细节显存控制、阈值选取与中文乱码排坑6.1 显存不够时的两条路很多人微调 BERT 的第一个拦路虎是 CUDA out of memory。优先把 batch size 从 32 降到 8再看显存或者把 max_len 从 128 降到 64。想保大 batch梯度累积是常见做法每个小 batch 的梯度先累积到指定步数再更新参数效果接近大 batch 且显存友好。accumulation_steps 4 for step, batch in enumerate(train_loader): loss compute_loss(batch) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()计算 loss 后先除 accumulation_steps反向传播累积梯度到整数倍步数再做参数更新并清空梯度。这样等效于把 batch size 放大到 4 倍显存占用却只增加一个小 batch 的量。6.2 相似度阈值的自动化选择接口里的判定阈值不要手工拍。在验证集上对候选阈值逐个算精确率、召回率取 F1 最高点。几百对验证样本就够跑一轮分钟级完成。常见落在 0.75~0.85但你的数据分布不同结论不一定相同。判断“相似”或“不相似”只是产品化表达模型真正输出的是连续分。6.3 一张排错表现象常见根因处理方式训练 loss 为 NaN学习率过高、数据含空值学习率降到 1e-5检查文本推理时报 input/output type mismatchtensor 不在同一设备统一to(device)batch 推理维度对不齐tokenizer 未统一 padding加paddingmax_lengthMySQL 中文乱码库表连接编码不一致统一utf8mb4全量预测都接近 1负样本分布差异大重建负采样加干扰样本交付时把模型权重目录、tokenizer 目录、建表 SQL 和 Flask 启动脚本放进同一目录树README 写清 Python 版本与 pip freeze 结果。答辩演示或后续接手的人按说明拉起服务就能跑不用再猜依赖关系。本文还有配套的精品资源点击获取
返回列表