Python + BERT:文本语义相似度检测完整实战
2026/9/11 23:17:05 网站建设 项目流程

简介:一份基于Python与BERT深度学习的文本相似度检测系统毕业设计源码包,面向计算机相关专业学生、毕业设计开发者及自然语言处理入门者。项目以BERT预训练模型为核心,提供完整可运行代码与配套数据库,覆盖文本预处理、模型微调、句向量抽取、余弦相似度计算以及Flask/Django接口封装的全流程,能够解决文本语义匹配、查重、问答推荐等场景中的相似度衡量问题。压缩包约52.54MB,包内按程序、数据库、文档等模块化整理,程序部分为Python源码与执行入口,数据库部分为项目的数据存储文件,文档部分涵盖项目背景、需求分析、使用手册和API参考,便于从理论到实践完整理解系统。已有586人学习下载,适合用于毕业设计、课程设计或NLP项目练手;同时可以帮助初学者快速掌握BERT在文本相似度任务中的落地流程,以及数据处理、模型调优、Web服务封装等工程细节,是构建完整NLP应用的参考范本。

1. 当文本相似度检测告别 TF-IDF:BERT 为什么能读懂“同一个意思”

第一次把 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, dim=1) sum_mask = torch.clamp(input_mask_expanded.sum(dim=1), min=1e-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, dim=1).item()

dim=1表示按句子对两两比较,返回形状[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 词表,中文按字和词根切分,模型自己掌握边界。额外分词反而改变输入分布,微调效果容易变差。直接用官方 tokenizer:

from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") def encode_pair(text_a, text_b, max_len=128): encoded = tokenizer( text_a, text_b, max_length=max_len, padding="max_length", truncation=True, return_tensors="pt" ) 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_length=128截断过长的文本,长文本场景再调大;padding="max_length"保证 batch 内每个样本维度一致;truncation=True配合 max_length 生效。返回的 input_ids 是 token 序号,attention_mask 标记真实 token,token_type_ids 区分第一句与第二句。这三个字段直接作为模型输入。

3.3 文本清洗与训练样本构造

数据质量决定相似度上限。清洗阶段我做三件事:去掉大段空白和不可见字符;过滤 URL 与 HTML 标签;繁体统一转简体。中文语义相似度不需要去停用词,停用词在 BERT 里不是删除对象,删了反而可能改变语义。真正值得花时间的是样本构造。

正样本来自同义改写,比如“预计到达时间”和“ETD”;负样本从不同类别里随机抽,保持 1:1。负样本建议加入“表面相近、意思不同”的干扰项,比如“怎么查航班”和“怎么取消航班”,两者只差一个词,但语义方向相反,模型要学的判别信息更多。整理成(text_a, text_b, label)三元组,label 为 0 或 1,存成 CSV 或 JSON 均可。

数据准备好后按 8:2 划分训练集和验证集。验证集有两个用途:一是看模型有没有过拟合,二是微调结束后用来选推理阈值。DataLoader 里训练集开shuffle=True,验证集不开,保证每次评估顺序一致。

4. PyTorch 微调 BERT 与相似度推理实战

4.1 分类还是回归:模型的输出设计

相似度任务有两种建模方式:二分类和回归。二分类里 num_labels=2,模型输出“不相似/相似”两类概率;回归里 num_labels=1,直接输出分数,训练用 MSE 逼近人工标注。如果只有 0/1 标签,分类更稳定;有连续标注分数时,回归更自然。下面按回归实现,接口阶段直接返回小数分数,展示直观。

from transformers import BertForSequenceClassification model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=1 )

num_labels=1会把分类头改成单输出神经元,配合回归 loss 使用。如果使用过程中发现输出分布与预期差异大,再切回 num_labels=2 也不复杂,只需要把 loss 换成交叉熵。

4.2 自定义 Dataset 与完整训练循环

定义 Dataset,把 tokenizer 输出与标签封装起来:

from torch.utils.data import Dataset import torch class TextPairDataset(Dataset): def __init__(self, pairs, labels, tokenizer, max_len=128): 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_length=self.max_len, padding="max_length", truncation=True, return_tensors="pt" ) return { "input_ids": encoded["input_ids"].squeeze(0), "attention_mask": encoded["attention_mask"].squeeze(0), "label": torch.tensor(self.labels[idx], dtype=torch.float) }

__getitem__返回单条样本,输入形状[max_len],标签是标量;DataLoader 会把一批样本堆叠成[batch_size, max_len]。这个阶段最常见的报错是样本长度不一,最后一批维度对不上,原因就是 tokenizer 没加padding="max_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_labels=1) model.to(device) optimizer = AdamW(model.parameters(), lr=2e-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_ids=input_ids, attention_mask=attention_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(f"epoch {epoch + 1} loss: {total_loss / len(train_loader):.4f}")

这里没有向分类头传 labels,而是手动取 logits 计算 MSE,squeeze(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_length=128, padding="max_length", truncation=True, return_tensors="pt" ) 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 1,QPS 不高时足够。

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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

text_atext_b用 TEXT,因为用户输入长度不可控;label用字符串记录判定结果,比 0/1 更直观,也方便以后扩展多档标签;created_at由数据库生成,Python 端不用管时间。索引建在created_at上,历史查询按时间倒序会很快。若未来数据量涨到几十万条,文本元数据继续留在 MySQL,向量抽出来放 FAISS 或 Milvus。

5.2 Python 写入与查询:pymysql 的基本操作

连接参数集中在代码开头,密码用环境变量,避免入库提交时暴露凭据:

import os import pymysql conn = pymysql.connect( host="127.0.0.1", port=3306, user="root", password=os.getenv("MYSQL_PASSWORD"), database="bert_sim", charset="utf8mb4" ) 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.py,Flask 里只做路由编排。接口只需一个 POST:

from 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 未统一 paddingpadding="max_length"
MySQL 中文乱码库表连接编码不一致统一utf8mb4
全量预测都接近 1负样本分布差异大重建负采样,加干扰样本

交付时把模型权重目录、tokenizer 目录、建表 SQL 和 Flask 启动脚本放进同一目录树,README 写清 Python 版本与 pip freeze 结果。答辩演示或后续接手的人,按说明拉起服务就能跑,不用再猜依赖关系。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询