본문 바로가기
Machine Learning/유튜브, 책, 아티클 정리

만들면서 배우는 생성 AI 11장 - 음악생성

by W_log 2023. 12. 15.

이번 장에서는 음악 생성에 활용된 생성AI 모델에 대해서 정리해보았습니다.

 

1. 배경

 

음악은 음표가 이어지면서 만들어내기 때문에 시퀀스 예측 문제로 접근하는 경우가 많습니다. 그래서 기본적으로는 텍스트 생성 모델들을 차용해서 접근하는 경우가 꽤 있습니다.

 

문장의 단어와 유사하게 음표를 토큰의 시퀀스로 취급하도록 해서 트랜스포머 모델을 적용합니다. 문장도 직전 단어만 중요한게 아니라 하나의 문장 안의 다양한 단어들의 영향도를 고려한게 어텐션 모델인데, 음표도 동일하게 적용했다고 보시면 됩니다.

 

다만, 음악 창작은 텍스트 생성과는 다르게 피치(음의 높이)와 리듬이 있는 점을 고려해야합니다. 또한 악기가 여러가지가 있어서 그 악기들 간의 화음을 만들어내는 것도 고려해야합니다. 텍스트만 처리해야하는 방식과는 다릅니다. 또한 텍스트는 한 단어 한 단어를 생성하는 형태로 작동해도 괜찮지만 주로 음악 생성은 음표 하나하나보다는 한 음절, 한마디 정도를 생성하는 등 만들어내는 단위가 다른 문제도 있습니다.

 

이 장에서는 위의 언급한 음악 도메인 자체에서 생길 수 있는 고민거리들을 어떻게 해결해나갔는지를 중심으로 살펴보려고 합니다. 결국 도메인이 중요하다는 것을 느끼는 챕터입니다. 그만큼 이 장에서는 음악에 대한 기본적인 이론에 대한 정리에 많은 공간을 할애했다.

 

 

2. 음악 생성을 위한 트랜스포머 - MuseNet

 

기본적인 문제

  • 음악 생성 작업에서 시퀀스 N의 길이는 음악이 진행되면서 커지며, 이는 각 트랜스포머 헤드에 대한 NXN 어텐션 행렬을 저장하고 계산하는데 많은 비용이 듭니다. 
  • 사람이 작곡을 하는 방식에서는 모델이 장기적인 구조를 중심으로 곡을 구성하고 몇분 전의 모티프와 소절을 반복해야하므로 위 문제를 해결하기 위해 입력 시퀀스의 토큰 개수를 짧게 만드는 것은 이상적이지 않습니다.

해결책

  • OpenAI는 이 문제를 희소 트랜스포머라는 트랜스포머의 한 형태를 활용해서 해결했습니다.
    • sparse란 (1,0,0,0,1) 이렇게 띄엄띄엄 값이 있는 것을 의미하는데 즉 입력의 일부 위치에 대한 가중치만 계산하도록 해서 계산 복잡성과 메모리를 줄였습니다. 
    • 그 결과 4096개 이상의 토큰에 대해 완전한 어텐션을 구성하여 작동할 수 있게 되었습니다.

 

구현

 

1. 음악 데이터 불러오기

 

파이썬에서는 music21이라는 라이브러리를 통해서 음악데이터를 읽고 활용할 수 있습니다.

import music21

file = "/app/data/bach-cello/cs1-2all.mid"
example_score = music21.converter.parse(file).chordify()

example_score.show("text")
<output>
{4.0} <music21.stream.Measure 2 offset = 4.0>
    {0.0} <music21.chord.Chord G2 D3 B3>

 

위 음표는 음악의 4번째 박자(0부터 시작)에서 시작하고 (다음 음표가 5번째 박자부터 시작하므로) 길이가 1박자입니다. 낮은 G, D, B 코드로 구성됩니다. 

 

또한 G와 같은 알파벳 뒤에 있는 숫자는 옥타브를 의미하고 G2는 G3보다 한 옥타브 아래라고 볼 수 있습니다.

 

이렇게 불러온 정보를 음표 문자열과 각 음표의 지속 시간 문자열(4분음표, 8분음표)로 변형합니다.

 

2. 토큰화

 

TextVectorization이라는 라이브러리를 쓰는거라서 코드로 대체합니다.

def create_dataset(elements):
    ds = (
        tf.data.Dataset.from_tensor_slices(elements).batch(BATCH_SIZE, drop_remainder = True).shuffle(1000)
        
    vectorize_layer = layers.TextVectorization(standardize = None, output_mode = "int")
    vectorize_layer.adapt(ds)
    vocab = vectorize_layer.get_vocabulary()
    return ds, vectorize_layer, vocab


notes_seq_ds, notes_vectorize_layer, notes_vocab = create_dataset(notes)

duration_seq_ds, durations_vectorize_layer, durations_vocab = create_dataset(durations)

seq_ds = tf.data.Dataset.zip((notes_seq_ds, durations_seq_ds))

 

 

3. 훈련 세트 만들기

 

MuseNet에서는 슬라이딩 윈도우 기법을 사용했는데 한 마디 정도를 데이터로 넣고 한음표씩 옮겨가면서 이를 다시 출력으로 활용하는 형태로 진행했다. 

 

4. 세부 모델 구조

class TokenAndPositionEmbedding(layers.Layer):
    def __init__(self, vocab_size, embed_dim):
        super(TokenAndPositionEmbedding, self).__init__()
        self.vocab_size = vocab_size
        self.embed_dim = embed_dim
        self.token_emb = layers.Embedding(input_dim = vocab_size, output_dim = embed_dim)
        self.pos_emb = keras_nlp.layers.SinePositionEncoding()
        
    def call(self, x):
        embedding = self.token_emb(x)
        positions = self.pos_emb(embedding)
        return embedding + positions
  • 사인 위치 인코딩  : 앞서 우리는 트랜스포머에서 토큰의 위치 정보를 제공하기 위해서 위치 인코딩을 사용했었는데 이는 시퀀스의 최대 길이를 정의해야한다는 한계가 있었ㅅ브니다.
    • 음악은 상대적으로 긴 시퀀스이기 때문에(한곡이 3분 정도, 클래식은 1시간 정도까지의 길이) 이 문제를 피하고자 사인 위치 임베딩을 사용합니다. 
    • Keras에서 이미 이 인코딩 방식을 구현해놓았기 때문에 아래 코드로 불러올 수 있다.

 

  • 다중입력과 다중출력 처리

우리는 모델에 음표와 지속시간을 두개 제공하고 모델은 동일하게 다음 음표와 다음 지속시간을 알려줘야한다. 이전 텍스트는 하나 입력 하나 출력이었지만 이번은 다르다. 

 

토큰 쌍을 만드는 방법도 있지만 이럴 경우에는 새로운 조합 쌍을 만들어내는데 보수적일 수밖에 없다. 예시로 G5, 1/3 조합이 input에 없고 G5, 1/2만 많은 경우에는 주로 출력에서는 G5, 1/3과 같은 새로운 음을 만들어낼 확률이 매우 낮을 것이다.

 

그래서, 위 모델에서는 음표 입력과 지속시간 입력을 concatenate해서 [x, y, 128] 2개를 합쳐서 [x,y,256]으로 바꾼다.

 

음표와 지속시간 토큰을 번갈아 배치하여 단일한 모형으로 만들고 동일하게 출력하는 형태로 학습할 수 있지만 이 경우에는 올바르게 음표, 지속시간이 나오도록 학습해야하는게 어려울 수 있다. 

 

5. 결과 해석

 

실제로 epoch이 진행되면서 더 복잡한 음악을 만들어내는 것을 볼 수 있고, 전혀 나올 수 없는 음의 조합들이나 같은 장조의 음이 나오는 것을 확인할 수 있었습니다.

 

추가로 어텐션이 잘작동했는지는 아래처럼 볼 수 있는데 초반에는 초반 부분밖에 정보가 없기 때문에 신경망이 모든 관심을 START 토큰에 집중하지만 이후부터는 옅어지는 것을 볼 수 있다. 주로 직전 4개까지의 음만 참고하는 것도 볼 수 있다.

 

 

6. 다성 음악으로의 확장

 

앞선 모델은 하나의 악기로만 연주하는 음악이어서 토큰을 하나의 시퀀스로 활용할 수 있었지만 다성 음악에서는 이를 활용하기가 어렵습니다. 그래서 "Music Transformer : Generating Music with Long-Term Structure" 에서 소개한 그리드 토큰화와 이벤트 기반 토큰화라는 두가지 방식을 사용해서 이 문제를 풀어봅니다.

 

  • 그리드 토큰화

소프라노, 알토, 테너, 베이스를 하나의 오선지에 그린다고 보면 이를 마치 간트 차트처럼 각각이 입력한 형태로 벡터로 표현하는 방식입니다. 하단의 이미지를 피아노롤이라고 부릅니다. 나중에 MuseGAN 때 참고할 이미지이기 때문에 기억해두시면 좋습니다. 이 때 들어가는 값은 소프라노, 알토, 테너, 베이스 순으로 음의 높낮이 값이 순차적으로 들어가서, 하나의 시퀀스로 만듭니다.

피아노롤 이미지

하지만, 모델이 음표의 지속시간을 인코딩하지 않고 음표 여부만 인코딩하기 때문에 하나의 긴 음표와 음정이 같고 연속된 두개의 짧은 음표 사이의 차이를 구분할 방법이 없다는 점이나, 셋잇단음표같이 매우 박자가 작은 음표 그룹을 인코딩하면 필요한 토큰수가 매우 크게 늘어나 이를 훈련시키는 것이 어렵습니다. 

 

 

  • 이벤트 토큰화

음표를 연주하는 행위를 이벤트 단위로 정의를 해서 토큰화한 후 이 토큰을 학습시키는 방식을 사용합니다. 

  • NOTE_ON<피치> : 주어진 피치의 음표 재생 시작 
  • NOTE_OFF<피치> : 주어진 피치의 음표 재생 중지
  • TIME_SHIFT<스텝> : 주어진 스텝만큼 앞으로 이동

위 어휘사전을 이용해서 아래와 같이 음악으로 표현할 수 있습니다.

 

[
 NOTE_ON<74>, NOTE_ON<70>, NOTE_ON<65>, NOTE_ON<58>,
 TIME_SHIFT<1.0>,
 NOTE_OFF<74>,NOTE_ON<65>, NOTE_ON<58>,
 TIME_SHIFT<0.5>, 
 NOTE_OFF<58>,  NOTE_ON<60>
 ...
]

 

 

 ]

 

사실 구조화가 잘 되어 있는 방식은 아니기 때문에 트랜스포머가 내재된 패턴을 학습하기가 더 어려울 수 있습니다.

 

 

 

 

3. MuseGAN

 

위의 간트차트처럼 보이는 이미지는 마치 그림처럼 보일 수도 있습니다. 이런 생각으로 이 피아노롤 이미지를 이미지로 생각하고 이미지 생성방법을 활용하는 방식으로 접근한 것이 MuseGAN입니다. 

 

결국 모델은 패턴을 파악하는 것이기 때문에 다양한 패턴 파악 방식을 시도해보는 것입니다. 

 

생성자

 

다른 GAN과 다르게 생성자가 하나의 잡음을 입력으로 받지 않고 각각 화음, 스타일, 멜로디, 리듬을 의미하는 4개의 입력을 나누어서 제공합니다. 

 

이 때 화음과 멜로디 같이 마디 한개한개 생성할 때마다 변주를 가하는 요소는 temporal network를 넣고 곡 전체에서 바뀌지 않는 리듬이나 스타일 입력은 그대로 전달됩니다. 

 

이렇게 4개의 벡터를 concatenate해서 마디 생성기에 넘겨주면, 판별자는 해당 이미지와 우리가 모방하려는 곡으로부터 뽑아낸 이미지와의 차이를 판별하는 형태로 진행됩니다.

 

판별자

 

일반적인 판별자와 거의 동일한 작동을 하고 있으며, WGAN-GP를 사용하기 때문에 배치 노멀라이제이션을 사용하지는 않습니다.

 

 

 

관련해서 더 다양한 정보를 얻고 싶은 경우, 음악 생성과 관련해 다양한 기법들을 정리해놓은 https://oreil.ly/YfaiJ  사이트를 추천합니다. 

 

요약

 

무엇보다 AI로 문제를 해결하는데 있어서 도메인을 어떻게 이해하고 접근하냐가 중요한지를 느낄 수 있었던 장이었습니다. 앞에서 배운 내용들을 음악이라는 도메인 안에서 어떻게 해결할지를 공부할 수 있어서 좋았습니다.