VGGNet

VGGNet(Visual Geometry Group Network)은 2014년 1000개의 이미지 클래스를 분류하는 이미지넷 이미지 인식 대회에서 준우승을 한 모델입니다. 옥스포드 대학의 연구팀 VGG에 의해 개발되었다고 하는데요. 이번 포스팅에서는 VGGNet 논문에서 중요한 내용을 살펴보고 직접 코드화하는 과정을 정리해 작성해 보겠습니다.


<VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION>
논문 링크 : https://arxiv.org/pdf/1409.1556

VGG의 핵심 장점은 다음과 같습니다.

1. convolution의 depth를 깊게 구성
2. 연산량을 획기적으로 줄여 좋은 성능 확보
3. 깊은 레이어 구조를 통해 Activation function을 여러 번 집어넣어서 비선형성을 더 많이 확보

왜 이런 장점이 있는지 논문 내용과 함께 살펴봅시다.


2.1. Architecture

VGG 이전의 기존 CNN 모델들은 주로 Convolution 레이어와 Pooling 레이어를 번갈아 연속적으로 사용하는 것이 일반적이었습니다. VGGNet은 이러한 기존의 틀에서 벗어나 Convolution을 2번 또는 3번 연속해서 쌓은 뒤 Pooling 레이어를 배치하는 새로운 구조를 제시합니다. 

VGG16 구조

위 사진은 대표적인 VGG16모델의 레이어 설계도입니다. 책이나 인터넷에서 쉽게 찾아볼 수 있는 그림인데요. 보이는 것처럼 파란색의 Convolution 레이어가 연속해서 2-3번 쌓이고, 이어서 빨간색 max pooling layer가 배치된 것을 볼 수 있습니다. 논문 2.1 Architecture에 관한 부분을 함께 살펴보도록 하겠습니다.

the input to our ConvNets is a fixed-size 224 × 224 RGB image
  • 입력 이미지의 shape은 (224, 224, 3)인 것을 알 수 있습니다.
The image is passed through a stack of convolutional (conv.) layers, where we use filters with a very small receptive field: 3 × 3 (which is the smallest size to capture the notion of left/right, up/down, center). (중략) The convolution stride is fixed to 1 pixel
  • 콘볼루션 레이어를 여러겹 쌓고 필터는 (3 x 3) 작은 사이즈로 고정하여 사용한다고 명시하고 있습니다. VGG 이전의 CNN 모델들은 전통적으로 필터 사이즈가 7, 9, 11 정도로 큰 것이 일반적이었다고 합니다. 그래서 사이즈가 3인 필터는 'very small'이라고 표현되는 것으로 보입니다.
  • 콘볼루션 레이어의 stride는 1픽셀로 고정됩니다.
Max-pooling is performed over a 2 × 2 pixel window, with stride 2.
  • 사용된 Maxpooling의 필터 사이즈와 stride 모두 (2, 2)인 것을 알 수 있습니다.
A stack of convolutional layers is followed by three Fully-Connected (FC) layers: the first two have 4096 channels each, the third performs 1000- way ILSVRC classification and thus contains 1000 channels (one for each class). The final layer is the soft-max layer.
  • CNN 레이어 이후에는 순서대로 4096 - 4096 - 1000개의 필터가 사용된 FC(Fully-Connected) 레이어가 3번 배치되었습니다.
  • 마지막 필터가 1000개인 이유는 VGG모델이 총 1000개의 이미지 클래스를 분류하기 때문입니다.
  • 마지막 레이어의 activation function로는 소프트맥스가 사용된 것을 알 수 있습니다.

2.2. Configuration

다음으로 여러 가지 버전의 VGG모델을 표현된 논문의 Table 1, 2를 함께 살펴보겠습니다.

The width of conv. layers (the number of channels) is rather small, starting from 64 in the first layer and then increasing by a factor of 2 after each max-pooling layer, until it reaches 512. 
  • 콘볼루션 레이어의 필터의 수는 64개로 작게 시작해서 마지막으로 512가 될때까지 2의 배수로 늘려 가겠다고 말하고 있습니다.
The convolutional layer parameters are denoted as “conv[receptive field size]-[number of channels]”. The ReLU activation function is not shown for brevity.
※ [2-1] All hidden layers are equipped with the rectification (ReLU (Krizhevsky et al., 2012)) non-linearity.
  • Table 1에서 표현된 레이어 수식(conv0-000형식)을 읽을 수 있어야 합니다. 예를 들어 conv3-256의 경우 필터 사이즈 (3x3)인 콘볼루션 레이어가 256장 쌓인 것으로 해석할 수 있습니다.
  • 간결한 표현을 위해 아래 배치도(Table 1)에서 활성함수 ReLU는 생략해서 표현되었으나, 논문 2.1 Architecture에 언급되었듯이 모든 히든레이어에는 ReLU가 적용되었습니다.

Table 1

  • 우리가 맨 위에서 살펴본 빨강파랑 레이어 구조도는 VGG16모델입니다. Convolution 레이어와 Dense 레이어를 합해 16개의 레이어가 사용되었습니다. 여기에서 레이어가 3개 늘어나면 VGG19(E모델)이 됩니다.
  • VGG16에 해당하는 모델 C와 D의 차이점은 3겹의 콘볼루션 레이어에서 마지막에 필터 사이즈 1짜리 레이어가 쓰였느냐 3짜리가 쓰였느냐 정도로 구분이 되네요.

VGG16 구조
Table 2

In Table 2 we report the number of parameters for each configuration. In spite of a large depth, the number of weights in our nets is not greater than the number of weights in a more shallow net with larger conv. layer widths and receptive fields (144M weights in (Sermanet et al., 2014)).
  • 테이블 2에는 백만 단위의 파라미터 수가 표현이 되어 있는데요. VGG16에 해당하는 모델 C-D의 경우 1억 3천만 개 정도의 파라미터를 찾아야 하네요.
  • 그렇다면 사이즈 3의 작은 필터를 사용한 3개의 콘볼루션 레이어를 연속적으로 사용해서 얻은 이점이 무엇일까요?

Table 2 아래에 중요한 내용이 담겨있습니다.

So what have we gained by using, for instance, a stack of three 3×3 conv. layers instead of a single 7×7 layer? First, we incorporate three non-linear rectification layers instead of a single one, which makes the decision function more discriminative. Second, we decrease the number of parameters
예: (10, 10) 이미지에 (7, 7) 필터 1번 적용
* 콘볼루션 후 최종 사이즈 : (4, 4)
* 찾아야 할 파라미터  : (7, 7) -> 49개
예 : (10, 10) 이미지에 (3, 3) 필터 3번 적용
* 콘볼루션 후 최종 사이즈 : (4, 4)
* 찾아야 할 파라미터 : (3, 3) 3개 -> 9 * 3 -> 27개
  • 최종적으로 얻는 이미지의 사이즈는 (4, 4)로 동일한데 찾아야 할 파라미터의 수는 49개에서 27개로 줄어드는 것을 확인할 수 있습니다.
  • 콘볼루션 레이어를 연속해서 쌓는 VGG 구조를 통해 찾아야 할 파라미터 수를 획기적으로 줄여 연산량을 줄이고 성능을 개선할 수 있다는 것입니다.
  • 또한 한 번의 콘볼루션 레이어만 추가했을 때보다 세 번의 연속적인 콘볼루션 레이어를 추가하게 되면서 비선형성을 더욱 강조해 모델의 유연성을 확보할 수 있게 되었고 이 역시 성능 개선으로 이어지게 되었습니다.

코드화

VGG16 모델 D - 코드로 구현해보기

from tensorflow.keras.layers import Conv2D, MaxPool2D, Flatten, Dense, Dropout

필요한 라이브러리를 호출합니다.

# 논문 VGG16 D모델 그대로 설계해보기
    
model_D_VGG_origin = tf.keras.Sequential([
    Conv2D(64, 3, input_shape = (224, 224, 3), padding = "same", activation = "relu"),
    Conv2D(64, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    Conv2D(512, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Flatten(),
    Dense(4096, activation = "relu"),
    Dense(4096, activation = "relu"),
    Dense(1000, activation = "softmax") # 1000가지 이미지를 분류하는 대회였음
])


VGG16 모델 D - Fashion Mnist에 맞게 튜닝하기

VGG16 Model D를 튜닝해서 fashion_mnist 분류모델을 구현하는 코드로 작성해 보겠습니다.

  • 논문 상 입력 데이터는 (224, 224, 3)이지만 Fashion Mnist의 데이터는 (28, 28, 1)입니다. 28을 224로 늘리는 것은 해상도가 너무 깨지고 특징이 소실되므로 의미가 없다는 판단 하에, 입력 부분을 28로 튜닝해서 사용하기로 결정했습니다.
  • MaxPool 횟수를 4회에서 3회로 줄이기로 했습니다. 이미지 사이즈가 가로 세로 (28, 28)이기 때문에 논문 그대로 4번 사용하면 사이즈가 소실될 수도 있습니다.
  • 논문 속 레이블은 1000개이지만 Fashion Mnist의 레이블은 10개입니다. 따라서 마지막 Dense 레이어의 필터 수를 1000에서 10으로 줄이겠습니다.
import tensorflow as tf

# 데이터 불러오기
fashion_mnist = tf.keras.datasets.fashion_mnist
(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()

# 정규화
X_train, X_test = X_train/255. , X_test/255.

# 3D를 4D로 변환
X_train = X_train.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)
# 논문 VGG16 D모델 -> 패션 엠니스트에 맞는 모델로 바꾸어보기
    
model_D_VGG_fashion = tf.keras.Sequential([
    Conv2D(64, 3, input_shape = (28, 28, 1), padding = "same", activation = "relu"),
    Conv2D(64, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    Conv2D(128, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    Conv2D(256, 3, padding = "same", activation = "relu"),
    MaxPool2D(pool_size = 2, strides = 2),
    Flatten(),
    Dense(4096, activation = "relu"),
    Dense(4096, activation = "relu"),
    Dense(10, activation = "softmax") # 10가지 이미지로 분류하기
])
model_D_VGG_fashion.summary()

model_D_VGG_fashion.compile(loss = tf.keras.losses.SparseCategoricalCrossentropy(),
                 optimizer = Adam(),
                 metrics = ['accuracy'])
import os

# callbacks
early = tf.keras.callbacks.EarlyStopping(patience = 5)
cp_path = "training/cp-{epoch:04d}.ckpt"
cp_dir = os.path.dirname(cp_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(cp_path,
                                                 monitor = 'val_loss',
                                                 verbose = 1,
                                                 save_weights_only=True)

# train
history_D_fashion = model_D_VGG_fashion.fit(X_train,
                                            y_train,
                                            validation_split = 0.25,
                                            batch_size = 128,
                                            epochs = 200,
                                            callbacks = [early,cp_callback])

model_D_VGG_fashion.evaluate(X_test, y_test)

# 313/313 [==============================] - 2s 5ms/step - loss: 0.2621 - accuracy: 0.9195
# [0.2621194124221802, 0.9194999933242798]

VGG16모델을 튜닝해서 사용한 결과 단 한 번의 시도만에 0.92에 가까운 valid accuracy를 확보하게 되었습니다 :)


 
 
이것으로 간단하게 살펴본 VGG 논문 리뷰를 마치겠습니다. 감사합니다 :)

 

오늘 포스팅에는 유명한 Kaggle 신용카드 사기 감지 데이터셋(Credit Card Fraud Detection)을 가지고 데이터 전처리/분석/머신러닝을 하는 과정을 기록할 것입니다. 데이터 EDA를 진행하고 적절한 전처리를 해준 후 머신러닝 모델링을 수행하고 성능 지표를 비교하는 일련의 과정을 전부 담을 예정인데요, 의식의 흐름대로 작성할 예정이라 중간 중간 Tmi도 많고 삽질하는 내용까지도 필터링 없이 기록할 것임을 미리 알려드립니다.

 

1. 데이터 불러오기, 컬럼/결측치/데이터 타입 확인

https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud/data

 

Credit Card Fraud Detection

Anonymized credit card transactions labeled as fraudulent or genuine

www.kaggle.com

!kaggle datasets download -d mlg-ulb/creditcardfraud
!unzip "/content/creditcardfraud.zip"

먼저 캐글 신용카드 사기 감지 데이터셋을 다운로드받아서 가지고 옵니다. 저는 API Command를 복사하여 실행하고 코랩에 다운받아진 파일을 unzip해주는 형식으로 간단히 데이터를 불러왔습니다. 이렇게 하면 파일을 직접 다운로드해서 가져오는것보다 훨씬 빠릅니다.

 준비된 데이터프레임에 총 31개의 column, 284807개의 row를 확인했습니다.

card.info()

info 메소드를 통해 컬럼의 데이터타입과 결측치 여부를 확인했는데, 다행히 모든 컬럼에 결측치 없이 데이터가 잘 들어가 있었고, 정수타입의 Class 컬럼을 제외한 모든 열은 float64 타입임을 확인했습니다. 컬럼명에 들어있는 V1~V28의 경우 어떤 속성인지 알 수는 없고, Amount는 해당 row의 결제 금액, Class는 정상/사기 이진 분류(binary classification) 결과(label) 컬럼에 해당합니다.

만약 object 타입의 컬럼이 있었다면 라벨 인코딩(Label Encoding)이나 원핫 인코딩(One-Hot Encoding) 등의 작업을 통해 값을 숫자로 변환하는 작업이 필요합니다. 다행히 모두 숫자로 이루어져 있기 때문에 따로 인코딩 작업은 들어가지 않아도 될 것 같습니다 :-)

또, NaN 등의 결측치가 있는 경우 적절한 근거를 가지고 대표값으로 결측치를 채워 넣거나 해당 row를 삭제하는 등의 작업을 통해 결측치를 제거해 주어야 합니다. 다행히도 우리의 친절한 캐글 신용카드 데이터셋은 우리에게 그런 노가다를 요구하지 않고 있습니다... (흡족)

 

2. 결제 금액 분포도 그래프로 확인

결제 금액을 나타내는 Amount 컬럼의 분포도를 그래프로 확인해 보겠습니다.

import seaborn as sns
import matplotlib.pyplot as plt

plt.style.use('fivethirtyeight')
plt.figure(figsize = (10, 4))
plt.xticks(range(0, 40000, 1000), rotation = 45)
sns.histplot(card['Amount'], bins = 100, kde = True)
plt.show()

Tmi지만 저는 matplotlib의 테마 중에서 fivethirtyeight를 가장 좋아합니다. 왜인지는 모르겠습니다. 그래서 웬만하면 그래프를 그릴 때 fivethirtyeight으로 테마를 변경한 다음에 그래프를 뽑는 편입니다. 이런 각박한 작업 속에서도 내 취향이란 걸 반영할 수 있다는 게 전 재밌더라구요^_^...;;;

plt.figure(figsize = (8, 4))
plt.xlim(0, 1000)
plt.xticks(range(0, 1000, 50), rotation = 45)
sns.histplot(card['Amount'], bins = 500, kde = True)
plt.show()

다시 본론으로 돌아와서... 0부터 약 500달러 미만의 결제 금액이 차지하는 비율이 압도적으로 많은 것을 확인할 수 있었습니다. 이 그래프를 확인한 후 저는

과하게 치우친 Amount 컬럼의 값을 로그변환하여 보정해 주면 모델의 성능 지표가 상승할 것이다

라는 첫 번째 가설을 세우게 됩니다.

 

3. Amount 컬럼 박스 플롯 그리기

결제 금액 분포도를 살펴본 이후 저는 Amount 컬럼의 박스 플롯을 그려서 이상치를 확인해 봐야겠다는 생각이 들었는데요.

sns.boxplot(data = card, y = 'Amount')

쩝... 네... 첫 번째 삽질 보여드립니다. 방금 위에서 데이터가 쏠려 있음을 확인해놓고 박스플롯이 예쁘게 그려지리라고 생각한 제가 좀 바보같네요. 결제 금액의 박스 플롯은 큰 의미가 없는 것 같으니 다음으로 데이터 프레임의 상관 계수를 확인해 보도록 하겠습니다.

 

4. 상관 계수 시각화하기

card.corr()

corr() 메소드를 이용해서 모든 컬럼 사이의 상관계수를 나타내어 보았습니다. 한 눈에 들어오질 않으니 일단 쉐입을 확인해 볼게요...

card_corr = card.corr()
card_corr.shape
# (31, 31)

31개의 컬럼 사이의 상관계수가 (31, 31) 정사각 쉐입의 데이터프레임으로 이쁘게 반환되었습니다. 저는 저렇게 e어쩌고로 나타내진 숫자값들은 봐도 봐도 적응이 안되더라구요. 어쨌든 이 상태로는 전체 분포를 한 눈에 알아보기 힘들기 때문에 seaborn의 heatmap을 이용해서 그래프로 시각화를 해 보도록 하겠습니다.

두 번째 Tmi... 저는 또 이렇게 색상이 중요한 그래프를 그릴 때 괜히 컬러 팔레트 고르느라 1-2분을 더 낭비하는 것을 좋아합니다.. 역시 각박한 일상 속에서 찾아내는 저만의 소소한 작은 행복입니다 (^_^*) 이번에는 따뜻한 색감의 노랑-브릭 계열의 팔레트를 골라 보았습니다.

plt.figure(figsize = (8, 8))
sns.heatmap(card_corr, cmap = "YlOrBr")
plt.tight_layout()
plt.savefig("corr.png")

그래프를 큼직하게 뽑아서 괜히 savefig를 이용해 png파일로 다운로드까지 해 보았습니다. 이것도 자주 안하면 계속 까먹어요. ㅋ

상관관계 히트맵에서 양의 상관관계가 높을수록 색깔이 진한 갈색에 가깝고, 음의 상관관계가 높을수록 연한 노란색에 가깝습니다. 그래프의 변두리에 희끗희끗하게 보이는 밝은 부분들이 음의 상관관계가 높은 지점입니다. 

이 때, 레이블에 해당하는 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되는데요.

이렇게 레이블과 관계 없는 컬럼의 이상치를 제거하면 모델의 성능 지표가 상승할 것이다

라는 두 번째 가설을 세우게 됩니다. 

 

5. [before] baseline 모델링

(5-1) train, test 구분하고 레이블 비율 확인해 보기

card.drop('Time', axis = 1, inplace = True)

먼저 큰 의미가 없는 Time 컬럼은 drop을 통해 삭제를 해 주고 시작하겠습니다.

def train_test(df):
    card_copy = card.copy()
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = train_test(card)

card 데이터프레임의 마지막 'Class' 컬럼을 기준으로 feature과 label을 구분했구요. 싸이킷런의 train_test_split을 통해 8:2의 비율로 train과 test를 분할했습니다. 3번째 Tmi...인데.. 저는 random_state로 항상 1004(천사)를 사용합니다. ^^;;;;;

len(X_train), len(X_test), len(y_train), len(y_test)
# (227845, 56962, 227845, 56962)

이렇게 나눠졌구요. 그럼 데이터에서 레이블 비율(0과 1의 비율)이 어떻게 되는지 살펴볼까요?

y_train.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')
y_test.value_counts(normalize = True).apply(lambda x: str(round(x * 100,4)) + '%')

와우! 사기에 해당하는 1번 레이블이 약 0.17%에 해당하는 극소수로 확인되었습니다. 하긴, 사기 거래가 2~30%씩 차지하고 있으면 그것도 말이 안되겠네요. 저는 이렇게 뭐든 시각화하면서 가지고 있는 데이터와 친숙해지는 작업이 꼭 필요하다고 생각합니다. 그렇지 않으면 의미 없는 숫자놀음에 그치게 된다고 생각해요,,, 어쨌거나 저쨌거나 이 비율 확인 후 저는 

평가 지표(metrics)로 Accuracy가 아닌 다른 지표들을 전부 다 확인할 필요가 있음

이라는 결론을 내렸고, 다양한 평가 지표를 모두 확인해 보기로 결정했습니다.

(5-2) 모델 학습 후 예측 성능 평가

def train_eval(model, f_tr = None, f_test= None, t_tr= None, t_test= None):
    # 모델 학습
    model.fit(f_tr, t_tr)

    # 예측 
    pred = model.predict(f_test)
    pred_proba = model.predict_proba(f_test)[:, 1]

    # 평가 (1) confusion matrix 출력
    from sklearn.metrics import confusion_matrix
    confusion = confusion_matrix(t_test, pred)
    print("----- confusion matrix -----")
    print(confusion)
    
    # 평가 (2) accuracy / precision / recall / f1 / roc_score 출력
    from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
    accuracy = accuracy_score(t_test, pred)
    precision = precision_score(t_test, pred)
    recall = recall_score(t_test, pred)
    f1 = f1_score(t_test, pred)
    roc_auc = roc_auc_score(t_test, pred_proba)
    print("----- evaluation score -----")
    print(f"accuracy : {accuracy:.4f}")
    print(f"precision : {precision:.4f}")
    print(f"recall : {recall:.4f}")
    print(f"f1 : {f1:.4f}")
    print(f"ROC_SCORE : {roc_auc:.4f}")

 

먼저 baseline 모델을 학습시키고 예측을 수행한 다음 confusion matrix와 여러 평가 지표를 출력하는 함수를 작성했습니다.

from lightgbm import LGBMClassifier
from xgboost import XGBClassifier

lgbm = LGBMClassifier(n_estimators = 1000,
                      num_leaves = 64,
                      n_jobs = -1,
                      boost_from_average = False)

xgb = XGBClassifier(n_estimators = 1000,
                    learning_rate = 0.05,
                    max_depth = 3, 
                    eval_metric = 'logloss')

이후 XGBM, LightGBM 모델 2가지를 학습하고 예측 평가를 수행했습니다.

train_eval(lgbm, X_train, X_test, y_train, y_test)
train_eval(xgb, X_train, X_test, y_train, y_test)

(좌) LGBM / (우) XGB

결과값은 위와 같이 나왔는데요. Accuracy는 대체로 동일하고, precision과 recall, f1은 LightBGM 모델이 더 높았고, roc_auc 점수는 XGB 모델이 더 높게 측정되었습니다.

 

6. [after] 가설 2가지 적용하기

이 번엔 데이터 분석을 통해 세웠던 2가지의 가설을 검증해보는 작업을 수행해 보겠습니다.

레이블과 관계 없는 컬럼의 이상치를 제거하고 Amount 컬럼의 값을 로그변환하여 보정한다면 모델의 성능 지표를 상승시킬 수 있을 것이다

(6-1) 이상치 찾아내기

def find_outliers(df, column):
    # 해당 컬럼의 사기 데이터 series 선언
    frauds = df[df['Class'] == 1][column]
    
    # IQR 구하기
    import numpy as np
    Q1 = np.percentile(frauds.values, 25)
    Q3 = np.percentile(frauds.values, 75)
    IQR = Q3 - Q1

    # 1QR에 1.5를 곱해서 최댓값, 최솟값 구하기
    lowest_value = Q1 - IQR * 1.5
    highest_value = Q3 + IQR * 1.5

    # 이상치 데이터 찾아서 인덱스만 리스트로 뽑아내기
    outliers = frauds[(frauds < lowest_value) | (frauds > highest_value)].index

    # 이상치 인덱스 리스트 리턴
    return outliers

데이터 프레임과 원하는 컬럼명을 인자로 받아서 해당 컬럼의 이상치 인덱스를 리스트로 반환하는 함수를 작성했습니다 :) 처음부터 바로 함수를 작성한 건 아니고, 컬럼 하나를 지정해서 하나하나 실행해본 다음 깔끔하게 함수로 정리하는 작업을 통해 함수를 완성했습니다.

find_outliers(card, "V14")
# Index([8296, 8615, 9035, 9252], dtype='int64')

find_outliers(card, "V17")
# Index([], dtype='int64')

아까 히트맵을 통해 확인했을 때 Class컬럼과 가장 낮은 상관관계를 보이는 컬럼은 V14와 V17인것으로 확인되었었는데요, find_outliers 함수를 통해 확인해 본 결과 V14 컬럼의 경우 이상치가 4개 발견되었고, V17 컬럼의 경우 이상치가 발견되지 않았습니다 :)

 따라서 저는 V14 컬럼의 이상치 데이터 4개를 삭제해 보겠습니다.

 

(6-2) 이상치 제거, 로그 변환 후 train, test 분리

def train_test_no_outliers(df):
    card_copy = df.copy()
    # outlier 제거
    outliers = find_outliers(card_copy, "V14")
    card_copy.drop(outliers, axis = 0, inplace = True)

    # Amount 컬럼 로그변환
    import numpy as np
    card_copy['Amount'] = np.log1p(card_copy['Amount'])
    
    # 데이터셋 나누기
    X_features = card_copy.iloc[:, :-1] # label에 해당하는 마지막 'Class'컬럼 제외
    y_label = card_copy.iloc[:, -1]     # label에 해당하는 마지막 'Class'컬럼만

    from sklearn.model_selection import train_test_split
    X_train, X_test, y_train, y_test = train_test_split(X_features,
                                                        y_label,
                                                        test_size = 0.2,
                                                        random_state = 1004,
                                                        stratify = y_label)
    
    return X_train, X_test, y_train, y_test

X_train_af, X_test_af, y_train_af, y_test_af = train_test_no_outliers(card)

train, test 분리하는 함수에 Amount 컬럼 로그변환과 outlier rows 제거하는 과정을 추가한 다음 분리 작업을 실시했습니다.

(6-3) 모델 학습 후 예측 성능 평가

train_eval(lgbm, X_train_af, X_test_af, y_train_af, y_test_af)
train_eval(xgb, X_train_af, X_test_af, y_train_af, y_test_af)

(좌) LGBM / (우) XGB

다음과 같이 결과가 나왔습니다.

 

7.  모델 4개 비교

지금까지 학습시킨 모델들의 성능 지표를 표로 작성하여 한눈에 비교해 보겠습니다.

  Baseline(XGB) Baseline(LGBM) After(XGB) After(LGBM)
Accuracy 0.9996 0.9996 0.9996 0.9996
Precision 0.9419 0.9405 0.9405 0.9302
Recall 0.8265 0.8061 0.8061 0.8163
F1 0.8804 0.8681 0.8681 0.8696
ROC Score 0.9853 0.9863 0.9882 0.9793

저는 이상치 제거와 로그 변환의 전처리 과정을 거친 다음 학습한 After 모델들의 모든 성능 지표가 다 개선될 것이라고 생각했습니다. 하지만 실습 결과 Precision / Recall / F1의 경우 오히려 Baseline의 수치가 더 높게 나온것을 확인할 수 있었습니다. 역시 인생이란 건 그렇게 호락호락하지 않습니다. 원하는 대로 결과가 안 나와서 좀 찝찝하긴 한데요...

다만, After XGB 모델 ROC-AUC 스코어의 경우 Baseline 모델들보다 점수가 더 상승한 것을 확인할 수 있었습니다. 이번에 다룬 Kaggle 신용카드 사기감지 데이터셋의 경우, 기존에 미리 확인한 대로 전체 데이터의 약 0.17%만이 사기에 해당하는 극도로 치우친 값을 가지고 있었습니다. 이렇게 과하게 편향된 데이터셋의 경우 ROC-AUC 스코어가 유의미하므로, 만약 4가지 모델 중 한 가지를 골라야 한다면 저는 ROC-AUC 스코어가 가장 높게 나온 After(XGB) 모델을 고르는 것도 나쁘지 않겠다는 생각을 했습니다.

 

8. 마무리

이번 포스팅은 여기에서 실습을 마치도록 할텐데요. 위의 4가지 모델에서 성능 지표를 더 올리기 위해서는 0.17%에 해당하는 사기 데이터를 펌핑해서 수를 늘려 주는 방법을 사용하면 좋을 것 같습니다. 마음같아서는 지금 당장 하고싶지만 할일이 많아서 일단 여기에서 끊겠습니다만, 좀 찝찝하기 때문에 시간여유가 생기면 모델 개선 작업을 추가로 수행해서 추가 포스팅을 하도록 하겠습니다.

이번 실습을 통해서 앞으로 다가올 머신러닝 팀프로젝트에 어떤 식으로 데이터 EDA를 진행하고 모델링을 개선해나갈 수 있을 지 좋은 가이드라인이 되었습니다. 

읽어주셔서 감사합니다 :-)

 

 

사이킷런의 프레임워크와 연동할 수 있는 전용 XGBoost 래퍼 클래스에는 분류용 XGBoostClassifier, 회귀용 XGBoostRegressor이 있습니다. 래퍼 클래스는 다음과 같은 장점을 가지고 있습니다.

  • 사이킷런의 기본 estimator를 그대로 상속해 만들었기 때문에 fit()과 predict()만으로 학습과 예측이 가능합니다.
  • GridSearchCV, Pipeline 등 다른 사이킷런의 다른 유틸리티를 그대로 함께 사용할 수 있습니다.
  • 기존의 다른 프로그램의 알고리즘으로 XGBoost 래퍼 클래스를 사용할 수도 있습니다.

 

https://smartest-suri.tistory.com/40

 

ML | 파이썬 XGBoost API 사용하여 위스콘신 유방암 예측하기

XGBoost란?트리 기반의 앙상블 학습에서 가장 각광받고 있는 알고리즘 중 하나로, 캐글(kaggle) 등 경연 대회에서 입상한 많은 데이터 사이언티스트들이 XGboost를 사용하면서 널리 알려지게 되었습니

smartest-suri.tistory.com

 

이전 글에서 학습했던 기본 XGBoost API 대신 사이킷런 연동 XGBoost 래퍼 클래스 XGBoostClassifier를 사용해 모델을 학습시키고 예측을 수행해 보겠습니다.

 


 

 

호출 및 hyperparameter

from xgboost import XGBClassifier
xgb_wrapper = XGBClassifier(n_estimators = 400, # num_boost_round -> n_estimators
                            learning_rate = 0.05, # eta -> learning_rate
                            max_depth = 3, 
                            eval_metric = 'logloss')

 

learning_rate와 같이 기존 사이킷런 하이퍼 파라미터와의 호환성 유지를 위해 변경된 하이퍼 파라미터들이 있으므로 유의합니다.

 

 

 

학습 및 예측

fit()과 predict() 메소드를 이용해서 모델을 학습시키고 예측을 수행해 보겠습니다.

# 학습
xgb_wrapper.fit(X_train, y_train, verbose = True)

# 예측
w_preds = xgb_wrapper.predict(X_test)
w_preds[:10]
# array([1, 0, 1, 0, 1, 1, 1, 1, 1, 0])

w_pred_proba = xgb_wrapper.predict_proba(X_test)[:, 1]
w_pred_proba[:10]
# array([8.8368094e-01, 2.7957589e-03, 8.9875424e-01, 1.8748048e-01,
#        9.9204481e-01, 9.9990714e-01, 9.9954444e-01, 9.9904817e-01,
#        9.9527210e-01, 1.9664205e-04], dtype=float32)

 

아주 빠르고 간단하게 학습과 예측을 수행했습니다.

 

 

평가

이전 포스팅에서 작성해 둔 get_clf_eval() 함수를 이용해서 사이킷런 래퍼 XGBoost로 만들어진 모델의 예측 성능 평가를 해 보겠습니다.

get_clf_eval(y_test, w_preds, w_pred_proba)

지표 이전 이후
Accuracy 약 0.96 약 0.97
Precision 약 0.97 약 0.97
Recall 약 0.97 약 0.98
F1-Score 약 0.97 약 0.98
ROC-AUC 약 0.99 약 0.99

 

이전 실습보다 평가 지표가 조금 상승했습니다. 이번 실습에서는 early stopping을 따로 설정하지 않은 관계로 train 데이터를 train과 valid 데이터로 나누는 과정을 생략하였고, 그래서 트레인 데이터셋의 수가 늘어난 영향이 있을 것으로 파악됩니다. (애초에 트레이닝 데이터가 풍부한 데이터셋은 아닌 관계로)

 

 

여기까지 XGBoost 관련 두 번째 실습을 마쳐봅니다. 감사합니다 :-)

 

XGBoost란?

트리 기반의 앙상블 학습에서 가장 각광받고 있는 알고리즘 중 하나로, 캐글(kaggle) 등 경연 대회에서 입상한 많은 데이터 사이언티스트들이 XGboost를 사용하면서 널리 알려지게 되었습니다. 대체로 분류에 있어서 뛰어난 예측 성능을 보이는 특징을 가지고 있습니다.

 

XGboost는 GBM에 기반하고 있는데요. GBM보다 빠르게 학습이 가능하고 오버핏팅 규제 부재 문제 등을 해결한다는 장점이 있다고 합니다. 그 밖에도 Tree pruning이 가능하여 더 이상 긍정 이득이 없는 분할을 가지치기 해서 분할 수를 더 줄이는 추가적인 장점, 자체 내장된 교차 검증 기능, 결손값을 자체 처리할 수 있는 기능 등의 장점도 가지고 있습니다.

 

XGBoost API 학습을 위해 위스콘신 유방암 데이터로 실습을 진행해 본 결과를 정리하여 공유하고자 합니다 :-)

 

 

 

sklearn dataset 위스콘신 유방암 데이터 불러오기

sklearn의 자체 내장 데이터인 load_breast_cancer을 불러오겠습니다.

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings('ignore')

dataset = load_breast_cancer()
features = dataset.data
labels = dataset.target

cancer_df = pd.DataFrame(data = features, columns = dataset.feature_names)
cancer_df.head(3)

cancer_df['target'] = labels
cancer_df.head(3)

 

데이터프레임 마지막에 target 컬럼을 추가하여 각 row의 label을 0 또는 1로 나타냈습니다.

dataset.target_names
# array(['malignant', 'benign'], dtype='<U9')

cancer_df['target'].value_counts()
# target
# 1    357
# 0    212
# Name: count, dtype: int64

 

label이 무엇이 있는지 확인해 보니, 0은 malignanat, 1은 benign임을 확인할 수 있었고, 각각 212개와 357개가 있는 것을 확인할 수 있었습니다.

 

 

train, valid, test 데이터셋 나누기

X_features = cancer_df.iloc[:, :-1]
y_label = cancer_df.iloc[:, -1]

 

먼저 target 컬럼을 제거한 X_feature 데이터프레임, target 컬럼만 떼어낸 y_label 데이터프레임을 생성합니다.

# 8:2 비율로 train:test 나누기
X_train, X_test, y_train, y_test = train_test_split(X_features, y_label, 
                                                    test_size = 0.2, 
                                                    random_state = 156)
                                                    
# 9:1 비율로 다시 train:valid 나누기
X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, 
                                            test_size = 0.1, 
                                            random_state = 156)

 

train_test_split() 메소드를 이용하여 8:2 비율로 train:test 데이터셋을 나누고,

나누어진 train은 다시 한 번 9:1 비율로 train:valid으로 나누었습니다.

len(X_tr), len(X_val), len(y_tr), len(y_val), len(X_test), len(y_test)
# (409, 46, 409, 46, 114, 114)

X_tr.shape, y_tr.shape
# ((409, 30), (409,))

 

train, valid, test 데이터의 개수와 shape 등을 확인해 보았습니다.

 

 

 

DMatrix

파이썬 래퍼 XGBoost는 전용 데이터 객체인 DMatrix를 사용합니다. DMatrix의 주요 입력 파라미터는 data와 label입니다.

  • data : 피처 데이터 세트
  • label : (분류) 레이블 데이터 세트 / (회귀) 숫자형인 종속값 데이터 세트
dtr = xgb.DMatrix(data = X_tr, label = y_tr)
dval = xgb.DMatrix(data = X_val, label = y_val)
dtest = xgb.DMatrix(data = X_test, label = y_test)

 

 

Hyperparameter

딕셔너리 형태로 하이퍼 파라미터를 설정합니다.

params = {
    'max_depth' : 3,  # 트리의 최대 깊이는 3
    'eta' : 0.05,     # 학습률 0.05
    'objective' : 'binary:logistic',
    'eval_metric' : 'logloss'
}

num_rounds = 400       # 부스팅 반복 횟수는 400회

 

 

XGBoost 모델 학습

  • early_stopping_rounds : 더이상 지표 개선이 없을 경우에 횟수를 모두 채우지 않고 중간에 반복을 빠져 나올 수 있도록 하는 탈출 도구 (조기 중단, 얼리스탑핑)
  • evals : 평가용 데이터 세트, [(학습튜플), (평가튜플)] 형태 - 반복마다 지정된 평가용 데이터 세트에서 eval_metric의 지정된 평가 지표로 예측 오류를 측정 
xgb_model = xgb.train(params = params,
                      dtrain = dtr,
                      num_boost_round = num_rounds,
                      early_stopping_rounds = 50, # 50번째부터 얼리스탑핑 가능
                      evals = [(dtr,'train'), (dval,'eval')])

400회를 다 채우지 못하고 250회에서 중단되었습니다

 

예측 수행

predict() 메소드를 이용해서 test 데이터의 결과를 예측해 보겠습니다.

pred_probs = xgb_model.predict(dtest)

 

예측한 결과를 다양하게 살펴봅시다. (초반 10개값만 봅시다.)

pred_probs[:10]
# array([9.3829882e-01, 3.6695320e-03, 7.5020140e-01, 4.9393266e-02,
#        9.8045909e-01, 9.9958366e-01, 9.9899417e-01, 9.9919862e-01,
#        9.9767953e-01, 5.2314228e-04], dtype=float32)

 

어떤 값인지 한 눈에 보이지 않으므로 np.round()를 이용해서 살펴보겠습니다.

np.round(pred_probs[:10])
# array([1., 0., 1., 0., 1., 1., 1., 1., 1., 0.], dtype=float32)
# 일의 자리까지 반올림

np.round(pred_probs[:10], 3)
# array([0.938, 0.004, 0.75 , 0.049, 0.98 , 1.   , 0.999, 0.999, 0.998,
#       0.001], dtype=float32)
# 소수 셋째 자리까지 반올림

 

반올림을 해서 보니까 예측한 값이 0과 1 사이의 값으로 도출되었음을 확인할 수 있었습니다. 

이제 예측 확률이 0.5보다 크면 1, 그렇지 않으면 0으로 최종 예측값을 결정하여 preds 리스트에 저장하겠습니다.

preds = [1 if x > 0.5 else 0 for x in pred_probs]
preds[:10]
# [1, 0, 1, 0, 1, 1, 1, 1, 1, 0]

 

 

get_clf_eval() 함수

모델을 평가할 수 있는 함수를 작성해 보겠습니다.

from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, f1_score, roc_auc_score

def get_clf_eval(y_test, pred = None, pred_proba = None):
    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_score(y_test, pred)
    roc_auc = roc_auc_score(y_test, pred_proba)
    print('Confusion Matrix')
    print(confusion)
    print(f'accuracy : {accuracy}, precision : {precision:.4f}, recall : {recall:.4f}')
    print(f'f1 : {f1}, roc_auc : {roc_auc}')
get_clf_eval(y_test, preds, pred_probs)

  • 정확도 accuracy 약 0.96
  • 정밀도 precision 약 0.97
  • 재현율 recall 약 0.97
  • F1-스코어 0.97
  • ROC-AUC 약 0.99

평가 지표가 굉장히 좋네요 :-)

 

 

XGBoost 내장 시각화 기능 수행하기

XGBoost의 plot_importance() API피처의 중요도를 막대그래프 형식으로 나타냅니다.

  • 기본 평가 지표로 f스코어를 기반으로 해당 피처의 중요도를 나타냅니다.
  • f스코어는 해당 피처가 트리 분할 시 얼마나 자주 사용되었는지를 지표로 나타낸 값입니다.
  • xgboost 패키지는 plot_importance()를 이용해 바로 피처 중요도를 시각화할 수 있습니다.
  • plot_importance() 호출 시 파라미터로 앞에서 학습이 완료된 모델 객체 및 matplotlib의 ax 객체를 입력하면 됩니다.
import matplotlib.pyplot as plt
%matplotlib inline

fix, ax = plt.subplots(figsize = (10, 12))
plot_importance(xgb_model, ax = ax)
plt.tight_layout()
plt.savefig('plot.png')

 

 

 

여기까지 XGBoost API 실습 포스팅이었습니다 :-)

감사합니다.

 

 

지난 글에서 서울특별시 공중화장실 공중데이터를 판다스 데이터프레임으로 만들고 간단히 정제작업을 해 보았는데요. 정제한 데이터프레임을 가지고 태블로를 이용해서 아주 간단히만 시각화 작업을 진행해 보았습니다.

 

 

 

대시보드 구성 방법

  1. 서울시의 25개 구별 공중화장실 수 합계를 계산하여 그 수를 비교할 수 있도록 시각화했습니다.
  2. 대시보드의 왼쪽에는 지도를 배치하여 화장실의 수를 원의 크기와 색깔로 직관적으로 파악할 수 있도록 구성했습니다
    • 지도를 확대하면 보이지 않는 레이블을 모두 확인할 수 있어요.
    • 화장실 수가 많을수록 원의 크기가 큽니다.
    • 화장실 수가 많을수록 원의 색깔이 진합니다.
  3. 대시보드의 오른쪽에는 가로막대그래프를 배치하여 수치별로 좀더 직관적인 비교가 가능하도록 구성했습니다.
    • 오른쪽의 비교 파라미터를 이용해서 비교선을 100단위로 조절하면서 이동시킬 수 있어요.
    • 비교선을 움직이면서 비교선을 기준으로 색깔이 바뀌는 것을 확인할 수 있어요.

 

태블로 퍼블릭으로 보러가기 (클릭)

 

지하철

지하철

public.tableau.com

클릭하시면 태블로 퍼블릭 웹사이트에서 인터렉티브하게 직접 결과를 조절해 보실 수 있습니다.

 


 

 

<시각화 이후 생각해볼만한 것들>

 

1. 구별 화장실 수와 구별 면적의 관계는 어떻게 되는가?
2. 구별 화장실 수와 지하철 역의 개수의 상관관계가 있는가?
3. 구별 화장실 수와 구별 인구 수의 관계는 어떻게 되는가? 인구와 구별 화장실 총 수는 비례하는가?
4. 서울특별시 구별 장애인 화장실 데이터를 따로 구할 수 있는가? 구할 수 있다면, 전체 화장실과 장애인 화장실의 비율을 비교해 보자.
5. 상업 단지와 화장실 수의 상관관계가 있는가?

6. 관광 구역과 화장실 수의 상관관계가 있는가?

7. 공공화장실 중에서 지하철 역 화장실의 개수를 특정할 수 있는가? 있다면 그 비율은 어떻게 되는가?

8. 지하철 역 화장실 개수를 구할 수 있다면, 지하철 노선별 유동인구 데이터와 병합하여 화장실의 갯수가 적절하게 비치되어 있는지 비교해보자.

 

 

 

여기까지입니다 :-) 좀더 생각해볼 수 있을것 같지만 본격적인 개인 EDA 프로젝트 준비를 위해서 이번엔 이정도로 간단히만 포스팅을 마치려고 합니다. 쉽고 짧은 작업이었지만 맘에드는 공공데이터를 구하고, 정제하고, 시각화 후 생각해볼거리 도출까지 은근 시간이 걸렸네요. 다음엔 더 능숙하고 멋진 프로젝트를 가져와서 공유해보겠습니다. 감사합니다. :)

 

 


 

 

(+) 판다스 데이터프레임 CSV파일로 내보내기 한 후 태블로에 불러온 과정

 

t.to_csv("toilet_df.csv")

 

먼저 to_csv() 메소드를 통해 간단하게 csv파일로 내보내기를 해주었습니다. 저는 구글 코랩에서 실습을 진행했습니다.

 

 

 

 

 

구글 코랩의 왼쪽 파일메뉴에서 간단하게 바로 다운로드를 해서 다운로드 폴더에 넣어 주었는데요. 로컬 환경에 따로 다운로드하지않고 태블로 퍼블릭 환경에서 구글 드라이브를 연동해서 바로 오픈해도 됩니다.

 

 

 

 

 

태블로 퍼블릭 프로그램을 실행하고 로컬 환경에 다운로드한 toilet_df.csv 파일을 오픈했습니다.

 

 

 

따로 사용하지 않을 예정인 산지, 부지번 컬럼은 숨기기(Hide) 해주었습니다. 이후 시트 2개에서 작업을 하고 대시보드 1개에서 두개를 합쳐주는 방식으로 간단히 끝을 내주었습니다.

 

 

 

 

주제 : 심장과 혈관 의학 분야에서의 인공지능과 데이터과학

일시 : 2024년 4월 4일 (목) 20:00 - 21:10

강사 : 미국 Mayo Clinic 이은정

 

 

서론

4월 4일 목요일 Zoom을 이용한 실시간 화상 온라인 방식으로 진행된 이은정 강사님의 <심장과 혈관 의학 분야에서의 인공지능과 데이터과학> 세미나에 참여했습니다. 이은정 강사님께서는 서울대학교에서 석박사 과정을 마치신 뒤 미국에서 가장 큰 병원 중 하나인 Mayo Clinic에서 Senior Data Science anlayst로 계시면서 다양한 의학 인공지능 모델을 개발하고 데이터과학을 연구하고 계신 멋진 분이셨습니다. 저는 현재 고려사이버대학교에서 최대영 교수님의 빅데이터 개론 수업을 듣고 있는데, 교수님께서 본 세미나에 참가할 수 있도록 초대해 주신 덕분에 좋은 기회로 참여할 수 있었습니다.

 

제 주변에 의학 관련 분야에 종사하는 지인이나 친인척이 몇 있습니다. 그래서 저는 평소 의학 분야에도 어느 정도 관심을 가지고 있었습니다. 특히 의료 분야에서 데이터의 중요성, 인공지능의 빠른 성장에 대해서는 익히 들어본 바가 있어 큰 호기심을 가지고 있었습니다. 그러나 의료 분야는 어쩐지 진입장벽이 있다고 느껴졌어요. 어디서부터 관련 도메인 지식을 수집해 나갈지 막막했습니다. 그렇게 멀게만 느껴지던 의학 도메인에 조금 더 가까워지는 계기가 되기를 바라며 강의를 듣기 시작하였습니다.

 

본론

(1)

 

먼저 강의는 미국의 심혈관 질환에 대한 통계 자료와 분석으로 시작하였습니다. 2016년 데이터를 기준으로 미국 인구의 약 절반이 고혈압을 가지고 있는 것으로 추정되며, 2035년까지 미국 인구의 약 45%가 심장 관련 질병 가지게 될 것으로 예상된다고 하는데요. 이를 뒷받침하는 자료로 미국에서는 성인 5명 중 오직 1명만이 적정한 양의 운동을 하고, 전자담배의 사용으로 흡연률이 치솟고 있는 등 다양한 통계를 함께 볼 수 있었습니다.

 

2022년 잠깐이나마 뉴욕에서 어학연수를 했던 기억을 떠올려 보았습니다. 뉴욕에는 확실히 한국보다 다양한 체형을 가진 사람들이 있었어요. 다양한 사이즈의 체형이 용납되는 개방적이고 자유로운 사회적 분위기가 저는 참 좋았던 기억이 있습니다. 지금 돌아보니 빅 사이즈 국민들의 건강 관리가 그만큼 중요한 과제가 될 수도 있겠다는 생각이 드네요. 이렇게 미국에서는 큰 사회적 이슈가 되고 있는 심혈관 질병 진단과 관리를 위해 인공 지능이 다양하게 개발되고 활용되고 있다고 합니다.

미국에서 심혈관 질환 관련 의료 마켓에서 AI의 성장률을 확인해볼 수 있는 그래프 자료입니다.

(2) - 1

다음으로 심혈관 질환을 위한 인공지능 모델 개발을 위한 데이터 자료에 관해 말씀해 주셨습니다. AI 트레이닝, 테스팅 데이터로 활용되는 가장 대표적인 데이터 자료 두 가지는 환자 기록과 검사 결과입니다. 환자 기록으로는 몸무게, 키, 혈압, 피검사, 환자 내원시 상담 내용 등이 있습니다. 검사 결과로는 ECG라고 불리는 심전도 검사 결과, 심초음파, 혈관조영상, 망막이미지 등이 있습니다. (심혈관 질병 관련)

 

요즘은 스마트 워치로도 간단하게 심전도 측정이 가능한 세상입니다. 이렇게 스마트 워치를 활용하여 측정한 심전도 자료 역시 인공지능 개발에 활용이 되기도 한다고 합니다. 물론 병원의 전문 장비를 이용한 측정 결과랑 비교하자면 신뢰도가 많이 떨어지기 때문에, 이렇게 신뢰도가 낮은 데이터를 가지고 모델 개발을 하면 그만큼 정확도가 떨어질 수밖에 없다고 하셨습니다. 그래도 집에서 간단하게 간이 방식으로 심전도를 측정하여 문제 상황을 조금이나마 예측하고 예방할 수 있다면 큰 도움이 되겠지요. 언젠가 병원의 전문 장비만큼 실력이 짱짱한 스마트 워치가 보급될 지도 모를 일입니다.

 

망막 이미지와 목소리에 관련해 말씀해 주신 부분이 굉장히 흥미로워 기억에 남는데요. 요즘은 망막 이미지 하나만으로도 나이, 성별, 흡연여부, 혈압 비만도 등의 건강 정보를 예측을 할 수 있는 수준으로 모델 개발이 이루어져 있다고 합니다. 굉장히 신기했어요. 또 목소리를 이용하여 심장 관련 진환을 예측을 할 수 있다고도 하셨습니다. 목소리라고 하면 기관지 컨디션이나 기분 정도만 짐작해볼 수 있는게 아니냐고들 생각하지만 목소리에는 예상 외로 몸에서 발생하는 이상 신호들이 잘 반영된다고 합니다. 생각보다 예측율이 높은 편이라고 해서 정말 신기했어요. 이렇게 상식을 뛰어 넘는 재밌는 모델링 연구 작업에 저도 참여할 수 있다면 얼마나 좋을지 기대가 되었습니다. 의료 도메인에도 꾸준한 관심을 가지고 포트폴리오를 구축해 나가 봐야 겠다는 욕심이 들었어요.

 

(2) - 2

다음으로 데이터를 활용해 어떤 모델을 개발할 수 있는지에 대해 말씀해 주셨습니다. 가장 큰 연구가 이루어지고 있는 분야는 바로 질병을 예측할 수 있는 모델 개발이라고 합니다. 가격이 비싼 혈관조영상 촬영 없이 상대적으로 저렴한 심장 박동, 심초음파 검사 결과만으로 좌심방의 크기와 대동맥 판막 협착증을 예측하는 모델을 예시로 들어 주셨습니다. 초기에 잡아냈다면 미리미리 관리하여 쉽게 치료할 수 있었던 질병들을 뒤늦게 발견해서 큰 문제가 되는 경우가 많잖아요. 저희 외조모께서도 대장암을 초기에 발견하지 못해 결국 투병하시다가 2년만에 돌아가셨었거든요. 인공지능 모델을 통해 세계적으로 질병 예측의 시기가 앞당겨지고 정확성이 크게 증진될 수 있기를 바라는 마음입니다.

 

또 기존에는 상위 검사만으로 측정이 가능했던 수치들을 하위 검사로 측정 가능할 수 있도록 돕는 모델 개발에 관해서도 언급하셨습니다. 상위 검사는 검사 방법이 복잡한 대신 정확도가 높은 특징이 있습니다. 그만큼 비용이 높을 수밖에 없는데, 특히 미국의 악랄한 의료비에 대해서는 다들 잘 알고 계시지요. 대부분의 상위 검사들을 한국에서는 어렵지 않게 받을 수 있지만 미국에서는 그렇지가 못한 현실이라고 해요. 이렇게 상위 검사만으로 측정 가능했던 수치들을 하위 검사 결과로부터 예측해낼 수 있는 모델들을 개발하는 겁니다. 이게 보편화가 된다면 비용때문에 의료 서비스를 받지 못했던 사회적 약자들에게 커다란 도움이 되겠다는 생각을 했습니다. 저도 뉴욕에 있을 때 갑자기 엄청난 복통이 찾아와 응급실에 갔다가 아픈 것보다도 병원비를 걱정하느라 마음 고생을 했던 경험이 있거든요. 질 좋은 의료 서비스를 모두가 평등하게 받을 수 있는 세상을 만드는 데 저도 도움이 될 수 있으면 좋겠습니다.

 

그 밖에 인공지능 모델 개발을 위해 데이터 레이블링하는 작업에 큰 시간과 비용이 소요되는데, 이런 레이블링을 대신하는 모델을 역시 활발하게 개발이 이루어지고 있다고 알려 주셔서 흥미로웠습니다. 인공지능 모델 개발을 돕는 모델이라니! 미래에는 인공지능끼리 주르륵 체인을 이루면서 인간의 개입이 전혀 필요하지 않은 세상이 언젠가 오겠다는 귀엽고도 무서운 생각을 해 봤어요. 그 안에 나의 역할이 무언가 있기를 바랄 뿐입니다.

 

(3)

마지막으로 메이요 클리닉에서 개발하고 있는 인공지능 모델 예제를 몇가지 알려 주셨습니다. 간단히만 정리해 보겠습니다.

 

[1] 정상 심장 리듬에서 심방세동을 예측하는 모델

  • 심방세동은 간헐적으로 발생하며 특이한 증상이 없는 경우가 많아서 진단에 어려움이 많다.
  • 심방세동의 정확한 진단 및 예측을 위해서는 환자가 병원에 긴 시간 내원해야 한다. 대체로 환자가 24시간동안 몸에 리드줄을 부착하고서 수집한 데이터로 판단을 내리는 방식으로 진단이 이루어졌다고 한다.
  • 메이요 클리닉에서 보관하고 있는 18만명이 넘는 환자들의 데이터, 65만개가 넘는 ECG 기록을 가지고 단시간 측정한 정상 리듬에서도 심방세동을 예측해낼 수 있는 인공지능 모델을 개발하였다.
  • 기존의 진단이 여러 전문 인력의 수작업으로 이루어져 시간과 자원이 과하게 소모되었으나 이 과정들을 인공지능으로 대체할 수 있게 되면서 시간과 자원 비용을 크게 절약하게 되었다.

[2] 비대성 심근병증 (HCM) 분류

  • 비대성 심근병증은 심장 근육이 비정상적으로 두꺼워져 형태가 변형되고 기능이 악화되는 질환으로, 특히 미국에서는 HCM으로 돌연사하는 프로, 아마추어 운동선수가 많아서 사회적 이슈라고 한다.
  • 메이요 클리닉에서 저렴한 심장 박동 검사 결과만을 가지고 HCM을 예측할 수 있는 모델을 개발하였다. 심장 박동 검사는 비용이 저렴할 뿐만 아니라 다양한 시설에 보편화가 많이 되어 있어서 접근성이 무척 높은 하위 검사이다.
  • 테스팅 어큐러시가 0.95 - 0.97으로 무척 높았는데, 미국 뿐만 아니라 다른 여러 나라들에서도 이벨류에이팅을 해 보니 역시 높은 어큐러시 결과가 나왔다고 한다.

[3] ECG(심전도)를 이용한 좌심실 이완기능 평가

  • 좌심실의 이완 기능은 심장 기능 평가에 있어서 아주 중요한 사항이다. 좌심실 이완 시 높은 filling pressure는 다양한 심혈관 질환과 관련되는 악조건이다. 현재 미국에서 70세 이상의 노인의 70%가 불완전한 좌심실 이완 기능을 가지고 있다는 통계 결과가 있다고 한다.
  • 좌심실 이완 기능 평가는 혈관조영상으로 판단하는 것이 가장 이상적이다. 그러나 혈관조영상 촬영은 굉장히 어렵고 복잡하며 비용이 높은 상위 검사이다. 이를 대체하기 위해 심초음파 결과를 이용하는 경우가 많은데, 정확성이 매우 떨어진다고 한다.
  • 메이요 클리닉에서 ECG(심전도) 검사 결과를 통해 좌심실 이완 기능을 평가하는 인공지능을 개발하였다. 검사 결과로 Grade 1-2-3 세 단계의 등급을 매기게 되는데, 등급이 높을 수록 위험한 상태를 의미한다.

 

결론

이번 이은정 강사님의 세미나는 심혈관 질환의 예측과 진단에 데이터와 인공지능이 어떻게 활용되고 있는지 그 실제 사례를 알아볼 수 있는 무척 좋은 학습 기회였습니다. 평소 '미래에는 인공지능이 의사를 대체할 것이다!'는 무시무시한 이야기를 들어만 보았지, 실제로 병원에서 어떤 식으로 개발이 되고 활용이 되고 있는지 그 개별 사례를 알아보기가 쉽지는 않았거든요. 강사님께서 실제 사례와 경험을 토대로 강의해 주신 덕분에 앞으로 심혈관 질환 뿐만 아니라 다른 분야에서도 인공지능 적용 사례를 찾아보기가 수월해질 것 같습니다.

 

이런 의료 도메인에서의 인공지능의 개발은 질 높은 의료 서비스를 더 많은 사람들이 받을 수 있도록 돕습니다. 그만큼 사회적, 인도적으로 큰 의미가 있다고 생각해요. 과학 기술 발달의 바람직한 예라고 할 수 있겠죠. 특히 저는 초등교육에 제 20대 모두를 바쳤던 만큼 어린이와 청소년의 신체건강, 정신건강에 특히 큰 관심을 가지고 있는데요. 학생 개인정보 보호를 위해 자세한 사례를 여기에 세세히 밝힐 수는 없지만, 타고난 유전병으로 인해 자유로운 활동이 어려운 학생을 가르쳐 보았고, 신체적 장애를 가지고 있어 신체활동에 제약이 있는 학생도 가르쳐 보았고, 자폐 스펙트럼을 가지고 태어나 친구를 사귀기 어려워하는 학생도 가르쳐 보았습니다. 가정환경이 어려워 필요한 만큼 의료 서비스를 받지 못하는 친구도 있었는데 제가 도울 방법이 제한적이라 참 안타깝고 미안했었어요. 이렇게 다양한 어려움을 가지고 있는 학생들을 도울 수 있는 인공지능 모델에는 무엇이 있을지 앞으로 계속해서 고민해 보고, 관련 데이터를 찾아보고 분석해 보고자 합니다.

 

질의응답 시간에 이은정 강사님께 어린이를 대상으로 인공지능 모델을 개발해 본 경험이 있으신지 질문을 드렸었는데요. 어린이들은 신체적으로 어른과 무척이나 다르기 때문에 어른을 대상으로 개발한 모델을 어린이들에게 동일하게 적용하기는 어렵다고 하셨습니다. 따라서 영유아나 어린이를 대상으로 한 모델의 경우 성인을 대상으로 개발한 성공적인 모델을 가지고 수정 보완하여 만들어내는 경우가 많다고 하셨어요. 좋은 답변이 되었습니다. 추후에 시간을 내어 구체적인 사례를 찾아보기로 하였습니다.

 

마지막으로 이렇게 개발된 인공지능 모델이 완전히 전문인력을 대체하고 있는 상황은 아니라고 말씀을 해 주셨습니다. 의료진이 진단을 하고 판단을 내리는 데 근거가 되는 하나의 수단으로 인공지능 모델의 예측 결과를 활용하고 있다고 하셨어요. 데이터를 분석하고 모델을 개발할 줄 아는 능력 있는 데이터 사이언티스트는 이렇게 원하는 분야의 전문 인력과 협업할 수 있구나! 좋은 자극이 되었습니다. 열심히 노력해서 저 역시 이렇게 세상의 발전에 기여하고 다른 이들에게 영감이 되는 전문가가 되겠다고 다짐하며, 이번 세미나 리뷰를 마칩니다.

 

 

 

읽어주셔서 감사합니다.

 

+ Recent posts