hacktricks/a.i.-exploiting/bra.i.nsmasher-presentation/ml-basics/feature-engineering.md

15 KiB
Raw Blame History

从零开始学习AWS黑客技术成为专家 htARTEHackTricks AWS红队专家

其他支持HackTricks的方式

可能数据的基本类型

数据可以是连续的无限个值)或分类的(名义),其中可能的值的数量是有限的。

分类类型

二元

只有2个可能的值1或0。如果数据集中的值是字符串格式例如"True"和"False"),您可以为这些值分配数字:

dataset["column2"] = dataset.column2.map({"T": 1, "F": 0})

序数

数值遵循一定顺序比如第1名第2名... 如果类别是字符串(比如:"初学者""业余爱好者""专业人士""专家"),你可以将它们映射为数字,就像我们在二进制情况下看到的那样。

column2_mapping = {'starter':0,'amateur':1,'professional':2,'expert':3}
dataset['column2'] = dataset.column2.map(column2_mapping)
  • 对于字母列,您可以更轻松地对其进行排序:
# First get all the uniq values alphabetically sorted
possible_values_sorted = dataset.column2.sort_values().unique().tolist()
# Assign each one a value
possible_values_mapping = {value:idx for idx,value in enumerate(possible_values_sorted)}
dataset['column2'] = dataset.column2.map(possible_values_mapping)

循环的

看起来像序数值因为有一个顺序,但这并不意味着一个比另一个大。另外,它们之间的距离取决于你计数的方向。例如:一周中的每一天,星期日并不比星期一“大”。

  • 有不同的方法来编码循环特征,有些可能只适用于某些算法。通常情况下,可以使用虚拟编码
column2_dummies = pd.get_dummies(dataset.column2, drop_first=True)
dataset_joined = pd.concat([dataset[['column2']], column2_dummies], axis=1)

日期

日期是连续变量。可以被视为循环的(因为它们重复)作为序数变量(因为一个时间比前一个时间大)。

  • 通常日期被用作索引
# Transform dates to datetime
dataset["column_date"] = pd.to_datetime(dataset.column_date)
# Make the date feature the index
dataset.set_index('column_date', inplace=True)
print(dataset.head())

# Sum usage column per day
daily_sum = dataset.groupby(df_daily_usage.index.date).agg({'usage':['sum']})
# Flatten and rename usage column
daily_sum.columns = daily_sum.columns.get_level_values(0)
daily_sum.columns = ['daily_usage']
print(daily_sum.head())

# Fill days with 0 usage
idx = pd.date_range('2020-01-01', '2020-12-31')
daily_sum.index = pd.DatetimeIndex(daily_sum.index)
df_filled = daily_sum.reindex(idx, fill_value=0) # Fill missing values


# Get day of the week, Monday=0, Sunday=6, and week days names
dataset['DoW'] = dataset.transaction_date.dt.dayofweek
# do the same in a different way
dataset['weekday'] = dataset.transaction_date.dt.weekday
# get day names
dataset['day_name'] = dataset.transaction_date.apply(lambda x: x.day_name())

多类别/名义

超过2个类别,没有相关顺序。使用 dataset.describe(include='all') 获取每个特征的类别信息。

  • 引用字符串标识示例的列(如人名)。这可能是重复的(因为两个人可能有相同的名字),但大多数是唯一的。这些数据是无用的,应该被移除
  • 关键列用于链接表格之间的数据。在这种情况下,元素是唯一的。这些数据是无用的,应该被移除

为了将多类别列编码为数字(以便机器学习算法理解它们),使用虚拟编码(而不是独热编码,因为它不能避免完美的多重共线性)。

您可以使用 pd.get_dummies(dataset.column1) 对多类别列进行独热编码。这将把所有类别转换为二进制特征,因此将为每个可能的类别创建一个新列,并将一个True值分配给一个列其余将为false。

您可以使用 pd.get_dummies(dataset.column1, drop_first=True) 对多类别列进行虚拟编码。这将把所有类别转换为二进制特征,因此将为每个可能的类别创建一个新列减去一个,因为最后两列将在最后创建的二进制列中反映为"1"或"0"。这将避免完美的多重共线性,减少列之间的关系。

共线性/多重共线性

2个特征彼此相关时出现共线性。当这些特征超过2个时出现多重共线性。

在机器学习中,您希望您的特征与可能的结果相关,但不希望它们之间相关。这就是为什么虚拟编码混合最后两列,比独热编码更好,后者不会创建清晰的关系,而是创建了来自多类别列的所有新特征之间的明显关系。

VIF是方差膨胀因子,用于衡量特征的多重共线性。值大于5意味着应该移除两个或多个共线特征中的一个

from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant

#dummies_encoded = pd.get_dummies(dataset.column1, drop_first=True)
onehot_encoded = pd.get_dummies(dataset.column1)
X = add_constant(onehot_encoded) # Add previously one-hot encoded data
print(pd.Series([variance_inflation_factor(X.values,i) for i in range(X.shape[1])], index=X.columns))

类别不平衡

这种情况发生在训练数据中每个类别的数量不相同时。

# Get statistic of the features
print(dataset.describe(include='all'))
# Get an overview of the features
print(dataset.info())
# Get imbalance information of the target column
print(dataset.target_column.value_counts())

在不平衡的情况下,总会存在多数类别少数类别

解决这个问题有两种主要方法:

  • 欠采样:从多数类别中随机删除数据,使其样本数量与少数类别相同。
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUserSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_under, y_under = rus.fit_resample(X,y)
print(y_under.value_counts()) #Confirm data isn't imbalanced anymore
  • 过采样:为少数类生成更多数据,直到其样本数量与多数类相同。
from imblearn.under_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_over, y_over = ros.fit_resample(X,y)
print(y_over.value_counts()) #Confirm data isn't imbalanced anymore

您可以使用参数**sampling_strategy来指示您想要欠采样或过采样的百分比**默认为1100%,这意味着将少数类的数量与多数类相等)

{% hint style="info" %} 如果您获取过/欠采样数据的统计信息(使用.describe()),并将其与原始数据进行比较,您会发现它们已经改变。因此,过采样和欠采样会修改训练数据。 {% endhint %}

SMOTE 过采样

SMOTE通常是一种更可靠的过采样数据的方法

from imblearn.over_sampling import SMOTE

# Form SMOTE the target_column need to be numeric, map it if necessary
smote = SMOTE(random_state=1337)
X_smote, y_smote = smote.fit_resample(dataset[['column1', 'column2', 'column3']], dataset.target_column)
dataset_smote = pd.DataFrame(X_smote, columns=['column1', 'column2', 'column3'])
dataset['target_column'] = y_smote
print(y_smote.value_counts()) #Confirm data isn't imbalanced anymore

很少出现的类别

想象一下一个数据集,其中一个目标类别出现的次数非常少

这就像前一节中的类别不平衡问题,但是很少出现的类别甚至比那种情况下的“少数类”出现的次数还要少。原始过采样欠采样方法也可以在这里使用,但通常这些技术不会给出真正好的结果

权重

在一些算法中,可以修改目标数据的权重,这样在生成模型时,一些数据会默认获得更多的重要性。

weights = {0: 10 1:1} #Assign weight 10 to False and 1 to True
model = LogisticRegression(class_weight=weights)

主成分分析PCA

是一种有助于减少数据维度的方法。它将结合不同特征减少它们的数量,生成更有用的特征需要更少的计算)。

生成的特征对人类来说是不可理解的,因此它还对数据进行匿名化

不一致的标签类别

数据可能存在错误,可能是由于转换失败,也可能是由于人为错误。

因此,您可能会发现相同标签存在拼写错误不同大小写缩写例如_BLUEBluebbule。在训练模型之前您需要修复数据中的这些标签错误。

您可以通过将所有内容转换为小写并将拼写错误的标签映射到正确的标签来清理这些问题。

非常重要的是要检查您拥有的所有数据是否被正确标记,因为例如,数据中的一个拼写错误,在对类别进行虚拟编码时,将在最终特征中生成一个新列,对最终模型会产生不良后果。通过对一个列进行独热编码并检查所创建列的名称,可以非常容易地检测到这种情况。

缺失数据

研究中可能会缺少一些数据。

可能会发生一些完全随机的数据丢失,这是一种完全随机缺失MCAR)的情况。

也可能是一些随机数据丢失,但有一些因素使得某些特定细节更有可能丢失,例如更频繁地男性会告诉他们的年龄而女性不会。这被称为随机缺失MAR)。

最后,可能存在非随机缺失MNAR)的数据。数据的值与拥有数据的概率直接相关。例如,如果您想测量某些令人尴尬的事情,某人越尴尬,他分享的可能性就越小。

前两类缺失数据可以被忽略。但第三类需要考虑只有部分未受影响的数据或尝试以某种方式对缺失数据进行建模

发现缺失数据的一种方法是使用.info()函数,因为它将指示每个类别的值的数量。如果某个类别的值少于行数,则存在一些数据缺失:

# Get info of the dataset
dataset.info()

# Drop all rows where some value is missing
dataset.dropna(how='any', axis=0).info()

通常建议,如果数据集中超过20%的数据缺失,则应删除该列:

# Remove column
dataset.drop('Column_name', axis='columns', inplace=True)
dataset.info()

{% hint style="info" %} 请注意,数据集中并非所有缺失值都是缺失的。可能缺失值已被赋予值为"Unknown"、"n/a"、""、-1、0等。您需要检查数据集使用dataset.column_name.value_counts(dropna=False)来检查可能的值)。 {% endhint %}

如果数据集中有一些数据缺失(而不是太多),您需要找到缺失数据的类别。基本上,您需要知道缺失数据是否是随机的,为此,您需要找出缺失数据是否与数据集的其他数据相关

要找出缺失值是否与另一列相关联您可以创建一个新列如果数据缺失或不缺失则将1和0放入该列然后计算它们之间的相关性

# The closer it's to 1 or -1 the more correlated the data is
# Note that columns are always perfectly correlated with themselves.
dataset[['column_name', 'cloumn_missing_data']].corr()

如果您决定忽略缺失数据,仍然需要处理它:您可以删除具有缺失数据的行(模型的训练数据将更少),您可以完全删除该特征,或者可以对其进行建模

您应该检查缺失特征与目标列之间的相关性,以查看该特征对目标的重要性如何,如果确实很小,您可以删除它或填充它

要填补缺失的连续数据,您可以使用:均值中位数或使用填充算法。填充算法可以尝试使用其他特征来找到缺失特征的值:

from sklearn.impute import KNNImputer

X = dataset[['column1', 'column2', 'column3']]
y = dataset.column_target

# Create the imputer that will fill the data
imputer = KNNImputer(n_neightbors=2, weights='uniform')
X_imp = imputer.fit_transform(X)

# Check new data
dataset_imp = pd.DataFrame(X_imp)
dataset.columns = ['column1', 'column2', 'column3']
dataset.iloc[10:20] # Get some indexes that contained empty data before

为了填补分类数据,首先需要考虑数值缺失的原因。如果是用户选择(他们不想提供数据),也许可以创建一个新类别来指示这一点。如果是因为人为错误,可以删除行特征(在之前提到的步骤中检查),或者用众数填充,即最常用的类别(不建议)。

合并特征

如果发现两个特征之间相关,通常应该删除其中一个(与目标相关性较低的那个),但也可以尝试将它们合并并创建一个新特征

# Create a new feautr combining feature1 and feature2
dataset['new_feature'] = dataset.column1/dataset.column2

# Check correlation with target column
dataset[['new_feature', 'column1', 'column2', 'target']].corr()['target'][:]

# Check for collinearity of the 2 features and the new one
X = add_constant(dataset[['column1', 'column2', 'target']])
# Calculate VIF
pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)
从零开始学习AWS黑客技术成为专家 htARTEHackTricks AWS红队专家

其他支持HackTricks的方式