Ideas for 9th Kaggle TensorFlow Speech Recognition Challenge

kaggle
competition
speech

#1

Tensorflow Speech Recognition Challenge

짧은 명령어를 이해하는 단순하고 효과적인 모델을 두고 경쟁하는 캐글 컴피티션입니다. yes, no, up, down 등과 같은 10개의 명렁어를 구분(classification)해야 합니다. 거기에 추가적인 2개의 Class가 있다는 점이 특이한데,

  • silence : 아무 소리도 안나는 경우의 class
  • unknown : 10개의 클래스가 아닌 소리. out of vocabulary로 생각하면 됨.

의 2가지가 있습니다.

트레이닝 데이터는 65000개 쯤의 wav파일과 label로 이루어져 있습니다. 파일명에 화자 정보가 Hash 값으로 들어있습니다. 즉 파일명을 보면 같은 사람인지 다른 사람의 목소리인지는 알 수 있습니다.

테스트 데이터는 15만개가 넘는 wav파일로 이루어져 있으며, 이를 분류해서 제출하는 방식으로 대회가 구성되어 있습니다.


Approaches

스피치 데이터를 다루어본 적이 없어서 피쳐 선정이나, Augmentation 등에 다양한 시도를 했습니다. 기본적으로 wav파일을 decode해서 1차원의 PCM 데이터를 얻을 수 있습니다. 이 대회는 1초짜리 wav이고 샘플링 frequency가 16000이므로, 1*16000 개만큼의 1차원 벡터로 디코딩 됩니다.

파일명을 통해 화자 ID를 얻을 수 있으므로, training 셋과 validation을 나눌 때는 화자가 한쪽 셋에만 존재하도록 나누었습니다.

baseline : Spectrogram + 2d CNN

기본적으로 입력 피쳐로 간단하게 사용할 수 있는 Spectrogram으로 baseline을 선정하기로 했습니다. 빠르게 baseline 잡고 개선해나가자는 전략이었기 때문에, 제가 알아본 결과 가장 standard한 approach로 보이는 방식으로 구현을 했습니다.

초기에 cnn은 Lenet 수준의 단순한 아키텍쳐로 먼저 시작을 했습니다. baseline은 69% 정도 나왔습니다. 여기에 마지막 FC 직전에 dropout을 추가하니 72% 수준 정도까지 달성했습니다.

Network Architecture + Data Augmentation

baseline을 빠르게 올리기 위해 capacity가 높은 네트워크 아키텍쳐들을 시도하기로 했습니다. 제가 2D CNN의 아키텍쳐를 리뷰해 모아놓은 곳을 참고하셔도 됩니다. - 'Image Classification' Outline

  • VGG16
    • Review
    • VGG16은 3x3 Convolution만 있어서 아키텍쳐가 단순해, 빠르게 구현해서 실험해볼 수 있으면서도, 성능이 좋은 편에 속합니다.
    • 80% 달성
  • Inception V4
    • 84% 달성
  • Resnet, Densenet, …

Inception V4로 84%까지 달성했고, 더 깊고 큰 모델에서는 큰 향상은 없었습니다. 대신 VGG16은 Inception에 비해 트레이닝 속도가 빨라서, 실험하기가 오히려 더 나았구요. 네트워크 capacity가 적지않은 모델들인데다가, 문제도 쉬운편이라고 생각하고 있었기 때문에 84% 라는 성능이 일종의 generalization 성능이 떨어지는 것으로 판단했습니다. 트레이닝과 밸리데이션 성능은 95% 수준이었습니다.

따라서 l2 regularzation과 dropout을 추가해서 generalization에 도움이 될 수 있게 추가하고, data augmentation도 본격적으로 시작했습니다. 주로 numpy와 librosa를 이용해서 작성했고, 텐서플로우에 큐가 항상 꽉 차길 바라는 마음으로^^; tensorpack을 이용해 multiprocess로 처리하도록 구현했습니다.

  • 볼륨 조절 : 70%~130% 수준까지 볼륨 조절. 일정 확률로 작은 소리(30%~50%)로도 조절
  • 위치 조절 : 소리의 peak 볼륨으로부터 10% 수준이면 배경음이라고 생각하고, 배경음인 부분만큼만 shift해서 위치 조절. 즉 같은 소리지만, 소리가 빨리나거나, 늦게나는 등의 조절.
  • Stretch : 소리를 늘리거나 빠르게 만듦.
  • Pitch : pitch를 높이거나 줄여서 음의 높낮이를 변경.
  • 노이즈 추가
    • 화이트노이즈, 브라운노이즈, 핑크노이즈, 블루노이즈, …
    • 트레이닝셋에 있는 배경음을 임의로 삽입

대체로 효과가 좋았으나 pitch가 다른 논문에서 소개한 것보다는 성능이 떨어졌고, 노이즈 추가의 경우 성능 향상에 도움이 많이 되었습니다. 특히 silence class를 맞추는데도 도움이 많이 되었습니다.

추가로 미니배치에 클래스별 데이터 밸런스도 맞추는 등의 처리를 해서, 아래와 같은 성능 달성을 했어요.

  • Vgg16 : 85%
  • Inception V4 : 86%

High resolution spectrogram

원래 사용하던 스펙트로그램 피쳐를 훨씬 높은 해상도로 만들 수 있는 방법이 캐글 kernel에 올라왔습니다. 간단히 원리를 설명하면, 스펙트로그램의 윈도우 사이즈와 스텝 사이즈를 다양하게하여 뽑은 피쳐를 적절히 concat해서 위와 같이 고해상도로 만드는 것입니다. 추출된 피쳐가 훨씬 고해상도이기 때문에, 담고있는 정보가 많을 것이라는 기대도 있었지만, 실제로 validation set에서 유사한 소리들에 대한 confusion matrix가 문제가 되었기 때문에 사용하게 되었습니다. 예를 들면 on과 off 그리고 up 같은 클래스들끼리는 다른 클래스에 비해 상대적으로 오분류하는 비율이 높았고, 이는 더 정밀한 피쳐가 필요하다고 봤습니다.

결과적으로는 약간의 상승을 도모해 87~88% 수준으로 도약하게 되었습니다. 추가적으로 네트워크의 피쳐 사이즈나, FC 크기, 필터 사이즈 등을 조절해가면서 튜닝한 덕도 있었습니다.

실제로 Confusion Matrix를 통해 분석해보면, silence와 unknown에 대한 오분류와 유사 발음에 대한 오분류가 개선되는 과정이 보였습니다.

1d Conv Model & Ensembles

특이하게도, 캐글 커뮤니티에 raw PCM을 input으로하고, 1d convolution만으로 괜찮은 성능을 올렸다는 report가 있었습니다. 일반적인 시도는 아니라고 생각은 들었는데, 단순한 모델로도 괜찮은 성능이 나온다는 이야기에 시도를 좀 했었고, 85% 수준까지 얻었습니다. 이후에 조석제님 과 팀을 이루면서, 작성하셨던 1d conv 모델이 87% 수준의 성능을 내고계셔서, 앙상블을 통해 그 이상의 성능까지도 올릴 수 있게 되었습니다.

조석제님의 캐글 솔루션에 대한 설명 을 보시면 초기에 1d conv 설계에서부터 이후 앙상블을 시도한 것, 그리고 어그먼테이션과 모델 용량을 증가시켜서 성능향상을 한 과정에 대해서 설명되어 있습니다.

최종적으로는 여러 모델의 앙상블을 포함하면서 90.6% 수준의 성능을 얻으면서 대회 9위로 마무리하게 되었습니다.


Trials, Remained Questions & Ideas

유사 소리(on, off, up)에 대한 분류도를 높이기 위한 시도들

on, off, up 같은 유사 소리에서는 상대적으로 분류 성능이 떨어졌습니다. High resolution spectrogram으로 피쳐를 바꾸면서 많이 개선되기는 했지만, 그 외에도 이를 위해 다양한 시도를 했었어요.

Bach Selection for Hard example

어려운 샘플을 더 자주 샘플링해서 학습시키겠다는 겁니다. 이건 ONLINE BATCH SELECTION FOR FASTER TRAINING OF NEURAL NETWORKS 이나 디텍션 쪽의 OHEM: Training Region-based Object Detectors with Online Hard Example Mining 같은 논문들의 아이디어이기도 합니다. 아주 약간 성능 향상이 있었습니다만, 의미있다고 생각하지 않았고 구현 상의 복잡함 때문에 최종적으로는 이용되지 않았습니다. 또 레이블이 잘못된 트레이닝 샘플이 생각보다 많이 있는데, 이 부분이 영향을 미칠 것 같다는 우려가 있었습니다.

Focal Loss로 hard class에 집중하는건 어떨까?

물론 문제 상황은 다르지만, Focal Loss for Dense Object Detection 에서는 cross entropy loss를 조금 변형 해 어려운 클래스나 hard example에서 성능 향상을 올리는 방법이 있었어서, 간단히 적용을 해보았습니다.

결과적으로는 성능 하락인데, 저는 그 이유를 gradient vanishing으로 파악했습니다. 간단히 설명하면 focal loss는 정답을 잘 맞출수록 gradient 크기를 더 극단적으로 줄이는데, 이게 학습에 오히려 방해가 된다는 판단입니다.

Unknown Unknowns?

Unknown 클래스를 2개로 나누어 생각해볼 수 있습니다.

  • known unknowns : 우리가 아는 ‘unknown’. 즉 트레이닝셋에 unknown으로 레이블링된 소리들.
  • unknown unknowns : 우리가 모르는 ‘unknown’. 즉 트레이닝셋에는 없는 unknown 소리들.

우리가 학습 과정에서 온갖 소리를 모아 '이게 unknown이야’라고 모아둔 데이터가 있을텐데, 그 소리가 아닌 다른 unknown을 어떻게 맞추어야할까라는 고민입니다. 트레이닝 셋에서 up, down, yes, no … 등등과 함께 온갖 unknown을 학습시켰다고 해봅시다. 테스트 셋에서 'town’이라는 소리가 들어오면 네트워크는 아마도 down으로 예측할 확률이 높겠죠. 비슷한 소리로 들릴 가능성이 높습니다. 그런데 우리가 원하는 건 unknown으로 예측을 하는 것입니다.

즉, 네트워크에게 '알 수 없음’을 가르치는 문제가 됩니다. 혹은 다르게 표현하면 out-distribution, 즉 비정상적인 인풋임을 가르쳐야합니다. 실제로 제가 테스트셋을 들어보면서 분석해본 결과 아주 높은 비율로 unknown unknowns 들을 못맞추는 것을 확인을 했습니다. 캐글 1위가 90~91% 수준인데, 왠지 이 문제를 해결하지 못해 벽에 부딪힌게 아닌가하는 의심을 합니다.

Training Confidence-calibrated Classifiers for Detecting Out-of-Distribution Samples

GAN을 이용해 in/out-distribution을 추정하고 네트워크를 학습하는 로스에 KL-Divergence 텀을 추가해 out distribution에 대해 robust하게 만든다는 아이디어입니다. 너무나도 매력적인 아이디어입니다만, GAN Training을 따로 해야하고, 특히 speech 도메인에서 그런 시도를 한다는 것이 일단은 컴피티션의 범위를 벗어나는 일로 봤습니다. 이후에 조금 더 고민해볼 여지를 남겨두는 것으로 정리했습니다.

Pseudo-labeling, learning by association

시도했던 방법 중 하나는 test-set에 대한 Semi-supervised learning을 해보자는 것이었습니다.

Pseudo label은 테스트셋에 대해 프리딕션을 해보고 높은 confidence가 나오는 것들은 정답이라고 생각하고 training set에 포함하는 아이디어입니다. 논문의 저자 분-이동현님이 카카오브레인에 계시기 때문에 조언을 얻을 수 있었고, 저자분께서 직접 테스트도 해주셨으나 성능은 오히려 하락하는 결과가 있었습니다.

Learning by Association은 제가 관심있게 시도했던 방식입니다. 제가 리뷰한 내용을 참고해주세요 : Learning by Association: A versatile semi-supervised training method for neural networks

imagehttp://openresearch.ai/uploads/default/optimized/1X/4803a423bdd746399c298d73304eeb8d318d1e73_1_690x319.jpg

레이블 없는 데이터에 대해 간접적으로 레이블링을 매기는 방식입니다. 이것 역시 성능이 하락했습니다. 하락하는 이유는 단순하게도, unknown class의 test sample들이 엉뚱한 클래스에 mapping되는 것 같다는 것이었습니다. 레이블 없는 데이터를 어딘가에는 association을 시켜야하는데, 이 대회처럼 unknown class가 있는 경우에는 해당 데이터가 임의의 클래스에 할당되어 버리는 문제가 있겠다는 판단입니다.

안타깝게도 성능 향상은 얻지 못했지만, 레이블없이 모아진 대규모의 데이터가 존재하는 경우에 이를 이용해 unknown class 혹은 out-distribution에 대한 판단을 향상시킬 수 있는 방법이 있을까? 라는 고민을 하게 되었습니다.

Testset as Unknown Data

주어진 테스트셋 15만개 중 대략 8만개 이상이 unknown인 것으로 예상이되는 상황이었습니다. dominant한 상황으로 보았고, 소개받은 Universum Prescription: Regularization using Unlabeled Data 에서도 영감을 얻어, 테스트셋 전체를 unknown으로 생각하고 트레이닝 데이터로 포함시키면 어떨까하는 아이디어입니다.

일단 일반적인 트레이닝에서는 성능 하락이 있었습니다만, 잘 학습된 모델에

  • 48 샘플 : 기존 트레이닝 샘플
  • 16 샘플 : 테스트샘플을 unknown으로 레이블

추가 학습(finetune) 하였더니 확실히 unknown에 대해서 강인한 결과를 확인했습니다. 성능 향상도 marginal하게 있었습니다만, 대회 종료 직전 캐글 submission 제한까지 있는 상황이라 추가 실험은 대회 이후로 미루게되었습니다.

아무래도 테스트 샘플에 unknown이 dominant한 상황이고, 잘 학습된 모델에 기존 트레이닝 샘플까지 배치로 들어오다보니, 영향이 거의 없는 상태에서 unknown unknowns에만 더 좋은 영향을 미친 것 같습니다.

기타 시도들

  • mfcc나 다른 피쳐들도 엄청나게 시도를 해보았지만, 이 대회의 경우에는 좋은 성능에는 못미쳤습니다.
  • RNN이나 다른 아키텍쳐의 시도도 좋은 성능은 못 얻었습니다. 문제 특성 상 이해되는 부분입니다.
  • unknown unknowns를 위해 조금 다른 모델링을 할 수도 있겠다는 조언이 많았습니다. 실제로 프로덕션에서 그렇게 하기도 한다고…
  • 시간 상 하지는 못했습니다만, 작은 모델에 transfer learning / distilation을 해보았어도 의미가 있었을 것 같습니다. 9위로 나쁜 성적이 아니므로 상당히 promising했을텐데, submission도 부족하고 개인적인 업무도 많은 상황이라 아이디어로 그쳤습니다.
  • 트레이닝 서머리와 각종 밸리데이션/테스트 샘플에 대한 네트워크 아웃풋과 함께 소리를 들어볼 수 있는 웹앱을 작성했습니다. 이게 네트워크의 약점을 잡는데에 큰 역할을 했습니다. 아무래도 아직까지는 사람이 핸드튜닝해줘야하는 여지가 있다보니… ^^ 좋은 툴을 잘 만들어서 쓰는 것이 귀찮기는해도 큰 도움이 됩니다.

마치며

대회에서 좋은 성적을 올리기보다는 의미있는 시도를 하고 싶었습니다. 문제를 보며 이런 저런 생각을 해보고, 해결을 위한 다양한 시도도 해보는 재미있는 시간이었습니다. 그 과정에서 카카오와 카카오브레인에서 계시는 훌륭한 연구자분들이 조언해주신 게 너무나도 큰 도움이 되었습니다. 이런 저런 논문을 읽다보면 카카오와 카카오브레인에 계신 연구자 분들이 예전에 쓰셨던 논문이 나오기도 했습니다 ^^;

그리고 카카오의 GPU 클라우드가 엄청난 힘이 되었습니다. 업무 외 시간에, 짧은 시간에 여기에 있는 것처럼 많은 실험을 할 수 있었던 것은 GPU 클라우드라는 인프라 덕분입니다. (참고로 저도 초반에 개발을 참여했습니다ㅎ) 수백가지 모델 돌려놓고 최고의 결과만 뽑아서 확인하고, 또 새로운 실험을 돌리는 일이 병렬적으로 쉽게 이루어졌습니다. 게다가 p40 8 gpu에 48코어급의 빵빵한 서버들을 분산 노드로 묶어서 트레이닝까지할 수 있어서, 실험 하나 하는데 수 시간이 채 걸리지 않았던 것도 주요한 이유였습니다.

마지막으로 팀메이트가 매우 중요했습니다. 조석제님과 팀을 한게 신의 한 수 였습니다. 너무나도 훌륭하신 실력에 열정까지 가지고 계셨고, 컴피티션 경험이 많으셔서 마지막으로 가면갈수록 실질적으로 대회 성적에 엄청난 대다수의 기여를 하셨습니다.

컴피티션은 끝났지만… 여전히 unknown unknowns에 대한 잔상이 남아있습니다. 아마 틈 나는대로 조금씩 실험을 해볼 것 같습니다.

구인 광고

구내식당은 없지만, 카카오의 좋은 문화 아래에 훌륭한 리서처/엔지니어와 함께 연구하고 슈퍼 빵빵한 GPU 인프라의 응원까지 받으면서 일하고 싶으신 분들은 아래로!

References


(jake) #2

안녕하세요 librosa를 이용해서데이터 변경하면 데이터 크기가 2배로 커져서
제 노트북으로는 감당을 못하더라구요 혹시 데이터 사이스 커진체로 진행 하셨나요?


#3

데이터 크기가 2배로 커진다는 말씀을 이해 못했어요. [1, 16000] (1초) 짜리 데이터를 변경해도 대략 1초로 비슷한 사이즈가 나오기는 할텐데요… 여러 데이터를 들고 있으면 그만큼 메모리는 필요하겠지만… 엄청나게 많은 메모리를 쓸 것 같지는 않습니다.


(jake) #4

혹시 librosa를 이용해서 데이터 변경해주시는 코드를 공유 받을수있을까요
저는 librosa.stretch 같은것을 이용해서 소리를 늘려주는 방식을 사용했는데요
예를 들어 파일 사이즈가 4 라면 librosa를 이용해서 변경된 파일을 다시 librosa.write를 이용해서
저장했을때 파일 사이즈가 8이 되더라구요.


#5

음… 파일 저장할 때 sampling rate 같은게 달라서 사이즈가 늘어났을 수도 있습니다만… 저는 파일 저장 없이 on the fly로 augmentation해가면서 썼습니다. 아래에 일부 코드를 공유해드릴게요.

"""
Augmentation : https://www.kaggle.com/CVxTz/audio-data-augmentation
"""
import logging
import random
import os
import math

import numpy as np
import noise
from scipy.io import wavfile
try:
    import librosa
except Exception as e:
    print(e)
import colorednoise

__bgdatapath = None
__datapath = None
__extra_silences = None

logger = logging.getLogger('data_augment')
logger.setLevel(logging.WARNING)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)


def _set_bgdatapath(datapath):
    global __bgdatapath
    __bgdatapath = datapath


def set_datapath(datapath):
    global __datapath, __extra_silences
    __datapath = datapath

    _set_bgdatapath(os.path.join(datapath, 'train/audio/_background_noise_'))

    with open('./etcs/silences.txt', 'r') as fin:
        extra_silence = fin.readlines()
        extra_silence = [x.strip() for x in extra_silence if x.strip()]

    # concat all added silences
    silence_samples = []
    for fn in extra_silence:
        sample_rates, samples = wavfile.read(os.path.join(__datapath, 'train/audio', fn))
        silence_samples += [samples]
        assert (sample_rates == 16000)
    __extra_silences = np.concatenate(silence_samples)
    __extra_silences = __extra_silences.astype(np.float32) / np.iinfo(np.int16).max


def sound_trim_32000_random_for_speed(wav):
    if len(wav) > 32000:
        return sound_trim(wav, 32000, True)
    return wav


def sound_trim_16000_random(wav):
    return sound_trim(wav, 16000, True)


def sound_trim_16000_center(wav):
    return sound_trim(wav, 16000, False)


def sound_trim(wav, secs, random=True):
    if len(wav) == secs:
        return wav
    elif len(wav) < secs:
        if random:
            beg = np.random.randint(0, secs - len(wav))
        else:
            beg = (secs - len(wav)) // 2
        new_wav = np.zeros((secs,), dtype=wav.dtype)
        new_wav[beg:beg+len(wav)] = wav
        return new_wav
    else:
        if random:
            beg = np.random.randint(0, len(wav) - secs)
        else:
            beg = (len(wav) - secs) // 2
        return wav[beg:beg+secs]


def augment_volume(wav):
    if random.randint(0, 9) == 0:
        # very small
        logger.debug('augment_volume small')
        return wav * random.uniform(0.3, 0.5)

    return wav * random.uniform(0.8, 1.3)


def augment_noise(wav):
    bgwav = get_silence(length=len(wav))
    ratio = random.uniform(0.15, 0.5) * (max(abs(wav)))
    data_wn = wav + ratio * bgwav

    logger.debug('augment_noise %f' % ratio)

    return data_wn


def augment_roll(wav):
    max = np.max(np.abs(wav))
    th = max / 10

    firstidx = np.argmax(np.abs(wav) > th) * 0.5
    firstidx = int(firstidx)
    lastidx = (len(wav) - np.argmax(np.abs(wav[::-1]) > th)) * 0.5
    lastidx = int(lastidx)
    shift = random.randint(-firstidx, lastidx)
    wav_roll = np.roll(wav, shift)

    logger.debug('augment_shift %d' % shift)

    return wav_roll


def augment_stretch(data, rate=None):
    if rate is not None or random.uniform(0.0, 1.0) < 0.7:
        return data
    input_length = len(data)
    rate = random.uniform(0.85, 1.25) if rate is None else rate
    data = librosa.effects.time_stretch(data, rate)
    if len(data) > input_length:
        data = data[:input_length]
    else:
        data = np.pad(data, (0, max(0, input_length - len(data))), "constant")

    logger.debug('augment_stretch')

    return data


def augment_pitch(data):
    sh = random.uniform(-3.0, 3.0)
    data = librosa.effects.pitch_shift(data, 16000, sh)
    return data


def get_silence(length=16000):
    global __bgdatapath
    silence_type = random.uniform(0.0, 1.0)
    logger.debug('silence_type %f' % silence_type)
    if silence_type <= 0.15:
        # colored noise
        beta = random.uniform(0.01, 1.5)
        bgwav = colorednoise.powerlaw_psd_gaussian(beta, length)
        if random.uniform(0.0, 1.0) < 0.9:
            bgwav /= max(abs(bgwav)) * random.uniform(0.7, 1.5)
    elif silence_type < 0.20:
        # with silence noise
        silence_fname = os.path.join(
            __bgdatapath,
            random.choice(['white_noise.wav', 'pink_noise.wav'])
        )
        _, bgwav = wavfile.read(silence_fname)
        bgwav = bgwav.astype(np.float32) / np.iinfo(np.int16).max
        bgwav = sound_trim(bgwav, length, True)
    elif silence_type < 0.25:
        # perlin - 1
        stepsize = random.uniform(0.1, 0.4)
        base = random.randint(-1000, 1000)
        bgwav = np.zeros((length, ))
        for i in range(length):
            bgwav[i] = noise.pnoise1(i * stepsize, base=base)
    elif silence_type < 0.30:
        # perlin - 2
        stepsize = random.uniform(0.1, 0.6)
        base = random.randint(-1000, 1000)
        stepsize2 = random.random() * 0.5
        base2 = random.randint(-1000, 1000)
        bgwav = np.zeros((length,))
        for i in range(length):
            bgwav[i] = noise.pnoise1(i * stepsize, base=base) * noise.pnoise1(i * stepsize2, base=base2)
    elif silence_type < 0.50:
        global __extra_silences
        bgwav = sound_trim(__extra_silences, length, random=True)
    else:
        # with silence noise
        silence_fname = os.path.join(
            __bgdatapath,
            random.choice([
                'doing_the_dishes.wav', 'dude_miaowing.wav', 'exercise_bike.wav', 'running_tap.wav'
            ])
        )
        _, bgwav = wavfile.read(silence_fname)
        bgwav = bgwav.astype(np.float32) / np.iinfo(np.int16).max
        bgwav = sound_trim(bgwav, length, random=True)

    # drop
    drop_type = random.uniform(0.0, 1.0)
    if drop_type < 0.1:
        d = random.uniform(0.0, 1.0)
        bgwav = np.sign(bgwav) * (np.maximum(np.abs(bgwav) - d * np.max(np.abs(bgwav)), 0.0)) * (1.0 / (1.0 - d))
    elif drop_type < 0.2:
        # small spot drop
        m = random.randint(0, 16000 - 1)
        stddev = random.randint(500, 3000)
        normalizer = 1.5 / norm_dist(m, m, stddev) * random.uniform(0.5, 1.0)
        bgwav = [norm_dist(idx, m, stddev) * normalizer * x for idx, x in enumerate(bgwav)]
        bgwav = np.array(bgwav)

    return bgwav


def norm_dist(x, mean, stddev):
    return math.exp(-(x - mean) ** 2 / (2 * stddev ** 2)) / math.sqrt(2 * math.pi * stddev ** 2)


if __name__ == '__main__':
    set_datapath('/data/private/kaggle-tf/data/')

    wav = get_silence(length=16000)
    wav = (wav * np.iinfo(np.int16).max).astype(np.int16)
    wavfile.write('./testsample/gen_silence.wav', 16000, wav)

    _, wav = wavfile.read('./testsample/right_w.wav')
    wav = wav.astype(np.float32) / np.iinfo(np.int16).max
    wav_wn = augment_noise(wav)
    wav_wn = (wav_wn * np.iinfo(np.int16).max).astype(np.int16)
    wavfile.write('./testsample/aug_wn.wav', 16000, wav_wn)

    wav_roll = augment_roll(wav)
    wav_roll = (wav_roll * np.iinfo(np.int16).max).astype(np.int16)
    wavfile.write('./testsample/aug_roll.wav', 16000, wav_roll)

    wav_pitch = augment_pitch(wav)
    wav_pitch = (wav_pitch * np.iinfo(np.int16).max).astype(np.int16)
    wavfile.write('./testsample/aug_pitch.wav', 16000, wav_pitch)

    # wav_stretch = augment_stretch(wav)
    # wav_stretch = (wav_stretch * np.iinfo(np.int16).max).astype(np.int16)
    # wavfile.write('./testsample/aug_stretch.wav', 16000, wav_stretch)

#6

기간이 많이 지나서 댓글이 달릴지 모르겠지만… 그래도 궁금한 내용이 있어 질문 드립니다.
공유해주신 librosa를 활용한 time stretch and pitch shift를 data augmentation 코드 수행 후 들어보았습니다.

time stretch 0.8과 1.2를 만들어서 들어보니 원본에 비해 소리가 느려지고 빨라지고를 느낄수 있었습니다.
그런데 소리의 속도만 들린게 아니라 반향도 같이 조절한거 같은 기분이 들어 질문을 남기게 되었습니다.

리눅스의 일반적으로 sound exchange라고 sox를 많이 이용하는데,
이 라이브러리를 이용하여 time stretch를 동일한 비율로 조절하여 들어보면 librosa의 결과와 매우 다르게 들려서
librosa의 소리가 너무 로보트 합성음 같이 들리게 되다보니…
이러한 음원을 학습에 써도 되는지가 궁금하여 문의 드리게 되었습니다.

제가 듣기에는 librosa를 이용한 time stretch는 뭔가 다른 프로그램의 결과와 매우 달르게 들렸습니다.
참고로 Audacity(또는 audition)라는 프로그램으로 오디오를 로딩한 후 speed를 변경해도 libsora와 같은 효과가 나오지 않는것 같습니다.

참고로 KALDI에서도 sox를 이용하여 audio augmentation을 수행합니다.

linux의 sox명령어도 같이 첨부합니다.
$ sox -t wav your_input.wav -t wav output.wav speed 1.2
(sox 명령 없을 경우 sudo apt-get install sox)


#7

안녕하세요, 좋은 질문 감사합니다. 제가 음성이나 사운드 쪽을 처음해보다보니 사실 어떤 라이브러리가 있는 지 모르는 상태에서 작업한 것이긴 합니다. stretch 등의 augmentation을 했을 때 원본에 대비해서는 다소 부자연스러워지는 것은 인지하고 있었지만, 음성 소리가 갖는 본질적인 의미는 달라지지 않는 수준이어서 그대로 학습을 했습니다.
다만, 말씀하신 sox 라이브러리를 알았다면, 아마 더 선호하지 않았을까 하는 생각이 드네요 :slight_smile:


#8

답글 달아주셔서 감사합니다^^
질문 달고 이거저거 테스트 하다가 나중에라도 도움이 되실까하여 다시 글을 달게 되었습니다.
Data Augmentation에 사용 간으한 응용프로그램에서 time stretch를 테스트 해보았는데 아래와 같은 순서로
사람이 듣기에 자연스러운것 같더라고요

Audition > Sox = Audacity > Librosa = MUDA

이외에 MUDA라는 라이브러리도 있었는데 이녀석은 내부에 Librosa를 이용하여 아마도 Librosa와 동일하다고 보시면 될것 같습니다.

글보고 텐서도 잘쓰시고 딥러닝쪽에 많은 관심이 있으신것 같아 친해져보고파 ㅋㅋㅋ
오지랖성으로 댓글 다시 달아 봅니다.

그럼 연구 열심히 하시고 다음에 기회되면 또 뵙도록 하시지요^^
감사합니다.
안녕히 계세요.


#9

감사합니다. 담부턴 꼭 추천해주신 걸로 해야겠네요 ^^ 패이스북 친추 주셔요 ㅎ