同過濾電影推薦系統(tǒng):從零實現(xiàn)ItemCF與UserCF畢業(yè)設(shè)計)
簡介這是一套面向計算機相關(guān)專業(yè)畢業(yè)設(shè)計與課程設(shè)計場景的完整項目資料核心為基于協(xié)同過濾算法的電影推薦系統(tǒng)Python實現(xiàn)附帶論文與項目說明適合正在準(zhǔn)備畢設(shè)、期末大作業(yè)或需要推薦算法實戰(zhàn)練習(xí)的學(xué)習(xí)者直接參考使用。資源包共約2000個文件壓縮后約28.04MB其中以1159個py源碼文件為主體配合324個pyc編譯文件、124個html頁面、40個js腳本與16個css樣式文件構(gòu)成Web交互界面另有148張jpg與11張png圖片資源、7個csv數(shù)據(jù)集、5個json與5個xml配置以及63個po與63個mo多語言文件整體結(jié)構(gòu)完整、層次清晰。項目經(jīng)過嚴(yán)格調(diào)試可正常運行并配有論文文檔與說明材料便于理解協(xié)同過濾推薦流程、系統(tǒng)模塊劃分與前后端組織方式。目前已有147人學(xué)習(xí)關(guān)注可作為畢設(shè)選題落地的可靠參考。1. 從零手寫協(xié)同過濾電影推薦系統(tǒng)畢業(yè)設(shè)計到底在做什么很多同學(xué)拿到「基于協(xié)同過濾的電影推薦系統(tǒng)Python實現(xiàn)源碼論文」這個題目時第一反應(yīng)是去搜一份現(xiàn)成源碼改改界面就交差。但真正答辯時被問一句「你的相似度矩陣怎么算的、冷啟動怎么處理」往往就答不上來。這個題目的核心其實只有兩件事一是用 Python 把用戶對電影的評分?jǐn)?shù)據(jù)變成一張可計算的矩陣二是用協(xié)同過濾算法從這張矩陣?yán)镱A(yù)測出「你可能喜歡的電影」。它適合計算機、軟件工程、大數(shù)據(jù)方向的本科畢業(yè)生也適合想入門推薦算法的 Python 學(xué)習(xí)者。整套系統(tǒng)通常包含數(shù)據(jù)加載、相似度計算、評分預(yù)測、Top-N 推薦、可視化展示和論文撰寫六個環(huán)節(jié)。下面我按自己帶過幾屆畢設(shè)的經(jīng)驗把這條鏈路拆開講清楚讓你既能跑通代碼也能在論文里寫出有說服力的算法分析。2. 協(xié)同過濾的兩條路線UserCF 和 ItemCF 怎么選協(xié)同過濾Collaborative Filtering的本質(zhì)是「物以類聚人以群分」。它不關(guān)心電影本身是什么類型、導(dǎo)演是誰只關(guān)心「誰和誰的口味像」「哪些電影被同一批人喜歡」。這個思路決定了它有兩個分支基于用戶的協(xié)同過濾UserCF和基于物品的協(xié)同過濾ItemCF。選哪條路線直接決定你后面代碼怎么寫、論文怎么論證。2.1 UserCF 與 ItemCF 的數(shù)學(xué)差異UserCF 的核心假設(shè)是如果用戶 A 和用戶 B 對很多電影的打分都接近那 A 喜歡但 B 沒看過的電影就可以推薦給 B。它的計算對象是「用戶-用戶」相似度矩陣。假設(shè)有 M 個用戶、N 部電影評分矩陣 R 是 M×N 的稀疏矩陣UserCF 要算的是 M×M 的用戶相似度矩陣。ItemCF 反過來如果電影 X 和電影 Y 被很多同一批用戶喜歡那喜歡 X 的人大概率也會喜歡 Y。它算的是 N×N 的物品相似度矩陣。兩者的數(shù)學(xué)公式都以余弦相似度或皮爾遜相關(guān)系數(shù)為基礎(chǔ)。以余弦相似度為例用戶 u 和用戶 v 的相似度sim(u,v) Σ(r_ui · r_vi) / (√Σr_ui2 · √Σr_vi2)其中 r_ui 是用戶 u 對電影 i 的評分。ItemCF 把用戶換成物品即可。實際選型時有個經(jīng)驗法則用戶數(shù)遠小于物品數(shù)時用 UserCF物品數(shù)遠小于用戶數(shù)時用 ItemCF。電影推薦場景里MovieLens 數(shù)據(jù)集通常有幾千用戶、幾千電影兩者都能跑。但 UserCF 有個明顯問題——用戶口味變化快相似度矩陣需要頻繁更新而電影的內(nèi)容屬性相對穩(wěn)定ItemCF 的相似度矩陣可以離線算好、定期更新。所以工業(yè)界比如早期的亞馬遜更偏向 ItemCF。畢業(yè)設(shè)計里我一般建議主做 ItemCF論文里對比 UserCF 作為改進論證這樣既有工作量又能體現(xiàn)你對兩種算法的理解。2.2 用 Python 加載 MovieLens 并構(gòu)建評分矩陣MovieLens 是這個題目最常用的公開數(shù)據(jù)集常見的是 ml-latest-small 版本包含 ratings.csv、movies.csv 兩個核心文件。ratings.csv 的字段是 userId、movieId、rating、timestamp每行是一條評分記錄。下面這段代碼完成數(shù)據(jù)加載和評分矩陣構(gòu)建import pandas as pd import numpy as np # 加載評分?jǐn)?shù)據(jù)和電影信息 ratings pd.read_csv(ml-latest-small/ratings.csv) movies pd.read_csv(ml-latest-small/movies.csv) # 只保留有評分的用戶和電影過濾掉評分次數(shù)過少的用戶冷啟動處理 user_counts ratings[userId].value_counts() active_users user_counts[user_counts 20].index ratings ratings[ratings[userId].isin(active_users)] # 構(gòu)建用戶-電影評分矩陣缺失值填0表示未評分 rating_matrix ratings.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) print(f評分矩陣形狀: {rating_matrix.shape}) print(f稀疏度: {1 - (ratings.shape[0] / (rating_matrix.shape[0] * rating_matrix.shape[1])):.4f})這段代碼有三個關(guān)鍵點。第一pivot_table把長表轉(zhuǎn)成寬表行是用戶、列是電影這是協(xié)同過濾的標(biāo)準(zhǔn)輸入格式。第二fillna(0)把未評分填成 0但要注意——0 在余弦相似度里表示「沒有交互」不是「打了 0 分」這個區(qū)別在論文里必須寫清楚否則會被答辯老師追問。第三過濾掉評分少于 20 條的用戶是為了降低矩陣稀疏度、提升相似度計算的信噪比。MovieLens 小數(shù)據(jù)集的稀疏度通常在 98% 以上也就是說矩陣?yán)?98% 的位置都是 0這是協(xié)同過濾面臨的核心挑戰(zhàn)之一。2.3 ItemCF 相似度矩陣的計算與參數(shù)說明構(gòu)建好評分矩陣后下一步是算物品之間的相似度。這里用余弦相似度把評分矩陣轉(zhuǎn)置后按列計算from sklearn.metrics.pairwise import cosine_similarity # 轉(zhuǎn)置矩陣行變成電影列變成用戶 item_user_matrix rating_matrix.T.values # 計算電影之間的余弦相似度 item_sim_matrix cosine_similarity(item_user_matrix) # 將對角線置0避免電影和自己相似度為1影響推薦 np.fill_diagonal(item_sim_matrix, 0) # 取相似度最高的K部電影作為鄰居K是核心調(diào)參項 K 20 item_sim_df pd.DataFrame( item_sim_matrix, indexrating_matrix.columns, columnsrating_matrix.columns ) print(f相似度矩陣形狀: {item_sim_df.shape}) print(f電影1最相似的5部電影: {item_sim_df[1].nlargest(5).index.tolist()})這里有幾個參數(shù)需要重點說明。K 值控制鄰居數(shù)量K 太小推薦結(jié)果不穩(wěn)定K 太大則引入噪聲。經(jīng)驗值在 10 到 50 之間我一般先用 20 跑基線再在論文里做 K 值敏感性分析。相似度度量方式除了余弦還可以用皮爾遜相關(guān)系數(shù)后者對用戶評分偏置更魯棒但計算量更大。對角線置 0是必須的否則推薦結(jié)果里會出現(xiàn)「因為你看過這部電影所以推薦這部電影」的荒謬情況。另外要注意cosine_similarity返回的是 numpy 數(shù)組轉(zhuǎn)成 DataFrame 時索引和列名要對齊否則后面按 movieId 取相似電影時會取錯。3. 評分預(yù)測與 Top-N 推薦從相似度到可解釋的推薦列表有了相似度矩陣接下來要解決的是「給定一個用戶怎么生成推薦列表」。這一步分兩個子問題一是預(yù)測用戶對未看電影的評分二是按評分排序取前 N 個。很多同學(xué)的代碼到這里就開始「玄學(xué)」了——推薦結(jié)果時好時壞自己也說不清為什么。核心原因通常是評分預(yù)測公式?jīng)]寫對或者沒有做去偏處理。3.1 基于加權(quán)平均的評分預(yù)測公式ItemCF 預(yù)測用戶 u 對電影 i 的評分思路是找到和電影 i 最相似的 K 部電影看用戶 u 對這些電影的打分用相似度加權(quán)平均。公式如下pred(u,i) Σ(sim(i,j) · r_uj) / Σ|sim(i,j)|其中 j 遍歷電影 i 的 K 個最近鄰且用戶 u 對 j 有過評分。這個公式的直覺是和你喜歡的電影越像的電影你越可能喜歡。下面是 Python 實現(xiàn)def predict_rating(user_id, movie_id, rating_matrix, item_sim_df, K20): 預(yù)測用戶對某部電影的評分 if movie_id not in item_sim_df.columns: return 0 # 獲取該用戶已評分的電影 user_ratings rating_matrix.loc[user_id] rated_movies user_ratings[user_ratings 0].index # 取與目標(biāo)電影最相似的K部且用戶已評分的電影 sim_scores item_sim_df[movie_id].loc[rated_movies] top_k sim_scores.nlargest(K) if top_k.sum() 0: return 0 # 加權(quán)平均 numerator sum(top_k[j] * user_ratings[j] for j in top_k.index) denominator sum(abs(top_k[j]) for j in top_k.index) return numerator / denominator if denominator ! 0 else 0這段代碼里有個容易翻車的點sim_scores.nlargest(K)取的是相似度最高的 K 部電影但如果用戶對其中某部電影的評分是 0未評分加權(quán)平均時會被錯誤地當(dāng)成「打了 0 分」。所以必須先過濾rated_movies只保留用戶真正評過分的電影。另一個點是分母用了abs()因為相似度可能為負(fù)余弦相似度在評分向量夾角大于 90 度時為負(fù)取絕對值保證權(quán)重為正。如果你的數(shù)據(jù)集里出現(xiàn)負(fù)相似度說明這兩個電影的用戶群體幾乎不重疊這種鄰居其實應(yīng)該被剔除可以在nlargest之前先過濾掉相似度小于 0 的項。3.2 生成 Top-N 推薦列表并過濾已看預(yù)測完評分后要對用戶沒看過的所有電影按預(yù)測分排序取前 N 個。這里的關(guān)鍵是「過濾已看」——推薦系統(tǒng)最忌諱把用戶已經(jīng)看過的電影再推一遍。實現(xiàn)如下def recommend_movies(user_id, rating_matrix, item_sim_df, top_n10, K20): 為用戶生成Top-N電影推薦 # 用戶已評分的電影 user_ratings rating_matrix.loc[user_id] rated_movies set(user_ratings[user_ratings 0].index) # 所有電影 all_movies set(rating_matrix.columns) # 候選集 所有電影 - 已看 candidate_movies all_movies - rated_movies # 預(yù)測評分 predictions [] for movie_id in candidate_movies: pred predict_rating(user_id, movie_id, rating_matrix, item_sim_df, K) if pred 0: predictions.append((movie_id, pred)) # 按預(yù)測分降序排序取前N predictions.sort(keylambda x: x[1], reverseTrue) top_n_movies predictions[:top_n] # 關(guān)聯(lián)電影標(biāo)題 result [] for movie_id, pred in top_n_movies: title movies[movies[movieId] movie_id][title].values result.append({ movieId: movie_id, title: title[0] if len(title) 0 else Unknown, predicted_rating: round(pred, 2) }) return result # 測試給用戶1推薦10部電影 recs recommend_movies(1, rating_matrix, item_sim_df, top_n10) for r in recs: print(f{r[title]} 預(yù)測評分: {r[predicted_rating]})這段代碼的性能瓶頸在for movie_id in candidate_movies這個循環(huán)。如果候選集有幾千部電影每部都要算一次加權(quán)平均Python 原生循環(huán)會非常慢。優(yōu)化方向有兩個一是用 numpy 向量化把相似度矩陣和評分向量做矩陣乘法二是預(yù)先算好每部電影的 K 個最近鄰存成字典預(yù)測時直接查表。畢業(yè)設(shè)計的數(shù)據(jù)量通常不大原生循環(huán)能跑通但論文里可以提一句「工程上可用向量化或倒排索引優(yōu)化」體現(xiàn)你有工程意識。3.3 用 RMSE 和 MAE 評估推薦質(zhì)量推薦系統(tǒng)不能只看「推出來的電影像不像」要有量化指標(biāo)。最常用的是 RMSE均方根誤差和 MAE平均絕對誤差衡量預(yù)測評分和真實評分的差距。做法是把評分?jǐn)?shù)據(jù)按 8:2 切成訓(xùn)練集和測試集在訓(xùn)練集上算相似度在測試集上預(yù)測并計算誤差from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error # 劃分訓(xùn)練集和測試集 train_data, test_data train_test_split(ratings, test_size0.2, random_state42) # 用訓(xùn)練集重建評分矩陣和相似度矩陣代碼同上此處省略 # ... # 在測試集上評估 true_ratings [] pred_ratings [] for _, row in test_data.iterrows(): pred predict_rating(row[userId], row[movieId], train_matrix, train_item_sim_df, K20) if pred 0: true_ratings.append(row[rating]) pred_ratings.append(pred) rmse np.sqrt(mean_squared_error(true_ratings, pred_ratings)) mae mean_absolute_error(true_ratings, pred_ratings) print(fRMSE: {rmse:.4f}, MAE: {mae:.4f})MovieLens 小數(shù)據(jù)集上ItemCF 的 RMSE 通常在 0.85 到 0.95 之間MAE 在 0.65 到 0.75 之間。如果你的結(jié)果明顯偏離這個范圍先檢查是不是把測試集的評分泄漏到了訓(xùn)練集里——這是畢設(shè)里最常見的「翻車」原因。另外train_test_split的random_state要固定否則每次跑出來的指標(biāo)不一樣論文里的數(shù)據(jù)就沒法復(fù)現(xiàn)。4. 避坑與排查協(xié)同過濾畢設(shè)里最容易翻車的 5 個地方帶過幾屆畢設(shè)后我發(fā)現(xiàn)同學(xué)們踩的坑高度集中。下面這 5 條按「現(xiàn)象 → 原因 → 解決」整理每條都是血淚經(jīng)驗。4.1 推薦結(jié)果全是冷門電影現(xiàn)象給用戶推薦的電影大部分是評分人數(shù)很少的冷門片用戶根本沒聽過。原因余弦相似度對熱門電影有天然偏好——熱門電影和很多電影都有共同評分用戶相似度容易偏高而冷門電影因為評分用戶少相似度計算不穩(wěn)定偶爾會出現(xiàn)「兩個冷門電影因為唯一一個共同用戶而相似度極高」的情況。另外如果沒做評分次數(shù)過濾長尾電影會污染相似度矩陣。解決在計算相似度之前過濾掉評分次數(shù)少于閾值比如 50 次的電影或者在相似度公式里加入熱門度懲罰項比如用sim(i,j) / log(1 count(i))降低熱門電影的權(quán)重。更簡單的做法是在推薦結(jié)果里做多樣性重排避免同一類型的冷門片扎堆。4.2 RMSE 低但推薦結(jié)果很差現(xiàn)象論文里 RMSE 只有 0.82看起來不錯但實際推薦列表里全是用戶不感興趣的電影。原因RMSE 衡量的是評分預(yù)測精度不是推薦排序質(zhì)量。一個總是預(yù)測「平均分 3.5」的模型RMSE 可能很低但它沒有區(qū)分能力。推薦系統(tǒng)的核心是排序不是回歸。解決補充排序指標(biāo)比如 PrecisionK、RecallK、NDCGK。做法是對每個用戶把測試集里評分大于 4 的電影作為「真正相關(guān)」看推薦列表前 K 個里命中了幾個。Precision10 能到 0.15 以上就算及格。論文里同時報告 RMSE 和 PrecisionK論證才完整。4.3 矩陣太大導(dǎo)致內(nèi)存溢出現(xiàn)象跑相似度計算時程序卡死或者報MemoryError。原因cosine_similarity會生成一個 N×N 的稠密矩陣。如果電影數(shù)超過 1 萬這個矩陣就是 1 億個浮點數(shù)占 800MB 內(nèi)存如果用戶數(shù)也很大轉(zhuǎn)置后的矩陣更夸張。解決用稀疏矩陣scipy.sparse存儲評分矩陣相似度計算改用sklearn.metrics.pairwise.cosine_similarity的稀疏版本或者只計算每個物品的 Top-K 鄰居而不是全量矩陣。畢業(yè)設(shè)計的數(shù)據(jù)量通常不至于溢出但如果你用了 ml-20m 這種大數(shù)據(jù)集就必須做稀疏化處理。4.4 用戶相似度計算時把「未評分」當(dāng)成「差評」現(xiàn)象UserCF 跑出來的推薦結(jié)果和直覺完全相反喜歡的電影被預(yù)測成低分。原因評分矩陣?yán)镉?0 填充未評分項但余弦相似度會把 0 當(dāng)成「評分為 0」導(dǎo)致兩個用戶即使都沒看過某部電影也會因為「都打了 0 分」而增加相似度。這是協(xié)同過濾最經(jīng)典的陷阱。解決計算相似度時只考慮兩個用戶共同評過分的電影用掩碼矩陣過濾掉未評分項。或者改用皮爾遜相關(guān)系數(shù)它本身會減去用戶平均分對未評分項的處理更魯棒。代碼上可以用np.where(rating_matrix 0, rating_matrix, np.nan)把 0 變成 NaN再用np.nanmean之類的函數(shù)處理。4.5 論文里的公式和代碼對不上現(xiàn)象答辯老師對照論文公式和源碼發(fā)現(xiàn)符號定義不一致、下標(biāo)范圍不對、甚至公式寫錯了。原因論文里的公式是直接從參考文獻抄的代碼是按自己理解寫的兩者沒有對齊。比如論文里寫「sim(u,v) 表示用戶 u 和 v 的相似度」代碼里卻用item_sim變量名老師一看就懵。解決寫論文時每個公式下面用一段話解釋「對應(yīng)代碼里的哪個函數(shù)、哪個變量」符號表單獨列一張表。代碼里的變量命名盡量和論文公式一致比如論文用sim(i,j)代碼里就用sim_ij而不是score。這個細節(jié)能大幅提升答辯通過率。5. 從能跑到能寫進論文三個讓畢設(shè)加分的小技巧跑通代碼只是及格線要讓畢設(shè)拿到好成績還得在「可解釋性」和「對比實驗」上做文章。下面三個技巧是我?guī)W(xué)生時反復(fù)驗證有效的不需要復(fù)雜工程但能讓論文的算法章節(jié)厚實很多。5.1 用相似電影解釋推薦理由推薦系統(tǒng)最被詬病的是「黑匣子」——用戶不知道為什么被推薦了這部電影。ItemCF 天然有可解釋性推薦電影 i 是因為你喜歡電影 j而 i 和 j 相似。把這個理由展示出來既提升用戶體驗又能在論文里作為「可解釋推薦」的亮點。實現(xiàn)很簡單在推薦結(jié)果里加上「因為你看過 XX」def explain_recommendation(user_id, movie_id, rating_matrix, item_sim_df, K5): 解釋為什么推薦這部電影 user_ratings rating_matrix.loc[user_id] rated_movies user_ratings[user_ratings 0].index sim_scores item_sim_df[movie_id].loc[rated_movies] top_k sim_scores.nlargest(K) explanations [] for mid, sim in top_k.items(): title movies[movies[movieId] mid][title].values explanations.append({ because_you_watched: title[0] if len(title) 0 else Unknown, similarity: round(sim, 3), your_rating: user_ratings[mid] }) return explanations這段代碼返回的是「因為你看了 A你打了 4 分而 A 和 B 的相似度是 0.85所以推薦 B」。論文里可以把這個作為「推薦解釋模塊」單獨寫一節(jié)配上界面截圖工作量就上來了。5.2 對比 UserCF 和 ItemCF 的實驗設(shè)計論文里只寫一種算法會顯得單薄。建議做一組對比實驗同樣的數(shù)據(jù)集、同樣的訓(xùn)練測試劃分分別跑 UserCF 和 ItemCF對比 RMSE、MAE、Precision10 和運行時間。下面是一個對比表格的模板指標(biāo)UserCFItemCFRMSE0.920.88MAE0.710.68Precision100.120.16相似度矩陣大小M×MN×N運行時間秒4532表格里的數(shù)據(jù)要自己跑出來不能編。實驗設(shè)計部分寫清楚數(shù)據(jù)集版本、訓(xùn)練測試比例、K 值、相似度度量方式、隨機種子。這樣老師問「你的實驗可復(fù)現(xiàn)嗎」你可以直接說「固定了 random_state42代碼在附錄」。5.3 冷啟動問題的低成本處理方案冷啟動是推薦系統(tǒng)的經(jīng)典難題新用戶沒有評分歷史新電影沒有用戶評分。畢業(yè)設(shè)計里不需要做復(fù)雜的深度學(xué)習(xí)方案兩個低成本處理就能寫進論文。第一新用戶引導(dǎo)注冊時讓用戶勾選幾部看過的電影并打分用這幾條數(shù)據(jù)算相似度雖然稀疏但比沒有強。第二熱門兜底如果用戶評分記錄少于 5 條直接推薦全站評分最高的 10 部電影按貝葉斯平均分排序避免只有 1 個 5 分的新電影排第一。貝葉斯平均分公式bayesian_score (v / (v m)) · R (m / (v m)) · C其中 v 是電影評分人數(shù)R 是電影平均分m 是最小評分人數(shù)閾值比如 50C 是全站平均分。這個公式在論文里寫出來比單純說「推薦熱門電影」專業(yè)得多。最后說個我自己的習(xí)慣每次跑完實驗把參數(shù)、指標(biāo)、隨機種子記在一個experiment_log.md里論文寫到哪一步都能回溯。帶過的學(xué)生里凡是堅持記日志的答辯時被問細節(jié)都不慌凡是跑完就忘的最后都在「你這個 0.88 是怎么來的」上卡殼。希望幫到你。本文還有配套的精品資源點擊獲取