T'SPACE

다채로운 에디터들의 이야기

컴퓨터공학/LG Aimers

[LG Aimers] 해카톤 후기, 코드 분석

Tonny Kang 2024. 3. 6. 15:14
반응형

*LG Aimers 문제를 공개하면 문제가 있을까봐 간소하게 설명 합니다

 

문제


Train.csv 파일과

Submission.csv 파일을 

두개 준다

 

Train 파일로 학습을 시켜 True, False를 판단해야하고

Submission에 주어진 정보로 True, False를 판단해 제출하면 채점을 하게 된다

 

1. Column, 필드(파라미터)가 엄청 많다

2. 결측값(Null Data, Missing Data)이 상당하다

3. 데이터의 질(오타, 형식)등이 고르지 않다

728x90

코드


1. 데이터 셋 읽어오기

df_train = pd.read_csv("train.csv") # 학습용 데이터
df_test = pd.read_csv("submission.csv") # 테스트 데이터(제출파일의 데이터)

column_to_drop = ['customer_country.1',  'ver_pro', 'business_subarea',"product_subcategory", "product_modelname", 
                  "com_reg_ver_win_rate",
                 "historical_existing_cnt",
                  "ver_win_ratio_per_bu",
                  "ver_win_rate_x",
                  "business_area","expected_timeline"]
df_train['customer_country'] = df_train['customer_country'].astype(str).apply(lambda x:x.split("/")[-1])
df_test['customer_country'] = df_test['customer_country'].astype(str).apply(lambda x:x.split("/")[-1])
df_train.drop(columns=column_to_drop, inplace=True)
df_test.drop(columns=column_to_drop, inplace=True)

# Assuming your DataFrame is named df_train
missing_values = df_train.isnull().sum()

# Display columns with missing values
print("Columns with missing values:")
print(missing_values[missing_values > 0])

 

 "Curse of Dimensionality"

차원의 저주이다

Richard E. Bellman이 처음 이용한 용어로

 

생각만 으로는 , T/F를 판별할 수 있는 정보를 많이 줄수록 더 정확하게 판별할 수 있다

 

예를 들어, 어떤 사람이 대학생인지 아닌지를 판단하려한다고 보자

나이를 파라미터로 활용해 판단하면 어느정도 높은 정확도로 예측할 것이다

거기에 수익이라는 파라미터가 추가되고, 자차 보유 여부 등등이 추가되면 

더 정확하게 예측 할 수 있을 것이다

 

그러나 인공지능 모델은 그렇게 생각하지 않는다

오히려 더 많은 정보들이 줄수록 추가 정보가 아닌 Noise에 가깝다 

이 말이 뜻을 하는 것은 

데이터의 차원이 증가할 수록 공간이 지수적으로 증가해 Data가 Sparse 해져서

대부분의 인공지능이 활용하는 비슷한 특징을가진 Data들을 제대로 구별하고 연결짓지 못해 성능이 떨어지는 현상이다

 

그래서

1. 결측치가 너무 많은 열

2. 결측값을 채우지 못한 열

3. 관련없다고 본인이 판단한 열

들은 그냥 삭제 했다 .drop()을 통해

 

그리고 전처리로 국적에 관한 열을 간소화 시켰다

 

ex)

Gimhae/Gyeongnam/SouthKorea -> SouthKorea

Seoul/Seoul/SouthKorea -> SouthKorea

그리고 마지막 부분에는 각 열에 결측값의 수를 알기위해 출력했다

반응형

2. True와 False의 비율을 확인하기

import plotly.express as px
labels=["Not Converted","Converted"]

converted_or_not = df_train['is_converted'].value_counts().tolist()
values = [converted_or_not[0], converted_or_not[1]]

fig = px.pie(values=df_train['is_converted'].value_counts(), names=labels , width=700, height=400, color_discrete_sequence=["skyblue","black"]
             ,title="Converted vs Non Converted")
fig.show()

# Assuming 'is_converted' is the name of the column
counts = df_train['is_converted'].value_counts()

# Count of True values
true_count = counts.loc[True]

# Count of False values
false_count = counts.loc[False]

print("Number of True values in 'is_converted' column:", true_count)
print("Number of False values in 'is_converted' column:", false_count)

 

출력을 해보니 아래와 같이 나왔다


UnderSampling & OverSampling

 

샘플링을 할 때 비율을 적절하게 뽑는것이 좋다

그러기 위해 위와 같은 비대칭적인 데이터를 샘플링할 때 활욜 할 수 있는 기법들이 있다

 

UnderSampling: 비율이 압도적이게 많은 데이터를 적게 뽑는 샘플링 방법

OverSampling: 비율이 압도적이게 적은 데이터를 많이 뽑는 샘플링 방법 (데이터 수가 부족하니 Synthesize (합성) 하기도 함)

SMALL

3. 이상치(Outliers) 처리하기

numerical_columns = [#'com_reg_ver_win_rate', 
                     #'historical_existing_cnt', 
    'lead_desc_length', 
    #'ver_win_rate_x', 
    #'ver_win_ratio_per_bu'
]
import seaborn as sns

# checking boxplots
def boxplots_custom(dataset, columns_list, rows, cols, suptitle):
    fig, axs = plt.subplots(rows, cols, sharey=True, figsize=(13,5))
    fig.suptitle(suptitle,y=1, size=25)
    axs = axs.flatten()
    for i, data in enumerate(columns_list):
        sns.boxplot(data=dataset[data], orient='h', ax=axs[i])
        axs[i].set_title(data + ', skewness is: '+str(round(dataset[data].skew(axis = 0, skipna = True),2)))
        
boxplots_custom(dataset=df_train, columns_list=numerical_columns, rows=2, cols=3, suptitle='Boxplots for each variable')
plt.tight_layout()

Outlier들은 보통 Boxplot으로 확인한다

그래서 값들이 숫자형인 열들만 List에 담아둬 표시 했다

 

근데 #으로 주석 처리된 것은 위에 어차피 drop한 열들이라서 배제했다

그래서 아래처럼 출력해준다

from collections import Counter

def IQR_method (df,n,features):
    """
    Takes a dataframe and returns an index list corresponding to the observations 
    containing more than n outliers according to the Tukey IQR method.
    """
    outlier_list = []
    
    for column in features:
        # 1st quartile (25%)
        Q1 = np.percentile(df[column], 25)
        # 3rd quartile (75%)
        Q3 = np.percentile(df[column],75)
        # Interquartile range (IQR)
        IQR = Q3 - Q1
        # outlier step
        outlier_step = 1.5 * IQR
        # Determining a list of indices of outliers
        outlier_list_column = df[(df[column] < Q1 - outlier_step) | (df[column] > Q3 + outlier_step )].index
        # appending the list of outliers 
        outlier_list.extend(outlier_list_column)
        
    # selecting observations containing more than x outliers
    outlier_list = Counter(outlier_list)        
    multiple_outliers = list( k for k, v in outlier_list.items() if v > n )
    
    # Calculate the number of records below and above lower and above bound value respectively
    out1 = df[df[column] < Q1 - outlier_step]
    out2 = df[df[column] > Q3 + outlier_step]
    
    print('Total number of deleted outliers is:', out1.shape[0]+out2.shape[0])
    
    return multiple_outliers

 

# detecting outliers
Outliers_IQR = IQR_method(df_train,1,numerical_columns)

# dropping outliers
df = df_train.drop(Outliers_IQR, axis = 0).reset_index(drop=True)
df_train=df

Tukey IQR Method


 

John Tukey라는 수학자, 통계학자의 이름을 딴 기법이며 

1977년 "Exploratory Data Analysis"라는 책으로 처음 소개했다

 

1. 데이터를 "Quartile"로 나눈다 (사분위수)

Q1~4를 25퍼로 나눠준다

 

2. Interquartile Range (IQR) 계산

IQR=Q3-Q1 즉 50% 범위이다

 

3. 이상치 속출

IQR의 1.5배 범위를 양방향으로 생각해

Q1-IQR*1.5

Q3+IQR*1.5 를 벗어나는 범위는 이상치라고 판단한다

 

4. 레이블들 인코딩하기

def label_encoding(series: pd.Series) -> pd.Series:
    """범주형 데이터를 시리즈 형태로 받아 숫자형 데이터로 변환합니다."""

    my_dict = {}

    # 모든 요소를 문자열로 변환
    series = series.astype(str)

    for idx, value in enumerate(sorted(series.unique())):
        my_dict[value] = idx
    series = series.map(my_dict)

    return series

 

이 함수는 모든 value들을 문자열로 변환 한 뒤

unique()를 통해 출연하는 모든 값들에게 숫자 값으로 mapping 해준다

 

무슨말이냐면

ex)

국적

-South Korea ->1

-Kenya ->2

-Brazil ->3

 

등으로 모델이 입력할 수 있게 숫자 값을 준다 (변수명 같은거임)

 

5. 본격 전처리

# 레이블 인코딩할 칼럼들
label_columns = [
    "customer_country",
    #"business_subarea",
    #"business_area",
    "business_unit",
    #"customer_type",
    "enterprise",
    "customer_job",
    "inquiry_type",
    "product_category",
    #"product_subcategory",
    #"product_modelname",
    #"customer_country.1",
    "customer_position",
    "response_corporate",
    #"expected_timeline",
]

#df_train['business_area'] = df_train['business_area'].ffill()

mode_fill = df_train.groupby('lead_owner')['customer_country'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_train['customer_country'] = df_train['customer_country'].fillna(mode_fill)

mode_fill = df_test.groupby('lead_owner')['customer_country'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_test['customer_country'] = df_test['customer_country'].fillna(mode_fill)

mode_fill = df_train.groupby('customer_position')['inquiry_type'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_train['inquiry_type'] = df_train['inquiry_type'].fillna(mode_fill)

mode_fill = df_test.groupby('customer_position')['inquiry_type'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_test['inquiry_type'] = df_test['inquiry_type'].fillna(mode_fill)

mode_fill = df_train.groupby('customer_type')['inquiry_type'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_train['inquiry_type'] = df_train['inquiry_type'].fillna(mode_fill)

mode_fill = df_test.groupby('customer_type')['inquiry_type'].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else None)
df_test['inquiry_type'] = df_test['inquiry_type'].fillna(mode_fill)



df_train.loc[df_train['business_unit'] == 'ID', 'id_strategic_ver'] = 1
df_train.loc[df_train['business_unit'] == 'IT', 'it_strategic_ver'] = 1
df_train.loc[(df_train['id_strategic_ver'] == 1) | (df_train['it_strategic_ver'] == 1), 'idit_strategic_ver'] = 1
df_train.fillna({'id_strategic_ver': 0, 'it_strategic_ver': 0, 'idit_strategic_ver':0 }, inplace=True)

df_test.loc[df_test['business_unit'] == 'ID', 'id_strategic_ver'] = 1
df_test.loc[df_test['business_unit'] == 'IT', 'it_strategic_ver'] = 1
df_test.loc[(df_test['id_strategic_ver'] == 1) | (df_test['it_strategic_ver'] == 1), 'idit_strategic_ver'] = 1
df_test.fillna({'id_strategic_ver': 0, 'it_strategic_ver': 0, 'idit_strategic_ver': 0}, inplace=True)

df_train.drop(columns='customer_type', inplace=True)
df_test.drop(columns='customer_type', inplace=True)

print(df_train.isna().sum()/len(df_train)) # 결손값 표시

df_train.head(30)

#df_train.to_csv('train_customer_country_filled.csv', index=False)
df_all = pd.concat([df_train[label_columns], df_test[label_columns]])

for col in label_columns:
    df_all[col] = label_encoding(df_all[col])

 

순서가 복잡할텐데 다 이유가 있다

인코딩할 레이블 정의->전처리->인코딩한다

 

사실 전처리가 인코딩 보다 앞에만 오면 되는데 이 이유는 인코딩 되고 나서 전처리를 하게 되면

결측값도 unique한 value라고 인식해서 encoding을 해서 mapping을 시켜버리고

국적같은 경우

 

ex)

Gimhae/Gyeongnam/SouthKorea -> SouthKorea->1

Seoul/Seoul/SouthKorea -> SouthKorea->1

 

이렇게 인식해야하는데

 

Gimhae/Gyeongnam/SouthKorea -> 1

Seoul/Seoul/SouthKorea -> 2

 

이렇게 레이블링 되기 때문이다

 

크게 결측값을 처리하는데는

1. 평균값

2. 최빈값

을 채우는 방법이 있다

 

국적 채우기

국적에 결측값들이 많았는데 

이것들을 어떻게 채울까 고민하다 분석하며 발견 한게

특정 다른 열 (담당자) 값이 같은 애들은 거의 같은 국적이였다

 

그래서 값들을 담당자 열에 대해 groupby한 뒤 mode, 최빈값, 으로 채웠다

 

또 몇개 있는데 문제를 밝히지 않고는 자세하게 설명하기 어려워

위와 같은 인과관계를 찾은게 아니면 그냥 열들을 삭제했다고 보면 된다

 

for col in label_columns:  
    df_train[col] = df_all.iloc[: len(df_train)][col]
    df_test[col] = df_all.iloc[len(df_train) :][col]

 

인코딩할 때 보면 학습데이터와 테스트데이터를 합쳐서 했는데

다시 분리해준다

 

6. OverSampling

from imblearn.over_sampling import SMOTE

x_train, x_val, y_train, y_val = train_test_split(
    df_train.drop("is_converted", axis=1),
    df_train["is_converted"],
    test_size=0.3,
    shuffle=True,
    random_state=400,
)

# Instantiating SMOTE
smote = SMOTE(random_state=42)

# Performing oversampling only on the training data
x_train_resampled, y_train_resampled = smote.fit_resample(x_train, y_train)

 

라이브러리를 받아서

.fit_resample을 통해 위에 앞서 언급한 이유 때문에 오버샘플링을해준다

 

7. Scaling

from sklearn.preprocessing import StandardScaler

# Creating function for scaling
def Standard_Scaler (df, col_names):
    features = df[col_names]
    scaler = StandardScaler().fit(features.values)
    features = scaler.transform(features.values)
    df[col_names] = features
    
    return df

 

Scaling은 뭐냐면 

숫자형 자료형의 평균값이 0이 되도록 조정해주는 함수이다

 

하지만 이 또한 인코딩이 되기 전에 했어야 하는 부분이다

(이유는 전처리를 인코딩 되기 전에 해야하는 이유와 같음)

col_names = numerical_columns
X_train = Standard_Scaler (x_train_resampled, col_names)
x_train_resampled=X_train
Y_test = Standard_Scaler (x_val, col_names)
x_val=Y_test

# Get the count of True and False values in y_train
true_count = (y_train == True).sum()
false_count = (y_train == False).sum()

print("Number of True values:", true_count)
print("Number of False values:", false_count)

과연 이 코드의 위치를 옮겼으면 더 좋은 점수가 나왔을지는 의문이지만

사실 숫자형 열이 하나밖에 없어서 큰 영향은 없었을 것 같다

 

8. 모델 만들기

 

우리는 Voting Classifier을 사용하기로 했다

뭐냐면은

여러 모델을 학습시켜 모델들이 투표를 해 이것이 True인지 False인지 판별하는 모델이다

 

Voting Classifier은 크게 두가지로 나닌다

Soft Voting & Hard Voting

 

전자 같은 경우는 True일 확률로 투표를 하는거다

 

ex)

모델 1,2,3이 있다고 하자

모델 1은 99%True 라고 하고

모델 2는 44%True 라고 하고 (사실 False라고 판단 한 것)

모델 3 은 33% True 라고 했다 치자

그러면 이 Voting Classfier은 True라고 판별한다 

사실은 더 정교한 과정으로 각 모델 마다 weight를 줘서 평균을 구하지만은

확률의 평균을 구해 True 라고 판별했다고 이해하면 된다

 

후자 같은 경우는 그냥 1모델당 1 투표를 주는 것이다

그럼 위와같은 ex)는 False라고 판단한 모델이 2개로 True 보다 많으니까 False라고 판단할 것이다

 

model=[]

#model.append(('ExtraTreesClassifier', ExtraTreesClassifier() ))
model.append(('RandomForest1', RandomForestClassifier() ))
model.append(('RandomForest2', RandomForestClassifier() ))
#model.append(('SVC', SVC(gamma ='auto', probability = True)))
#model.append(('AdaBoostClassifier', AdaBoostClassifier() ))
#model.append(('GradientBoostingClassifier', GradientBoostingClassifier() ))
model.append(('XGB1', XGBClassifier(max_depth=3, learning_rate=0.03, n_estimators=950, gamma=0.1, reg_alpha=1, reg_lambda=1) ))
model.append(('XGB2', XGBClassifier(max_depth=4, learning_rate=0.01, n_estimators=950, gamma=0.1, reg_alpha=1, reg_lambda=1) ))
model.append(('XGB3', XGBClassifier(max_depth=3, learning_rate=0.01, n_estimators=950, gamma=0.1, reg_alpha=1, reg_lambda=1) ))
model.append(('XGB4', XGBClassifier(max_depth=4, learning_rate=0.03, n_estimators=950, gamma=0.1, reg_alpha=1, reg_lambda=1) ))
#model.append(('CatBoost', CatBoostClassifier(logging_level='Silent') ))

 

처음에는 여러 모델들을 학습 시켜서 모델이라는 리스트에 넣어줬는데 최종적으로는 그냥

XGBClassifier을 다양한 파라미터로 학습 시켰다

 

처음에는 이런 모델 말고

GridSearch라는 방법을 통해 한 모델에 최적의 파라미터를 찾아주는 방법을 택했는데

여기서 평가 방법이 학습한 그 데이터로 평가를 하기 때문에

->Overfitting 

이 일어나는 것이 불가피 했다

 

Overfitting이 뭐냐? 간단히 설명하면 (과적합)

학습한 데이터에 너무 익숙해지는거임

 

ex)

시험공부를 하면 나 자신(model)이 그 과목에 관련된 문제를 주어지면 잘 푸는 학생이 되어야 고득점을 할 수 있다

좋은 선배님께서 작년 족보(train data)를 주셨다 하자

족보를 가지고 공부하면 다른 학생들 보다 더 효율적으로 나의 시험 문제 대비 능력은 오를 것이다 (train error 감소)

그러나 족보만 너무 많이 학습하고 다른 자료를 공부 안하면

진짜 시험(valiation data) 때 족보에 없는 문제가 나오면 머리가 하얘지고 못 풀것이다 (validation error 감소하지 않음)

이 현상을 나 자신이 족보에 Overfitting 되었다 볼 수 있다

 

이를 방지 하기 위해서는

문제를 많이 풀거나->학습 데이터의 양을 늘리기

문제 없으면 친구랑 서로 질문 만들기->학습 데이터 합성해서 만들기

좋은 문제를 풀고->질좋은 데이터 만들기

족보를 적당히 참고->학습을 overfitting되기 전에 멈춘다

할 수 있다

오버피팅(overfitting)이란?


https://tonnykang.tistory.com/136

 

Overfitting 과적합

오버피팅(overfitting)이란? 학습 데이터에 대해 과하게 학습하여 실제 데이터에 대한 오차가 증가하는 현상 train-set에서는 정확도 매우 높게 나옴, but test-set에서는 낮은 정확도 오버피팅이 발생하

tonnykang.tistory.com

 

 

 

그래서 오버피팅을 방지하기위해 처음으로 한 방법은 xgboost의 파라미터들을 손 봤다

처음 그래프를 그려보니

위와 같이 그려졌다

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

전형적인 오버피팅의 그래프이다

Train정확도는 0의 수렴되어가는데 Validation은 그대로 유지되는 그래프...

 

그래서

max_depth를 3~4 정도로 제한하고

n_estimator를 딱 그래프에 두 줄이 분리 되기 직전으로 (950)으로 끊어보니

이렇게 매우 아름다운 그래프가 그려졌다

 

본인은 이 그래프를 그리고 눈물을 조금 흘림

 

9. 모델학습

 

앞서 언급한 voting classifier중 soft로 했는데 이유는 밑에 가면 밝혀질 것이다

from sklearn.model_selection import StratifiedKFold
import numpy as np
from sklearn.model_selection import cross_validate
    
# Create a voting classifier with the XGBoost models
voting_clf = VotingClassifier(estimators = model, voting ='soft')

# Fit the voting classifier on the entire training data
#voting_clf.fit(x_train_resampled, y_train_resampled)

# Define cross-validation strategy
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

best_accuracy = 0
best_voting_clf = None

print(x_train_resampled.index)
print(y_train_resampled.index)

for train_index, val_index in skf.split(x_train_resampled, y_train_resampled):
    num_rows_x_train, num_cols_x_train = x_train_resampled.shape
    print("Number of rows in x_train_resampled:", num_rows_x_train)
    print("Number of rows in y_train_resampled:", len(y_train_resampled))
    
    print("Train Index Size:", len(train_index))
    print("Validation Index Size:", len(val_index))
       # Extract train and validation folds using row indices
    x_train_fold, x_val_fold = x_train_resampled.iloc[train_index], x_train_resampled.iloc[val_index]
    y_train_fold, y_val_fold = y_train_resampled.iloc[train_index], y_train_resampled.iloc[val_index]
    
    # Train the voting classifier
    voting_clf.fit(x_train_fold, y_train_fold)
    
    # Validate
    val_pred = voting_clf.predict(x_val_fold)
    val_accuracy = accuracy_score(y_val_fold, val_pred)
    print("trained once")
    # Check for improvement in validation accuracy
    if val_accuracy > best_accuracy:
        best_accuracy = val_accuracy
        best_voting_clf = voting_clf
    else:
        # If validation accuracy doesn't improve much, stop training
        break

 

여기서 Overfitting을 방지하기 위해 이용한 또 다른 방법은

Cross-Validation 을 k-fold방법으로 했다

k-fold cross-validation


https://tonnykang.tistory.com/137

 

k-fold cross-validation 교차 검증 (오버피팅 방지)

cf) 데이터train data : 학습을 통해 가중치, 편향 업데이트validation data : 하이퍼파라미터 조정, 모델의 성능 확인test data : 모델의 최종 테스트하이퍼파라미터 : 값에 따라서 모델의 성능에 영향을 주

tonnykang.tistory.com

 

10. 모델 성능 보기

def get_clf_eval(y_test, y_pred=None):
    confusion = confusion_matrix(y_test, y_pred, labels=[True, False])
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred, labels=[True, False])
    recall = recall_score(y_test, y_pred)
    F1 = f1_score(y_test, y_pred, labels=[True, False])

    print("오차행렬:\n", confusion)
    print("\n정확도: {:.4f}".format(accuracy))
    print("정밀도: {:.4f}".format(precision))
    print("재현율: {:.4f}".format(recall))
    print("F1: {:.4f}".format(F1))

 

오차 행렬을 출력하는 함수이다

오차행렬


tp=true positives 맞다고 판단해서 정답이 진짜 맞은 경우

fp=false positives 맞다고 판단했지만 오답 이였던 경우

fn=false negatives 틀렸다고 판단했지만 오답 이였던 경우

tn=true negatives 틀렸다고 판단해서 정답도 틀렸던 경우

 

1.    Accuracy

Accuracy를 직역하면 정확도가 된다. 이진 판단에서도 틀린 말이 아니다. 이진판단에서의 Accuracy는 모델의 예측, 분류의 전반적인 정확도를 수치화 한 것이다.

분류해보자면 tpfp는 정확했던 것이고 (yes or no 문제이니)

tn fn은 모델이 틀리게 판단한 것이다.

그러면 accuracy를 판단하려면 모든 판단(예측)에서 정답의 비율을 살펴보면 된다. 그래서 (tp+tn)/(tp+tn+fp+fn) 으로 정의된다

 

2.    Precision

Precision도 사전에 검색하면 정확도 Accuracy와 비슷한 단어로 정의된다. 하지만 이진 판별에서는 조금 다르게 정의된다. 펄서(별 의 종류)이거나 아니거나 같은 yes or no 질문에 yes라고 대답한 ,tp(true positives) tn(true negatives)내에서 정확도를 의미한다.

Precision fp(false positives)를 최소화 하고 싶을 때 중요하다. 예를 들어 병원에서 질병 판별 시나리오에서 높은 precision을 보여준다면 모델이 질병을 판별하면 옳을 확률이 높은 것이다

 

3.    Recall a.k.a. Sensitivity or True Positive rate

Recall이란 영어단어의 뜻은, 상기하다, 기억하다 라는 뜻이다. 사실 이진 판단코드에서 사용된 의미와는 조금 다르기에 더 찾아보니 Sensitivity, 민감도, 아니면 True Positive rate으로도 사용됨을 찾았다. Recall은 코드에서 yes or no 질문에 정답이 yes 인 경우들 중에서의 실제로 모델이 맞춘 비율이다.

전체 tp/(tp+fn)으로 사용된 이유는 tp는 당연히 필요하고 fn false negativeno라고 대답했지만 오답 이였던 경우, 즉 실제 정답이 yes 였던 경우이기에 함께 더해줘야 한다.

 

   recall같은 수치는 fn을 최소화 하고싶을 때 중요하다. 또 다시 병원에서의 질병을 판별하는 시나리오를 예로 들자면, 높은 recall을 보여준다는 것은 모델은 모든 경우의 질병의 판별을 잘한다는 뜻이다. 그 판별이 가끔 fp 실제 병이 없지만 있다고 판별할지라도.

 

1.    F1-Score

F1변수는 코드에서 PrecisionRecall의 조화평균으로 정의된다. 조화평균은

값들의 역수의 산술평균의 역수로 정의된다

 

F1-Score01사이에 범위를 가지고 있고, 더 높은 값은 Precision Recall사이의 더 좋은 균형을 뜻한다. 이는 Precision을 높이면 Recall의 값이 줄어들 수도 있고 그 반대의 현상이 일어날 수 있기 때문에 둘의 적절한 조화를 찾기 위해 사용한다.

from sklearn.metrics import precision_recall_curve
from sklearn.metrics import PrecisionRecallDisplay

# Create Precision-Recall curve display
precision_recall_display = PrecisionRecallDisplay.from_estimator(voting_clf, x_val, y_val)

# Plot the precision-recall curve
precision_recall_display.plot()

# Show the plot
plt.show()

 

그리고 위에 코드는 Recall과 Precision 사이의 관계 (Trade-Off)를 보여주는 그래프를 그려준다

Trade-Off라는 단어를 쓰는이유는 마치 두개의 요인들이 거의 반비례하는 관계를 가지기 때문에

마치 둘이 거래를 하듯이 적절한 중간값을 찾는 것이기 떄문이다

 

Recall을 올리는 것이 목표였는데 

그래프에 보시다 시피 Recall이 한 0.6을 넘어가기 시작하면 급격하게 Precision이  떨어지기 시작한다

그러나 Recall이랑 Precision 둘 중 한개라도 값이 낮으면 F1-Score(성능)이 낮기 때문에 적절한 0.7정도의 Recall을 목표로 하는게 적절한 Trade-Off이다

 

그럼 이 두개를 어떻게 조절하나?

Threshold의 등장이다

test_pred = voting_clf.predict(x_val.fillna(0))
pred_proba = voting_clf.predict_proba(x_val.fillna(0))[:, 1]

# 임계값 조정을 통한 예측
threshold = 0.5  # 임계값 초기값
while sum(pred_proba > threshold) < 2000:
    threshold -= 0.01  # 임계값을 0.01씩 감소시킴
test_pred = (pred_proba > threshold).astype(int)
get_clf_eval(y_val, test_pred)
print(threshold)

 

위에서 오차행렬을 확인해보면 True값이 약 1450개가 된다

근데 그냥 돌려보면

#test_pred = best_model.predict(x_test.fillna(0))
#test_pred = best_tree.predict(x_test.fillna(0))
pred_proba = voting_clf.predict_proba(x_test.fillna(0))[:, 1]
test_pred = (pred_proba > threshold).astype(int)
test_pred_size = test_pred.shape
print("Size of test_pred array:", test_pred_size)
sum(test_pred) # True로 예측된 개수

이 코드에서 True라고 예측한 수가 나오는데 터무니 없이 낮은 갯수가 나온다

1450개가 True인데

그러면 한 2000개를 맞추면 True를 다 커버 하지 않을까..? 해서 

True갯수가 2000개 이하면 Threshold를 0.01씩 감소시키도록 시켰다

 

여기서 Threshold가 뭐냐면
model이 True라고 예상하는 확률이 Threshold보다 높으면 True라고 판단한다

기본은 0.5로 50% 보다True가 높으면 True라고 판단하는 것이다 그러면

Threshold가 0.4로 낮아지면

원래 40%로 True라고 판단해 False였던 값이 이제 True가 되는 것이다

그래서 Threshold를 한 0.35정도로 낮추니 2000개를 예상 했다

 

그렇게 LG Aimers에서 준 제출 코드를 그대로 사용하니

우리들의 최고 점수가 나왔다

당시 140등 정도였으며 

절대로 어디 자랑할 정도의 성과는 아니고 우리보다 더 잘한 팀들은 많겠지만

 

우리처럼 인공지능을 처음 접해보는 사람들에게 도움이 되고자

해카톤 사고 과정을 기록해봅니다!

반응형