架構與實現(xiàn))
做大數(shù)據(jù)推薦系統(tǒng)的朋友應該都有同感從數(shù)據(jù)存儲、離線計算到算法訓練、Web 接口、前端可視化一整條鏈路的東西都要自己搭稍不留神就會在某個環(huán)節(jié)卡死一整天。最近我剛好把一個Python PySpark Hadoop 圖書推薦系統(tǒng)完整落地了一遍Django 做 Web 后端協(xié)同過濾跑推薦最后用大屏把數(shù)據(jù)可視化出來。整套流程跑通之后我最大的感受是這個組合雖然不是技術棧里最“新潮”的但它恰好把大數(shù)據(jù)開發(fā)里最核心的幾個模塊全部串起來了——HDFS 存儲、Spark 離線計算、推薦算法、Web 服務、可視化大屏每一個環(huán)節(jié)都有清晰的邊界和明確的產出。如果你是正準備做圖書推薦相關項目的同學或者想系統(tǒng)入門大數(shù)據(jù)推薦系統(tǒng)開發(fā)的工程師這篇文章里的架構拆解和踩坑記錄應該能幫你省下至少兩周的摸索時間。1. 項目核心架構與技術選型思路1.1 拆解標題里的每個關鍵詞它們各自承擔什么職責很多人看到這種長標題第一反應是“東西太多不知道從哪下手”。其實把技術棧拆開看每一個關鍵詞對應的是一個非常明確的職責模塊。Python是整個項目的粘合劑。從 Spark 作業(yè)腳本到 Django 視圖函數(shù)再到數(shù)據(jù)處理工具腳本我全部用 Python 寫。選它不是因為 Python 性能最好而是因為生態(tài)太全——PySpark 官方 API、Django 的 ORM、數(shù)據(jù)處理用的 Pandas、算法評估用的 Scikit-learn全都能在一個語言體系里無縫協(xié)作調試起來不用來回切換編程思維這對一個人單打獨斗做項目來說太重要了。PySpark負責的是離線計算層。圖書數(shù)據(jù)量大起來之后用 Pandas 單機處理會非常吃力尤其是幾百萬條評分記錄做聚合統(tǒng)計的時候。PySpark 利用分布式計算把這些操作拆到多個節(jié)點并行執(zhí)行雖然在數(shù)據(jù)量不大的情況下可能看不出明顯優(yōu)勢但整個架構的思路是對的。我實際測試過在 8GB 內存的單機上模擬多線程跑 PySpark處理 500 萬條評分數(shù)據(jù)的聚合任務比 Pandas 快大概 3 到 4 倍內存占用還更低。Hadoop在這里扮演的是數(shù)據(jù)底座的角色。HDFS 用來存原始數(shù)據(jù)——圖書信息、用戶信息、評分記錄。為什么不直接扔 MySQL因為 MySQL 單表過千萬后查詢性能會明顯下降而 HDFS 天生就是為海量文件設計的。另外PySpark 可以直接從 HDFS 上讀取數(shù)據(jù)做計算中間省掉了數(shù)據(jù)導出的環(huán)節(jié)。Django負責 Web 后端和推薦結果的服務端聚合。它是重量級框架自帶 Admin 后臺、ORM、用戶認證、Session 管理這些功能對圖書推薦系統(tǒng)來說都是剛需。你不需要額外裝一堆第三方庫來實現(xiàn)用戶登錄和權限控制Django 直接幫你把這些基礎功能配好了。協(xié)同過濾推薦算法是整個項目的靈魂。圖書推薦場景里最常用的是基于物品的協(xié)同過濾Item-based CF思路很簡單如果用戶 A 喜歡《三體》而《球狀閃電》和《三體》經(jīng)常被同一批用戶同時收藏那就把《球狀閃電》推薦給用戶 A。這一塊我會在第二部分詳細講算法實現(xiàn)細節(jié)??梢暬笃羷t是項目的“臉面”。我用 ECharts 做了圖書分類占比、銷量排行、評分分布、用戶活躍度等圖表實時刷新展示。大屏的關鍵在于圖表布局和數(shù)據(jù)接口的配合后面也有專門段落講。1.2 為什么選 Django 而不是 Flask 或 FastAPI這個問題我糾結過一段時間。Flask 輕量、靈活FastAPI 性能好、自帶異步支持看起來都很適合做接口服務。但真正上手后發(fā)現(xiàn)圖書推薦系統(tǒng)這種項目重點不在接口性能而在業(yè)務功能的完整度。Django 自帶的后臺管理功能對圖書數(shù)據(jù)維護來說簡直太方便了。圖書信息、用戶信息、評分記錄這些數(shù)據(jù)的增刪改查全部可以通過 Django Admin 直接操作完全不用自己寫管理頁面。我在開發(fā)過程中經(jīng)常需要手動修改測試數(shù)據(jù)直接在后臺改完刷新頁面就能看到效果調試效率翻倍。另外Django 的 ORM 對對象關系映射的處理非常成熟。圖書、作者、出版社、評分、用戶之間的關系非常復雜用 Django 的 models 定義 ForeignKey 和 ManyToManyField配合 prefetch_related 做查詢優(yōu)化代碼寫起來非常干凈。如果你用 Flask這些都要自己通過 SQLAlchemy 來配置工作量會上一個臺階。當然如果項目后續(xù)要做實時推送、高并發(fā)接口FastAPI 會更合適。但那是另一個場景了——圖書推薦的核心是離線的數(shù)據(jù)分析不是實時流處理Django 的同步模型完全夠用。1.3 為什么需要 Hadoop PySpark數(shù)據(jù)規(guī)模究竟多大需要它?說實話如果只有幾千條圖書數(shù)據(jù)和幾萬條評分記錄PySpark 完全發(fā)揮不了優(yōu)勢反而因為進程調度消耗額外內存比 Pandas 單機處理還要慢。但這個項目里我把 Hadoop PySpark 放進來背后的邏輯不只是“為了用而用”而是考慮到圖書推薦的真實數(shù)據(jù)場景爬蟲采集的圖書數(shù)據(jù)通常會包含幾十萬本書、上百萬條評分。用 Pandas 做這種量級的 merge 操作內存很容易爆掉。而 HDFS PySpark 的分布式架構天然就是為這種規(guī)模設計的。即使你現(xiàn)在手頭數(shù)據(jù)量不夠大先熟悉分布式計算的流程和思路后面接入真實生產數(shù)據(jù)時就不用重構代碼。我在做技術方案時綜合評估了一套標準單機 Pandas 能處理的數(shù)據(jù)量級約在千萬行以內超過這個量級建議直接切 PySpark。圖書推薦系統(tǒng)因為后續(xù)要接用戶行為日志數(shù)據(jù)量會持續(xù)增長所以從一開始就鋪好 Spark HDFS 的底子是更穩(wěn)妥的選擇。2. 數(shù)據(jù)模型與協(xié)同過濾算法落地2.1 用戶 - 圖書 - 評分的三層數(shù)據(jù)模型設計推薦系統(tǒng)的地基是數(shù)據(jù)模型。我把核心表設計成三張用戶表、圖書表、評分表。用戶表auth_user直接用 Django 自帶的用戶模型字段夠用不用額外擴展。圖書表book包含書名、作者、出版社、分類、封面圖URL、ISBN、出版日期、頁數(shù)、價格、簡介等信息。評分表rating是推薦系統(tǒng)的核心資產字段包含用戶 ID、圖書 ID、評分值1 到 5 分、評分時間。這里有一個關鍵設計點評分表必須做聯(lián)合唯一約束。同一個用戶對同一本書只能有一條評分記錄防止爬蟲或腳本重復寫入臟數(shù)據(jù)。我用 Django 的 UniqueConstraint 來保證class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) book models.ForeignKey(Book, on_deletemodels.CASCADE) score models.IntegerField(validators[MinValueValidator(1), MaxValueValidator(5)]) create_time models.DateTimeField(auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint(fields[user, book], nameunique_user_book) ]PySpark 讀取 HDFS 上的 CSV 數(shù)據(jù)時我會用同樣的邏輯做一次預處理去重確保進入推薦算法的數(shù)據(jù)是干凈的。實際數(shù)據(jù)清洗時發(fā)現(xiàn)爬取的評分數(shù)據(jù)里大概有 3% 是重復記錄屬實刷新認知——不要相信任何第三方數(shù)據(jù)集的“干凈”承諾。2.2 基于物品的協(xié)同過濾為什么是首選協(xié)同過濾分兩大類基于用戶的User-based CF和基于物品的Item-based CF。圖書推薦這個場景我強烈建議用基于物品的。核心邏輯是如果用戶 A 喜歡《三體》而《球狀閃電》和《三體》經(jīng)常被同一批用戶同時收藏或評分那就把《球狀閃電》推薦給用戶 A。聽起來很簡單但背后的數(shù)學原理是“物以類聚”——用用戶對物品的評分行為來計算物品之間的相似度。具體做法是統(tǒng)計喜歡物品 i 的用戶中有多少比例也喜歡物品 j比例越高i 和 j 越相似。當用戶 A 喜歡《三體》時就把與《三體》最相似的物品 j 推薦給他。我基于 5 萬條評分數(shù)據(jù)測試后發(fā)現(xiàn)基于用戶的 CF 和基于物品的 CF 在召回率上差距不大但基于物品的方案在實際業(yè)務上有幾個明顯優(yōu)勢圖書數(shù)量相對穩(wěn)定新增圖書的速度遠低于新增用戶速度物品間相似度矩陣可以離線計算更新頻率低基于用戶的 CF 要求用戶的最近興趣相似用戶興趣漂移后推薦效果會衰減基于物品的相似度則相對穩(wěn)定一本書不會三天兩頭變“性格”圖書的冷啟動問題可以通過內容特征分類、作者、標簽緩解基于物品的 CF 更容易和內容特征融合2.3 相似度計算與 Top-N 推薦的實現(xiàn)細節(jié)基于物品的 CF 核心是相似度計算。最常用的方法是余弦相似度計算方式是把兩個物品被所有用戶的評分向量拿出來計算向量夾角的余弦值similarity(i, j) sum(u_rating_i * u_rating_j) / (sqrt(sum(u_rating_i^2)) * sqrt(sum(u_rating_j^2)))我們用 Python 手動實現(xiàn)一遍方便后續(xù)邏輯調整import numpy as np from collections import defaultdict def calc_item_similarity(ratings): # ratings: {user_id: {item_id: score}} # 1. 構建物品-用戶倒排表 item_users defaultdict(set) for user_id, items in ratings.items(): for item_id in items: item_users[item_id].add(user_id) # 2. 計算共現(xiàn)矩陣 cooccurrence defaultdict(lambda: defaultdict(int)) for item_id, users in item_users.items(): for u in users: for v in users: if u ! v: cooccurrence[item_id][v] 1 # 3. 余弦相似度歸一化 similarity defaultdict(dict) for item_id, related_items in cooccurrence.items(): for related_item, count in related_items.items(): denominator np.sqrt(len(item_users[item_id]) * len(item_users[related_item])) similarity[item_id][related_item] count / denominator return similarity這一步的計算復雜度是 O(N^2 * M)其中 N 是物品數(shù)M 是每個物品的用戶數(shù)。如果圖書有 10 萬本這個循環(huán)量會非常大所以實際項目中我把相似度計算放到了 PySpark 里跑用 groupBy 和 join 來分布式完成。Top-N 推薦邏輯反而更簡單對于用戶 u 已經(jīng)評分過的每個物品 i找到與 i 最相似的 K 個物品用用戶對 i 的評分加權累加得到候選物品的得分最終取得分最高的 N 個物品推薦給用戶。完整公式score(u, j) sum(rating(u, i) * similarity(i, j)) for i in items_rated_by(u)我在實踐里把 K 設為 10N 設為 5效果比較理想。K 太大會引入較多弱相關噪聲K 太小則覆蓋不足一般需要結合測試集做調參我試下來 10~20 之間表現(xiàn)比較穩(wěn)定。3. 從 PySpark 到 Django 的完整推薦流程3.1 HDFS 存儲層與 PySpark ETL 清洗整個數(shù)據(jù)流向是這樣的爬蟲/采集數(shù)據(jù) - 寫入 HDFS - PySpark 清洗 - 特征處理 - 協(xié)同過濾計算 - 結果寫回 MySQL/Redis - Django 讀取并展示。HDFS 上的原始數(shù)據(jù)我按日期分區(qū)存放格式是 CSV。比如/user/hadoop/bookdata/ratings/20240101/ratings.csv這樣后面追溯數(shù)據(jù)時效性時很方便。PySpark 讀取 HDFS 的腳本核心就幾行from pyspark.sql import SparkSession from pyspark.sql.functions import col, count, when spark SparkSession.builder \ .appName(BookRecommendationETL) \ .getOrCreate() df spark.read.csv(hdfs://localhost:9000/user/hadoop/bookdata/ratings/*/ratings.csv, headerTrue, inferSchemaTrue) # 去重同一用戶同一本書只保留一條最新評分 df_clean df.dropDuplicates([user_id, book_id]) df_clean df_clean.filter((col(score) 1) (col(score) 5))關于 PySpark 的內存配置最容易被忽視的是 executor memory 和 driver memory 的比例。默認配置下 driver 只管任務調度executor 才是真正執(zhí)行計算的。我測試的機器是 16GB 內存最終配置是spark.executor.memory4g spark.driver.memory2g spark.executor.cores4數(shù)據(jù)量超過 500 萬條后再調整參數(shù)也不遲。關鍵是這兩步——去重和評分范圍過濾——必須放在算法計算之前否則評分矩陣會出現(xiàn)大量噪聲推薦質量會直線下降。3.2 離線推薦結果落庫從 Spark 結果到 MySQL 與 RedisPySpark 計算出的相似度矩陣和推薦結果不能直接給 Django 用需要先落入業(yè)務數(shù)據(jù)庫。我把數(shù)據(jù)分成兩部分相似度矩陣寫入 Redis推薦結果寫入 MySQL。相似度矩陣寫 Redis的理由是Redis 的 Hash 結構天然適合表示 item - [(related_item, score), ...] 這種映射關系。Django Web 服務收到用戶請求時直接從 Redis 里把相似物品撈出來毫秒級響應。我用的存儲結構# Redis Key 設計 item_sim:{book_id} - hash, field: related_book_id, value: similarity_score再配合 TTL 設置過期時間我就是設置的 24 小時保證每天離線計算后自動刷新。推薦結果寫 MySQL是因為它需要和用戶表、圖書表做關聯(lián)查詢。表結構大概是class RecommendResult(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) book models.ForeignKey(Book, on_deletemodels.CASCADE) score models.FloatField() reason models.CharField(max_length255, blankTrue)這樣 Django 視圖層只需要一次簡單的 ORM 查詢就能拿到推薦列表。注意給 user score 建聯(lián)合索引否則用戶量大之后查詢會越來越慢。3.3 Django 推薦接口與冷啟動處理推薦接口的設計遵循一個原則推薦結果要帶上理由。純推薦列表的體驗太干癟告訴用戶“因為你喜歡《三體》所以推薦《球狀閃電》”會明顯提升用戶對推薦結果的信任度。我在推薦接口里返回的數(shù)據(jù)結構是{ user_id: 1001, items: [ { book_id: 128, title: 球狀閃電, cover_url: ..., reason: 因為你對《三體》評了 5 分, score: 4.87 } ] }冷啟動問題怎么處理對于沒有評分記錄的新用戶協(xié)同過濾直接失效。我的策略分兩級一級策略基于內容的推薦根據(jù)用戶注冊時選擇的感興趣分類比如“科幻”“歷史”“懸疑”直接推薦該分類下評分最高的圖書二級策略熱門兜底如果新用戶連分類都沒選就推薦全站評分人數(shù)最多的 20 本書這個策略雖然樸素但在實際部署中能把新用戶推薦結果的點擊率做到和老用戶差不多的水平基本夠用。3.4 實時推薦優(yōu)化相似圖書表 評分配權純離線協(xié)同過濾的問題是用戶剛評分完推薦結果不會馬上變化。我在方案里加了一個輕量級的實時優(yōu)化當用戶對某本書新評分后立刻讀取 Redis 里這本書的相似圖書表把相似度高的圖書臨時插入用戶的推薦列表排在已有推薦前面。這個邏輯用 Django signal 實現(xiàn)簡單幾行代碼from django.db.models.signals import post_save from django.dispatch import receiver from .models import Rating, RecommendResult receiver(post_save, senderRating) def update_recommend(sender, instance, **kwargs): # 實時更新 Redis 中的用戶推薦列表 redis_client.zadd(fuser_realtime:{instance.user_id}, {fbook:{item}: score for item, score in get_similar_books(instance.book_id)})這樣用戶評分后刷新頁面推薦列表里立刻多出幾本相關的書體驗感直接拉滿。4. 圖書可視化大屏的圖表拆解與實現(xiàn)4.1 大屏整體布局哪些圖表值得放上去可視化大屏是整個項目的門面設計時容易踩的坑是“為了放圖而放圖”堆一堆沒信息量的圖表。我做數(shù)據(jù)大屏堅持一個原則每一塊圖表都必須回答一個業(yè)務問題。最終選定了 6 塊核心圖表圖表位置圖表類型回答的業(yè)務問題頂部中央數(shù)字翻牌器平臺總用戶、總圖書數(shù)、總評分量左側上部柱狀圖圖書分類 Top10 分布左側下部餅圖/環(huán)形圖圖書分類占比中間主視覺排行列表熱門圖書 Top10評分人數(shù)最多右側上部折線圖近 30 天評分趨勢右側下部雷達圖用戶活躍度分布布局比例大概遵循 3:4:3 的原則左右兩側放輔助圖表中間主體放核心排行。大屏的視覺重點應該引導用戶第一眼看到核心數(shù)據(jù)不要平均用力。4.2 后端統(tǒng)計接口設計大屏所有數(shù)據(jù)都通過 Django 接口提供。我做了一個統(tǒng)一的/api/dashboard/接口一次請求返回所有圖表需要的 JSON 數(shù)據(jù)避免前端多次請求加長加載時間def dashboard_data(request): data { total_users: User.objects.count(), total_books: Book.objects.count(), total_ratings: Rating.objects.count(), top_categories: get_top_categories(), category_ratio: get_category_ratio(), hot_books: get_hot_books(10), rating_trend: get_rating_trend(30), user_activity: get_user_activity(), } return JsonResponse(data)注意每個圖表對應的接口數(shù)據(jù)量都不大但聚合計算對數(shù)據(jù)庫壓力不小。尤其是熱門圖書 Top10 和評分趨勢需要對幾十萬條評分做 group by。我的做法是提前在 PySpark 里把統(tǒng)計結果算好寫入一個統(tǒng)計結果表Django 接口直接讀表返回。4.3 ECharts 核心配置細節(jié)ECharts 配置里最容易被忽視的是dataZoom和animation的配合。大屏數(shù)據(jù)是實時刷新的如果每次刷新都從第一幀重新播放動畫視覺上會非常亂。解決辦法option { series: [{ type: bar, animationDurationUpdate: 500, animationEasingUpdate: linear }], dataZoom: { type: inside, start: 0, end: 100 } };自動刷新我用的setInterval每 30 秒拉一次/api/dashboard/接口然后調用setOption更新圖表平滑過渡。另一個容易踩的坑是數(shù)據(jù)格式。ECharts 的餅圖需要{name: 科幻, value: 1234}這種格式柱狀圖需要{category: 科幻, count: 1234}。前端拿到后端數(shù)據(jù)后要做一個 transform用Array.map轉成 ECharts 需要的結構在接口里直接調整好格式會省掉不少事。5. 部署聯(lián)調階段最常踩的 5 個坑5.1 PySpark 與 Django 的 Python 環(huán)境沖突這是我整個項目里花時間最多的問題沒有之一。PySpark 需要 Java 環(huán)境而 Django 跑在 Python 虛擬環(huán)境里兩邊如果版本不匹配啟動 Spark 作業(yè)時會直接崩掉。我的排查思路是先看pyspark版本和 Spark 版本是否一致再看 Java 版本是否在 Spark 支持的范圍內。推薦用 conda 建獨立環(huán)境把 Spark 相關的包和 Django 完全隔離。實際操作中我用的是conda create -n bookrec-spark python3.8 conda activate bookrec-spark pip install pyspark3.2.0Django 則單獨在另一個環(huán)境跑兩邊互不干擾。如果你在同一個環(huán)境里同時跑 Django 和 PySpark大概率會遇到java.lang.NoClassDefFoundError這類報錯非常磨人。5.2 Hadoop 中文亂碼與數(shù)據(jù)編碼問題HDFS 上存的中文數(shù)據(jù)經(jīng)常出現(xiàn)亂碼問題。根因是 CSV 文件編碼不統(tǒng)一——有的文件是 UTF-8有的是 GB2312PySpark 讀取時默認 UTF-8遇到 GB2312 就會顯示亂碼。我的通用解決辦法是不要直接用 PySpark 讀取原始文件先用 Python 腳本把原始文件統(tǒng)一轉成 UTF-8 編碼再上傳到 HDFS。別小看這一步它能幫你省掉后面所有環(huán)節(jié)的亂碼排查時間。另外在 PySpark 讀取時顯式指定編碼df spark.read.csv(path, headerTrue, encodingutf-8)5.3 協(xié)同過濾的稀疏矩陣內存問題評分矩陣隨著用戶數(shù)和圖書數(shù)增長會變得非常稀疏。比如 1 萬用戶、1 萬本書評分矩陣就有 1 億個單元格如果換成稠密矩陣存儲光一個 float 數(shù)組就要 800MB 內存。解決辦法是永遠用稀疏矩陣存儲。PySpark 的Rating數(shù)據(jù)結構本身就支持三元組表示即 (user, item, score)存儲的是非零元素和稠密矩陣相比內存開銷縮小幾十倍。另外做相似度計算前先過濾掉評分記錄少于 5 條的用戶和少于 10 條的圖書能把噪聲數(shù)據(jù)去掉的同時減少矩陣規(guī)模。5.4 推薦結果緩存策略為什么 Redis 比直接查庫快這么多最開始我把推薦結果的查詢直接打在 MySQL 上結果用戶量上來之后接口延遲從 80ms 漲到了 1200ms體驗直線下降。后來我把熱門的 Top 20 推薦結果全部緩存到 Redis設置 TTL 為 3 分鐘。用戶請求推薦時先查 Redis命中就直接返回未命中再查 MySQL同時回寫緩存。改動上線后接口延遲穩(wěn)定在 50ms 以內。緩存策略要注意一個問題必須處理緩存穿透。如果某個用戶沒有推薦結果緩存里直接存空值否則每次請求都會穿透到數(shù)據(jù)庫。我就是把空結果也緩存 2 分鐘效果很明顯。5.5 大屏適配與跨域配置最后一個坑是大屏部署時的跨域問題。前端大屏如果單獨跑在另外一個端口調用 Django 接口會遇到 CORS 錯誤。用 Django 的django-cors-headers包配置很簡單# settings.py INSTALLED_APPS [ corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ] CORS_ALLOW_ALL_ORIGINS True # 開發(fā)環(huán)境用生產環(huán)境配置具體域名但是注意CORS_ALLOW_ALL_ORIGINS 只在開發(fā)環(huán)境用。生產環(huán)境下一定要改成具體允許的域名列表否則有跨站請求偽造的安全風險。大屏的響應式適配方面最省事的方式是把大屏當作一個固定分辨率的頁面來做ECharts 用固定寬度容器然后用transform: scale()根據(jù)窗口大小整體縮放不用做復雜的響應式斷點適配。我在實際項目中用了基于rem的字體適配方案配合flex布局的容器效果比固定像素方案明顯更穩(wěn)。6. 一點個人體會整個項目從零到跑通我最深的感受是這類項目的難點從來不在某一個單獨的技術點上而在如何把整條鏈路串起來。HDFS 存數(shù)據(jù)、PySpark 算特征、協(xié)同過濾出推薦、Django 接業(yè)務、ECharts 畫大屏每一層之間有清晰的邊界和依賴關系。做的時候不妨先把每層單獨調通再逐層聯(lián)調問題定位會容易得多。如果你正在做類似的項目我的一個建議是不要一上來就追求算法的復雜度。先把基于物品的協(xié)同過濾跑通把推薦結果的準確率、召回率算出來再考慮加矩陣分解或者深度學習模型也不遲。大部分場景下簡單算法加良好工程落地效果已經(jīng)能超過復雜算法加粗糙部署。最后分享一個小技巧在做推薦效果評估時除了算算法指標一定要留出一些真實用戶做人工體驗測試。算法指標再好用戶覺得“推薦的書不對味”也沒有意義。我后來在推薦列表里加入了“不喜歡”按鈕收集負反饋數(shù)據(jù)回填到評分矩陣這個迭代對推薦體驗的提升比我調了一周的相似度計算參數(shù)還要明顯——方向對了熱愛才會有回報。