在做機器學習相關項目時,通常會出現(xiàn)樣本數(shù)據(jù)量不均衡操作,這時可以使用 imblearn 包進行重采樣操作,可通過 pip install imbalanced-learn 命令進行安裝。
注 在 imblearn 包使用過程中,通常輸入項 x 多為 2D 的結構。否則會包 ``
不均衡分析
在數(shù)據(jù)化運營過程中,以下場景會經常產生樣本分布不均衡的問題:
- 異常檢測場景 大多數(shù)企業(yè)中的異常個案都是少量的,比如惡意刷單、黃牛訂單、信用卡欺詐、電力竊電、設備故障等,這些數(shù)據(jù)樣本所占的比例通常是整體樣本中很少的一部分,以信用卡欺詐為例,刷實體信用卡的欺詐比例一般都在0.1%以內。
- 客戶流失場景 大型企業(yè)的流失客戶相對于整體客戶通常是少量的,尤其對于具有壟斷地位的行業(yè)巨擘,例如電信、石油、網絡運營商等更是如此。
- 罕見事件的分析 罕見事件與異常檢測類似,都屬于發(fā)生個案較少;但不同點在于異常檢測通常都有是預先定義好的規(guī)則和邏輯,并且大多數(shù)異常事件都對會企業(yè)運營造成負面影響,因此針對異常事件的檢測和預防非常重要;但罕見事件則無法預判,并且也沒有明顯的積極和消極影響傾向。例如由于某網絡大V無意中轉發(fā)了企業(yè)的一條趣味廣告導致用戶流量明顯提升便屬于此類。
- 發(fā)生頻率低的事件 這種事件是預期或計劃性事件,但是發(fā)生頻率非常低。例如每年1次的雙11盛會一般都會產生較高的銷售額,但放到全年來看這一天的銷售額占比很可能只有1%不到,尤其對于很少參與活動的公司而言,這種情況更加明顯。這種屬于典型的低頻事件。
抽樣類別
抽樣是解決樣本分布不均衡相對簡單且常用的方法,包括過抽樣和欠抽樣兩種。
過抽樣
過抽樣(也叫上采樣、over-sampling)方法通過增加分類中少數(shù)類樣本的數(shù)量來實現(xiàn)樣本均衡,最直接的方法是簡單復制少數(shù)類樣本形成多條記錄,這種方法的缺點是如果樣本特征少而可能導致過擬合的問題;經過改進的過抽樣方法通過在少數(shù)類中加入隨機噪聲、干擾數(shù)據(jù)或通過一定規(guī)則產生新的合成樣本,例如SMOTE算法。
欠抽樣
欠抽樣(也叫下采樣、under-sampling)方法通過減少分類中多數(shù)類樣本的樣本數(shù)量來實現(xiàn)樣本均衡,最直接的方法是隨機地去掉一些多數(shù)類樣本來減小多數(shù)類的規(guī)模,缺點是會丟失多數(shù)類樣本中的一些重要信息。
總體上,過抽樣和欠抽樣更適合大數(shù)據(jù)分布不均衡的情況,尤其是第一種(過抽樣)方法應用更加廣泛。
實現(xiàn)方式
本文中使用開放的微博4種情緒數(shù)據(jù)集 simplifyweibo_4_modes.csv 作為樣本數(shù)據(jù)進行數(shù)據(jù)處理操作,其中所有的預操作如下:
import sys
import os
import pandas as pd
import jieba
from keras_preprocessing.sequence import pad_sequences
# keras 里類似
from preprocessing.text import Tokenizer
# 模型評價工具
from sklearn import metrics
# xgboost / lightgbm 模型
import xgboost as xgb
import lightgbm as lbm
# 多模型投票
from sklearn.ensemble import VotingClassifier
from collections import Counter
# 重采樣
from imblearn.under_sampling import RandomUnderSampler
sys.path.extend([os.path.dirname(os.getcwd())])
dataset = pd.read_csv("data/samples/simplifyweibo_4_moods.csv", header=0)
moods = {0: '喜悅', 1: '憤怒', 2: '厭惡', 3: '低落'}
x = dataset['review'].values
def transform(a):
return moods[a]
# 將數(shù)字標簽轉為文字標簽
y = dataset['label'].apply(transform).values
# 進行結巴分詞并添加 padding
data_tokenizer = Tokenizer(split=jieba.cut)
data_tokenizer.fit_on_texts(x)
data_seq = data_tokenizer.texts_to_sequences(x)
data_seq = pad_sequences(data_seq, maxlen=200)
1. SMOTE 抽樣
print('Original dataset shape %s' % Counter(y))
# 建立 SMOTE模型
smote = SMOTE()
# 對x和y過抽樣處理后的數(shù)據(jù)集,將兩份數(shù)據(jù)集轉換為數(shù)據(jù)框然后合并為一個整體數(shù)據(jù)框
data_seq, y = smote.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# 建立 SMOTE + ENN
smote = SMOTEENN()
# 對x和y過抽樣處理后的數(shù)據(jù)集,將兩份數(shù)據(jù)集轉換為數(shù)據(jù)框然后合并為一個整體數(shù)據(jù)框
data_seq, y = smote.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# 建立 SMOTE + Tomek
smote = SMOTETomek()
# 對x和y過抽樣處理后的數(shù)據(jù)集,將兩份數(shù)據(jù)集轉換為數(shù)據(jù)框然后合并為一個整體數(shù)據(jù)框
data_seq, y = smote.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
過抽樣方法通過在少數(shù)類中加入隨機噪聲、干擾數(shù)據(jù)或通過一定規(guī)則產生新的合成樣本。
2. 隨機抽樣
print('Original dataset shape %s' % Counter(y))
# 數(shù)據(jù)樣本會以最小樣本數(shù)在多樣本中進行隨機采樣
rus = RandomUnderSampler()
data_seq, y = rus.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# 隨機選取數(shù)據(jù)的子集
ros = RandomOverSampler(random_state=0)
data_seq, y = ros.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
經過 RandomUnderSampler 重采樣之后,。
3. ADASYN 抽樣
from imblearn.over_sampling import ADASYN
print('Original dataset shape %s' % Counter(y))
adasyn = ADASYN()
data_seq, y = adasyn.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
注 SMOTE 算法與 ADASYN 都是基于同樣的算法來合成新的少數(shù)類樣本
4. 原型生成
from imblearn.under_sampling import ClusterCentroids
print('Original dataset shape %s' % Counter(y))
cc = ClusterCentroids(random_state=0)
data_seq, y = cc.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
注 每一個類別的樣本都會用K-Means算法的中心點來進行合成, 而不是隨機從原始樣本進行抽取.
5. 最近鄰算法下采樣
應用最近鄰算法來編輯(edit)數(shù)據(jù)集, 找出那些與鄰居不太友好的樣本然后移除. 對于每一個要進行下采樣的樣本, 那些不滿足一些準則的樣本將會被移除; 他們的絕大多數(shù)(kind_sel='mode')或者全部(kind_sel='all')的近鄰樣本都屬于同一個類, 這些樣本會被保留在數(shù)據(jù)集中.
from imblearn.under_sampling import EditedNearestNeighbours
from imblearn.under_sampling import RepeatedEditedNearestNeighbours
from imblearn.under_sampling import AllKNN
print('Original dataset shape %s' % Counter(y))
enn = EditedNearestNeighbours(random_state=0)
data_seq, y = enn.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# 多次執(zhí)行 EditedNearestNeighbours
renn = RepeatedEditedNearestNeighbours(random_state=0)
data_seq, y = renn.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# ALLKNN算法在進行每次迭代的時候, 最近鄰的數(shù)量都在增加
allknn = AllKNN(random_state=0)
data_seq, y = allknn.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
# KMeans + SMOTE
sm = KMeansSMOTE(random_state=0)
data_seq, y = sm.fit_resample(data_seq, y)
print('resampled dataset shape %s' % Counter(y))
構建不均衡樣本
from sklearn.datasets import load_iris
from imblearn.datasets import make_imbalance
iris = load_iris()
# 指定數(shù)量生成
ratio = {0: 20, 1: 30, 2: 40}
x_imb, y_imb = make_imbalance(iris.data, iris.target, sampling_strategy=ratio)
# Out[37]: [(0, 20), (1, 30), (2, 40)]
sorted(Counter(y_imb).items())
# 當類別不指定時, 所有的數(shù)據(jù)集均導入
ratio = {0: 10}
x_imb, y_imb = make_imbalance(iris.data, iris.target, sampling_strategy=ratio)
# Out[38]: [(0, 10), (1, 50), (2, 50)]
sorted(Counter(y_imb).items())
# 同樣亦可以傳入自定義的比例函數(shù)
def ratio_multiplier(y):
multiplier = {0: 0.5, 1: 0.7, 2: 0.95}
target_stats = Counter(y)
for key, value in target_stats.items():
target_stats[key] = int(value * multiplier[key])
return target_stats
x_imb, y_imb = make_imbalance(iris.data, iris.target, sampling_strategy=ratio_multiplier)
# Out[39]: [(0, 25), (1, 35), (2, 47)]
sorted(Counter(y_imb).items())
常見問題
- 安裝
imblearn包之后,默認會更新sklearn包,這時候會導致sklearn2pmml報如下錯誤:
Standard output is empty
Standard error:
Apr 15, 2020 9:21:53 AM org.jpmml.sklearn.Main run
INFO: Parsing PKL..
Apr 15, 2020 9:21:53 AM org.jpmml.sklearn.Main run
INFO: Parsed PKL in 17 ms.
Apr 15, 2020 9:21:53 AM org.jpmml.sklearn.Main run
INFO: Converting..
Apr 15, 2020 9:21:53 AM org.jpmml.sklearn.Main run
SEVERE: Failed to convert
java.lang.IllegalArgumentException: The transformer object (Python class sklearn.ensemble._voting.VotingClassifier) is not a supported Transformer
at org.jpmml.sklearn.CastFunction.apply(CastFunction.java:43)
at sklearn.pipeline.Pipeline$1.apply(Pipeline.java:121)
at sklearn.pipeline.Pipeline$1.apply(Pipeline.java:112)
at com.google.common.collect.Lists$TransformingRandomAccessList.get(Lists.java:599)
at sklearn.TransformerUtil.getHead(TransformerUtil.java:35)
at sklearn2pmml.pipeline.PMMLPipeline.encodePMML(PMMLPipeline.java:189)
at org.jpmml.sklearn.Main.run(Main.java:145)
at org.jpmml.sklearn.Main.main(Main.java:94)
Caused by: java.lang.ClassCastException: Cannot cast net.razorvine.pickle.objects.ClassDict to sklearn.Transformer
at java.lang.Class.cast(Class.java:3369)
at org.jpmml.sklearn.CastFunction.apply(CastFunction.java:41)
... 7 more
Exception in thread "main" java.lang.IllegalArgumentException: The transformer object (Python class sklearn.ensemble._voting.VotingClassifier) is not a supported Transformer
at org.jpmml.sklearn.CastFunction.apply(CastFunction.java:43)
at sklearn.pipeline.Pipeline$1.apply(Pipeline.java:121)
at sklearn.pipeline.Pipeline$1.apply(Pipeline.java:112)
at com.google.common.collect.Lists$TransformingRandomAccessList.get(Lists.java:599)
at sklearn.TransformerUtil.getHead(TransformerUtil.java:35)
at sklearn2pmml.pipeline.PMMLPipeline.encodePMML(PMMLPipeline.java:189)
at org.jpmml.sklearn.Main.run(Main.java:145)
at org.jpmml.sklearn.Main.main(Main.java:94)
Caused by: java.lang.ClassCastException: Cannot cast net.razorvine.pickle.objects.ClassDict to sklearn.Transformer
at java.lang.Class.cast(Class.java:3369)
at org.jpmml.sklearn.CastFunction.apply(CastFunction.java:41)
... 7 more
更新 sklearn 版本即可 pip install --upgrade git+https://github.com/jpmml/sklearn2pmml.git