序言
在处理机器学习中的不平衡数据集时,SMOTE(Synthetic Minority Over-sampling Technique)和NearMiss算法是两种常用的技术。SMOTE通过合成少数类样本来增加其数量,从而平衡数据集,这种方法能够有效地缓解由数据不平衡导致的模型偏倚问题。而NearMiss算法则是一种基于距离的选择性采样技术,它从多数类中挑选与少数类最接近的样本,以此丰富模型对少数类的学习。这两种方法共同构成了处理不平衡数据集的有效工具,帮助提高分类模型的性能。
概述
在机器学习和数据科学中,我们经常会遇到一个称为“不平衡数据分布”的术语,通常发生在其中一个类的观测值远高于或低于其他类的时候。由于机器学习算法倾向于通过减少误差来提高准确性,因此它们不考虑类分布。这个问题在欺诈检测、异常检测、面部识别等示例中普遍存在。
决策树和逻辑回归等标准机器学习(Standard ML)技术偏向于多数类,并且倾向于忽略少数类。它们倾向于只预测多数类别,因此,与多数类别相比,少数类别的分类存在重大错误。用更专业的话来说,如果我们的数据数据集中的数据分布不平衡,那么我们的模型就更容易出现少数类的召回率可以忽略不计或非常少的情况。广泛用于处理不平衡数据技术:主要有两种算法。
- SMOTE算法
- NearMiss算法
SMOTE算法—过采样(Oversampling)
SMOTE(Synthetic Minority Oversampling Technique, 合成少数过采样技术)是解决不平衡问题最常用的过采样方法之一。它旨在通过复制随机增加少数阶级示例来平衡阶级分布。SMOTE 在现有少数群体实例之间合成新的少数群体实例。它通过线性插值为少数类生成虚拟训练记录。这些合成训练记录是通过为少数类中的每个示例随机选择一个或多个 k k k最近邻来生成的。在过采样过程之后,对数据进行重构,并可以对处理后的数据应用多个分类模型。更深入地了解SMOTE算法的工作原理!
- 步骤1 :设置少数类集合 A \text{A} A,对于每个 x ∈ A \boldsymbol{x} \in \text{A} x∈A。通过计算 x \boldsymbol{x} x 与集合 A \text{A} A中所有其他样本之间的欧几里得距离(Euclidean distance,又称欧氏距离)来获得 x \boldsymbol{x} x 的 k k k-最近邻。
- 步骤2:根据不平衡比例设置采样率 N \text{N} N。对于每个 x ∈ A \boldsymbol{x} \in \text{A} x∈A, N \text{N} N个示例(即 x 1 , x 2 , … , x n x_1,x_2,\dots,x_n x1,x2,…,xn)从其 k k k-最近邻中随机选择,它们构成集合 A 1 \text{A}_1 A1。
- 步骤3:对于每个示例 x k ∈ A 1 \boldsymbol{x}_k \in \text{A}_1 xk∈A1 ( k = 1 , 2 , 3 , … , N k=1,2,3,\dots,\text{N} k=1,2,3,…,N),以下公式用于生成一个新示例: x ′ = x + r a n d ( 0 , 1 ) ∗ ∣ x − x k ∣ \boldsymbol{x}^\prime=\boldsymbol{x}+rand(0,1) \ast |\boldsymbol{x}-\boldsymbol{x}_k| x′=x+rand(0,1)∗∣x−xk∣,其中 r a n d ( 0 , 1 ) rand(0,1) rand(0,1)表示0和1之间的随机数。
NearMiss算法—欠采样(Undersampling)
NearMiss是一种欠采样技术。它旨在通过随机消除多数类示例来平衡类分布。当两个不同类的实例彼此非常接近时,我们删除多数类的实例以增加两个类之间的空间。这有助于分类过程。为防止在大多数欠采样技术中的信息丢失问题,近邻方法被广泛使用。关于近邻法工作的基本直觉如下:
- 步骤1:首先,找出多数类的所有实例与少数类的实例之间的距离。这里,多数类将被欠采样。
- 步骤2:然后,名词选择与少数类中的实例具有最小距离的多数类的实例。
- 步骤3:如果少数类中有 k k k个实例,则最接近的方法将导致 k ∗ n k\ast n k∗n多数类的实例。
为了在多数类中找到
n
n
n个最接近的实例,有几种应用NearMiss算法的变体:
NearMiss–版本1:它选择到
k
k
k的平均距离的多数类的样本最近的少数类的实例最小。
NearMiss–版本2:它选择到
k
k
k的平均距离的多数类的样本最远的少数类的实例最小。
NearMiss–版本3:它分为两个步骤。首先,对于每个少数类实例,其M最近的邻居将被存储。最后,选择到
n
n
n个最近邻居的平均距离最大的多数类实例。
SMOTE和NearMiss区别
SMOTE算法
原理
- 基于K近邻算法,对少数类样本进行插值以创建新的合成样本,从而平衡数据集。
操作方式
- 对于每个少数类样本,计算其K个最近邻样本。
- 在两个随机选择的最近邻之间生成新样本。
优点
- 提高模型对少数类的识别能力,提升整体分类性能。
- 减轻过拟合风险,因为数据集得到了平衡。
- 解决数据分布不均问题,提高模型的鲁棒性。
缺点
容易受噪声干扰,生成的合成样本可能不准确。
增加计算复杂度,特别是处理大规模数据集时。
可能导致过拟合,因为生成的合成样本可能引入冗余信息。
NearMiss算法
原理
- 一种基于KNN算法的欠采样方案,对多数类样本进行选择性采样以达到数据平衡。
操作方式(以NearMiss-1为例)
- 对于每个多数类样本,选择与少数类样本最近的k个并计算这k个少数类样本的平均距离。
- 保留平均距离最小的多数类样本。
优点(基于一般理解)
- 选择性地从多数类中挑选代表性样本,降低计算复杂度。
- 通过减少多数类的样本数量,降低过拟合的可能性。
缺点(基于一般理解)
- 可能忽略重要样本,如果选择的代表性样本不够全面。
- 算法的效果可能受到数据分布特性的影响。
案例源码演示
# coding:utf-8
"""
使用SMOTE算法和NearMiss算法分别处理信用卡中不平衡数据
"""
import warnings
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.model_selection import train_test_split # 将数据拆分为测试集和训练集
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
from lightgbm import LGBMClassifier
from imblearn.over_sampling import SMOTE # SMOTE Over Sampling / "imblearn" means "Imbalanced Learn"
from imblearn.under_sampling import NearMiss
warnings.filterwarnings('ignore')
# 加载信用数据集
data = pd.read_csv('datas/creditcard.csv')
# print(data.head())
# print(data.info())
# print(data.shape)
# 规范化Amount列
# data['normAmount'] = StandardScaler().fit_transform(np.array(data['Amount']).reshape(-1, 1))
# 删除“时间”和“金额”列,因为它们与预测目的无关
# data = data.drop(['Time', 'Amount'], axis = 1)
# print(data)
# 从结果中,我们发现数据集是不平衡的,有284315笔正常交易,有492笔欺诈交易。
# print(data['Class'].value_counts() )
# 绘制柱状图1
# plt.figure(figsize=(8, 4))
# plt.xticks(range(0, 30000, 1000), rotation=60)
# sns.histplot(data['Amount'], bins=100, kde=True)
# 绘制柱状图2
# plt.figure(figsize=(8, 4))
# sns.histplot(np.log1p(data['Amount']), bins=50, kde=True)
# 热力图
# plt.figure(figsize=(8, 8))
# corr = data.corr()
# sns.heatmap(corr, cmap='RdBu')
# plt.show()
######################## Data Preprocessing ########################
def deal_with_outliers(df=None, column=None, weight=1.5):
"""
处理异常值的方法。
参数:
- df: pandas.DataFrame对象,包含需要分析的数据。
- column: 字符串,指定需要分析的列名。
- weight: 浮点数,用于扩展IQR的权重,默认为1.5。
返回:
- outlier_index: pandas.Index对象,包含被识别为异常值的行索引。
"""
# 提取指定类别的特定列数据
fraud = df[df['Class']==1][column]
# 计算第一四分位数和第三四分位数
quantile_25 = np.percentile(fraud.values, 25)
quantile_75 = np.percentile(fraud.values, 75)
# 计算IQR(四分位数范围)
iqr = quantile_75 - quantile_25
# 根据权重计算异常值检测范围
iqr_weight = iqr * weight
low = quantile_25 - iqr_weight
high = quantile_75 + iqr_weight
# 根据范围识别异常值的索引
outlier_index = fraud[(fraud < low) | (fraud > high)].index
return outlier_index
def preprocess_data(df=None):
"""
预处理数据函数,主要进行以下操作:
1. 对输入的DataFrame副本应用对数变换;
2. 删除不需要的列;
3. 处理异常值。
参数:
- df: pandas DataFrame对象,包含原始数据。默认为None,如果提供,则使用该数据进行处理。
返回值:
- df_copy: pandas DataFrame对象,已完成预处理的数据。
"""
df_copy = df.copy() # 创建数据的副本以避免修改原始数据
amount_n = np.log1p(df_copy['Amount']) # 对'Amount'列应用对数变换(+1之后的对数)
df_copy.insert(0, 'Amount_Scaled', amount_n) # 将变换后的'Amount'列插入到DataFrame的首列
df_copy.drop(columns=['Time', 'Amount'], axis=1, inplace=True) # 删除'Time'和'Amount'列
outlier_index = deal_with_outliers(df_copy, column="V14", weight=1.5) # 获取异常值的索引
df_copy.drop(outlier_index, axis=0, inplace=True) # 根据索引删除异常值
return df_copy # 返回预处理后的数据
def split_data(df=None):
"""
拆分数据集为训练集和测试集。
参数:
- df: pandas.DataFrame对象,待拆分的数据集,默认为None。
返回值:
- X_train: pandas.DataFrame对象,训练集的特征部分。
- X_test: pandas.DataFrame对象,测试集的特征部分。
- y_train: pandas.Series对象,训练集的目标变量。
- y_test: pandas.Series对象,测试集的目标变量。
"""
# 预处理数据
df_copy = preprocess_data(df)
# 特征提取
X_features = df_copy.iloc[:, :-1]
# 目标变量提取
y_target = df_copy.iloc[:, -1]
# 数据集拆分
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, random_state=0, stratify=y_target)
return X_train, X_test, y_train, y_test
def evaluate_data(y_test, pred=None, pred_proba=None):
"""
评估给定模型的预测性能。
参数:
y_test : array-like of shape (n_samples,) 或 (n_samples, n_classes)
实际标签值的测试集。
pred : array-like of shape (n_samples,) 或 (n_samples, n_classes)
预测标签值。如果未提供,则不计算混淆矩阵和ROC AUC分数。
pred_proba : array-like of shape (n_samples, n_classes)
预测每个类的概率。如果未提供,则不计算ROC AUC分数。
返回值:
无
"""
# 计算混淆矩阵
confusion = confusion_matrix(y_test, pred)
# 计算准确率
accuracy = accuracy_score(y_test, pred)
# 计算精确度
precision = precision_score(y_test, pred)
# 计算召回率
recall = recall_score(y_test, pred)
# 计算F1分数
f1 = f1_score(y_test, pred)
# 计算ROC AUC分数
roc_auc = roc_auc_score(y_test, pred_proba)
# 打印混淆矩阵
print('Confusion Matrix')
print(confusion)
print()
# 打印各项性能指标
print('Accuracy: {0:.4f}, Precision: {1:.4f}, Recall: {2:.4f}, F1: {3:.4f}, AUC: {4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
# 训练模型
def train_model(model, ftr_train=None, ftr_test=None, tgt_train=None, tgt_test=None):
"""
训练模型并进行预测与评估。
参数:
- model: 要训练的模型对象。
- ftr_train: 训练集的特征数据,默认为None。
- ftr_test: 测试集的特征数据,默认为None。
- tgt_train: 训练集的目标数据,默认为None。
- tgt_test: 测试集的目标数据,默认为None。
返回值:
无。函数直接在给定的模型上进行训练,并对测试集进行预测与评估,不返回任何结果。
"""
# 训练模型
model.fit(ftr_train, tgt_train)
# 使用训练好的模型对测试集进行预测
pred = model.predict(ftr_test)
# 进一步预测测试集的类别概率
pred_proba = model.predict_proba(ftr_test)[:, 1]
# 对预测结果进行评估
evaluate_data(tgt_test, pred, pred_proba)
model.fit(ftr_train, tgt_train)
pred = model.predict(ftr_test)
pred_proba = model.predict_proba(ftr_test)[:, 1]
evaluate_data(tgt_test, pred, pred_proba)
X_train, X_test, y_train, y_test = split_data(data)
print("Number transactions X_train dataset: ", X_train.shape)
print("Number transactions y_train dataset: ", y_train.shape)
print("Number transactions X_test dataset: ", X_test.shape)
print("Number transactions y_test dataset: ", y_test.shape)
# lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, verbosity=-1, boost_from_average=False)
# I higly recommend to use "boost_from_average=False" when a dataset is extremely imbalanced.
# train_model(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
# smote = SMOTE(random_state=0)
# X_train_over, y_train_over = smote.fit_resample(X_train, y_train)
# print("Before SMOTE: ", X_train.shape, y_train.shape)
# print("After SMOTE: ", X_train_over.shape, y_train_over.shape)
# print("After SMOTE Label Distribution: ", pd.Series(y_train_over).value_counts())
# lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, verbosity=-1, boost_from_average=False)
# train_model(lgbm_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)
# 在不处理不平衡的类部分下,训练模型
def dont_deal_with_imbalanced_data(df=None):
"""
处理不平衡数据集的函数:在不考虑类不平衡的情况下训练逻辑回归模型。
参数:
- df: pandas.DataFrame, 输入的数据集,默认为None。如果提供,数据集将被用于训练和测试模型。
返回值:
- 无返回值,函数主要打印模型的分类报告。
"""
# 数据分割:将数据集分为训练集和测试集
X_train, X_test, y_train, y_test = split_data(data)
# 初始化逻辑回归模型
lr = LogisticRegression()
# 训练逻辑回归模型
lr.fit(X_train, y_train.ravel())
# 使用训练好的模型进行预测
predictions = lr.predict(X_test)
# 打印预测结果的分类报告
print(classification_report(y_test, predictions))
# 使用SMOTE算法处理不平衡数据后,训练模型
def deal_with_imbalanced_data_using_over_sampling_alg(df=None):
"""
使用SMOTE算法处理不平衡数据,通过过采样技术调整数据集,并训练逻辑回归模型。
参数:
df: pandas.DataFrame, 输入的数据集,默认为None。如果提供,将对此数据集进行处理。
返回值:
无返回值,该函数直接打印处理前后的数据统计信息,训练模型及预测结果的报告。
"""
# 打印训练数据集中标签为1和0的数量
print("Before OverSampling, counts of label '1': {}".format(sum(y_train == 1)))
print("Before OverSampling, counts of label '0': {} \n".format(sum(y_train == 0)))
# 初始化SMOTE对象,用于过采样处理
sm = SMOTE(random_state = 2)
# 对训练数据进行过采样处理
X_train_res, y_train_res = sm.fit_resample(X_train, y_train.ravel())
# 打印过采样处理后的数据集形状及标签数量
print('After OverSampling, the shape of train_X: {}'.format(X_train_res.shape))
print('After OverSampling, the shape of train_y: {} \n'.format(y_train_res.shape))
print("After OverSampling, counts of label '1': {}".format(sum(y_train_res == 1)))
print("After OverSampling, counts of label '0': {}".format(sum(y_train_res == 0)))
# 训练逻辑回归模型,并对测试集进行预测
lr = LogisticRegression()
lr.fit(X_train_res, y_train_res.ravel())
predictions = lr.predict(X_test)
# 打印预测结果的分类报告
print(classification_report(y_test, predictions))
# 使用NearMiss算法处理不平衡数据后,训练模型
def deal_with_imbalanced_data_using_under_sampling_alg(df=None):
"""
使用NearMiss算法处理不平衡数据集,通过下采样方法调整样本数量。
参数:
df: pandas.DataFrame, 输入的数据集,默认为None。如果提供,将对此数据集进行处理。
返回值:
无返回值,该函数主要输出处理前后的数据统计信息和数据形状。
"""
# 打印处理前标签为'1'和'0'的样本数量
print("Before Undersampling, counts of label '1': {}".format(sum(y_train == 1)))
print("Before Undersampling, counts of label '0': {} \n".format(sum(y_train == 0)))
# 初始化NearMiss算法对象
nr = NearMiss()
# 应用NearMiss算法对训练数据进行下采样
X_train_miss, y_train_miss = nr.fit_resample(X_train, y_train.ravel())
# 打印下采样后训练数据的形状
print('After Undersampling, the shape of train_X: {}'.format(X_train_miss.shape))
print('After Undersampling, the shape of train_y: {} \n'.format(y_train_miss.shape))
# 打印下采样后标签为'1'和'0'的样本数量,比较与处理前的变化
print("After Undersampling, counts of label '1': {}".format(sum(y_train_miss == 1)))
print("After Undersampling, counts of label '0': {}".format(sum(y_train_miss == 0)))
# 训练逻辑回归模型,并使用该模型对测试集进行预测
lr = LogisticRegression()
lr.fit(X_train_miss, y_train_miss.ravel())
predictions = lr.predict(X_test)
# 打印预测结果的详细报告,包括精度、召回率和F1-score等指标
print(classification_report(y_test, predictions))
dont_deal_with_imbalanced_data(data)
deal_with_imbalanced_data_using_over_sampling_alg(data)
deal_with_imbalanced_data_using_under_sampling_alg(data)
# 输出(略)
总结
使用SMOTE算法和NearMiss算法处理不平衡数据是机器学习领域的重要策略。SMOTE通过生成少数类样本以平衡数据集,而NearMiss则选择性地从多数类中挑选代表性样本。这两种方法都能有效缓解数据不平衡导致的模型偏倚问题,提高分类器的性能。综合应用这两种算法,可以更全面地处理不平衡数据,为机器学习模型的训练提供更为均衡和有效的数据集。