☰
Python+Django文本相似度计算系统设计与实现
2026/10/31 11:16:39 网站建设 项目流程

简介:这是一份面向计算机与软件工程专业毕业设计、课程设计及NLP入门实践的文档型资料,主题为基于Python的文本相似度计算系统,适合需要完成文本处理类项目、撰写系统实现论文或了解相似度系统设计的人群参考。压缩包内共1个docx文件,大小约749KB,内容以毕业论文/设计说明为主体,涵盖文本清洗与分词、TF-IDF关键词提取、词向量构建、余弦相似度计算及Django可视化界面等模块,并涉及Python、Django、MySQL等技术栈的选型说明。文档还包含可行性分析、系统架构、实验评估与结论展望,可帮助读者梳理从需求分析到算法验证的完整写作与实现思路。目前已有138人学习,适合用作毕业设计框架参考、功能模块拆解和相似度算法实现思路的对照材料。

1. 相似度算出来 0.87 却答不上为什么:文本相似度计算系统要解决的真问题

做毕业设计选到「文本相似度计算系统」这个题,多数人第一反应是调一行cosine_similarity就收工,真正跑起来才发现问题不在公式,而在工程:两段八竿子打不着的文本算出来 0.87,老师随口一问「这个分数凭什么」就卡住了。这套基于 Python 的文本相似度计算系统,把文本清洗、中文分词、停用词过滤、特征向量化、相似度计算到可视化展示串成一条完整链路,Web 层用 Django,数据落 MySQL,前端 Bootstrap 加 Ajax,同时提供文本对文本、文件对语料库两种比对入口。它适合正在做文本处理方向毕业设计或课程设计的人直接对照落地,也适合要给搜索、推荐、问答类功能做文本匹配原型的开发者参考模块边界。下面按需求、架构、算法、踩坑、验收的顺序,把这套东西拆开讲透。

2. 需求拆解与技术选型:Python + Django + MySQL 为什么适合这个题

2.1 三类使用者和五条硬功能

这套系统的使用者其实分得很清楚。第一类是普通用户,打开网页,往两个文本框里各粘一段新闻,点「开始计算」,要的是一个分数加一段可视化结果;第二类是内容维护者,负责往库里录新闻标题和正文,这些数据构成了文件分析时的比对语料;第三类是系统管理员,管用户、看操作记录、盯首页那四个统计数字——总用户数、总新闻数、总操作次数、当前月份。

从这三类人身上能抽出五条必须实现的功能:文本预处理(清洗、分词、去停用词)、关键词与词向量构建、多算法相似度计算(余弦、Jaccard、编辑距离、欧氏与曼哈顿距离)、结果可视化展示、文件分析(上传 txt 后与库内新闻做匹配,返回最相似的一条)。这五条不是拍脑袋列的,它们对应一条完整的请求链路:输入进来先洗,洗完切词,切完转向量,向量进算法,分数回前端。

需求里还有几条容易被忽略但答辩必问的约束:响应时间要控制在一秒内、要能同时处理中英文、要能扛住长时间不中断运行、要保证用户输入的数据不被越权访问。这几条落到设计上,分别对应索引设计、编码选择、异常兜底和登录鉴权,后面章节会逐个对上。

提示:功能需求写成「能算相似度」是过不了需求分析这一关的,要写成「输入两段中文文本,选择余弦算法,1 秒内返回 0 到 1 之间的分数并展示」。可测量,才叫需求。

2.2 技术栈取舍:为什么走 Python 全栈而不混搭 JSP

关键词里常出现 JSP、Java 这类词,很多学校的课程体系也确实以 Java Web 为主,于是有人想前端 JSP、算法 Python,搞成两套服务。我的建议是这个题不要混搭。原因很实际:文本处理生态几乎全在 Python 这边,jieba、nltk、scikit-learn、numpy 一条线打通,你为了「技术栈丰富」硬拆一个 Java 服务出来,等于给自己加一层 HTTP 调用、一套序列化协议和一份部署脚本,答辩时还得多解释一遍为什么这么干。

真正合理的选型是 Python 3.8 以上 + Django 3.x + MySQL 5.7/8.0。Django 自带 ORM、表单、模板、认证、后台管理,新闻录入这种 CRUD 几乎零成本;MySQL 存文本正文、计算记录、操作日志足够,配合 utf8mb4 能覆盖绝大多数中文场景;前端用 Bootstrap 加少量原生 JavaScript,表单提交走 Ajax,避免整页刷新打断演示节奏。

如果课程硬性要求 Java 技术栈,也不是不能平移:把engine这一层的预处理、向量化、度量三个模块照着同样的接口用 Java 重写,Django 那部分换成 Spring Boot 的 Controller 和 Thymeleaf 模板,数据库表结构一行不用改。模块边界设计对了,换语言只是换壳。

2.3 性能指标怎么落到可验收的数字上

「响应快」「能处理大数据」这种描述在验收时没法判定。我在做这类系统时习惯把指标翻译成三条可测的线:

指标目标值测量方式
单次文本比对响应时间2000 字以内 ≤ 1s浏览器 Network 面板看 TTFB
单文件分析(≤2MB txt)≤ 3s后端打点记录cost_ms
并发可用性10 并发无明显超时本地压测脚本循环请求
结果可复现性同一输入重复计算分数一致固定随机种子与语料

第一条决定了你不能在每次请求里重新fit一遍向量器;第二条决定了文件大小必须设上限;第三条决定了数据库连接池和分词器初始化要放在启动阶段;第四条决定了语料库必须固定,不能随着新闻新增而让历史分数漂移。这四条约束在后面第四章的代码里会一一体现,现在先记住它们的存在。

3. 总体架构与数据模型:B/S 三层结构加六张核心表的职责划分

3.1 前后端分层与一次请求的完整链路

系统走的是标准 B/S 结构,前端负责输入与展示,后端负责计算与存储,中间靠 HTTP 通信。真正值得说清楚的是后端内部的分层,因为这一层决定了你后期改算法时会不会牵一发动全身。

整个后端拆成四块:预处理层、特征提取层、相似度计算层、结果呈现层。预处理层只干一件事——把任意脏文本变成干净 token 列表,它不知道后面要用什么算法;特征提取层把 token 列表变成数值向量,它不知道这些向量会被拿去算余弦还是算 Jaccard;计算层只接收两个向量或两个集合,返回一个 0 到 1 的分数,它不知道数据从哪来;呈现层只负责组装模板上下文。四层之间用纯 Python 函数通信,不跨进程。

这种切法的好处在联调阶段特别明显。某次我发现中文分词的停用词表漏了几个高频虚词,只需要改preprocess.py一个文件,向量化和计算层完全不动,回归测试也只跑预处理那几个用例。要是当初把清洗、切词、算分写在一个视图函数里,这一改就得整体重测。

一次完整的文本比对请求链路是这样走的:浏览器提交表单 → Django URL 路由到视图 → 表单校验通过 → 调用SimilarityEngine→ 预处理 → 向量化(语料库向量在启动时已建好)→ 计算 → 分数写入similarity_record表 → 渲染模板返回。文件分析走的是同一条链路,只是入口换成文件解析,比对对象换成库内全部新闻。

3.2 六张核心表与字段设计

数据库这块,我的原则是能用 Django ORM 定义就不手写 SQL,但表结构本身要想清楚。系统里真正承载业务的有六张表:用户表(复用 Django 自带auth_user)、新闻表、相似度计算记录表、操作日志表、新闻分类表、上传文件表。

表名作用关键字段
auth_user登录与权限username、password、is_staff
news语料库主体title、content、category_id、created_at
similarity_record每次计算的留痕text_a、text_b、algorithm、score、cost_ms
operation_log操作审计user_id、action、target、created_at
news_category新闻分类name、sort
upload_file文件分析记录filename、size、matched_news_id、score

新闻表和计算记录表是重点。新闻表的content用MEDIUMTEXT,因为新闻正文可能上万字;title上加前缀索引,因为文件分析时要按标题做快速召回;字符集统一utf8mb4,这一点在第五章会重点讲,用utf8迟早翻车。计算记录表看起来可有可无,实际是答辩时的护身符——老师问「你这个结果可复现吗」,你直接SELECT score FROM similarity_record WHERE text_a=? AND text_b=?给他看历史记录,比嘴上解释一百句都管用。

CREATE TABLE news ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content MEDIUMTEXT NOT NULL, category_id INT DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_title (title(64)), KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; CREATE TABLE similarity_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, text_a TEXT NOT NULL, text_b TEXT NOT NULL, algorithm VARCHAR(16) NOT NULL DEFAULT 'cosine', score DECIMAL(6,4) NOT NULL, cost_ms INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

上面两段 DDL 里,DECIMAL(6,4)是刻意的——相似度只可能是 0 到 1 之间的小数,四位小数足够表达,用FLOAT会出现 0.30000000000000004 这种展示尴尬。idx_user_time这个联合索引服务于「某用户最近的计算记录」这类查询,属于典型的时间倒序翻页场景。text_a和text_b直接存原文而不是存哈希,是为了让记录表本身能当复盘素材,代价是占空间,本地演示规模完全扛得住。

3.3 目录结构与模块边界

工程目录按 Django 的 app 划分,一个大原则是算法不进视图:

text_sim/ ├── manage.py ├── text_sim/ # 配置:settings、urls、wsgi ├── similarity/ # 核心计算应用 │ ├── engine/ │ │ ├── preprocess.py # 清洗、分词、停用词 │ │ ├── features.py # 词袋 / TF-IDF 向量化 │ │ └── metrics.py # 余弦、Jaccard、编辑距离 │ ├── forms.py │ ├── models.py │ ├── views.py │ └── urls.py ├── news/ # 新闻语料管理 ├── accounts/ # 用户与个人信息 ├── static/ └── templates/

engine这个包不 import 任何 Django 模块,纯 Python,意味着它可以脱离 Web 框架单独跑测试。这一点在做对比实验时特别省事:写个脚本from similarity.engine.metrics import cosine,直接对着一批样本算,不用启动服务、不用构造请求。很多同学把算法写在views.py里,结果想批量测十组数据还得构造十个 HTTP 请求,纯属给自己找麻烦。

4. 算法链路实现:从 jieba 分词、TF-IDF 向量化到四种相似度计算

4.1 文本预处理:清洗、分词、去停用词的可复现代码

预处理是整条链路的入口,也是最容易被低估的一环。输入文本可能带着 HTML 标签、URL、全角标点、表情符号、连续空白,这些东西如果直接进分词,会产生大量无意义 token,把相似度分数拉偏。

清洗的取舍要想清楚:中英文都保留,数字和符号统一替换为空格。为什么留数字?如果比对的是带年份、带编号的新闻,数字其实是有效特征;如果你的场景里数字纯属噪音,把它也过滤掉即可。

import re import jieba def clean_text(text: str) -> str: """去标签、去链接、保留中英文,其余字符压成空格""" text = re.sub(r'<[^>]+>', ' ', text) # HTML 标签 text = re.sub(r'https?://\S+', ' ', text) # URL text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', ' ', text) # 只留中英文数字 return re.sub(r'\s+', ' ', text).strip()

分词这块,中文用jieba.lcut,英文用正则切分后接 Porter 词干化。停用词表自己维护一份文本文件,一行一个词,加载时转成set,查找复杂度 O(1)。这里有个细节:len(t) > 1这个过滤条件会把单字词全部丢掉,中文里「爱」「恨」这种单字其实有语义,但它们在短文本相似度里贡献极小且噪音大,我在这个系统里选择丢弃,换来的是分数更聚焦在实词上。

def tokenize(text: str, lang: str = 'zh') -> list: text = clean_text(text).lower() if lang == 'zh': tokens = jieba.lcut(text) stop = load_stopwords('stopwords_zh.txt') else: tokens = re.findall(r'[a-z]+', text) stop = set(stopwords.words('english')) # 过滤停用词、单字和纯数字,减少噪音 return [t for t in tokens if len(t) > 1 and t not in stop and not t.isdigit()]

lang参数是从表单里透传下来的,用户在前端选择「中文 / 英文」,后端据此走不同分支。如果你的系统要支持中英混合,别指望自动语种识别,直接按中文字符占比做启发式判断就行,超过 30% 走中文分支,简单可靠。

停用词表本身值得花半小时整理。网上随手搜的表动辄一两千词,里面塞满了「我们」「他们」「可以」这类在特定语料里其实是关键词的词。我的做法是先跑一批真实新闻,统计词频,把明显无区分度的高频词删掉,最后留下三四百个。这一步做完,同一对文本的分数通常会更接近人的直觉。

4.2 特征提取:词袋与 TF-IDF 的取舍

初稿里常见的写法是词袋模型:把整个语料库的词汇做成一个词表,每篇文本对应一个等长向量,出现过的词标 1,没出现的标 0。这个方案实现简单、解释直观,但它有两个硬伤。

第一个硬伤是长文本天然占优。一篇两千字的文章,词袋里命中 1 的维度远多于一段五十字的短句,余弦相似度算出来必然偏高,哪怕两者主题完全不同。第二个硬伤是常见词权重过大。「的」「是」「在」虽然被停用词过滤了一部分,但像「新闻」「记者」这种在语料里高频出现的词,在词袋模型里和生僻专有名词的权重完全一样。

TF-IDF 正好解决第二个问题:词频乘以逆文档频率,一个词在越多文档里出现,它的区分度越低,权重就被压下去。实现上直接用 scikit-learn 的TfidfVectorizer,把自定义的tokenize函数塞进tokenizer参数,注意lowercase=False,否则它会二次小写化。

from sklearn.feature_extraction.text import TfidfVectorizer def build_vectorizer(corpus_tokens): """语料库固定后 fit 一次,后续 transform 复用,保证向量空间一致""" vectorizer = TfidfVectorizer( tokenizer=lambda x: x, # 传入的已经是 token 列表 lowercase=False, max_features=5000, # 控制维度,避免稀疏矩阵过大 token_pattern=None ) vectorizer.fit(corpus_tokens) return vectorizer

这里最关键的一点是:fit只能在语料库上做一次。我见过不少实现是在每次请求里把「文本 A + 文本 B」临时拼成语料再fit_transform,这样算出来的余弦相似度永远偏高,因为向量空间只有两个维度,两者只要有一个公共词,夹角就会很小。正确做法是启动时用全部新闻正文 fit 出一个固定的向量器,存成全局对象或缓存,请求来时两段文本各自transform,落进同一个 5000 维空间里再比较。

词袋模型也不是完全弃用。我在系统里保留它作为可切换的向量化方式,答辩时正好做对照实验:同一组样本,词袋算出 0.62,TF-IDF 算出 0.41,然后解释为什么后者更贴近语义判读。这组对比数据本身就是加分项。

4.3 相似度计算:四种度量的封装与适用边界

计算层封装成统一接口,输入两个对象,输出 0 到 1 的分数。四种度量各有各的适用场景,不能只会余弦。

余弦相似度衡量的是向量夹角,对长度不敏感,长文本短文本比较不会被长度带偏,是默认选项。Jaccard 看的是两个 token 集合的交并比,适合短文本、关键词匹配这种场景,比如判断两条新闻标题是不是同一事件。编辑距离衡量的是把 A 改成 B 最少要几步,适合错别字容忍、拼写纠错,但它对长度差极其敏感,两段长度差一倍的中文几乎必然分数很低。欧氏距离和曼哈顿距离本质是距离,需要再转成相似度,通常用1 / (1 + d)归一化。

import numpy as np def cosine(vec_a, vec_b) -> float: a, b = np.asarray(vec_a, dtype=float), np.asarray(vec_b, dtype=float) denom = np.linalg.norm(a) * np.linalg.norm(b) if denom == 0: # 空向量直接判 0,绝不允许除零 return 0.0 return float(np.dot(a, b) / denom) def jaccard(tokens_a, tokens_b) -> float: sa, sb = set(tokens_a), set(tokens_b) if not sa or not sb: # 任一方为空,交集必为空 return 0.0 return len(sa & sb) / len(sa | sb)

denom == 0这个判断看着不起眼,它是整个系统最容易被漏掉的一行。空文本、纯符号文本、全部被停用词过滤掉的文本,在transform之后会得到一个全零向量,np.linalg.norm返回 0,接下来就是ZeroDivisionError,前端拿到 500 错误页。第五章还会专门说这个坑。

编辑距离用difflib.SequenceMatcher就够,不必引第三方库:

from difflib import SequenceMatcher def edit_similarity(text_a: str, text_b: str) -> float: # ratio 内部按最长匹配子序列计算,返回 0~1 return SequenceMatcher(None, text_a, text_b).ratio()

视图层把这几个函数统一包进一个引擎类,按前端传来的algorithm参数分派。这样加算法只要在类里加一个分支,不用动路由和模板。

4.4 Django 视图与文件分析接口

视图层要做三件事:接表单、调引擎、落记录。表单类这里有个初稿里常见的毛病——只定义了一个text字段,可界面上明明是「两个输入框加一个按钮」,一个字段装不下两段文本,提交后校验直接失败。正确写法是两个字段加一个算法选择:

# similarity/forms.py from django import forms class TextSimilarityForm(forms.Form): text_a = forms.CharField( label='文本 A', widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) text_b = forms.CharField( label='文本 B', widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6})) algorithm = forms.ChoiceField( choices=[('cosine', '余弦'), ('jaccard', 'Jaccard'), ('edit', '编辑距离'), ('euclid', '欧氏距离')], initial='cosine')

widget里指定的form-control是 Bootstrap 的类名,这样后端渲染出来的输入框和前端样式表能对上,不用手写一堆 CSS。algorithm用ChoiceField而不是让前端传自由字符串,是为了避免用户传个'cosinexxx'进来导致分派失败。

# similarity/views.py from django.shortcuts import render from django.contrib.auth.decorators import login_required from .forms import TextSimilarityForm from .engine.metrics import cosine, jaccard, edit_similarity from .models import SimilarityRecord @login_required def index(request): result = None if request.method == 'POST': form = TextSimilarityForm(request.POST) if form.is_valid(): a = form.cleaned_data['text_a'] b = form.cleaned_data['text_b'] algo = form.cleaned_data['algorithm'] # 分派到对应度量,空输入由 metrics 层兜底返回 0 if algo == 'jaccard': score = jaccard(tokenize(a), tokenize(b)) elif algo == 'edit': score = edit_similarity(a, b) else: va, vb = transform_pair(a, b) # 复用全局向量器 score = cosine(va, vb) SimilarityRecord.objects.create( user_id=request.user.id, text_a=a, text_b=b, algorithm=algo, score=round(score, 4)) result = {'score': round(score, 4), 'algorithm': algo} else: form = TextSimilarityForm() return render(request, 'similarity/index.html', {'form': form, 'result': result})

@login_required这个装饰器对应需求里的「保护用户数据」,未登录直接跳登录页,不需要自己写判断。SimilarityRecord.objects.create这一行是异步无关的,但要注意它会在主请求里多花几毫秒写库,如果追求极限响应可以改成消息队列,本地演示规模没必要。

文件分析接口的差异在于入口是request.FILES,且比对对象是库里全部新闻。这里必须做两层校验:扩展名必须是.txt,同时读前 512 字节判断是不是可解码文本。只校验扩展名不够,把.docx改名成.txt一样能传上来,读出来是二进制乱码,分词结果全是噪音,最后匹配出一条毫不相干的新闻,用户还以为系统坏了。

@login_required def analyze_file(request): f = request.FILES.get('file') if not f or not f.name.endswith('.txt') or f.size > 2 * 1024 * 1024: return JsonResponse({'code': 400, 'msg': '仅支持 2MB 以内的 txt'}) raw = f.read() try: content = raw.decode('utf-8') except UnicodeDecodeError: content = raw.decode('gbk', errors='ignore') # 兼容旧编码 tokens = tokenize(content) best = match_news(tokens) # 与库里全部新闻算相似度取最大 return JsonResponse({'code': 0, 'data': best})

这段代码里f.size > 2 * 1024 * 1024对应第二章那条「单文件分析 ≤ 3s」的指标,超过 2MB 的文本光分词就要好几秒,不如直接拒掉。decode('gbk')这个兜底是血泪经验,Windows 下导出的 txt 大概率是 GBK,不处理就是满屏乱码。

5. 避坑与排查:五个最容易翻车的环节

5.1 相似度分数人人都是 0.9

现象:随便拿两段不相关的新闻去算,余弦相似度都在 0.8 以上,前端展示出一条几乎拉满的进度条,看起来系统坏了。

原因有两层。表层是停用词表太小,虚词没滤干净,两段文本共享大量「我们」「这个」「表示」之类的词,向量在这些维度上高度重合。深层原因是向量器在每次请求里重新fit,向量空间只由这两段文本构成,维度塌缩到几十个,任何共享词都会把夹角压得很小。

解决:停用词表按语料词频重新筛一遍,删掉无区分度的高频词;向量器改成全局单例,启动时用全部新闻fit一次,请求里只transform;再加一条兜底,分数低于 0.1 和高于 0.95 的记录打日志抽样看,确认不是系统性偏差。改完之后同一组样本的分数会从 0.87 落到 0.35 左右,这才符合人的直觉。

5.2 空文本触发除零异常

现象:用户不填内容直接点计算,页面报 500,后端日志是ZeroDivisionError: float division by zero。

原因:空字符串经过清洗和分词得到空列表,向量化后是全零向量,模长为 0,余弦公式的分母直接变零。这个坑在正常测试里几乎撞不到,因为大家测试时都会老老实实填内容。

解决:在metrics层做双重兜底,一是算分母前判断denom == 0返回 0,二是在视图入口判断len(tokens) < 2就返回友好提示而不是继续往下走。前端也加一层 JavaScript 校验,输入框为空时禁用按钮。三层防护听着夸张,但这类边界用例恰恰是答辩时老师最爱点的。

5.3 中文乱码和表情符号存不进去

现象:存进去的新闻正文在列表页显示成问号,或者用户粘贴带表情符号的文本时,MySQL 直接抛Incorrect string value异常。

原因:数据库或表建的时候用了utf8字符集。MySQL 的utf8实际是三个字节的变长编码,存不下四字节的字符,表情符号和部分生僻汉字都会被拒。

解决:建库建表统一utf8mb4,Djangosettings.py里DATABASES的OPTIONS加上'charset': 'utf8mb4',连接串也确认一遍。已经建好的表用ALTER TABLE news CONVERT TO CHARACTER SET utf8mb4迁移,迁移前先备份。这个坑不改,等到演示当天用户粘一段带 emoji 的文本就当场翻车。

5.4 文件分析接口把请求拖死

现象:上传一个 5MB 的 txt,页面转圈十几秒最后超时,浏览器报 504。

原因:文件分析要把上传文本和库里每一条新闻逐一算相似度,新闻一千条就是一千次向量化加一千次余弦计算,全在请求线程里同步跑。再加上如果向量器没做缓存,每次transform都重新加载词典,时间还会翻倍。

解决:接口层限制文件 2MB 并做前置校验;新闻向量在启动时批量算好缓存起来,比对时只算上传文本的向量,然后做矩阵乘法一次性出全部分数,一千条新闻的比对能压到几十毫秒;新闻量再大就上分页或者只比对标题做粗召回,再对 Top 20 做正文精算。

5.5 同一组输入两次算出不同分数

现象:演示时先算了一遍 0.42,讲了两句再点一次变成 0.38,老师当场质疑结果不可信。

原因:语料库在两次计算之间被改动过——后台新录了一条新闻,或者向量器因为某种原因被重新fit。TF-IDF 的 IDF 值依赖整个语料库,语料一变,同一段文本的向量就变了,分数自然漂移。

解决:把语料快照和向量器版本绑在一起,每次fit后把vectorizer和语料 id 列表序列化落盘,请求时加载固定版本;同时similarity_record表里记下当时的vectorizer_version,复盘时有据可查。演示前锁库,不要在现场加新闻,这条听着像废话,但每年都有人栽在上面。

6. 验收与答辩:一套可复现的测试用例和十分钟演示脚本

6.1 功能验收清单与测试用例

验收这件事,靠「我点了几下没报错」是过不了的。我在做这类系统时习惯先写一张用例表,把正常路径、边界路径、异常路径各覆盖几条,跑完打勾才算交付。

编号输入预期结果判定
TC-01两段完全相同的文本分数 1.0000通过
TC-02两段主题无关的新闻分数 < 0.20通过
TC-03两段空文本提示「请输入内容」,不抛 500通过
TC-04纯标点符号返回 0 并给出提示通过
TC-052000 字中文,切换四种算法均返回 0~1,响应 ≤ 1s通过
TC-06上传 2MB 以内 txt正常解析并返回最相似新闻通过
TC-07把 docx 改名成 txt 上传拒绝并提示格式错误通过
TC-08未登录访问计算页跳转登录页通过

TC-01 和 TC-02 是一对,一个验上限一个验下限,缺了下限那条,你无法证明系统不是「什么都算得很像」。TC-03 到 TC-05 是边界,TC-06 到 TC-08 是异常和权限。八条用例跑完大概五分钟,建议录屏存一份,答辩现场网络出问题时直接放录像。

6.2 现场演示的十分钟脚本

演示最忌讳临场发挥,我会提前把每一步的顺序和话术写死:

第 0 到 2 分钟,登录进首页,指着四个统计数字说清楚系统在管什么——总用户、总新闻、总操作、当前月份,顺便点开操作日志证明每次计算都留了痕。

第 2 到 5 分钟,进文本分析页,粘两段同主题新闻,默认余弦,点计算,得到 0.6 左右;然后把算法切到 Jaccard,分数掉到 0.3 左右;再切到编辑距离,分数更低。每切一次解释一句「余弦看方向、Jaccard 看词集重合、编辑距离看字面改动」,这一段是整个演示的信息密度高点。

第 5 到 7 分钟,进文件分析,上传一个准备好的 txt,展示返回的最相似新闻标题和分数,再点进去看原文,证明匹配结果说得通。

第 7 到 9 分钟,进后台新闻管理,加一条新闻,回前台重算,说明语料变化对分数的影响,顺势引出「向量器版本固定」这个设计。

第 9 到 10 分钟,打开数据库客户端,展示similarity_record里刚才那几条记录,收尾。

6.3 答辩常被追问的三个点

第一个问题几乎必问:为什么用余弦不用欧氏距离。回答要落到场景上——文本向量是高维稀疏的,长度差异大,欧氏距离对长度敏感,两篇长短悬殊但主题一致的文章会被判得很远;余弦只看夹角,对长度不敏感,更适合文本。

第二个问题:词袋和 TF-IDF 差在哪。直接拿演示时那组对比数据说话,同一对文本两种方式算出的分数差多少,差在哪个词上,比背定义有说服力。

第三个问题:系统能处理多长的文本,瓶颈在哪。老实回答瓶颈在分词和向量化,2MB 的文件要几秒,再大就得上分批和缓存,然后把第五章那套缓存方案讲一遍。承认边界比吹嘘无限能扛更能拿分。

从那以后我每次带人做这个题,都强制先跑一遍空文本、纯符号、docx 改名这三条异常用例,再谈功能演示——顺序错了,后面全是补救。希望这套结构和清单能帮到你,照着搭一遍,再按自己的语料把停用词表和用例表改一改,就是一份能交差的完整方案。

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

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

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

立即咨询