매직코드
article thumbnail

프로젝트를 하게된 이유

처음 데이터사이언티스트로 취업했을 때는 겨우 머신러닝을 사용할 줄 아는 병아리였는데 그래도 시간이 지나면서 다양한 데이터들을 다루고 모델들을 구축하다보니 점점 그 범위가 확장되어져갔다.

정형데이터를 이용한 머신러닝에 만족하다가 점점 딥러닝, 컴퓨터비전으로 영역을 넓히기도 했고, 그러다보니 자연스럽게 멀티모달에도 관심이 생겨서 토이프로젝트를 진행해봤다.

 

그 중에 정형데이터 + 오디오 멀티모달을 먼저 하게된 이유는 쉬워보였기 때문이다.

멀티모달을 하고자 마음먹고 여러가지 레퍼런스들을 찾으며 공부하는데 오디오 멀티모달이 가장 원초적이고 입문하기에 허들이 낮은편인것 같았다. 이미 오디오데이터도 다뤄본 적이 있었기 때문에 이해하기도 쉬웠다. 혹시 오디오데이터를 다뤄본적이 없다면 오디오데이터를 이용한 프로젝트를 먼저 해보는걸 추천한다.

 

 

개요

멀티모달이란?

모델을 학습시킬 때 사용하는 원천데이터의 형태가 2가지 이상인 경우를 의미한다.

이번 프로젝트에서는  오디오데이터(비정형)와 환자정보(정형) 데이터를 사용했기 때문에 멀티모달 프로젝트다.

 

중간에 오디오데이터를 수치화하여 데이터프레임으로 바꿨기 때문에 언듯 봐서는 그냥 데이터프레임 학습인 것 같지만

데이터프레임에 포함되어있는 오디오데이터를 해석할 수 없기때문에 여전히 비정형데이터라고 할 수 있다.

따라서 모델에 학습시키는 데이터의 종류가 비정형과 정형으로 2가지 종류이기 때문에 멀티모달이다.

 

또 하나 헷갈릴 수 있는 경우로는 모델학습이 완료되고 나면 예측에 필요한 데이터의 종류가 한가지인 경우가 있을 수 있는데

멀티모달은 모델을 학습시킬 때 데이터의 종류로 판단하기 때문에 이미 완성된 모델에는 데이터종류가 한가지여도 학습때 데이터 형태가 2종 이상이었다면 멀티모달 모델로 판단할 수 있다.

 

데이터 다운로드

내가 사용한 데이터는 DACON에 올라와있는 <월간 데이콘 음향 데이터 COVID-19 검출 AI 경진대회> 에서 다운받았다.

검사자의 음향 데이터(기침소리)와 건강상태데이터(호흡기상태, 근육통 등)를 다운받을 수 있다.

 

문제 해결 방안

오디오 데이터는 librosa라는 패키지를 이용해 손쉽게 수치화할 수 있다.

정형데이터와 수치화한 오디오데이터를 데이터프레임으로 결합한 후 간단한 분류 모델을 만들어봤다.

 

실습코드

import warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import os
import joblib
import random
from tqdm import tqdm

# 오디오 데이터처리
import librosa
import librosa.display
import IPython.display as ipd
from imblearn.over_sampling import SMOTE
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.model_selection import train_test_split

# 모델
from copy import deepcopy
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score

 

변수설정

# 오디오 데이터 처리하는데 필요한 변수
# 사람목소리는 대부분 16000 안에 포함됨
sample_rate = 16000

# mfcc coefficient 개수
n_mfcc = 30

# DNN 모델에 필요한 변수
n_epochs = 100
batch_size = 128

# gpu사용 변수
device = torch.device('cuda:3') if torch.cuda.is_available() else torch.device('cpu')

 

 

정형데이터 확인

def get_meta_data(csv_path):
    df = pd.read_csv(csv_path)
    
    # 성별이 male, female로 적혀있어서 숫자로 변경
    df['gender'] = np.where(df['gender']=='male', 0, 1)
    return df
train_df = get_meta_data('../../data/covid19/train_data.csv')
test_df = get_meta_data('../../data/covid19/test_data.csv')

좌) train_df.head()    우) test_df.head()

 

오디오 데이터 확인 및 특징 추출

# 오디오 확인
ipd.Audio('../../data/covid19/train/00001.wav')
def get_data(data_path, df, train=True):
    ids = []
    labels = []
    ages = []
    genders = []
    respiratories = []
    fever_or_muscle = []
    mfccs = []
    
    for file in tqdm(os.listdir(data_path)):
        if 'wav' in file:
            data_id = int(file[:-4])
            ids.append(data_id)
            
            wav_path = os.path.join(data_path, file)
            data, sr = librosa.load(wav_path, sr=sample_rate)
            mfcc = librosa.feature.mfcc(y=data, sr=sr, n_mfcc=n_mfcc)
            
            age = int(df[df.id==int(file[:-4])].age)
            ages.append(age)
            
            gen = int(df[df.id==int(file[:-4])].gender)
            genders.append(gen)
            
            resp = int(df[df.id==int(file[:-4])].respiratory_condition)
            respiratories.append(resp)
            
            pain = int(df[df.id==int(file[:-4])].fever_or_muscle_pain)
            fever_or_muscle.append(pain)

            mean_mfcc = []
            for m in mfcc:
                mean_mfcc.append(np.mean(m))
            mfccs.append(mean_mfcc)
            
            if train:
                label = int(df[df.id==int(file[:-4])].covid19)
                labels.append(label)
            # break
                
    if train:
        meta_data = {'id':ids,
                     'label':labels,
                     'age':ages,
                     'gender':genders,
                     'respiratory':respiratories,
                     'fever_or_muscle':fever_or_muscle
                    }
        df_meta = pd.DataFrame(meta_data)
        return df_meta, mfccs
    else:
        meta_data = {'id':ids,
                     'age':ages,
                     'gender':genders,
                     'respiratory':respiratories,
                     'fever_or_muscle':fever_or_muscle
                    }
        df_meta = pd.DataFrame(meta_data)
        return df_meta, mfccs
def make_train_mfcc_df(data_path, df):
    df_meta, mfccs = get_data(data_path, df, train=True)
    df_mfcc = pd.DataFrame(mfccs, columns=['mfcc' + str(i+1) for i in range(n_mfcc)])
    df = df_meta.join(df_mfcc)
    df.to_csv('../../data/covid19/result/train_mfcc.csv', index=False)
    return df
    
def make_test_mfcc_df(data_path, df):
    df_meta, mfccs = get_data(data_path, df, train=False)
    df_mfcc = pd.DataFrame(mfccs, columns=['mfcc' + str(i+1) for i in range(n_mfcc)])
    df = df_meta.join(df_mfcc)
    df.to_csv('../../data/covid19/result/test_mfcc.csv', index=False)
    return df
    
train_mfcc = make_train_mfcc_df('../../data/covid19/train', train_df)
test_mfcc = make_test_mfcc_df('../../data/covid19/test', test_df)

train_mfcc.head() - 5rows x 36columns / label0:3499, label1:306
test_mfcc.head() - 5rows x 35columns

 

오디오 특성 시각화

  • harmonic(고조파): 특정한 주파수가 일정하게 유지될 때 나타나는 특성
  • percussive(퍼커시브): 파형이 반복적으로 등장할 때 나타나는 특성
# 시각화 예시
y, sr = librosa.load('../../data/covid19/train/00001.wav', sr=sample_rate, duration=10)
fig, ax = plt.subplots(nrows=2, sharex=True)
librosa.display.waveshow(y, sr=sr, ax=ax[0])
ax[0].set(title='Envelope view, mono')
ax[0].label_outer()

y_harm, y_perc = librosa.effects.hpss(y)
librosa.display.waveshow(y_harm, sr=sr, alpha=0.5, ax=ax[1], label='Harmonic')
librosa.display.waveshow(y_perc, sr=sr, color='r', alpha=0.5, ax=ax[1], label='Percussive')
ax[1].set(title='Multiple waveforms')
ax[1].legend()

기침소리 오디오 특성 시각화

# mfcc 특성 시각화
y_mfcc = librosa.feature.mfcc(y, sr=sr, n_mfcc=n_mfcc)
librosa.display.specshow(y_mfcc, sr=sr)

기침소리 오디오 mfcc 특성 시각화

 

 

Over Sampling

라벨의 개수를 보면 정상 3499개, 비정상 306개로 데이터 불균형이 심각하다.

SMOTE 라는 오버샘플링 기법을 사용해 데이터 불균형 문제를 일부 해소해준다.

X = train_mfcc.drop(['id', 'label'], axis=1)
y = train_mfcc['label']

sampler = SMOTE(random_state=100)
X_sample, y_sample = sampler.fit_resample(X, y)
print('-----Over Sampling-----')
print(y.value_counts())
print(y_sample.value_counts())

X_train, X_val, y_train, y_val = train_test_split(X_sample, y_sample, test_size=0.2, random_state=100, stratify=y_sample)
print('-----Train Valid Split-----')
print(X_train.shape, y_train.shape, X_val.shape, y_val.shape)

input data 확인

 

모델 학습 - 간단한모델

# random forest
model = RandomForestClassifier(random_state=100)
model.fit(X_train, y_train)
joblib.dump(model, 'SMOTE_RF.pkl')

val_pred = model.predict(X_val)
rf_accuracy = accuracy_score(y_val, val_pred)
rf_accuracy
# MLP
model = MLPClassifier(random_state=100)
model.fit(X_train, y_train)
joblib.dump(model, 'SMOTE_MLP.pkl')

val_pred = model.predict(X_val)
mlp_accuracy = accuracy_score(y_val, val_pred)
mlp_accuracy

간단한 모델에 학습한 경우 Random Forest 모델에서는 0.95, MLP 모델에서는 0.84 정도의 정확도를 얻었다.

간단한 딥러닝 모델에도 학습해보자.

 

class CustomDataset(Dataset):
    def __init__(self, X, y):
        self.X = X
        self.y = y
        
        self.X = torch.tensor(self.X.values).float()
        self.y = torch.tensor(self.y).float()
        
    def __len__(self):
        return len(self.X)
        
    def __getitem__(self, index): # index에 해당하는 data return
        X = self.X[index]
        y = self.y[index]
        return X, y
dataset = CustomDataset(X_sample, y_sample)
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
X, y = next(iter(data_loader))
print(X.shape, y.shape)
# torch.Size([128, 34]) torch.Size([128])
class MyModel(nn.Module):
    def __init__(self, input_size):
        super(MyModel, self).__init__()
        self.input_size = input_size
        
        self.layer1 = torch.nn.Sequential(nn.Linear(self.input_size, 16),
                                          nn.LeakyReLU())
        self.layer2 = torch.nn.Sequential(nn.Linear(16, 8),
                                          nn.LeakyReLU())
        self.layer3 = nn.Linear(8, 1)
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.sigmoid(x)
        return x
model = MyModel(X.size(-1))
model.to(device)
model

간단한 linear 모델 구조

loss_fn = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

losses = []
best_loss = 1
dnn_accuracy = 0.0000000001

for epoch in range(n_epochs):
    model.train()
    running_loss = 0
    
    for x, y in data_loader:
        x = x.to(device)
        y = y.to(device)
        y = y.unsqueeze(1)
        
        y_pred = model(x)
        loss = loss_fn(y_pred, y)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
    
    loss = running_loss/len(data_loader)
    losses.append(loss)
    
    output = (y_pred >= 0.5).float()
    accuracy = (output == y).float().mean()
        
    if accuracy > dnn_accuracy:
        dnn_accuracy = accuracy
        best_model = deepcopy(model)
        best_loss = loss
    
    if epoch % 10 == 0:
        print(f'Epoch {epoch}/{n_epochs} \tLoss {loss:.6f} \tAccuracy {accuracy}')
        
torch.save(best_model, 'SMOTE_DNN.pth')
torch.save(best_model.state_dict(), 'SMOTE_DNN_state_dict.pth')

 

모델 학습 결과

df_result = pd.DataFrame({'model':['Random Forest', 'MLP', 'DNN'],
                          'Over Sampling':['SMOTE', 'SMOTE', 'SMOTE'],
                          'Best Accuracy': [rf_accuracy, mlp_accuracy, dnn_accuracy.to('cpu').numpy()]
                         })
df_result

모델 학습 결과

 

Inference 코드

실습에서는 모델을 학습하는것까지만 했는데 만들어진 모델과 테스트데이터를 가지고 실제환경에서 추론할 수 있도록 Inference 코드도 구현했다. Inference에서는 실제 입력데이터가 모델 input과 동일한 형태를 가질 수 있도록 전처리역시 동일하게 해준다.

위에서 train_mfcc를 만들면서 test_mfcc를 함께 전처리해주었기 때문에 저장한 test_mfcc.csv 파일을 가져오기만 하면 된다.

 

test_mfcc = pd.read_csv('../../data/covid19/result/test_mfcc.csv')
test_mfcc = test_mfcc.drop('id', 1)
# Random Forest 모델로 예측
model = joblib.load('./SMOTE_RF.pkl')
preds = model.predict(test_mfcc)

# MLP 모델로 예측
model = joblib.load('./SMOTE_MLP.pkl')
preds = model.predict(test_mfcc)
# 간단한 Linear 딥러닝 모델로 예측
model = torch.load('./SMOTE_DNN.pth')
model.load_state_dict(torch.load('./SMOTE_DNN_state_dict.pth'))
model.eval()

# test 데이터 tensor 변환
x = torch.tensor(test_mfcc.values).float()
x = x.to(device)

# 모델 예측
preds = model(x)
preds = preds.detach().cpu().numpy()
preds

 

데이콘에 제출해본 결과 가장 좋은 점수를 받은건 SMOTE 기법을 사용해서 학습시킨 Random Forest 모델이었다.

 

 

 

profile

매직코드

@개발법사

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!