免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于Python的游戏推荐系统:协同过滤与Flask实战解析

基于Python的游戏推荐系统:协同过滤与Flask实战解析 每年这个时候总有一批做课程设计或毕业设计的同学来问我推荐系统能不能做游戏方向其实我一直觉得游戏推荐是个比电影、图书更“好讲”的场景因为游戏天然带类型、标签、平台、评分这些结构化属性做相似度计算时特征很好提取而且用户对游戏的评价行为也足够直观“喜欢/玩过/想玩”比“打了四颗星”更容易建模。我干脆把之前做过的一个“基于Python的热门游戏推荐系统”完整梳理出来从需求拆解、数据准备、算法实现、Web界面到本地部署全部过一遍。这份内容适合两类人一是准备做课程设计/毕设的同学想找一个代码结构清晰、能跑通、能讲清楚原理的项目参考二是刚开始学推荐系统的开发者想看看除了调包之外一套完整的召回—排序—展示链路到底怎么落地。整个项目体量不大但对Python基础、Pandas数据处理、协同过滤思想、Flask接口设计都是很好的综合训练。推荐系统这个方向最容易被误解的地方是很多人以为一定要有海量数据和分布式计算才能做。实际上教学级项目反而更适合用“小而完整”的方式呈现——离线准备一份游戏数据集用Item-based协同过滤计算相似度再用Flask包一层HTTP接口前端做几个简单的展示页面。这套路径麻雀虽小五脏俱全推荐系统该有的模块都能对应上数据层、特征层、算法层、应用层一层都没少。1. 拿到任务先别急着写代码需求拆解与整体方案选型真正动手之前我习惯先把问题拆清楚。很多同学一看到“推荐系统”四个字就想着上多复杂的模型其实完全没必要。你首先要回答四个问题系统给谁用推荐什么基于什么信号推荐在哪里展示这四个问题问完项目的开发量基本就能估算出来了。1.1 系统给谁用解决什么核心痛点这个系统的定位很明确给一个普通游戏爱好者提供一个Web页面他注册登录后可以浏览游戏列表给游戏打“喜欢”标记系统根据他的行为反馈给他推荐相似度高的热门游戏。也可以不做登录直接用游客模式点选几个喜欢的游戏让系统实时返回推荐结果。这样设计背后的逻辑是这既证明了推荐链路是通的又不用维护一套复杂的账号权限体系。毕设答辩时老师最关心的是你有没有把推荐算理论证清楚、把流程跑通而不是你用户系统做得有多花哨。1.2 核心模块可以拆成哪几层我最终把项目拆成了四层这个分层思路也可以直接写进论文的设计章节里层次职责对应技术关键产出数据层获取和清洗游戏数据Python爬虫/Pandasgames.csv、ratings.csv算法层计算游戏相似度并生成推荐列表Item-based协同过滤、余弦相似度相似度矩阵、推荐函数应用层对外提供HTTP接口Flask/api/recommend 等接口展示层用户交互页面HTML/CSS/JS Jinja2游戏列表页、推荐结果页千万别把四层代码全塞进一个app.py后面我会专门讲目录划分。分层最大的好处是答辩的时候老师问“推荐算法在哪儿”你直接指到recommender.py问“接口在哪儿”指到app.py代码工整本身就是加分项。1.3 选型理由为什么是Python、Flask、Pandas、SQLite技术栈我选得比较保守理由很实际Python是算法原型验证效率最高的语言Pandas处理表格数据几乎是零门槛Flask则是Python后端框架里最容易讲清楚HTTP请求流转的。数据库没有用MySQL而是直接用了SQLite因为整个项目数据量都在万级以内SQLite单文件部署、免安装的特性对课程设计和本机演示太友好了给老师演示的时候不用现场配数据库账号。关于算法层我也做过对比User-based CF和理解成本更低不恰恰相反。游戏场景里用户数量通常大于物品数量而且新用户往往没有多少行为记录User-based CF算起来慢、覆盖还差。Item-based CF则可以先离线把游戏间的相似度矩阵算好服务启动后直接查表响应速度快可解释性也强——“因为你喜欢《黑神话悟空》所以推荐你《战神》”比“和你有相似爱好的玩家也喜欢”要直观得多。所以这个项目最终选了Item-based CF作为主算法。2. 游戏数据从哪来构建干净可用的数据集没有数据推荐系统就是空中楼阁。我在这个项目里不推荐大家用过于复杂的方式取数也不建议把人家的接口爬到被封。当时的做法是两步走先用公开的Steam游戏排行榜页面或商店热门数据再把字段清洗成结构化表格。如果你所在网络环境访问不了原站也可以直接用Kaggle上现成的游戏数据集。2.1 从公开页面抓取游戏基本信息数据采集我用的Python第三方库是requestsBeautifulSoup。采集的字段控制在最核心的几个游戏名称、所属类型标签、好评率、价格、游戏简介。这里需要注意一点不要贪多不要试图抓用户评论正文评论正文清洗成本极高而且教学级推荐引擎根本用不上NLP。爬虫代码的大体逻辑如下import requests from bs4 import BeautifulSoup import pandas as pd def fetch_hot_games(page_limit5): games [] for page in range(1, page_limit 1): url fhttps://example-store.com/hot/{page} resp requests.get(url, headers{User-Agent: Mozilla/5.0}) soup BeautifulSoup(resp.text, html.parser) for card in soup.select(.game-card): games.append({ game_id: card.get(data-id), name: card.select_one(.game-name).text.strip(), genres: card.select_one(.game-tags).text.strip(), rating: float(card.select_one(.rating).text), price: card.select_one(.price).text.strip(), desc: card.select_one(.desc).text.strip()[:200], }) return pd.DataFrame(games)示例代码里直接用了一个占位域名实际使用时换成你能访问的页面结构就行。爬虫跑完之后把结果存成data/raw_games.csv这一步就完成了。2.2 数据清洗的两个关键细节游戏类文本数据最麻烦的是类型标签。有些游戏标签是英文逗号分隔有些是中文顿号还有的是斜杠分隔。我在清洗时做了统一处理把分隔符全部转成英文逗号然后再去空格。第二个麻烦是价格字段爬下来往往带着“¥”或者“Free”必须转成统一格式。df[price] ( df[price] .str.replace(¥, , regexFalse) .str.replace(Free, 0, caseFalse) .astype(float) ) df[genres] ( df[genres] .str.replace(、, ,, regexFalse) .str.replace(/, ,, regexFalse) .str.replace(;, ,, regexFalse) .str.strip() )清洗完的游戏字段大概长这样Action, Adventure, RPG这种逗号分隔的形式这一格式化能让后面做TF-IDF标签向量方便不少。2.3 造一份可解释的评分数据真实用户-游戏评分数据在这个小项目里很难拿到。我用的折中方案是找几位同学每人标记了10~30个自己玩过的游戏开心程度打1~5分再把“标记了相似度高的游戏却没标记过的”当作隐式反馈补进去。如果你的场景连人工标记都凑不齐就自己生成一个演示数据集——保证每个用户标记的游戏数量大致在10~30个之间不要全部集中在同一类型里否则后面推荐效果很假。推荐系统的质量上限说到底取决于数据质量。有一个被我反复提到的经验宁可要50条真实标注也不要5000条机器乱生成的数据。数据没有区分度相似度矩阵算出来全是一样的数那推荐结果就完全没有说服力。3. 推荐引擎实现相似度计算和协同过滤的核心代码讲解现在到了整个项目技术含量最高的部分。很多教程一讲协同过滤就是surprise库一行搞定这不利于理解原理。所以我的代码保留了手动实现的过程里面只用了NumPy和Pandas没有调现成的推荐算法库。3.1 从“用户-游戏评分矩阵”说起协同过滤的第一步永远是构建矩阵。行是用户ID列是游戏ID交叉点是评分。但在游戏推荐场景里用户很少给游戏打分更多是“标记喜欢”这种隐式反馈。我的做法是把题目转化一下喜欢显式打分5分没标记0不喜欢可以忽略然后在相似度计算公式里只取有评分的项来算。如果你手里确实有1~5分的显式得分就把评分矩阵填成本来值。Pandas构建矩阵的代码长这样import pandas as pd import numpy as np # ratings.csv 字段user_id, game_id, rating ratings pd.read_csv(data/ratings.csv) matrix ratings.pivot_table( indexuser_id, columnsgame_id, valuesrating, fill_value0 ) print(f用户数: {matrix.shape[0]}, 游戏数: {matrix.shape[1]})这里要理解一个重要的点矩阵的空白处不能直接填0再整体算余弦相似度。因为0在这里不是“打了0分”而是“没有打分”。尤其对于热门游戏大众没打过分实在太正常了。直接把0灌进去会让相似度计算出现严重偏差。所以在实现Item-based CF时我选择了“只对共同评分的游戏做均值中心化”的优化方法。3.2 余弦相似度的两种处理方式余弦相似度公式本身不复杂similarity(A, B) (A向量点乘B向量) / (|A向量| * |B向量|)在这个场景里A和B是两个游戏在用户向量空间中的表现。如果A游戏只有3个用户评分B游戏只有2个用户评分这两个向量大多数维度都是0直接套公式会被“无量纲的零”稀释。这个问题专业上叫“数据稀疏性”。我的处理办法是向量只保留有评分的用户维度。也就是说如果用户集合是{u1,u2,u3,u4,u5,u6}其中A游戏被u1、u3、u6评分那么给A建向量时就取[u1, u3, u6]对应的评分B游戏同理最后只在交集里计算。代码如下from sklearn.metrics.pairwise import cosine_similarity # 转置变成 games × users 的评分矩阵 game_matrix matrix.T # 相似度计算 game_sim cosine_similarity(game_matrix.values) game_sim_df pd.DataFrame( game_sim, indexgame_matrix.index, columnsgame_matrix.index )如果用sklearn里的函数它会自动处理零值的情况。不过我还是建议初学者自己写一遍余弦相似度这样才能真正理解为什么两个受欢迎的游戏可能因为用户群体重叠不高反而相似度低。3.3 Item-based CF的完整推荐函数推荐一个游戏的Top N相似游戏其实就是一个“查表”操作def recommend_by_game(game_id, sim_matrix, games_df, top_n10): if game_id not in sim_matrix.columns: return pd.DataFrame() scores sim_matrix[game_id].drop(indexgame_id).sort_values(ascendingFalse) top_ids scores.head(top_n).index return games_df[games_df[game_id].isin(top_ids)]但系统不能只做“以游戏找游戏”因为它还有个冷启动的问题如果用户什么游戏都没标记过后端该返回什么这里我加了一个策略分支如果用户有历史喜欢就按他喜欢的每个游戏的相似度加权求和取加权和最高的Top N。如果用户没有任何标记直接返回平台热门榜的前N个游戏这叫“热门兜底”。加权推荐的核心代码我贴出来这属于“答辩时被问到能讲很久”的部分def recommend_for_user(user_id, behavior_df, sim_matrix, games_df, top_n10): liked_games behavior_df[ behavior_df[user_id] user_id ][game_id].tolist() if not liked_games: return games_df.sort_values(hot_score, ascendingFalse).head(top_n) # 累积相似度得分 score_series pd.Series(dtypefloat) for gid in liked_games: if gid not in sim_matrix.columns: continue sim_scores sim_matrix[gid].drop(indexgid) score_series score_series.add( sim_scores * (1.0 / len(liked_games)), fill_value0 ) # 去掉已经玩过的避免反复推荐 score_series score_series.drop(indexliked_games, errorsignore) top_ids score_series.sort_values(ascendingFalse).head(top_n).index return games_df[games_df[game_id].isin(top_ids)]用“平均加权”而不是“直接相加”是为了防止某个用户点了一堆游戏后累计分数过于集中在某个高频相似度的游戏上。这行注释我在代码里专门写了答辩或讲代码的时候能直接点出设计意图。3.4 用TF-IDF做基于内容的辅助信号只靠协同过滤有个问题某个游戏没人标记过典型的长尾冷启动它永远不会被推荐出来。所以我还加了基于内容(Content-based)的辅助思路——把游戏类型标签、简介文本拼接起来放进TF-IDF向量然后计算文本相似度。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel games_df[text_feature] ( games_df[genres] games_df[desc].fillna() ) tfidf TfidfVectorizer(stop_wordsenglish, max_features5000) tfidf_matrix tfidf.fit_transform(games_df[text_feature]) content_sim linear_kernel(tfidf_matrix, tfidf_matrix)这套混合策略很经典协同过滤负责“人相近”内容过滤负责“物相近”。实际返回时权重可以设成7:3主流教学项目里已经算考虑周到了。4. Flask接口层和前端页面怎么把算法结果送到用户眼前算法层写完下面要做的就是把计算结果暴露成可以被浏览器调用的HTTP接口。这里我用Flask搭了一个非常标准的Web服务。4.1 数据库表结构和初始化数据库我用SQLite建表语句放在schema.sql里CREATE TABLE games ( game_id TEXT PRIMARY KEY, name TEXT NOT NULL, genres TEXT, rating REAL, price REAL, desc TEXT, hot_score REAL DEFAULT 0 ); CREATE TABLE users ( user_id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE user_likes ( user_id TEXT NOT NULL, game_id TEXT NOT NULL, liked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, game_id) );Flask启动时执行初始化import sqlite3 def init_db(): conn sqlite3.connect(game_reco.db) with open(schema.sql, r, encodingutf-8) as f: conn.executescript(f.read()) games_df.to_sql(games, conn, if_existsreplace, indexFalse) conn.commit() conn.close()to_sql把Pandas DataFrame直接写进SQLite开发效率极高。如果线上生产环境再用迁移工具也不迟课程设计阶段完全够用。4.2 推荐接口输入用户ID输出JSON后端接口设计成/api/user/user_id/recommend返回JSON数组。前端拿到数据后渲染卡片。接口内部要干的事情就是读用户喜欢记录、调用推荐函数、把结果转换成列表。from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/user/user_id/recommend, methods[GET]) def api_recommend(user_id): top_n int(request.args.get(top_n, 10)) behavior_df load_user_likes(user_id) # 从user_likes表读取 result_df recommend_for_user( user_id, behavior_df, sim_matrix, games_df, top_ntop_n ) return jsonify(result_df.to_dict(orientrecords))这里有个细节要提醒一下如果推荐结果是从games_df里用isin筛选出来的顺序会被打乱。必须显式按照推荐分数重新排序否则前端拿到的第一条并不是推荐分最高的那个。这个坑不显眼但初学很容易踩到表现为“推荐的游戏好像都对但顺序明显不对”。# 正确做法先构造索引顺序再重新索引 ranked result_df.set_index(game_id).loc[top_ids].reset_index()4.3 前端怎么做才最简单又能看我的前端没有用前后端分离那套复杂的工程框架直接用了Jinja2模板 少量原生JavaScript。页面分两个index.html做游戏浏览用户点击“喜欢”按钮把当前game_id存到后端的user_likes表recommend.html展示推荐结果。核心交互代码如下async function likeGame(gameId) { const userId demo_user_ localStorage.getItem(uid) || u_001; const resp await fetch(/api/like, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ user_id: userId, game_id: gameId }) }); if (resp.ok) { updateButtonStatus(gameId); } } async function loadRecommendations() { const userId localStorage.getItem(uid) || u_001; const resp await fetch(/api/user/${userId}/recommend?top_n12); const games await resp.json(); renderGameCards(games, #rec-container); }页面不用追求炫酷能把结果清晰展示出来即可。我建议给每个游戏卡片加上“推荐理由”的伪解释字段——基于内容相似度的一些标签重叠信息这种解释性展示在答辩时非常得人心因为老师看到后会认为你不只是调了个包而是理解了“可解释性”这个推荐系统的关键诉求。5. 本地部署完整流程从源码到浏览器能访问一个推荐系统写得再漂亮没法在你电脑上跑起来也是白搭。这个项目的部署没有用Docker因为课程设计场景下要求评委老师去装Docker容器有点没必要直接在本地起一个Python虚拟环境就行。5.1 环境准备和依赖安装首先确保你已经安装了Python 3.8及以上版本。然后在项目根目录执行cd game-recommendation-system python3 -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txtrequirements.txt的内容我列一下全是常用库flask2.3.3 pandas2.0.3 numpy1.24.3 requests2.31.0 beautifulsoup44.12.2 scikit-learn1.3.0注意版本号我锁定过主要是为了避免新版sklearn接口变动导致代码报错。部署阶段最不划算的时间浪费就是“代码没问题但版本不兼容”。5.2 数据初始化和启动服务安装完依赖后先跑数据预处理脚本python scripts/preprocess.py python scripts/build_similarity.py两个脚本会依次产出data/games_clean.csv、data/user_likes.csv并最终生成一个data/sim_matrix.pkl的相似度矩阵缓存文件。把相似度矩阵预先缓存能省去每次重启服务都要重新算一遍的时间游戏数量在5000款左右时这个优化效果很明显。最后启动python app.py看到如下日志就说明服务启动成功了* Running on http://127.0.0.1:5000浏览器访问http://127.0.0.1:5000输入演示用户ID点击几个喜欢的游戏推荐列表就能正常返回来。5.3 部署过程中最容易翻车的几个点我整理了实际跑这个项目时最容易出的问题都是真实踩过的坑现象原因解决办法中文乱码控制台或CSV编码不支持UTF-8pd.read_csv(..., encodingutf-8-sig)或打开CSV时指定编码pip安装scikit-learn失败Python版本太老换Python 3.10以上别在系统自带旧版本里边挣扎服务启动后端口被占用本机5000端口已被其他程序占用app.run(port5001)换端口打开页面非常慢每次请求都重新算相似度确认已缓存sim_matrix.pkl不要每次现算浏览器显示Internal Server ErrorFlask debug没开先设app.run(debugTrue)看错误栈定位后再关还有一种极小概率出现的情况在Windows控制台里用python app.py启动浏览器访问正常但终端里看不到任何日志输出且CtrlC没法停。这多半是IDE shell缓冲问题换个PowerShell或直接在IDE里点运行按钮就能解决。6. 源码结构建议与下一步扩展思路最后讲一下源码组织。项目到了中后期最怕的就是所有代码堆在几个文件里。合理的目录划分既是给自己减负也是在答辩时为“软件工程素养”加分。6.1 一个清晰的项目目录长什么样我建议按这样组织game-reco-system/ ├── app.py # Flask 应用入口与路由 ├── schema.sql # 建表语句 ├── requirements.txt ├── README.md # 快速部署说明 ├── recommender/ │ ├── __init__.py │ ├── data_loader.py # 数据加载和缓存 │ ├── similarity.py # 相似度矩阵构建 │ ├── collaborative.py # 协同过滤推荐逻辑 │ └── content_based.py # 基于内容的推荐逻辑 ├── data/ │ ├── games_clean.csv │ ├── user_likes.csv │ └── sim_matrix.pkl ├── scripts/ │ ├── preprocess.py │ ├── build_similarity.py │ └── fetch_games.py ├── templates/ │ ├── index.html │ └── recommend.html └── static/ ├── css/style.css └── js/main.js这种组织方式把算法实现、数据脚本、Web层完全分开。多个开发成员协作时不容易冲突改推荐算法不影响接口层改前端页面也不影响算法层。6.2 如果想做毕设创新点从这五个方向切入如果只是交作业上面这套已经够用了。但如果你想做能拿优秀论文的版本下面几个方向都是低成本高收益的扩展点第一在推荐结果里加入时间衰减因子。用指数衰减函数给历史喜欢行为加权越近的喜欢权重越高能有效解决用户兴趣漂移问题。第二引入图结构把用户和游戏当节点构造二部图用PersonalRank或Node2Vec这种图算法会比你写一万字业务背景更有说服力。第三做在线学习当前推荐给你并点击的游戏不一定是喜欢的可以通过是否跳转到详情页来构造隐式反馈。更复杂的强化学习不做没有数据。第四加个排行榜功能把热门游戏的热度值用收藏数、近期点击数和好评率综合计算。这一项能让系统看起来更完整。第五写测试——我是认真的给推荐函数写几个pytest单测很容易在老师问“你验证过你的结果吗”时拿出真凭实据。6.3 关于代码讲解和论文写作的一些建议我见过不少同学代码写得不错但答辩时只会复述代码讲不出设计过程。给大家一个相对可靠的讲述逻辑先用一句话回答“做了什么”再说“为什么这样做”然后说“过程中遇到什么坑”最后说“如果数据量扩大十倍会怎么优化”。这个逻辑能覆盖大部分提问。论文里建议搭配两张图一张系统架构图一张推荐流程图。通常论文里的架构图画的是分层结构加上数据库表设计流程图是从用户点“喜欢”开始经过相似度矩阵、最终返回推荐结果的数据流向。还有一个小细节所有核心函数的注释请用中文写清楚“输入是什么、输出是什么、为什么用这种策略”。老师翻阅源码时第一眼看的往往不是代码逻辑而是函数的注释面貌。注释写得清晰印象分立刻上来了。这个项目做下来我最大的体会有两点一是推荐系统的核心不在模型多复杂而在于你对数据信号的理解是否到位二是做课程设计或毕设完整性和可解释性永远比算法炫技重要。能用一个朴素的Item-based CF把链路跑通、讲明白本身就比拿个黑盒模型套上去强得多。如果你也在写类似的项目按照上面的顺序把数据层、推荐引擎、Web接口、部署脚本一步步搭起来整个过程大概需要一周左右的课余时间。代码少说也有千行量级但真正核心的算法其实不到200行剩下的都是让系统完整可用的“脚手架”不要被吓到。
返回列表