拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python与混合推荐算法的服饰推荐系统设计与实现

基于Python与混合推荐算法的服饰推荐系统设计与实现

简介:这是一套基于Python实现的服饰推荐系统完整项目,面向毕业设计、课程设计与实际项目开发场景,适合计算机相关专业学生与初中级开发者参考。项目采用前后端分离结构,前端应用、服务端、脚本与图像数据集分目录组织,附带项目文档,覆盖数据采集、预处理、推荐逻辑与界面展示的完整链路。压缩包共2000个文件,以1863张JPG图像样本为主,辅以Python源码、Vue与JavaScript前端代码、XML/CSV/JSON配置数据,以及BSON商品和搭配数据,整体约223.8MB,结构清晰便于按模块学习和二次开发。源码已经严格测试,并配有说明文档,可放心在此基础上延展使用。目前已有79人学习下载,对需要完整项目参考、希望深入理解服饰推荐系统实现细节的读者具有较高参考价值。

1. 服饰推荐系统的实际定位:一个能直接拿去交差的完整工程

做毕业设计或者课程设计的人最怕的不是题目难,而是题目太泛。服饰推荐系统这个题目属于典型的「看着不新,做起来事多」——它既要跑通数据、算法、接口,又要有一整套能验收的文档。这套基于 Python 实现的服饰推荐系统,把用户画像、服饰属性标签、协同过滤和内容推荐都串在了一起,网页端能注册登录、选择风格偏好、看推荐结果,后台有 Flask 接口支撑,数据集和 SQL 脚本也都配好。它的价值不在于某个算法有多前沿,而在于你拿到手之后,改一改就能变成自己的毕设或者课设作品。适合谁?正在选题的本科生、需要交课程设计的大三学生,以及接了私活想快速交付的人。这篇文章会把整个工程的运行流程、关键代码、文档构成和最容易翻车的地方全部拆开讲。

2. 数据与算法选型:为什么这个系统用混合推荐而不是单一模型

2.1 数据字段与用户交互表的搭建

打开源码包之后,第一件事是看dataset目录下的数据文件。这个系统用的不是公开的 MovieLens 那种电影评分数据,而是自己构建的服饰数据。我拆过很多这样的项目,服饰数据一般分两张核心表:一张存服饰本身的属性,一张存用户与服饰的交互记录。

服饰表里常见的字段有clothing_id、category(上衣/裤子/裙子/外套)、color(颜色标签)、style(休闲/通勤/运动/甜美)、season(春夏秋冬)、material(棉/麻/化纤)、image_url。用户交互表则记录user_id、clothing_id、rating(1到5分)和timestamp。

CREATE TABLE clothing ( clothing_id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, color TEXT NOT NULL, style TEXT NOT NULL, season TEXT NOT NULL, material TEXT, image_url TEXT ); CREATE TABLE user_clothing_rating ( user_id INTEGER NOT NULL, clothing_id INTEGER NOT NULL, rating INTEGER CHECK (rating BETWEEN 1 AND 5), timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, clothing_id) );

这张user_clothing_rating表是整个推荐系统的核心。协同过滤算法全靠它来计算用户之间的相似度,或者物品之间的相似度。为什么要单独建一张交互表而不是把评分字段塞进用户表?因为一个用户会对多件衣服评分,如果放在用户表里,每加一条评分都要改用户表的记录,后续查询和计算都变得极其低效。关系表的设计能让我们直接用 SQL 做聚合统计,比如计算每个用户的平均评分、活跃度,这些特征后面都要用到。

2.2 协同过滤与内容推荐的取舍

服饰推荐这个场景,用单纯的基于用户的协同过滤会有一个很明显的毛病:衣服不像电影,用户不太可能给大量衣服打分,大部分人只浏览、收藏、加购物车,评分数据通常非常稀疏。所以这个系统里没有完全依赖协同过滤,而是做了「基于标签内容推荐 + 协同过滤」的混合方案。

基于内容推荐的逻辑很好理解——把用户点过、收藏过的服饰的属性标签提取出来,统计用户对颜色、风格、类别的偏好,然后按照这个偏好去匹配服饰表里没有看过的新衣服。这个方案在冷启动阶段特别重要,因为新用户没有任何评分记录时,协同过滤算不出任何相似用户。

def compute_user_preference(user_id, ratings_df, clothing_df): user_ratings = ratings_df[ratings_df['user_id'] == user_id] if user_ratings.empty: return None merged = user_ratings.merge(clothing_df, on='clothing_id') preference = {} for attr in ['style', 'color', 'season']: preference[attr] = merged.groupby(attr)['rating'].mean().to_dict() return preference

这段代码的意思是:把你评分过的每一件衣服的属性取出来,按属性分组求平均分。比如你在休闲风格的服装上平均给了 4.8 分,在运动风格上只给了 2.5 分,那系统就知道你偏爱休闲风格。groupby之后返回的是一个字典,键是具体属性值,值是该属性下所有已评分衣服的平均分。这个偏好字典会作为后续内容推荐的特征输入。

2.3 混合推荐的整体流程

整个系统的推荐流程可以分成三步。第一步,从数据库里读数据,然后构建用户偏好画像和协同过滤的相似度矩阵;第二步,分别用内容推荐和协同过滤各自产生一份候选集,这里注意各自都要做去重,并且去掉用户已经评分过的衣服;第三步,按照一定权重把两份结果合并,权重可以手动配,默认是内容推荐 0.6、协同过滤 0.4。

def hybrid_recommend(user_id, top_n=10, content_weight=0.6, cf_weight=0.4): content_recs = content_based_recommend(user_id, top_n=top_n * 2) cf_recs = collaborative_filter_recommend(user_id, top_n=top_n * 2) score_dict = {} for item, score in content_recs.items(): score_dict[item] = score_dict.get(item, 0) + content_weight * score for item, score in cf_recs.items(): score_dict[item] = score_dict.get(item, 0) + cf_weight * score sorted_items = sorted(score_dict.items(), key=lambda x: x[1], reverse=True) return [item for item, score in sorted_items[:top_n]]

这里有个细节值得注意,两份候选列表都取了top_n * 2,然后再合并排序取前top_n。原因是单模型给出的前 10 个结果里可能有不少是重复的或者用户已经看过的,如果不扩大候选范围,混合之后的结果会明显变少。我个人调试的时候更倾向于内容权重调高一点,因为服饰类的属性标签相对明确,用户对风格的偏好稳定性高于对某个具体用户的相似性。

3. 把系统跑起来:环境配置与核心代码复现

3.1 环境依赖与项目目录结构

拿到源码包后建议先用 Python 3.8 到 3.10 之间的版本,太新的版本偶尔会出现一些第三方库还没适配的情况。项目根目录下的requirements.txt是这个系统能跑起来的关键,里面主要有 Flask、pandas、numpy、scikit-learn 这几样。不要自己去 pip 乱装最新版,严格按照这个文件装。

pip install virtualenv virtualenv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -r requirements.txt

用虚拟环境是必须养成的习惯,因为 Flask 和 scikit-learn 的版本之间偶尔会有依赖冲突,不用虚拟环境的话,你机器上原有的项目可能会被连带搞坏。装完依赖后,项目根目录下应该能看到app.py、recommend/、models/、data/、static/、templates/这几个关键目录。recommend/里放的是推荐算法的实现,models/里是数据库模型定义,templates/里是网页模板。

3.2 协同过滤相似度计算的复现

系统里的协同过滤部分用的是基于物品的相似度计算,也就是 ItemCF。ItemCF 的核心思想是:如果两件衣服经常被同一批用户购买或评分,那这两件衣服就是相似的,用户喜欢其中一件,就大概率喜欢另一件。相比基于用户的 UserCF,ItemCF 在电商场景的推荐里更稳定,因为物品的相似关系相对固定,不需要频繁重新计算。

from sklearn.metrics.pairwise import cosine_similarity def build_item_similarity(ratings_df): user_item_matrix = ratings_df.pivot_table( index='user_id', columns='clothing_id', values='rating', fill_value=0 ) item_similarity = cosine_similarity(user_item_matrix.T) item_ids = list(user_item_matrix.columns) sim_df = pd.DataFrame(item_similarity, index=item_ids, columns=item_ids) return sim_df

pivot_table的作用是把长表转成宽表,每一行是一个用户,每一列是一件衣服,值是对应的评分。转置之后求余弦相似度,得到的矩阵里第i行第j列的数字就是衣服i和衣服j的相似度,范围在 0 到 1 之间。1 意味着两件衣服被完全相同的用户以相同的评分对待,0 意味着完全没有交集。这个矩阵在数据量大起来之后会非常耗内存,所以系统里默认只对评分次数超过阈值的衣服做计算,这个阈值放在配置文件的MIN_RATING_COUNT参数里。

3.3 基于内容的标签匹配代码

内容推荐部分,系统用了一个很直接的做法:把用户偏好和每件衣服的属性做加权匹配。每匹配上一个属性就累加一次权重分,最后按总分排序。

def content_based_recommend(user_id, top_n=10): preference = compute_user_preference(user_id, ratings_df, clothing_df) if preference is None: return get_popular_clothing(top_n) scores = {} unseen = clothing_df[~clothing_df['clothing_id'].isin( ratings_df[ratings_df['user_id'] == user_id]['clothing_id'] )] for _, item in unseen.iterrows(): score = 0.0 for attr in ['style', 'color', 'season']: attr_score = preference[attr].get(item[attr], 0) score += attr_score scores[item['clothing_id']] = score return dict(sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n])

第 3 行有个很关键的兜底逻辑:如果compute_user_preference返回None,说明这个用户一条评分都没有,系统会直接返回热门衣服的列表,而不是返回空结果。unseen变量筛选出了用户没看过的衣服,这一步不能省,否则推荐列表里会出现用户已经买过的衣服,体验很糟糕。属性匹配的分数直接累加,好处是代码简单、可解释性强——你可以跟用户说「因为您偏好休闲风格、喜欢深色系、主要在秋天穿,所以推荐了这些」。

3.4 API 接口与前端页面的联调

后端服务用 Flask 起了两个核心接口,一个是注册登录,另一个是获取推荐结果。app.py里最核心的接口定义大概长这样:

@app.route('/api/recommend', methods=['GET']) def recommend_api(): user_id = request.args.get('user_id', type=int) if not user_id: return jsonify({'error': 'user_id is required'}), 400 rec_list = hybrid_recommend(user_id, top_n=10) rec_clothing = [ { 'clothing_id': i, 'name': clothing_map[i]['name'], 'image_url': clothing_map[i]['image_url'], 'reason': generate_reason(i, user_id) } for i in rec_list ] return jsonify({'code': 0, 'data': rec_clothing})

generate_reason函数是这套系统在毕设答辩时的一个亮点,它会把推荐命中的属性标签拼成一句话,比如「因为您在休闲风格上评分较高,为您推荐这件休闲衬衫」。前端拿到这个字段后直接渲染在卡片下方,用户就知道为什么推荐这件衣服,而不是看到一个黑匣子一样的推荐列表。这段接口代码里我比较在意的是参数校验,user_id如果不是合法的整数,接口直接返回 400 而不是抛异常,这一点在答辩演示时能省掉不少尴尬。

4. 项目文档的构成:从需求文档到答辩 PPT 的完整链路

4.1 源码包里的文档清单

很多人下载资源后只盯着代码跑,其实这个包里最值钱的是文档。毕设和课设的评分标准里,文档通常占 30% 到 40% 的比例。这套资源里包含了五类文档:需求分析说明书、数据库设计文档、系统详细设计文档、测试报告、答辩 PPT 模板。每一份文档都是按本科毕设的格式要求写的,章节编号、图目录、表目录都是齐全的。

文档名称核心内容对应开发阶段
需求分析说明书用户角色、用例图、非功能需求前期调研
数据库设计文档ER 图、表结构、字段说明设计阶段
系统详细设计文档架构图、算法流程、接口定义设计阶段
测试报告测试用例、缺陷记录、结论测试阶段
答辩 PPT 模板项目背景、技术架构、演示效果答辩准备

我见过太多人在答辩前一周才开始写文档,最后交上去的东西一看就是凑数的。到手这份文档之后,建议你第一时间打开需求分析说明书,把项目背景那一段用自己的话改写一遍,因为答辩老师很可能问的就是「这个系统要解决什么问题」,你如果答得和文档里一字不差,老师马上就知道你没有参与真实项目。

4.2 数据库设计与 ER 关系梳理

数据库设计文档里给了一个完整的 ER 图,核心实体有四个:用户、服饰、服饰标签、评分记录。你和服饰之间是多对多的关系,评分记录是它们的联系表。这套设计在很多电商推荐系统里都能复用,换一个领域就能变成图书推荐、美食推荐。

CREATE TABLE user ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, preferred_style TEXT, preferred_color TEXT, register_time DATETIME );

运维上需要注意的一点是,preferred_style和preferred_color这两个字段在用户注册时会要求选择,但这只是最粗粒度的偏好,真正的偏好还是从行为数据里算出来的。这意味着系统里存在两套偏好:一套是用户主动声明的,另一套是从数据中推断的。推荐算法的优先级上,主动声明的偏好权重要高于推断偏好,这个规则在详细设计文档里有单独一节说明,答辩时如果被问到,你可以直接引用这个设计决策。

4.3 测试用例与验收标准

测试报告里的内容不是那种「点开页面—没报错—通过」的水货,而是把每个模块的接口都配了输入、输出、预期结果。比如推荐接口的测试用例可以复用下面的 JSON 格式:

{ "用例编号": "TC-REC-001", "接口": "/api/recommend", "前置条件": "用户 1001 已有至少 5 条评分记录", "输入": {"user_id": 1001}, "预期输出": "返回 10 条服饰记录,每条包含 clothing_id、name、reason", "实际结果": "返回 10 条,reason 字段无空值" }

写测试用例的时候有个潜在的坑:很多人把「接口不报错」当成「测试通过」,实际上要验证推荐列表里不能包含用户已经评分过的衣服,这个用一条 SQL 就能查出来,但大多数学生写的测试报告里根本没有这一项。你可以往测试报告里补一条「推荐结果中不包含已交互服饰」的用例,这一条就能让答辩老师觉得你真的在系统层面思考过问题。

5. 避坑指南:服饰推荐系统开发中的典型问题排查

5.1 冷启动:新用户没有任何评分,推荐列表为空

现象:注册一个新账号登录系统,点击获取推荐,前端页面一片空白,接口返回data为空列表。

原因:hybrid_recommend函数里先调content_based_recommend,这个函数在用户没有评分时返回None,虽然写了兜底逻辑,但兜底逻辑只出现在内容推荐内部,如果协同过滤部分也直接按矩阵计算,就会出现空结果。

解决:把兜底逻辑从函数内部上提到混合推荐的入口。在hybrid_recommend的开头判断用户评分数量,如果为 0,直接返回热门服饰列表,不再进入任何推荐算法分支。同时在前端做一个判断,如果data为空,展示「先去逛逛并收藏喜欢的服饰吧」的提示页,不要展示空白的推荐区域。

5.2 中文路径和编码导致的数据库读取问题

现象:在 Windows 上运行init_db.py初始化数据库时,报UnicodeDecodeError,SQL 脚本里中文注释乱码,服饰数据插进去之后显示成乱码。

原因:Windows 系统默认编码不是 UTF-8,Python 打开 SQL 和 CSV 文件时如果没指定编码,就会用系统默认编码去读,中文数据直接坏掉。

解决:在所有打开文件的代码里显式指定编码,不要依赖默认值。写 SQL 脚本时保存为 UTF-8 格式,并在 Python 读取时加一条encoding='utf-8'参数。如果你用的是 PyCharm,需要在 File Encoding 里把项目全局编码设为 UTF-8,这个不设置会有很多隐藏的诡异问题。

5.3 数据稀疏导致的相似度矩阵全为零

现象:协同过滤推荐出来的是随机衣服,每次刷新结果都不一样,而且推荐的衣服和用户历史偏好明显不符。

原因:服饰数据的评分数量太少,比如总共只有几十个用户、几百条评分记录,很多衣服只被一个人评过分,计算余弦相似度时矩阵里大量是零向量,相似度全部为零或极小,排序时相当于在随机排。

解决:给 ItemCF 加一个「共现次数」的门槛,也就是两件衣服至少要被同一个用户同时评分过 N 次以上,才把相似度纳入计算。另一个办法是引入类目补全,如果找不到相似的衣服,退而匹配同风格同色系的其他衣服。这个策略在系统里叫做规则兜底,虽然是土办法,但在毕设场景里比上模型效果更可靠。

5.4 Flask 前后端联调时的跨域问题

现象:前端页面单独用npm run dev起在 3000 端口,后端 Flask 跑在 5000 端口,浏览器里调用接口显示CORS policy: No 'Access-Control-Allow-Origin' header is present。

原因:浏览器的同源策略,不允许不同端口之间的跨域请求,后端没有返回跨域响应头。

解决:用flask-cors扩展,初始化时直接允许所有来源,毕设阶段不需要把跨域配得太严格。

from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "*"}})

这段代码放在app.py里创建 Flask 实例之后。资源的匹配规则只对/api/开头的接口生效,避免把静态资源也暴露出去。加了flask-cors之后如果还报跨域,多半是前端用了application/x-www-form-urlencoded的 POST 请求,而后端只处理 JSON,这时需要在请求头里加Content-Type: application/json。

5.5 推荐结果不稳定:每次点击刷新结果顺序都变

现象:同一个用户连续刷新三次推荐接口,推荐的衣服集合一样,但显示顺序不同,每隔几秒排序就变一次。

原因:在这些代码里,如果对评分相同的物品做sorted,Python 的排序是稳定的,但多个候选集的合并过程中,字典的遍历顺序是基于插入顺序的,而插入顺序来自不同的集合运算,导致同分物品的前后顺序不确定。

解决:在sorted时同时传入两个排序键,主键是推荐分数,次键是固定不变的衣服 ID,这样同分数的物品会按 ID 升序排列,结果每次都一致。注意别把里层的外层的键搞混。

6. 让推荐效果真正可用的三个进阶做法

第一个进阶做法是召回和排序分离。当前系统的hybrid_recommend做的是「召回 20 条,再混合排序取前 10」,这其实已经是召回/排序的雏形。如果想要效果更好,可以再加一层重排序:前 10 条结果不要直接输出,而是再用一条规则去重和打散——比如连续出现的必须是不同类别的衣服,不能让五条裙子排在一起。

第二个做法是加入时间衰减。服饰是一个季节性很强的品类,冬季的羽绒服如果推荐给用户,其他条件相同的情况下,夏装的权重应该更高。实现方式也不复杂,在计算用户偏好时给评分乘一个时间衰减系数,越近的评分权重越大,做的事情本质上是给timestamp增加一个指数衰减函数。

import math def time_decay_weight(timestamp, half_life_days=30): days_since = (current_time - timestamp).days return 0.5 ** (days_since / half_life_days)

half_life_days的含义是评分权重减半需要的天数,默认 30 天意味着一个月前的评分权重只有现在的一半。这个函数可以直接嵌入之前写好的compute_user_preference里,把原来的rating乘以权重系数。答辩时讲这个点,老师会很容易理解你是考虑了业务场景的。

第三个做法是输出可解释的推荐理由。整套推荐代码里,最容易被忽略却又最容易被老师追问的,就是「为什么推荐这条裙子」。我的习惯是在输出结果之前,记录每一件被推荐衣服命中了哪些用户偏好标签,再把标签组合成自然语言。比如用户的偏好字典里休闲风格是 4.8 分,衣服的风格也是休闲,那推荐理由就是「休闲风格是您评分较高的类型,这件衣服也属于该风格」。这个逻辑不用写到多复杂的程度,一个if嵌套就能实现,但它能证明不推荐是拍脑袋的事。

整套系统跑通之后,我自己每次调试时都会强制走一遍三件事:先清空数据库重新初始化,确认数据没问题;再注册一个新账号验证冷启动兜底;最后测试推荐结果与已评分数据不能重复。这三步走完之后才会去动算法参数,不然改了一堆参数分不清是算法提升了还是数据的随机波动。从那以后我接手的每个推荐类项目都保留这个习惯,这套服饰推荐系统能让你在毕设答辩时少流很多汗。希望这篇拆解帮到你拿到想要的分数。

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

返回列表