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

18 KiB

Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks:

Tipos básicos de dados possíveis

Os dados podem ser contínuos (infinitos valores) ou categóricos (nominais) onde a quantidade de valores possíveis é limitada.

Tipos Categóricos

Binário

Apenas 2 valores possíveis: 1 ou 0. No caso de um conjunto de dados onde os valores estão em formato de string (por exemplo, "True" e "False"), você atribui números a esses valores com:

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

Ordinal

Os valores seguem uma ordem, como em: 1º lugar, 2º lugar... Se as categorias forem strings (como: "iniciante", "amador", "profissional", "especialista"), você pode mapeá-las para números como vimos no caso binário.

column2_mapping = {'starter':0,'amateur':1,'professional':2,'expert':3}
dataset['column2'] = dataset.column2.map(column2_mapping)
  • Para colunas alfabéticas você pode ordená-las mais facilmente:
# 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)

Cíclico

Parece como valor ordinal porque há uma ordem, mas isso não significa que um é maior que o outro. Além disso, a distância entre eles depende da direção que você está contando. Exemplo: Os dias da semana, domingo não é "maior" que segunda-feira.

  • Existem diferentes maneiras de codificar características cíclicas, algumas podem funcionar apenas com alguns algoritmos. Em geral, a codificação dummy pode ser usada
column2_dummies = pd.get_dummies(dataset.column2, drop_first=True)
dataset_joined = pd.concat([dataset[['column2']], column2_dummies], axis=1)

Datas

Datas são variáveis contínuas. Podem ser vistas como cíclicas (porque se repetem) ou como variáveis ordinais (porque um momento é maior que o anterior).

  • Geralmente, datas são usadas como índice
# 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())

Multi-categoria/nominal

Mais de 2 categorias sem ordem relacionada. Use dataset.describe(include='all') para obter informações sobre as categorias de cada recurso.

  • Uma string de referência é uma coluna que identifica um exemplo (como o nome de uma pessoa). Isso pode ser duplicado (porque 2 pessoas podem ter o mesmo nome), mas a maioria será única. Esses dados são inúteis e devem ser removidos.
  • Uma coluna chave é usada para vincular dados entre tabelas. Neste caso, os elementos são únicos. Esses dados são inúteis e devem ser removidos.

Para codificar colunas de multi-categoria em números (para que o algoritmo de ML as entenda), codificação dummy é usada (e não codificação one-hot porque não evita multicolinearidade perfeita).

Você pode obter uma coluna de multi-categoria codificada em one-hot com pd.get_dummies(dataset.column1). Isso transformará todas as classes em recursos binários, criando assim uma nova coluna por classe possível e atribuirá 1 valor Verdadeiro a uma coluna, e o restante será falso.

Você pode obter uma coluna de multi-categoria codificada em dummy com pd.get_dummies(dataset.column1, drop_first=True). Isso transformará todas as classes em recursos binários, criando assim uma nova coluna por classe possível menos uma, já que as últimas 2 colunas serão refletidas como "1" ou "0" na última coluna binária criada. Isso evitará multicolinearidade perfeita, reduzindo as relações entre colunas.

Colinear/Multicolinearidade

Colinear aparece quando 2 recursos estão relacionados um com o outro. Multicolinearidade aparece quando são mais de 2.

Em ML você quer que seus recursos estejam relacionados com os resultados possíveis, mas você não quer que eles estejam relacionados entre si. É por isso que a codificação dummy mistura as duas últimas colunas disso e é melhor do que a codificação one-hot, que não faz isso, criando uma relação clara entre todos os novos recursos da coluna de multi-categoria.

VIF é o Fator de Inflação de Variância que mede a multicolinearidade dos recursos. Um valor acima de 5 significa que um dos dois ou mais recursos colineares deve ser removido.

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))

Desequilíbrio Categórico

Isso ocorre quando não há a mesma quantidade de cada categoria nos dados de treinamento.

# 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())

Em um desequilíbrio, sempre há uma classe ou classes majoritárias e uma classe ou classes minoritárias.

Existem 2 maneiras principais de corrigir esse problema:

  • Undersampling: Remoção aleatória de dados da classe majoritária para que ela tenha o mesmo número de amostras que a classe minoritária.
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
  • Oversampling: Gerar mais dados para a classe minoritária até que ela tenha tantas amostras quanto a classe majoritária.
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

Você pode usar o argumento sampling_strategy para indicar a porcentagem que deseja subamostrar ou sobreamostrar (por padrão é 1 (100%), o que significa igualar o número de classes minoritárias com as classes majoritárias)

{% hint style="info" %} Subamostragem ou Sobreamostragem não são perfeitas se você obter estatísticas (com .describe()) dos dados sub/sobreamostrados e compará-los com os originais, você verá que eles mudaram. Portanto, sobreamostragem e subamostragem estão modificando os dados de treinamento. {% endhint %}

Sobreamostragem com SMOTE

SMOTE geralmente é uma maneira mais confiável de sobreamostrar os dados.

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

Categorias Raramente Ocorrentes

Imagine um conjunto de dados onde uma das classes alvo ocorre muito poucas vezes.

Isso é como o desequilíbrio de categoria da seção anterior, mas a categoria raramente ocorrente está ocorrendo ainda menos que a "classe minoritária" naquele caso. Os métodos de oversampling e undersampling puros também podem ser usados aqui, mas geralmente essas técnicas não fornecerão resultados realmente bons.

Pesos

Em alguns algoritmos é possível modificar os pesos dos dados alvo para que alguns deles tenham por padrão mais importância ao gerar o modelo.

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

Você pode misturar os pesos com técnicas de sobreamostragem/subamostragem para tentar melhorar os resultados.

PCA - Análise de Componentes Principais

É um método que ajuda a reduzir a dimensionalidade dos dados. Vai combinar diferentes características para reduzir a quantidade delas, gerando características mais úteis (menos computação é necessária).

As características resultantes não são compreensíveis por humanos, então também anonimiza os dados.

Categorias de Rótulos Incongruentes

Os dados podem ter erros devido a transformações mal-sucedidas ou simplesmente por erro humano ao escrever os dados.

Portanto, você pode encontrar o mesmo rótulo com erros de ortografia, diferentes capitalizações, abreviações como: BLUE, Blue, b, bule. Você precisa corrigir esses erros de rótulo dentro dos dados antes de treinar o modelo.

Você pode limpar esses problemas tornando tudo minúsculo e mapeando rótulos mal escritos para os corretos.

É muito importante verificar que todos os dados que você possui estão corretamente rotulados, porque, por exemplo, um erro de ortografia nos dados, ao codificar as classes em dummies, gerará uma nova coluna nas características finais com consequências ruins para o modelo final. Este exemplo pode ser detectado facilmente codificando uma coluna em one-hot e verificando os nomes das colunas criadas.

Dados Ausentes

Alguns dados do estudo podem estar ausentes.

Pode acontecer de alguns dados completamente aleatórios estarem ausentes por algum erro. Esse tipo de dado é Missing Completely at Random (MCAR).

Pode ser que alguns dados aleatórios estejam ausentes, mas há algo que torna mais provável que alguns detalhes específicos estejam ausentes, por exemplo, é mais frequente que homens revelem sua idade, mas não mulheres. Isso é chamado Missing at Random (MAR).

Finalmente, pode haver dados Missing Not at Random (MNAR). O valor dos dados está diretamente relacionado com a probabilidade de ter os dados. Por exemplo, se você quer medir algo embaraçoso, quanto mais embaraçosa for a pessoa, menos provável é que ela compartilhe isso.

As duas primeiras categorias de dados ausentes podem ser ignoráveis. Mas a terceira requer considerar apenas partes dos dados que não são impactadas ou tentar modelar os dados ausentes de alguma forma.

Uma maneira de descobrir sobre dados ausentes é usar a função .info(), pois ela indicará o número de linhas, mas também o número de valores por categoria. Se alguma categoria tem menos valores do que o número de linhas, então há alguns dados ausentes:

# Get info of the dataset
dataset.info()

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

Geralmente é recomendado que se um recurso estiver ausente em mais de 20% do conjunto de dados, a coluna deve ser removida:

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

{% hint style="info" %} Observe que nem todos os valores ausentes estão faltando no conjunto de dados. É possível que valores ausentes tenham sido atribuídos o valor "Desconhecido", "n/a", "", -1, 0... Você precisa verificar o conjunto de dados (usando dataset.columnname.valuecounts(dropna=False) para verificar os possíveis valores). {% endhint %}

Se alguns dados estão ausentes no conjunto de dados (e não são muitos), você precisa encontrar a categoria dos dados ausentes. Para isso, basicamente precisa saber se os dados ausentes são aleatórios ou não, e para isso você precisa descobrir se os dados ausentes estavam correlacionados com outros dados do conjunto de dados.

Para descobrir se um valor ausente está correlacionado com outra coluna, você pode criar uma nova coluna que coloque 1s e 0s se o dado está ausente ou não e então calcular a correlação entre eles:

# 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()

Se você decidir ignorar os dados ausentes, ainda precisará decidir o que fazer com eles: Você pode remover as linhas com dados ausentes (os dados de treino para o modelo serão menores), pode remover a característica completamente, ou poderia modelá-la.

Você deve verificar a correlação entre a característica ausente com a coluna alvo para ver quão importante essa característica é para o alvo, se for realmente pequena você pode descartá-la ou preenchê-la.

Para preencher dados contínuos ausentes, você poderia usar: a média, a mediana ou usar um algoritmo de imputação. O algoritmo de imputação pode tentar usar outras características para encontrar um valor para a característica ausente:

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

Para preencher dados categóricos, primeiro você precisa pensar se há algum motivo pelo qual os valores estão ausentes. Se for por escolha dos usuários (eles não quiseram fornecer os dados), talvez você possa criar uma nova categoria indicando isso. Se for devido a erro humano, você pode remover as linhas ou a característica (verifique os passos mencionados antes) ou preenchê-la com a moda, a categoria mais usada (não recomendado).

Combinando Características

Se você encontrar duas características que estão correlacionadas entre si, geralmente você deve descartar uma delas (aquela que é menos correlacionada com o alvo), mas você também pode tentar combiná-las e criar uma nova característica.

# 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)
Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks: