펭균 ML 보고서

펭귄 암수예측 기계학습모형 개발 보고서
저자
소속
이광춘

TCS

공개

2023년 02월 02일

1 들어가며

파머 펭귄 성별 분류 머신러닝 모델은 인구 관리 및 모니터링을 더 효율적으로 할 수 있기 때문에 중요합니다. 개별 펭귄의 성별을 정확하게 식별함으로써 펭귄 서식지에서 성비와 산란 패턴을 더 잘 이해하고 추적 할 수 있습니다. 이러한 정보는 지구온난화로 인한 기후위기 시대 펭귄생존을 보장하는 데 필수적입니다. 또한 성별 분류 모델은 노동집약적인 성별 식별 업무를 대폭 줄여 시간과 자원을 절약을 효율적으로 사용할 수 있도록 기여합니다.

2 데이터

2.1 데이터 입수

펭귄 데이터를 입수할 수 있는 방법은 크게 3가지가 있습니다.

  • 데이터 패키지
  • 로컬 csv 파일
  • 클라우드 저장소: Microsoft Azure Blob Storage, Google Cloud Storage, DigitalOcean Spaces, Backblaze B2, Wasabi Hot Cloud Storage, Alibaba Cloud OSS, IBM Cloud Object Storage, Oracle Cloud Infrastructure Object Storage, OpenStack Object Storage (Swift), Rackspace Cloud Files

2.1.1 데이터 패키지

코드
library(palmerpenguins)

penguins <- palmerpenguins::penguins

penguins
#> # A tibble: 344 × 8
#>    species island    bill_length_mm bill_depth_mm flipper_…¹ body_…² sex    year
#>    <fct>   <fct>              <dbl>         <dbl>      <int>   <int> <fct> <int>
#>  1 Adelie  Torgersen           39.1          18.7        181    3750 male   2007
#>  2 Adelie  Torgersen           39.5          17.4        186    3800 fema…  2007
#>  3 Adelie  Torgersen           40.3          18          195    3250 fema…  2007
#>  4 Adelie  Torgersen           NA            NA           NA      NA <NA>   2007
#>  5 Adelie  Torgersen           36.7          19.3        193    3450 fema…  2007
#>  6 Adelie  Torgersen           39.3          20.6        190    3650 male   2007
#>  7 Adelie  Torgersen           38.9          17.8        181    3625 fema…  2007
#>  8 Adelie  Torgersen           39.2          19.6        195    4675 male   2007
#>  9 Adelie  Torgersen           34.1          18.1        193    3475 <NA>   2007
#> 10 Adelie  Torgersen           42            20.2        190    4250 <NA>   2007
#> # … with 334 more rows, and abbreviated variable names ¹​flipper_length_mm,
#> #   ²​body_mass_g
$ pip install palmerpenguins
코드
import pandas as pd
from palmerpenguins import load_penguins 

penguins = load_penguins()

penguins.head()
#>   species     island  bill_length_mm  ...  body_mass_g     sex  year
#> 0  Adelie  Torgersen            39.1  ...       3750.0    male  2007
#> 1  Adelie  Torgersen            39.5  ...       3800.0  female  2007
#> 2  Adelie  Torgersen            40.3  ...       3250.0  female  2007
#> 3  Adelie  Torgersen             NaN  ...          NaN     NaN  2007
#> 4  Adelie  Torgersen            36.7  ...       3450.0  female  2007
#> 
#> [5 rows x 8 columns]

2.1.2 csv 데이터

코드
library(tidyverse)

penguins_csv <- read_csv('data/penguins.csv')

penguins_csv
#> # A tibble: 344 × 9
#>    rowid species island    bill_length_mm bill_dep…¹ flipp…² body_…³ sex    year
#>    <dbl> <chr>   <chr>              <dbl>      <dbl>   <dbl>   <dbl> <chr> <dbl>
#>  1     1 Adelie  Torgersen           39.1       18.7     181    3750 male   2007
#>  2     2 Adelie  Torgersen           39.5       17.4     186    3800 fema…  2007
#>  3     3 Adelie  Torgersen           40.3       18       195    3250 fema…  2007
#>  4     4 Adelie  Torgersen           NA         NA        NA      NA <NA>   2007
#>  5     5 Adelie  Torgersen           36.7       19.3     193    3450 fema…  2007
#>  6     6 Adelie  Torgersen           39.3       20.6     190    3650 male   2007
#>  7     7 Adelie  Torgersen           38.9       17.8     181    3625 fema…  2007
#>  8     8 Adelie  Torgersen           39.2       19.6     195    4675 male   2007
#>  9     9 Adelie  Torgersen           34.1       18.1     193    3475 <NA>   2007
#> 10    10 Adelie  Torgersen           42         20.2     190    4250 <NA>   2007
#> # … with 334 more rows, and abbreviated variable names ¹​bill_depth_mm,
#> #   ²​flipper_length_mm, ³​body_mass_g
코드
import pandas as pd

palmer_df = pd.read_csv('data/penguins.csv')

print(palmer_df.head())
#>    rowid species     island  ...  body_mass_g     sex  year
#> 0      1  Adelie  Torgersen  ...       3750.0    male  2007
#> 1      2  Adelie  Torgersen  ...       3800.0  female  2007
#> 2      3  Adelie  Torgersen  ...       3250.0  female  2007
#> 3      4  Adelie  Torgersen  ...          NaN     NaN  2007
#> 4      5  Adelie  Torgersen  ...       3450.0  female  2007
#> 
#> [5 rows x 9 columns]

2.1.3 클라우드 저장소

코드
library(pins)
library(Microsoft365R)

od <- Microsoft365R::get_personal_onedrive()
board <- board_ms365(od, "krvote_board")
# board %>% pin_write(penguins)

board %>% pin_read("penguins")
#> # A tibble: 344 × 8
#>    species island    bill_length_mm bill_depth_mm flipper_…¹ body_…² sex    year
#>    <fct>   <fct>              <dbl>         <dbl>      <int>   <int> <fct> <int>
#>  1 Adelie  Torgersen           39.1          18.7        181    3750 male   2007
#>  2 Adelie  Torgersen           39.5          17.4        186    3800 fema…  2007
#>  3 Adelie  Torgersen           40.3          18          195    3250 fema…  2007
#>  4 Adelie  Torgersen           NA            NA           NA      NA <NA>   2007
#>  5 Adelie  Torgersen           36.7          19.3        193    3450 fema…  2007
#>  6 Adelie  Torgersen           39.3          20.6        190    3650 male   2007
#>  7 Adelie  Torgersen           38.9          17.8        181    3625 fema…  2007
#>  8 Adelie  Torgersen           39.2          19.6        195    4675 male   2007
#>  9 Adelie  Torgersen           34.1          18.1        193    3475 <NA>   2007
#> 10 Adelie  Torgersen           42            20.2        190    4250 <NA>   2007
#> # … with 334 more rows, and abbreviated variable names ¹​flipper_length_mm,
#> #   ²​body_mass_g

3 데이터 전처리

펭귄 데이터는 교육용으로 제공되지만 다른 데이터셋과 달리 결측값(NA)도 포함되어 있어 본격적인 분석을 위해서 결측값 현황을 정확히 확인하고 결측값에 대한 적절한 전략을 세우고 이를 처리해야 한다.

3.1 결측값 현황

데이터프레임에 결측값이 어떻게 분포되어 있는지 먼저 시각적으로 파악하자. 상당수 결측값이 sex 칼럼에 몰려있는 것이 확인된다. 특히, 일부 펭귄의 경우 측정 변수가 모두 결측된 현황도 파악할 수 있다.

코드
library(tidyverse)
library(naniar)
library(palmerpenguins)

penguins_raw <- palmerpenguins::penguins

vis_miss(penguins_raw)

코드
vis_miss(penguins_raw, cluster = TRUE, sort_miss = TRUE)

시각적으로 확인한 후에 기술통계량을 통해 정확한 결측값 현황을 확인해보자.

코드
nrow(penguins_raw) * ncol(penguins_raw)
#> [1] 2752
코드
naniar::n_miss(penguins_raw)
#> [1] 19
코드
naniar::n_complete(penguins_raw)
#> [1] 2733
코드
miss_var_summary(penguins_raw)
#> # A tibble: 8 × 3
#>   variable          n_miss pct_miss
#>   <chr>              <int>    <dbl>
#> 1 sex                   11    3.20 
#> 2 bill_length_mm         2    0.581
#> 3 bill_depth_mm          2    0.581
#> 4 flipper_length_mm      2    0.581
#> 5 body_mass_g            2    0.581
#> 6 species                0    0    
#> 7 island                 0    0    
#> 8 year                   0    0
코드
miss_case_summary(penguins_raw)
#> # A tibble: 344 × 3
#>     case n_miss pct_miss
#>    <int>  <int>    <dbl>
#>  1     4      5     62.5
#>  2   272      5     62.5
#>  3     9      1     12.5
#>  4    10      1     12.5
#>  5    11      1     12.5
#>  6    12      1     12.5
#>  7    48      1     12.5
#>  8   179      1     12.5
#>  9   219      1     12.5
#> 10   257      1     12.5
#> # … with 334 more rows

3.2 결측값 처리 전략

결측값 현황을 살펴보고 결측값이 많은 4번, 272번 펭귄은 분석에서 제거하고 sex 암수변수는 범주형 변수라 평균과 유사한 기능을 하는 최빈값(Mode) 정보를 이용하여 결측값을 채워넣는 것으로 한다.

코드

# get_mode <- function(x) {
#   ux <- unique(x)
#   ux[which.max(tabulate(match(x, ux)))]
# }
# 
# get_mode(penguins_csv$sex)

penguins_tbl <- penguins_raw %>% 
  filter( !row_number() %in% c(4, 272) ) %>% 
  mutate( sex = as.character(sex)) %>% 
  mutate( sex = if_else(is.na(sex), "male", sex)) %>% 
  mutate( sex = factor( sex, levels = c("female", "male")))

4 탐색적 데이터 분석

결측값 제거 등을 통해 데이터 전처리 작업이 완료되었다. 다음 단계로 탐색적 데이터 분석을 통해 암수성별과 관련된 정보를 탐색적으로 파악해보자.

4.1 기술통계량

코드
library(explore)

penguins_tbl %>% 
  describe()
#> # A tibble: 8 × 8
#>   variable          type     na na_pct unique    min   mean    max
#>   <chr>             <chr> <int>  <dbl>  <int>  <dbl>  <dbl>  <dbl>
#> 1 species           fct       0      0      3   NA     NA     NA  
#> 2 island            fct       0      0      3   NA     NA     NA  
#> 3 bill_length_mm    dbl       0      0    164   32.1   43.9   59.6
#> 4 bill_depth_mm     dbl       0      0     80   13.1   17.2   21.5
#> 5 flipper_length_mm int       0      0     55  172    201.   231  
#> 6 body_mass_g       int       0      0     94 2700   4202.  6300  
#> 7 sex               fct       0      0      2   NA     NA     NA  
#> 8 year              int       0      0      3 2007   2008.  2009

4.2 단변량분석

코드
penguins_tbl %>% 
  explore_all()

4.3 암수(target) 연관성

코드
penguins_tbl %>% 
  explore_all(target = sex)

4.4 암수(target) 의사결정나무

코드
penguins_tbl %>% 
  explain_tree(target = sex)

4.5 시각화

코드
penguins_tbl %>% 
  explore(body_mass_g, bill_length_mm, target = sex)

코드
library(tidymodels)
library(parttree)

penguins_dt <- 
  decision_tree() %>%
  set_engine("rpart") %>%
  set_mode("classification") %>%
  fit(sex ~ body_mass_g + bill_length_mm, data = penguins_tbl)

# 시각화
penguins_tbl %>%
  ggplot(aes(x = body_mass_g, y = bill_length_mm)) +
  # geom_jitter(aes(col=species), alpha=0.7) +
  geom_point(aes(color = sex)) +
  geom_parttree(data = penguins_dt, aes(fill=sex), alpha = 0.1,
                flipaxes = FALSE) +
  scale_color_manual(values = c("female"  = "blue",
                              "male" = "red")) +
  scale_fill_manual(values = c("female"  = "blue",
                              "male" = "red")) +  
  theme_minimal()

5 암수 분류 모형

5.1 훈련/시험 데이터셋

7:3으로 훈련시험 데이터셋을 나눈다.

코드
library(tidymodels)

set.seed(123)
penguin_split <- initial_split(penguins_tbl, strata = sex, prop = 0.7)
penguin_train <- training(penguin_split)
penguin_test <- testing(penguin_split)

set.seed(123)
penguin_boot <- bootstraps(penguin_train)
penguin_boot
#> # Bootstrap sampling 
#> # A tibble: 25 × 2
#>    splits           id         
#>    <list>           <chr>      
#>  1 <split [238/87]> Bootstrap01
#>  2 <split [238/85]> Bootstrap02
#>  3 <split [238/87]> Bootstrap03
#>  4 <split [238/86]> Bootstrap04
#>  5 <split [238/79]> Bootstrap05
#>  6 <split [238/83]> Bootstrap06
#>  7 <split [238/88]> Bootstrap07
#>  8 <split [238/83]> Bootstrap08
#>  9 <split [238/91]> Bootstrap09
#> 10 <split [238/87]> Bootstrap10
#> # … with 15 more rows

5.2 Feature Engineering

숫자형 변수에 대해서 정규화 작업을 진행하고 범주형 변수에 대해서 One-Hot 인코딩 작업을 수행하여 Feature 로 추가시킨다.

코드
penguin_rec <- recipe(sex ~ ., data = penguin_train) %>%
  step_corr(all_numeric(), threshold = 0.9) %>% 
  step_normalize(all_numeric()) %>% 
  step_dummy(all_nominal(), -all_outcomes(), one_hot = TRUE)

penguin_rec
#> Recipe
#> 
#> Inputs:
#> 
#>       role #variables
#>    outcome          1
#>  predictor          7
#> 
#> Operations:
#> 
#> Correlation filter on all_numeric()
#> Centering and scaling for all_numeric()
#> Dummy variables from all_nominal(), -all_outcomes()

5.3 기본모형(RF)

코드
rf_spec <- rand_forest() %>%
  set_mode("classification") %>% 
  set_engine("ranger") 

rf_spec
#> Random Forest Model Specification (classification)
#> 
#> Computational engine: ranger

5.4 작업흐름

코드
penguin_wf <- workflow() %>%
  add_recipe(penguin_rec) %>% 
  add_model(rf_spec)

penguin_wf
#> ══ Workflow ════════════════════════════════════════════════════════════════════
#> Preprocessor: Recipe
#> Model: rand_forest()
#> 
#> ── Preprocessor ────────────────────────────────────────────────────────────────
#> 3 Recipe Steps
#> 
#> • step_corr()
#> • step_normalize()
#> • step_dummy()
#> 
#> ── Model ───────────────────────────────────────────────────────────────────────
#> Random Forest Model Specification (classification)
#> 
#> Computational engine: ranger

5.5 병렬처리 설정

코드
library(doParallel)

cores <- parallel::detectCores(logical = FALSE)
cl <- makePSOCKcluster(cores)
registerDoParallel(cores = cl)

set.seed(77)

5.6 훈련 적합

코드
tictoc::tic()

rf_rs <- penguin_wf %>%
  fit_resamples(
    resamples = penguin_boot,
    control = control_resamples(save_pred = TRUE)
  )

rf_rs
#> # Resampling results
#> # Bootstrap sampling 
#> # A tibble: 25 × 5
#>    splits           id          .metrics         .notes           .predictions
#>    <list>           <chr>       <list>           <list>           <list>      
#>  1 <split [238/87]> Bootstrap01 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  2 <split [238/85]> Bootstrap02 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  3 <split [238/87]> Bootstrap03 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  4 <split [238/86]> Bootstrap04 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  5 <split [238/79]> Bootstrap05 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  6 <split [238/83]> Bootstrap06 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  7 <split [238/88]> Bootstrap07 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  8 <split [238/83]> Bootstrap08 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#>  9 <split [238/91]> Bootstrap09 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#> 10 <split [238/87]> Bootstrap10 <tibble [2 × 4]> <tibble [0 × 3]> <tibble>    
#> # … with 15 more rows

tictoc::toc()
#> 6.75 sec elapsed

5.7 모델 평가

코드
collect_metrics(rf_rs)
#> # A tibble: 2 × 6
#>   .metric  .estimator  mean     n std_err .config             
#>   <chr>    <chr>      <dbl> <int>   <dbl> <chr>               
#> 1 accuracy binary     0.887    25 0.00746 Preprocessor1_Model1
#> 2 roc_auc  binary     0.946    25 0.00396 Preprocessor1_Model1

5.7.1 오차 행렬

코드
rf_rs %>% 
  conf_mat_resampled() %>% 
  pivot_wider(names_from = Truth, values_from = Freq)
#> # A tibble: 2 × 3
#>   Prediction female  male
#>   <fct>       <dbl> <dbl>
#> 1 female      37.7   4.88
#> 2 male         4.84 38.4

5.7.2 ROC 곡선

코드
rf_rs %>% 
  collect_predictions() %>%
  group_by(id) %>%
  roc_curve(sex, .pred_female) %>%
  ggplot(aes(1 - specificity, sensitivity, color = id)) +
  geom_abline(lty = 2, color = "gray80", size = 1.5) +
  geom_path(show.legend = FALSE, alpha = 0.6, size = 1.2) +
  coord_equal()

5.8 최종 모형

코드
penguin_final <- penguin_wf %>%
  last_fit(penguin_split)

penguin_final
#> # Resampling results
#> # Manual resampling 
#> # A tibble: 1 × 6
#>   splits            id               .metrics .notes   .predictions .workflow 
#>   <list>            <chr>            <list>   <list>   <list>       <list>    
#> 1 <split [238/104]> train/test split <tibble> <tibble> <tibble>     <workflow>

5.9 모형 성능

코드
model_perf <- collect_metrics(penguin_final)

암수 성별 예측모형의 최종 모형 정확도는 0.8557692 이고 ROC 는 0.9377778 이다.

코드
collect_predictions(penguin_final) %>%
  conf_mat(sex, .pred_class)
#>           Truth
#> Prediction female male
#>     female     41    6
#>     male        9   48

실제 암컷 펭귄을 수컷 펭귄으로 오분류하는 것이 그 반의 경우보다 30% 정도 더 높게 나온다.

6 설명가능한 ML

DALEX 패키지 도구를 사용하여 기계확습모형을 꼼꼼히 살펴본다.

코드
library(DALEX)
library(DALEXtra) 
library(modelStudio) # install_dependencies()

penguin_fit <- penguin_wf %>%
  fit(data = penguin_train)

y_penguin <- as.numeric(penguin_train$sex)

penguin_explainer <- explain_tidymodels(penguin_fit,
                     data = penguin_train,
                     y = y_penguin,
                     label = "Penguin RF")

set.seed(123)
new_penguins <- penguin_train %>% slice_sample(n = 3)

modelStudio(penguin_explainer, new_observation = new_penguins)

7 주요 기여사항

파머 펭귄 성별 분류 머신러닝 모델은 인구 관리 및 모니터링을 더 효율적으로 할 수 있는 예측 분류 모형을 개발하였다. 예측 모형의 정확도는 0.8557692 으로 기존 통계모형보다 우수한 정확도를 보이고 있다.

8 한계

예측 모형의 정확도가 0.8557692 으로 기존 통계모형보다 높지만 실제 업무에 배포하여 적용하는 기준 정확도 95%에는 미치지 못하는 한계가 있고, 개발된 암수분류 모형을 실제 운영환경에서 사용할 수 있는 추가 작업이 필요한 것으로 확인되었습니다.

9 결론

개별 펭귄의 성별을 정확하게 식별함으로써 펭귄 서식지에서 성비와 산란 패턴을 더 잘 이해하고 추적 할수 있는 성별 예측 기계학습 모형을 개발하였고 이를 실제 업무에 적용하기 위한 도전과제도 추가로 발굴되었다. 금번 프로젝트가 후속 연구개발로 이어져 기후 온난화로 인한 안정적인 펭귄 개체수 유지를 위한 발판으로 이어질 것으로 기대된다.