简介:这份面向毕业设计开题阶段的文档,围绕“短视频内容理解与推荐系统的设计与实现”展开,适合计算机相关专业学生及需要撰写开题报告的读者参考。内容从选题背景与意义切入,梳理国内外在视频内容理解、推荐算法与分布式系统架构方面的研究现状,并给出基于Python、Hadoop、Flask与Vue的系统方案,涉及深度学习特征提取、用户行为分析、个性化推荐,以及用户管理、交流论坛、通知公告等功能模块。资源包共1个docx文件,约121KB,篇幅紧凑,便于直接查阅与改写,已有75人学习下载。文档还包含研究内容、研究思路与方法、拟解决的主要问题及措施、功能模块图等开题报告常见栏目,可帮助读者快速搭建论文框架、明确技术路线与后续实现重点,也能为答辩准备和课题论证提供较完整的参考。
1. 一个真实的选择难题:为什么这套系统不直接用现成推荐接口
选题会上被问得最多的一句话是——你这套短视频内容理解与推荐系统,和直接调一个推荐接口有什么区别。区别在于,接口给的是黑盒结果,而这份方案要能说清三件事:视频内容怎么被理解成向量、用户行为怎么变成画像、离线算出来的结果怎么在几百毫秒内送到前端。所以技术栈不是随手凑的:Python 承担抽帧、特征提取与模型推理,Hadoop 承担离线批处理与海量存储,Flask 出接口,Vue 做交互,四段链路各有明确的输入输出和失败边界。这份毕业设计开题报告配套的完整方案,适合正在写毕设、课设的同学照着搭,也适合要交一版推荐系统技术方案的从业者直接改。
2. 需求拆解与技术选型:四个组件各管一段链路
2.1 功能性需求:六类页面背后的真实动作
开题报告里列的功能是「管理员管理、个人中心、通知公告、交流论坛、个人资料」,如果只照这个写,做出来的东西就是个带视频的论坛,推荐只是个摆设。我一般会把页面翻成「动作 + 数据变化」,一个一个对到表上,这样后面写接口和写论文的用例图都能直接对上。
| 页面 | 用户可见动作 | 后端真实动作 | 落到的表 |
|---|---|---|---|
| 首页推荐流 | 上下滑、点开、划走 | 调推荐接口,写曝光/点击/负反馈行为 | t_user_action、t_recommend_log |
| 视频详情 | 播放、点赞、评论、分享 | 累加互动计数,写入行为权重 | t_video、t_user_action |
| 个人中心 | 看历史、看收藏、改偏好 | 读行为聚合出的画像标签 | t_user_profile |
| 管理员-视频管理 | 上传、下架、改分类 | 入 HDFS 队列,触发离线特征任务 | t_video、hdfs 原始目录 |
| 管理员-用户管理 | 封禁、重置、看活跃度 | 改状态位,写审计日志 | t_user |
| 交流论坛/通知公告 | 发帖、回复、看公告 | 普通 CRUD,但帖子文本要参与兴趣计算 | t_post、t_notice |
这张表最大的作用是帮你砍需求。论坛和公告是纯 CRUD,工作量占比不低但对评分贡献有限,做得出来就行;真正决定这套系统像不像「内容理解与推荐」的,是行为采集和特征表这两块,必须写厚。很多同学把时间花在页面美化上,最后答辩被问「你的推荐结果是怎么算出来的」直接卡住,这个坑后面还会专门讲。
2.2 非功能性需求:为什么非要上 Hadoop
先说个实话:一个毕设规模的数据集,单机 pandas 跑完特征也就几十分钟,Hadoop 在这里的价值不是性能,而是架构分层——它把「原始视频 → 抽帧文件 → 特征向量 → 业务库」这条链路拆成可扩展的两段,让你能在论文里讲清楚离线与在线的边界。答辩时如果被问「你这个 Hadoop 是不是为了用而用」,正确的回答是:在线服务不能碰原始视频,必须由离线任务把非结构化数据压成结构化特征,Hadoop 就是承担这个压缩过程的那一层。
容量估算要在设计阶段就做出来,不能拍脑袋。按一条视频 30 秒、每秒抽 1 帧、每帧 224×224 的 JPEG 算,单条视频的抽帧文件大约 30 张 × 15KB ≈ 450KB;5000 条视频就是 2.2GB 左右,加上原始视频按 5MB/条算又是 25GB。这个量级单机存得下,但用 HDFS 存的好处是三副本天然容灾、目录按日期分区后可以只重跑某一天的任务。特征向量按 512 维 float32 存,单条 2KB,5000 条才 10MB,完全可以进 MySQL 直接做在线检索。
提示:论文里不要写「使用 Hadoop 处理海量数据」这种空话,写清楚「原始视频与抽帧文件存 HDFS,特征计算结果落 MySQL 供在线查询,Hadoop 只负责每天增量重算」,评审一眼就能看出你理解分层。
2.3 模块划分与核心数据表
模块按职责切四层:接入层(Vue 页面 + Flask 路由)、业务层(用户、视频、论坛、公告)、算法层(特征提取、召回、排序)、数据层(MySQL + HDFS)。算法层单独拎出来,是为了让它既可以被 Flask 在线调用,也可以被离线任务批量调用,同一份代码两处复用,不然你会在联调时发现线上和离线算出来的特征对不上,这是血泪经验。
核心表设计控制在五张,多了写论文反而乱:
-- 视频主表:只放元信息和统计值,特征单独存,避免宽表拖慢列表查询 CREATE TABLE t_video ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, video_url VARCHAR(500) NOT NULL COMMENT 'HDFS 路径或对象存储地址', cover_url VARCHAR(500), duration INT COMMENT '时长,单位秒', author_id BIGINT NOT NULL, category_id INT COMMENT '一级分类,用于多样性打散', play_cnt INT DEFAULT 0, like_cnt INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1 上架 0 下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 特征表:与主表一对一,BLOB 存序列化后的 float32 数组 CREATE TABLE t_video_feature ( video_id BIGINT PRIMARY KEY, visual_vec BLOB COMMENT 'CNN+LSTM 输出的视觉时序向量', text_vec BLOB COMMENT '标题+标签+评论的文本向量', fused_vec BLOB COMMENT '归一化融合向量,在线余弦检索用这个', dim INT, model_ver VARCHAR(20) COMMENT '模型版本,换模型后要能整批重刷', update_time DATETIME ); -- 行为表:只追加不更新,按 user_id + 时间建联合索引 CREATE TABLE t_user_action ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, video_id BIGINT NOT NULL, action_type TINYINT COMMENT '1曝光 2点击 3完播 4点赞 5评论 6分享 7负反馈', weight DECIMAL(4,2) COMMENT '由 action_type 映射,见 4.1', action_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, action_time), KEY idx_video (video_id) );model_ver这个字段很多人会漏。等你把文本特征从 TF-IDF 换成句向量,旧向量和新向量混在一张表里做余弦,结果会莫名其妙——留个版本号,换模型时按版本整批重刷,就不会出现「一半视频推荐得准一半乱推」的玄学现象。行为表只追加不更新也是同理,推荐系统里最难找回的就是「用户当时为什么点了这条」,日志一覆盖就没后悔药了。
3. 内容理解与离线特征工程:抽帧、CNN-LSTM 与 Hadoop 批处理
3.1 抽帧、采样与预处理流水线
视频理解的第一步不是模型,是别让脏数据进模型。短视频里有横屏有竖屏、有 15 秒有 3 分钟、有黑屏片头,抽帧策略必须先把这些抹平。我一般用固定采样率而不是固定帧数:每秒 1 帧,长视频截断到 32 帧上限,这样 30 秒的片子和 60 秒的片子拿到的向量语义密度接近,不会出现「长视频特征被平均稀释」。
import cv2, os def extract_frames(video_path, out_dir, fps_sample=1.0, max_frames=32): """按固定采样率抽帧,返回落盘的图片路径列表。 fps_sample: 每秒采样帧数,1.0 对短视频足够描述内容主线 max_frames: 上限,防止长视频把磁盘打满 """ cap = cv2.VideoCapture(video_path) if not cap.isOpened(): # 关键:读不到必须抛异常,不能 return [] # 返回空列表会让下游静默生成一条零向量,这类脏数据最难查 raise IOError(f"无法打开视频: {video_path}") src_fps = cap.get(cv2.CAP_PROP_FPS) or 25 step = max(int(round(src_fps / fps_sample)), 1) # 每 step 帧取一张 paths, idx, saved = [], 0, 0 while saved < max_frames: ok, frame = cap.read() if not ok: break if idx % step == 0: frame = cv2.resize(frame, (224, 224)) # 与 ResNet 输入尺寸对齐 p = os.path.join(out_dir, f"{idx:06d}.jpg") cv2.imwrite(p, frame) paths.append(p) saved += 1 idx += 1 cap.release() if not paths: raise ValueError(f"抽帧结果为空: {video_path}") return paths代码里两个raise是刻意写的。线上跑批时最怕的就是静默失败:视频损坏、编码不支持、路径带中文,cv2.VideoCapture都只是返回 False,如果你顺手写个return [],后面特征提取会拿到空数组,最后在推荐列表里表现为「有几条视频永远推不出去」,而日志里干干净净。宁可让任务失败重跑,也不要让脏数据流进特征表。
fps_sample和max_frames这两个参数怎么调:手势舞、卡点视频变化快,采样率可以提到 2.0;口播类内容画面几乎不动,1.0 甚至 0.5 都够。参数调完要重刷整批特征,所以改之前先想清楚,别跑到一半改。
3.2 CNN + LSTM 的视觉时序特征提取
视觉特征有两种做法:单帧提特征再平均,或者用循环网络建模时序。前者实现简单但会丢掉动作信息——一个「从桌上拿起杯子」的动作,平均之后和「桌上放着一个杯子」几乎没区别。短视频的核心信息大量来自动作和转场,所以我倾向 CNN 提帧级特征、LSTM 做时序聚合,这也是开题报告里提到的 CNN + RNN 组合最直接的落地方式。
import torch, torch.nn as nn from torchvision import models class VideoEncoder(nn.Module): def __init__(self, hidden=256, pretrained=True): super().__init__() backbone = models.resnet50(pretrained=pretrained) # 砍掉最后的全连接层,保留全局池化输出的 2048 维 self.cnn = nn.Sequential(*list(backbone.children())[:-1]) for p in self.cnn.parameters(): p.requires_grad = False # 数据量小,冻结主干只训上层,防止过拟合 # 双向 LSTM:正向看动作发展,反向看动作回归,拼接后语义更稳 self.lstm = nn.LSTM(2048, hidden, batch_first=True, bidirectional=True) self.out_dim = hidden * 2 def forward(self, frames): # frames: (B, T, 3, 224, 224) b, t = frames.shape[:2] x = frames.view(b * t, *frames.shape[2:]) with torch.no_grad(): feat = self.cnn(x).view(b, t, -1) # (B, T, 2048) seq, _ = self.lstm(feat) # (B, T, 512) return seq.mean(dim=1) # 时序均值池化,得整段视频向量几个设计取舍要说清楚。冻结主干是因为毕设数据集通常只有几千条视频,微调 50 层 ResNet 一定过拟合,而且还慢;用预训练权重做纯特征提取,效果反而稳定。双向 LSTM 的hidden=256,拼起来输出 512 维,这个维度是刻意的——512 维 float32 存一条只要 2KB,几千条视频全放内存做余弦检索毫无压力,比 2048 维省 4 倍空间且精度损失很小。如果显存不够,把t从 32 降到 16,或者把 ResNet50 换成 ResNet18,输出维度改成 512 后 LSTM 输入同步改,别只改一处。
注意:模型导出特征时一定要
model.eval()并且套torch.no_grad()。少了eval(),BatchNorm 会继续用当前 batch 的统计量,同一张图两次推理结果都不一样,这种不确定性在排查时几乎无解。
3.3 标题、标签与评论的文本侧特征
只看画面会翻车。很多视频画面高度相似(比如都是「一个人对着镜头说话」),真正区分它们的是标题和话题标签。文本侧我一般做两路:标题和标签走 TF-IDF 拿关键词权重,评论取前 20 条拼进去补充语义,但权重压到最低——评论区噪声大,一条「哈哈哈」会把主题带偏。
import jieba, numpy as np from sklearn.feature_extraction.text import TfidfVectorizer def build_text(video): # 标题重复一次提权,标签次之,评论只取前 20 条防噪声 title = video.get("title", "") tags = " ".join(video.get("tags", [])) comments = " ".join(video.get("top_comments", [])[:20]) return f"{title} {title} {tags} {comments}" def fuse(visual_vec, text_vec, w_v=0.6, w_t=0.4): """先各自 L2 归一化再加权拼接,保证两个模态量纲一致""" v = visual_vec / (np.linalg.norm(visual_vec) + 1e-8) t = text_vec / (np.linalg.norm(text_vec) + 1e-8) return np.concatenate([v * w_v, t * w_t]).astype("float32")融合方式选的是早期融合(直接拼接),不是注意力加权。原因是可解释:答辩时被问「为什么这条视频推给了这个用户」,你可以拆开说「视觉相似度 0.72、文本相似度 0.65」,注意力权重虽然理论上更强,但在几千条数据的规模下训练不稳,解释成本还高。w_v=0.6这个比例是经验值,画面主导的内容(舞蹈、美食制作)可以调到 0.7,知识口播类可以降到 0.5,调参依据写进论文的实验对比章节,比空谈「效果良好」有价值得多。
3.4 Hadoop 离线特征任务的落法
离线链路用 Hadoop Streaming 最省事:mapper 读 HDFS 上的视频清单,逐个抽帧、提特征,输出video_id \t 向量 JSON,reducer 直接用cat汇总,再统一写回 MySQL 或落成 HDFS 上的 parquet 供后续同步。不写 Java 是因为算法代码本来就是 Python,重写一遍纯属浪费工期。
# video_feature_mapper.py —— 由 hadoop-streaming 调起 import sys, json from encoder import load_model, encode_video model = load_model() # 在 mapper 启动时加载一次,绝不能放进循环里 for line in sys.stdin: row = json.loads(line) try: vec = encode_video(model, row["local_path"]) print(f"{row['video_id']}\t{json.dumps(vec.tolist())}") except Exception as e: # 失败样本走 stderr,Hadoop 会单独收集,不阻塞整个作业 sys.stderr.write(f"{row['video_id']}\t{type(e).__name__}:{e}\n")这里最容易出问题的不是算法而是模型加载位置。把load_model()写进循环体,五千条视频就加载五千次权重,任务能跑但耗时翻几十倍,看起来像「Hadoop 很慢」,实际是自己写的。另一个点是local_path:HDFS 上的视频没法直接给 OpenCV 读,标准做法是先用hdfs dfs -get拉到本地临时目录再处理,处理完删掉,临时目录按 task attempt id 命名,避免多个 mapper 抢同一个文件。同步回 MySQL 用一个独立脚本按video_id批量 upsert,每 500 条提交一次,别一条一条 insert。
4. 推荐与在线服务:混合召回、Flask 接口与 Vue 联调
4.1 行为采集与用户画像
推荐的原料是行为,不是播放量。曝光、点击、完播、点赞、评论、分享、负反馈这七类动作的信息量差别极大:曝光只说明「被推过」,完播才说明「内容留住了人」,快速划走则是明确的负反馈,权重必须为负。权重表要固定下来写进代码常量,不能散落在各处。
| 行为 | 权重 | 说明 |
|---|---|---|
| 曝光 | 0.1 | 只证明出现过,权重最低 |
| 点击 | 0.5 | 有兴趣,但可能只是被封面骗了 |
| 完播 | 1.0 | 内容真正被消费,最可靠的隐式反馈 |
| 点赞 | 1.5 | 主动正向 |
| 评论/分享 | 2.0 | 强正向,成本高 |
| 负反馈(3 秒内划走) | -2.0 | 明确不喜欢,必须参与计算 |
用户画像不用搞得太复杂,按分类维度聚合行为权重就够用:t_user_profile里存一个{category_id: score}的 JSON,每 10 分钟由定时任务重算一次。画像更新太频繁会让推荐结果抖动,用户刚点了一条美食视频,下一秒首页全变成美食,体验反而差。
4.2 混合召回与打分排序
单路召回一定不够用:ItemCF 受限于共现稀疏,内容召回受限于特征质量,热度召回毫无个性化。三路各出 50 条,加权融合成候选池,再按分数排序取前 N——这套结构在论文里叫混合推荐,在工程上就是三个函数加一个归并。
def hybrid_recall(user_id, item_cf, content_sim, hot_list, top_n=10): """三路召回加权融合。每路先归一化到 0~1,避免量纲差异导致某一路独大""" scores = {} # 1) 协同过滤:找相似用户看过的、当前用户没看过的 for vid, s in item_cf.get(user_id, {}).items(): scores[vid] = scores.get(vid, 0) + 0.5 * s # 2) 内容召回:按用户近期正反馈视频的融合向量找最近邻 for vid, s in content_sim.get(user_id, {}).items(): scores[vid] = scores.get(vid, 0) + 0.35 * s # 3) 热度兜底:保证新内容和新用户都有东西可推 for rank, vid in enumerate(hot_list[:50]): scores[vid] = scores.get(vid, 0) + 0.15 * (1 - rank / 50) # 已看过的不再重复推,同分类最多 3 条,防止首页全是同一类 ranked = sorted(scores.items(), key=lambda x: -x[1]) picked, cat_cnt = [], {} for vid, sc in ranked: cat = video_category(vid) if cat_cnt.get(cat, 0) >= 3: continue picked.append((vid, sc)) cat_cnt[cat] = cat_cnt.get(cat, 0) + 1 if len(picked) >= top_n: break return picked三个权重 0.5 / 0.35 / 0.15 是初始值,不是定论。调参的正确方式不是拍脑袋改,而是在离线评估集上跑 Recall@10 和覆盖率,看改完是涨还是跌,把对比表放进论文的实验章节。分类打散的阈值 3 也要在实验里验证,设成 1 会把结果打得太散导致相关性下降,设成 10 又等于没打散。
4.3 冷启动、多样性与实时性
冷启动分两种,处理方式完全不同。新用户没有任何行为,只能靠热度榜加一个「选三个你感兴趣的标签」的引导,把选的标签直接映射成category_id权重灌进画像,第二次请求就有个性化结果了。新视频没有行为数据,ItemCF 完全召不回来,只能靠内容相似度推给「看过同类内容」的用户,同时给小流量池做探索,播放数据回流后再交给协同过滤。
多样性除了分类打散,还有一个更隐蔽的点:同作者的视频要限流。一个作者连发五条,如果都进推荐流,用户会觉得这个平台内容很少。加一条「同作者最多 1 条」的规则,代价是几行代码,收益是体感上的内容丰富度。
实时性方面,别一上来就搞流式计算。行为表按 user_id 建了索引,每次请求时现查该用户最近 50 条行为来调整画像权重,延迟完全够用。等数据量真到了毫秒级要求,再考虑把行为写进消息队列做增量聚合,这是后面的事,不是开题阶段该操心的。
4.4 Flask 推荐接口与 Vue 页面联调
接口设计要克制:一个推荐接口只接受user_id、page、size三个参数,返回列表和分页信息,别把排序策略暴露成前端参数,那等于把黑匣子打开了又没完全打开。
from flask import Blueprint, request, jsonify, current_app from service.recommend import recommend_for_user bp = Blueprint("recommend", __name__, url_prefix="/api") @bp.route("/recommend", methods=["GET"]) def recommend(): uid = request.args.get("user_id", type=int) page = request.args.get("page", 1, type=int) size = min(request.args.get("size", 10, type=int), 50) # 上限 50,防止一次拉全站 if not uid: return jsonify(code=400, msg="user_id 必填"), 400 try: data = recommend_for_user(uid, page, size) except Exception: # 一定要 exception() 带堆栈,只写 error(msg) 排查时等于没有日志 current_app.logger.exception("推荐接口异常 uid=%s", uid) return jsonify(code=500, msg="推荐服务繁忙"), 500 return jsonify(code=0, data=data)前端只管拿数据渲染,把请求封在一个 api 文件里,别在组件里散着写 axios:
// api/recommend.js import request from '@/utils/request' export function fetchRecommend(userId, page = 1, size = 10) { return request.get('/api/recommend', { params: { user_id: userId, page, size } }) }联调阶段最常见的摩擦不是逻辑错,是格式错:Python 的datetime直接jsonify会变成"Tue, 26 Dec 2024 10:00:00 GMT",Vue 那边slice(0, 10)一切就废了。统一在后端做序列化,时间字段一律输出YYYY-MM-DD HH:mm:ss字符串,金额和分数统一保留两位小数,前后端约定一次,后面就不用反复吵。跨域在开发环境用 Flask-CORS 放开,生产环境交给 Nginx 同源代理,别把*带上去。
5. 上线前最容易翻车的五个点:排查清单
5.1 特征全变成零向量,推荐列表里总有几条视频永远不出现
现象是推荐结果正常,但某些视频无论怎么刷都推不出来,查特征表发现这些行的fused_vec全是 0。原因是抽帧阶段cv2.VideoCapture打开失败后代码走了return []分支,下游拿到空数组,np.mean对空数组返回 nan,序列化后变 0,整条链路一声不响。解决办法是在抽帧函数里直接抛异常,让批任务失败可见;同时在写特征表前加一道校验,向量模长小于 1e-6 的一律拒收并记入错误表,跑完批任务先看错误表条数,这条我一般放在验收清单第一条。
5.2 刷新几次推荐结果一模一样,用户觉得「这系统没在算」
现象是所有用户拿到的列表高度重合,或者同一用户连续刷新结果不变。原因通常有三个:召回没有排除已看过的视频,热度那一路权重过高直接盖掉了个性化,以及排序后没有做随机扰动导致分数相同的条目顺序固定。解决是维护一份「最近 7 天已观看」集合做过滤,把热度权重压到 0.15 以下,排序时对同分条目加一个极小的随机项。判断是不是这个问题的快办法:拿两个行为差异很大的测试账号请求同一接口,结果重合度超过 70% 就说明个性化那两路基本没生效。
5.3 接口报 500 但日志里什么都没有
现象是前端拿到 500,后端控制台干干净净。原因是路由里写了try/except却只return jsonify(msg="error"),异常被吞了。解决是把except里改成current_app.logger.exception(...),它会带上完整堆栈;更彻底的做法是只在最外层做统一异常处理,业务代码里不要到处 try,让异常自然冒到能看见的地方。这条坑我踩过两次,第二次是因为复制了第一次的代码。
5.4 Hadoop 任务卡在 reduce 99% 不动
现象是 map 阶段很快跑完,reduce 长时间停在 99%。原因是数据倾斜:某个热门视频被大量用户行为关联,分到同一个 reducer 上的记录数远超其他分区。解决办法是在 mapper 输出 key 上加随机后缀打散,比如video_id#0~9,reduce 完再合并;或者干脆不用 reduce,map 端直接输出到 HDFS 目录,用cat合并。毕设规模下后者更省事,别为了「看起来像分布式」硬凑 reduce 逻辑。
5.5 联调时列表页一片空白,控制台报 CORS
现象是 Postman 请求接口正常,浏览器里请求被拦。原因是开发阶段前端跑在 8080、后端跑在 5000,跨域没放行,或者放了Access-Control-Allow-Origin: *但请求带了 cookie 导致浏览器拒绝。解决是开发环境用 Flask-CORS 显式指定来源,生产环境用 Nginx 把/api反代到后端,前后端同源,跨域问题从根上消失。别在 Vue 里配 proxy 就以为万事大吉,打包部署后 proxy 是不生效的,这个坑每年都有人踩。
6. 验收与答辩:把「能跑」讲成「可验证」
答辩现场最尴尬的不是系统有 bug,是老师说「你演示一下推荐效果」,你点开首页发现推的还是热度榜。避免这个场面的办法是在验收前自己先跑一遍清单,每一项都要有可展示的证据,而不是「我记得能跑」。
| 验收项 | 验收动作 | 通过标准 |
|---|---|---|
| 内容理解 | 抽 3 条不同类目视频,打印融合向量并算两两余弦 | 同类目相似度明显高于跨类目,差值大于 0.1 |
| 特征完整性 | 查特征表总数与视频表上架数对比 | 覆盖率 100%,错误表条数为 0 |
| 推荐个性化 | 用两个行为差异大的账号请求接口 | 结果重合度低于 40% |
| 冷启动 | 新建空行为账号请求推荐 | 有返回且不为空,不报错 |
| 响应时间 | 连续请求 50 次记录耗时 | 平均低于 300ms,无超时 |
| 离线任务 | 重跑一次批任务 | 幂等,重复跑结果一致,不产生重复行 |
演示顺序也有讲究。先讲数据流——一条视频从上传到进入 HDFS、抽帧、提特征、落 MySQL,讲完这条线,老师就知道你不是调包;再开系统演示,演示时用一个「已经养了行为」的账号,别用新号,新号推热度榜看不出效果;最后打开实验对比表,讲混合召回和单路召回的 Recall@10 差异、分类打散前后的覆盖率变化。指标不用漂亮,但必须是自己跑出来的,被问「这个数怎么来的」要能立刻翻到对应的脚本和数据集。
我自己的习惯是答辩前一晚把整条链路从零跑一遍:删库、导数据、跑批任务、起服务、走一遍六个页面。跑通了才敢睡,跑不通至少还有一晚上时间。这个习惯是被一次「本地好好的、现场数据库连不上」教出来的,从那以后我每次交付前都强制走一遍全链路冷启动,不看缓存、不看好几小时前跑起来的环境。希望这套结构和清单能帮到你,少走几个我走过的弯路。
本文还有配套的精品资源,点击获取