简介:一份基于Django与Python的招聘信息推荐系统毕业论文,面向计算机相关专业毕业生、毕业设计指导老师及需要构建个性化推荐系统的开发者,重点解决招聘信息数量激增下的高效管理、求职者与岗位精准匹配等现实问题。论文围绕协同过滤算法展开,从研究背景、拟解决问题到论文结构均有清晰阐述;详细介绍了Python语言特性、Django框架的MVC架构、MySQL数据库存储及B/S结构部署环境,并完成了系统可行性分析、需求分析、数据库设计、模块划分与功能测试。整体内容既包含理论分析,也涵盖系统实现与优化思路,可作为撰写毕业设计论文、设计招聘推荐系统或学习协同过滤算法落地的综合参考。资源为单个DOCX文档,大小仅3.83MB,内容结构清晰,便于按章节查阅和引用。目前已有331人学习下载,适合正在准备毕业论文或从事招聘类系统开发的人群借鉴。
1. 协同过滤在招聘信息推荐系统里的定位:从规则匹配到行为预测
在招聘信息推荐系统里,一种常见的偷懒做法是:按职类筛选、按地域过滤、按企业规模排序,结果就是 Java 工程师看到 Java 岗位、产品经理看到产品岗位,表面没错,但这种规则不会因为你多看了一家创业公司就调整结果。协同过滤算法正好相反:它把用户和职位的交互行为当作输入,找到投递行为相似的用户,或经常被一起查看的职位,让推荐从分类匹配变成行为预测。放进 Django 项目里,这套系统不需要 Hadoop 或 Spark 集群,用 Python 的 NumPy 配合 management command 就能跑通,这也是很多招聘类毕业设计的切入点。这篇文章把算法选型、数据建模、离线计算、在线接口整条链路过一遍,给出可以直接复现的工程方案。
2. 协同过滤在招聘推荐里的算法选型与相似度矩阵设计
2.1 从“用户-职位”交互矩阵看协同过滤的输入
协同过滤不做内容分析,它只关心一件事:谁对哪些职位产生了行为。无论是浏览、收藏还是投递,这些行为最终都能抽象成一个稀疏矩阵,行是用户,列是职位,单元格是用户在某个职位上的有效分值。招聘场景里,这个矩阵会有两个显著特点:一是极度稀疏,一个求职者一天最多浏览几十个职位,而平台职位可能有几十万;二是正负样本不均衡,绝大多数职位没有行为,不能简单地把无行为当成负样本,因为用户可能根本没看到这个职位。
构建这个矩阵时,最直接的做法是从 Django 的 Behavior 表聚合出 user_id、job_id 和加权分值。我一般用 pandas 一步完成,同时也为后续的相似度计算准备好数据格式。
import pandas as pd df = pd.DataFrame(Behavior.objects.values('user_id', 'job_id', 'behavior_type')) weight_map = {'view': 1.0, 'fav': 2.0, 'apply': 4.0} df['weight'] = df['behavior_type'].map(weight_map) # 同一用户对同一职位产生多种行为,取最大权重而不是累加 df = df.groupby(['user_id', 'job_id'], as_index=False)['weight'].max() matrix = df.pivot_table( index='user_id', columns='job_id', values='weight', fill_value=0 ).astype('float32')这段代码的逻辑是:先查询出所有行为记录,行为类型映射为分值,然后用 pivot_table 转成用户-职位矩阵。取 max 而不是累加,是因为一次投递已经表达了明确意向,再叠加浏览记录会让同一行为的重复曝光失去比例。fill_value=0 会把缺失行为变成 0,这在相似度计算里要特别留意:0 不是负样本,它只是“没有观察到行为”。
提示:如果行为数据量很大,不要直接在视图里跑 pivot_table,应该放到离线任务里,计算完成后再把相似度矩阵导入 Redis 或数据库,在线接口只做读取。
2.2 用户CF与物品CF:招聘场景选哪个
协同过滤分两大类:基于用户的 UserCF,找的是和我行为相似的邻居用户,邻居喜欢的职位我可能也喜欢;基于物品的 ItemCF,找的是行为上经常一起出现的职位,看了 A 职位的人也会看 B 职位。两者输出的推荐语义完全不同,选型要看招聘系统里的用户规模和职位更新速度。
| 维度 | UserCF | ItemCF |
|---|---|---|
| 计算对象 | 用户与用户的相似度 | 职位与职位的相似度 |
| 适合体感 | 用户少、职位多 | 用户多、职位相对稳定 |
| 实时性 | 新增行为需重算用户向量 | 新增职位可增量计算 |
| 冷启动 | 新用户没有邻居,无法推荐 | 新职位无关联,也没法推 |
| 解释性 | 像“同类人也在投递” | 像“看过这个职位的还看了” |
招聘系统里,职位库的增长速度远大于活跃用户数,且一个职位上线一两个月就下架,职位生命周期短。这种情况如果单用 ItemCF,每天都有大量新职位进入冷启动池;如果单用 UserCF,用户的求职意图变化快,上周在找后端,这周可能在看产品,邻居关系沿用旧数据会失真。我一般会把两者做加权混合:主推 UserCF 结果,将 ItemCF 结果作为补充,用在线接口里的一个参数控制比例。
落地时不需要两个模型都独立训练。可以把用户历史行为向量和职位行为向量统一出来,相似度函数写一套,只是 row 和 column 互换。这样代码维护成本低,论文里也多了一个可对比的实验维度。
2.3 余弦相似度的实现与热门职位降权
招聘行为是隐式反馈,用户没有显式打分,皮尔逊相关系数会因为没有共同评分而大量失效;而 Jaccard 只看是否发生行为,会忽略行为权重。所以我一般选余弦相似度,它既能使用分值向量,又天然对用户行为数量做了归一化。
import numpy as np def compute_similarity(matrix): matrix = matrix.to_numpy(dtype='float32') # 行向量归一化,避免行为多的用户主导相似度 norm = np.sqrt(np.sum(matrix ** 2, axis=1)) norm[norm == 0] = 1 matrix_normed = matrix / norm[:, np.newaxis] # 余弦相似度 = 归一化后的内积 return matrix_normed @ matrix_normed.T user_sim = compute_similarity(matrix) job_matrix = matrix.T job_sim = compute_similarity(job_matrix)这里把用户矩阵转置后直接套同一个函数,就能得到职位相似度矩阵。矩阵稀疏时直接用矩阵乘法可能撑不住大矩阵,论文规模通常几千用户、几千职位没有问题;如果数据量上到十万级,可以改成只计算“有过共同行为”的 pair,用 dict 保存稀疏相似度。
热门职位降权是另一个容易被忽略的细节。招聘平台上头部大厂职位天然获得更多曝光,导致它和谁相似度都高。按原始权重计算,推荐列表会被几个热门职位占满。降权的常用做法是用“行为用户数”的对数作为惩罚项,即每个职位在相似度计算前先除以 log(1 + 行为用户数)。
popularity = matrix.astype(bool).sum(axis=0) penalty = np.log1p(popularity) matrix = matrix / penalty这一段核心是:先对列加权,再走同一套相似度计算。加了降权后,小众但匹配度高的职位才有机会出现在 Top-N 里,这也是论文里可以单独写一节的对比实验。
3. 用 Django 建立可采集行为数据的推荐数据模型
3.1 核心模型字段与行为权重设计
协同过滤依赖行为数据,所以 Django 项目里最该先设计好的不是推荐算法模块,而是行为表。招聘系统里,求职者的一个完整动作链路是:搜索职位、浏览详情、收藏、投递简历。这四个动作对推荐的价值不一样,浏览只表示“有点兴趣”,收藏是明确的意向,投递则是整条链路里权重最高的信号。我建议在初始版本里先做一张行为表,把行为类型、用户、职位、发生时间都记下来,后面想扩展搜索词消费行为也方便。
# models.py from django.conf import settings from django.db import models class Job(models.Model): title = models.CharField(max_length=128) company = models.CharField(max_length=128) city = models.CharField(max_length=32, db_index=True) salary_min = models.PositiveIntegerField(default=0) salary_max = models.PositiveIntegerField(default=0) requirement = models.TextField(blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [ models.Index(fields=['is_active', '-created_at']), ] class Behavior(models.Model): VIEW = 'view' FAV = 'fav' APPLY = 'apply' BEHAVIOR_TYPE_CHOICES = [ (VIEW, '浏览'), (FAV, '收藏'), (APPLY, '投递'), ] user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE) job = models.ForeignKey(Job, on_delete=models.CASCADE) behavior_type = models.CharField(max_length=8, choices=BEHAVIOR_TYPE_CHOICES) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [ models.Index(fields=['user', 'behavior_type', 'created_at']), ]参数说明:user 字段允许 null 是用来承接匿名浏览记录的,未登录用户可以用 session_key 存在另一个字段里;job 的 is_active 默认 True,计算训练样本时要显式过滤掉已下架职位,否则推荐接口会把不可投递的职位也返出去。behavior_type 用短字符串存,比用整型数字可读性好,模型建好后在 Django Admin 里就能直接筛选统计。
行为权重不放进数据库里,而是作为配置项存在推荐模块中。因为同一行为在不同阶段的含义可能变化,比如投递权重是否要超过收藏,可以在离线实验里调。常见做法是投递 4.0、收藏 2.0、浏览 1.0,如果发现最终的推荐列表过于集中在用户投过类似岗位的品类上,就降低投递权重、提升收藏权重。
| 行为类型 | 权重 | 推荐语义 |
|---|---|---|
| 浏览 | 1.0 | 浅层兴趣,量大但噪声多 |
| 收藏 | 2.0 | 明确意向,可解释性强 |
| 投递 | 4.0 | 强意图,正样本价值最高 |
行为权重表在代码实现里只是几个常数,但在论文实验里是一个要解释清楚的参数。不同的权重组合会直接改变相似度矩阵的分布,写毕业论文时可以把这三组数值当作对照实验的变量。
定义好模型后,记得先执行python manage.py makemigrations recommendation和python manage.py migrate。如果要新增一个推荐模块 app,推荐直接命名为 recommendation,把 models、middleware、signals 分开模块管理。
3.2 用中间件记录匿名浏览行为
登录用户的行为好办,直接在前端页面的“职位详情”接口里写一条 Behavior 就能记录。问题在匿名用户:用户还没注册,但已经浏览了一堆职位,如果直接丢掉就丢失了非常重要的早期偏好。Django 的中间件是在每个请求进入视图前执行的一段代码,适合在这时提取 session 标识,并在浏览详情页时异步写入一条 view 行为。
# middleware.py from django.utils import timezone from django.contrib.sessions.models import Session from .models import Behavior from jobs.models import Job class BrowserBehaviorMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) if request.path.startswith('/jobs/') and request.method == 'GET': job_id = request.path.strip('/').split('/')[-1] if job_id.isdigit() and getattr(request, 'user', None): Behavior.objects.create( user=request.user if request.user.is_authenticated else None, job_id=int(job_id), behavior_type=Behavior.VIEW, ) return response这段中间件在响应返回之后才写库,不会阻塞页面渲染。实际项目中还要加一个去重逻辑:同一用户同一职位在 30 分钟内只记一次,避免刷新页面导致权重被刷高。可以先用 Behavior.objects.filter 判断命中就不重复写入,判断条件加上 created_at__gte=timezone.now() - timedelta(minutes=30) 即可。
注意:中间件里直接写数据库会把响应时间拉长,生产环境要改成发送到 Redis 队列,再由后台任务消费。毕设阶段这样直接写没关系,但要区分清在线采集和离线计算两条链路。
3.3 用 Django 信号给投递与收藏加权
投递行为发生在简历投递接口里,如果在视图函数里手动调一次 Behavior.objects.create,一旦投递逻辑改动,非常容易漏写行为日志。用 Django 的 post_save 信号监听投递模型,任何入口只要创建了投递记录,行为表就会自动多一条 apply 记录。
# signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Behavior, Application @receiver(post_save, sender=Application) def record_apply_behavior(sender, instance, created, **kwargs): if not created: return Behavior.objects.create( user_id=instance.user_id, job_id=instance.job_id, behavior_type=Behavior.APPLY, )信号的好处是把业务动作和行为采集解耦。投递、收藏都各自走自己的业务逻辑,推荐系统只需要订阅结果即可。如果后面增加“简历被查看”之类的逆向往返行为,同样可以加一个 Invitation 模型,再挂一个信号。这里用到了 Django 信号机制,注意在 AppConfig 的 ready 方法里 import signals 模块,确保 Django 启动时能注册上。
4. 用 Django 实现协同过滤推荐的离线计算与在线接口
4.1 用 Management Command 离线计算相似度矩阵
推荐模型的训练不适合放在 Web 请求里跑,特别是相似度矩阵计算对内存和时间的要求,会让页面响应变成灾难。Django 的 management command 是标准做法:写一个python manage.py compute_similarity,定时执行,把计算结果落到存储层。
# recommendation/management/commands/compute_similarity.py from pathlib import Path import numpy as np import pandas as pd from django.core.management.base import BaseCommand from django.conf import settings from jobs.models import Behavior, Job class Command(BaseCommand): help = '计算用户/职位相似度矩阵并写入缓存' def handle(self, *args, **options): df = pd.DataFrame( Behavior.objects.filter(job__is_active=True).values( 'user_id', 'job_id', 'behavior_type' ) ) if df.empty: return weight_map = {'view': 1.0, 'fav': 2.0, 'apply': 4.0} df['weight'] = df['behavior_type'].map(weight_map) df = df.groupby(['user_id', 'job_id'])['weight'].max().reset_index() matrix = df.pivot_table( index='user_id', columns='job_id', values='weight', fill_value=0 ).astype('float32') # 热门职位降权、归一化、计算相似度 pop = matrix.astype(bool).sum(axis=0) matrix = matrix / np.log1p(pop) norm = np.sqrt(np.sum(matrix ** 2, axis=1)) norm[norm == 0] = 1 matrix_normed = matrix / norm.values[:, None] job_sim = matrix_normed.T @ matrix_normed # 物品相似度 np.save(settings.BASE_DIR / 'data' / 'job_sim.npy', job_sim) job_ids = matrix.columns.tolist() with open(settings.BASE_DIR / 'data' / 'job_ids.txt', 'w') as f: f.write('\n'.join(map(str, job_ids)))这段命令把三件事合在一起:读库、算相似度、落盘。物品相似度矩阵是“职位数 × 职位数”,如果职位数在 5000 以内,直接存为 NumPy 文件完全够用;超过 1 万后文件会到几百兆,建议把矩阵分片存进 Redis,或者只保留每个职位 top 50 的相似邻居。命令结束时打印出 job_sim 的 shape 和内存占用,便于第一次跑的时候就能发现问题。
Job 过滤条件用了Behavior.objects.filter(job__is_active=True),这样能跳过已下架的职位,防止相似度矩阵里堆积无效列。注意这里没有真的删除 Job 对象,而是用 is_active 标记下架,原因很简单:被删除的职位会导致外键关联的行为记录一起被级联清理,而离线计算需要完好的历史行为,所以生产环境一般不用 delete 处理下架。
4.2 相似度矩阵的存储与更新策略
相似度矩阵不是每次请求都重新计算,它应该有一个相对稳定的周期。招聘场景里,职位上新和用户行为变化都很快,一天算一次是合理的。如果写论文要对比不同周期的效果,可以把计算周期作为一个配置项暴露出来。
| 存储方式 | 优点 | 缺点 | 适用规模 |
|---|---|---|---|
| NumPy npy 文件 | 加载快、实现简单 | 不适合并发写 | 职位数 < 1 万 |
| Redis + pickle | 支持分布式读取 | 需要额外依赖 | 1 万 ~ 10 万 |
| 数据库表存稀疏 pair | 便于可视化验证 | 读取超时 | 100 万以上稀疏矩阵 |
我个人的习惯是:毕设和中小项目用 npy 文件;要演示在线效果时再切到 Redis。npy 文件在 Django 里用 np.load 加载后放在进程缓存里,避免每个请求都读一次磁盘。更新策略上用 cron 或 Celery Beat 定时执行 management command,执行频率放在 Django settings 里,便于在论文里写明实验周期。
4.3 在线推荐 API:输入 user_id 返回 job 列表
在线部分要回到 Django 视图。推荐接口接收 user_id,先用该用户最近的行为向量和职位相似度矩阵做加权,生成候选职位列表,再过滤掉已经投递或已经下架的职位。
# recommendation/services.py import numpy as np class Recommender: def __init__(self, job_sim, job_ids): index_by_id = {job_id: i for i, job_id in enumerate(job_ids)} self.job_sim = job_sim self.job_ids = job_ids self.index_by_id = index_by_id def recommend_for_user(self, user_id, user_behavior_df, top_n=10): user_vec = np.zeros(len(self.job_ids)) weights = {'view': 1.0, 'fav': 2.0, 'apply': 4.0} for row in user_behavior_df.itertuples(): if row.job_id not in self.index_by_id: continue idx = self.index_by_id[row.job_id] user_vec[idx] = max(user_vec[idx], weights[row.behavior_type]) scores = user_vec @ self.job_sim # 排除已产生行为的职位 for row in user_behavior_df.itertuples(): if row.job_id in self.index_by_id: scores[self.index_by_id[row.job_id]] = -np.inf top_idx = np.argsort(scores)[::-1][:top_n] return [ {'job_id': self.job_ids[i], 'score': float(scores[i])} for i in top_idx if scores[i] != -np.inf ]推荐算法的核心步骤是向量和矩阵相乘,本质上是把用户所有历史行为的职位向量相加,再按相似度排序。由于用户行为天然稀疏,这个计算量非常小,单个请求通常几毫秒完成。排除已行为职位这一步很重要,不然推荐列表会频繁把用户已经投递过的职位再推回来。
# views.py from django.http import JsonResponse from django.views.decorators.http import require_GET from django.contrib.auth.decorators import login_required @require_GET def recommend_jobs(request): if not request.user.is_authenticated: return JsonResponse({'code': 401, 'msg': '请先登录'}, status=401) user_id = request.user.id recommender = get_recommender() # 进程内单例 behavior_df = get_user_behavior(user_id) if behavior_df.empty: return JsonResponse({'code': 0, 'items': get_hot_jobs()}) items = recommender.recommend_for_user(user_id, behavior_df) response = JsonResponse({'code': 0, 'items': items}) return response视图里的 get_recommender 可以直接用 functools.lru_cache 包一层,进程内只加载一次相似度矩阵。没有行为数据的新用户走热门兜底,这个分支就是冷启动策略在接口层的实现入口。JsonResponse 默认使用 content_type='application/json',不需要额外设置。
5. 冷启动、评估指标和毕业设计论文里的数据验证
5.1 新职位与新用户的冷启动兜底
新用户没有行为,协同过滤无法给出任何结果。这时候常见的兜底方案是按城市、技能关键词或热门职位返回一个初始列表,等用户浏览了几个职位后再切到协同过滤。新职位则相反,它没有历史关联记录,解决办法是用规则先顶上:给新职位打一个“新职”权重,在推荐列表里保留一定比例,获得少量行为后再进入相似度矩阵。最简单的实现是在推荐接口里按0.7 * 协同过滤得分 + 0.3 * (新职基础分 + 1/log1p(曝光次数))计算最终得分。
5.2 用离线评估指标验证推荐效果
论文里最常用的三个指标是精确率、召回率和覆盖率。把用户历史行为按时间切分,前 80% 作为训练集,后 20% 作为测试集,用模型给测试用户推荐 Top-10,然后统计命中数。
def evaluate(train_df, test_df, recommender, k=10): hits = 0 total = 0 user_ids = test_df.user_id.unique() for user_id in user_ids: test_jobs = set(test_df[test_df.user_id == user_id].job_id) if not test_jobs: continue rec_jobs = recommender.recommend_for_user(user_id, train_df[train_df.user_id == user_id], top_n=k) rec_jobs = {item['job_id'] for item in rec_jobs} hits += len(rec_jobs & test_jobs) total += len(test_jobs) precision = hits / (len(user_ids) * k) recall = hits / total return {'precision': precision, 'recall': recall}这里推荐接口已经排除历史行为职位,所以直接取交集就能统计命中。精确率分母是“用户数 * K”,召回率分母是测试集职位总数。两个指标互相牵制,调参时建议把权重映射和相似度阈值分开对比。
5.3 用 Django 命令导出论文图表数据
论文里的图表要求还原实验过程,最好用一个命令把多组参数跑出来的指标写入 CSV。导出 CSV 时可以直接用 StreamingHttpResponse 逐行写,同时设置 content_type='text/csv' 和 Content-Disposition 的 attachment 参数,这样拿到的响应体就能在 Excel 里打开。
class Command(BaseCommand): def handle(self, *args, **options): for top_n in [5, 10, 20]: result = evaluate(train_df, test_df, recommender, top_n) self.stdout.write(f'{top_n},{result["precision"]:.4f},{result["recall"]:.4f}')对两个模型做对比时,先跑 UserCF 再跑 ItemCF,导出的数据就是论文里那张对比表的基础。做统计分析时,建议取多个随机时间切分,分别计算两组模型的精确率差值,再做符号秩检验,p 值小于 0.05 才写“算法改进显著”。CSV 里出现 precision 逐级下降、coverage 上升时,先检查测试集划分是否一致,不要急着调推荐参数。
本文还有配套的精品资源,点击获取