news 2026/10/27 21:39:47

Django文本相似度计算系统:jieba分词、TF-IDF与余弦相似度实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django文本相似度计算系统:jieba分词、TF-IDF与余弦相似度实践

简介:面向计算机专业毕业设计与自然语言处理入门场景的文本相似度计算系统文档,重点服务需要完成选题、系统设计与论文撰写的学生及初级开发者,帮助梳理文本清洗、分词、关键词提取和相似度比较的实现路径。包内共1个docx文件,约749KB,以毕业设计说明书形式组织,含封面、中英文摘要、目录、绪论、可行性分析及系统实现等章节,便于按框架查阅。内容围绕Python、Django与MySQL等技术栈展开,涉及正则清洗、nltk分词、TF-IDF关键词提取、词向量构建、余弦相似度计算及可视化界面,并包含实验评估与结论展望。已有138人学习,可作为选题立项、功能模块划分、论文目录和写作模板的对照材料,也能帮助读者理解从文本预处理到相似度结果展示的完整设计思路。

1. 项目定位:把「两段文本像不像」做成一个能点开就用的 Web 系统

做推荐、做查重、做问答匹配,绕不开一件事:给两段文本打分,判断它们有多像。很多人拿到标题写着「基于 Python 的文本相似度计算系统」的工程,解压发现确实是完整的 Django 项目,却卡在数据库连接和分词库导入上,页面打不开,分数也出不来。这套工程要解决的正是把算法 Demo 变成可交付系统:浏览器里输入两段文本或上传一个 txt,后端清洗、分词、向量化,用余弦相似度算出 0 到 1 的分数,连同关键词一起渲染回页面;背后还挂着新闻文本库、比较记录和用户管理,构成一条 B/S 闭环。它适合想拿它当二次开发底座的人——换语料、换算法、做批量查重。手上若是 jsp/java 版本的同题工程,模块划分思路相通,接口层换成 Servlet 那套即可。

2. 工程结构与启动:Django 分层、MySQL 建表到一次跑通

这一章的目标很直接:把服务在本机拉起来,能登录、能提交两段文本、能看到分数。中途任何一个环节断掉,后面讲算法都是空谈。

2.1 依赖清单与选型理由

项目正文已经定死了三件套:Python、Django、MySQL。这个组合放在文本相似度这类系统里是合理的——Django 自带 ORM、模板引擎、admin 后台和一套完整的用户认证,正文里提到的「用户管理」「新闻管理」这两个模块几乎不用从零写;MySQL 负责持久化语料和比较记录;算法部分单独抽成一个 Python 包,不依赖框架,方便单独跑测试。

依赖清单我一般会写成这样,版本区间而不是写死某个小版本,方便在不同机器上装:

# requirements.txt Django>=3.2,<4.2 # 3.2 是 LTS 分支,生态兼容性最好 mysqlclient>=2.1 # MySQL 驱动,比纯 Python 的 pymysql 快一截 jieba>=0.42 # 中文分词,正文明确使用 nltk>=3.8 # 英文分词与停用词,做中英混排时兜底 numpy>=1.23 # 向量运算,余弦相似度批量算时用得上 scikit-learn>=1.2 # TfidfVectorizer / cosine_similarity,可选 python-Levenshtein>=0.20 # 编辑距离的 C 实现,长文本比纯 Python 快一个量级

参数说明:mysqlclient需要本机有 MySQL 的开发头文件,这是后面避坑章节要专门说的第一条;python-Levenshtein是可选增强,装不上可以用纯 Python 的 DP 版本顶替,功能一致只是慢;scikit-learn只在做批量检索时才需要,单对文本比较用它属于杀鸡用牛刀。

2.2 目录结构:算法层与 Web 层怎么切

这套工程的结构我建议按「框架配置 / 业务应用 / 算法包 / 静态资源」四块分,而不是把所有 py 文件堆在一个 app 里。原因很实际:算法包要能被单元测试直接 import,不该依赖 Django 的 settings 初始化。

textsim/ ├── manage.py ├── requirements.txt ├── textsim/ # 项目配置层 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── core/ # 业务应用层:模型、视图、表单、路由 │ ├── models.py │ ├── views.py │ ├── forms.py │ ├── urls.py │ ├── admin.py │ └── migrations/ ├── algo/ # 算法层:纯 Python,不 import django │ ├── preprocess.py # 清洗、分词、停用词 │ ├── vectorizer.py # 词袋 / TF-IDF 向量化 │ └── similarity.py # 余弦、Jaccard、编辑距离 ├── templates/ │ ├── base.html │ ├── index.html │ └── result.html ├── static/ │ ├── css/ │ └── js/ └── data/ ├── stopwords.txt # 中文停用词表 └── corpus/ # 初始语料,txt 一批一篇

这个切法的好处在二次开发时特别明显:想换算法只动algo/,想加页面只动core/views.py和templates/,两边不会互相牵连。data/单独放词表和语料,是为了让运维能直接替换文件而不用改代码。

数据模型这边,正文提到的「新闻管理」「文本分析」「操作记录」落到表上其实就是两张核心表加 Django 自带的auth_user:

# core/models.py from django.db import models from django.contrib.auth.models import User class News(models.Model): """语料库:每条记录是一篇可比对的文本""" title = models.CharField(max_length=200, db_index=True) content = models.TextField() category = models.CharField(max_length=50, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'news' indexes = [models.Index(fields=['category'])] class CompareRecord(models.Model): """一次比较的留痕,后续做效果评估和 TopK 检索都靠它""" user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) left_text = models.TextField() right_text = models.TextField() algorithm = models.CharField(max_length=20, default='cosine') score = models.FloatField() created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'compare_record' ordering = ['-created_at']

title上单独加db_index=True,是因为结果页要做「匹配最佳新闻」的标题回显和模糊搜索;algorithm字段记下这次用的是哪套算法,以后加新算法时历史数据不会混淆;user用SET_NULL而不是级联删除,删用户不该把比较记录一起抹掉,否则线上排查问题时日志就成了黑匣子。

2.3 数据库配置与启动命令

settings.py里改数据库段,中文语料必须指定utf8mb4,否则生僻字和表情符号会直接截断报错:

# textsim/settings.py 片段 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'textsim', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }

完整的启动流程按下面这几步走,每步都能单独验证:

# 1. 建虚拟环境,别把系统 Python 装乱 python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt # 2. 建库,字符集必须显式指定 mysql -u root -p -e "CREATE DATABASE textsim DEFAULT CHARSET utf8mb4;" # 3. 生成表结构并建管理员 python manage.py makemigrations core python manage.py migrate python manage.py createsuperuser # 4. 起服务,0.0.0.0 方便同局域网用手机测页面 python manage.py runserver 0.0.0.0:8000

参数上注意两点:makemigrations只对core这个 app 生成迁移文件,如果后面新加了 app 记得把名字补上;runserver带0.0.0.0时 Django 会提示这是开发服务器,别拿到生产环境用。跑完这四步,浏览器打开8000端口能看到首页、/admin能进后台,就说明基础链路通了。

2.4 请求链路:从 URL 到视图到模板

路由分两级,项目级只挂 admin 和业务前缀,业务级在core/urls.py里细分:

# textsim/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('core.urls')), ] # core/urls.py from django.urls import path from . import views app_name = 'core' urlpatterns = [ path('', views.index, name='index'), # 首页 + 输入框 path('compare/', views.compare, name='compare'), # 两段文本比较 path('file/', views.compare_file, name='compare_file'), # txt 文件比对语料库 path('history/', views.history, name='history'), # 比较历史 ]

app_name加上之后,模板里就能写{% url 'core:compare' %},后期改路径不用全局搜字符串。视图层只负责取参数、调algo/里的函数、把结果塞进 context,不写算法逻辑——这条边界一旦破了,后面想给算法加单元测试就得先 mock 整个 Django 环境,血泪经验。

3. 文本预处理到向量化:jieba 分词、停用词过滤与 TF-IDF 权重落地

预处理是这类系统里最容易被低估的一段。正文里写的「清洗、分词、去停用词、词干化」四步看着简单,实际效果差异全在细节里:标点没清干净,余弦相似度会被逗号句号拉偏;停用词表用错了,两段毫不相关的新闻也能算出 0.6。

3.1 清洗与分词:正则边界和停用词加载

先把「什么算有效字符」定死。中文语料常见的问题是全角标点、多余空白、HTML 残留标签,这三样都会污染分词结果。

# algo/preprocess.py import re import jieba from functools import lru_cache from pathlib import Path STOPWORD_PATH = Path(__file__).resolve().parent.parent / 'data' / 'stopwords.txt' # 只保留中文、英文、数字,其余一律替换成空格 PUNCT_RE = re.compile(r'[^\u4e00-\u9fa5a-zA-Z0-9]+') @lru_cache(maxsize=1) def load_stopwords() -> set: """停用词表只读一次,进程内缓存,避免每次请求都读盘""" if not STOPWORD_PATH.exists(): return set() with open(STOPWORD_PATH, encoding='utf-8') as f: return {line.strip() for line in f if line.strip()} def clean_text(text: str) -> str: if not text: return '' return PUNCT_RE.sub(' ', text.lower()).strip() def tokenize(text: str, min_len: int = 2, pos_filter: bool = False) -> list: """返回过滤后的 token 列表""" cleaned = clean_text(text) stopwords = load_stopwords() if pos_filter: import jieba.posseg as pseg # 只保留名词、动词、形容词,虚词和量词对相似度贡献很低 words = [w for w, flag in pseg.cut(cleaned) if flag.startswith(('n', 'v', 'a'))] else: words = list(jieba.cut(cleaned)) return [w for w in words if len(w) >= min_len and w not in stopwords]

逻辑说明:PUNCT_RE用取反字符集,把中日韩统一表意文字区、英文字母、数字之外的所有字符统一换成空格,比逐个replace干净得多,也不会把「3.5%」这类数字切碎。lru_cache(maxsize=1)是这里的关键优化点——停用词表通常几千行,每次请求读一遍盘,QPS 一上来就是瓶颈。

参数上,min_len=2过滤掉单字,中文单字歧义太大(「的」「了」已进停用词表,「行」「打」这类单字仍会干扰);pos_filter打开后精度会提升,但分词速度会掉三到五成,短文本用没问题,长文本批量比对时我一般关掉。

3.2 词袋与 TF-IDF:两套向量化路径

正文里描述的词袋模型是「出现记 1、未出现记 0」,这套二值化方案对短文本够用,但对长文本区分度很差——一个词出现一次和出现二十次被当成同一回事。实际工程里我一般同时保留两条路径,让用户在界面上选。

# algo/vectorizer.py import math from collections import Counter def to_bow_vector(tokens: list, binary: bool = False) -> dict: """词袋:binary=True 时退化成正文里那套 0/1 标记""" tf = Counter(tokens) if binary: return {w: 1.0 for w in tf} return {w: float(c) for w, c in tf.items()} def build_idf(corpus_tokens: list) -> dict: """语料库级 IDF,n 为文档数,df 为包含该词的文档数""" n = len(corpus_tokens) df = Counter() for tokens in corpus_tokens: df.update(set(tokens)) # 注意用 set,一篇文档只计一次 # 加一平滑,避免未登录词 idf 为 0 导致整个向量塌缩 return {w: math.log((n + 1) / (c + 1)) + 1 for w, c in df.items()} def to_tfidf_vector(tokens: list, idf: dict) -> dict: """词频归一化后乘 IDF,得到稀疏 dict 表示""" tf = Counter(tokens) total = sum(tf.values()) or 1 return {w: (c / total) * idf.get(w, 1.0) for w, c in tf.items()}

build_idf里df.update(set(tokens))这一行必须用set,不然一篇长文档里重复出现的词会把df撑大,IDF 被压到接近 1,权重区分度直接消失。math.log((n + 1) / (c + 1)) + 1是带平滑的 IDF,+1保证语料库外的新词权重不低于基准值 1.0,不会出现「用户输入了一句语料库里没有的话,向量全零」的尴尬。

to_tfidf_vector里total = sum(tf.values()) or 1,那个or 1是防御性写法:空 token 列表时total为 0,除零会抛异常,加个兜底让函数安全返回空 dict。

存到数据库时要注意,dict不能直接塞进TextField,我一般json.dumps后存,读的时候反序列化。语料量上千篇后,每次请求都重新算 IDF 会明显卡顿,所以向量表要么在启动时预热到内存,要么单独建一张TermVector表落库,用news_id + term做联合索引。

3.3 中英文混排的处理

正文提到中文用 jieba、英文用 nltk,实际的语料往往是混着来的。我一般先按字符类型分段,中文段走 jieba,英文段走nltk.word_tokenize,再合并 token 列表。英文还有个额外步骤是词形还原,running和run在词袋里是两个不同的键,不处理会白白拉低相似度。不用 nltk 的话,jieba的英文支持也能凑合,但大小写和复数形式得自己处理。

4. 相似度算法内核:余弦、Jaccard、编辑距离三条打分路径

算法层是这套系统的核心,正文里明确提到余弦相似度是默认方法,同时支持欧几里得距离、曼哈顿距离和 Jaccard。这几种算法适用场景差别很大,混着用而不加区分,结果会让人怀疑人生。

4.1 余弦相似度:为什么它是默认选项

余弦相似度衡量的是两个向量在方向上的夹角,对文本长度不敏感——一段一百字的新闻和一段三千字的文档,只要主题一致,余弦值依然能到 0.7 以上。这正是文本比对场景需要的性质。

# algo/similarity.py import math from collections import Counter def cosine_sim(vec_a: dict, vec_b: dict) -> float: """输入是两个稀疏 dict:{词: 权重}""" if not vec_a or not vec_b: return 0.0 common = set(vec_a) & set(vec_b) dot = sum(vec_a[k] * vec_b[k] for k in common) norm_a = math.sqrt(sum(v * v for v in vec_a.values())) norm_b = math.sqrt(sum(v * v for v in vec_b.values())) if norm_a == 0 or norm_b == 0: return 0.0 return round(dot / (norm_a * norm_b), 4)

逻辑说明:先取交集再做点积,避免对不共同的词做无意义的乘法,语料量大时这个优化能把计算量砍掉一大半。两个norm分母单独算,是因为稀疏向量里大部分位置是零,用values()遍历实际存储的键比补零成稠密数组快。

参数上,round(..., 4)保留四位小数,位数太多界面上显示一长串没意义,位数太少 0.9999 和 1.0 分不开。返回 0.0 的边界条件有两个:任一向量为空、任一向量模长为零,后者在「两个文本分词后全是停用词」时会出现,不加这个判断就是除零异常。

4.2 Jaccard 与编辑距离:两种完全不同的思路

Jaccard 只看词集合的交并比,完全不管词频和顺序,适合做快速粗筛。编辑距离看的是字符级的最少编辑操作数,适合短文本的精确比对,比如标题级别。

def jaccard_sim(tokens_a: list, tokens_b: list) -> float: sa, sb = set(tokens_a), set(tokens_b) if not sa and not sb: return 0.0 return round(len(sa & sb) / len(sa | sb), 4) def edit_distance(s1: str, s2: str, max_len: int = 2000) -> int: """滚动数组版 Levenshtein,空间 O(n)""" s1, s2 = s1[:max_len], s2[:max_len] # 超长截断,纯 DP 是 O(m*n) m, n = len(s1), len(s2) prev = list(range(n + 1)) for i in range(1, m + 1): cur = [i] + [0] * n for j in range(1, n + 1): cost = 0 if s1[i - 1] == s2[j - 1] else 1 cur[j] = min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost) prev = cur return prev[n] def edit_sim(s1: str, s2: str) -> float: """归一化成 0~1 的相似度,值越大越像""" if not s1 and not s2: return 0.0 dist = edit_distance(s1, s2) return round(1 - dist / max(len(s1), len(s2)), 4)

jaccard_sim的分母len(sa | sb)在两个集合都为空时是 0,前面那个if挡住了;但还有一种情况是只有一边为空,此时结果是 0.0,逻辑上正确,不需要额外处理。

edit_distance用滚动数组把空间从 O(m*n) 降到 O(n),两个长度一万的文本原本要开 1 亿的矩阵,现在只开两个数组。max_len=2000的截断是不得已的妥协:DP 的复杂度是长度乘积,字数超过两千后单次计算就要几百毫秒,用户在页面上等不了。真要处理长文本的字符级相似度,得换成 SimHash 或者直接上python-Levenshtein的 C 扩展。

edit_sim的归一化用max(len(s1), len(s2))做分母,保证结果落在 0 到 1,编辑距离为 0 时相似度为 1。

4.3 一个统一的分发入口

三种算法参数形态不一样,视图层不该关心这些细节。加一层分发,把「文本 → token → 向量 → 分数」整条链路封起来:

# algo/similarity.py 追加 from .preprocess import tokenize from .vectorizer import to_tfidf_vector, to_bow_vector, build_idf ALGO_REGISTRY = {} def register(name): def wrapper(fn): ALGO_REGISTRY[name] = fn return fn return wrapper @register('cosine') def _cosine(text_a: str, text_b: str, idf: dict = None) -> float: ta, tb = tokenize(text_a), tokenize(text_b) idf = idf or {} return cosine_sim(to_tfidf_vector(ta, idf), to_tfidf_vector(tb, idf)) @register('jaccard') def _jaccard(text_a: str, text_b: str, idf: dict = None) -> float: return jaccard_sim(tokenize(text_a), tokenize(text_b)) @register('edit') def _edit(text_a: str, text_b: str, idf: dict = None) -> float: return edit_sim(text_a.strip(), text_b.strip()) def compute(text_a: str, text_b: str, algo: str = 'cosine', idf: dict = None) -> dict: if algo not in ALGO_REGISTRY: raise ValueError(f'不支持的算法: {algo}') score = ALGO_REGISTRY[algo](text_a, text_b, idf) return {'algorithm': algo, 'score': score}

用注册表模式的好处是加算法不用改compute的函数体,写个@register('simhash')就能挂上去,前端下拉框的值和后端 key 一一对应。compute返回 dict 而不是裸 float,是为了以后扩展字段(比如返回耗时、命中的共同词列表)时不用改所有调用方。

5. 避坑与常见问题排查:五个真实翻车现场

这套系统我自己部署过几轮,下面这五条是按踩坑频率排的,每条都按「现象、原因、解决」写,遇上了直接对照。

5.1 装 mysqlclient 报 mysql_config not found

现象:pip install mysqlclient在依赖编译阶段挂掉,终端刷一屏红色报错,核心一行是EnvironmentError: mysql_config not found。

原因:mysqlclient是 C 扩展,编译时需要 MySQL 的客户端开发头文件,很多系统默认只装了运行时客户端,没装-dev或者-devel包。

解决:Linux 上装default-libmysqlclient-dev和build-essential;macOS 用brew install mysql-client后把mysql-client/bin加进PATH;懒得折腾的话,settings.py里用pymysql顶替,在__init__.py里加两行:

# textsim/__init__.py import pymysql pymysql.install_as_MySQLdb()

代价是性能略差,并发高了能感觉到,做课程演示够用。

5.2 第一次点「开始计算」一直转圈

现象:首页正常打开,提交文本后请求卡住十几秒,甚至直接 504。

原因:两个叠加问题。一是jieba首次调用会加载词典,几百毫秒到一两秒;二是视图里每次都重新读整个语料库、重算 IDF,语料上千篇时这一步就是灾难。

解决:把 IDF 的构建挪到应用启动时做一次,用模块级变量缓存,或者直接落库;jieba可以调jieba.initialize()在AppConfig.ready()里预热。视图里只做查表和向量计算,别在请求路径上做全量扫描。

5.3 余弦相似度全是 0 或者全是 1

现象:随便输入两段不相干的文本,分数也是 1.0;或者明显相似的两段,分数是 0.0。

原因:前者通常是 IDF 全被算成了同一个值,向量退化成词频向量后,短文本之间恰好方向一致;后者多半是停用词表把内容词也过滤掉了,两边 token 都为空,走了返回 0.0 的分支。

解决:先打印中间结果,print(tokenize(text))看 token 列表是不是空的;再检查 IDF 构建时的df是不是没去重。调试时用这行快速定位:

# 排查用:看两段文本的 token 和向量是否为空 ta, tb = tokenize(text_a), tokenize(text_b) print(len(ta), len(tb), ta[:10], tb[:10])

5.4 提交表单报 403 CSRF 校验失败

现象:页面上点按钮没反应,看控制台是 403,翻 Django 日志写着CSRF verification failed。

原因:用 Ajax 提交时没带 CSRF token,或者模板里忘了{% csrf_token %}。Django 的中间件默认对 POST 做校验,纯fetch请求不会自动带 cookie。

解决:模板里保留{% csrf_token %},前端从 cookie 或隐藏域取 token 塞进请求头:

// static/js/compare.js function getCookie(name) { const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)'); return v ? v.pop() : ''; } fetch('/compare/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCookie('csrftoken'), // 关键:请求头名不能写错 }, body: new URLSearchParams({ text_a: a, text_b: b, algorithm: 'cosine' }), }).then(r => r.json()).then(renderResult);

X-CSRFToken这个头名是 Django 默认配置,改过CSRF_HEADER_NAME的话要对应调整。

5.5 中文入库后变成问号或直接截断

现象:后台看到新闻正文后半段没了,或者出现一串???。

原因:建库时没指定字符集,MySQL 默认可能是latin1;或者连接配置里少了charset参数。

解决:建库语句必须带DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci,settings.py的OPTIONS里补上charset。已经建错的库可以改,但历史数据要重新导入,这一步没有后悔药,建库的时候就该定死。

6. 二次开发:把单对比较扩成批量查重与算法可插拔

拿到这套工程的人,需求十有八九不是「比两段文本」,而是「我有一篇稿子,帮我从库里找出最像的十篇」。这就是从单对比较到 TopK 检索的跨越,改动集中在向量层和视图层,模型基本不用动。

第一步是把语料的向量预先算好。不要在每次请求里现算,语料上千篇时那点延迟用户能明显感觉到。我的做法是加一张向量表或者用缓存,语料新增时增量更新。批量检索用矩阵乘法比逐对比快得多:

# algo/retrieval.py import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel def build_index(corpus_texts: list, corpus_ids: list): """一次性构建语料索引,返回向量矩阵和 id 映射""" vec = TfidfVectorizer(tokenizer=lambda t: t.split(), lowercase=False) # 注意:这里假定入参已经过分词,用空格拼成字符串 matrix = vec.fit_transform(corpus_texts) return {'vectorizer': vec, 'matrix': matrix, 'ids': corpus_ids} def top_k(index: dict, query_tokens: list, k: int = 10): """查询文本走同一套 vectorizer,保证词表对齐""" q_vec = index['vectorizer'].transform([' '.join(query_tokens)]) sims = linear_kernel(q_vec, index['matrix']).flatten() order = np.argsort(-sims)[:k] return [(index['ids'][i], round(float(sims[i]), 4)) for i in order if sims[i] > 0]

linear_kernel在 TF-IDF 向量上等价于余弦相似度,但底层是矩阵运算,比在 Python 里循环调cosine_sim快两个数量级。query_tokens必须用和建索引时同一个tokenize函数处理,否则词表对不上,相似度会莫名其妙偏低——这是二次开发里最常见的翻车点,很多人换了自己的分词逻辑,忘了同步索引侧。

第二步是算法可插拔。第 4 章那个ALGO_REGISTRY就是为这个准备的,加一条语义向量路径只需要新增一个@register函数,把文本送进句向量接口拿到定长向量,再算余弦。前端下拉框的选项从ALGO_REGISTRY.keys()动态渲染,加算法不用改模板。这里有个细节要注意:句向量返回的是numpy.ndarray而不是稀疏 dict,cosine_sim收不了,得写个适配函数把它转成{索引: 权重}的形式,或者单独写一个基于数组的余弦实现。

第三步是验证。改完算法不能只看页面上分数变没变,得有个可复现的评估集。我的习惯是准备五十对人工标注过的文本,相似的标 1、不相似的标 0,跑一遍算准确率和 TopK 的命中率:

改造点改动文件主要风险
换分词或加词性过滤algo/preprocess.py索引侧与查询侧不同步,分数整体偏低
加语义向量算法algo/similarity.py、core/views.py向量格式不统一,余弦函数报类型错误
批量 TopK 检索新增algo/retrieval.py语料更新后索引未重建,返回过期结果
结果缓存core/views.py或独立 cache 模块缓存键漏了算法名,切算法后拿到旧分数

这张表里最容易被忽略的是最后一行,缓存键必须带上「算法名 + 文本哈希 + 语料版本」,少一个维度就会出现「换了算法分数没变」的玄学问题,排查起来能耗掉一下午。

从那以后我每次动向量化或分词逻辑,都强制把索引重建一遍再验证,不看页面上那一个数字,而是跑完整评估集对比前后准确率。做文本相似度这类系统,分数好看不难,难的是分数稳定可信。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 21:39:08

e2e测试的本质:以用户真实行为为标尺的交付契约

1. “e2e”不是缩写谜题&#xff0c;而是工程交付的临门一脚很多人第一次在项目文档里看到“e2e”&#xff0c;下意识会去查全称——End-to-End&#xff1f;没错&#xff0c;但这个答案毫无价值。就像你问“螺丝刀是啥”&#xff0c;答“拧螺丝的工具”一样正确却 useless。真正…

作者头像 李华
网站建设 2026/10/27 21:39:01

AI辅助空间改造全流程:从需求梳理到落地避坑的实操指南

空间改造这件事&#xff0c;过去十年我经手过不少案例&#xff0c;从几十平的老破小到挑高复式&#xff0c;踩过的坑比画过的图纸还多。2024年最明显的变化是&#xff0c;AI从一个“锦上添花”的噱头&#xff0c;变成了真正能参与决策的工具。以前业主拿着户型图问我“这里能不…

作者头像 李华
网站建设 2026/10/27 21:38:46

Redis实现高性能消息队列的技术方案与实践

1. Redis作为消息队列的可行性分析在分布式系统架构中&#xff0c;消息队列是解耦生产者和消费者的重要组件。虽然Kafka、RabbitMQ等专业消息中间件更为常见&#xff0c;但Redis凭借其高性能和丰富的数据结构&#xff0c;完全可以胜任轻量级消息队列的角色。Redis实现消息队列的…

作者头像 李华
网站建设 2026/10/27 21:38:13

OpenMontage:开源批量拼图与视频画面墙生成实战指南

做内容创作的朋友&#xff0c;尤其拍视频或者做运营的&#xff0c;应该都经历过这种场景&#xff1a;手里一堆素材&#xff0c;几十张甚至上百张图&#xff0c;想拼成一张完整的封面墙或者九宫格&#xff0c;找工具、调间距、一张张拖进去对齐&#xff0c;一弄就是一下午。Open…

作者头像 李华
网站建设 2026/10/27 21:38:00

局部子结构与补语建模的药物-药物相互作用预测

第一次在一篇分子图模型的标题里看到"补语"两个字&#xff0c;我愣了几秒——这不是语法课上的概念吗&#xff1f;读完才明白&#xff0c;作者是借语言学的说法&#xff1a;一句话里关键词是主干&#xff0c;补语交代时间、地点、方式&#xff1b;一个药物分子里&…

作者头像 李华
网站建设 2026/10/27 21:37:38

VLP-16激光雷达数据解析与故障排查权威指南

简介&#xff1a;本资源是Velodyne官方发布的VLP-16激光雷达用户手册&#xff08;.docx格式&#xff09;&#xff0c;面向自动驾驶、机器人感知、三维建模等领域的工程师、高校研究人员及嵌入式开发者&#xff0c;解决LiDAR设备安全上电、硬件安装、以太网/GPS连接、点云数据解…

作者头像 李华