又到毕设选题季,后台私信里问得最集中的一类项目,就是“大数据深度学习”方向的推荐系统。今年反复被提到的一个题目组合是:Streamlit + 多模态RAG的电影推荐系统。这个方向确实值得做,它不像纯协同过滤那么老套,也不像全链路大模型那样难以掌控,难度适中、展示效果好、答辩时有得聊。这篇文章我会把项目完整拆一遍,从数据准备、特征嵌入、向量检索、推荐融合,到Streamlit界面和答辩现场的高频问题,一条线讲清楚。
这个项目之所以在一堆“XX推荐系统”里脱颖而出,核心在于多模态RAG把传统推荐从“只看评分”变成了“能理解描述、能看懂海报”的语义检索系统。用户可以直接输入一句“一部色调温暖、发生在海边小镇的治愈系爱情片”,系统返回匹配的电影;也可以上传一张海报,找到风格相近的作品。对有Python基础、想做推荐方向、又希望系统看起来“有点东西”的同学来说,这是性价比很高的毕设方向。
1. 项目整体设计与技术选型思路
先聊方向选择。每年推荐系统毕设都有大量“撞车”,大家不约而同做协同过滤、矩阵分解、或者是普通的内容推荐。这类方案成熟没错,但答辩容易陷入“背书”模式,老师很难问出有区分度的问题。Streamlit + 多模态RAG的巧妙之处在于,它把推荐系统和当下比较热的技术点结合了:向量检索、多模态Embedding、大模型生成推荐理由。技术栈不虚,工作量真实,演示效果又很直观。
整个系统我拆成四层:数据层、特征层、检索融合层、展示交互层。数据层负责收集评分、元数据、海报图片;特征层把文本和图像分别向量化;检索融合层做多路召回和加权排序;展示交互层用Streamlit承接,用户在前端操作,背后触发检索链路。分层清晰,答辩画架构图也好讲,每层都能单独展开,不会一问就漏洞百出。
1.1 为什么选Streamlit而不是Flask或Gradio
很多同学一开始会纠结在Web框架选型上。我见过有人用Flask从零写HTML页面,结果前后端联调了两周,CSS样式没调明白,最后答辩前一天还在改布局。毕业设计的时间有限,Streamlit这类纯Python交互框架是更理性的选择。它不需要写HTML、CSS、JS,只要写Python逻辑,组件会自动渲染,而且刷新机制、状态管理、缓存机制都内置了,对数据类应用非常顺手。
对比Gradio,它更适合快速展示单个模型效果,比如上传图片、返回分类结果这种单任务交互。但电影推荐系统需要多页签、多组件联动、结果列表、推荐理由展示这类偏应用层的交互,Gradio的自由度和视觉风格会受限。Streamlit的布局能力更强,表格、卡片、侧边栏、文件上传都能直接支持,可塑性和演示效果都更好。
还有一点容易被忽略:Streamlit的st.cache_data和st.cache_resource在设计上就考虑了“模型加载一次、数据缓存多次”这类场景。推荐系统最大的性能瓶颈就是Embedding生成和向量检索,用缓存把结果复用之后,页面响应速度会快很多。Flask要自己搭缓存,而Streamlit是开箱即用,实际开发效率差距非常大。
1.2 多模态RAG在电影推荐里到底解决什么问题
先说RAG的基本套路:用户输入一条查询,系统不做硬编码匹配,而是把查询转成向量,在向量库里召回最相关的若干条内容,再把召回结果交给后续模块生成回答或推荐理由。放到电影推荐场景里,就是用户描述一句模糊需求,系统在几千部电影里找到“最贴合的候选集”,而不是只靠历史行为数据猜。
多模态的部分更有意思。传统推荐系统只用用户-物品评分矩阵,冷启动用户和冷门电影都难以处理。多模态RAG引入文本Embedding和图像Embedding之后,系统能理解剧情简介、导演风格、海报视觉风格。用户没评过几部片,但只要给出描述,或者上传一张海报,系统依然能给出高质量推荐。这对推荐系统的覆盖率和多样性是非常明显的补充。
我实际做完后的感受是:多模态RAG解决的不是“预测评分更准”的问题,而是“用户表达更自由”的问题。以前用户只能得到“猜你喜欢”,现在用户可以直接和推荐系统对话,或者用图像说话。这个视角差异本身就是答辩时最能讲清楚创新点的地方。
2. 数据准备与多模态特征工程
数据是整个系统的天花板。很多同学上来就闷头跑模型,结果做出来的推荐结果千奇百怪,问题基本都出在数据处理上。这个项目需要三类数据:用户评分数据、电影元数据、海报图片数据。三者要能通过电影ID对齐,供后续特征拼接和向量索引使用。
评分数据我建议用常用的公开电影评分数据集,量级在10万到100万评分即可,太多处理耗时,太少说服力不足。电影元数据需要包含标题、类型、导演、演员、剧情简介、年代、评分信息。海报图片数据是另一条关键支线,需要按电影ID逐一下载并做图片校验,这一步耗时较长,建议写脚本批量抓取,失败重试,最后把能成功获取的图片单独归档。
2.1 数据处理的核心链路
数据处理最耗时的不是“处理”,而是“对齐”。评分数据里的电影ID,和元数据API里的电影ID往往不是同一套体系,需要自己建立映射表。映射对不上,后面所有特征都白算。我的做法是:先加载评分数据里的全部电影ID,再去开放元数据接口逐条补齐信息,然后以元数据表为主表,逐个匹配海报图片,最后生成统一的数据表。
清洗时要关注几个细节:
- 年代、时长这些数值字段可能缺失或异常,统一用中位数或众数填充
- 类型字段往往是多值,建议拆成标签列表,后面做文本拼接时更方便
- 简介文本可能包含很多无关标点和广告词,要统一做一次清洗
- 海报图片下载失败的比例比想象中高,要记录缺失列表,后续可手动补
给一个参考:我当时处理5000部电影,元数据补齐率约96%,海报能成功下载约87%。这已经足够完成推荐演示,没必要追求100%。与其花时间补齐小比例图片,不如把现有数据的质量提上来。
2.2 文本Embedding怎么生成才不“跑偏”
文本Embedding的目的是把电影的描述性信息转成向量,让语义相近的电影在向量空间里靠近。关键不是选多大模型,而是如何拼接文本。
经验做法是把类型、导演、演员、简介、风格标签拼成一段固定格式的文本,中间用换行或分隔符隔开。导演和演员建议放靠前位置,因为它们是决定“这部电影像谁”的重要因素。直接简单拼接简介容易让模型抓不住重点,尤其是长文本里大量“剧情转折”描述反而会稀释风格信息。
具体Embedding生成可以用预训练模型。我用了句子向量模型,输出384维向量。生成时要注意两点:一是文本统一小写并清理多余空白,二是向量要归一化。归一化之后,内积等价于余弦相似度,检索效率更高。批量生成时控制batch size,实测下来文本平均长度200字符左右时,5000条文本大约需要一到两分钟,完全可接受。
存储体积可以估算:假设5000部电影,每部384维,用float32存储,一个维度的体积是4字节,总内存约5000乘384乘4,约7.7MB。这在内存里就是小意思,根本不需要上分布式存储;答辩时如果老师问数据规模与存储可行性,直接算这笔账就行。
2.3 图像Embedding与多模态对齐的关键点
图像Embedding我建议直接用多模态CLIP系列模型,它最核心的特点是文本和图像在同一个向量空间里。这意味着你可以把“海报图像”和“电影文本描述”直接做向量相似度计算,而不需要额外训练映射层。这是本项目多模态能力成立的基础。
CLIP模型输入图像前要做预处理:resize到224x224或336x336,转成RGB,归一化。实际使用中最好把预处理封装成一个函数,因为用户上传的海报可能是任意尺寸,不统一处理直接喂进模型会报错。输出向量维度通常比文本向量高,比如512维。文本向量和图像向量维度不一样,不影响各自检索,但后续融合计算时要做维度统一或分开打分再加权。
一个容易忽略的细节是图像Embedding的batch处理。如果你一次性对5000张图片跑特征,显存不够会很尴尬。我在实践里按每批32到64张循环处理,输出到numpy数组存下来。整个流程耗时和机器有关,用带GPU的环境相当快;纯CPU会慢很多,建议提前规划好运行环境,不要等到答辩前才去跑全量特征。
3. 向量检索与混合推荐链路
向量检索是系统从“存特征”走向“出结果”的临门一脚。这个模块的核心任务是把用户输入的文本或图像转换成查询向量,然后在电影向量库中召回最相似的Top-K个候选。这里的技术方案直接决定系统的响应速度和推荐质量,值得花时间调优。
我采用的方式是把“向量检索”和“推荐理由生成”拆成两段。向量检索负责快而准地召回候选,生成模块负责慢而详细地把候选解释给用户看。如果一上来就让大模型逐部生成推荐理由,几十部候选会等很久;先召回Top-5再生成,效率和体验都能兼顾。
3.1 向量库选型:Chroma还是FAISS
向量库我用过Chroma和FAISS,各有优势。Chroma使用门槛极低,支持Python原生、自动持久化到本地磁盘,后端存储类似SQLite风格,非常适合几千条数据量的场景。它自带元数据过滤,可以直接存电影ID、标题、年代等字段,查询时还能用where条件过滤。
FAISS是更偏传统的向量检索库,性能上限高,支持大规模向量索引和GPU加速。但在几千条数据规模下,它的性能优势不明显,反而要手动管理索引的保存和加载。真正实践经验是:小项目优先Chroma,因为上手快、不容易出错;如果演示时希望展示“工业级”技术栈,可以把FAISS和Chroma结合,向量用FAISS存索引,元数据用Chroma或SQLite,两条链路配合用。
选型带来的答辩收益也值得一提。老师问“为什么选这个向量库”,最怕听到“因为网上教程多”。能说出Chroma适合小规模持久化、FAISS适合大规模高性能召回,再补一句“数据量不同,选型策略不同”,这个回答比背概念强很多。
3.2 检索策略:从查询向量到候选集
标准RAG检索链路是:用户输入文本 → 模型生成文本查询向量 → 在向量库中执行Top-K相似度检索 → 返回电影ID列表 → 读取元数据详情。这一步看似简单,但有个很关键的细节是相似度阈值。
我最初做召回时没有阈值控制,结果用户随便输入“空调坏了”都能召回一堆热门电影,因为热门电影在向量空间里天然处于密集区域,相似度偏高。后来我加了一个“最低相似度”的判断,比如低于0.25的查询结果直接提示用户“描述太模糊,试试更具体的关键词”。这样既能避免无意义推荐,也能在答辩演示时显得系统有判断力。
在一句话查询之外,我也加了个“关键实体提取”的轻量环节。查询先拆出几个关键词,比如“温暖色调、海边小镇、治愈系爱情”,然后分别对每个关键词做向量检索,最后把多路召回结果合并去重。这种查询扩展策略对抗长句漂移效果明显,也是正文向量直接检索做不到的细节。
3.3 多路召回与融合排序
我的推荐结果来自三路召回:协同过滤、文本语义检索、图像相似检索。三路分数都不能直接比较,所以要先归一化再加权融合。协同过滤分数基于用户历史评分,语义分数基于自然语言/元数据相似度,图像分数基于海报特征相似度。
融合公式按场景动态调整。用户只输入描述时,文本语义权重最高;用户上传海报时,图像权重拉高;用户单纯想看历史偏好推荐时,协同过滤权重占主导。实际代码里可以用一个简单的加权函数,根据当前交互模式动态分配权重。你在答辩时可以把这张“权重分配表”直接摆出来,向老师解释不同场景下推荐如何兼顾用户行为与语义理解。
融合最大的作用是缓解冷启动。新电影没有评分,协同过滤给不出分数,但语义和视觉索引依然能靠内容特征补位。新用户没有历史行为,协同过滤也无能为力,此时自然语言或海报输入就是最佳入口。多路召回+融合,本质上是用多路信息互相兜底,这个逻辑讲清楚,推荐系统的鲁棒性问题就回答了一半。
4. Streamlit应用层实现与性能优化
Streamlit层的目标不是做出复杂的后台管理系统,而是让用户在三个自然入口轻松获得推荐结果。我最终把系统设计成三个页签:自然语言推荐、海报找片、我的片单。侧边栏统一放筛选条件,比如年代范围、类型过滤、最低评分阈值,这些条件在每次推荐时都会参与排序调权。
自然语言推荐页签是重头戏。用户输入一句话,系统先判断类型:是“找电影”还是“随便看看”。找电影时走完整RAG链路,生成推荐理由;随便看看时输出随机Top结果,配合当前日期和主题的轻量推荐。这个交互设计让系统显得“更聪明”,实际上只是在入口处加了一个简单的意图分隔逻辑。
4.1 页面结构与交互核心
页面布局我建议不要堆砌组件。Streamlit虽然加组件很容易,但过度堆砌会显得没有产品思维。核心交互围绕两个输入、一个输出:两个输入分别是文本输入框和图片上传框,一个输出是推荐结果卡片列表。每张卡片展示电影标题、年代、类型、余弦相似度分数,以及大模型生成的推荐理由。
推荐理由生成是展示环节的加分项。我接入了本地可运行的轻量大模型,把召回的电影元数据拼成Prompt,让模型输出两到三句推荐语。Prompt大概长这样:基于以下电影信息,用一句话推荐这部电影,并点出它在视觉风格和剧情上与用户需求的契合点。注意生成必须做超时控制,否则用户输入一条奇怪描述时,页面会卡在原地。
“我的片单”页签则是为了承接协同过滤。选一个用户ID,展示他评分过最高的几部电影,然后输出协同过滤的推荐结果。这里的交互重点是让评委一眼看出“系统确实在做个性化推荐”,而不是每次输出都一样。配合侧边栏的筛选条件,同一用户更换类型偏好后结果有明显变化,演示效果就很直观。
4.2 性能优化:从卡顿到秒开
Streamlit最容易翻车的地方是:每次页面刷新都重新加载模型、重新计算特征、重新生成推荐结果。如果没做缓存,一次交互要等几十秒,答辩现场会非常尴尬。必须把缓存设计放到工程优先级第一位,否则演示效果直接崩塌。
第一层缓存是模型加载。用st.cache_resource缓存Embedding模型实例,确保整个应用生命周期内模型只在第一次加载时初始化。第二层缓存是数据索引。电影特征向量、元数据表、向量库连接都是只读数据,用st.cache_data缓存,避免每次刷新重新读取。第三层缓存是推荐结果。同一个查询在短时间内重复点击,直接复用上次生成好的推荐结果和理由。
图像上传环节也要做压缩。用户上传的海报可能是几兆大小,直接喂进模型会拖慢预处理。我先把图片统一压缩到最长边不超过512像素,再走CLIP预处理。这样既能保证特征提取效果,也能把上传图片的处理时间从几秒降到几百毫秒。
另外要给用户视觉反馈。检索耗时较长时,用st.spinner加上“正在召回最相关的电影...”之类的文案,配合st.progress展示进度。很多毕设系统并不是不好用,而是交互等待太干瘪。加上反馈之后,整体体验会有非常明显的提升,答辩评委的印象分也会更好。
5. 答辩高频问题与排坑实录
答辩环节的问题其实是可以预判的。评委不会为难你用多么高深的技术,他们更在意你能否把“为什么这么做”讲清楚,以及系统在不同场景下是否有真实思考。这里我把项目里最容易被追问的五类问题整理出来,并给出我实际准备的回答思路。
第一类问题是“为什么用RAG,和传统推荐有什么区别”。我准备的回答是:传统推荐基于用户行为或内容标签匹配,RAG核心是把推荐变为带语义检索的生成链路。用户表达的需求,经过向量化召回候选,再交给大模型生成解释。区别在于系统不是“猜你会不会喜欢”,而是“理解你为什么喜欢”。
第二类问题是“多模态向量怎么对齐”。我坦诚说CLIP本身提供了共享表征空间,所以文本和图像可以在同一度量下比较。但维度不同,融合时采用“分开打分再加权”的方式,不强行拼接。同时说明如果遇到更细粒度的跨模态对齐,可以通过对比学习微调,这是论文方向,不是毕设必需。
第三类问题是冷启动。我的答案是多路召回方案:新用户没有评分,就走文本和图像检索,用户表达越具体,结果越准。新电影没有评分,就走内容和语义特征,因此冷启动不再是死局。配合侧边栏筛选机制,短时间内也能产出合理推荐。
第四类问题是效果怎么评价。我的回答是分两层:离线指标上用召回率和多样性辅助验证;在线演示上强调“解释性”,即不只给结果,还给出推荐理由,让用户理解推荐逻辑。同时强调在大规模在线评测中才用A/B测试,毕设场景以案例验证为主。
第五类问题是“数据量这么小,结果能说明问题吗”。我建议这样回答:小数据聚焦验证流程完整性,如果你的框架在5000部电影中跑通,迁移到更大数据只需替换数据源和扩容索引。关键系统设计不依赖数据量,而依赖索引结构和融合策略。答辩时把这个格局打开,比一直道歉有用得多。
5.1 答辩现场老师最爱问的几个问题与回答思路
答辩时最容易露出破绽的不是算法,而是细节。老师可能会追问:文本描述包含多个语言时,Embedding模型是否支持?回答思路是:预训练模型对英语支持最好,因此系统对英文元数据做标准化处理;对中文元数据可以先用翻译或双语模型转换,或者只在纯英文环境中展示完整效果。不要回避问题,而是说明你意识到了这个限制,并有预案。
老师还可能好奇“推荐结果里面有重复电影吗”,这其实是在考察多路召回后的去重逻辑。我实际实现了基于电影ID的集合去重,并对同一电影的多个召回分数取最高分。这块代码很简短,但被问到时能体现出工程严谨性。
还有个高频追问是“如果用户的自然语言描述和电影简介完全不匹配怎么办”。我的做法是输入侧加了相似度阈值和提示词引导,输出侧保持冷静,给出“暂时没有找到高度匹配的电影”并展示相近结果。这比强行推荐一个有调性的结果更有可信度,让评委看到你考虑过边界情况。
5.2 调试中踩过的坑与规避方法
调试过程里踩得最深的坑是嵌入向量“语言不一致”。我最初把中文简介和英文元数据混在一起生成特征,结果语义检索几乎不可用,热门中文电影和热门英文电影挤成一团,完全没有区分度。统一语言之后,检索效果立刻正常。这块的教训是:特征工程的第一原则是标准化,而不是用好模型。
第二个常见问题是模型重复初始化。我把模型加载写进了Streamlit的回调函数里,结果每点一次按钮都重新加载一次CLIP,页面直接卡死。改成@st.cache_resource装饰器后问题解决。要学会区分缓存资源和缓存数据,前者针对模型、数据库连接,后者针对计算结果。
第三个问题是内存溢出。图像特征一次性全量提取时,机器内存直接飙满。改成批次提取并写入本地文件后稳定了很多。这个问题的通用解法是:任何大数据量级处理都要分批,不要指望一次循环解决所有事。
第四个问题是向量库持久化失败。Chroma默认持久化路径如果没有提前创建,会经常写一半报错。解决办法是先初始化持久化目录,再操作集合。我曾经因为路径问题重跑了三遍全量索引,非常浪费时间。希望学弟学妹看到这条能少走弯路。
最后一个建议是答辩前一定要把整个系统在干净环境里跑通三遍。Streamlit依赖版本兼容性很敏感,同一个代码在不同机器上表现可能完全不同。提前做好虚拟环境锁定,把模型权重和向量索引预下载到本地,避免答辩现场临时下载文件。把这些细节准备到位,你的演示基本就稳了。