정화 코딩

[ML] 감정 기반 오디오 변환 모델 만들기 본문

Machine Learnig

[ML] 감정 기반 오디오 변환 모델 만들기

jungh150c 2024. 11. 22. 01:51

가상환경 생성

conda create -n emotionTTS python=3.10

필요한 라이브러리 설치

conda activate emotionTTS
pip install librosa torch torchvision torchaudio matplotlib pandas

사진에는 몇개 빠졌지만 위에 적은 것들 전부 받아야 한다.

프로젝트 구조

모델 관련 코드

# emotionTTS.py

import torch
import torch.nn as nn

class EmotionTTS(nn.Module):
    def __init__(self, emotion_embedding_dim, input_dim, output_dim):
        """
        감정 정보를 반영하여 Mel Spectrum을 생성하는 TTS 모델.
        
        Parameters:
        - emotion_embedding_dim (int): 감정 임베딩 벡터의 차원.
        - input_dim (int): 입력 Mel Spectrum의 차원.
        - output_dim (int): 출력 Mel Spectrum의 차원.
        """
        super(EmotionTTS, self).__init__()
        
        # 감정 임베딩 레이어 (6개의 감정 레이블)
        self.emotion_embedding = nn.Embedding(6, emotion_embedding_dim)
        
        # Encoder: Mel Spectrum과 감정 임베딩을 처리
        self.encoder = nn.Conv1d(input_dim + emotion_embedding_dim, 512, kernel_size=3, stride=1, padding=1)
        
        # Decoder: 감정 반영 Mel Spectrum 생성
        self.decoder = nn.Conv1d(512, output_dim, kernel_size=3, stride=1, padding=1)
        
        # 활성화 함수
        self.relu = nn.ReLU()
    
    def forward(self, x, emotion_label):
        """
        Forward pass
        
        Parameters:
        - x (Tensor): 입력 Mel Spectrum, shape = (batch_size, input_dim, seq_len)
        - emotion_label (Tensor): 감정 레이블, shape = (batch_size,)
        
        Returns:
        - Tensor: 출력 Mel Spectrum, shape = (batch_size, output_dim, seq_len)
        """
        # 감정 레이블을 임베딩 벡터로 변환
        emotion_embedding = self.emotion_embedding(emotion_label).unsqueeze(2)  # (batch_size, embedding_dim, 1)
        
        # Mel Spectrum에 감정 임베딩 추가
        x = torch.cat((x, emotion_embedding.expand(-1, -1, x.size(2))), dim=1)  # (batch_size, input_dim + embedding_dim, seq_len)
        
        # Encoder: 입력 Mel Spectrum 처리
        encoded = self.relu(self.encoder(x))
        
        # Decoder: 감정 반영 Mel Spectrum 생성
        decoded = self.decoder(encoded)
        
        return decoded
# train.py

import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from preprocess import MelSpectrogramDataset, collate_fn  # CSV 기반 데이터셋 사용
from emotionTTS import EmotionTTS  # 모델 임포트

# CSV 파일 경로 설정
CSV_PATH = "dataset/emotion_melpath_dataset.csv"
# CSV 구조: [Mel Spectrum .npy 파일 경로, 감정 레이블]

# 데이터셋 및 DataLoader 준비
if not os.path.exists(CSV_PATH):
    raise FileNotFoundError(f"CSV file '{CSV_PATH}' does not exist. Please check the path.")

dataset = MelSpectrogramDataset(csv_file=CSV_PATH)  # 데이터셋 클래스
train_loader = DataLoader(dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)  # DataLoader

# 모델 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
input_channels = 128  # Mel Spectrum의 채널 수
emotion_embedding_dim = 32
output_dim = 128  # 출력 Mel Spectrum 크기

model = EmotionTTS(emotion_embedding_dim=emotion_embedding_dim, input_dim=input_channels, output_dim=output_dim)
model.to(device)

# 손실 함수 및 최적화 함수
criterion = nn.MSELoss()  # Mel Spectrum의 차이를 최소화
optimizer = optim.Adam(model.parameters(), lr=0.0001)  # 학습률을 낮게 설정하여 안정적 학습

# 훈련 루프
epochs = 20
for epoch in range(epochs):
    model.train()
    total_train_loss = 0

    for mel_spectrograms, labels, masks in train_loader:  # masks 포함
        mel_spectrograms, labels, masks = mel_spectrograms.to(device), labels.to(device), masks.to(device)

        optimizer.zero_grad()

        # 모델에 입력
        outputs = model(mel_spectrograms, labels)  # 감정 레이블과 함께 모델에 전달

        # 출력 크기를 입력 크기에 맞게 조정
        mel_spectrograms_resized = F.interpolate(mel_spectrograms, size=outputs.size(2), mode='linear', align_corners=False)

        # 손실 계산 (마스크를 적용하여 패딩된 부분 무시)
        loss = criterion(outputs, mel_spectrograms_resized)
        masked_loss = (loss * masks).sum() / masks.sum()  # 마스크 적용
        total_train_loss += masked_loss.item()

        # 역전파 및 최적화
        masked_loss.backward()
        optimizer.step()

    # 에포크별 손실 출력
    print(f"Epoch [{epoch + 1}/{epochs}], Training Loss: {total_train_loss / len(train_loader)}")

# 모델 저장
MODEL_PATH = "models/emotion_tts_model.pth"
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)  # 경로 생성
torch.save(model.state_dict(), MODEL_PATH)
print(f"Model saved to {MODEL_PATH}")
# inference.py

import torch
import librosa
import librosa.display
import numpy as np
import matplotlib.pyplot as plt
from train import EmotionTTS
from preprocess import AudioDataset

def transform_audio(input_file, target_emotion, model_path='models/emotion_tts_model.pth'):
    # 모델 로드
    model = EmotionTTS()
    model.load_state_dict(torch.load(model_path))
    model.eval()

    # 오디오 파일 로드
    y, sr = librosa.load(input_file, sr=22050)
    mel = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
    mel_db = librosa.power_to_db(mel, ref=np.max)

    # 모델을 통한 Mel Spectrum 변형
    mel_tensor = torch.tensor(mel_db).unsqueeze(0).unsqueeze(0).float()
    with torch.no_grad():
        emotion_vector = model(mel_tensor)
        # 여기서 emotion_vector를 통해 변형된 Mel Spectrum 생성 (추가 논리 필요)

    # 변형된 Mel Spectrum을 오디오로 변환
    transformed_audio = librosa.feature.inverse.mel_to_audio(mel_db, sr=sr)
    librosa.output.write_wav('outputs/transformed.wav', transformed_audio, sr)
    print(f"Transformed audio saved at outputs/transformed.wav")

# 예시 실행
transform_audio(input_file='data/neutral/example.wav', target_emotion='happy')
# utils.py

import librosa

def load_audio(file_path, sr=22050):
    y, _ = librosa.load(file_path, sr=sr)
    return y

def save_audio(file_path, y, sr=22050):
    librosa.output.write_wav(file_path, y, sr)
# preprocess.py

import librosa
import numpy as np
import os
import pandas as pd
import torch

def generate_mel_spectrograms(csv_path, output_dir):
    os.makedirs(output_dir, exist_ok=True)  # 저장 디렉토리 생성

    # CSV 파일 읽기
    df = pd.read_csv(csv_path)

    for _, row in df.iterrows():
        wav_path = row['file_path']
        output_path = os.path.join(output_dir, os.path.basename(wav_path).replace(".wav", ".npy"))

        try:
            y, sr = librosa.load(wav_path, sr=22050)
            mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
            mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)

            np.save(output_path, mel_spec_db)
            print(f"Saved: {output_path}")
        except Exception as e:
            print(f"Error processing {wav_path}: {e}")

class MelSpectrogramDataset(torch.utils.data.Dataset):
    def __init__(self, csv_file):
        self.data = pd.read_csv(csv_file)

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

    def __getitem__(self, idx):
        mel_path = self.data.iloc[idx, 0]
        mel_spectrogram = np.load(mel_path)
        mel_spectrogram = torch.tensor(mel_spectrogram, dtype=torch.float32)

        label = int(self.data.iloc[idx, 1])
        label = torch.tensor(label, dtype=torch.long)

        mask = torch.ones(mel_spectrogram.shape[1], dtype=torch.float32)
        return mel_spectrogram, label, mask

def collate_fn(batch):
    mel_spectrograms, labels, masks = zip(*batch)

    max_len = max(mel.shape[1] for mel in mel_spectrograms)
    mel_spectrograms_padded = torch.zeros(len(batch), mel_spectrograms[0].shape[0], max_len)
    masks_padded = torch.zeros(len(batch), max_len)

    for i, mel in enumerate(mel_spectrograms):
        mel_spectrograms_padded[i, :, :mel.shape[1]] = mel
        masks_padded[i, :mel.shape[1]] = 1

    return mel_spectrograms_padded, torch.stack(labels), masks_padded

# 실행 코드 추가
if __name__ == "__main__":
    csv_path = "datasets/emotion_melpath_dataset.csv"
    output_dir = "datasets/melspecs/"
    generate_mel_spectrograms(csv_path, output_dir)
# process_emotion_datasets.py

import os
import pandas as pd

# 감정 레이블 매핑
emotion_mapping = {
    "neutral": 0,
    "happy": 1,
    "sad": 2,
    "angry": 3,
    "fear": 4,
    "disgust": 5
}

# 데이터셋 경로 설정
ravdess_path = "datasets/RAVDESS/"
tess_path = "datasets/TESS/"
cremad_path = "datasets/CREMA-D/"

# 결과 저장 리스트
result = []

# RAVDESS 처리
for root, _, files in os.walk(ravdess_path):
    for file in files:
        if file.endswith(".wav"):
            # 파일 이름에서 감정 번호 추출 (예: "03-01-01-01-01-01-01.wav")
            parts = file.split("-")
            if len(parts) > 2:
                emotion_code = parts[2]
                if emotion_code == "01":
                    result.append((os.path.join(root, file), emotion_mapping["neutral"]))
                elif emotion_code == "03":
                    result.append((os.path.join(root, file), emotion_mapping["happy"]))
                elif emotion_code == "04":
                    result.append((os.path.join(root, file), emotion_mapping["sad"]))
                elif emotion_code == "05":
                    result.append((os.path.join(root, file), emotion_mapping["angry"]))
                elif emotion_code == "06":
                    result.append((os.path.join(root, file), emotion_mapping["fear"]))
                elif emotion_code == "07":
                    result.append((os.path.join(root, file), emotion_mapping["disgust"]))

# TESS 처리
for root, _, files in os.walk(tess_path):
    for file in files:
        if file.endswith(".wav"):
            # 파일 경로에서 감정 추출 (예: "OAF_angry/OAF_back_angry.wav")
            emotion = root.split("/")[-1].split("_")[-1].lower()
            if emotion in emotion_mapping:
                result.append((os.path.join(root, file), emotion_mapping[emotion]))

# CREMA-D 처리
for root, _, files in os.walk(cremad_path):
    for file in files:
        if file.endswith(".wav"):
            # 파일 이름에서 감정 추출 (예: "1001_DFA_ANG_XX.wav")
            parts = file.split("_")
            if len(parts) > 2:
                emotion = parts[2].lower()
                if emotion == "neu":
                    result.append((os.path.join(root, file), emotion_mapping["neutral"]))
                elif emotion == "hap":
                    result.append((os.path.join(root, file), emotion_mapping["happy"]))
                elif emotion == "sad":
                    result.append((os.path.join(root, file), emotion_mapping["sad"]))
                elif emotion == "ang":
                    result.append((os.path.join(root, file), emotion_mapping["angry"]))
                elif emotion == "fea":
                    result.append((os.path.join(root, file), emotion_mapping["fear"]))
                elif emotion == "dis":
                    result.append((os.path.join(root, file), emotion_mapping["disgust"]))

# 결과를 DataFrame으로 변환
df = pd.DataFrame(result, columns=["file_path", "emotion_label"])

# CSV로 저장
output_csv_path = "datasets/emotion_melpath_dataset.csv"
df.to_csv(output_csv_path, index=False)
print(f"CSV 파일이 '{output_csv_path}'에 저장되었습니다.")
# update_csv.py

import pandas as pd
import os

# 원본 CSV 파일 경로
input_csv_path = "datasets/emotion_melpath_dataset.csv"

# 수정된 CSV 파일 저장 경로
output_csv_path = "datasets/emotion_melpath_dataset_updated.csv"

# Mel Spectrum 저장 디렉토리
mel_dir = "datasets/melspecs/"

# CSV 파일 읽기
df = pd.read_csv(input_csv_path)

# 파일 경로를 .npy로 변환
def update_to_npy_path(wav_path):
    # .wav 파일 경로를 .npy 파일 경로로 변환
    npy_file_name = os.path.basename(wav_path).replace(".wav", ".npy")
    return os.path.join(mel_dir, npy_file_name)

# DataFrame의 file_path 필드 업데이트
df['file_path'] = df['file_path'].apply(update_to_npy_path)

# 업데이트된 CSV 저장
df.to_csv(output_csv_path, index=False)
print(f"Updated CSV saved to: {output_csv_path}")

데이터셋 준비하기

RAVDESS Emotional speech audio 다운받기
CREMA-D 다운받기
Toronto emotional speech set (TESS) 다운받기

datasets/RAVDESS/
├── Actor_01/
│   ├── 03-01-01-01-01-01-01.wav
│   ├── ...
└── Actor_24/
    ├── 03-01-06-02-02-01-24.wav
    └── ...
datasets/TESS/
├── OAF_angry/
│   ├── OAF_back_angry.wav
│   ├── ...
└── YAF_sad/
    ├── YAF_beg_sad.wav
    └── ...
datasets/CREMA-D/
├── 1001_DFA_ANG_XX.wav
├── 1002_IEO_HAP_HI.wav
└── ...

다운받은 데이터 셋들을 datasets라는 폴더 아래에 위와 같이 위치시킨다. 

python src/process_emotion_datasets.py

process_emotion_datasets.py 파일을 실행시킨다. 

 

그 결과로 이렇게 emotion_melpath_dataset.csv 파일이 생성된 것을 확인할 수 있다. 이렇게 하나의 csv파일로 모아서 감정 라벨링을 하는 이유는 각 데이터 셋마다 파일명 중 어떤 부분이 감정을 나타내는지 다르기 때문이다. 

python src/preprocess.py

preprocess.py 파일을 실행시켜서 .wav 파일을 Mel Spectrum .npy 파일로 변환해준다.

 

그 결과 이렇게 melspecs라는 새로운 폴더가 만들어지고 그 안에 .npy 파일들이 생긴 것을 확인할 수 있다. 

python src/update_csv.py

update_csv.py 파일을 실행시켜 emotion_melpath_dataset.csv에서 .wav 파일 경로를 .npy 파일 경로로 업데이트하여 emotion_melpath_dataset_updated.csv라는 새로운 파일로 저장해준다. 

 

이렇게 기존의 .wav 파일 경로가 .npy 파일 경로로 수정되어 저장되었다.

모델 학습시키기

python src/train.py

모델 테스트하기

python src/test.py

 

Comments