본문 바로가기

프로젝트

무이메이커스_간 (GAN) 을 활용한 인공지능 (AI) 음성 생성 (Audio Generation) 딥러닝 프로젝트

  • 프로젝트 진행 순서
  • 1. 간 (GAN) 을 통한 인공지능 (AI) 음성 생성 (Audio Generation) 개요
  • 2. 음성 데이터 전처리 (Audio Preprocessing)
  • 3. 딥러닝 모델 생성
  • 4. 모델 평가 및 실생활 적용

 

안녕하세요 헬스케어 제품 개발회사 허니컴의 무이메이커스페이스 입니다.

저희는 4차산업혁명을 맞이하여 딥러닝을 접목시킨 제품 개발을 위해 다양한 프로젝트를 수행하고,

이를 활용하여 인공지능 (AI) 을 지닌 다양한 헬스케어 제품을 생산하는데 그 목적이 있습니다.

이번 시간에는 딥러닝 모델 중 하나인 간 (GAN) 을 활용하여 만든 인공지능 (AI) 으로

음성을 생성하는 (Audio Generation) 프로젝트를 소개하고자 합니다.

목표는 음성을 생성하기 위해 만들어진 간 (GAN) 구조의 딥러닝 모델인 WaveGAN 을 사용하여

인공지능 AI 가 생각하는 사람의 음성과 유사한 소리를 만들어내는 것입니다.

개발 환경은 윈도우 (Window), 언어는 파이썬 (Python), 라이브러리는 파이토치 (Pytorch) 를 사용합니다.

1. 음성 생성 (Audio Generation) 개요

음성 신호는 이미지, 자연어와 더불어 딥러닝의 핵심 분야로 발전중인 항목입니다.

기존의 딥러닝 네트워크는 음성 신호를 인식하여 분류하는 형태로 발전을 해왔으나,

간 (GAN) 구조의 등장으로 원하는 음성을 만들어내기에 이르렀습니다.

https://arxiv.org/abs/1802.04208

비교적 최신에 나온 WaveGAN 모델은 이전에 언급했던 DCGAN 모델을 활용하여

보다 음성 신호에 맞춘 형태로 연구되었고, 이에 해당하는 결과들은 다음 사이트에 명시되어있습니다.

또한 이전 글에서 사용했던 CycleGAN 을 활용한 Style Transfer 를 사용하면 아래 사이트와 같이

음악의 장르를 바꾼 새로운 음성을 생성하는 것도 가능합니다.

https://arxiv.org/abs/1811.09620

 

2. 음성 데이터 전처리 (Preprocessing)

2차원의 픽셀로 이루어진 이미지와 달리 음성 데이터는 1차원의 신호로 구성되어 있습니다.

먼저 해당 형태의 신호를 DCGAN 형태로 맞추기 위해 전처리 과정이 필요합니다.

DCGAN 딥러닝 모델은 Latent Vector 를 64 by 64 픽셀 이미지 형태로 생성하는 구조였습니다.

이를 Discriminator 에 넣어 훈련 데이터랑 비교하는 형식으로 구성이 되어있었는데,

1차원 데이터의 경우, 이는 64 by 64 픽셀 이미지가 아닌 4096 개의 음성 샘플이 나오게 하는 구조가 됩니다.

하지만 사람의 가청 주파수는 20 Hz ~ 20kHz 범위이므로, 4096 개의 샘플만으로는 음성을 담기 부족하고,

해당 논문에서는 Upsampling Layer 를 하나 추가하여 16384 개의 샘플을 추출합니다.

DCGAN 과 WaveGAN 의 차이점

해당 Sampling Rate 는 훈련 데이터와 동일한 형태로 구성되야 하므로,

전처리 과정에서 모든 훈련용 음성 데이터를 통일시켜야 합니다.

이미지를 불러올 때와 동일하게 glob 을 통해 root 에 있는 파일들을 모두 불러오고,

파이토치 (Pytorch) 의 딥러닝 형태에 맞추기 위해 Dataset 클래스를 만들어줍니다.

훈련용 데이터로 사용된 SC09 (Speech Commands 0 through 9) 음성은 16kHz 로 되어있으므로

이를 불러와 16384 로 변환해줍니다.

 

def Audios(self,args):
    files = []
    for i in self.classes:
        file = glob.glob(os.path.join(self.root + i + '\\') + '/*.*')
        for a in range(len(file)):
            file[a] = (file[a], self.class_to_idx[i])
        files += file
    return files

class AudioDataset(torch.utils.data.Dataset):
    def __init__(self,args):
        self.root = args.root + args.mode + '\\'
        self.files = Audios(self, args)
        self.Audio_Length = 16384

    def __getitem__(self, index):
        item, sr = librosa.core.load(self.files[index][0],sr=16000,mono=False)
        if len(item) < self.Audio_Length:
            padding_size = self.Audio_Length - len(item)
            item = np.pad(item, (0, padding_size), mode='edge')
        item = item.reshape(1, item.shape[0])
        item /= np.max(np.abs(item))
        return item

    def __len__(self):
        return len(self.files)

 

전처리를 위한 설정 및 모델을 위한 설정값을 미리 지정해둡니다.

DCGAN 을 근간으로 하고있기 때문에 해당 포스팅과 큰 차이는 없습니다.

 

def main():

    parser = argparse.ArgumentParser(description='Audio_Generation')
    # Preprocess Setting
    parser.add_argument('--cuda',type=bool,default=True)
    parser.add_argument('--ngpu',type=int,default=1)
    parser.add_argument('--mode',type=str,default='train')
    parser.add_argument('--num_workers',type=int,default=0)
    parser.add_argument('--root',type=str,default='')
    parser.add_argument('--stat',type=int,default=5)
    parser.add_argument('--save_stat',type=int,default=10)
    parser.add_argument('--save_root',type=str,default='')
    # Hyperparameter Setting
    parser.add_argument('--batch_size',type=int,default=64)
    parser.add_argument('--epoch',type=int,default=10)
    parser.add_argument('--lr',type=float,default=0.001)
    parser.add_argument('--beta1',type=float,default=0.5)
    # Model Setting
    parser.add_argument('--channels', type=int, default=1)
    parser.add_argument('--noise',type=int,default=100)
    parser.add_argument('--feature_g',type=int,default=64)
    parser.add_argument('--feature_d',type=int,default=64)

    args = parser.parse_args()

    device = torch.device('cuda:0' if (torch.cuda.is_available() and args.ngpu>0) else 'cpu')

    dataset = Dataset.AudioDataset(args)
    dataloader = torch.utils.data.DataLoader(dataset, batch_size=args.batch_size,
                                             shuffle=True, num_workers=args.num_workers)

 

 

3. 딥러닝 모델 생성

위에서 말한 것 처럼 DCGAN 에 Upsampling Layer 를 하나 추가합니다.

Noise 신호는 DCGAN 과 마찬가지로 1 by 100 Vector 형태의 랜덤값을 사용합니다.

class Generator(nn.Module):
    def __init__(self,args):
        super(Generator, self).__init__()
        self.fc = nn.Linear(args.noise,256*args.feature_d)
        self.deconv1 = nn.ConvTranspose1d(16*args.feature_d, 8*args.feature_d, kernel_size=24, stride=4, padding=10)
        self.deconv2 = nn.ConvTranspose1d(8*args.feature_d, 4*args.feature_d, kernel_size=24, stride=4, padding=10)
        self.deconv3 = nn.ConvTranspose1d(4*args.feature_d, 2*args.feature_d, kernel_size=24, stride=4, padding=10)
        self.deconv4 = nn.ConvTranspose1d(2*args.feature_d, args.feature_d, kernel_size=24, stride=4, padding=10)
        self.deconv5 = nn.ConvTranspose1d(args.feature_d, 1, kernel_size=24, stride=4, padding=10)

        ### Initialize weights ###
        for module in self.modules():
            if isinstance(module, nn.ConvTranspose1d) or isinstance(module, nn.Linear):
                nn.init.xavier_uniform_(module.weight.data)

    def forward(self, input, args):
        x = self.fc(input)
        x = x.reshape((-1,16*args.feature_g,16))
        z = F.relu(self.deconv1(x))            #(64, 8d, 64)
        z = F.relu(self.deconv2(z))            #(64, 4d, 256)
        z = F.relu(self.deconv3(z))            #(64, 2d, 1024)
        z = F.relu(self.deconv4(z))            #(64, d, 4096)
        x = torch.tanh(self.deconv5(z))   #(64, 1, 16384)
        return x

Discriminator 는 Generator 를 통해 생성된 데이터를 훈련 데이터와 유사하도록 학습합니다.

class Discriminator(nn.Module):
    def __init__(self,args):
        super(Discriminator,self).__init__()
        self.phase_shuffle = PhaseShuffle(2)
        self.fc = nn.Linear(256 * args.feature_d, 1)
        self.conv1 = nn.Conv1d(1, args.feature_d, 25, 4, 11)
        self.conv2 = nn.Conv1d(args.feature_d, 2*args.feature_d, 25, 4, 11)
        self.conv3 = nn.Conv1d(2*args.feature_d, 4*args.feature_d, 25, 4, 11)
        self.conv4 = nn.Conv1d(4*args.feature_d, 8*args.feature_d, 25, 4, 11)
        self.conv5 = nn.Conv1d(8*args.feature_d, 16*args.feature_d, 25, 4, 11)

    def forward(self,x,args):
        x = F.leaky_relu(self.conv1(x),negative_slope=0.2)
        x = self.phase_shuffle(x)
        x = F.leaky_relu(self.conv2(x),negative_slope=0.2)
        x = self.phase_shuffle(x)
        x = F.leaky_relu(self.conv3(x),negative_slope=0.2)
        x = self.phase_shuffle(x)
        x = F.leaky_relu(self.conv4(x),negative_slope=0.2)
        x = self.phase_shuffle(x)
        x = F.leaky_relu(self.conv5(x),negative_slope=0.2)

        x = x.reshape((-1,256*args.feature_d))
        x = self.fc(x)
        return x

여기에서 DCGAN 과 다르게 Phase Shuffle 이라는 기법을 추가로 사용합니다.

Phase Shuffle 은 Discriminator 가 Artifact 를 보고 음성을 생성하지 않도록 방해하는 역할을 합니다.

음성 신호는 이미지와 다르게 주기적인 신호이므로 GAN 의 특성 상 주기적인 Artifact 가 발생할 수 있습니다.

class PhaseShuffle(nn.Module):
    '''
    Performs phase shuffling (to be used by Discriminator ONLY) by:
       -Shifting feature axis of a 3D tensor by a random integer in [-n, n]
       -Performing reflection padding where necessary
    '''

    def __init__(self, shift_factor):

        super(PhaseShuffle, self).__init__()
        self.shift_factor = shift_factor

    def forward(self, x):  # x shape: (64, 1, 16384)
        # Return x if phase shift is disabled
        if self.shift_factor == 0:
            return x

        # k_list: list of batch_size shift factors, randomly generated uniformly between [-shift_factor, shift_factor]
        k_list = torch.Tensor(x.shape[0]).random_(0, 2 * self.shift_factor + 1) - self.shift_factor
        k_list = k_list.numpy().astype(int)

        # k_map: dict containing {each shift factor : list of batch indices with that shift factor}
        # e.g. if shift_factor = 1 & batch_size = 64, k_map = {-1:[0,2,30,...52], 0:[1,5,...,60], 1:[2,3,4,...,63]}
        k_map = {}
        for sample_idx, k in enumerate(k_list):
            k = int(k)
            if k not in k_map:
                k_map[k] = []

            k_map[k].append(sample_idx)

        shuffled_x = x.clone()

        for k, sample_idxs in k_map.items():
            if k > 0:  # Remove the last k values & insert k left-paddings
                shuffled_x[sample_idxs] = F.pad(x[sample_idxs][..., :-k],
                                                pad=(k, 0),
                                                mode='reflect')

            else:  # 1. Remove the first k values & 2. Insert k right-paddings
                shuffled_x[sample_idxs] = F.pad(x[sample_idxs][..., abs(k):],
                                                pad=(0, abs(k)),
                                                mode='reflect')

        assert shuffled_x.shape == x.shape, "{}, {}".format(shuffled_x.shape, x.shape)

        return shuffled_x

또한 최종적으로 학습하기 위한 Loss 값으로 기존의 1과 0으로만 학습 방향을 정하던 DCGAN 과 달리

W distance 라는 확률분포의 거리를 사용하는데, 이를 통해 더 나은 학습을 진행할 수 있습니다.

def gradientPenalty(D, x, z, batch_size, device, args,lmbda=10.0):
    ''' lmbda : gradient penalty regularization factor '''
    ### 1. Compute interpolation factors
    alpha = torch.rand(x.shape[0], 1, 1)  # x shape: (batch_size, 1, signal_length)
    alpha = alpha.expand(x.size())  # duplicate the same random # signal_length times
    alpha = alpha.to(device)

    ### 2. Interpolate between real & fake data
    interpolates = alpha * x + ((1 - alpha) * z)
    interpolates = interpolates.to(device)
    interpolates = Variable(interpolates, requires_grad=True)

    ### 3. Evaluate D
    D_interpolates = D(interpolates,args)

    ### 4. Obtain gradients of D w.r.t. inputs x
    gradients = grad(inputs=interpolates,
                     outputs=D_interpolates,
                     grad_outputs=torch.ones(D_interpolates.size()).to(device),
                     create_graph=True,
                     retain_graph=True,
                     only_inputs=True)[0]

    gradients = gradients.view(gradients.size(0), -1)

    gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean() * lmbda

    return gradient_penalty

4. 모델 평가 및 실생활 적용

실제 WaveGAN 에 사용되는 음성 신호들은 데이터가 크기 때문에 학습에 오랜 시간이 소요됩니다.

또한 이미지와 달리 아직 Loss 값이 불안정하게 학습되는 경향이 있어,

여기서는 결과를 명시한 사이트를 제시합니다.

해당 사이트는 WaveGAN 을 통해 사람의 목소리, 새, 드럼, 피아노 등의 음성을 생성합니다.

https://chrisdonahue.com/wavegan_examples/

 

또한 이를 활용하여 Style Transfer 를 진행시킨 TimbreTron 은 음악의 악기를 다른 악기로 변환시켜

더욱 멋진 음악을 생성하는데 도움을 줄 수 있습니다.

https://www.cs.toronto.edu/~huang/TimbreTron/index.html

 

4차산업혁명을 통한 빅데이터와 인공지능 (AI) 붐은 다양한 딥러닝의 발전을 가져왔으나,

아직까지 사람들에게 이는 친숙하지 못한 방법론이며

어떠한 데이터냐에 따라 그 변화가 다양하므로 정답이 존재하지 않습니다.

허니컴 메이커스페이스는 데이터에 따라 다양한 방향으로 인공지능 (AI) 딥러닝 모델을 적용해보며

'정답' 에 근접한 모델을 생성하고,

이를 사람들이 이해하기 쉽도록 시각화와 같은 방법을 통해 설명해나가고자 합니다.

........

시제품 제작 문의