站用戶性別預(yù)測(cè):從日志ETL到模型融合實(shí)戰(zhàn))
簡介這份資源是一套基于KNN算法與Hadoop平臺(tái)的電影網(wǎng)站用戶性別預(yù)測(cè)實(shí)現(xiàn)程序面向具備一定Java與大數(shù)據(jù)基礎(chǔ)、希望實(shí)踐分類算法與分布式計(jì)算的學(xué)習(xí)者。項(xiàng)目圍繞MovieLens用戶評(píng)分?jǐn)?shù)據(jù)展開涵蓋數(shù)據(jù)預(yù)處理、KNN建模及模型評(píng)價(jià)等環(huán)節(jié)可用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或算法練手場景。壓縮包共78個(gè)文件約5.86MB包含28個(gè)java源碼、30個(gè)class編譯文件、5個(gè)jar包以及dat數(shù)據(jù)文件、properties配置與readme說明分別對(duì)應(yīng)預(yù)處理、KNN計(jì)算與評(píng)價(jià)模塊結(jié)構(gòu)完整便于分階段運(yùn)行。目前已有3037人學(xué)習(xí)下載。讀者可據(jù)此掌握Hadoop環(huán)境下數(shù)據(jù)預(yù)處理打包運(yùn)行、KNN分類建模與模型評(píng)估的完整流程并參考作者兩小時(shí)運(yùn)行經(jīng)驗(yàn)合理規(guī)劃計(jì)算資源快速復(fù)現(xiàn)并理解大規(guī)模數(shù)據(jù)下的性別預(yù)測(cè)實(shí)現(xiàn)思路。1. 從電影網(wǎng)站日志到性別標(biāo)簽這條鏈路到底在算什么電影網(wǎng)站后臺(tái)每天沉淀大量行為日志誰在幾點(diǎn)看了哪部片、打了多少分、停留多久、搜索了什么關(guān)鍵詞。把這些日志丟進(jìn) Hadoop 做離線批處理再訓(xùn)練一個(gè)分類模型去推斷用戶性別就是標(biāo)題里說的「基于 Hadoop 的電影網(wǎng)站用戶性別預(yù)測(cè)」。它解決的不是「猜得準(zhǔn)不準(zhǔn)」這一個(gè)問題而是當(dāng)站點(diǎn)日活上到幾十萬、日志按 TB 漲的時(shí)候單機(jī) pandas 已經(jīng)跑不動(dòng)必須把特征工程和模型訓(xùn)練拆到 HDFS 和 YARN 上。適合誰適合手里有站內(nèi)行為日志、想用 Hadoop 生態(tài)做用戶畫像的工程師也適合正在做 Hadoop 課程設(shè)計(jì)、需要一條能跑通的完整鏈路的人。核心難點(diǎn)不在算法而在日志清洗、特征拼接和分布式任務(wù)調(diào)度的銜接。2. 為什么性別預(yù)測(cè)要掛在 Hadoop 上數(shù)據(jù)規(guī)模與特征選型2.1 單機(jī)跑不動(dòng)的那條分界線在哪先算一筆賬。一個(gè)中等規(guī)模電影站日活 20 萬人均產(chǎn)生 30 條行為事件瀏覽、評(píng)分、搜索、收藏一天就是 600 萬條。每條事件帶上用戶 ID、影片 ID、時(shí)間戳、事件類型、評(píng)分值、停留秒數(shù)序列化后約 200 字節(jié)一天 1.2 GB。看起來不大但性別預(yù)測(cè)不是拿一天數(shù)據(jù)就能訓(xùn)的通常要取 90 天做窗口那就是 100 GB 量級(jí)。再疊加用戶側(cè)的基礎(chǔ)屬性、影片側(cè)的元數(shù)據(jù)類型、年代、導(dǎo)演做 join 之后中間結(jié)果會(huì)膨脹到原始數(shù)據(jù)的 3 到 5 倍。單機(jī) pandas 處理 100 GB 的 join內(nèi)存直接爆就算用分塊讀取特征拼接的 shuffle 也會(huì)把磁盤 IO 打滿。這時(shí)候 Hadoop 的價(jià)值就出來了HDFS 負(fù)責(zé)把日志按塊分散存儲(chǔ)MapReduce 或 Spark on YARN 負(fù)責(zé)把 join 和聚合拆到多個(gè)節(jié)點(diǎn)并行做。我一般會(huì)跟人說數(shù)據(jù)量沒到單機(jī)內(nèi)存的 3 倍以上別急著上 Hadoop但一旦過了這條線Hadoop 就是剛需。2.2 性別預(yù)測(cè)的特征到底取哪些性別預(yù)測(cè)本質(zhì)是一個(gè)二分類問題但特征設(shè)計(jì)決定了天花板。從電影網(wǎng)站日志里能抽出的有效特征分四類特征類別具體字段為什么對(duì)性別有區(qū)分度影片類型偏好用戶觀看影片的類型分布動(dòng)作/愛情/科幻/文藝統(tǒng)計(jì)上男性更偏動(dòng)作科幻女性更偏愛情文藝但這不是絕對(duì)只是概率傾向評(píng)分行為平均評(píng)分、評(píng)分方差、高分影片占比男性評(píng)分往往更極端打 1 分或 5 分女性評(píng)分更集中在中高段活躍時(shí)段按小時(shí)統(tǒng)計(jì)的活躍分布晚間和周末的活躍模式存在性別差異搜索詞搜索關(guān)鍵詞的 TF-IDF 向量搜索「復(fù)仇者聯(lián)盟」和搜索「怦然心動(dòng)」的人群分布不同注意這里說的都是統(tǒng)計(jì)傾向不是刻板印象。模型學(xué)的是數(shù)據(jù)里的相關(guān)性不是因果。實(shí)際做的時(shí)候我會(huì)把用戶 ID 做哈希脫敏影片 ID 做 embedding 或者 one-hot時(shí)間戳拆成小時(shí)和星期兩個(gè)離散特征。2.3 為什么選 Hadoop 而不是直接上 Spark有人會(huì)問既然要做機(jī)器學(xué)習(xí)為什么不直接上 Spark MLlib答案是可以而且常見做法就是 Spark on YARN。但標(biāo)題里寫的是 Hadoop說明底層存儲(chǔ)和資源調(diào)度還是 HDFS YARN。實(shí)際鏈路通常是原始日志落 HDFS用 MapReduce 或 Spark 做 ETL 清洗特征寬表存 Hive 或 Parquet最后用 Spark MLlib 或單機(jī) sklearn 在采樣后的特征上訓(xùn)練。Hadoop 在這里的角色是數(shù)據(jù)底座和批處理引擎不是模型本身。提示如果數(shù)據(jù)量在 10 GB 以內(nèi)單機(jī) sklearn pandas 完全夠用硬上 Hadoop 只會(huì)增加運(yùn)維成本。選型要看數(shù)據(jù)規(guī)模不要為了用而用。3. 從原始日志到特征寬表Hadoop 上的 ETL 怎么寫3.1 日志格式約定與清洗規(guī)則假設(shè)電影網(wǎng)站的行為日志是 JSON 行格式每行一條事件。典型字段包括 user_id、movie_id、event_typeview/rate/search/favorite、event_time、rating、duration、search_keyword。清洗要做四件事去掉 user_id 為空的臟數(shù)據(jù)、把 event_time 統(tǒng)一轉(zhuǎn)成 yyyy-MM-dd HH:mm:ss、過濾掉 duration 小于 1 秒的誤觸記錄、把 rating 不在 1 到 5 之間的異常值置空。下面是一個(gè)用 Hadoop Streaming 做清洗的 Python mapper 示例#!/usr/bin/env python # mapper.py - 清洗電影網(wǎng)站行為日志 import sys import json from datetime import datetime for line in sys.stdin: line line.strip() if not line: continue try: obj json.loads(line) except ValueError: continue # 跳過解析失敗的臟行 user_id obj.get(user_id) if not user_id: continue # 用戶ID為空無法歸屬直接丟棄 event_time obj.get(event_time, ) try: # 統(tǒng)一時(shí)間格式兼容時(shí)間戳和字符串兩種輸入 if event_time.isdigit(): dt datetime.fromtimestamp(int(event_time)) else: dt datetime.strptime(event_time, %Y-%m-%d %H:%M:%S) event_time dt.strftime(%Y-%m-%d %H:%M:%S) except Exception: continue duration obj.get(duration, 0) if duration is not None and int(duration) 1: continue # 停留小于1秒視為誤觸 rating obj.get(rating) if rating is not None: try: rating float(rating) if rating 1 or rating 5: rating None # 異常評(píng)分置空 except ValueError: rating None # 輸出清洗后的記錄用制表符分隔 print(\t.join([ str(user_id), str(obj.get(movie_id, )), str(obj.get(event_type, )), event_time, str(rating) if rating is not None else , str(duration), str(obj.get(search_keyword, )) ]))邏輯說明mapper 逐行讀取標(biāo)準(zhǔn)輸入用 json.loads 解析任何解析失敗或字段缺失的行直接跳過保證下游不會(huì)因?yàn)榕K數(shù)據(jù)翻車。時(shí)間字段做了雙格式兼容因?yàn)閷?shí)際日志里經(jīng)常混著時(shí)間戳和格式化字符串。rating 異常值置空而不是丟棄整行是因?yàn)樵u(píng)分只是特征之一丟掉整條記錄會(huì)損失其他字段的信息。參數(shù)說明duration 閾值設(shè)為 1 秒這個(gè)值可以根據(jù)業(yè)務(wù)調(diào)整短視頻場景可能要設(shè) 3 秒。rating 范圍鎖定 1 到 5如果站點(diǎn)用的是 10 分制這里要改成 1 到 10。3.2 用 Hive 做用戶-影片特征聚合清洗后的數(shù)據(jù)落到 HDFS 的 /user/hadoop/movie_clean 目錄接下來用 Hive 建外部表做聚合。核心思路是按 user_id 分組把每個(gè)用戶的行為聚成一行特征向量。-- 建外部表指向清洗后的數(shù)據(jù) CREATE EXTERNAL TABLE IF NOT EXISTS movie_behavior ( user_id STRING, movie_id STRING, event_type STRING, event_time STRING, rating DOUBLE, duration INT, search_keyword STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS TEXTFILE LOCATION /user/hadoop/movie_clean; -- 用戶側(cè)聚合特征 CREATE TABLE user_features AS SELECT user_id, COUNT(*) AS total_events, SUM(CASE WHEN event_type view THEN 1 ELSE 0 END) AS view_cnt, SUM(CASE WHEN event_type rate THEN 1 ELSE 0 END) AS rate_cnt, SUM(CASE WHEN event_type search THEN 1 ELSE 0 END) AS search_cnt, AVG(rating) AS avg_rating, STDDEV(rating) AS std_rating, AVG(duration) AS avg_duration, -- 晚間活躍占比18點(diǎn)到23點(diǎn) SUM(CASE WHEN HOUR(event_time) BETWEEN 18 AND 23 THEN 1 ELSE 0 END) / COUNT(*) AS night_ratio, -- 周末活躍占比 SUM(CASE WHEN DAYOFWEEK(event_time) IN (1,7) THEN 1 ELSE 0 END) / COUNT(*) AS weekend_ratio FROM movie_behavior GROUP BY user_id;邏輯說明這段 HiveQL 把原始事件按用戶聚合生成每個(gè)用戶的統(tǒng)計(jì)特征。total_events 反映活躍度view_cnt 和 rate_cnt 反映行為偏好avg_rating 和 std_rating 捕捉評(píng)分習(xí)慣night_ratio 和 weekend_ratio 刻畫時(shí)間模式。這些特征后面會(huì)和影片類型偏好拼接成完整特征向量。參數(shù)說明晚間時(shí)段定義為 18 到 23 點(diǎn)這個(gè)窗口可以根據(jù)站點(diǎn)用戶的實(shí)際活躍曲線調(diào)整。周末用 DAYOFWEEK 判斷1 是周日7 是周六不同 Hive 版本可能有差異跑之前先用 SELECT DAYOFWEEK(2024-01-01) 驗(yàn)證一下。3.3 影片類型偏好特征的拼接用戶看過的影片類型分布是強(qiáng)特征。做法是先把影片元數(shù)據(jù)movie_id 到 genre 的映射加載成一張維度表然后和用戶行為表 join再按用戶聚合出各類型的觀看占比。-- 影片類型維度表 CREATE TABLE movie_genre ( movie_id STRING, genre STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t; -- 用戶類型偏好 CREATE TABLE user_genre_pref AS SELECT b.user_id, g.genre, COUNT(*) AS genre_cnt, COUNT(*) / SUM(COUNT(*)) OVER (PARTITION BY b.user_id) AS genre_ratio FROM movie_behavior b JOIN movie_genre g ON b.movie_id g.movie_id WHERE b.event_type IN (view, rate) GROUP BY b.user_id, g.genre;邏輯說明先按 user_id 和 genre 分組計(jì)數(shù)再用窗口函數(shù)算每個(gè)類型在該用戶總觀看量中的占比。這樣每個(gè)用戶會(huì)得到多行每個(gè)類型一行后面可以用 pivot 轉(zhuǎn)成寬表或者直接在 Spark 里做 one-hot。參數(shù)說明WHERE 條件只取 view 和 rate 事件因?yàn)?search 事件沒有 movie_id 關(guān)聯(lián)favorite 事件量通常很小加不加對(duì)結(jié)果影響不大。如果站點(diǎn)類型體系很細(xì)超過 30 種建議先歸并成 8 到 10 個(gè)大類否則特征維度太高模型容易過擬合。4. 性別預(yù)測(cè)模型訓(xùn)練從特征寬表到分類器4.1 特征寬表的最終形態(tài)與標(biāo)簽對(duì)齊走到這一步手上有兩張表user_features用戶統(tǒng)計(jì)特征和 user_genre_pref類型偏好。需要把它們拼成一張寬表并且和性別標(biāo)簽對(duì)齊。性別標(biāo)簽通常來自用戶注冊(cè)時(shí)填寫的資料或者第三方登錄返回的 gender 字段。實(shí)際場景里標(biāo)簽缺失率可能高達(dá) 40% 到 60%這是最大的坑之一。# 用 Spark 做特征拼接和標(biāo)簽對(duì)齊 from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, lit from pyspark.ml.feature import VectorAssembler, StringIndexer from pyspark.ml.classification import RandomForestClassifier from pyspark.ml.evaluation import BinaryClassificationEvaluator spark SparkSession.builder \ .appName(GenderPrediction) \ .enableHiveSupport() \ .getOrCreate() # 讀取 Hive 表 user_feat spark.sql(SELECT * FROM user_features) genre_pref spark.sql(SELECT * FROM user_genre_pref) labels spark.sql(SELECT user_id, gender FROM user_profile WHERE gender IS NOT NULL) # 把類型偏好 pivot 成寬表 genre_wide genre_pref.groupBy(user_id).pivot(genre).sum(genre_ratio).fillna(0) # 拼接所有特征 df user_feat.join(genre_wide, onuser_id, howleft) \ .join(labels, onuser_id, howinner) \ .fillna(0) # 標(biāo)簽編碼male0, female1 df df.withColumn(label, when(col(gender) female, lit(1.0)).otherwise(lit(0.0))) # 特征向量組裝 feature_cols [c for c in df.columns if c not in (user_id, gender, label)] assembler VectorAssembler(inputColsfeature_cols, outputColfeatures) df assembler.transform(df) # 劃分訓(xùn)練集和測(cè)試集 train, test df.randomSplit([0.8, 0.2], seed42) # 隨機(jī)森林訓(xùn)練 rf RandomForestClassifier(labelCollabel, featuresColfeatures, numTrees100, maxDepth8) model rf.fit(train) # 評(píng)估 predictions model.transform(test) evaluator BinaryClassificationEvaluator(labelCollabel, metricNameareaUnderROC) auc evaluator.evaluate(predictions) print(AUC %.4f % auc)邏輯說明先用 pivot 把類型偏好從長表轉(zhuǎn)成寬表每個(gè)類型一列值是該類型的觀看占比。然后和用戶統(tǒng)計(jì)特征、性別標(biāo)簽做 joininner join 會(huì)丟掉沒有標(biāo)簽的用戶這是有意的因?yàn)闊o標(biāo)簽數(shù)據(jù)不能用于監(jiān)督訓(xùn)練。標(biāo)簽編碼成 0 和 1特征用 VectorAssembler 拼成一個(gè)向量列。隨機(jī)森林的 numTrees 設(shè) 100maxDepth 設(shè) 8是為了在偏差和方差之間取平衡。參數(shù)說明randomSplit 的 0.8/0.2 是常見比例如果數(shù)據(jù)量超過 100 萬行可以改成 0.9/0.1。numTrees 越大模型越穩(wěn)但訓(xùn)練越慢100 到 200 是常用區(qū)間。maxDepth 超過 10 容易過擬合尤其是特征維度高的時(shí)候。4.2 類別不平衡的處理與閾值調(diào)整電影網(wǎng)站的用戶性別分布通常不是 50/50男性用戶可能占 60% 到 70%。如果直接訓(xùn)練模型會(huì)傾向于預(yù)測(cè)多數(shù)類導(dǎo)致少數(shù)類的召回率很低。常見做法有兩種一是對(duì)多數(shù)類降采樣二是用類別權(quán)重。# 方法一對(duì)多數(shù)類降采樣 male_df df.filter(col(label) 0.0) female_df df.filter(col(label) 1.0) # 按少數(shù)類數(shù)量對(duì)齊多數(shù)類 ratio female_df.count() / male_df.count() male_sampled male_df.sample(withReplacementFalse, fractionratio, seed42) balanced_df male_sampled.union(female_df) # 方法二在隨機(jī)森林里設(shè)置類別權(quán)重 from pyspark.ml.classification import RandomForestClassifier rf_balanced RandomForestClassifier( labelCollabel, featuresColfeatures, numTrees150, maxDepth8, weightColclass_weight # 需要提前構(gòu)造權(quán)重列 )邏輯說明降采樣簡單直接但會(huì)損失多數(shù)類的信息量。類別權(quán)重不丟數(shù)據(jù)但需要額外構(gòu)造權(quán)重列通常少數(shù)類權(quán)重設(shè)為多數(shù)類數(shù)量的倒數(shù)比。我一般先試降采樣如果 AUC 掉得厲害再換權(quán)重法。參數(shù)說明sample 的 fraction 參數(shù)按少數(shù)類與多數(shù)類的比例算比如女性占 30%男性占 70%fraction 就是 30/70 約等于 0.43。weightCol 的權(quán)重值建議用總樣本數(shù)除以類別樣本數(shù)讓兩類總權(quán)重接近。4.3 模型評(píng)估AUC 之外還要看什么AUC 是排序指標(biāo)反映模型把正樣本排在負(fù)樣本前面的能力。但業(yè)務(wù)上更關(guān)心的是在某個(gè)閾值下預(yù)測(cè)為女性的用戶里有多少是真的女性精確率以及所有真實(shí)女性里有多少被找出來召回率。這兩個(gè)指標(biāo)此消彼長閾值調(diào)高精確率上升召回率下降反之亦然。from pyspark.ml.evaluation import BinaryClassificationEvaluator from pyspark.sql.functions import col # 按不同閾值統(tǒng)計(jì)精確率和召回率 thresholds [0.3, 0.4, 0.5, 0.6, 0.7] for t in thresholds: tp predictions.filter((col(prediction) 1.0) (col(label) 1.0)).count() fp predictions.filter((col(prediction) 1.0) (col(label) 0.0)).count() fn predictions.filter((col(prediction) 0.0) (col(label) 1.0)).count() precision tp / (tp fp) if (tp fp) 0 else 0 recall tp / (tp fn) if (tp fn) 0 else 0 print(threshold%.1f, precision%.4f, recall%.4f % (t, precision, recall))邏輯說明這段代碼手動(dòng)遍歷閾值統(tǒng)計(jì)混淆矩陣的四個(gè)值算出精確率和召回率。實(shí)際項(xiàng)目里可以用 BinaryClassificationEvaluator 的 precisionRecall 曲線但手動(dòng)算更直觀方便和業(yè)務(wù)方溝通。參數(shù)說明閾值從 0.3 到 0.7 遍歷步長 0.1。如果業(yè)務(wù)要求高精確率比如只對(duì)高置信用戶做定向推薦閾值可以設(shè)到 0.7 以上如果要求高召回率比如做全量用戶畫像補(bǔ)全閾值可以降到 0.3。5. 避坑與排查這條鏈路上最容易翻車的五個(gè)地方5.1 標(biāo)簽缺失率過高導(dǎo)致模型學(xué)不到東西現(xiàn)象訓(xùn)練出來的 AUC 只有 0.55 左右接近隨機(jī)猜。原因性別標(biāo)簽來自用戶注冊(cè)資料但大量用戶沒填或者填了「保密」實(shí)際有標(biāo)簽的樣本可能只占總用戶的 20% 到 30%而且有標(biāo)簽的用戶本身就有偏愿意填性別的用戶可能更活躍。解決先統(tǒng)計(jì)標(biāo)簽覆蓋率如果低于 30%考慮用半監(jiān)督學(xué)習(xí)或者用行為相似度做標(biāo)簽傳播另外檢查有標(biāo)簽用戶和無標(biāo)簽用戶在行為特征上是否有顯著差異如果有模型只能適用于有標(biāo)簽的那部分人群。5.2 Hive 表分區(qū)沒建全表掃描拖垮任務(wù)現(xiàn)象一個(gè)簡單的聚合查詢跑了 40 分鐘還沒出結(jié)果。原因movie_behavior 表按天分區(qū)但建表時(shí)沒寫 PARTITIONED BY所有數(shù)據(jù)堆在一個(gè)目錄下每次查詢都全量掃描。解決建表時(shí)加上 PARTITIONED BY (dt STRING)加載數(shù)據(jù)時(shí)用 INSERT OVERWRITE ... PARTITION(dt2024-01-01)查詢時(shí)帶上 WHERE dt BETWEEN ... 條件。這個(gè)坑我踩過不止一次血淚經(jīng)驗(yàn)就是Hive 表不分區(qū)等于白用 Hadoop。5.3 特征穿越用了未來信息預(yù)測(cè)過去現(xiàn)象離線 AUC 很高0.85 以上但上線后效果很差。原因特征里混入了標(biāo)簽泄露的字段比如用「用戶最近一次登錄時(shí)間」做特征但登錄時(shí)間是在性別填寫之后產(chǎn)生的等于用未來信息預(yù)測(cè)過去。解決梳理每個(gè)特征的時(shí)間戳確保特征的計(jì)算時(shí)間早于標(biāo)簽的產(chǎn)生時(shí)間。具體做法是在 ETL 里加一個(gè)時(shí)間窗口限制只用標(biāo)簽產(chǎn)生之前的行為數(shù)據(jù)。5.4 數(shù)據(jù)傾斜某個(gè)用戶的行為日志特別多現(xiàn)象Spark 任務(wù)卡在最后一個(gè) reduce 階段99% 的 task 都完成了剩下一個(gè)跑了半小時(shí)。原因某個(gè)爬蟲賬號(hào)或者測(cè)試賬號(hào)產(chǎn)生了海量行為日志按 user_id 分組時(shí)全落到一個(gè) reducer 上。解決先統(tǒng)計(jì)每個(gè)用戶的事件數(shù)把超過閾值比如 10 萬條的異常用戶單獨(dú)處理或者過濾掉或者在 groupBy 之前加隨機(jī)前綴打散聚合兩次。5.5 模型上線后性別分布漂移現(xiàn)象模型上線三個(gè)月后女性用戶的召回率從 0.7 掉到 0.5。原因站點(diǎn)用戶結(jié)構(gòu)變了新注冊(cè)用戶里女性比例上升但模型還是按舊分布訓(xùn)練的。解決定期用新數(shù)據(jù)重新訓(xùn)練監(jiān)控預(yù)測(cè)結(jié)果的性別分布和真實(shí)分布的 KL 散度超過閾值就觸發(fā)重訓(xùn)。這個(gè)沒有一勞永逸的辦法只能靠監(jiān)控和迭代。6. 把 AUC 再拉高五個(gè)點(diǎn)特征交叉與模型融合的實(shí)操技巧走到這一步基線模型已經(jīng)能跑了但 AUC 可能卡在 0.72 到 0.75 上不去。我一般會(huì)做兩件事特征交叉和模型融合。特征交叉方面最有效的是把「影片類型偏好」和「活躍時(shí)段」做交叉。比如一個(gè)用戶晚上 10 點(diǎn)后大量觀看動(dòng)作片這個(gè)組合特征比單獨(dú)的類型偏好或時(shí)段特征更有區(qū)分度。實(shí)現(xiàn)上可以在 Spark 里用 VectorAssembler 之后接一個(gè) PolynomialExpansion但維度會(huì)爆炸更穩(wěn)妥的做法是手動(dòng)構(gòu)造幾個(gè)關(guān)鍵交叉項(xiàng)from pyspark.sql.functions import col, when # 構(gòu)造交叉特征晚間動(dòng)作片偏好 df df.withColumn( night_action, col(night_ratio) * col(Action_ratio) # Action_ratio 來自類型偏好寬表 ) # 構(gòu)造交叉特征周末愛情片偏好 df df.withColumn( weekend_romance, col(weekend_ratio) * col(Romance_ratio) ) # 評(píng)分極端程度高分和低分占比之和 df df.withColumn( rating_extreme, col(high_rating_ratio) col(low_rating_ratio) )邏輯說明night_action 捕捉「夜間看動(dòng)作片」這個(gè)組合行為weekend_romance 捕捉「周末看愛情片」的組合。rating_extreme 反映評(píng)分是否走極端男性用戶往往更極端。這些交叉項(xiàng)不需要太多三到五個(gè)就夠多了反而引入噪聲。參數(shù)說明Action_ratio 和 Romance_ratio 是類型偏好寬表里的列名實(shí)際列名取決于 genre 的取值。high_rating_ratio 和 low_rating_ratio 需要在 user_features 里提前算好比如評(píng)分大于等于 4 算高小于等于 2 算低。模型融合方面我習(xí)慣用隨機(jī)森林加邏輯回歸做 stacking。隨機(jī)森林捕捉非線性關(guān)系邏輯回歸捕捉線性關(guān)系兩者互補(bǔ)。具體做法是把隨機(jī)森林的預(yù)測(cè)概率作為一個(gè)新特征和原始特征一起喂給邏輯回歸from pyspark.ml.classification import LogisticRegression from pyspark.ml.feature import VectorAssembler # 隨機(jī)森林預(yù)測(cè)概率作為新特征 rf_pred model.transform(df).select(user_id, probability, label) # 提取正類概率 from pyspark.ml.functions import vector_to_array rf_pred rf_pred.withColumn(rf_prob, vector_to_array(probability)[1]) # 和原始特征拼接 df_ensemble df.join(rf_pred.select(user_id, rf_prob), onuser_id) feature_cols_2 feature_cols [rf_prob] assembler_2 VectorAssembler(inputColsfeature_cols_2, outputColfeatures_2) df_ensemble assembler_2.transform(df_ensemble) # 邏輯回歸融合 lr LogisticRegression(labelCollabel, featuresColfeatures_2, maxIter100) lr_model lr.fit(df_ensemble)邏輯說明先用隨機(jī)森林跑一遍把預(yù)測(cè)概率取出來作為新特征再和原始特征拼在一起訓(xùn)練邏輯回歸。這樣邏輯回歸能同時(shí)利用原始特征的線性信息和隨機(jī)森林學(xué)到的非線性模式。注意要用交叉驗(yàn)證的方式生成 rf_prob否則會(huì)有標(biāo)簽泄露。參數(shù)說明vector_to_array 是 Spark 3.0 以上的函數(shù)低版本需要用 udf 手動(dòng)解析。maxIter 設(shè) 100 通常夠用如果沒收斂可以加到 200。融合后的 AUC 一般能比單模型高 3 到 5 個(gè)點(diǎn)但訓(xùn)練時(shí)間也會(huì)翻倍。最后說一個(gè)我自己的習(xí)慣每次跑完模型我都會(huì)把特征重要性打出來看一遍。如果某個(gè)特征的重要性異常高比如占了 80% 以上我第一反應(yīng)不是高興而是懷疑標(biāo)簽泄露。這個(gè)習(xí)慣幫我抓過好幾次穿越特征。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取