解析機器學習中的資料漂移問題

Baihai_IDP發表於2023-02-06

編者按:當模型在生產中呈現的輸入與訓練期間提供的分佈不對應時,通常會發生資料漂移。

Vatsal P.的這篇文章,介紹瞭如何透過漂移指標直觀瞭解資料漂移程度,並n透過一個使用合成資料的例子來展示如何利用Python計算資料隨時間的漂移指標。

以下是譯文,Enjoy!

作者 | Vatsal P.
編譯 | 嶽揚

640.jpg
由湧現AIGC引擎生成

本文主要介紹了資料漂移的基本概念以及如何利用Python處理資料漂移問題。本文涉及兩種計算漂移指標的方法,即交叉熵和KL散度的實現和區別。

以下是這篇文章的大綱:

  1. 什麼是資料漂移?
  2. 漂移指標
    2.1 交叉熵
    2.2 KL散度
  3. 解決方案架構
    程式依賴要求
  4. 程式實現
    4.1 生成資料
    4.2 訓練模型
    4.3 生成觀察結果
    4.4 計算漂移指標
    4.5 隨時間變化的漂移視覺化
  5. 計算漂移指標的障礙
  6. 結語

1 什麼是資料漂移?

MLOps是構建機器學習模型並將其部署到生產環境中的一個組成部分。資料漂移可以屬於MLOps中模型監控的範疇。它指的是量化觀察資料相對於訓練資料的變化,這些變化隨著時間的推移,會對模型的預測質量產生巨大的影響,而且往往是更糟的影響。跟蹤與訓練特徵和預測有關的漂移指標應該是模型監測和識別模型何時應該重新訓練的重要組成部分。

可以參考作者的另一篇文章(https://pub.towardsai.net/mon...),瞭解在生產環境中監控ML模型相關概念和架構的更多細節。

https://pub.towardsai.net/mon...

你可能不想監測與你的模型預測或模型特徵相關的漂移,比如當你在進行模型預測的基礎上定期重新訓練模型。此為一種與時間序列模型的應用相關的常見情況。然而,你可以去追蹤其他指標,以確定你所生成的模型質量。本文將主要關注那些與經典機器學習(分類、迴歸和聚類)相關的模型。

2 漂移指標

下文概述的兩種指標都是量化機率分佈相似程度的統計方法。

2.1 交叉熵

交叉熵可以透過以下公式定義:

640.png
交叉熵計真實的機率分佈

  • p:真實的機率分佈
  • q:估計的機率分佈

從資訊理論的角度來看,熵反映了消除不確定性所需的資訊量[3]。請注意,分佈A和B的交叉熵將與分佈B和A的交叉熵不同。

2.2 KL散度

Kullback Leibler散度,也稱為KL散度,可以透過以下公式定義:

640.png

  • P:真實的機率分佈
  • Q:估計的機率分佈

然後,Kullback-Leibler散度是使用針對Q最佳化的編碼而不是針對P最佳化的編碼對P的樣本進行編碼所需的位元數的平均差[1]。請注意,分佈A和B的KL散度與分佈B和A的KL散度不同。

這兩種度量都不是距離度量(distance metrics),因為這些度量缺乏對稱性。

entropy / KL divergence of A,B != entropy / KL divergence of B,A

3 解決方案架構

下圖概述了機器學習生命週期的執行方式,同時也包括了模型監控。正如上文說明,為了監測模型的效能,應該在訓練階段儲存各種資料,也就是用於訓練模型的特徵和目標資料。這樣就可以提供一個真實的基礎資料來源,以便與新的觀察結果進行比較。

640.png
圖片模型監測架構,圖片由作者提供

3.1 程式依賴要求

以下Python模組及其版本是下文原始碼執行的依賴。這些都是著名的資料科學/資料分析/機器學習的庫,所以對大多數使用者來說,安裝特定的版本應該不是什麼大問題。

Python=3.9.12
pandas>=1.4.3
numpy>=1.23.2
scipy>=1.9.1
matplotlib>=3.5.1
sklearn>=1.1.2

4 程式實現

下面將透過一個使用合成資料的例子來展示如何計算資料隨時間的漂移指標。請注意,該示例生成的值不會與你操作生成的值一致,因為它們是隨機生成的。此外,由於是隨機生成的,所以從提供的視覺化影像和資料中並不能解釋結果。我們的目的是提供可重複使用和可重新配置的程式碼供你使用。

4.1 生成資料

import uuid
import random
import pandas as pd
import numpy as np
from scipy.stats import entropy
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
# generate data
def generate_data(n):
    """
    This function will generate n rows of sample data.
    
    params:
        n (Int) : The number of rows you want to generate
        
    returns:
        A pandas dataframe with n rows.
    """
    data = {
        'uuid' : [str(uuid.uuid4()) for _ in range(n)],
        'feature1' : [random.random() for _ in range(n)],
        'feature2' : [random.random() for _ in range(n)],
        'feature3' : [random.random() for _ in range(n)],
        'target' : [sum([random.random(), random.random(), random.random()]) for _ in range(n)]
    }
    return pd.DataFrame(data)
  
sample_df = generate_data(1000)

上述程式碼將生成一個合成資料集,該資料集由1000行和列uuid、feature1、feature2、feature3、target組成。這是我們的基礎資料,模型將在此基礎上進行訓練。

640.png

4.2 訓練模型

# train model
ft_cols = ['feature1', 'feature2', 'feature3']
X = sample_df[ft_cols].values
Y = sample_df['target'].values

X_train, X_test, y_train, y_test = train_test_split(
    X, Y, test_size=0.3
)

rfr = RandomForestRegressor().fit(X_train, y_train)

為了本教程的目的,上述程式碼允許使用者根據我們上面生成的特徵和目標建立一個隨機森林迴歸模型。假設這個模型會被推送到生產環境中,並且每天都會被呼叫。

4.3 生成觀察結果

# generate observations
obs_df = generate_data(1500)
obs_df.drop(columns = ['target'], inplace = True)

# generate predictions
obs_df['prediction'] = obs_df[ft_cols].apply(lambda x : rfr.predict([x])[0], axis = 1)

obs_df = obs_df.rename(columns = {
    'feature1' : 'obs_feature1',
    'feature2' : 'obs_feature2',
    'feature3' : 'obs_feature3'
})

上面的程式碼將生成第一天與模型投入生產並被呼叫的特徵相關觀測資料。現在我們可以直觀地看到真實訓練資料與觀測資料之間的差異。

plt.plot(sample_df['feature1'], alpha = 0.5, label = 'Ground Truth')
plt.plot(obs_df['obs_feature1'], alpha = 0.5, label = 'Observation')
plt.legend()
plt.title("Visualization of Feature1 Training Data vs Observations")
plt.show()

640.png
圖片訓練資料與feature1的觀測資料視覺化,圖片由作者提供

從上圖可以看出,在模型投入生產的第一天,該特徵的觀測值比實際情況要多。這是一個比較麻煩的問題,因為我們不能去比較兩個長度不一樣的數值列表。如果我們比較兩個長度不同的陣列,就會產生錯誤的結果。現在,為了計算漂移指標,我們需要使觀測值的長度與真實資料的長度相等。我們可以透過建立N個buckets,並確定每個bucket中的觀測值的頻率來實現這一需求。其實本質上這是建立一個直方圖,下面的程式碼片段可以顯示這些分佈的相互關係。

plt.hist(sample_df['feature1'], alpha = 0.5, label = 'Ground Truth', histtype = 'step')
plt.hist(obs_df['obs_feature1'], alpha = 0.5, label = 'Observation', histtype = 'step')
plt.legend()
plt.title("Feature Distribution of Ground Truth Data and Observation Data")
plt.show()

640.png
圖片真實資料的特徵分佈與feature1的觀測資料,圖片由作者提供

現在兩個資料集的規模相同,我們可以比較兩個分佈的漂移情況。

4.4 計算漂移指標

def data_length_normalizer(gt_data, obs_data, bins = 100):
    """
    Data length normalizer will normalize a set of data points if they
    are not the same length.
    
    params:
        gt_data (List) : The list of values associated with the training data
        obs_data (List) : The list of values associated with the observations
        bins (Int) : The number of bins you want to use for the distributions
        
    returns:
        The ground truth and observation data in the same length.
    """

    if len(gt_data) == len(obs_data):
        return gt_data, obs_data 

    # scale bins accordingly to data size
    if (len(gt_data) > 20*bins) and (len(obs_data) > 20*bins):
        bins = 10*bins 

    # convert into frequency based distributions
    gt_hist = plt.hist(gt_data, bins = bins)[0]
    obs_hist = plt.hist(obs_data, bins = bins)[0]
    plt.close()  # prevents plot from showing
    return gt_hist, obs_hist 

def softmax(vec):
    """
    This function will calculate the softmax of an array, essentially it will
    convert an array of values into an array of probabilities.
    
    params:
        vec (List) : A list of values you want to calculate the softmax for
        
    returns:
        A list of probabilities associated with the input vector
    """
    return(np.exp(vec)/np.exp(vec).sum())

def calc_cross_entropy(p, q):
    """
    This function will calculate the cross entropy for a pair of 
    distributions.
    
    params:
        p (List) : A discrete distribution of values
        q (List) : Sequence against which the relative entropy is computed.
        
    returns:
        The calculated entropy
    """
    return entropy(p,q)
    
def calc_drift(gt_data, obs_data, gt_col, obs_col):
    """
    This function will calculate the drift of two distributions given
    the drift type identifeid by the user.
    
    params:
        gt_data (DataFrame) : The dataset which holds the training information
        obs_data (DataFrame) : The dataset which holds the observed information
        gt_col (String) : The training data column you want to compare
        obs_col (String) : The observation column you want to compare
        
    returns:
        A drift score
    """

    gt_data = gt_data[gt_col].values
    obs_data = obs_data[obs_col].values

    # makes sure the data is same size
    gt_data, obs_data = data_length_normalizer(
        gt_data = gt_data,
        obs_data = obs_data
    )

    # convert to probabilities
    gt_data = softmax(gt_data)
    obs_data = softmax(obs_data)

    # run drift scores
    drift_score = calc_cross_entropy(gt_data, obs_data)
    return drift_score
    
calc_drift(
    gt_data = sample_df, 
    obs_data = obs_df, 
    gt_col = 'feature1', 
    obs_col = 'obs_feature1'
)

上面的程式碼概述瞭如何計算觀察資料相對於訓練資料的漂移(使用scipy中的entropy實現)。首先透過matplotlib中的hist方法將輸入向量的大小歸一化為相同的長度,透過softmax函式將這些值轉換為機率,最後透過熵函式計算出漂移指標。

4.5 隨時間變化的漂移指標視覺化

drift_scores = {k:[] for k in ft_cols}
days = 5
for i in range(days):
    # calculate drift for all features and store results
    for i in ft_cols:
        drift = calc_drift(
            gt_data = sample_df, 
            obs_data = generate_data(1500), 
            gt_col = 'feature1', 
            obs_col = 'feature1'
        )
        drift_scores[i].append(drift)
        
drift_df = pd.DataFrame(drift_scores)

# visualize drift
drift_df.plot(kind = 'line')
plt.title("Drift Scores Over Time for Each Feature")
plt.ylabel("Drift Score")
plt.xlabel("Times Model Was Used")
plt.show()

640.png
圖片與模型中每個特徵相關的漂移指標視覺化,隨模型在生產中的使用時間而變化,圖片由作者提供

對基於資料集產生的結果可以設定閾值,如果模型的大多數重要特徵的漂移分數超過該閾值,這將是重新訓練模型的一個重要指標。對於基於樹的模型,可以透過sklearn或SHAP來識別每一個特徵的重要性。

5 計算漂移指標的障礙

在計算機器學習模型的資料漂移指標時,可能會遇到各種障礙:

  1. 處理值為0的特徵值或預測值。這將產生與兩個漂移實現相關的除以0的錯誤。對於該問題,一個快速而簡單的解決方法是用一個非常接近於零的極小值來代替零。由於監測到的資料漂移可能是一種別人沒有遇到的情況,所以要了解這對你正在處理的問題會產生什麼影響。
  2. 比較一對長度不相同的分佈。假設你在與每個特徵和目標相關的1,000個觀測值上訓練模型,而每天生成預測時,根據平臺獲得的流量顯示特徵和目標的觀察量從1,000到10,000不等。這明顯是有問題的,因為你不能比較兩個不同長度的分佈。為了解決這個問題,可以使用上文程式碼實現中所做的分選方法,將訓練資料和觀測值分選成相同大小的組,然後在這些資料之上計算漂移。這種情況可以透過matplotlib庫中的histogram方法輕鬆完成。
  3. 在使用softmax函式將頻率轉換為機率時得到NaN值。這是因為softmax函式依賴指數,在softmax的輸出中得到NaN的結果是因為計算機無法計算一個大數字的指數。對於這種情況,需要另一種不使用softmax的實現方法,或者研究怎樣將你傳入的數值規範化,以便softmax能夠工作。

6 結語

這篇文章的重點是詳細介紹如何在經典機器學習的應用中計算資料漂移。本文回顧了常見的漂移計算指標(如KL Divergence和Cross Entropy)相關的原理和實現。

此外,這篇文章還概述了我們在嘗試計算漂移時遇到的一些常見問題。比如當有零值時,除以零的錯誤和關於比較一對大小不一樣的分佈的問題。需要注意,這篇文章主要對那些不經常重新訓練模型的人有幫助。模型監測將作為一種重要手段來衡量一個模型是否成功,並確定它的效能何時因漂移而出現退步。這兩者都是重新訓練或重新進入模型開發階段的指標。

歡迎在我的GitHub頁面上檢視與本教程相關的資源庫。(https://github.com/vatsal220/...

參考資料

https://en.wikipedia.org/wiki...

https://en.wikipedia.org/wiki...

https://stats.stackexchange.c...

https://docs.scipy.org/doc/sc...

本文經原作者授權,由Baihai IDP編譯。如需轉載譯文,請聯絡獲取授權。(原文連結:https://towardsdatascience.co...

相關文章