值與文本混合特征工程實(shí)戰(zhàn):從清洗到模型融合全攻略)
做機(jī)器學(xué)習(xí)項(xiàng)目時(shí)我自己最怕遇到的一類數(shù)據(jù)就是“既有文本又有數(shù)值”的混合表格。比如用戶畫像里既有“年消費(fèi)金額”這種數(shù)值列又有“商品評價(jià)內(nèi)容”這種自由文本做退款風(fēng)險(xiǎn)預(yù)測既有“訂單總價(jià)”、“歷史退款率”又有“客服工單描述”、“買家留言”。這類數(shù)據(jù)在真實(shí)業(yè)務(wù)里非常常見但大多數(shù)教程要么只講純數(shù)值表格怎么調(diào)LightGBM要么只講NLP文本怎么進(jìn)BERT很少有人把“數(shù)值和文本怎么揉在一起做特征工程”講透。這篇內(nèi)容我打算用一整篇的篇幅把這套處理思路、工具鏈、實(shí)操路線和踩坑點(diǎn)都盤一遍。如果你想做大數(shù)據(jù)開發(fā)、數(shù)據(jù)挖掘或者準(zhǔn)備大數(shù)據(jù)面試這個(gè)主題其實(shí)是高頻考點(diǎn)值得認(rèn)真看。1. 混合特征難處理的根源在哪里先別急著調(diào)包。要處理混合特征首先要搞清楚一個(gè)問題為什么數(shù)值和文本拼在一起不像“把兩個(gè)DataFrame合并”那么簡單很多新人一上來就把文本用TF-IDF轉(zhuǎn)成稀疏矩陣再和數(shù)值列拼在一起丟進(jìn)模型結(jié)果效果稀爛還找不到原因。其實(shí)問題不在模型而在特征本身的“底層邏輯”完全不一樣。1.1 數(shù)值與文本的底層數(shù)據(jù)性質(zhì)不同數(shù)值特征通常是稠密的、有序的、量綱統(tǒng)一的。比如訂單金額800元、歷史退款次數(shù)2次、用戶停留時(shí)長320秒這些數(shù)字本身就具備“可比較性”和“數(shù)學(xué)運(yùn)算意義”。模型可以直接通過分裂點(diǎn)來捕捉“金額大于500時(shí)風(fēng)險(xiǎn)升高”這種規(guī)律連續(xù)特征還能做分箱、歸一化、多項(xiàng)式交叉都不會(huì)破壞它本身的語義。文本特征則完全不同。文本是離散的、稀疏的、無序的字符串單個(gè)詞匯必須經(jīng)過“向量化”才能被模型理解。把“退款好慢客服態(tài)度差”這句話轉(zhuǎn)成向量時(shí)不同分詞方式、不同詞典大小、不同權(quán)重算法產(chǎn)出的特征空間天差地別。哪怕同樣一句中文用jieba分詞和用字符級切分出來的結(jié)果可能完全不沾邊。而且文本的長度差異極大短的可能只有幾個(gè)字長的能有上千字這種“變長結(jié)構(gòu)”是純數(shù)值表格完全不具備的。也正是因?yàn)橐粋€(gè)稠密連續(xù)、一個(gè)稀疏離散直接把兩者拼起來放進(jìn)單一模型會(huì)讓模型在同一套分裂邏輯里同時(shí)處理兩種截然不同的分布形態(tài)。此時(shí)如果文本特征維度巨大、數(shù)值特征維度很小權(quán)重天然失衡模型很容易被文本側(cè)的高維稀疏信號帶偏而數(shù)值側(cè)真正有區(qū)分度的業(yè)務(wù)字段反而沒有被充分利用。1.2 真正的難點(diǎn)三類隱性矛盾在實(shí)際項(xiàng)目里混合特征真正的難點(diǎn)其實(shí)不只是“拼接”本身而是三類很容易被忽略的隱性矛盾。第一類是“尺度矛盾”。數(shù)值特征做標(biāo)準(zhǔn)化后基本都在0到1之間或者以均值為中心波動(dòng)但TF-IDF的取值區(qū)間是0到某個(gè)正數(shù)而且大量是0少量是幾個(gè)點(diǎn)以上的浮點(diǎn)數(shù)兩者的分布形態(tài)差異極大。如果模型是神經(jīng)網(wǎng)絡(luò)或基于距離的算法如KMeans、KNN尺度不一致會(huì)造成嚴(yán)重的特征淹沒如果是樹模型雖然對尺度不敏感但大量稀疏維度會(huì)顯著拖慢訓(xùn)練速度并降低分裂質(zhì)量。第二類是“語義鴻溝”。數(shù)值特征表達(dá)的語義是“多少、多快、多大”文本特征表達(dá)的語義是“發(fā)生了什么、用戶說了什么”。同樣是“退款”這個(gè)行為數(shù)值列可能只是退款1次、退款金額50元但文本評論里可能寫了“申請退款多次未果客服一直推諉”前者是行為事實(shí)后者是過程體驗(yàn)。如果只靠數(shù)值側(cè)的“退款次數(shù)1”來預(yù)測風(fēng)險(xiǎn)根本捕捉不到用戶情緒和客服處理時(shí)效問題的深層信息。文本的價(jià)值恰恰在于它能補(bǔ)充數(shù)值記錄中沒有的過程細(xì)節(jié)。第三類是“樣本對齊問題”。一個(gè)用戶有10條文本評論、對應(yīng)一條數(shù)值記錄怎么把10條文本聚合成一條文檔向量一個(gè)訂單里有5個(gè)商品標(biāo)題、若干條客服備注怎么融合到訂單粒度去做二分類這種一對多、多對一、甚至多對多的粒度關(guān)系往往比“特征怎么設(shè)”更先決定成敗。2. 數(shù)值側(cè)預(yù)處理別讓基礎(chǔ)字段拖后腿數(shù)值特征雖然看起來簡單但在混合建模里反而是最容易“先入為主”出錯(cuò)的地方。因?yàn)榇蠹叶加X得數(shù)值列不用處理直接讀進(jìn)來就能用。實(shí)際上數(shù)值列的質(zhì)量直接影響文本側(cè)特征能不能發(fā)揮價(jià)值。畢竟模型最后是一個(gè)整體一側(cè)崩了另一側(cè)再強(qiáng)也會(huì)被拖累。2.1 先做業(yè)務(wù)口徑歸一再做統(tǒng)計(jì)變換很多初學(xué)者在處理數(shù)值特征時(shí)分不清“歸一化”和“統(tǒng)計(jì)變換”更不會(huì)去想“這個(gè)數(shù)值在本業(yè)務(wù)里到底代表什么”。第一步我建議先做業(yè)務(wù)口徑歸一。比如“消費(fèi)金額”這個(gè)字段不同渠道傳入的口徑可能不一樣有的含運(yùn)費(fèi)有的不含有的是實(shí)付金額有的是商品原價(jià)“退款金額”有的是正數(shù)有的是負(fù)數(shù)。如果不把業(yè)務(wù)口徑拉齊后面的統(tǒng)計(jì)變換全是白費(fèi)。做這一步時(shí)最好列一個(gè)字段口徑清單每列寫明單位、允許范圍、缺失值的業(yè)務(wù)含義是該用戶沒有記錄還是記錄丟失這個(gè)動(dòng)作能在后面省下大量排查時(shí)間。第二步才是統(tǒng)計(jì)變換。我自己的習(xí)慣是先觀察數(shù)值分布形狀再?zèng)Q定用不用對數(shù)或Box-Cox變換。比如“用戶累計(jì)充值金額”這類偏態(tài)極大的長尾數(shù)據(jù)我一般會(huì)先做np.log1p變換因?yàn)橹苯游乖冀痤~模型的分裂點(diǎn)基本會(huì)被極少數(shù)大額用戶帶跑而大量普通用戶的金額差異在幾百元以內(nèi)反而區(qū)分不開。再比如“距上次登錄天數(shù)”這種本身就是“時(shí)間間隔”語義的字段我更傾向于直接保留原始天數(shù)同時(shí)構(gòu)造“間隔是否超過7天/30天”等業(yè)務(wù)閾值特征因?yàn)殚g隔本身的分界點(diǎn)在業(yè)務(wù)上是可解釋的。歸一化方面有一點(diǎn)要特別注意樹模型不做歸一化沒關(guān)系但一旦后續(xù)要把數(shù)值特征和文本向量同時(shí)送入神經(jīng)網(wǎng)絡(luò)或做距離計(jì)算必須統(tǒng)一做標(biāo)準(zhǔn)化或MinMax縮放。這正是混合建模里最容易埋雷的環(huán)節(jié)——文本側(cè)已經(jīng)做了TF-IDF歸一化數(shù)值側(cè)如果不做兩個(gè)空間就會(huì)歪掉。2.2 離散化與目標(biāo)編碼的取舍數(shù)值特征在混合建模里另一個(gè)重要議題是離散化還是保持連續(xù)該不該用目標(biāo)編碼從純樹模型角度來看連續(xù)數(shù)值會(huì)被自動(dòng)尋找最優(yōu)切分點(diǎn)所以很多情況下我們不需要手動(dòng)分箱。但手動(dòng)分箱有一個(gè)不可替代的好處可以對缺失值單獨(dú)成箱、對異常值單獨(dú)成箱并且能讓模型捕捉“非線性區(qū)間效應(yīng)”。比如“年齡”這個(gè)變量如果沒有業(yè)務(wù)經(jīng)驗(yàn)直接喂連續(xù)值樹模型雖然能找切分點(diǎn)但切出來的區(qū)間可能不符合業(yè)務(wù)認(rèn)知解釋性也不強(qiáng)。如果業(yè)務(wù)上已知“24歲以下和40歲以上退款風(fēng)險(xiǎn)高”可以先做出風(fēng)險(xiǎn)曲線再按曲線拐點(diǎn)分箱這樣每箱都有明確的業(yè)務(wù)標(biāo)簽。目標(biāo)編碼Target Encoding在混合特征場景里也非常好用因?yàn)楸砀駭?shù)據(jù)往往有很少的類別列比如商品類目、店鋪ID、渠道來源等這些本質(zhì)上不是自由文本但可以和文本、數(shù)值構(gòu)成上下文。目標(biāo)編碼就是用“該類別下的目標(biāo)均值全局先驗(yàn)平滑系數(shù)”替換類別本身能夠高效利用類別信息對樹模型尤其友好。不過要切記防止標(biāo)簽泄漏目標(biāo)編碼必須在訓(xùn)練集內(nèi)用K-Fold方式計(jì)算均值再映射到驗(yàn)證集和測試集不能在整份數(shù)據(jù)上直接groupby求均值。我這里給出一段典型的數(shù)值列清洗順序偽代碼能覆蓋大多數(shù)常規(guī)場景import pandas as pd import numpy as np def preprocess_numeric(df, num_cols): for col in num_cols: # 1. 缺失值填充業(yè)務(wù)無記錄填-999讓樹模型單獨(dú)成枝 df[col] df[col].fillna(-999) # 2. 異常值截?cái)喑^99.9%分位的值縮到該分位 upper df[col].quantile(0.999) df[col] df[col].clip(upperupper) # 3. 長尾偏態(tài)分布做log變換 if df[col].skew() 2: df[col] np.log1p(df[col].clip(lower0)) # 4. 高頻類別如渠道等單獨(dú)做頻率編碼或目標(biāo)編碼 return df這段代碼的關(guān)鍵在于第四步渠道、類目這類高基數(shù)類別列不要直接扔給模型做LabelEncoder因?yàn)長abelEncoder只是賦予任意整數(shù)編號樹模型會(huì)誤以為編號有大小關(guān)系。正確做法要么OneHot類別少時(shí)要么目標(biāo)編碼或頻率編碼類別多時(shí)。3. 文本側(cè)表示與向量化方法選擇要跟場景走文本特征處理是混合建模里的重頭戲也往往是耗時(shí)最長的部分。文本側(cè)處理的目標(biāo)不是“把文本轉(zhuǎn)成模型能讀的數(shù)字”就完了核心目標(biāo)是讓文本特征成為數(shù)值特征的“語義補(bǔ)充”而不是“噪聲疊加”。我實(shí)際在項(xiàng)目中按數(shù)據(jù)規(guī)模和應(yīng)用場景把文本處理分成三檔第一檔是統(tǒng)計(jì)型向量TF-IDF/BOW第二檔是詞向量聚合Word2Vec/Doc2Vec第三檔是預(yù)訓(xùn)練語言模型Embedding。三檔方案的選型和調(diào)參思路下面分開說。3.1 TF-IDF是主力方案但關(guān)鍵在調(diào)參在很多真實(shí)業(yè)務(wù)場景中時(shí)間緊、標(biāo)注少、文本短、還要可解釋性TF-IDF其實(shí)是最好的起點(diǎn)。它無需GPU、訓(xùn)練極快、特征含義明確任何一個(gè)維度都能映射回原始詞項(xiàng)方便做錯(cuò)誤分析。但TF-IDF“上限不高下限不低”很多人直接調(diào)用TfidfVectorizer默認(rèn)參數(shù)效果平庸還不知道問題出在哪。我總結(jié)了一套相對穩(wěn)定的調(diào)法。首先一定要按業(yè)務(wù)先清洗文本。中文文本要去除特殊符號、HTML標(biāo)簽、郵箱、網(wǎng)址、連續(xù)數(shù)字和無意義停用詞但不要過度清洗比如“退款”“客服”這種關(guān)鍵詞必須保留。切忌一開始就上完整停用詞表把所有高頻詞干掉因?yàn)楹芏辔谋驹诟哳l詞里恰恰藏著業(yè)務(wù)信號。from sklearn.feature_extraction.text import TfidfVectorizer import jieba def cut_words(text): text re.sub(r[a-zA-Z0-9], , str(text)) words jieba.lcut(text) # 只保留長度2的詞去掉單字和空白 words [w.strip() for w in words if len(w.strip()) 2] return .join(words) tfidf TfidfVectorizer( token_patternr(?u)\b\w\b, max_features3000, min_df5, max_df0.6, sublinear_tfTrue, strip_accentsunicode, ngram_range(1, 2), ) X_tfidf tfidf.fit_transform(corpus)這里幾個(gè)參數(shù)的含義值得展開講一下。min_df5的意思是“一個(gè)詞至少在5條樣本中出現(xiàn)過否則丟棄”這能過濾掉只在一條樣本里出現(xiàn)的噪音詞。max_df0.6的意思是“一個(gè)詞如果在60%以上的樣本里都出現(xiàn)也丟棄”原因是這類詞基本是通用詞區(qū)分能力弱。max_features根據(jù)文本量級定我一般經(jīng)驗(yàn)是數(shù)據(jù)量在幾萬條、文本短時(shí)設(shè)1000到2000足夠數(shù)據(jù)量在幾十萬條、文本較長時(shí)3000到5000比較穩(wěn)。max_features太大容易過擬合且拖慢訓(xùn)練太小又可能丟棄有效詞。sublinear_tfTrue非常關(guān)鍵它用 1log(tf) 替代原始詞頻做子線性縮放能抑制那些在單條文檔中反復(fù)出現(xiàn)的詞的權(quán)重避免“某個(gè)詞出現(xiàn)10次和出現(xiàn)50次被模型視作10倍重要”的失真實(shí)際效果對短文本尤其明顯。ngram_range(1,2) 則是同時(shí)保留單個(gè)詞和相鄰雙詞。比如“客服態(tài)度差”如果不加bigram模型看到的是“客服”“態(tài)度”“差”三個(gè)孤立的詞“差”字很容易被其他場景的“差評”“差異”稀釋。加了bigram后“態(tài)度差”作為一個(gè)整體特征語義強(qiáng)度高出很多。不過bigram會(huì)讓維度飆升所以必須配合max_features做截?cái)唷?.2 詞向量與預(yù)訓(xùn)練模型的適用邊界那什么時(shí)候不用TF-IDF而要使用Word2Vec類方法或預(yù)訓(xùn)練模型呢我的判斷標(biāo)準(zhǔn)很簡單如果文本是短文本、業(yè)務(wù)關(guān)鍵詞明確、需要快速迭代和可解釋性TF-IDF足夠如果文本是長文本、語義復(fù)雜、上下文依賴明顯或者數(shù)值側(cè)本身已經(jīng)包含很強(qiáng)結(jié)構(gòu)化信息而文本是唯一的信息增量來源那么詞向量或BERT類模型值得上。Word2Vec類方法最大的優(yōu)勢是把詞映射成稠密向量保留了部分語義關(guān)系比如“客服”和“售后”的向量距離會(huì)比較近。但用一個(gè)粗糙的Word2Vec直接去Mean Pooling整條文本效果往往不如TF-IDF好。原因是短文本里不同詞的重要性差異很大簡單平均會(huì)把“好的”“不錯(cuò)”這類泛化詞和“漏水”“爆炸”這樣的強(qiáng)情感詞同等對待。我實(shí)測下來如果業(yè)務(wù)沒有那么復(fù)雜用TF-IDF做主特征再用Word2Vec做一個(gè)“語義相似度特征”作為輔助比如計(jì)算文本向量和“風(fēng)險(xiǎn)詞庫”向量的余弦相似度反而更容易帶來增量。至于BERT等預(yù)訓(xùn)練模型適合文本較長、語義理解要求高、且有時(shí)間做微調(diào)的場景。但在大數(shù)據(jù)量、上千維稀疏特征混合建模的場景里我不建議直接把BERT輸出的768維向量和數(shù)值特征粗暴拼接后送進(jìn)XGBoost因?yàn)锽ERT向量空間和TF-IDF空間的性質(zhì)差異太大拼接后解釋性極差也很難做特征篩選。我的折中做法是用BERT對每條文本抽出一個(gè)低維的Embedding比如通過句向量模型壓到128維或256維再和數(shù)值特征混合建模或者直接把BERT當(dāng)作一個(gè)獨(dú)立模型輸出預(yù)測概率再用這個(gè)概率作為新特征加入主模型也就是做Stacking。這種“文本模型吐特征表格模型吃特征”的思路在很多競賽和業(yè)務(wù)項(xiàng)目里都更穩(wěn)定。4. 把兩類特征真正融到一起前面數(shù)值側(cè)和文本側(cè)各自預(yù)處理完畢后接下來的融合環(huán)節(jié)才是整篇內(nèi)容的核心。為什么很多項(xiàng)目做到這一步效果不升反降因?yàn)椤叭诤稀辈恢挥幸环N方案而不同方案對數(shù)據(jù)和模型類型的適配性完全不同。我在項(xiàng)目里把融合方法分成四個(gè)層級從最基礎(chǔ)的拼接一路到深度模型結(jié)構(gòu)設(shè)計(jì)后面分別講清楚適用場景和細(xì)節(jié)。4.1 直接拼接是基線但要注意稀疏度第一種方案也是最基礎(chǔ)的方案——直接橫向拼接。用scipy.sparse.hstack把數(shù)值特征矩陣和TF-IDF稀疏矩陣拼在一起再丟給LightGBM或XGBoost。from scipy.sparse import hstack X_all hstack([X_numeric_scaled, X_tfidf]).tocsr()這是最省事的做法但有幾個(gè)問題必須處理。首先是稀疏度問題。TF-IDF矩陣通常稀疏度在90%以上而數(shù)值列拼進(jìn)去后是稠密的。樹模型在訓(xùn)練時(shí)遍歷特征維度如果稀疏特征占絕大多數(shù)分裂點(diǎn)搜索時(shí)大量時(shí)間花在“判斷是否為0”上訓(xùn)練速度會(huì)明顯下降。解決方案是控制文本維度我一般把max_features控制在3000以內(nèi)并且確保稀疏矩陣以CSR格式存儲(chǔ)訓(xùn)練時(shí)算法庫對CSR的遍歷優(yōu)化會(huì)好很多。其次是特征重要性的誤導(dǎo)。在拼接后的XGBoost特征重要性里TF-IDF的幾千個(gè)維度會(huì)占據(jù)多數(shù)表面看“文本特征貢獻(xiàn)最大”但實(shí)際上可能是文本特征維度多、單維重要性低總和虛高。反觀2到3個(gè)數(shù)值特征雖然每次分裂都能帶來很大的Information Gain卻因?yàn)榫S度少累積重要性反而偏低。因此評估模型時(shí)不要只看gain型重要性還要結(jié)合shap值或permutation importance。4.2 比拼接更穩(wěn)的做法分組建模與交叉特征直接拼接的替代方案是“分組建?!薄?shù)值特征、文本特征分別訓(xùn)練兩個(gè)模型比如一個(gè)LightGBM只吃數(shù)值列一個(gè)LogisticRegression或者LightGBM只吃TF-IDF再把兩個(gè)模型輸出的預(yù)測概率作為新特征放入第二層模型融合。這種做法在Kaggle和實(shí)際業(yè)務(wù)中都非常有效好處有三個(gè)第一避免高維文本淹沒低維數(shù)值第二兩個(gè)子模型可以用各自最擅長的預(yù)處理方式不必強(qiáng)行統(tǒng)一尺度第三第二層模型能學(xué)到“文本側(cè)概率高但數(shù)值側(cè)概率低”這種非線性關(guān)系。數(shù)值分組建模的另一個(gè)重要拓展是構(gòu)造文本與數(shù)值的交叉特征。我舉一個(gè)實(shí)際業(yè)務(wù)案例電商客服工單里用戶輸入文本中出現(xiàn)了“發(fā)票”二字同時(shí)訂單金額字段大于5000元那么大概率是B端企業(yè)客戶需要專票報(bào)銷這個(gè)組合信號比單獨(dú)看任意一側(cè)都強(qiáng)。具體做法是先用文本分類或關(guān)鍵詞規(guī)則給文本打一個(gè)“意圖標(biāo)簽”比如“發(fā)票問題”、“物流問題”、“退款問題”然后做“意圖標(biāo)簽×金額分箱”的交叉特征。如果想讓模型自己學(xué)交叉還可以直接把文本規(guī)則篩選出的樣本ID作為一個(gè)二值特征和數(shù)值列一起進(jìn)樹模型讓樹模型自動(dòng)去找交叉切分點(diǎn)。# 交叉特征示例意圖標(biāo)簽從文本中提取再與數(shù)值分箱做交叉 df[intent] df[text].apply(lambda t: detect_intent(t)) # 關(guān)鍵詞/分類模型均可 df[amount_bucket] pd.cut(df[order_amount], bins[0, 200, 500, 1000, np.inf], labels[0-200, 200-500, 500-1000, 1000]) df[intent_amount_cross] df[intent].astype(str) _ df[amount_bucket].astype(str)這種交叉特征通常能帶來非常顯著的AUC提升遠(yuǎn)高于直接堆更多TF-IDF維度。4.3 時(shí)間窗口類特征文本與數(shù)值混合的高頻場景金融、風(fēng)控、電商場景里時(shí)間窗口類文本數(shù)值混合特征是另一個(gè)大頭。比如歷史上每個(gè)月的文本評價(jià)量、近7天客服投訴關(guān)鍵詞出現(xiàn)次數(shù)、近30天平均退款金額變化率等。這些特征本質(zhì)上已經(jīng)屬于“時(shí)序聚合”范疇處理起來有幾條特別容易踩坑的經(jīng)驗(yàn)。時(shí)間窗口的計(jì)算必須在“樣本時(shí)間點(diǎn)之前”截止嚴(yán)禁使用未來數(shù)據(jù)這是時(shí)序建模的絕對鐵律。我用自己的親身教訓(xùn)舉個(gè)例當(dāng)時(shí)做用戶流失預(yù)警用某個(gè)用戶30天內(nèi)工單文本里的投訴關(guān)鍵詞數(shù)量做特征因?yàn)橛?jì)算腳本在跑全量ETL時(shí)沒有按時(shí)間過濾導(dǎo)致驗(yàn)證集里包含了未來信息離線AUC做到了0.87上線后線上AUC直接跌到0.71。后來排查半天才發(fā)現(xiàn)特征本身存在時(shí)間穿越。處理這類特征的正確做法是在訓(xùn)練集的每個(gè)樣本上用“以該樣本的截止時(shí)間向前回溯窗口”不是先聚好一張全量大寬表再劃分訓(xùn)練集和測試集。時(shí)間窗口通常取多個(gè)長度比如近7天、近14天、近30天因?yàn)闃I(yè)務(wù)上不同行為的影響時(shí)效不同。文本側(cè)可以聚合出“窗口內(nèi)負(fù)面詞數(shù)”、“窗口內(nèi)平均文本長度”、“窗口內(nèi)意圖類別熵”等高層變量再和數(shù)值側(cè)的“窗口內(nèi)消費(fèi)總額”、“最近一次交互間隔”做時(shí)間衰減加權(quán)。因?yàn)槲谋敬髷?shù)據(jù)量聚合計(jì)算量大一般建議先在離線節(jié)流側(cè)把時(shí)間分桶、用戶分組、關(guān)鍵詞分類都做好索引。5. 實(shí)操記錄電商退款預(yù)測中的混合特征全流程理論講了不少下面用一個(gè)可落地的業(yè)務(wù)場景走一遍全流程。場景是電商平臺的退款風(fēng)險(xiǎn)預(yù)測任務(wù)預(yù)測目標(biāo)是“用戶下單后7天內(nèi)是否申請退款二分類”。數(shù)據(jù)布局大概是每行一個(gè)用戶數(shù)值列有歷史訂單數(shù)、歷史退款數(shù)、近30天登錄次數(shù)、客單價(jià)等文本列是用戶最近的客服投訴描述長度從幾個(gè)字到幾百字不等而且不是每個(gè)人都有描述很多人這一欄是空的。這是一個(gè)相當(dāng)?shù)湫偷摹拔谋九c數(shù)值混合特征”場景。5.1 數(shù)據(jù)觀察與指標(biāo)設(shè)定我先做數(shù)據(jù)抽樣質(zhì)檢發(fā)現(xiàn)文本列空值率約為42%。這非常關(guān)鍵因?yàn)槿绻苯影堰@列文本處理成“空字符串”TF-IDF會(huì)把空字符串給一個(gè)全零向量但如果42%都是全零行會(huì)造成特征矩陣底部堆積大量零向量影響后續(xù)的稀疏計(jì)算和模型學(xué)習(xí)。于是我構(gòu)造了一個(gè)二值列“is_text_present”標(biāo)記是否有文本記錄這個(gè)簡單特征本身往往也有很強(qiáng)的業(yè)務(wù)含義因?yàn)闀?huì)留文字投訴的用戶大概率比沉默用戶的退款率更高。業(yè)務(wù)指標(biāo)選什么也很重要。退款預(yù)測這種正負(fù)樣本不平衡任務(wù)正樣本大約只占15%AUC是主要參考指標(biāo)但做特征工程和閾值決策時(shí)還要兼顧召回率和精確率。因?yàn)闃I(yè)務(wù)核心是想提前盯住高風(fēng)險(xiǎn)用戶寧可有部分誤報(bào)也不要漏掉真正會(huì)退款的人所以線下評估時(shí)我通常會(huì)同時(shí)觀察AUC、召回率Precision0.5以及KS值多個(gè)角度驗(yàn)證特征有沒有帶來真正的業(yè)務(wù)增量。5.2 特征構(gòu)建完整流水線整條特征構(gòu)建流水線我是這樣設(shè)計(jì)的拆成四個(gè)模塊模塊一是“用戶靜態(tài)數(shù)值特征”。包括歷史訂單數(shù)、歷史退款數(shù)、退款率、平均訂單金額、近30天登錄天數(shù)等。退款率我做了平滑處理退款率歷史退款數(shù)1/歷史訂單數(shù)5避免新用戶訂單少時(shí)退款率為0%或100%的極端表現(xiàn)。平均訂單金額單獨(dú)保留同時(shí)增加一條“用戶最高訂單額是否大于500”的布爾特征。模塊二是“客服投訴文本特征”。先對文本做分詞清洗再計(jì)算每條描述的TF-IDF。max_features設(shè)為2000min_df設(shè)為5max_df設(shè)為0.6。同時(shí)算了幾個(gè)文本側(cè)統(tǒng)計(jì)特征文本長度、句子數(shù)、負(fù)面詞數(shù)量基于自建負(fù)面詞典、“退款”“賠償”等關(guān)鍵詞數(shù)量。因?yàn)門F-IDF維度只有2000不會(huì)造成過大的稀疏壓力這部分可以直接參與拼接。模塊三是“交叉與目標(biāo)編碼特征”。把“是否投訴過退款”與“歷史退款率分箱”做交叉把“商品是否有售后問題關(guān)鍵詞”的布爾值作為單列加入。另外我針對“最近一次投訴文本”做了目標(biāo)編碼基于K-Fold計(jì)算每個(gè)投訴關(guān)鍵詞組合下的平均退款率。模塊四是“模型輸入拼接”。用ColumnTransformer把數(shù)值列做StandardScaler、把TF-IDF保持稀疏再用scipy.sparse.hstack拼接成總特征矩陣最后喂給LightGBM。from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.feature_extraction.text import TfidfVectorizer import lightgbm as lgb from scipy.sparse import hstack num_cols [history_order, history_refund, avg_order_amount, login_days_30] text_col complaint_text preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_cols), (txt, TfidfVectorizer(max_features2000, min_df5, max_df0.6, sublinear_tfTrue, ngram_range(1, 2)), text_col), ], remainderdrop ) X_trans preprocessor.fit_transform(df) y df[is_refund].values model lgb.LGBMClassifier( n_estimators500, learning_rate0.03, num_leaves31, max_depth6, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, ) model.fit(X_trans, y, eval_set[(X_trans, y)], eval_metricauc)5.3 實(shí)驗(yàn)對比三組方案的差異為了驗(yàn)證混合特征方案是否真的有效我同一份數(shù)據(jù)上跑了三組實(shí)驗(yàn)做對照。第一組只使用數(shù)值特征不做任何文本處理。第二組只使用文本TF-IDF不看數(shù)值列。第三組按上面完整流水線拼接了數(shù)值與文本特征。線下AUC結(jié)果分別大約為0.712、0.745和0.823。文本特征單用確實(shí)比數(shù)值強(qiáng)畢竟投訴描述里包含大量直接業(yè)務(wù)信號但第三組的混合拼接提升更大比只拼文本特征提升了約7.8個(gè)AUC點(diǎn)。隨后我又做了消融實(shí)驗(yàn)在第三組的基礎(chǔ)上去掉“歷史退款率與投訴詞交叉特征”AUC跌回0.801去掉文本的is_text_present布爾列AUC再跌0.005。這些細(xì)微變化說明混合特征的價(jià)值不是某一列突然帶來的而是“多組信號交叉覆蓋”的結(jié)果。沒有交叉特征的純拼接文本側(cè)和數(shù)值側(cè)只是并排放置缺少讓模型把兩邊信號“聯(lián)動(dòng)”起來的橋梁效果提升就會(huì)受限。6. 實(shí)際項(xiàng)目中最常見的幾個(gè)坑與排查方法混合特征處理項(xiàng)目里特征本身往往不是最大的坑能成功跑通流程之后真正耗時(shí)的是“排查問題”。我把自己實(shí)踐過程中反復(fù)踩過的幾類問題和排查思路整理成清單這些內(nèi)容在常規(guī)文檔里都很難找到屬于做完幾個(gè)項(xiàng)目后才慢慢沉淀下來的經(jīng)驗(yàn)。6.1 數(shù)據(jù)泄漏陷阱清洗和編碼放錯(cuò)了時(shí)間點(diǎn)典型錯(cuò)誤包括在全量數(shù)據(jù)上用TF-IDF的fit_transform后再切訓(xùn)練集和驗(yàn)證集或者用全量數(shù)據(jù)計(jì)算目標(biāo)編碼均值。這種全局fit操作會(huì)把驗(yàn)證集和測試集的信息帶進(jìn)訓(xùn)練過程導(dǎo)致離線指標(biāo)虛高。正確做法是在Pipeline內(nèi)部完成fit或者手動(dòng)先切分?jǐn)?shù)據(jù)再用訓(xùn)練集fit、驗(yàn)證集只做transform。一個(gè)簡單的檢查方法是在特征工程完成后隨機(jī)抽取一條驗(yàn)證集樣本看它的TF-IDF維度權(quán)重和全量fit出來的權(quán)重是否完全一致如果不一致說明transform鏈路有問題。6.2 詞表不一致線上線下的隱藏殺手另一個(gè)非常隱蔽的坑是詞表不一致。我遇到過這樣的情況離線訓(xùn)練時(shí)用的是全量數(shù)據(jù)分詞得到的詞典包含文本里的縮寫、表情符號、新詞線上服務(wù)時(shí)由于前端清洗邏輯把某些字符轉(zhuǎn)掉了導(dǎo)致線上文本經(jīng)過詞表映射后大量詞匯變成了OOV特征分布和離線完全不一樣。最典型的案例是離線把“000”視為無意義噪聲去除線上卻保留了“000”并映射到某個(gè)維度甚至語義完全變了。解決辦法是提前固定清洗規(guī)則和詞表一般通過joblib保存TfidfVectorizer對象線上加載同一個(gè)對象做transform。如果有增量更新需求也要在離線流程中嚴(yán)格走詞表版本管理。6.3 多值文本融合不當(dāng)如果一個(gè)用戶可以有多條評論不能直接把所有評論拼接成一個(gè)超長文本丟進(jìn)TF-IDF因?yàn)殚L文本的詞頻統(tǒng)計(jì)會(huì)被長度主導(dǎo)而且不同數(shù)量評論的樣本間文本長度差距極大。我在項(xiàng)目里常用的做法是分別計(jì)算每條評論文本的TF-IDF然后做均值池化或者對文本進(jìn)行截?cái)嗝總€(gè)用戶只保留最近2條評論拼接。但截?cái)嗖呗砸Y(jié)合業(yè)務(wù)如果評論包含售后過程和最終解決反饋較早那條可能包含問題產(chǎn)生原因最近一條包含最終結(jié)果兩條都很重要。更穩(wěn)健的折中方案是對用戶的所有評論文本做“先拼接但按條數(shù)歸一化”同時(shí)附加“評論條數(shù)”作為數(shù)值特征。6.4 超大數(shù)據(jù)量下的性能壓力最后補(bǔ)一個(gè)工程向的坑。幾十萬甚至千萬級數(shù)據(jù)量做TF-IDF的fit_transform過程默認(rèn)會(huì)非常占用內(nèi)存因?yàn)閟klearn會(huì)先構(gòu)建完整的詞匯表再轉(zhuǎn)換。如果有幾百萬條長文本即便max_features設(shè)2000分詞階段的內(nèi)存峰值也可能很高。實(shí)測中我會(huì)先用jieba分詞并落盤成parquet格式再抽一個(gè)小樣本來fit詞表最后用已經(jīng)fit好的詞表對整個(gè)大數(shù)據(jù)集做transform并把過程拆成多個(gè)批次。同時(shí)TF-IDF轉(zhuǎn)換的結(jié)果一定用scipy.sparse.csr_matrix保存不要轉(zhuǎn)成numpy數(shù)組否則幾百萬文本乘以2000維會(huì)直接把你機(jī)器的內(nèi)存打爆。7. 從快速實(shí)現(xiàn)到穩(wěn)定落地的一些補(bǔ)充經(jīng)驗(yàn)聊了這么多具體方法和坑點(diǎn)最后補(bǔ)充一些我自己的個(gè)人經(jīng)驗(yàn)尤其是當(dāng)你把混合特征方案放到更大的工程體系里時(shí)需要注意的細(xì)節(jié)。7.1 關(guān)于列名管理在混合特征項(xiàng)目里特征列名一定不能靠記住“第幾列是哪個(gè)”來管理。數(shù)字特征經(jīng)過縮放后拼上txt前綴再追加到稀疏矩陣?yán)飼r(shí)和原本的文本列名、原數(shù)字列名已經(jīng)形成了很長的列名串聯(lián)。如果你要用shap解釋模型、做錯(cuò)誤分析沒有好用的列名映射會(huì)崩潰。我的習(xí)慣是保存一個(gè)“列名列表”文件包含模型輸入時(shí)的全部列名尤其對文本側(cè)每個(gè)TF-IDF維度標(biāo)注出它對應(yīng)的原始詞。這樣在分析某個(gè)特征造成誤判時(shí)能直接找到文本內(nèi)容和權(quán)重來源調(diào)試效率高很多。7.2 先設(shè)一個(gè)快速的弱基線不要一開始就把所有特征工程做完再訓(xùn)練模型。我會(huì)先跑一個(gè)“只用數(shù)值列”的LightGBM基線然后很快跑一個(gè)“只用文本TF-IDF”的基線最后再做拼接和交叉。這樣如果完整方案的性能不如最弱基線能快速定位問題在“融合方式”還是“單側(cè)特征構(gòu)造”上。具體來說第一版跑通以后先看錯(cuò)誤樣本里文本和數(shù)值各自的貢獻(xiàn)等到所有方案性能都提升到穩(wěn)定區(qū)段再回頭給文本加更多語義特征。7.3 文本側(cè)不是模型越多越好最后一句話文本特征處理方案永遠(yuǎn)是“簡單方案先搞通復(fù)雜方案按需上”。在真實(shí)業(yè)務(wù)場景中時(shí)間成本、線上延遲、可解釋性往往比模型精度更值錢。如果一個(gè)TF-IDF加手工交叉特征已經(jīng)讓AUC從0.71升到0.82再加BERT反而可能只提升0.005卻讓線上推理時(shí)間翻了十倍那這筆賬要好好衡量。根據(jù)我個(gè)人的經(jīng)驗(yàn)文本與數(shù)值混合特征項(xiàng)目的成功關(guān)鍵通常在“兩側(cè)特征如何交互補(bǔ)充”而不在于用了多厲害的文本模型。把數(shù)值列吃透、把文本意圖抽準(zhǔn)、把時(shí)間窗口算對往往比盲目套用深度模型更有效。這篇文章里我的核心思路是從工程實(shí)踐出發(fā)盡量把文本與數(shù)值混合特征從清洗、編碼到模型融合的關(guān)鍵環(huán)節(jié)都過了一遍。不管你是剛開始接觸大數(shù)據(jù)特征工程還是在準(zhǔn)備特征工程方向的大數(shù)據(jù)面試題我相信這些方法和案例都能給到一些可以落地的參考。以后你有機(jī)會(huì)拿到真實(shí)的混合特征數(shù)據(jù)可以先用我給的整套流程快速出第一版結(jié)果再基于具體業(yè)務(wù)調(diào)細(xì)節(jié)這個(gè)方向會(huì)比你想象中容易出成績。