Kaggle-KR

PUBG-SR-GunSound Classification Tutorial 본문

Kaggle 한글 커널 with Python/개인 커널

PUBG-SR-GunSound Classification Tutorial

알 수 없는 사용자 2018. 6. 3. 19:37
Kaggle_TeamBlog

*시작하기에 앞서 본 튜토리얼은 PUBG 사의 배틀그라운드라는 게임의 스나이퍼 라이플 총기 소리 분류에 관한 내용을 다루고 있습니다. 코드와 설명에 대한 피드백 및 질문은 언제나 환영입니다. 출처만 남겨주신다면 어디에든 사용하셔도 좋습니다.

하지만 전처리 부분을 제외한다면 Kaggle Competition tensorflow-speech-recognition-challenge Dataset에도 충분히 적용될 수 있는 모델입니다. https://www.kaggle.com/c/tensorflow-speech-recognition-challenge

** 소리데이터 변환 및 전처리하는 과정에서 Kaggle Competition https://www.kaggle.com/davids1992/speech-representation-and-data-exploration/notebook 코드를 참고했으며, 코드 사용을 허락해준 DavidS에게 감사 인사 전합니다.

** In the process of converting and preprocessing sound data, I refer to the Kaggle Competition https://www.kaggle.com/davids1992/speech-representation-and-data-exploration/notebook code and thank DavidS for allowing the code I will.

***해당 튜토리얼의 코드는 https://github.com/kcs93023/KagglePractice 여기 깃허브 Kaggle_Team blog 폴더에서 확인하실 수 있습니다.

1. 데이터셋 확인

소리

소리 또는 음(音)은 사람의 청각기관을 자극하여 뇌에서 해석되는 매질의 움직임이다. 공기나 물 같은 매질의 진동을 통해 전달되는 종파이다. 우리들의 귀에 끊임없이 들려오는 소리는 공기 속을 전해오는 파동이다.

소리는 우리들에게 여러 가지 정보를 전해준다. 눈에는 보이지 않는 파동이지만 파동의 여러 가지 성질은 음파의 경우 귀에 들리는 소리의 변화로 알 수가 있다. 사람이 소리를 들을 수 있는 것도 공기가 진동하기 때문이다. 즉 주파수(진동수)를 가지기 때문이다.

사람의 가청주파수는 약 20~20000Hz(20 KHz) 이내이며 나이가 듦에 따라 최대 가청주파수는 낮아지게 된다. 공학에서의 가청주파수 대역폭은 300~3400 Hz이다

출처 - 한국 위키피디아 https://ko.wikipedia.org/wiki/소리


이전의 타이타닉 튜토리얼에서 명시적이고, 행렬로 이루어진 데이터들을 살펴보고 데이터 속에 숨어있는 정보들을 찾아보는 시도를 해보셨을텐데요. 이번 튜토리얼에서는 형태가 정해지지 않은 연속적인 데이터인 소리로 된 데이터들을 분석해보려고 합니다.

이번 튜토리얼에서 우리는 소리 데이터가 가지고 있는 특성을 이해하고, 그러한 특성을 이용해 전처리를 한 후 분류를 하려고 합니다.

우리가 이번에 사용할 배틀그라운드 스나이퍼 라이플 총기소리는 (이하 pubg-sr) 직접 배틀그라운드를 플레이하며 수집한 리플레이에서 추출한 데이터입니다. class의 개수는 총 6개 (AWM, Kar98, M24, SKS, MK14, Mini)로 이루어져 있습니다. 데이터의 수는 2909개 이며, 16bits PCM, mono, .wav, 1.5sec 길이의 특성을 가지고 있습니다.

이번 튜토리얼에서 사용할 라이브러리는 다음과 같습니다.

In [1]:
#path 관련 라이브러리
import os
from os.path import isdir, join
from pathlib import Path

# Scientific Math 라이브러리  
import numpy as np
from scipy.fftpack import fft
from scipy import signal
from scipy.io import wavfile

# Visualization 라이브러리
import matplotlib.pyplot as plt
import tensorflow as tf
import IPython.display as ipd
import plotly.offline as py
py.init_notebook_mode(connected=True)
import plotly.graph_objs as go

%matplotlib inline

먼저 데이터셋이 있는 Path를 지정 해줍니다.

In [2]:
audio_path = '/home/kcs/Git/KagglePractice/Speech_Representation_And_Data_Exploration/dataSets/prepDataSet/'

이제 소리데이터를 표현해볼건데요. 먼저 배틀그라운드의 대표적인 총기인 AWM의 소리를 들어보겠습니다.

In [3]:
filename = 'AWM/AWM_01.wav'
sample_rate, samples = wavfile.read(str(audio_path) + filename)
ipd.Audio(samples, rate= sample_rate)
Out[3]:

우리가 이러한 소리를 어떻게 전처리하고 다룰 수 있을까요?

여러가지 방법이 있지만 지금은 푸리에 변환이라는 과정을 통해 소리데이터에서 시간대별 주파수를 분리해 스펙트로그램이라는 이미지 형태로 변경하려고합니다.

정확하게 말하면 이미지 처럼 보이지만 실제로는 matplotlib를 이용해 그래프를 그린 것입니다.

푸리에 변환에 관한 자세한 내용은 다크 프로그래머 님의 블로그에서 쉽게 설명하고 있으니 수식에 대해 이해가 가지 않으시더라도 간단히 개념을 확인해 보시기 바랍니다. http://darkpgmr.tistory.com/171

다음 함수가 소리 파일을 입력받아 주파수 대역의 범위, 시간의 범위, 그리고 각 시간대별 주파수대역의 값을 반환합니다.

정확하게는 scipy 라이브러리 내에 signal 함수를 사용합니다.

In [4]:
def log_specgram(audio, sample_rate, window_size=20, step_size = 10,
                eps = 1e-10):
    nperseg = int(round(window_size * sample_rate / 1e3))
    noverlap = int(round(step_size * sample_rate / 1e3))
    freqs, times, spec = signal.spectrogram(audio,
                                           fs = sample_rate,
                                           window='hann',
                                           nperseg = nperseg,
                                           noverlap=noverlap,
                                           detrend=False)
    return freqs, times, np.log(spec.T.astype(np.float32) + eps)

이제 각 총기별 스펙트로그램을 살펴보도록 하겠습니다.

In [5]:

dirs = [f for f in os.listdir(audio_path) if isdir(join(audio_path, f))] dirs.sort() for direct in dirs: waves = [f for f in os.listdir(join(audio_path, direct)) if f.endswith('.wav')] sample_rate, samples = wavfile.read(join(audio_path, direct,direct+ '_01.wav')) freqs, times, spec = log_specgram(samples,sample_rate) plt.imshow(spec.T, aspect='auto', origin='lower', extent=[times.min(), times.max(), freqs.min(), freqs.max()]) print(direct) plt.show()

AWM
Kar98
M24
MK14
SKS
mini

가로축이 시간축 세로축이 주파수 대역이며, 각 점에서 밝은 정도가 해당 주파수 성분의 세기라고 볼 수 있습니다.

각 총기별 스펙트로그램은 사람눈으로 보았을 때 비슷해 보이지만, 실제로 사람이 들었을 때 다르게 들립니다. 이는 총기 소리별로 각 주파수 성분의 세기가 다르기 때문입니다.

그리고 몇몇 총기의 스펙트로그램에서 검정색으로 표현된 부분을 볼 수 있는데 이 부분은 해당 주파수 성분 값이 음수가 되어서 생기는 형태입니다.

물론 노이즈가 존재하므로 완전한 침묵으로 볼 수는 없으나 푸리에 변환을 하는 과정에서 생긴 노이즈로 판단할 수 있습니다.

또한 공통점으로는 15000Hz 이상의 주파수 대역에서는 소리 성분이 없는것으로 보여 집니다.

보기 쉽게 3차원으로 살펴보도록 하겠습니다.

In [6]:

filename = 'AWM/AWM_01.wav' sample_rate, samples = wavfile.read(str(audio_path) + filename) freqs, times, spec = log_specgram(samples,sample_rate) #spectrogram의 3d 그림을 보겠습니다. data = [go.Surface(z=spec.T)] layout = go.Layout( title = 'Spectrogram of "AWM" in 3d', scene = dict( yaxis = dict(title='Frequencies', range=freqs), xaxis = dict(title='Time', range=times), zaxis = dict(title='Log amplitude'), ), ) fig = go.Figure(data=data,layout=layout) py.iplot(fig)

* plotly가 로딩이 되지 않는 경우가 있어 일단 스크린샷으로 올려뒀습니다. 로딩이될 경우 실제로는 3차원으로 이리저리 돌려보실 수 있습니다.



3차원으로 보아도 알 수 있듯이 스펙트로그램에서 확인 되었던 주파수 15000Hz 대역 이상에는 소리 성분이 없다는 것을 확인할 수 있었습니다.

2. 전처리

15000Hz 주파수 이상에는 소리 성분이 없다는 것을 확인했기 때문에 이제 우리는 15000Hz 대역 이상에 존재하는 데이터를 제거하고자 합니다. 시간 역시 1.5sec인 것을 1sec로 조정합니다.

이렇게 전처리를 하는 이유는 우리가 분류를 할 때 연산의 양도 줄일 수 있고, Uninformative한 데이터를 제거해 정확하게 필요한 부분으로 분류를 성공적으로 이끌기 위함 입니다.

나이퀴스트 - 섀넌 표본화 이론(https://ko.wikipedia.org/wiki/표본화_정리) 에 의해 Sample_rate 를 30000으로 지정하고 소리 파일을 Resample 합니다. 동시에 소리도 비교해봅시다. 여기에서 Resample 하는 과정에서 소리 신호의 길이가 1.5sec에서 1.0sec로 조절됩니다. Sample_rate가 1초에 수집하는 횟수이기 때문입니다.

In [7]:
filename = 'AWM/AWM_01.wav'
sample_rate, samples = wavfile.read(str(audio_path) + filename)
freqs, times, spec = log_specgram(samples,sample_rate)

Resample 이전

In [8]:
ipd.Audio(samples, rate= sample_rate)
Out[8]:

Sample_rate 30000 (즉, 주파수 대역이 0Hz ~ 15000Hz) 으로 Resample 한 후의 소리

In [9]:
new_sr = 30000
resampled = signal.resample(samples, new_sr)
In [10]:
ipd.Audio(resampled, rate= new_sr)
Out[10]:

다음은 축을 주파수 대역과 주파수의 세기로 나타낸 그래프를 그릴때 사용하는 함수입니다.

In [11]:
def custom_fft(y, fs):
    T = 1.0 / fs
    N = y.shape[0]
    yf = fft(y)
    xf = np.linspace(0.0, 1.0/(2.0*T), N//2)
    vals = 2.0/N * np.abs(yf[0:N//2])
    return xf,vals

기존의 소리 데이터셋의 Sample_rate는 48000 즉, 주파수대역 0Hz ~ 24000Hz 범위를 사용했는데, Sample_rate 30000으로 15000Hz 이상의 값이 날아간 것을 확인할 수 있습니다.

In [12]:
origin_xf, origin_vals = custom_fft(samples, sample_rate)
resampled_xf, resampled_vals = custom_fft(resampled, new_sr)
fig = plt.figure(figsize=(14, 10))

ax1 = fig.add_subplot(211)
ax1.set_title('AWM FFT of recording sampled with ' + str(sample_rate) + ' Hz')
ax1.plot(origin_xf, origin_vals)
ax1.set_xlabel('Frequency')
plt.grid()

ax2 = fig.add_subplot(212)
ax2.set_title('AWM FFT of recording sampled with ' + str(new_sr) + ' Hz')
ax2.plot(resampled_xf, resampled_vals)
ax2.set_xlabel('Frequency')
plt.grid()
plt.show()

3. 데이터 준비

이제 각 총기별로 Sample_rate를 30000으로 변경해 Resample된 데이터들을 분류하기 좋게 numpy 배열로 변경합니다.

각 폴더내에 들어있는 소리 파일을 읽어옵니다.

데이터를 준비하는 과정에서 min-max 정규화를 실시했는데, 이는 우리가 분류 과정에서 활성화 함수로 Relu function을 사용하기 위함입니다. 우리가 사용할 스펙트로그램에는 log를 취하다 보니 작은 값에 대해서는 음수로 된 값이 많습니다.

Relu function은 0보다 큰 값은 그대로 통과시키고 0보다 작은 값은 0으로 통과시키는 함수이기 때문에 값을 0-1 사이 값으로 변경해주는 정규화 과정이 필요합니다.

In [13]:
freqs, times, spec = log_specgram(resampled,new_sr)
freqs_size = len(freqs)
times_size = len(times)

print('Sample Rate : ' + str(new_sr))
print('Number of labels: ' + str(len(dirs)))

print(dirs)
all_data = []
spec_all = []
target_all = []
target_value = {}

for i, direct in enumerate(dirs):
    #각 폴더별로 읽습니다.
    waves = [f for f in os.listdir(join(audio_path, direct)) if f.endswith('.wav')]
    target_value[direct] = i
    print(str(i)+':' + str(direct) + ' ')
    for j, wav in enumerate(waves):
        target_all.append(direct)
        #폴더 내의 소리 파일을 읽어 Sample_rate 30000으로 Resample하고 numpy 배열에 저장합니다.
        sample_rate, samples = wavfile.read(join(audio_path, direct, wav))
        #Resample
        resamples = signal.resample(samples, new_sr)
        #스펙트로그램 생성
        freqs, times, spec = log_specgram(samples, sample_rate)
        #변경 전 후 스펙트로그램 확인
        if j == 0:
            fig = plt.figure(figsize=(14, 5))
            ax1 = fig.add_subplot(121)
            print('변경 전 후')
            ax1.imshow(spec.T, aspect='auto', origin='lower', extent=[times.min(), times.max(), freqs.min(), freqs.max()])
        freqs, times, spec = log_specgram(resamples, new_sr)
        if j == 0:
            ax2 = fig.add_subplot(122)
            ax2.imshow(spec.T, aspect='auto', origin='lower', extent=[times.min(), times.max(), freqs.min(), freqs.max()])
            plt.show()
        #스펙트로그램의 값을 min max 정규화를 통해 0-1 의 범위로 보정해준다.
        spec = (spec - spec.min())/(spec.max() - spec.min())
        all_data.append([np.reshape(spec,(freqs_size,times_size)), direct])
Sample Rate : 30000
Number of labels: 6
['AWM', 'Kar98', 'M24', 'MK14', 'SKS', 'mini']
0:AWM 
변경 전 후
1:Kar98 
변경 전 후
2:M24 
변경 전 후
3:MK14 
변경 전 후
4:SKS 
변경 전 후
5:mini 
변경 전 후

데이터에 대한 전처리도 마쳤고, 분류에 사용할 데이터들도 수집했습니다.

이제 데이터셋을 훈련 데이터와 테스트 데이터로 나누어야 합니다.

이는 네트워크가 훈련데이터에 과적합되는 Overfitting 현상이 일어나게 되었을 때 모든 데이터를 훈련데이터로 사용하면 이를 확인할 방법이 없기 때문입니다. 따라서 우리는 데이터를 나누어 훈련 데이터로 네트워크를 훈련하고, 테스트 데이터로 훈련이 올바르게 특정 데이터셋에 과적합 되지 않고 잘 진행되는지 확인 할 수 있습니다.

일반적으로 8:2로 데이터를 분할 합니다. 분할하기 이전에 우리의 데이터는 총기 순서대로 저장되어 있기 때문에 Shuffle을 해주고 분할 합니다.

In [14]:
# 데이터를 섞습니다.
np.random.shuffle(all_data)
# 데이터를 스펙트로그램 데이터와 정답을 가지고 있는 라벨로 나눕니다.
spec_all = np.reshape(np.delete(all_data,1,1),(len(all_data)))
target_all = [i for i in np.delete(all_data,0,1).tolist()]
In [15]:
# 훈련 데이터에 대한 80% 비율의 인덱스를 가져옵니다.
train_indices = np.random.choice(len(target_all),
                                 round(len(target_all) * 0.8), replace=False)
# 훈련 데이터에 대한 80% 비율의 인덱스를 제외한 20%의 테스트 데이터의 인덱스를 가져옵니다.
test_indices = np.array(list(set(range(len(target_all)))
                                 - set(train_indices)))
#데이터들을 다루기 쉽게 정리합니다.
spec_vals = np.array([x for x in spec_all])
target_vals = np.array([x for x in target_all])

#데이터들을 훈련, 테스트에 맞게 변수에 할당합니다.
train_spec = spec_vals[train_indices][:]
train_target = target_vals[train_indices][:]
test_spec = spec_vals[test_indices][:]
test_target = target_vals[test_indices][:]

4. 네트워크 설계

우리는 분류를 위한 네트워크 모델로 Supervised Learning 방식을 사용할 것이고 그 중 Convolutional Neural Network(이하 CNN, 합성곱 신경망)를 사용하려고 합니다.

CNN은 이미지 분류에서 뛰어난 성능을 보이고 있는데, 이는 이미지의 특성인 근처 픽셀들의 값이 비슷하다는 점을 이용해 필터로 특정 범위에서 특징을 뽑아내어 해당 특징을 네트워크에 훈련시키는 방식을 사용합니다.

위에서도 언급 했지만 소리를 변환한 스펙트로그램은 매우 이미지와 유사한 특성을 보이고 있기 때문에 충분히 CNN이 좋은 성능을 보일 수 있습니다.

구성한 네트워크 구조는 다음과 같습니다.


이미지의 특징을 뽑는 Convolution 계층이 3개 연속적으로 연결된 구조입니다. 3개의 Convolution 계층이 충분한 특징을 뽑으면 특정 범위에서 가장 큰 값을 취하는 Pooling 계층, 이후에 앞선 4개의 계층이 추출한 특징으로 분류를 실시한 Fully Conected 계층으로 이루어진 구조로 되어있습니다. 중간중간 차원은 제가 임의로 지정한 차원이지만, 맨 마지막 FCN의 출력 차원은 분류하고자 하는 클래스의 개수로 지정되어 있습니다. 이는 Softmax 함수를 사용하기 위함인데, 네트워크의 출력이 가지는 값이 각각의 클래스에 할당될 확률로 표현될 수 있도록 합니다.

각 계층 사이의 활성화 함수로는 Relu funcion을 사용합니다.

네트워크를 통과한 데이터는 총합이 1인 각 클래스로 분류될 확률로 표현이 되고 그 중 가장 큰 값을 취하는 Softmax function 방식을 사용합니다.

코드를 보면 알 수 있듯이 3개의 Conv 계층의 fileter 크기는 11x11 -> 5x5 -> 3x3 형식으로 처음에는 큰 특징을 뽑고 다음 계층에서는 더 작은 특징 마지막 계층에서는 가장 작은 특징을 뽑는 구조로 되어있습니다.

In [16]:
def cnn_model(input_images, batch_size,drop_out_rate=0.1, is_training=False, train_logical=True):
    def truncated_normal_var(name, shape, dtype):
        return(tf.get_variable(name=name, shape=shape, dtype=dtype, initializer = tf.truncated_normal_initializer(stddev=0.05)))

    def zero_var(name, shape, dtype):
        return(tf.get_variable(name=name, shape=shape, dtype=dtype, initializer=tf.constant_initializer(0.0)))
    
    with tf.variable_scope('conv1') as scope:
        conv1_kernel = truncated_normal_var(name='conv_kernel1',shape=[11,11,1,16],
                                           dtype= tf.float32)
        conv1 = tf.nn.conv2d(input_images, conv1_kernel, [1,3,3,1], padding='SAME')
        conv1_bias = truncated_normal_var(name='conv_bias1', shape=[16], dtype=tf.float32)
        conv1_add_bias = tf.nn.bias_add(conv1,conv1_bias)
        relu_conv1 = tf.nn.relu(conv1_add_bias)
    
    norm1= tf.nn.lrn(relu_conv1,depth_radius=5, bias=2.5,alpha=1e-3, beta=0.75, name='norm1')
    
    with tf.variable_scope('conv2') as scope:
        conv2_kernel = truncated_normal_var(name='conv_kernel2', shape=[5, 5, 16, 32], dtype=tf.float32)
        conv2 = tf.nn.conv2d(norm1, conv2_kernel, [1, 1, 1, 1], padding='SAME')
        conv2_bias = truncated_normal_var(name='conv_bias2', shape=[32], dtype=tf.float32)
        conv2_add_bias = tf.nn.bias_add(conv2, conv2_bias)
        relu_conv2 = tf.nn.relu(conv2_add_bias)

    norm2 = tf.nn.lrn(relu_conv2, depth_radius=5, bias=2.0, alpha=1e-3, beta=0.75, name='norm2')
    
    with tf.variable_scope('conv3') as scope:
        conv3_kernel = truncated_normal_var(name='conv_kernel3', shape=[3, 3, 32, 64], dtype=tf.float32)
        conv3 = tf.nn.conv2d(norm2, conv3_kernel, [1, 1, 1, 1], padding='SAME')
        conv3_bias = truncated_normal_var(name='conv_bias3', shape=[64], dtype=tf.float32)
        conv3_add_bias = tf.nn.bias_add(conv3, conv3_bias)
        relu_conv3 = tf.nn.relu(conv3_add_bias)

    pool = tf.nn.max_pool(relu_conv3, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool_layer')
    norm3 = tf.nn.lrn(pool, depth_radius=5, bias=2.0, alpha=1e-3, beta=0.75, name='norm3')
    reshaped_output = tf.reshape(norm3, [batch_size, -1])
    reshaped_dim = reshaped_output.get_shape()[1].value
    
    with tf.variable_scope('full1') as scope:
        full_weight1 = truncated_normal_var(name='full_mult1', shape=[reshaped_dim, 384], dtype=tf.float32)
        full_bias1 = truncated_normal_var(name='full_bias1', shape=[384], dtype=tf.float32)
        full_layer1 = tf.nn.relu(tf.add(tf.matmul(reshaped_output, full_weight1), full_bias1))

    with tf.variable_scope('full2') as scope:
        full_weight2 = truncated_normal_var(name='full_mull2', shape=[384, 192], dtype=tf.float32)
        full_bias2 = truncated_normal_var(name='full_bias2', shape=[192], dtype=tf.float32)
        full_layer2 = tf.nn.relu(tf.add(tf.matmul(full_layer1, full_weight2), full_bias2))

    with tf.variable_scope('full3') as scope:
        full_weight3 = truncated_normal_var(name='full_mull3', shape=[192, len(target_value)], dtype=tf.float32)
        full_bias3 = truncated_normal_var(name='full_bias3', shape=[len(target_value)], dtype=tf.float32)
        final_output = tf.nn.relu(tf.add(tf.matmul(full_layer2, full_weight3), full_bias3))
    final_output = tf.layers.dropout(final_output, rate=drop_out_rate, training=is_training)
    
    return final_output

우리가 네트워크에 사용할 Hyper Parameter는 다음과 같습니다.

여기에서 우리가 주의할 점이 있는데, 훈련되는 정도를 나타내는 lr(learning_rate)의 값을 너무 크게 주면 네트워크를 훈련할 때 loss가 크게 흔들리거나 Overfitting 현상이 일어날 수 있으니 적당한 값을 주어야합니다.

하지만 우리에게는 Overfitting을 방지하는 장치인 dropout을 사용할 수 있습니다. dropout이란 간단하게 말해서 훈련된 네트워크의 일부를 제거하는 기법인데, 이를 통해 우리는 과적합을 방지할 여지를 남길 수 있습니다.(우리의 모델은 그렇게 쉽게 과적합되지 않으므로 가볍게 0.05 정도 주겠습니다.)

In [17]:
# Parameters
lr = 0.001
generations = 5000
num_gens_to_wait = 250
drop_out_rate = 0.05
batch_size = 32

우리 데이터 셋의 정답 즉, label은 사람이 읽을 수 있는 nomial한 값으로 이루어져 있습니다. 따라서 네트워크가 이해할 수 있는 one-hot encoding 을 통해 각 클래스를 숫자로 변경해줍니다.

In [18]:
# 레이블을 네트워크가 이해할 수 있는 숫자형태로 변경
temp = []
for v in train_target:
    temp.append(target_value[v[0]])
train_target = np.array(temp)

temp = []
for v in test_target:
    temp.append(target_value[v[0]])
test_target = np.array(temp)
In [19]:
#네트워크에 사용할 placeholder 정의
x_input_shape = (batch_size, freqs_size,times_size,1)
y_input_shape = (batch_size, )
x_input = tf.placeholder(tf.float32, shape=x_input_shape)
y_target = tf.placeholder(tf.int32, shape=y_input_shape)
eval_input_shape = (batch_size, freqs_size, times_size,1)
eval_input = tf.placeholder(tf.float32, shape=eval_input_shape)
eval_target = tf.placeholder(tf.int32, shape=y_input_shape)

모델의 출력을 정의해줍니다. 여기에서 with 구문을 잘 짚고 넘어가야 하는데, tensorflow의 그래프 구조에서는 변수 스코프를 지정 해주지 않으면 같은 네트워크를 따로 변수로 지정해 사용할 수 없게 됩니다.

따라서 with tf.variable_scope('scope',reuse=tf.AUTO_REUSE) as scope: 라는 구문을 통해 같은 네트워크에 대해 네트워크를 훈련할(dropout을 적용할) model_output 변수와 단순히 네트워크를 테스트해볼 test_model_output 변수로 나눌 수 있습니다.

In [20]:
config = tf.ConfigProto()
sess = tf.Session(config=config)
with tf.variable_scope('scope',reuse=tf.AUTO_REUSE) as scope:
    model_output = cnn_model(x_input,batch_size,drop_out_rate=drop_out_rate, is_training=True)
    test_model_output = cnn_model(eval_input,batch_size)

네트워크를 훈련하는 과정에서 가장 중요한 loss function(cost function)을 정의할 차례입니다. logistic한 분류를 하는 네트워크이므로 위에서 언급한 softmax cross entropy 함수를 사용합니다.

loss function을 최소화하는 것을 도와줄 Optimizer는 Momentum 계열인 AdamOptimizer를 사용했습니다. 여기서 Momentum 계열이란 loss를 줄이는 방향으로 경사를 따라 이동하는 것은 Gradient 방법과 동일합니다. 하지만 관성이라는 개념을 응용해 지속적으로 힘을 받는 방향(경사)으로는 속도가 증가하는 반면, 힘의 방향이 지속적으로 바뀌는 (흔들린다면) 방향으로는 속도가 서로 상쇄되는 Optimizer들을 말합니다.

In [21]:
targets = tf.squeeze(tf.cast(y_target,tf.int32))

loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=model_output, labels=y_target))
prediction = tf.nn.softmax(model_output)
test_prediction = tf.nn.softmax(test_model_output)

my_optimizer = tf.train.AdamOptimizer(learning_rate=lr, epsilon=10e-2)

train_step = my_optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess.run(init)
In [22]:
# Define def to calculate accuracy
def get_accuracy(logits, targets):
    batch_predictions = np.argmax(logits, axis=1)
    num_correct = np.sum(np.equal(batch_predictions, targets))
    return (100. * num_correct)/ batch_predictions.shape[0]

네트워크를 훈련하기 전 데이터들이 잘 분할되었는지 마지막으로 확인하겠습니다.

In [23]:
print('Train_Spectrogram Demension : ' + str(np.shape(train_spec)))
Train_Spectrogram Demension : (2310, 301, 99)
In [24]:
print('Train_Label Demension : ' + str(np.shape(train_target)))
Train_Label Demension : (2310,)
In [25]:
print('Test_Spectrogram Demension : ' + str(np.shape(test_spec)))
Test_Spectrogram Demension : (577, 301, 99)
In [26]:
print('Test_Label Demension : ' + str(np.shape(test_target)))
Test_Label Demension : (577,)
In [27]:
print('Number Of Labels : ' + str(len(target_value)))
Number Of Labels : 6
In [28]:
train_loss = []
train_acc = []
test_acc = []
for i in range(generations):
    rand_index = np.random.choice(len(train_spec),size = batch_size)
    rand_x = train_spec[rand_index]
    rand_x = np.expand_dims(rand_x, -1)
    rand_y = train_target[rand_index]

    sess.run(train_step,feed_dict = {x_input: rand_x, y_target: rand_y})
    temp_train_loss, temp_train_preds = sess.run([loss, prediction], feed_dict={x_input: rand_x, y_target: rand_y})
    temp_train_acc = get_accuracy(temp_train_preds, rand_y)
    
    # logging temp result
    if (i + 1) % 50 == 0:
        eval_index = np.random.choice(len(test_spec), size=batch_size)
        eval_x = test_spec[eval_index]
        eval_x = np.expand_dims(eval_x, -1)
        eval_y = test_target[eval_index]

        test_preds = sess.run(test_prediction, feed_dict={eval_input: eval_x})
        temp_test_acc = get_accuracy(test_preds, eval_y)

        # Logging and Printing Results
        train_loss.append(temp_train_loss)
        train_acc.append(temp_train_acc)
        test_acc.append(temp_test_acc)
        acc_and_loss = [(i + 1), temp_train_loss, temp_train_acc,
                            temp_test_acc]
        acc_and_loss = [np.round(x, 10) for x in acc_and_loss]
        print('Generation # {}. Train Loss: {:.10f}. Train Acc (Test Acc): {:.2f} ({:.2f})'.format(*acc_and_loss))
Generation # 50. Train Loss: 1.7847064734. Train Acc (Test Acc): 21.88 (15.62)
Generation # 100. Train Loss: 1.7795782089. Train Acc (Test Acc): 21.88 (15.62)
Generation # 150. Train Loss: 1.8071990013. Train Acc (Test Acc): 9.38 (18.75)
Generation # 200. Train Loss: 1.7728501558. Train Acc (Test Acc): 25.00 (31.25)
Generation # 250. Train Loss: 1.7937307358. Train Acc (Test Acc): 18.75 (18.75)
Generation # 300. Train Loss: 1.7853044271. Train Acc (Test Acc): 18.75 (31.25)
Generation # 350. Train Loss: 1.7806307077. Train Acc (Test Acc): 18.75 (37.50)
Generation # 400. Train Loss: 1.7730836868. Train Acc (Test Acc): 25.00 (31.25)
Generation # 450. Train Loss: 1.7884653807. Train Acc (Test Acc): 15.62 (9.38)
Generation # 500. Train Loss: 1.7908794880. Train Acc (Test Acc): 15.62 (34.38)
Generation # 550. Train Loss: 1.6653219461. Train Acc (Test Acc): 46.88 (37.50)
Generation # 600. Train Loss: 1.7514144182. Train Acc (Test Acc): 21.88 (25.00)
Generation # 650. Train Loss: 1.6933798790. Train Acc (Test Acc): 31.25 (34.38)
Generation # 700. Train Loss: 1.6421906948. Train Acc (Test Acc): 34.38 (21.88)
Generation # 750. Train Loss: 1.6802370548. Train Acc (Test Acc): 31.25 (37.50)
Generation # 800. Train Loss: 1.6308742762. Train Acc (Test Acc): 31.25 (40.62)
Generation # 850. Train Loss: 1.6444752216. Train Acc (Test Acc): 34.38 (34.38)
Generation # 900. Train Loss: 1.7794677019. Train Acc (Test Acc): 25.00 (37.50)
Generation # 950. Train Loss: 1.5822188854. Train Acc (Test Acc): 40.62 (21.88)
Generation # 1000. Train Loss: 1.6723458767. Train Acc (Test Acc): 31.25 (31.25)
Generation # 1050. Train Loss: 1.6679995060. Train Acc (Test Acc): 28.12 (31.25)
Generation # 1100. Train Loss: 1.5305647850. Train Acc (Test Acc): 56.25 (43.75)
Generation # 1150. Train Loss: 1.7224267721. Train Acc (Test Acc): 40.62 (43.75)
Generation # 1200. Train Loss: 1.6892120838. Train Acc (Test Acc): 28.12 (50.00)
Generation # 1250. Train Loss: 1.4903165102. Train Acc (Test Acc): 40.62 (40.62)
Generation # 1300. Train Loss: 1.8239430189. Train Acc (Test Acc): 15.62 (56.25)
Generation # 1350. Train Loss: 1.5088479519. Train Acc (Test Acc): 50.00 (43.75)
Generation # 1400. Train Loss: 1.3990495205. Train Acc (Test Acc): 50.00 (46.88)
Generation # 1450. Train Loss: 1.8007878065. Train Acc (Test Acc): 25.00 (25.00)
Generation # 1500. Train Loss: 1.6380696297. Train Acc (Test Acc): 37.50 (59.38)
Generation # 1550. Train Loss: 1.3657389879. Train Acc (Test Acc): 53.12 (59.38)
Generation # 1600. Train Loss: 1.2822355032. Train Acc (Test Acc): 53.12 (31.25)
Generation # 1650. Train Loss: 1.4324523211. Train Acc (Test Acc): 46.88 (37.50)
Generation # 1700. Train Loss: 1.3394958973. Train Acc (Test Acc): 50.00 (37.50)
Generation # 1750. Train Loss: 1.1567890644. Train Acc (Test Acc): 53.12 (53.12)
Generation # 1800. Train Loss: 1.2411756516. Train Acc (Test Acc): 53.12 (50.00)
Generation # 1850. Train Loss: 1.0837237835. Train Acc (Test Acc): 59.38 (50.00)
Generation # 1900. Train Loss: 1.2481327057. Train Acc (Test Acc): 46.88 (43.75)
Generation # 1950. Train Loss: 1.0901304483. Train Acc (Test Acc): 59.38 (68.75)
Generation # 2000. Train Loss: 1.1850624084. Train Acc (Test Acc): 59.38 (62.50)
Generation # 2050. Train Loss: 1.2340710163. Train Acc (Test Acc): 46.88 (56.25)
Generation # 2100. Train Loss: 1.0024583340. Train Acc (Test Acc): 59.38 (56.25)
Generation # 2150. Train Loss: 1.0016852617. Train Acc (Test Acc): 65.62 (68.75)
Generation # 2200. Train Loss: 0.8577659130. Train Acc (Test Acc): 78.12 (71.88)
Generation # 2250. Train Loss: 0.7704766989. Train Acc (Test Acc): 65.62 (56.25)
Generation # 2300. Train Loss: 0.7688508034. Train Acc (Test Acc): 71.88 (75.00)
Generation # 2350. Train Loss: 0.7586882710. Train Acc (Test Acc): 78.12 (75.00)
Generation # 2400. Train Loss: 0.7952678204. Train Acc (Test Acc): 71.88 (65.62)
Generation # 2450. Train Loss: 0.8903024793. Train Acc (Test Acc): 68.75 (78.12)
Generation # 2500. Train Loss: 0.6756498218. Train Acc (Test Acc): 81.25 (78.12)
Generation # 2550. Train Loss: 0.5564248562. Train Acc (Test Acc): 84.38 (84.38)
Generation # 2600. Train Loss: 1.0039947033. Train Acc (Test Acc): 65.62 (71.88)
Generation # 2650. Train Loss: 0.6251598597. Train Acc (Test Acc): 84.38 (65.62)
Generation # 2700. Train Loss: 0.4506621957. Train Acc (Test Acc): 87.50 (81.25)
Generation # 2750. Train Loss: 0.5059022307. Train Acc (Test Acc): 81.25 (84.38)
Generation # 2800. Train Loss: 0.3566779494. Train Acc (Test Acc): 93.75 (71.88)
Generation # 2850. Train Loss: 0.6588535309. Train Acc (Test Acc): 68.75 (87.50)
Generation # 2900. Train Loss: 0.5523803234. Train Acc (Test Acc): 78.12 (75.00)
Generation # 2950. Train Loss: 0.3843995333. Train Acc (Test Acc): 87.50 (71.88)
Generation # 3000. Train Loss: 0.6198287010. Train Acc (Test Acc): 78.12 (68.75)
Generation # 3050. Train Loss: 0.5491774082. Train Acc (Test Acc): 75.00 (78.12)
Generation # 3100. Train Loss: 0.7420556545. Train Acc (Test Acc): 71.88 (84.38)
Generation # 3150. Train Loss: 0.4417073727. Train Acc (Test Acc): 84.38 (81.25)
Generation # 3200. Train Loss: 0.3610717654. Train Acc (Test Acc): 87.50 (71.88)
Generation # 3250. Train Loss: 0.3082919121. Train Acc (Test Acc): 90.62 (93.75)
Generation # 3300. Train Loss: 0.5771259069. Train Acc (Test Acc): 81.25 (75.00)
Generation # 3350. Train Loss: 0.3007945716. Train Acc (Test Acc): 93.75 (75.00)
Generation # 3400. Train Loss: 0.6186054945. Train Acc (Test Acc): 84.38 (78.12)
Generation # 3450. Train Loss: 0.2964857817. Train Acc (Test Acc): 90.62 (78.12)
Generation # 3500. Train Loss: 0.4189838767. Train Acc (Test Acc): 84.38 (87.50)
Generation # 3550. Train Loss: 0.4048108160. Train Acc (Test Acc): 90.62 (90.62)
Generation # 3600. Train Loss: 0.6209578514. Train Acc (Test Acc): 75.00 (87.50)
Generation # 3650. Train Loss: 0.5429416299. Train Acc (Test Acc): 84.38 (81.25)
Generation # 3700. Train Loss: 0.1585085094. Train Acc (Test Acc): 96.88 (90.62)
Generation # 3750. Train Loss: 0.3675480187. Train Acc (Test Acc): 87.50 (90.62)
Generation # 3800. Train Loss: 0.5118711591. Train Acc (Test Acc): 84.38 (84.38)
Generation # 3850. Train Loss: 0.2876999080. Train Acc (Test Acc): 87.50 (84.38)
Generation # 3900. Train Loss: 0.3224277794. Train Acc (Test Acc): 90.62 (81.25)
Generation # 3950. Train Loss: 0.3754746020. Train Acc (Test Acc): 87.50 (81.25)
Generation # 4000. Train Loss: 0.2683393955. Train Acc (Test Acc): 90.62 (90.62)
Generation # 4050. Train Loss: 0.3520234823. Train Acc (Test Acc): 84.38 (81.25)
Generation # 4100. Train Loss: 0.2498609126. Train Acc (Test Acc): 90.62 (78.12)
Generation # 4150. Train Loss: 0.2847018838. Train Acc (Test Acc): 90.62 (90.62)
Generation # 4200. Train Loss: 0.1475564241. Train Acc (Test Acc): 96.88 (68.75)
Generation # 4250. Train Loss: 0.4887551069. Train Acc (Test Acc): 81.25 (93.75)
Generation # 4300. Train Loss: 0.3853343725. Train Acc (Test Acc): 78.12 (87.50)
Generation # 4350. Train Loss: 0.2770698071. Train Acc (Test Acc): 87.50 (78.12)
Generation # 4400. Train Loss: 0.2427094728. Train Acc (Test Acc): 90.62 (93.75)
Generation # 4450. Train Loss: 0.1535968632. Train Acc (Test Acc): 93.75 (78.12)
Generation # 4500. Train Loss: 0.1555101871. Train Acc (Test Acc): 90.62 (90.62)
Generation # 4550. Train Loss: 0.6623867154. Train Acc (Test Acc): 71.88 (78.12)
Generation # 4600. Train Loss: 0.2051379383. Train Acc (Test Acc): 93.75 (84.38)
Generation # 4650. Train Loss: 0.3478705287. Train Acc (Test Acc): 87.50 (90.62)
Generation # 4700. Train Loss: 0.5311002731. Train Acc (Test Acc): 84.38 (84.38)
Generation # 4750. Train Loss: 0.1163112223. Train Acc (Test Acc): 93.75 (87.50)
Generation # 4800. Train Loss: 0.1268692315. Train Acc (Test Acc): 96.88 (90.62)
Generation # 4850. Train Loss: 0.2348402888. Train Acc (Test Acc): 90.62 (87.50)
Generation # 4900. Train Loss: 0.3462639451. Train Acc (Test Acc): 87.50 (75.00)
Generation # 4950. Train Loss: 0.4347902536. Train Acc (Test Acc): 87.50 (90.62)
Generation # 5000. Train Loss: 0.2270024717. Train Acc (Test Acc): 93.75 (96.88)

5000번의 훈련결과 훈련 데이터에 대한 정확도는 93.75% 테스트 데이터에 대한 정확도는 96.88%가 나왔습니다.

상당히 분류를 잘하는 네트워크를 만들었다고 할 수 있는데, 뉴럴네트워크는 하이퍼 파라미터를 어떻게 지정해주느냐 혹은 Optimizer, loss funtion 정의 방식 등등에 따라 성능이 천차만별로 갈릴 수 있습니다. 다행히 제가 만든 네트워크는 성능이 준수하네요

이번 튜토리얼을 시작하기 전에 말씀 드렸던 것처럼 해당 네트워크 모델은 Kaggle Competition tensorflow-speech-recognition-challenge Dataset에도 적용될 수 있는 모델입니다. 물론 전처리하는 과정을 변경해 주어야 정상적으로 작동합니다.

처음 작성해본 튜토리얼이고 직접 만든 네트워크라 부족한점이 많습니다. 피드백은 항상 환영입니다. 긴 글 읽어주신분들께 모두 감사드립니다.


Comments