简介:基于Python的知识图谱与图神经网络电影推荐系统,是一套完整的高分毕业设计项目,源码经导师指导并通过评审,得分98分,覆盖电影推荐场景中数据预处理、知识图谱构建、图神经网络模型训练到Web可视化展示的完整链路,适合计算机相关专业学生完成毕业设计或大作业,也适合中级开发者进行推荐系统项目实战。压缩包共31个文件,主体为21个Python脚本(对应知识图谱构建、数据预处理、模型训练、网页应用等模块),5个dat格式数据集(用户、评分、电影数据),另含readme与md说明文档,整体大小仅14.84MB,结构清晰,便于按模块阅读与调试。目前已有50人学习下载,项目具备较高的参考价值。从内容预览可见,项目不仅提供可直接运行的训练测试脚本,还包含知识图谱构建工具、图神经网络模型层、数据加载器与Web端展示。借助这套代码,读者能深入理解知识图谱与图神经网络融合实现电影推荐的技术路线,也可将其作为优质毕设模板进行二次开发。
1. 为什么“知识图谱 + 图神经网络”成了电影推荐系统的毕设标配?
先给一个反直觉的结论:一套能跑通的电影推荐系统,最花时间的往往不是模型代码,而是把电影、演员、导演、类型这些数据揉成一张可用的图。传统协同过滤只盯着用户-物品评分矩阵,一部新电影没有交互数据就直接冷启动;而知识图谱给物品之间补上了语义路径,“用户 A 看过 X,X 和 Y 有同一个主演”,图神经网络就能沿这些路径做消息传递。于是“Python 实现的知识图谱 + 图神经网络电影推荐系统”成了源码、数据、可视化齐全的优质毕业设计方向。这篇文章按“数据→图谱→GNN→训练→评估→避坑”的链路,把它拆成可以跟着复现的实战记录。适合准备毕设、想把它写成简历项目的同学。
2. 知识图谱构建:从 MovieLens 到三元组和邻接矩阵
我一般会把整个系统的第一步定义成知识图谱构建。这个阶段的目标不是把模型跑起来,而是把散落的评分记录变成 (head, relation, tail) 的三元组集合,再转成图神经网络能消费的稀疏矩阵。很多同学拿到源码包后第一反应是去看模型文件,结果卡在数据加载上——节点编号对不上、关系边重复、正负样本混在图里,后续训练再用力也是白费。这一章按能一遍跑通的顺序讲。
2.1 为什么推荐不用纯评分矩阵而用知识图谱
协同过滤的假设是“喜欢过相似物品的人会喜欢同一件物品”,但当评分矩阵稀疏到百分之九十以上都是空值时,用户向量和物品向量都不可靠。电影推荐里的稀疏尤其明显:头部电影有几十万人评分,长尾电影只有个位数评价。知识图谱解决的是“物品之间怎么产生联系”的问题——即使两个电影没有任何共同评分者,只要它们共享同一个导演、演员或类型,就能在图上形成一条可见的路径。图神经网络沿着路径把邻居信息汇聚到节点表示上,用户向量和物品向量因此带上了语义。
2.2 数据选型:公开数据集优先,别急着爬 JSON
常见做法是直接下载 MovieLens 公开数据集,1M 版本有一百多万条评分,文件几十 MB,字段干净还带时间戳。用它做毕业设计足够:数据量能撑起 GNN 训练,又不会大到每次前向都要等半天。有些同学想拿电影网站 JSON 源码的数据,但很多 JSON 字段命名不统一,类型和演员数据要额外清洗,反而把主要精力消耗在爬虫和规则上。我的建议是:先把公开数据跑通,再决定要不要用爬虫数据替换实体表。
| 数据源 | 优点 | 常见坑 |
|---|---|---|
| MovieLens 1M | 字段规范、有时间戳 | 没有演员/导演关系,需补外部数据 |
| MovieLens 100K | 更小、上手快 | 数据量小,GNN 效果不明显 |
| 电影网站 JSON | 实体丰富、贴近真实业务 | 字段脏、需爬虫加清洗 |
时间戳很重要,它决定后面做时间切分时能不能用。
2.3 先切分再建图:三元组抽取的正确顺序
最开始我犯过的错是先把所有评分都揉进邻接矩阵再切训练测试集,结果测试集的正样本通过图结构泄漏进了训练,Recall 虚高到 0.6,换一个切分方案就掉到 0.2。血泪经验:先按时间戳把评分数据切开,然后用训练集部分去构建知识图谱。
import pandas as pd # 读取 MovieLens 1M ratings = pd.read_csv("ml-1m/ratings.dat", sep="::", names=["user_id", "movie_id", "rating", "timestamp"], engine="python", encoding="latin-1") movies = pd.read_csv("ml-1m/movies.dat", sep="::", names=["movie_id", "title", "genres"], engine="python", encoding="latin-1") # 按时间排序后切分:前 80% 训练,后 20% 测试 ratings = ratings.sort_values("timestamp").reset_index(drop=True) split = int(len(ratings) * 0.8) train_ratings = ratings.iloc[:split].copy() test_ratings = ratings.iloc[split:].copy() print(f"训练交互 {len(train_ratings)},测试交互 {len(test_ratings)}")MovieLens 1M 的分隔符是双冒号,所以sep写成::,同时engine必须指定 python,否则 C 引擎会误判。timestamp是 Unix 秒,直接 sort 即可。切分比例按常见 80/20,如果后面要加验证集,可以按 70/15/15 再拆一次。
紧接着抽取三元组:
entities = set() triples = [] # 电影 -> 类型 for _, row in movies.iterrows(): mid = f"movie_{row['movie_id']}" entities.add(mid) for genre in str(row["genres"]).split("|"): g = f"genre_{genre.strip()}" entities.add(g) triples.append((mid, "belongs_to", g)) # 训练集里 rating >= 4 视为“喜欢”边 for uid, mid, rating in train_ratings[["user_id", "movie_id", "rating"]].itertuples(index=False): if rating >= 4: u = f"user_{uid}" m = f"movie_{mid}" entities.add(u) entities.add(m) triples.append((u, "likes", m)) print(f"实体数 {len(entities)},三元组数 {len(triples)}")抽取规则里故意把rating >= 4定成正边,把 3 分以下排除。因为评分 1-3 的交互更适合当负样本,放进图里会让“不喜欢”和“未评价”边界模糊。阈值可以调成 3.5 或 4,但要保证训练正样本足够,MovieLens 1M 用 4 大概能留出 55% 的交互。实体统一加user_、movie_、genre_前缀,这一步看起来多余,实际能防掉后面要讲的 ID 撞车。
2.4 用邻接矩阵给图神经网络“喂图”
有了三元组,GNN 还差一个入口。常见的做法是把它拉成 COO 稀疏矩阵,再加自环、对称归一化。自环让节点在卷积时保留自身信息;对称归一化的目的是让度数高的节点不会在聚合中主导数值。
import numpy as np import scipy.sparse as sp import torch node2idx = {node: i for i, node in enumerate(entities)} num_nodes = len(node2idx) rows, cols, data = [], [], [] for h, _, t in triples: rows.append(node2idx[h]) cols.append(node2idx[t]) data.append(1.0) adj = sp.coo_matrix((data, (rows, cols)), shape=(num_nodes, num_nodes)) # 加上反向边,让消息能双向传播 adj = (adj + adj.T).tocsr() def normalize_adj(a): a = a.tocoo() d = np.array(a.sum(axis=1)).flatten() d_inv_sqrt = np.zeros_like(d, dtype=float) mask = d > 0 d_inv_sqrt[mask] = 1.0 / np.sqrt(d[mask]) diag = sp.diags(d_inv_sqrt) return (diag @ a @ diag).tocoo() adj_norm = normalize_adj(adj) # 转成 PyTorch 稀疏张量 def sparse_to_tensor(a): a = a.tocoo() values = torch.from_numpy(a.data).float() indices = torch.from_numpy(np.vstack((a.row, a.col))).long() return torch.sparse_coo_tensor(indices, values, a.shape) adj_t = sparse_to_tensor(adj_norm) print("邻接矩阵", adj_norm.shape)这里做的是 $D^{-1/2}AD^{-1/2}$ 规范化,$D$ 是节点度对角矩阵,代码里diag @ a @ diag就是矩阵形式。用np.zeros_like先初始化,再对d>0的位置赋值,避免除零警告。注意最终adj_t必须是 float32 稀疏张量,否则后面和 embedding 相乘会报 dtype 不一致。
注意:如果不加自环,两层 GCN 等于只聚合两跳邻居,节点自身信息会随层数衰减。我的做法是加自环后再归一化,也就是在
adj = adj + sp.eye(num_nodes)之后再做规范化。
3. 图神经网络模型:用 GCN 把知识图谱变成用户向量
模型层最核心的决策不是用哪个框架,而是用哪一类图神经网络。PyTorch 自己拼一个 GCN 就够用,不必为了毕设强行上复杂的库。这一章从选型讲到最小实现,再讲损失函数,最后给 RGCN 的扩展方法。
3.1 GCN / GAT / RGCN 怎么选
答辩时老师常问“你为什么选这个模型”。我一般按关系类型数量来决定:关系只有belongs_to和likes两类时,GCN 够用;关系种类超过 5 类,且每种关系对推荐的贡献不同,用 RGCN;想在聚合时给不同邻居分配重要程度,可以换成 GAT。
| 模型 | 关系类型数 | 邻居权重 | 实现成本 | 典型场景 |
|---|---|---|---|---|
| GCN | 少,可合并 | 固定 1/度 | 低 | 基础毕设、快速验证 |
| GAT | 少到中 | 注意力得分 | 中 | 需要解释邻居贡献 |
| RGCN | 多,语义差异大 | 每种关系一个变换 | 高 | 外部知识图谱关系丰富时 |
这一步的选型直接决定后面代码量。GCN 最省事,后边的坑也最少;GAT 有注意力权重,但训练更敏感;RGCN 更贴合“知识图谱”四个字,但要处理每个关系对应的稀疏矩阵。下面按 GCN 讲完整链路,最后给 RGCN 的改法。
3.2 手写一个两层 GCN:全图前向的最小实现
PyTorch 里写 GCN 不用装额外的图神经网络库,自己拼两个线性层和稀疏矩阵乘法就够了。
import torch import torch.nn as nn import torch.nn.functional as F class GCN(nn.Module): def __init__(self, num_nodes, embed_dim=64, hidden_dim=128, out_dim=64): super().__init__() self.node_embedding = nn.Embedding(num_nodes, embed_dim) self.lin1 = nn.Linear(embed_dim, hidden_dim) self.lin2 = nn.Linear(hidden_dim, out_dim) def forward(self, adj_t): # 全部节点一次前向:先取 embedding,再过两层图卷积 x = self.node_embedding.weight # [num_nodes, embed_dim] x = self.lin1(x) x = torch.sparse.mm(adj_t, x) # 聚合邻居特征 x = F.relu(x) x = self.lin2(x) x = torch.sparse.mm(adj_t, x) return x # [num_nodes, out_dim]这里没有给每个 batch 单独传节点索引,而是在训练前用整图做一次前向。好处是代码简单、不容易出错;代价是整图全算一次,节点量在十几万以下时能接受。lin1和lin2是逐点线性层,真正做邻居信息汇聚的是两个torch.sparse.mm调用。如果想用 GAT 的注意力机制,需要把lin输出后按边做注意力计算,代码量会翻倍。
参数说明:embed_dim是节点向量的初始维度,也是 embedding 表的宽度;hidden_dim是隐藏层宽度,常见 128;out_dim是最终向量维度,决定后面点积打分的表达力。out_dim和embed_dim不用相等,推荐打分时用户向量和物品向量在同一空间即可。
3.3 BPR 损失与正负采样
推荐场景里没有绝对负样本——用户没评分不代表不喜欢。常见的做法是 BPR loss:让“用户-正样本”的打分尽量大于“用户-负样本”,差值越大越好。负样本从用户没看过的电影里随机采。
def bpr_loss(user_emb, pos_emb, neg_emb): pos_score = (user_emb * pos_emb).sum(dim=-1) neg_score = (user_emb * neg_emb).sum(dim=-1) return -torch.log(torch.sigmoid(pos_score - neg_score) + 1e-8).mean()训练时输入是一批用户的user_emb、正样本电影pos_emb和负样本电影neg_emb,维度都是 [batch_size, out_dim]。加1e-8是为了防止 sigmoid 饱和时 log 输出 inf。BPR 是排序式目标,只关心正负样本的相对大小,不要求绝对打分,所以评估时可以直接用点积分数排序。
负采样细节放到训练循环里一起讲。这里想强调:BPR 的实现不要用 BCE 替换——BCE 对每个样本单独拟合 0/1 标签,而 BPR 是通过两个样本的相对关系来优化,前者在隐式反馈数据上容易把未观察到的交互全学成负样本,效果明显差一截。
3.4 想贴“知识图谱”标签:RGCN 怎么改
如果知识图谱关系类型很多,比如 actor、director、genre、writer、country 等等,建议把 GCN 升级成 RGCN。RGCN 的做法是为每种关系保留一个变换矩阵,聚合时先按关系分别做线性变换,再求和。核心改动只有一处:原来x = lin1(x)变成一个循环,每种关系 r 对应一个lin_r,最后对所有关系的结果求和。
class RGCNLayer(nn.Module): def __init__(self, in_dim, out_dim, num_relations): super().__init__() self.linears = nn.ModuleList([nn.Linear(in_dim, out_dim) for _ in range(num_relations)]) def forward(self, x, adj_by_rel): out = 0 for r, adj_t in enumerate(adj_by_rel): out = out + torch.sparse.mm(adj_t, self.linears[r](x)) return outadj_by_rel是每类关系单独归一化的邻接矩阵列表。这个版本只适合关系数量不多的情况,否则参数量会变成关系数量的线性层倍数。毕业设计如果只有 movie-genre 和 user-movie 两类关系,GCN 够用了,RGCN 可以作为答辩时的扩展点提一句。
4. 训练评估与推荐:让模型输出“猜你喜欢”
前面几章的产物——邻接矩阵、GCN、BPR 损失——要组装成一条能出推荐结果的流水线。这一章的代码都不长,但顺序很重要。先准备训练数据,再写训练循环,最后评估和输出。
4.1 训练循环:全图前向加批量采样
先把训练用到的正负样本工具准备好。
import random # 用户 -> 交互过的电影集合,只来自训练集 train_user_set = {} for uid, mid, _ in train_ratings.itertuples(index=False): train_user_set.setdefault(uid, set()).add(mid) movie_id_list = movies["movie_id"].tolist() def sample_neg(user_id, k=1): negs = [] while len(negs) < k: mid = random.choice(movie_id_list) if mid not in train_user_set.get(user_id, set()): negs.append(mid) return negs[0]训练集用户集合只保留有正交互的用户;负采样在全部电影列表里随机挑,但要把该用户已经交互过的电影全排除,否则正样本被当成负样本,梯度方向互相打架。
训练代码可以只做一个 epoch 一次全图前向:
model = GCN(num_nodes, embed_dim=64, hidden_dim=128, out_dim=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(50): model.train() optimizer.zero_grad() node_emb = model(adj_t) # 全图前向一次 total_loss = 0.0 for _ in range(128): # 128 个三元组累积成一个 batch uid = random.choice(list(train_user_set.keys())) pos = random.choice(list(train_user_set[uid])) neg = sample_neg(uid) ui = node2idx[f"user_{uid}"] pi = node2idx[f"movie_{pos}"] ni = node2idx[f"movie_{neg}"] loss = bpr_loss(node_emb[ui], node_emb[pi], node_emb[ni]) total_loss = total_loss + loss total_loss.backward() optimizer.step() if epoch % 10 == 0: print(f"epoch {epoch}, loss {total_loss.item():.4f}")代码里一个 epoch 只做一次全图前向,但通过 128 个三元组累积梯度。节点 embedding 在同一个 epoch 内固定,梯度通过 loss 流回模型参数,下一轮前向会拿到更新后的 embedding。这样比每个三元组单独 forward 快很多。Adam 学习率1e-3是推荐场景的常见起点,如果 loss 震荡就降到3e-4。
4.2 评估:用 Recall@K 判断推荐“准不准”
评估时需要拿没参与训练的测试交互来算。我习惯按用户逐个打分,排除训练集交互,再统计 Top-K 里命中测试集正样本的比例。
def evaluate(model, test_ratings, train_user_set, movie_id_list, K=10): model.eval() node_emb = model(adj_t) recall_sum, n_eval = 0.0, 0 for uid in set(test_ratings["user_id"]): if uid not in train_user_set: continue test_pos = set(test_ratings.loc[test_ratings["user_id"] == uid, "movie_id"]) u_emb = node_emb[node2idx[f"user_{uid}"]] scored = [] for mid in movie_id_list: if mid in train_user_set[uid]: continue m_emb = node_emb[node2idx[f"movie_{mid}"]] score = torch.sigmoid((u_emb * m_emb).sum()).item() scored.append((mid, score)) scored.sort(key=lambda x: x[1], reverse=True) hit = len(test_pos & {mid for mid, _ in scored[:K]}) / max(len(test_pos), 1) recall_sum += hit n_eval += 1 return recall_sum / max(n_eval, 1)这里 K 取 10,也可以同时输出 Recall@5 和 @20。node_emb在一次评估中固定,所以对每个用户循环打分不会重复计算前向。注意不能用训练状态的前向结果去算评估分数,否则结果会虚高。如果测试集用户没出现在图里,直接跳过,这是冷启动的边界,GNN 也救不了纯新用户。
4.3 推荐输出:把分数映射回电影标题
评估确认有效后,写一个推荐函数给业务侧调用。
def recommend(uid, top_k=10): model.eval() node_emb = model(adj_t).detach() u_emb = node_emb[node2idx[f"user_{uid}"]] scored = [] for mid in movie_id_list: if mid in train_user_set.get(uid, set()): continue score = torch.sigmoid((u_emb * node_emb[node2idx[f"movie_{mid}"]]).sum()).item() scored.append((mid, score)) scored.sort(key=lambda x: x[1], reverse=True) top_movies = scored[:top_k] title_map = dict(zip(movies["movie_id"], movies["title"])) return [(title_map[mid], round(score, 4)) for mid, score in top_movies] recommend(142)输出长这样:
| 排名 | 电影标题 | 得分 |
|---|---|---|
| 1 | The Matrix (1999) | 0.912 |
| 2 | Fight Club (1999) | 0.907 |
| 3 | The Dark Knight (2008) | 0.884 |
示例数值取决于数据切分和训练轮数。只要 Top10 里混进奇奇怪怪的片子,优先去查图谱边是否建错,而不是换模型。
4.4 参数速查:让复现不再玄学
| 参数 | 推荐值 | 说明 |
|---|---|---|
| embed_dim | 64 | 小数据集 32 也够,太大会 OOM |
| hidden_dim | 128 | 两层 GCN 的隐藏层宽度 |
| out_dim | 64 | 与 embed_dim 不必一致,但要统一 |
| 图卷积层数 | 2 | 超过 3 层会过平滑,节点向量趋同 |
| 学习率 | 1e-3 | Adam;震荡时降到 3e-4 |
| 负采样数 | 1 | BPR 一个正样本配一个负样本即可 |
这些是可以照着抄的经验值。真正需要调的是数据切分比例和 rating 阈值,它们对结果的影响比模型宽度更明显。
5. 避坑:知识图谱 + 图神经网络推荐最容易翻车的 5 个地方
5.1 实体编号冲突:user_1 和 movie_1 撞车
现象:训练 loss 下降得很快,但推荐结果全是同一个用户最近看过电影的同类片子,换一批数据效果天差地别。
原因:MovieLens 里 user_id 和 movie_id 都是从 1 开始递增,如果建节点时直接用str(id)而不是加前缀,用户 1 和电影 1 在node2idx里共享同一个索引。邻接矩阵里 user 节点和 movie 节点语义串位,GNN 根本分不清谁是用户谁是电影。
解决:所有实体 ID 统一加前缀,比如user_、movie_、genre_。建索引前检查len(set(entities))是否等于三元组里出现过的实体数。这个坑我踩过两次,现在代码里宁可多写一行前缀,也不省事。
5.2 数据泄漏:测试集进了邻接矩阵
现象:训练集上的 Recall@10 有 0.5,测试集上却只有 0.1,两者差距大到明显不合理。
原因:构建三元组时如果先把所有评分数据都放进图里,测试集中的正样本会通过图卷积的高阶邻居信息“渗透”到训练向量中。比如测试中用户 A 喜欢电影 X,而 X 在图中与用户 B 相连,A 的邻居 B 会把 X 的信息带过来,等于考试答案提前写在草稿纸上。
解决:严格先在 2.3 节的“按时间切分”处把 train/test 分开,然后用train_ratings构建 entities 和 triples。评估时不要让模型在训练图的隐藏层里看到任何测试交互。如果用了外部知识图谱数据,也要把测试集中的用户-电影边剔除后再建图。
5.3 邻接矩阵不归一化:训练震荡或者梯度爆炸
现象:loss 在前 5 个 epoch 里剧烈抖动,甚至出现 NaN;把学习率降到 1e-5 才勉强训练,但效果很差。
原因:直接拿未归一化的adj去做矩阵乘法,高分度节点的值可能在几百到几千,多层卷积后数值指数增长。GCN 公式里需要 $D^{-1/2}AD^{-1/2}$ 归一化,否则相当于模型在放大邻居数量带来的影响。
解决:2.4 节里的normalize_adj不能省。还有一个细节:归一化后邻接矩阵的行和不是 1,这是对称规范化,不是按行规范化,两者都可以用,但别改一半——改了行归一化却保留稀疏格式,会让稀疏矩阵乘法结果偏向某些节点。
5.4 负采样采到“看过但没打高分”的电影
现象:训练 loss 不掉,或者推荐列表里全是热门大片,因为模型没学会区分“用户没看过的电影”和“用户不喜欢的电影”。
原因:负采样只排除了train_user_set中的高分交互,但 MovieLens 里用户可能给某部电影打了 2 分,这同样是一条重要信号。把它当成负样本没问题,但如果把它当成“没看过”的随机负样本,模型很难学到评分等级差异。
解决:把评分小于 4 的电影也加进用户的已交互集合里,负采样时一并排除。更稳妥的做法是:评分 1-3 的交互既不放进图里,也不放进负采样候选里,让正负样本边界更干净。实际效果取决于数据分布,我一般会把 4 分以下全部视为弱交互。
5.5 全图前向 OOM:节点量超过 20 万就扛不住
现象:model(adj_t)时内存暴涨,开发环境直接崩掉;把 embedding 维度从 64 降到 8 也没有明显改善。
原因:全图前向需要存下所有节点的中间特征,[num_nodes, hidden_dim]在节点数超过 20 万、hidden_dim 为 128 时会占约 200MB 内存,加上稀疏矩阵乘法和反向传播的临时变量,轻松到几个 GB。如果还叠加多个 RGCN 关系矩阵,OOM 几乎必然。
解决:降维度、减层数是最快的后悔药。再进一步,用邻居采样代替全图前向——每个 batch 只取目标用户的两跳邻居子图,常见图神经网络库里的NeighborLoader就是做这个的。如果不想引入新依赖,就按用户分片,一次只算 5000 个节点的子图,分批 embedding 后拼起来。
6. 进阶验证:三种能说服答辩老师的实验方法
如果只跑通一条训练流程,答辩时很容易被问住:你怎么证明知识图谱和 GNN 真的有用?我自己习惯用三个实验补证据。
6.1 对比实验:和矩阵分解 PK
用同一个训练/测试切分,把 GCN 模型的输出换成一个简单的矩阵分解基线。MF 只使用 user-movie 边,而 GCN 加入了 movie-genre 边,这样对比可以直观看到知识图谱边带来的增益。记录两者在 Recall@10 和 NDCG@10 上的差值,哪怕 GCN 只高 1-2 个点,也比没有任何对比强得多。对比实验记得固定随机种子,否则差异可能来自初始化而不是模型结构。每次跑之前把random.seed(42)和torch.manual_seed(42)写好,这个习惯能去掉 80% 的玄学争议。
6.2 消融实验:看知识图谱边是不是真被用上了
把 movie-genre 边从邻接矩阵里去掉,只保留 user-movie 边,其余参数不变,训练一轮看结果。如果效果掉下来,说明 GCN 确实通过电影-类型路径学到了物品间相似;如果效果差不多,可能是数据切分或阈值有问题,需要回头检查。我一般会把去掉知识图谱边的结果当成底线。如果 GCN 模型的表现不低于底线,说明图结构没有起到作用;反之,则是有效证据。
6.3 可解释性:追一条推荐路径
挑一个测试集用户,看 Top1 电影,然后从知识图谱里找用户最近看过电影和这个 Top1 电影的公共子图路径。例如:“用户看过《黑客帝国》,它和《盗梦空间》同属 Sci-Fi 类型,所以被推荐”。把这条路径截图放进毕业设计论文里,比任何 loss 曲线都能直接证明模型原理。我自己的习惯是:不管模型多复杂,都在训练前先把一条推荐路径手工画出来,等训练后再对照。如果手工预期和模型输出差太远,多半是图建错了,而不是模型没学好。这个习惯帮我省了大量无效调参时间,也希望帮到你。
本文还有配套的精品资源,点击获取