Deep Learning

오디오 딥러닝을 해봅시다! (Sound Classification) - 1. 데이터 전처리

jinmc 2022. 9. 6. 19:04
반응형

오디오 딥러닝을 해 봅시다! (사운드 분류)

 

 

전체 개념

오디오 딥러닝을 하기 위해서는, 오디오 파일을 Spectrogram이라는 이미지 파일로 만들고,

그걸 이용해서 CNN을 이용한 딥러닝이 가능합니다.

참조에서 가져온 이미지

참조에서는 4초 정도의 오디오 파일들을 모은다고 합니다.

 

 

데이터 전처리 및 라벨링

라벨링은 여러가지 방법이 있겠지만, 여기서는 CSV 파일을 활용합니다.

Pandas를 이용해서 label 정보를 모아줍니다.

Pandas는 conda를 이용한다면 conda install, 또는 pip로 설치가 가능합니다.

 

# ----------------------------
# Prepare training data from Metadata file
# ----------------------------
import pandas as pd
from pathlib import Path

download_path = Path.cwd()/'UrbanSound8K'

# Read metadata file
metadata_file = download_path/'metadata'/'UrbanSound8K.csv'
df = pd.read_csv(metadata_file)
df.head()

# Construct file path by concatenating fold and file name
df['relative_path'] = '/fold' + df['fold'].astype(str) + '/' + df['slice_file_name'].astype(str)

# Take relevant columns
df = df[['relative_path', 'classID']]
df.head()

CSV가 아니더라도, 파일명으로 labeling을 하거나, 디렉토리 명으로 labeling하는 방법이 있습니다.

 

AudioUtil 클라스 준비

전처리를 위해서 AudioUtil 클라스를 준비합니다.

이 코드를 위해서는 torch(pytorch), torchaudio 설치가 필요합니다.

Ipython은 Jupyter notebook에서 오디오를 재생하기 위해서 필요하고,

python 파일을 돌리기 위해서는 굳이 필요하지 않습니다.

 

rechannel => channel의 수가 맞지 않은 경우 맞춰 줍니다.

resample => sampling rate이 맞지 않는 경우 맞춰 줍니다. 보통은 44100을 많이 쓰지만, 16000을 쓰는 경우도 많습니다. (yamnet의 경우 16000을 사용합니다.)

pad_trunc => 4초동안의 (아니면 정해진 시간 동안) 데이터를 맞춰줍니다. 4초보다 짧을 경우 나머지 시간을 침묵으로 넣어주고(pad), 4초보다 긴 경우 4초만큼만 사용할 수 있도록 잘라줍니다.

time_shift => Data Augmentation의 일환으로, 왼쪽이나 오른쪽으로 파형을 shift하는 메소드입니다.

spectro_gram => 스펙트로그램을 리턴합니다. 스펙트로그램에 대해서는 그 전 포스팅에서 많이 다뤘으니, 따로 다루지는 않겠습니다.

spectro_augment => Data Augmentation을 Mel Spectrogram에 FrequencyMasking과 TimeMasking 이 있는데, torchaudio의 tranforms library에 있는 함수들을 사용합니다. 

 

 

import math, random
import torch
import torchaudio
from torchaudio import transforms
from IPython.display import Audio

class AudioUtil():
  # ----------------------------
  # Load an audio file. Return the signal as a tensor and the sample rate
  # ----------------------------
  @staticmethod
  def open(audio_file):
    sig, sr = torchaudio.load(audio_file)
    return (sig, sr)

  # ----------------------------
  # Convert the given audio to the desired number of channels
  # ----------------------------
  @staticmethod
  def rechannel(aud, new_channel):
    sig, sr = aud

    if (sig.shape[0] == new_channel):
      # Nothing to do
      return aud

    if (new_channel == 1):
      # Convert from stereo to mono by selecting only the first channel
      resig = sig[:1, :]
    else:
      # Convert from mono to stereo by duplicating the first channel
      resig = torch.cat([sig, sig])

    return ((resig, sr))
  # ----------------------------
  # Since Resample applies to a single channel, we resample one channel at a time
  # ----------------------------
  @staticmethod
  def resample(aud, newsr):
    sig, sr = aud

    if (sr == newsr):
      # Nothing to do
      return aud

    num_channels = sig.shape[0]
    # Resample first channel
    resig = torchaudio.transforms.Resample(sr, newsr)(sig[:1,:])
    if (num_channels > 1):
      # Resample the second channel and merge both channels
      retwo = torchaudio.transforms.Resample(sr, newsr)(sig[1:,:])
      resig = torch.cat([resig, retwo])

    return ((resig, newsr))

  # ----------------------------
  # Pad (or truncate) the signal to a fixed length 'max_ms' in milliseconds
  # ----------------------------
  @staticmethod
  def pad_trunc(aud, max_ms):
    sig, sr = aud
    num_rows, sig_len = sig.shape
    max_len = sr//1000 * max_ms

    if (sig_len > max_len):
      # Truncate the signal to the given length
      sig = sig[:,:max_len]

    elif (sig_len < max_len):
      # Length of padding to add at the beginning and end of the signal
      pad_begin_len = random.randint(0, max_len - sig_len)
      pad_end_len = max_len - sig_len - pad_begin_len

      # Pad with 0s
      pad_begin = torch.zeros((num_rows, pad_begin_len))
      pad_end = torch.zeros((num_rows, pad_end_len))

      sig = torch.cat((pad_begin, sig, pad_end), 1)
      
    return (sig, sr)

  # ----------------------------
  # Shifts the signal to the left or right by some percent. Values at the end
  # are 'wrapped around' to the start of the transformed signal.
  # ----------------------------
  @staticmethod
  def time_shift(aud, shift_limit):
    sig,sr = aud
    _, sig_len = sig.shape
    shift_amt = int(random.random() * shift_limit * sig_len)
    return (sig.roll(shift_amt), sr)

  # ----------------------------
  # Generate a Spectrogram
  # ----------------------------
  @staticmethod
  def spectro_gram(aud, n_mels=64, n_fft=1024, hop_len=None):
    sig,sr = aud
    top_db = 80

    # spec has shape [channel, n_mels, time], where channel is mono, stereo etc
    spec = transforms.MelSpectrogram(sr, n_fft=n_fft, hop_length=hop_len, n_mels=n_mels)(sig)

    # Convert to decibels
    spec = transforms.AmplitudeToDB(top_db=top_db)(spec)
    return (spec)

  # ----------------------------
  # Augment the Spectrogram by masking out some sections of it in both the frequency
  # dimension (ie. horizontal bars) and the time dimension (vertical bars) to prevent
  # overfitting and to help the model generalise better. The masked sections are
  # replaced with the mean value.
  # ----------------------------
  @staticmethod
  def spectro_augment(spec, max_mask_pct=0.1, n_freq_masks=1, n_time_masks=1):
    _, n_mels, n_steps = spec.shape
    mask_value = spec.mean()
    aug_spec = spec

    freq_mask_param = max_mask_pct * n_mels
    for _ in range(n_freq_masks):
      aug_spec = transforms.FrequencyMasking(freq_mask_param)(aug_spec, mask_value)

    time_mask_param = max_mask_pct * n_steps
    for _ in range(n_time_masks):
      aug_spec = transforms.TimeMasking(time_mask_param)(aug_spec, mask_value)

    return aug_spec

 

커스텀 데이터 준비

Pytorch에 있는 Dataset, DataLoader 클래스를 사용해서 SoundDS 클래스를 만듭니다.

SoundDS에서 AudioUtil을 사용해서 preprocessing을 합니다.

 

from torch.utils.data import DataLoader, Dataset, random_split
import torchaudio

# ----------------------------
# Sound Dataset
# ----------------------------
class SoundDS(Dataset):
  def __init__(self, df, data_path):
    self.df = df
    self.data_path = str(data_path)
    self.duration = 4000
    self.sr = 44100
    self.channel = 2
    self.shift_pct = 0.4
            
  # ----------------------------
  # Number of items in dataset
  # ----------------------------
  def __len__(self):
    return len(self.df)    
    
  # ----------------------------
  # Get i'th item in dataset
  # ----------------------------
  def __getitem__(self, idx):
    # Absolute file path of the audio file - concatenate the audio directory with
    # the relative path
    audio_file = self.data_path + self.df.loc[idx, 'relative_path']
    # Get the Class ID
    class_id = self.df.loc[idx, 'classID']

    aud = AudioUtil.open(audio_file)
    # Some sounds have a higher sample rate, or fewer channels compared to the
    # majority. So make all sounds have the same number of channels and same 
    # sample rate. Unless the sample rate is the same, the pad_trunc will still
    # result in arrays of different lengths, even though the sound duration is
    # the same.
    reaud = AudioUtil.resample(aud, self.sr)
    rechan = AudioUtil.rechannel(reaud, self.channel)

    dur_aud = AudioUtil.pad_trunc(rechan, self.duration)
    shift_aud = AudioUtil.time_shift(dur_aud, self.shift_pct)
    sgram = AudioUtil.spectro_gram(shift_aud, n_mels=64, n_fft=1024, hop_len=None)
    aug_sgram = AudioUtil.spectro_augment(sgram, max_mask_pct=0.1, n_freq_masks=2, n_time_masks=2)

    return aug_sgram, class_id

 

나머지는 너무 길기 때문에 다음 포스트에 하도록 하겠습니다!

 

참조 : Audio deep learning made simple

반응형