ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于协同过滤的图书推荐系统:算法实现与前后端落地避坑指南

基于协同过滤的图书推荐系统:算法实现与前后端落地避坑指南

简介:一份基于Python与Vue的协同过滤图书推荐系统毕业设计资料包,面向计算机专业毕业生、推荐系统学习者及需要快速搭建完整项目的开发者。系统覆盖用户、图书、个性化推荐与结果展示等核心模块,实现基于用户与基于物品两种协同过滤算法,并融入前后端分离开发模式,可用于理解推荐机制与工程实现。资源共346个文件,含Python后端源码、Vue组件、JavaScript/CSS前端资源、SQL数据库设计脚本及mp4演示视频等,压缩包约24.6MB,附有一键运行脚本,目录结构清晰。目前已有81人学习下载,适合作为毕业设计选题或实战练手项目。除完整可运行工程外,还提供数据库表结构设计文档与演示录屏,便于快速掌握表关系、接口调用及推荐流程;源码模块划分合理,注释清晰,有利于二次开发和功能扩展,也可作为课程设计或工程实训的参考。

1. 毕业设计选图书推荐系统:为什么大家都在做协同过滤,又为什么容易翻车

每次看到“基于协同过滤算法的图书推荐系统”这类题目,我都觉得选得挺聪明:图书数据好找、评分语义清晰、推荐效果肉眼可见,而且Python+Vue这套组合既能展示算法能力又能展示工程能力,答辩时不缺东西讲。但真动手的人才会发现,最难的不是把协同过滤公式敲出来,而是你兴冲冲跑完代码后,推荐结果跟人工随机排序差不多——这才是这个题目真正的分水岭。

这篇笔记把整套方案从头到尾拆开讲:技术选型怎么做、UserCF和ItemCF怎么取舍、评分矩阵和相似度计算怎么写、Vue前端怎么把推荐结果接得自然,以及冷启动、稀疏矩阵、跨域这些坑为什么一定会踩。面向的是正在做毕业设计或想把这个方向落地成作品集的开发者,纯新手能照着复现,有点基础的也能在参数和边界上少走弯路。

2. 技术选型与工程结构:怎么搭出一个能答辩也能扩展的图书推荐系统

2.1 为什么是Python+Vue:两侧各自的不可替代性

毕业设计选型首先要考虑的是“每一层都有工作量可写、有截图可截”。Python负责算法和数据处理,Vue负责前端页面,两边分工明确,中间用HTTP接口连接,天然就是前后端分离结构。常见做法是后端用Flask或FastAPI暴露接口,推荐算法和数据库操作都收在Python里;前端用Vue CLI或Vite创建工程,配合Vue Router做页面跳转、Axios做请求,Element UI或Vant做图书卡片和评分弹窗。

有同学问过“能不能全用Python,前端用模板渲染”,技术上当然可以,但答辩时你的项目结构会显得单薄。Vue侧的加入不只是界面更漂亮,更是把“用户行为采集、推荐列表渲染、图书详情交互”这三块从前端角度做了完整闭环——而这恰好是图书馆管理系统类题目最容易被评委追问的地方。Python侧则负责把协同过滤算法的输入输出讲清楚,这比单纯写CRUD高级得多。

2.2 UserCF还是ItemCF:图书场景下的算法选型依据

协同过滤分两大类:基于用户的UserCF和基于物品的ItemCF。早期的推荐系统常用UserCF,社区性强,你朋友喜欢什么就推给你什么;但用户数量增长后,用户相似度矩阵的计算量是用户数的平方,而且新用户很难找到相似邻居,这是“冷启动”的经典来源。ItemCF则是先找和你历史评分过的图书相似的其它图书,再按相似度加权推荐给你,物品数量远小于用户数量时,计算量和维护成本都可控。

图书推荐场景下,我一般建议优先做ItemCF,尤其是数据量在几千本书、几百个用户的毕设规模时。原因有三条:书和书的相似关系相对稳定,不会像用户兴趣那样一个月变三次;新书只需要算它和已有书的相似度就能进入推荐池;更重要的是,ItemCF的结果更容易向评委解释——“因为你看过《三体》,所以推荐刘慈欣的其它作品”,这种因果讲法比“因为有381个相似用户”直观得多。当然,最后系统里可以同时保留UserCF选项,用参数切换,这本身就是加分项。

2.3 数据库表与工程目录:源文件的组织方式

数据库设计是很多人的扣分点,因为MySQL建表谁都会,但建出来的表能不能支撑推荐算法,完全是另一回事。图书推荐系统至少需要四张核心表:用户表、图书表、评分表、行为日志表。用户表和图书表是基础数据,评分表是协同过滤的输入,行为日志表用来缓解冷启动和做后续的离线评估。

评分表的字段建议这样设计:id、user_id、book_id、score、create_time,再加一个唯一索引(user_id, book_id),防止同一用户对同一本书重复评分。行为日志表则记录浏览、点击、收藏、评分四类行为,字段为id、user_id、book_id、behavior_type、create_time。很多毕设只做了评分表,等到想加“猜你喜欢”之外的推荐理由时发现没数据可用,只能返回空列表,非常被动。

后端工程结构按模块拆:app.py或main.py做接口入口,models/放数据库模型,services/放协同过滤算法与推荐服务,utils/放数据预处理和相似度计算函数,data/放原始CSV或SQL初始化脚本。前端工程则按页面拆:views/Home.vue做图书展示、views/Recommend.vue做推荐结果页、views/BookDetail.vue做详情与评分入口。源码包里常见的就是这套结构,拿到手后先对着目录找这几个文件夹,比盲目改代码高效得多。

3. 算法核心落地:用Python实现ItemCF并暴露成API

3.1 构建评分矩阵:从数据库查询到用户-物品倒排表

协同过滤的第一步是把数据库里的评分记录变成算法能算的矩阵。最朴素的做法是pandas的pivot_table,行是用户,列是图书,值是评分。但实际跑起来会发现两个问题:一是矩阵极其稀疏,绝大多数格子是NaN,直接拿去算余弦相似度会得到一堆没意义的结果;二是pivot后的矩阵占内存,几千本书几百个用户还好,上万本书就开始吃力。

常见做法是转换成“用户-物品倒排表”:一个字典,key是用户ID,value是该用户评过分的图书ID集合或评分字典。计算物品相似度时,只需要遍历每个用户的评分记录,统计两两图书共同被评分的次数。下面给出构建倒排表并统计物品共现矩阵的代码,这是ItemCF最基础但也最容易写错的一步。

from collections import defaultdict import pandas as pd def build_inverted_table(ratings_df): """ 从评分表构建用户->物品倒排表 ratings_df: 必须包含 user_id, book_id, score 三列 """ inverted = defaultdict(dict) for row in ratings_df.itertuples(): inverted[row.user_id][row.book_id] = row.score return inverted def build_cooccurrence(inverted_table): """ 统计物品两两共同被评分的次数 返回: cooccur[book_a][book_b] = 共同评分用户数 """ cooccur = defaultdict(lambda: defaultdict(int)) for user, items in inverted_table.items(): book_list = list(items.keys()) for i in range(len(book_list)): for j in range(i + 1, len(book_list)): book_a, book_b = book_list[i], book_list[j] cooccur[book_a][book_b] += 1 cooccur[book_b][book_a] += 1 return cooccur

这段代码的关键在于itertuples()遍历DataFrame比iterrows()快不少,在几千行评分数据上差距不明显,但养成熟练习惯没坏处。cooccur用了嵌套默认字典,外层key是图书ID,内层key是另一本图书ID,值是两个用户共同评过这两本书的人数。这里只统计了共同评分次数,还没引入评分值,因为相似度计算时往往需要区分“都打了高分”和“一个打高分一个打低分”,那要走到下一步的加权方案。

3.2 相似度计算:余弦相似度和皮尔逊相关系数的取舍

物品相似度的经典计算方式是余弦相似度:把每个用户对某本书的评分看成向量的一维,两本书的评分向量越接近,夹角越小,余弦值越接近1。但原始余弦相似度有个天生的坑:它不管评分绝对值,只看向量方向。用户A给所有书都打5分,用户B给所有书都打3分,这两人的评分向量方向完全一致,余弦相似度会是1,可实际上他们的口味差别很大。

所以在图书推荐里,我更推荐先做“均值中心化”再算相似度,也就是皮尔逊相关系数的思路:每个评分减去该用户对所有书的平均分,消除用户打分尺度的偏差。下面给出完整实现,并把最终相似度结果转成稠密矩阵方便后续召回。

import numpy as np from scipy.spatial.distance import cosine def pearson_similarity(inverted_table, book_a, book_b): """ 计算两本书的皮尔逊相关系数 只使用同时给这两本书评过分的用户作为样本 """ common_users = set(inverted_table.keys()) # 实际上需要筛选同时评过 book_a 和 book_b 的用户 common_users = [u for u in common_users if book_a in inverted_table[u] and book_b in inverted_table[u]] if len(common_users) < 2: return 0.0 scores_a = np.array([inverted_table[u][book_a] for u in common_users], dtype=float) scores_b = np.array([inverted_table[u][book_b] for u in common_users], dtype=float) # 每个用户减去自己的平均分,消除评分尺度差异 a_centered = scores_a - scores_a.mean() b_centered = scores_b - scores_b.mean() denom = np.sqrt(np.sum(a_centered ** 2) * np.sum(b_centered ** 2)) if denom == 0: return 0.0 return np.sum(a_centered * b_centered) / denom

这里有个参数细节很容易忽略:皮尔逊相关系数默认要求共同评分用户数不少于2,因为只有一个共同用户时算出来的相关系数永远是1或-1,没有统计意义。分母denom为0的情况也必须处理,否则会得到NaN,NaN一旦进入推荐列表排序,整个Top-N结果就崩了,这条我在踩坑章节还会展开。

3.3 生成Top-N推荐:召回策略与排序公式

相似度矩阵算完后,推荐生成阶段的目标是:给定用户U,找出他还没评过分的图书,按预测评分排序取前N本。ItemCF的预测评分公式是加权求和:用户U对图书B的预测分 = 所有用户评过分的图书A与B的相似度乘以用户U对A的评分,再除以相似度之和。但这里有个容易被忽略的工程细节——很多毕设直接对所有未评分图书全量预测,用户数多、书多的时候计算会很慢,所以要先做候选集召回。

候选集召回最简单的策略是:找出用户评分过的每本书的Top-K相似图书,合并去重后作为候选集。K一般取10到20,太大则计算量上升、推荐列表反而没那么精准,太小则覆盖不够。下面给出生成推荐的完整函数。

def recommend_for_user(user_id, inverted_table, similarity_matrix, top_n=10, k=15): """ ItemCF推荐主流程 similarity_matrix: dict, similarity_matrix[book_a][book_b] = 相似度 """ user_books = set(inverted_table.get(user_id, {}).keys()) if not user_books: return [] # 1. 召回候选集:每条已评分图书取TopK相似图书 candidates = defaultdict(float) for book in user_books: sim_list = similarity_matrix.get(book, {}) # 按相似度排序取前K,用sorted而不是全量遍历 top_k = sorted(sim_list.items(), key=lambda x: x[1], reverse=True)[:k] for similar_book, sim_score in top_k: if similar_book not in user_books: candidates[similar_book] += sim_score # 2. 对候选集做加权评分预测 predictions = {} for cand_book in candidates: total_sim = 0.0 weighted_score = 0.0 for rated_book in user_books: sim = similarity_matrix.get(cand_book, {}).get(rated_book, 0.0) if sim <= 0: continue weighted_score += sim * inverted_table[user_id][rated_book] total_sim += sim if total_sim > 0: predictions[cand_book] = weighted_score / total_sim # 3. 排序输出 ranked = sorted(predictions.items(), key=lambda x: x[1], reverse=True)[:top_n] return [book_id for book_id, score in ranked]

参数top_n控制最终推荐数量,k控制每本书的召回邻居数。需要留意的是,预测公式里sim <= 0的相似度直接跳过,这是有意的:负相关的图书不应该参与加权,否则会把预测分往下拉。另一个细节是候选集合并时直接累加sim_score,实际含义是“用户打过分的书与候选书的所有正向相似度之和”——这只是一个召回权重,真正排序用的是后面的加权平均分,两者不要混在一起。

3.4 用Flask把推荐结果暴露成接口

算法写完后必须封装成HTTP接口,Vue才能调用。常见做法是FastAPI或Flask二选一,毕设答辩场景我偏向Flask,因为代码直观、文档多、环境问题少。接口设计分成三类:获取推荐列表、提交评分、获取图书详情。下面给出最小可用的Flask应用骨架,只保留核心路由。

from flask import Flask, jsonify, request from flask_cors import CORS app = Flask(__name__) CORS(app) # 开发阶段直接允许跨域,后面会讲为什么不能永远开着 # 全局缓存相似度矩阵和倒排表,避免每次请求重新计算 SIMILARITY_CACHE = {} @app.route('/api/recommend/<int:user_id>', methods=['GET']) def recommend(user_id): top_n = request.args.get('top_n', default=10, type=int) books = recommend_for_user(user_id, INVERTED_TABLE, SIMILARITY_CACHE, top_n) # 这里需要根据book_id列表去MySQL查图书完整信息 return jsonify({'code': 0, 'data': books}) @app.route('/api/rate', methods=['POST']) def rate_book(): payload = request.get_json() user_id = payload['user_id'] book_id = payload['book_id'] score = payload['score'] # 写入评分表后,必须使缓存失效,否则新评分不生效 invalidate_cache() return jsonify({'code': 0, 'msg': '评分成功'}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)

这段骨架最需要注意的是“缓存失效”问题。很多人第一次做推荐系统,启动时加载一次相似度矩阵,之后用户评分了、新书入库了,推荐结果纹丝不动,就是因为缓存没刷新。常见做法是写操作后把SIMILARITY_CACHE清空,下次推荐请求到达时重建缓存。性能方面,毕设规模下重建缓存只要几秒钟,完全能接受,但要在答辩时能说清楚“我用的是定时或写后失效策略”,而不是被评委一问问住。

4. Vue前端落地:从图书列表到个性化推荐页的完整数据流

4.1 初始化Vue工程与路由配置

Vue侧的工程初始化建议直接用Vite,比Vue CLI更轻量,启动也快。创建完项目后第一件事是装路由和HTTP库:npm install vue-router@4 axios。如果你下载的源码包里已经带了package.json,直接npm install即可,版本冲突时优先看package.json里的锁文件版本,不要无脑升级。

路由设计上,推荐系统至少需要三个页面:首页图书广场、推荐页、图书详情页。路由配置用懒加载方式,避免首屏一次性加载全部组件,这在答辩演示时能明显感觉到切换流畅度差异。

// src/router/index.js import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', name: 'home', component: () => import('../views/Home.vue') }, { path: '/recommend', name: 'recommend', component: () => import('../views/Recommend.vue') }, { path: '/book/:id', name: 'book-detail', component: () => import('../views/BookDetail.vue') }, ] const router = createRouter({ // 用 hash 模式,部署到任意静态服务器都不用配 nginx 的回退 history: createWebHashHistory(), routes, }) export default router

hash模式是毕设部署最省心的选择。history模式虽然URL更漂亮,但刷新页面时如果后端没做路由回退配置,就会出现404,很多人的毕设演示视频里就在这一步翻车。hash模式丑一点,但稳定,推荐优先用它。

4.2 用Axios调用推荐接口并渲染图书卡片

Vue组件里调用后端接口的写法差异很大,有人把请求写在created里,有人用watch监听用户ID变化。我一般建议把请求逻辑封装到独立的api模块,而不是散落在每个组件里,这样后端接口路径调整时只改一个文件。

// src/api/recommend.js import axios from 'axios' import request from '../utils/request' // request 是封装好的 axios 实例,带 baseURL 和超时时间 export function fetchRecommendations(userId, topN = 10) { return request.get(`/recommend/${userId}`, { params: { top_n: topN }, }) } export function submitRating(userId, bookId, score) { return request.post('/rate', { user_id: userId, book_id: bookId, score: score, }) }

在Recommend.vue里拿到推荐书单后,渲染逻辑和首页图书列表几乎一样。区别在于推荐页要展示“推荐理由”,比如“因为你看过《三体》”,这个字段可以后端在返回时带上reason_books,前端在卡片下方用小字展示。这是很多现成源码没做的细节,但加上后整个页面看起来完成度立刻不一样,答辩时也更有说服力。

4.3 评分交互与推荐结果刷新:前后端联动的关键

用户提交评分后,前端做了两件事:更新UI(比如星星点亮)和刷新推荐列表。但刷新方式有讲究,不是简单调用一次推荐接口就完事。评分动作后协同过滤的相似度矩阵变了,如果后端没做缓存失效,刷新拿到的是旧结果,用户会直观感觉“我评分了推荐没变”,这是交互体验上的大忌。

推荐做法是:评分成功后,前端先乐观更新UI,再调用后端一个“刷新推荐”的专用接口,或者直接等后端缓存重建完成后重新请求推荐列表。为了不让用户等太久,前端可以加一个Loading状态,文案写“正在根据你的口味重新计算推荐”。这个细节很多人没做,数据流没闭环,答辩时被问到“你的推荐结果什么时候更新”容易卡壳。放个小节结束前,把路由、接口、数据流的串联逻辑再点透一遍,后面直接进踩坑章节。

5. 避坑指南:协同过滤图书推荐系统最常见的8个坑

5.1 冷启动:新用户和新书都推不出结果,列表一片空白

现象:刚注册的用户点进推荐页,接口返回空数组;新入库的图书永远没有曝光机会。原因:协同过滤本质是解剖学——你没有历史评分,系统无法判断跟谁相似;新书没有被评分过,任何相似度计算都算不出边。

解决思路分两条线。新用户场景,用一个“热门榜”兜底:按评分人数或平均分倒序推荐,等用户产生3到5条评分后再切换成协同过滤。新书场景则要给物品相似度计算加“最低共同评分人数”阈值和“新鲜度补偿”规则,比如入库时间在一周内的书,在热门推荐列表里加权展示。落地时,后端接口里加一个逻辑:如果用户评分记录少于阈值,返回热门图书而非协同过滤结果,前端感知不到差异,但推荐效果不会崩坏。

5.2 隐式反馈没被利用:只靠评分表,数据稀疏得不堪一击

很多人认为评分表就是协同过滤的全部输入,结果用户只有几百条评分,物品几千本,共现矩阵稀碎。原因:真实用户很少打分,但“点击图书、停留时间、收藏、加入书架”这些行为数据其实每时每刻都在产生。常见做法是把行为日志表里的点击和收藏折算成伪评分:点击记1分,收藏记3分,评分保留原始值。这个伪评分不直接写入评分表,而是在构造倒排表时动态融合,不污染原始数据。

这样处理后评分矩阵的稠密度通常能提升好几倍,推荐效果立竿见影。有一个参数值得调:伪评分的权重。收藏权重设为3相对安全,设太大会淹没真实评分,推荐结果偏向热门而非个性化,这就是过拟合行为偏好了。

5.3 相似度为NaN或全零:归一化和分母保护缺失

现象:相似度矩阵里大量NaN,排序时报错或推荐列表全乱。原因:皮尔逊相关系数分母为0;评分没有做均值中心化时,两本书的全部评分都是满分或者都是同一个值;denom == 0的处理分支被遗漏。解决:所有相似度计算函数都加分母保护,返回0.0而不是抛异常;输入评分先做中心化,不要依赖原始分;最后在构建相似度矩阵后跑一遍自检,统计NaN数量,不为0就直接报错,防止脏数据流到后端。

5.4 跨域请求失败:前端能打开页面但接口全部报错

现象:Vue页面起来了,F12看控制台全是CORS报错,或者请求发出去了但返回结果被浏览器拦截。原因:前端跑在5173端口,Flask跑在5000端口,不同源。解决:开发阶段在Flask里加flask-cors的CORS(app),允许所有来源;但是项目收尾时最好把允许来源限定为前端实际地址,而不是*,不然面试或答辩时被人问起安全策略会很尴尬。更稳的做法是部署时用Nginx做反向代理,把/api请求转发到后端端口,浏览器看来就是同源请求,彻底避开CORS。

5.5 数据库连接反复断开:长连接被MySQL踢掉

现象:系统跑一段时间后,推荐接口突然报MySQL server has gone away,重启后又正常。原因:SQLAlchemy或pymysql默认的连接池维持了长时间空闲连接,MySQL的wait_timeout把连接关了,连接池不知道继续复用。解决:SQLAlchemy引擎配置加pool_pre_ping=True和pool_recycle=3600,每秒在取连接前ping一下;或者每次请求都用短连接上下文管理,虽然慢一点但稳定。这个坑在毕业设计演示现场特别容易遇到,前面还好好的一进教室就挂,基本就是它。

5.6 Python与vue环境版本不匹配:装包装到怀疑人生

现象:照着README装依赖,npm install报错或者Python的scipy装不上。原因:Python版本过老,scipy/numpy的新轮子不支持;Node版本过高,Vue CLI或node-sass不兼容;换源问题拉取慢。解决:Python3.9或3.10比较稳,先装numpy再装scipy,避免一起装时的依赖冲突;Node用LTS版本,不要追latest。前端依赖装不上的时候,优先看package.json里锁的版本而不是全删重装;Python侧装包报错用pip install --only-binary :all:强制用预编译轮子。

5.7 ItemCF的K值选太大,推荐全是热门书

为了召回全面把k设到50,结果推荐的top-10全是豆瓣高分热门书,毫无个性。原因:K值过大让相似度很弱的边也参与加权,冷门但精准的书被淹没。解决:K值从10开始调,观察推荐结果里非热门书的占比;热门书占比太高就降K,冷门书太多导致预测分不稳定就升K。另外可以加一个“热门惩罚”:相似度乘以(1/log(1+item热门度)),这是比较通用的降热门权重做法。

5.8 演示视频与源码不一致:答辩被评委直接点穿

这个偏软性的坑但杀伤力极大。源码包里的数据库文档导出了,但实际工程里的ip和端口是写死的本机地址,评委换到学校电脑一跑就连不上。解决:所有数据库配置和API地址放.env文件,提交前用相对路径或环境变量替代硬编码。以及确保源码包里的database.sql能一键重建数据库,README里从零跑通的步骤不要省略哪怕一句“跳过这步就行”。

6. 进阶优化:离线评估与调参顺序,让推荐效果在答辩时拿得出手

协同过滤项目做得再多,如果只说得出“实现了推荐”,答辩分数会太平。值得花两三天时间做一套离线评估,用数据证明你的参数选择是有效的。最朴素的做法是把评分数据集按8:2拆成训练集和测试集——但在推荐系统里不能随机拆行,因为同一用户的评分不能同时出现在两边。常见做法是按用户拆:每个用户80%的评分进训练集,20%进测试集。

评估指标选精确率和召回率就够了,对毕设来说不用搞NDCG。构造测试方法也非常直观:对测试集中的每个用户,把他在测试集里的图书当作“真实喜欢”,跑一遍推荐流程取Top-10,命中了几本就记几,最后除以总推荐数就是精确率。

def evaluate_precision_recall(train_df, test_df, similarity_matrix, top_n=10, k=15): precision_list = [] recall_list = [] inverted_train = build_inverted_table(train_df) for user_id in test_df['user_id'].unique(): test_books = set(test_df[test_df['user_id'] == user_id]['book_id']) if len(test_books) == 0: continue rec_books = recommend_for_user(user_id, inverted_train, similarity_matrix, top_n, k) hit = len(set(rec_books) & test_books) precision_list.append(hit / top_n) recall_list.append(hit / len(test_books)) return np.mean(precision_list), np.mean(recall_list)

这个函数跑通后,你可以做一组很讨巧的对比实验:同一份数据下,K=10和K=30的精确率差异;原始评分和伪评分融合后的精确率差异;UserCF和ItemCF的差异。这三组数一放,答辩时“为什么这么调参”这个问题就轻松过关了。

我自己的习惯是把评估脚本单独放一个eval/目录,每次改相似度计算或K值后固定跑一遍,记录下来,而不是临时起意改个参数看看顺不顺眼。真正最值得养的工程习惯反而是“每次只改一个变量”,K值变了就只动K值,伪评分权重变了就只动权重;别一次改三个变量,不然根本分不清效果变化来自哪里。另外一个经验是把热门图书顶榜作为基线,评估结果必须比这个基线高才有实际意义,否则你做的只是花哨的热门榜而已。

调参顺序建议按这个来:先定相似度算法(皮尔逊还是余弦),再调K值,然后调伪评分权重,最后调Top-N数量。每步都记录一组评估指标,你会发现K=10时精确率最高但覆盖率偏低,K=30时结果更丰富但精确率下降,这是正常的,不存在完美参数,只有适合你这个数据集的参数。希望这本踩坑笔记能帮你少走几步弯路——毕竟我自己第一次做这个题目时,光那个NaN的坑就排查了一整天。

本文还有配套的精品资源,点击获取

返回列表