同過(guò)濾旅游推薦平臺(tái):ItemCF與余弦相似度從原理到落地)
簡(jiǎn)介一款基于協(xié)同過(guò)濾的在線通用旅游平臺(tái)網(wǎng)站源碼面向Java畢業(yè)設(shè)計(jì)學(xué)生及SSM框架初學(xué)者提供可直接運(yùn)行的前后端完整項(xiàng)目。項(xiàng)目集成MySQL數(shù)據(jù)庫(kù)按模塊實(shí)現(xiàn)了景點(diǎn)推薦管理、精選路線管理、用戶信息管理、系統(tǒng)公告與留言管理等主要功能其中景點(diǎn)信息可進(jìn)行增刪改查精選路線可針對(duì)游客需求靈活配置能夠直觀展示協(xié)同過(guò)濾推薦算法在旅游平臺(tái)中的落地過(guò)程。資源共1288個(gè)文件主要包括java源碼、jsp頁(yè)面、xml配置、jar依賴包、js/css前端腳本以及sql數(shù)據(jù)庫(kù)文件等壓縮包約52.7MB目錄結(jié)構(gòu)完整便于按模塊閱讀、調(diào)試和二次開(kāi)發(fā)。目前已有58人學(xué)習(xí)使用適合需要快速搭建旅游推薦系統(tǒng)原型、完成畢業(yè)設(shè)計(jì)或提升SSM項(xiàng)目實(shí)戰(zhàn)能力的開(kāi)發(fā)者參考。 做 java 畢業(yè)設(shè)計(jì)的人最怕的不是寫不出來(lái)而是打開(kāi)一套源碼發(fā)現(xiàn)能跑但講不清里面的推薦是怎么算出來(lái)的?;趨f(xié)同過(guò)濾的在線通用旅游平臺(tái)這套 SSM 畢設(shè)恰好把三類東西壓在一起SSMSpring SpringMVC MyBatis做業(yè)務(wù)、協(xié)同過(guò)濾做推薦、前端頁(yè)面做呈現(xiàn)?!巴ㄓ谩币馕吨唤壎骋粋€(gè)景區(qū)而是把不同城市的景點(diǎn)、路線、餐飲當(dāng)成可推薦的物品用用戶的歷史行為去預(yù)測(cè)下一步想去哪里。它適合三類人準(zhǔn)備 java 課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的學(xué)生、剛學(xué) SSM 整合的初級(jí)工程師以及想看看協(xié)同過(guò)濾在真實(shí)業(yè)務(wù)表上怎么落地的 java 開(kāi)發(fā)。這套源碼不是給你抄完就交的而是讓你有一個(gè)能講清楚的骨架。2. 算法選型與評(píng)分矩陣構(gòu)造旅游場(chǎng)景為什么先做 ItemCF 而不是 UserCF協(xié)同過(guò)濾不是一套算法而是兩類思路。UserCF 先找“和我興趣相似的人”再把這些相似的人喜歡的景點(diǎn)推給我ItemCF 則先找“和我看過(guò)的景點(diǎn)相似的景點(diǎn)”直接推相似內(nèi)容。選哪一邊取決于平臺(tái)里用戶和物品的數(shù)量關(guān)系以及反饋數(shù)據(jù)的稀疏程度而不是哪個(gè)名字聽(tīng)著更高級(jí)。2.1 UserCF 與 ItemCF 怎么選一張表看完適用邊界旅游平臺(tái)的特點(diǎn)是景點(diǎn)數(shù)量遠(yuǎn)小于用戶數(shù)量且大多數(shù)用戶只去過(guò)幾個(gè)地方評(píng)分記錄非常稀疏。UserCF 要維護(hù)用戶間的相似度矩陣用戶一多、行為一稀疏矩陣?yán)锎罅渴恰皼](méi)交集”的用戶對(duì)算出來(lái)的相似度可信度很低ItemCF 只需要算景點(diǎn)之間的相似度景點(diǎn)數(shù)量小一個(gè)量級(jí)計(jì)算量可控而且“去過(guò)杭州的人也會(huì)去蘇州”這類解釋在旅游場(chǎng)景里比“和你相似的人喜歡這里”更直觀。維度UserCFItemCF最小計(jì)算單元用戶-用戶相似度物品-物品相似度適合規(guī)模用戶少、物品多、行為稠密用戶多、物品少、行為稀疏實(shí)時(shí)性用戶行為變化需重算相似用戶物品相似度可離線定時(shí)算可解釋性“和你興趣相似的張三也去過(guò)”“你看過(guò)西湖也可能會(huì)喜歡黿頭渚”旅游場(chǎng)景匹配度一般冷啟動(dòng)嚴(yán)重高推薦結(jié)果更穩(wěn)實(shí)際寫這套畢設(shè)時(shí)我一般把相似度計(jì)算放到一個(gè)單獨(dú)的 Service 里白天跑業(yè)務(wù)、凌晨定時(shí)算一次物品相似度緩存到 Redis 或本地內(nèi)存。答辯時(shí)這是加分點(diǎn)你不僅會(huì)調(diào)包還知道相似度要離線算不能每次請(qǐng)求都現(xiàn)算。2.2 評(píng)分從哪來(lái)把瀏覽、收藏、評(píng)論折算成統(tǒng)一評(píng)分協(xié)同過(guò)濾的輸入是一個(gè)用戶-物品評(píng)分矩陣但旅游網(wǎng)站通常沒(méi)有“打分”這個(gè)強(qiáng)反饋入口。常見(jiàn)做法是把三類隱式行為折算成分?jǐn)?shù)用戶瀏覽某個(gè)景點(diǎn)詳情記 1 分收藏記 3 分發(fā)表評(píng)論按評(píng)論星級(jí)記 1 到 5 分。把這些統(tǒng)一寫入 rating 表字段帶上行為類型和創(chuàng)建時(shí)間方便后面做時(shí)間衰減和調(diào)試。CREATE TABLE travel_rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, scenic_id INT NOT NULL, behavior_type TINYINT NOT NULL COMMENT 1-瀏覽 2-收藏 3-評(píng)論, score DECIMAL(2,1) NOT NULL DEFAULT 1.0 COMMENT 折算后的分?jǐn)?shù), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_scenic (user_id, scenic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這表里的score不是讓前端傳的而是在 Service 層根據(jù)behavior_type折算后寫入。例如瀏覽行為寫入 1.0收藏寫入 3.0評(píng)論把 star 字段直接透?jìng)鳌V苑珠_(kāi)存behavior_type和score是為了調(diào)參時(shí)不改前端業(yè)務(wù)你覺(jué)得“瀏覽”不該有分把 Service 里那個(gè)常量從 1.0 改成 0 就行歷史數(shù)據(jù)不用回刷。2.3 余弦相似度計(jì)算一段可以直接用的 Java 方法評(píng)分矩陣構(gòu)造好以后下一步是算相似度。余弦相似度把每個(gè)景點(diǎn)看成一個(gè)向量向量維度是評(píng)過(guò)分的用戶集合兩個(gè)景點(diǎn)相似度就是這兩個(gè)向量的夾角余弦值。一段能直接放進(jìn)工具類的實(shí)現(xiàn)如下public class CosineSimilarity { /** * 計(jì)算兩個(gè)物品的余弦相似度 * param item1Ratings keyuserId value用戶對(duì)item1的評(píng)分 * param item2Ratings keyuserId value用戶對(duì)item2的評(píng)分 */ public static double calculate(MapInteger, Double item1Ratings, MapInteger, Double item2Ratings) { // 取兩個(gè)物品共同被評(píng)分過(guò)的用戶集合 SetInteger commonUsers new HashSet(item1Ratings.keySet()); commonUsers.retainAll(item2Ratings.keySet()); if (commonUsers.size() 2) { return 0.0; // 共同評(píng)分人數(shù)太少相似度直接判0 } double dotProduct 0.0; double norm1 0.0; double norm2 0.0; for (Integer userId : commonUsers) { double r1 item1Ratings.get(userId); double r2 item2Ratings.get(userId); dotProduct r1 * r2; norm1 r1 * r1; norm2 r2 * r2; } if (norm1 0.0 || norm2 0.0) { return 0.0; } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); } }這里有個(gè)容易被忽略的參數(shù)commonUsers.size() 2時(shí)直接返回 0。很多教程不寫這個(gè)判斷導(dǎo)致只有一兩個(gè)共同評(píng)分的景點(diǎn)對(duì)算出 1.0 的高相似度推薦列表里全是“只有一個(gè)用戶碰巧同時(shí)看過(guò)”的噪聲。代碼里的retainAll是求兩個(gè)評(píng)分 Map 的交集復(fù)雜度 O(min(n, m))先取共同用戶再算點(diǎn)積避免把整個(gè)矩陣的零值都遍歷一遍。3. SSM 代碼落地從數(shù)據(jù)庫(kù)表到旅游推薦接口的完整鏈路算法能跑通和項(xiàng)目能跑通是兩回事。這一章把 SSM 的骨架按“數(shù)據(jù)表 → Spring 配置 → 推薦 Service”的順序拆開(kāi)每部分都是可以直接往自己項(xiàng)目里搬的寫法。3.1 表結(jié)構(gòu)設(shè)計(jì)用戶、景點(diǎn)與行為評(píng)分三張核心表除了上一章的 rating 表還需要用戶表和景點(diǎn)表。用戶表存基礎(chǔ)登錄信息景點(diǎn)表存景點(diǎn)名稱、城市、分類、封面圖 URL、描述和熱度。熱度字段很關(guān)鍵冷啟動(dòng)時(shí)期評(píng)分?jǐn)?shù)據(jù)不足推薦模塊可以直接按熱度字段降序輸出默認(rèn)榜。CREATE TABLE travel_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), city VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE travel_scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, category VARCHAR(30) NOT NULL COMMENT 自然風(fēng)光/人文古跡/主題樂(lè)園等, cover_url VARCHAR(255), description TEXT, heat INT DEFAULT 0 COMMENT 熱度用于冷啟動(dòng)兜底推薦, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;設(shè)計(jì)要點(diǎn)在travel_scenic.city和category兩個(gè)字段。城市維度用于推薦結(jié)果的多樣性控制——相似度計(jì)算時(shí)可以只取同城市或相鄰城市的景點(diǎn)減少跨地區(qū)推薦造成的“不相關(guān)感”分類字段則是給 ItemCF 做物品過(guò)濾的候選集。如果兩個(gè)景點(diǎn)一個(gè)在杭州一個(gè)在新疆評(píng)分交集少相似度天然低不需要額外處理。3.2 SSM 三件套配置Spring、SpringMVC、MyBatis 的啟動(dòng)順序SSM 整合最容易被絆倒的地方是配置文件各管各的Spring 管 Service 和 DAOSpringMVC 管 ControllerMyBatis 的 Mapper 還要聲明掃描路徑。我習(xí)慣把配置拆成三個(gè)文件職責(zé)邊界清楚出問(wèn)題也好定位。!-- applicationContext.xml數(shù)據(jù)源 SqlSessionFactory Mapper 掃描 -- context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property nametestWhileIdle valuetrue/ property namevalidationQuery valueSELECT 1/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.example.travel.entity/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean mybatis:scan base-packagecom.example.travel.dao/這段配置里最容易漏的是mapUnderscoreToCamelCase。數(shù)據(jù)庫(kù)字段user_name要自動(dòng)映射成 Java 屬性u(píng)serName必須把這個(gè)開(kāi)關(guān)打開(kāi)否則 MyBatis 查出實(shí)體后 nickname、createTime 全是 null。mybatis:scan標(biāo)簽負(fù)責(zé)掃 DAO 接口沒(méi)有它 Controller 注入 Mapper 時(shí)會(huì)在啟動(dòng)階段直接報(bào)“No qualifying bean of type”。SpringMVC 部分要單獨(dú)配一個(gè) spring-mvc.xml重點(diǎn)處理靜態(tài)資源和注解驅(qū)動(dòng)!-- spring-mvc.xml只掃 Controller 層 -- context:component-scan base-packagecom.example.travel.controller/ mvc:annotation-driven/ mvc:resources mapping/static/** location/static//mvc:resources是很多畢設(shè)源碼翻車的重災(zāi)區(qū)。web.xml里把 DispatcherServlet 映射到/之后所有請(qǐng)求都會(huì)先進(jìn) SpringMVCHTML、CSS、JS 如果不做靜態(tài)資源放行頁(yè)面打開(kāi)時(shí)只剩赤裸裸的 HTML 結(jié)構(gòu)樣式和腳本全部 404。你要做的不是把映射改成*.do而是把 static 目錄放行給默認(rèn) Servlet。3.3 推薦服務(wù)實(shí)現(xiàn)Mapper 查數(shù)加內(nèi)存計(jì)算不引額外中間件這個(gè)畢設(shè)規(guī)模在幾百個(gè)用戶、幾百個(gè)景點(diǎn)量級(jí)完全不需要引入 Hadoop、Spark 之類的東西。推薦 Service 的做法是一次查出所有評(píng)分記錄在內(nèi)存里組裝成MapscenicId, MapuserId, score然后算相似度、取 TopK。Service public class RecommendService { Autowired private RatingMapper ratingMapper; Autowired private ScenicMapper scenicMapper; public ListScenic recommend(Integer userId, int topK) { // 1. 查出全部行為評(píng)分組裝成物品-用戶評(píng)分的倒排結(jié)構(gòu) ListRating allRatings ratingMapper.selectAll(); MapInteger, MapInteger, Double itemRatingsMap new HashMap(); MapInteger, MapInteger, Double userRatingsMap new HashMap(); for (Rating r : allRatings) { itemRatingsMap .computeIfAbsent(r.getScenicId(), k - new HashMap()) .put(r.getUserId(), r.getScore()); userRatingsMap .computeIfAbsent(r.getUserId(), k - new HashMap()) .put(r.getScenicId(), r.getScore()); } // 2. 找出當(dāng)前用戶評(píng)過(guò)分的景點(diǎn)列表 MapInteger, Double myRatings userRatingsMap .getOrDefault(userId, Collections.emptyMap()); // 3. 候選集用戶沒(méi)去過(guò)的景點(diǎn) MapInteger, Double scores new HashMap(); for (Integer itemId : itemRatingsMap.keySet()) { if (myRatings.containsKey(itemId)) { continue; // 去過(guò)的景點(diǎn)不重復(fù)推薦 } double totalScore 0.0; for (Integer ratedItemId : myRatings.keySet()) { double sim CosineSimilarity.calculate( itemRatingsMap.get(itemId), itemRatingsMap.get(ratedItemId)); totalScore sim * myRatings.get(ratedItemId); } scores.put(itemId, totalScore); } // 4. 按預(yù)測(cè)分?jǐn)?shù)降序取前 topK 個(gè)景點(diǎn)返回 return scores.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topK) .map(entry - scenicMapper.selectById(entry.getKey())) .collect(Collectors.toList()); } }這段代碼的查詢邏輯是當(dāng)前用戶評(píng)分過(guò)的每一個(gè)景點(diǎn)都和候選景點(diǎn)算相似度再用“用戶對(duì)該景點(diǎn)的評(píng)分”作為權(quán)重累加。這個(gè)加權(quán)累加就是 ItemCF 的預(yù)測(cè)分公式。參數(shù)topK控制最終返回幾個(gè)推薦在 Controller 層我一般固定傳 10。如果候選集為空用戶什么都沒(méi)看過(guò)Service 里要兜底返回scenicMapper.selectByHeatLimit(10)熱度榜就是冷啟動(dòng)階段的兜底方案。4. SSM 旅游平臺(tái)部署避坑五個(gè)常見(jiàn)運(yùn)行期問(wèn)題與排查過(guò)程這套源碼的價(jià)值在“能跑”但 SSM 項(xiàng)目跑起來(lái)的過(guò)程基本上就是和配置、依賴、序列化搏斗的過(guò)程。下面五條都是實(shí)際跑項(xiàng)目時(shí)反復(fù)出現(xiàn)的坑每條按現(xiàn)象、原因、解決的順序?qū)憽?.1 頁(yè)面樣式全部丟失或 JS 不執(zhí)行現(xiàn)象Tomcat 啟動(dòng)成功首頁(yè) HTML 能打開(kāi)但所有 CSS、JS、圖片都 404頁(yè)面裸奔。原因DispatcherServlet 的 url-pattern 配置成/把所有請(qǐng)求都攔進(jìn)了 SpringMVC靜態(tài)資源沒(méi)有放行。如果你在 JSP 里寫的是相對(duì)路徑css/style.css還要注意頁(yè)面請(qǐng)求路徑的層級(jí)否則路徑拼接后也會(huì) 404。解決在 spring-mvc.xml 里加mvc:resources mapping/static/** location/static//并確保前端資源放在webapp/static/下。如果你的頁(yè)面用了link hrefcss/style.css最好改成${pageContext.request.contextPath}/static/css/style.css防止在二級(jí)路由下路徑錯(cuò)亂。4.2 Jackson 序列化堆棧溢出或懶加載異?,F(xiàn)象用 AJAX 請(qǐng)求一個(gè)返回景點(diǎn)的接口瀏覽器直接 500后臺(tái)日志出現(xiàn)StackOverflowError或could not initialize proxy - no Session。原因?qū)嶓w類之間有雙向關(guān)聯(lián)。景點(diǎn)實(shí)體里放了評(píng)論列表評(píng)論實(shí)體里又引用了用戶用戶再引用評(píng)論Jackson 序列化時(shí)無(wú)限遞歸。懶加載導(dǎo)致的 no Session 是另一個(gè)變種事務(wù)在 Service 層關(guān)閉后Controller 才去序列化未加載的關(guān)聯(lián)對(duì)象。解決在實(shí)體類的多的一側(cè)加JsonIgnore或者在返回對(duì)象上單獨(dú)建 VO只序列化需要展示的字段。我一般偏向建 VO不用動(dòng)實(shí)體類結(jié)構(gòu)也順便把密碼等敏感字段擋在外面。懶加載問(wèn)題就調(diào)整事務(wù)邊界把查詢?cè)u(píng)論的方法挪到事務(wù)內(nèi)或者用EntityGraph這類方案主動(dòng)抓取關(guān)聯(lián)數(shù)據(jù)。4.3 協(xié)同過(guò)濾算相似度時(shí)內(nèi)存溢出現(xiàn)象部署后第一次觸發(fā)“為你推薦”接口日志出現(xiàn)java.lang.OutOfMemoryError: Java heap space重啟后過(guò)幾天又出現(xiàn)。原因相似度計(jì)算是雙重循環(huán)物品數(shù) M、評(píng)分?jǐn)?shù) N 的規(guī)模下復(fù)雜度接近 O(M2·N)。數(shù)據(jù)量上去之后每一對(duì)物品都要建 HashMap、算交集堆內(nèi)存很快撐滿。解決兩個(gè)方向。一是先過(guò)濾無(wú)行為物品只對(duì)至少有 2 個(gè)人評(píng)過(guò)分的景點(diǎn)做候選二是把相似度計(jì)算改成離線任務(wù)用 Spring 的Scheduled每天凌晨算一次并緩存結(jié)果請(qǐng)求時(shí)只查緩存。畢設(shè)答辯時(shí)能講到這兩點(diǎn)比貼一段“調(diào)大 -Xmx”更有說(shuō)服力。4.4 MyBatis 查詢結(jié)果全是 null 或者日期字段異常現(xiàn)象景點(diǎn)列表接口返回 200但每條數(shù)據(jù)的 city、description 全是 null只有 id 有值。原因數(shù)據(jù)庫(kù)字段是下劃線風(fēng)格cover_url、create_time實(shí)體類是駝峰風(fēng)格coverUrl、createTimeMyBatis 默認(rèn)不自動(dòng)映射下劃線到駝峰不匹配的字段全部留空。解決在 sqlSessionFactory 配置里開(kāi)啟mapUnderscoreToCamelCase已經(jīng)在前一章配置代碼里給過(guò)。如果你不想動(dòng)全局配置也可以在 Mapper XML 里給每個(gè)字段寫 resultMap 顯式映射。日期字段出現(xiàn)格式問(wèn)題的檢查一下數(shù)據(jù)庫(kù)連接 URL 有沒(méi)有加serverTimezoneAsia/Shanghai不加會(huì)在解析 DATETIME 時(shí)直接報(bào)錯(cuò)。4.5 隔夜或一段時(shí)間后數(shù)據(jù)庫(kù)連接失效現(xiàn)象項(xiàng)目剛啟動(dòng)時(shí)一切正常第二天第一次請(qǐng)求報(bào)Communications link failure刷新一下又好了。原因MySQL 的wait_timeout默認(rèn) 8 小時(shí)空閑超過(guò)這個(gè)時(shí)間的連接會(huì)被服務(wù)端斷開(kāi)。Druid 連接池默認(rèn)不主動(dòng)探測(cè)連接池里還躺著那些失效連接第一次請(qǐng)求把它拿出去用自然報(bào)錯(cuò)。解決在 Druid 配置里加testWhileIdletrue、validationQuerySELECT 1并把timeBetweenEvictionRunsMillis設(shè)成 60000讓連接池每分鐘檢測(cè)一次空閑連接。這里有一個(gè)血淚經(jīng)驗(yàn)testOnBorrow不要開(kāi)加上之后每次請(qǐng)求都多一次 ping高并發(fā)下反而拖慢接口。5. 驗(yàn)證推薦效果用離線評(píng)測(cè)與參數(shù)調(diào)優(yōu)把推薦做“看得見(jiàn)”推薦算法不是跑通就結(jié)束而是要回答“推得準(zhǔn)不準(zhǔn)”。最直接的辦法是拿歷史評(píng)分做離線切分把每個(gè)用戶的評(píng)分按時(shí)間分成訓(xùn)練集和測(cè)試集用訓(xùn)練集學(xué)相似度再預(yù)測(cè)測(cè)試集里的景點(diǎn)最后算召回率。5.1 用離線數(shù)據(jù)集估算準(zhǔn)確率與召回率先寫一條 SQL 把每個(gè)用戶最后一條評(píng)分當(dāng)作測(cè)試集其余當(dāng)訓(xùn)練集然后用前面的 RecommendService 給測(cè)試用戶生成 Top10 推薦再檢查測(cè)試集景點(diǎn)是否出現(xiàn)在推薦列表里。import pymysql conn pymysql.connect(hostlocalhost, userroot, password123456, databasetravel_db, charsetutf8mb4) cur conn.cursor() cur.execute( SELECT r1.user_id, r1.scenic_id FROM travel_rating r1 JOIN ( SELECT user_id, MAX(create_time) AS max_time FROM travel_rating GROUP BY user_id ) r2 ON r1.user_id r2.user_id AND r1.create_time r2.max_time ) test_pairs cur.fetchall() # 推薦列表從 RecommendService 導(dǎo)出后再做命中判斷 hit sum(1 for user_id, scenic_id in test_pairs if scenic_id in recommend_map.get(user_id, [])) recall hit / len(test_pairs) print(f召回率: {recall:.2f})這段腳本把“每個(gè)用戶的最近一次行為”當(dāng)標(biāo)準(zhǔn)答案命中率就是召回率。邏輯說(shuō)明MAX(create_time)找最近行為如果推薦列表里有這個(gè)景點(diǎn)說(shuō)明算法命中實(shí)際跑下來(lái)召回率在 0.2 到 0.4 都是正常范圍畢竟旅游低頻、行為稀疏不要指望推得全中。5.2 兩個(gè)值得先調(diào)的參數(shù)相似度閾值與鄰居數(shù) K參數(shù)范圍影響調(diào)整策略相似度閾值0.2 ~ 0.4太低則噪聲多太高則候選集為空先固定在 0.3看召回率再微調(diào)鄰居數(shù) K10 ~ 30K 太小結(jié)果隨機(jī)太大熱門效應(yīng)明顯數(shù)據(jù)少時(shí)用 10數(shù)據(jù)多了調(diào)到 20 到 30兜底熱度榜條數(shù)5 ~ 10緩解冷啟動(dòng)無(wú)行為用戶直接返回?zé)岫劝裾{(diào)參的次序先調(diào)相似度閾值再調(diào) K。一開(kāi)始我把閾值寫成 0.1推薦出來(lái)的全是只有一個(gè)人評(píng)過(guò)分的冷門景點(diǎn)用戶完全沒(méi)聽(tīng)過(guò)改成 0.3 之后候選集干凈了很多。后來(lái)我發(fā)現(xiàn)一個(gè)更穩(wěn)的做法先給每個(gè)景點(diǎn)按城市和分類過(guò)濾只對(duì)同城或同類景點(diǎn)算相似度召回率比單純調(diào)閾值提升更明顯。這里我踩過(guò)最大的坑是把閾值調(diào)太高導(dǎo)致推薦列表經(jīng)常不足 10 個(gè)后來(lái)加了兜底邏輯不足時(shí)用熱度榜補(bǔ)齊。這個(gè)項(xiàng)目給我最大的教訓(xùn)是協(xié)同過(guò)濾的代碼只有幾十行難的是讓數(shù)據(jù)、參數(shù)、兜底策略匹配業(yè)務(wù)。面試時(shí)被問(wèn)到協(xié)同過(guò)濾不要上來(lái)背公式先從“評(píng)分?jǐn)?shù)據(jù)怎么構(gòu)造”講起再講“相似度用什么算、閾值怎么定、冷啟動(dòng)怎么兜底”這幾層講完面試官基本就認(rèn)可你是真寫過(guò)而不是背過(guò)八股。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取