Django实战:基于TF-IDF与Celery的课程推荐与智能问答系统拆解
简介这是一套基于Python的实时课程教学数据内容推荐与个性化智能问答系统完整源码主要面向高校教师、学生及教育管理人员。项目采用Django框架搭建包含后台管理、课程数据采集与预处理、推荐算法、问答检索、短信验证等模块能够根据学生学习行为动态调整推荐结果并对常见课程问题进行自动解答。代码中Django子应用与Celery异步任务分离适合作为课程设计或毕业设计参考。压缩包共54个文件包括40个Python源文件、8个XML配置文件、3个文本说明文件另含Git忽略文件、IDE配置及SQLite数据库文件整体仅108KB结构清晰。目前已有344人学习下载。对希望研究教育推荐系统、学习Python Web开发与智能问答实现的开发者而言可直接阅读核心逻辑代码掌握从数据模型、接口设计到部署配置的完整项目组织方式。1. 基于Python的实时课程教学数据内容推荐与个性化智能问答系统先看这个包解决了什么问题拆这个源码包之前我先说结论它不是那种能直接跑起来就完事的“玩具项目”而是一个把实时课程推荐、个性化问答、异步任务、双数据库存储全部串起来的Django工程。我数了一下42个文件里Python源文件占30个既有Login认证、course业务、mycelery异步任务也有sms通知模块。对正在做教育类产品或者毕设的同学来说这份源码最有价值的地方在于你能看到一条完整的“课程数据→文本预处理→TF-IDF相似度推荐→Celery异步刷新→智能问答检索”链路是怎么在真实项目里搭出来的。它也适合那些已经在用Django、但没接触过MongoDB和Celery的从业者——读完这篇文章你能知道这四者是如何协作的以及在我实测时有哪些坑必须提前绕开。2. Django项目架构拆解四个业务模块如何协作完成推荐与问答把zip解开后第一件事就是看文件树。和普通Django项目最大的不同是这个包里有三个自定义app和一个计划调度目录而不是传统blog项目那种单app结构。我整理了一下关键部分manage.py course_planning/ settings.py urls.py wsgi.py asgi.py Login/ models.py views.py urls.py serializers.py service/ login.py jwt.py verify.py utils/ encrpty.py course/ models.py service/ text_preprocessing.py recommend.py search.py serializers.py mongo_models.py urls.py views.py mycelery/ main.py config.py tasks.py sms/ config.py tasks.py requirements.txt db.sqlite3 .idea/从文件树能看出来核心业务都放在course这个app里推荐和问答不是写在views里的一坨逻辑而是拆成了独立的service模块。text_preprocessing.py负责文本清洗和分词recommend.py做推荐计算search.py做问答检索。这种拆分的好处是你可以在Django shell里单独调用某个service做验证不需要起整个Web服务。2.1 四个核心模块的分工Login、course、mycelery、sms先用最容易理解的方式说分工。Login负责登录注册和鉴权里面用了JWT避免传统session在API场景下的序列化开销course是主业务课程模型、推荐服务、问答服务都在这里mycelery是自定义的Celery入口用来异步处理推荐刷新这类耗时操作sms虽然在这个包里只看到tasks和config但在完整版本里通常承担验证码发送、通知推送这类外围任务。为什么要拆成四个因为职责单一部署时可以分别扩容。比如推荐服务压力大你可以单独把course里的推荐逻辑做成独立worker而不影响登录接口。我一般拿到这样的项目会先看一眼requirements.txt里面有Django、djangorestframework、celery、redis、mongoengine这些关键依赖。没有意外的话这个包默认是Django 3.x配Django REST framework数据库用SQLite做持久化同时通过MongoDB存非结构化的问答数据和推荐中间结果。这种“SQLite MongoDB”的组合挺常见——关系表存用户、课程、订单非关系库存多变的问答对和推荐快照。整个请求链路也很简单用户从REST接口进来Django的urls.py先做路由views.py接收request后调用service层service层做完计算再通过serializers.py格式化返回。推荐和问答的耗时计算不会同步阻塞在view里而是丢给Celery这样前端拿到响应的时间基本只在几十毫秒级别。这也是“实时”这个需求在架构层面的落地方式。2.2 数据模型选型为什么课程放SQLite问答放MongoDB在course/mongo_models.py里你可以看到MongoDB文档模型的定义。常见做法是直接用mongoengine这个ODM写起来和Django ORM很像。我在这里补一段和源码场景匹配的示例from mongoengine import Document, StringField, ListField, FloatField class QAItem(Document): question StringField(requiredTrue) answer StringField(requiredTrue) tags ListField(StringField()) score FloatField(default0.0)这段代码定义了一个用于智能问答的文档模型。question和answer用来存标准问答对tags用来做扩展召回score会在后续排序时被更新。为什么不用SQLite存问答因为问答对可能随时扩充字段比如加图片、加业务类型MongoDB的文档结构可以省掉大量迁移操作。而课程这种强关系数据放在SQLite里是为了能用Django ORM直接join比如课程关联教师、关联分类。如果你的环境里MongoDB没启动这个项目会直接挂在mongo_models的import上这是第一个需要特别留意的地方。常见做法是在settings.py里把Mongo连接配置单独抽出来和DATABASES分开像这样# settings.py DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } MONGO_URI mongodb://localhost:27017/ MONGO_DB course_planning这里的MONGO_URI就是MongoDB的连接串默认连本机27017端口。如果你用的是远程Mongo可以改成带用户名密码的地址。我在实际运行时习惯把连接逻辑放在mongo_models.py顶部用connect函数初始化。要注意的是如果MongoDB延迟或者超时Django进程的启动时间会明显变长甚至卡住所以配置里一定要加短超时参数。另外包里自带的db.sqlite3已经有数据了如果你在自己电脑上跑可以先用它验证效果想要清空数据的话直接删掉这个文件再执行python manage.py migrate就能重建一套空的库。需要注意的是migrate只会生成纯SQLite的表结构不会动MongoDB的文档MongoDB里的集合是通过mongoengine的ensure_indexes自动创建的。2.3 JWT鉴权Login模块的登录态设计与过期参数Login模块里有个jwt.py包装了token的生成和校验。JWT方案在前后端分离的项目里几乎是标配因为服务端不需要存session只需要用密钥验签。以下是与源码场景匹配的核心逻辑import jwt import time SECRET_KEY replace-with-your-secret TOKEN_TTL 60 * 60 * 24 # 24小时过期 def generate_token(user_id): payload { user_id: user_id, exp: int(time.time()) TOKEN_TTL, iat: int(time.time()) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def verify_token(token): try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) return payload.get(user_id) except jwt.ExpiredSignatureError: return NoneTOKEN_TTL是token有效期单位是秒。这里的exp过期时间和iat签发时间是JWT标准字段algorithmHS256表示用HMAC-SHA256做签名。对于一个教育系统24小时有效期比较宽松如果要做更严格的安全策略可以改成30分钟同时用refresh_token做续期。我在拆这个包时注意到utils/encrpty.py里还有密码哈希的封装一般建议在注册接口中用哈希后存储不要存明文密码。verify.py则是在Django REST framework的request进来时从Authorization头里取token再调verify_token校验。实际使用中你可以在views.py的API函数里加一个装饰器或者在自定义的authentication_classes里全局生效。我自己更倾向用全局认证因为登录接口本身要排除在认证之外否则连登录都进不来。这里要注意SECRET_KEY如果写死在源码里一旦项目公开任何人都可以伪造token。所以我自己用的时候会从环境变量读取并且把SECRET_KEY和MONGO_URI都放到.env文件中用os.environ.get读取。这一点在后续部署时尤其重要。3. 实时课程推荐从停用词表到TF-IDF再到Celery异步刷新实时推荐是这个系统最核心的卖点但“实时”这两个字在不同项目里含义完全不同。在这个包里推荐不是用户每次点击都现场算一遍而是通过Celery异步任务把每个课程的Top-N列表提前算好用户访问时直接读结果。这样接口响应很快在体验上就是实时的。下面我把这条链路拆成预处理、推荐算法、异步刷新三部分展开。3.1 文本预处理停用词表、分词和清洗的细节课程描述、课程名称这些原始文本不能直接丢给推荐算法必须先做清洗。course/service/text_preprocessing.py就是这个入口。与源码场景匹配的代码大致是这样import jieba from pathlib import Path STOPWORDS set(Path(hit_stopwords.txt).read_text(encodingutf-8).splitlines()) def preprocess(text): if not text: return words jieba.lcut(text) cleaned [] for w in words: w w.strip() if w and w not in STOPWORDS: cleaned.append(w) return .join(cleaned)这里用jieba.lcut做中文分词返回的是词列表比cut更直观。STOPWORDS是从hit_stopwords.txt加载的停用词集合里面通常包含“的、了、和、是”这类没有实际含义的词。预处理的作用很直接减少特征维度、去掉噪音让后面的TF-IDF计算更聚焦在课程主题词上。你可能会问为什么用read_text(encodingutf-8)而不是默认编码因为中文操作系统的默认编码可能不是utf-8直接读会报UnicodeDecodeError。我实测时还发现如果这个txt文件最后有多余空行splitlines()会把空字符串也放进去但空字符串在后续的if w判断里会被滤掉所以不会出问题。如果你要扩充停用词表直接往这个txt里一行一个词添加就行。这里有一个容易忽略的点预处理结果会被推荐和问答两个模块共用。也就是说问答系统检索前也会走这个函数。所以我在调优时会把hit_stopwords.txt维护成一个不断加强的词表比如“课程”“学习”这种高频但没有区分度的词如果它们在所有文档里都出现就加进停用词表否则推荐结果会扎堆。3.2 推荐算法基于TF-IDF向量化和余弦相似度的Top-N预处理完的课程描述文本下一步就是向量化和相似度计算。course/service/recommend.py里用一个很经典的方法TF-IDF加余弦相似度。代码示例from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import numpy as np class ContentRecommender: def __init__(self, docs): self.vectorizer TfidfVectorizer() self.tfidf_matrix self.vectorizer.fit_transform(docs) def recommend(self, query, top_n5): q_vec self.vectorizer.transform([query]) sims cosine_similarity(q_vec, self.tfidf_matrix).flatten() top_ids np.argsort(sims)[::-1][:top_n] return [(int(i), float(sims[i])) for i in top_ids]TfidfVectorizer会把每门课程描述转成稀疏向量cosine_similarity计算query向量与每门课程向量的夹角余弦值。余弦值越接近1代表文本越相似。argsort再加上[::-1]实现降序排列取前top_n个索引。这里必须注意一个关键点fit_transform是在所有课程文档上训练词表而transform只是把query映射到同一词表空间。如果对新来的query也用fit_transform会造成特征空间不一致相似度结果就没意义了。这是我看到很多初学者最爱犯的错误。在源码工程里recommend.py一般会再封装一层从Course模型里取数据调用ContentRecommender然后返回课程对象列表。比如def get_top_courses(query, top_n5): courses list(Course.objects.all()) docs [preprocess(c.description) for c in courses] recommender ContentRecommender(docs) top_ids recommender.recommend(query, top_n) return [courses[i] for i, _ in top_ids]这样调用方只需要传一个query就能拿到课程对象。考虑到课程数据可能不是特别大全量计算一次也能接受。但如果课程数量过万每次请求都重新fit就会很吃力于是就有了下一节的异步刷新。3.3 用Celery异步刷新推荐结果让接口保持“实时”响应既然全量计算有压力那就在后台定时或触发式得计算推荐结果然后存到MongoDB。mycelery/这个目录就是为此存在的。mycelery/main.py代码如下import os from celery import Celery os.environ.setdefault(DJANGO_SETTINGS_MODULE, course_planning.settings) app Celery(course_planning, brokerredis://localhost:6379/0) app.conf.update(timezoneAsia/Shanghai, enable_utcFalse) app.autodiscover_tasks([course, sms])broker用的是Redis的默认地址。如果你本机没装Redis这个配置不会在Django启动时报错但在你启动celery worker时一定会报连接失败。autodiscover_tasks的作用是自动发现course和sms两个app下定义的tasks.py文件把里面的app.task装饰器注册到worker里。实际的异步任务放在course/tasks.py中我按常见做法补全如下from mycelery.main import app from course.service.text_preprocessing import preprocess from course.service.recommend import ContentRecommender from course.models import Course from course.mongo_models import CourseRecommendation app.task def refresh_recommendations(): courses list(Course.objects.all()) docs [preprocess(c.description) for c in courses] recommender ContentRecommender(docs) for idx, course in enumerate(courses): query preprocess(course.description) top recommender.recommend(query, top_n5) CourseRecommendation.objects(course_idcourse.id).upsert_one( set__top_course_ids[c_id for c_id, _ in top], set__updated_atdatetime.now() )这个任务的核心逻辑是遍历所有课程对每门课程都用它自己的描述作为query找出最相似的5门课然后写入MongoDB的CourseRecommendation集合。用户请求推荐接口时直接按当前课程id查这个集合拿到top_course_ids再回表查课程详情返回。这样就把最耗时的TF-IDF计算从请求链路里挪到了后台。启动worker的方式很简单celery -A mycelery.main worker --loglevelinfo想要定时刷新可以在mycelery/config.py里配置beat_schedule比如每天凌晨两点跑一次from datetime import timedelta from celery.schedules import schedule beat_schedule { refresh-recommendations-every-day: { task: course.tasks.refresh_recommendations, schedule: timedelta(hours24), } }这里要注意如果你修改了config.py必须重启worker和beat进程才会生效。我在踩坑时就遇到过改了beat_schedule但任务不执行的情况其实就是没重启beat。这个异步架构的价值在于课程数据新增或编辑后你只需要调一次refresh_recommendations.delay()等后台算完下一次用户请求分钟级内就会拿到最新结果。严格说这不是秒级实时但在这个教学场景下已经足够。4. 个性化智能问答搜索式召回加上用户历史权重问答模块在这个项目里走的是“检索式问答”路线先根据用户问题从问答库中召回候选再排序取最佳答案。它和推荐模块共享同一套文本预处理也共享MongoDB的连接这是项目设计里很聪明的一点因为问答和推荐本来就在服务同一个用户。4.1 问答的入口设计从问题输入到候选文档检索course/service/search.py负责问答检索。我按源码场景整理出的核心逻辑是先对用户问题进行分词然后在MongoDB里做关键词匹配召回可能的问答对。代码示例from course.service.text_preprocessing import preprocess from course.mongo_models import QAItem def search_candidates(question): terms preprocess(question).split() if not terms: return [] # 用问题的关键词做正则匹配同时匹配问题标题和标签 regex |.join([re.escape(t) for t in terms]) candidates QAItem.objects( __raw__{ $or: [ {question: {$regex: regex}}, {tags: {$in: terms}} ] } ) return list(candidates)这段代码里regex把多个关键词用|连接MongoDB的$regex会匹配包含任一关键词的问题。tags字段是文档模型里预留的扩展标签可以用来做同义词扩展。比如用户问“高数怎么学”如果问题标题里没有“高数”但标签里写了“高等数学”也能被召回。这里有一个值得注意的设计为什么要用__raw__而不是Django ORM的filter因为QAItem是mongoengine模型不是Django ORM模型标准的objects.filter也能用但__raw__能更直接地控制Mongo查询条件特别是$or这种逻辑。如果你用的是Django的connection直连MongoDB那就要换成pymongo的语法但mongoengine的__raw__是更简单的桥接方式。4.2 排序与答案抽取关键词重叠度加个性化权重召回只是第一步召回有十几条候选时得用排序来决定返回哪条答案。最简单的排序方式是计算用户问题和候选问题之间的关键词重叠度再叠加用户的历史行为权重。代码示例def rank_and_answer(question, candidates, user_idNone): q_terms set(preprocess(question).split()) scored [] for c in candidates: c_terms set(preprocess(c.question).split()) overlap len(q_terms c_terms) if user_id: overlap get_user_recent_bonus(user_id, c.question) scored.append((overlap, c.answer)) scored.sort(keylambda x: x[0], reverseTrue) return scored[0][1] if scored and scored[0][0] 0 else 暂无匹配答案这里的get_user_recent_bonus是一个概念性的函数实际实现时可以根据用户最近查看的课程tags给匹配的问答加分。比如用户最近在看“机器学习”相关的课程问题里又有“分类”那“机器学习分类算法”这条问答在排序时的权重就会比“分类讨论”更高。这就是个性化问答的落地点不是修改算法逻辑而是在排序阶段引入用户行为特征。如果你的问答库数据量比较大用词重叠会显得很粗糙。我一般建议在QAItem上建立文本索引用MongoDB的$text做全文检索然后用$meta取文本相似度分数作为排序主键再把用户行为当作加权因子。这个小改动能让检索质量上一个台阶而且代码改动量不大。4.3 与课程推荐共享数据层mongo_models的设计考量推荐模块和问答模块都写入了MongoDBmongo_models.py里除了QAItem通常还会定义一个推荐结果模型。示例class CourseRecommendation(Document): course_id IntField(requiredTrue) top_course_ids ListField(IntField()) updated_at DateTimeField()这个模型就是之前Celery任务写入的结果表。它的存在让“问答-推荐”之间可以形成闭环用户在问答里提到“线性代数”问答系统会返回答案同时把这个搜索关键词记录到用户行为中下一次推荐系统计算相似度时会把用户最近搜索的词也拼进课程描述里让推荐结果更贴合当前学习兴趣。我比较欣赏这种共享数据层的做法。很多项目把推荐和问答拆成两个独立服务连接两个独立的数据库然后要在中间做数据同步。这个源码选择让它们共用MongoDB虽然看起来不够“微服务”但在单体应用阶段这种设计极大降低了数据一致性成本。你自己扩展时可以在MongoDB里再建一个UserBehavior集合专门存用户的问答日志和课程点击日志推荐任务刷新时读取这个集合。这样个性化推荐的数据基础就真正建立起来了。再看一下REST接口层。course/views.py里会暴露一个问答接口用Django REST framework的装饰器写起来很简洁from rest_framework.decorators import api_view from rest_framework.response import Response from course.service.search import ask api_view([POST]) def ask_question(request): question request.data.get(question) user_id request.user.id if request.user.is_authenticated else None answer ask(question, user_iduser_id) return Response({answer: answer})ask函数内部做预处理、召回、排序三步最终返回一个答案字符串。如果用户未登录user_id就是None这时候问答就退化为通用的检索式问答不叠加个性化权重。这种设计保证了系统的健壮性游客也能用登录用户能获得更好的体验。5. 部署与调试避坑我实测时踩过的五个DjangoCelery问题这个包不值得反复看“怎么启动”因为这些教程太多了。对我来说真正有价值的是我在这套环境里跑起来时遇到的那些翻车点。下面五条是我真实踩过的按“现象→原因→解决”的格式记下来你可以直接对照排查。5.1 现象celery worker启动后一直不消费任务启动worker时日志正常然后调用refresh_recommendations.delay()worker这边完全没反应任务堆积在队列里。原因mycelery/main.py里的autodiscover_tasks只声明了[course, sms]但如果course/tasks.py里没有导入mycelery.main的app任务就不会注册成功。另外一个很常见的原因是你在Django的INSTALLED_APPS里没有加mycelery导致Celery应用没有被完整初始化。解决先确认course/tasks.py里的from mycelery.main import app这一句存在再确认Django的settings.py里INSTALLED_APPS包含mycelery。如果都没问题就重启workercelery -A mycelery.main worker --loglevelinfo -E。加上-E可以在worker日志里看到任务执行详情。5.2 现象Django启动时报错ModuleNotFoundError: No module named mongoengine我的第一反应是项目写错了但仔细看requirements.txt确实有这一项。报错原因是当前虚拟环境里没装全依赖。原因这是典型的依赖混乱。很多学生拿到zip包后直接python manage.py runserver忘了先激活虚拟环境并安装依赖。有时候即使装了也可能是系统路径下有多个Python版本pip装到了Python 3.8而Django用的是Python 3.10。解决用python -m pip install -r requirements.txt强制让当前解释器安装依赖然后用python -c import mongoengine; print(mongoengine.__version__)验证。这一步能排查掉一半环境问题。5.3 现象预处理中文文本时出现UnicodeDecodeError在第一次调用推荐接口时后台日志报错UnicodeDecodeError: gbk codec cant decode byte 0x...原因hit_stopwords.txt文件是utf-8编码但Path.read_text()在Windows上默认用GBK编码读取。项目里很多地方没有显式指定encodingutf-8导致和文本预处理相关的模块在Windows环境下一个接一个挂掉。解决把所有read_text()和open()都改成read_text(encodingutf-8)、open(..., encodingutf-8)。另外如果txt文件本身是GBK编码那就反向操作。最简单的办法是把文件另存为utf-8然后在代码里强制指定编码。5.4 现象登录后半小时就再次被拦截返回401前端调接口时token明明没过期但服务端就是校验失败。原因jwt.py里的TOKEN_TTL只控制了生成token时的exp但如果项目里有多个服务实例比如Django开发服务器和Celery worker共用同一个SECRET_KEY而某个实例中TOKEN_TTL被修改但没重新生成token就会导致新老token的过期时间不一致。另一个常见原因是Django项目的时间不是UTCexp字段算出的时间和本地时间差了8小时导致token“提前过期”。解决统一在settings.py里设置TIME_ZONE Asia/Shanghai并且在jwt.py中生成payload时用datetime.utcnow计算exp确保所有实例使用同一套时间规则。如果你只是本地调试直接把TOKEN_TTL改成60 * 60 * 24然后重新登录一次即可。5.5 现象.idea文件混进了Git提交导致团队协作冲突源码包里带了.idea目录里面是IntelliJ IDEA的工程配置。如果直接提交每个人的本地路径、运行配置都会写进.git导致每次拉代码都有一堆冲突。原因项目作者没有在.gitignore里忽略.idea或者忽略了但没清理Git缓存。关于upload.zip解压后的目录这个.idea会在IDE打开时自动生成不该入库。解决在项目根部.gitignore里加两行.idea/ *.iml然后执行git rm -r --cached .idea再提交一次这样既能保留本地配置又不会污染仓库。我自己的习惯是把.idea全局加入~/.gitignore_global这样所有项目都不会再误提交。6. 验证与进阶离线评估推荐效果和接口压测两个技巧拿到这个源码包不能只是跑通还得知道推荐效果到底行不行。这里分享我常用的两个验证技巧以及一个把内容推荐升级到协同过滤的方向。6.1 用Django shell离线评估推荐结果进入项目根目录运行python manage.py shell然后写一段小脚本用“留一法”验证推荐精度from course.service.text_preprocessing import preprocess from course.service.recommend import ContentRecommender from course.models import Course courses list(Course.objects.all()) docs [preprocess(c.description) for c in courses] recommender ContentRecommender(docs) hit 0 for i, c in enumerate(courses): query preprocess(c.description).split() if len(query) 3: continue query .join(query[:3]) top_ids recommender.recommend(query, top_n5) same_category sum(1 for j, _ in top_ids if courses[j].category c.category) if same_category 0: hit 1 print(推荐命中率, hit / len(courses))这里用课程的前三个词作为query看推荐结果是否包含同分类课程。命中率只是一个相对指标你可以把top_n从5调到10观察命中率变化。如果不管怎么调都只有20%说明预处理停用词表需要加强如果高得离谱可能是分类字段本身就太粗推荐“准”但没意义。6.2 用curl压测问答接口先用Django自带服务起一个进程然后开另一个终端curl -X POST http://127.0.0.1:8000/api/qa/ \ -H Authorization: Bearer 你的token \ -H Content-Type: application/json \ -d {question:什么是线性代数}看返回时间。如果这个接口耗时超过500ms说明你查MongoDB时没有建索引。常见做法是在QAItem的question字段上建唯一索引在tags上建普通索引。mongoengine里可以在meta中声明meta {indexes: [question, tags]}压测时建议用wrk或locust不要只靠postman。我一般会先压10个并发观察耗时的p95。如果p95超过300ms就需要考虑把问答的答案结果缓存到Redis或者做更精细的倒排索引。这个源码包本身不包含缓存层但你在真实部署时这一步几乎逃不掉。6.3 进阶方向从内容相似度升级到用户行为协同过滤内容推荐方法很简单但它的上限就是文本描述的长度。如果课程描述写得很泛推荐结果很难精准。真正的个性化推荐一般会叠加用户行为数据比如点击、收藏、完成度。具体做法是在MongoDB里新建一个UserBehavior集合记录用户每次点击课程和问答搜索的关键词。然后离线计算课程之间的共现矩阵如果两个课程被同一批用户查看过它们之间就有一条关系边。推荐时先取用户最近看过的课程再找这个课程在共现矩阵中的最强关联课程。这种协同过滤方法不需要理解文本语义只靠行为数据反而能发现“喜欢学Python的人通常也会学数据分析”这类难以从描述中挖掘的关联。我建议你在这个源码包里先跑通内容推荐然后把推荐结果导出成一个CSV再写脚本计算一下用户对推荐结果的点击率。如果发现点击率偏低就可以把协同过滤当作第二路召回来源用加权融合的方式替换掉一部分内容推荐结果。这个源码包给了你一个很好的底座数据和链路都是齐全的往里面加行为采集并不难。真的着手改的时候记得先确认MongoDB里有用户日志数据否则协同过滤算出来也是一个空矩阵。从那以后我每拆一个Django项目都会先跑一遍requirements安装、再检查数据库连接串、最后才迁移数据这个习惯帮我躲过了好几次低级翻车。希望帮到你。本文还有配套的精品资源点击获取