項目背景
原本業(yè)務內(nèi)容是比較常見的判定業(yè)務,即輸入為某個實體有一定誤差的測量信息和相關(guān)參考信息,輸出為其應當歸屬的實體。套用一個簡單場景就是輸入一篇未署名文章,根據(jù)文風歸屬到庫中已存在的作者名下,抑或是歸屬到一個新建的匿名作者名下。
問題的難點在于:
- 分類實體數(shù)量較多,在百萬量級
- 分類數(shù)量不確定,且在動態(tài)變化,即有新增和過期
- 測量存在誤差
- 場景較多
評價標準:
- 歸屬要準確(作者名下文章不要錯)
- 少遺漏(文章能盡量找到作者)
- 避免錯誤創(chuàng)建(同一作者不要創(chuàng)建多個實體)
原解決方案也比較傳統(tǒng),即制定了一套策略,將策略組合為一顆決策樹結(jié)構(gòu),邏輯較重的同時需要很多先驗知識。套用場景即根據(jù)常用的人稱代詞、語法結(jié)構(gòu)來判斷是否屬于同一作者。
隨著準確率要求不斷提高,策略組合的調(diào)整和維護愈發(fā)困難,主要原因在于:
- 策略不斷增加,維護難度提升
- 為兼容誤差打了不少補丁,維護難度提升
- 閾值、策略調(diào)整僅憑經(jīng)驗,每次調(diào)整需要回歸驗證的數(shù)據(jù)較多
- 場景較多,要針對每個場景制定對應的優(yōu)化策略的工作量較大
總之一句話,可預見的未來策略會繼續(xù)膨脹,維護難度也會進一步提升。意識到技術(shù)風險后,組內(nèi)討論認為如果使用機器學習方案一方面可降低解決方案的復雜度(降低維護成本),另一方面利于后期場景擴展和能力遷移。至少也應該能做到模型解決通用場景,在此基礎上再定制優(yōu)化策略(降低開發(fā)成本)。
本次幸運地被選中承擔調(diào)研工作,盡管之前沒有使用模型解決問題的經(jīng)驗(純小白),但剛好有個拓寬技術(shù)面的機會肯定是不能放過的。于是現(xiàn)學現(xiàn)賣記錄一下趟坑過程,也希望能給其他有興趣的非算法攻城獅們一點信心,其實(浮于表面的)模型應用并沒有通常大家認為的那么難。
前期準備
問題定義
問題的本質(zhì)是匹配,即目標比較兩個向量的距離。有很多方式可以實現(xiàn)這個比較,比如
- 分類:根據(jù)標注好的各類別數(shù)據(jù)參與訓練,預測新數(shù)據(jù)屬于哪一原定分類
- 聚類:把相似數(shù)據(jù)聚合為一類,最終得到多個分類
- 回歸:多用于根據(jù)標注好的數(shù)據(jù)和原預測值獲得一個擬合函數(shù),預測新數(shù)據(jù)的值
模型選擇
聚類模型
思路:特征聚為一類說明向量距離接近,屬于同一分類。
使用聚類模型則似乎不利于實現(xiàn)在線預測(每次預測都要聚類,投入較高),且常用的聚類模型存在一些限制條件,如:
- K-means等,需要預設類簇個數(shù)K,不適合本業(yè)務場景
- DBScan等,需要一些先驗知識如向量間的距離閾值等,在本場景中較難確定
- 聚類模型很難支持百萬量級類簇數(shù)量的聚類
回歸模型
實在不知如何將向量匹配問題轉(zhuǎn)換為回歸問題,所以干脆沒怎么嘗試。
多分類模型
思路:認為每個應當歸屬同一實體的數(shù)據(jù)屬于一個分類,為所有特征計算分類。
多分類本質(zhì)上是為每個類訓練了一個分類器,但訓練上百萬個分類器顯然也并不合理,放棄方案。
二分類模型
通過研究某屆天池競賽發(fā)現(xiàn),直接計算兩個特征向量的“差異”并根據(jù)最終是否匹配進行標注,是個比較合理的思路,最終便選擇了二分類模型。
舉例來說是這樣:
| x_diff | y_diff | match |
|---|---|---|
| 0.1 | 0.1 | true |
| 0.9 | 0.9 | false |
簡而言之,原本決策引擎使用的是多個isMatch(v1, v2, threshold)規(guī)則組成的規(guī)則組,滿足一組特定規(guī)則的就認為是同一類;而在使用機器學習模型后,使用的是[a1-a2, b1-b2, ... , x1-x2]向量映射在高維空間的結(jié)果,由模型劃線分類。
特征工程
基礎數(shù)據(jù)
基礎屬性數(shù)據(jù)盡量選擇與結(jié)果有相關(guān)性的,避免為明顯毫無關(guān)聯(lián)的屬性計算特征參與訓練。
特征計算
如前所述,最終特征的形式是輸入與候選間同一屬性維度的diff,最終形成的向量則是N個維度diff的結(jié)果。
特殊處理
- 首先明確,機器學習模型并不清楚特征各個維度之間的聯(lián)系,也不知道特征維度的diff如何計算,所以需要做一些處理;
- 其次,可以做一些特殊處理使得基礎特征與結(jié)果關(guān)聯(lián)性更強,比如將x、y坐標轉(zhuǎn)為二維坐標,使得最終的diff結(jié)果由x_diff、y_diff轉(zhuǎn)化為(x, y)間的歐式距離
通過將一些非數(shù)值類特征通過特殊手段處理映射為(連續(xù)的)數(shù)值特征,對模型訓練結(jié)果會有明顯幫助:
- 文本類:可以嘗試Levenshtein距離(編輯距離)
- 枚舉類:可嘗試One-Hot編碼距離
- 距離類:可嘗試余弦相似度、曼哈頓距離等
具體處理方式還是依業(yè)務而定,并非任何維度特征都應當處理。
數(shù)據(jù)準備
總數(shù)據(jù)量
通過特征工程計算的10w+特征向量。
打標
二分類模型需要將數(shù)據(jù)分為兩個類,如0類和1類,或a類和b類等,分類標簽需要體現(xiàn)在訓練時的特征數(shù)據(jù)中,也就是說訓練數(shù)據(jù)實際上是特征向量+分類標簽。
本業(yè)務中的分類就是兩者是否匹配。套用場景即每篇文章提供多個候選作者,與其真正的作者屬性diff形成的特征向量打標將會是a,而與其他非真正作者形成的特征向量打標將為b。
鑒于原決策模型準確率9x%左右,索性直接使用了原本業(yè)務決策模型輸出的結(jié)果作為分類標簽,當然使用人工打標結(jié)果效果是更好的,盡管人工打標準確率也不一定是100%。
預處理
- 歸一化
- 避免單一維度影響過大,將每個維度的值映射到0~1的區(qū)間內(nèi)
- 計算方式:
normalized_value = (value - min_value) / (max_value - min_value)
建模流程
建模
說明
本流程使用阿里云PAI平臺搭建,省去搭環(huán)境的痛苦。當然,這個流程平臺無關(guān),完全也可以自行搭建。
讀數(shù)據(jù)表
打標后的特征數(shù)據(jù)輸入。
預處理腳本
對某些稀疏矩陣類型的特征進行預處理,轉(zhuǎn)為標準順序格式。即把Java中的Map結(jié)構(gòu)按照擬定的順序轉(zhuǎn)為一維數(shù)組,由于特征較多,這個過程使用了代碼生成腳本。
類型轉(zhuǎn)換
某些特征值需要由非數(shù)值類型轉(zhuǎn)為數(shù)值類型,另外可以做一些缺失值填充。
全表統(tǒng)計
統(tǒng)計數(shù)據(jù)表的各項信息,包括最大最小值、方差等。用于在數(shù)據(jù)預處理時參考,如歸一化計算用到的每個維度的最大值最小值。
訓練時的預測中這不是必須的步驟,輸出的數(shù)據(jù)主要用于脫離平臺使用。
數(shù)據(jù)歸一化
將每個維度的值映射至0~1。
數(shù)據(jù)拆分
將輸入的全量特征數(shù)據(jù)隨機拆分成兩個部分:
- 訓練數(shù)據(jù)
- 用于訓練模型,理論上訓練數(shù)據(jù)越多,訓練好的模型預測越穩(wěn)定和準確
- 預測數(shù)據(jù)
- 用于給訓練完的模型進行預測測試,模型對預測數(shù)據(jù)的預測結(jié)果將成為模型評估的依據(jù)
在這里可以把訓練數(shù)據(jù)與預測數(shù)據(jù)按7:3或8:2拆分。
XGBoost二分類
XGBoost是基于梯度提升樹(GBDT)算法的模型,由華人大牛陳天奇博士團隊提出。
GBDT的思想可以用一個通俗的例子解釋,假如有個人30歲,我們首先用20歲去擬合,發(fā)現(xiàn)損失有10歲,這時我們用6歲去擬合剩下的損失,發(fā)現(xiàn)差距還有4歲,第三輪我們用3歲擬合剩下的差距,差距就只有一歲了。如果我們的迭代輪數(shù)還沒有完,可以繼續(xù)迭代下面,每一輪迭代,擬合的歲數(shù)誤差都會減小。
——參考資料
模型預測
將預測數(shù)據(jù)輸入訓練好的模型預測,結(jié)果用于模型效果評估。
模型導出
將訓練好的模型導出為pmml格式文件。
混淆矩陣/二分類評估
用于評估模型效果。
模型評估
混淆矩陣
結(jié)果
說明
| 預測Positive | 預測Negative | |
|---|---|---|
| 真值Positive | True Positive,該P的P了,正確 | False Negative,該P的N了,錯誤 |
| 真值Negative | False Positive,該N的P了,錯誤 | True Negative,該N的N了,正確 |
準確率
ACC = (TP + TN) / (TP + FN + FP + TN)
即整體準確率,所有預測正確的 / 總預測量。
精確率
PPV = TP / (TP + FP)
即該P也正確預測為P的準確率。也可以計算N的精確率。
召回率
TPR = TP / (TP + FN)
即P預測正確的占真值P的比例。也可計算N的。
總的來說準確率可以看出綜合分類能力,而精確率和召回率可以看出其中一個分類的預測能力。
二分類評估
二分類評估結(jié)果通常是計算得到ROC/K-S曲線等。
簡單地說,ROC曲線越靠左上角/AUC越大/F1 score越大/KS越大說明模型效果越好。
特征重要性
XGBoost模型對特征重要性進行了評估,對于貢獻度非常小的特征維度在訓練過程中舍棄掉了。
同時,訓練好的模型可以輸出特征重要性排序,也就是說可以根據(jù)特征重要性進行針對性的優(yōu)化。例如,某維度重要特征由某服務計算得來,那么提升這個服務能力將比提高其他能力對結(jié)果的影響更大。
服務集成
模型導出
訓練好的模型可導出為標準的pmml格式文件。
pmml格式是數(shù)據(jù)挖掘的通用規(guī)范格式,pmml文件其實就是一個很長的xml,包含用到的特征及特征間的關(guān)系。通過pmml文件可以加載訓練的模型并執(zhí)行預測,也就是說pmml是一個“類代碼”,用于生成可運行的“實例”。
集成至服務
可以簡單通過pmml-evaluator包加載pmml文件:
<!-- 依賴 -->
<dependency>
<groupId>org.jpmml</groupId>
<artifactId>pmml-evaluator</artifactId>
<version>1.5.9</version>
</dependency>
/* 集成 */
@Service
public class PmmlDemo {
/**
* 模型pmml文件路徑
*/
private static final String MODEL_PMML_PATH = "/model/gbdt_model_20210106.pmml";
/**
* 模型
*/
private Evaluator model;
/**
* 參數(shù)列表
*/
private List<InputField> paramFields;
/**
* Positive目標分類,同訓練數(shù)據(jù)打標中的Positive分類
*/
private static final Object TARGET_CATEGORY = "0";
@PostConstruct
public void init() throws IOException, JAXBException, SAXException {
model = buildEvaluator();
paramFields = model.getInputFields();
}
/**
* 加載模型
* pmml-evaluator 1.5.x版本的使用方式與1.4略有不同
*/
private static Evaluator buildEvaluator() throws JAXBException, SAXException, IOException {
InputStream inputStream = PmmlDemo.class.getResourceAsStream(MODEL_PMML_PATH);
PMML pmml = PMMLUtil.unmarshal(inputStream);
inputStream.close();
ModelEvaluatorBuilder evaluatorBuilder = new ModelEvaluatorBuilder(pmml, (String)null)
.setModelEvaluatorFactory(ModelEvaluatorFactory.newInstance())
.setValueFactoryFactory(ValueFactoryFactory.newInstance());
return evaluatorBuilder.build();
}
/**
* 模型預測
*/
public Double getPredictScore(BizFeature feature) throws InvocationTargetException, IllegalAccessException {
if (feature == null) {
throw new NullPointerException();
}
// 讀取feature數(shù)據(jù)
Map<String, Object> fieldMap = featureToMap(feature);
// 填充模型輸入
Map<FieldName, FieldValue> params = fillParams(fieldMap);
// 預測
ProbabilityDistribution result = predict(params);
if (result == null) {
return null;
}
return result.getProbability(TARGET_CATEGORY);
}
/**
* 通過反射把業(yè)務特征BO屬性轉(zhuǎn)為map結(jié)構(gòu)
* 包括數(shù)據(jù)的預處理
*/
private static Map<String, Object> featureToMap(BizFeature feature) throws InvocationTargetException, IllegalAccessException {
Map<String, Object> output = Maps.newHashMapWithExpectedSize(512);
Method[] methods = BizFeature.class.getDeclaredMethods();
for (Method method : methods) {
String key = method.getName();
if (!key.startsWith("get")) {
continue;
}
key = key.toLowerCase();
if (key.contains("bizid") || key.contains("entityid") || key.contains("label")) {
continue;
}
Object value = method.invoke(feature);
key = key.substring(3);
put(output, key, value);
}
return output;
}
/**
* 數(shù)據(jù)預處理
* 這里用到歸一化
*/
private static void put(Map<String, Object> outputMap, String key, Object value) {
if (value instanceof Integer) {
outputMap.put(key, BizFeatureNormalizationHelper.normalization(key, (Integer)value));
} else if (value instanceof Double) {
outputMap.put(key, BizFeatureNormalizationHelper.normalization(key, (Double)value));
}
}
/**
* 根據(jù)模型需要的特征,提取對應的業(yè)務特征值進行填充
*/
private Map<FieldName, FieldValue> fillParams(Map<String, Object> map) {
Map<FieldName, FieldValue> params = Maps.newHashMap();
for (InputField inputField : paramFields) {
FieldName inputFieldName = inputField.getName();
Object rawValue = map.get(inputFieldName.getValue());
FieldValue inputFieldValue = inputField.prepare(rawValue);
params.put(inputFieldName, inputFieldValue);
}
return params;
}
/**
* 預測似乎是非線程安全的?使用synchronized
*/
private synchronized ProbabilityDistribution predict(Map<FieldName, FieldValue> arguments) {
Map<FieldName, ?> results = model.evaluate(arguments);
List<TargetField> targetFields = model.getTargetFields();
if (CollectionUtils.isEmpty(targetFields)) {
return null;
}
TargetField targetField = targetFields.get(0);
FieldName targetFieldName = targetField.getName();
return (ProbabilityDistribution)results.get(targetFieldName);
}
}
注意:pmml-evaluator包1.5以前的版本模型加載方式和1.5.x版本方式不同。
在線預測
預測結(jié)果會輸出每個分類的0~1打分,在二分類中兩個分類的結(jié)果是互補的,如對a分類預測分0.3,則對b分類預測分就是0.7。
通過分類效果評估步驟可以確定一個合理的預測分閾值,即控制是以0.4分界還是0.6分界,小于這個閾值的為a分類,否則為b分類。
在線預測結(jié)果跟模型訓練階段同一條數(shù)據(jù)的預測結(jié)果可能有細微不同,因為數(shù)據(jù)預處理階段使用的統(tǒng)計信息不完全相同。本業(yè)務訓練時使用的歸一化統(tǒng)計信息和在線預測時的略有不同。
補充
在淌過這條河以后發(fā)現(xiàn),如果并不深究而是淺顯地使用機器學習模型解決問題的話,確實沒有想象中那么難。希望這篇簡短的記錄能夠幫助更多人更容易地切換思路,在工具包里添加一項新的利器。
另外,Java/Python也有現(xiàn)成工具可以直接訓練模型,導出為pmml文件,當然平臺會更方便一些就是了。
踩坑經(jīng)歷
- 一定要定義好問題,這可能是小白嘗試機器學習最難的一步(淚)
- 二分類中樣本數(shù)量最好相近,否則可能會導致模型對某個分類過擬合
- 集成pmml后需要做與訓練模型時相同的數(shù)據(jù)預處理
參考文檔
機器學習系列(七)——分類問題(classification)zxhohai的博客-CSDN博客分類問題
分類器評估指標——混淆矩陣 ROC AUC KS AR PSI Lift Gain_snowdroptulip的博客-CSDN博客