오디오 딥러닝을 해 봅시다! (사운드 분류)
전체 개념
오디오 딥러닝을 하기 위해서는, 오디오 파일을 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
나머지는 너무 길기 때문에 다음 포스트에 하도록 하겠습니다!
'Deep Learning' 카테고리의 다른 글
Pytorch에서 Batch Size 1로 했을 때 accuracy가 떨어지는 문제 해결 (model.eval()) (0) | 2022.09.23 |
---|---|
오디오 딥러닝을 해봅시다! (Sound Classification) - 2. 모델을 이용해서 학습하기 (0) | 2022.09.08 |
torchviz로 모델 시각화 하기 (0) | 2022.07.27 |
Coursera Deep Learning Specialization 후기 (0) | 2022.02.08 |
rtx 3070을 이용한 환경 설정 yolov5 (ft. 3080, 3090 및 30 시리즈..) (1) | 2021.05.05 |