有没有好奇歌星们清唱的声音怎么样?这一讲,我们将歌曲的人声和背景音乐分离出来,使用的网络是RNN。接下来一一讲解怎么实现。
下载数据集
搞机器学习,首先想到的是怎么获取训练的数据,网上有开放的数据集MIR-1k,下载地址如下:
http://mirlab.org/dataset/public/MIR-1K.rar
下载完数据,解压到dataset/下,结构如下图所示
数据集里有很多文件夹,其他的我们不管,我们只用到Wavfile和UndividedWavfile文件夹下的文件。简单介绍一下数据集,打开Wavfile文件夹
里面有1000个音频文件,随便播放一个,请忽略里面五音不全的唱功(这里还是对得向该数据集的制作者致敬,感谢你们的付出),注意听会发现,有一个声道的数据是纯背景音乐,另一个声道的数据是纯人声。有了这个特性,我们就可以很好的利用了,后面详解代码再说。UndividedWavfile文件夹下也是类似的音频文件,只是数量只有110个,正好我们可以使用Wavfile文件夹下的数据做训练集,用UndividedWavfile文件夹下的数据做测试集。
思路
有了数据集,接下来还的有思路,俗称套路。首先,我们现在做的是音频的项目, 做音频的项目,首先就得想到将时域转到频域,再做分析。
而神经网络的套路也基本是下面的几步:
- 创建占位符、变量
- 设置学习率和batch size等参数
- 构建神经网络
- 设置损失函数
- 设置优化器
- 创建会话
- 利用神经网络开始训练数据,一般都是mini-batch的方法
具体到我们的这个项目,我们应该做以下事情
- 导入需要训练的数据集文件路径,存到列表中即可
- 导入训练集数据,每一个训练集文件都是一个双声道的音频文件,其中,第一个声道存的是背景音乐,第二个声道存的是纯人声;我们需要三组数据,第一组是将双声道转成单声道的数据,即让背景音乐和人声混合在一起 第二组数据是纯背景音乐,第三组数据是纯人声数据
- 通过上一步获取的数据都是时域的,我们要通过短时傅里叶变换将声音数据转到频域
- 初始化网络模型
- 获取mini-batch数据,开始进行迭代训练
train主框架
有了套路,就开始撸代码,先来看看训练train部分的代码,代码在train.py文件里
#可以通过命令设置的参数:
#dataset_dir : 数据集路径
#model_dir : 模型保存的文件夹
#model_filename : 模型保存的文件名
#dataset_sr : 数据集音频文件的采样率
#learning_rate : 学习率
#batch_size : 小批量训练数据的长度
#sample_frames : 每次训练获取多少帧数据
#iterations : 训练迭代次数
#dropout_rate : dropout率
def parse_arguments(argv):parser = argparse.ArgumentParser()parser.add_argument('--dataset_train_dir', type=str, help='数据集训练数据路径', default='./dataset/MIR-1K/Wavfile')parser.add_argument('--dataset_validate_dir', type=str, help='数据集验证数据路径', default='./dataset/MIR-1K/UndividedWavfile')parser.add_argument('--model_dir', type=str, help='模型保存的文件夹', default='model')parser.add_argument('--model_filename', type=str, help='模型保存的文件名', default='svmrnn.ckpt')parser.add_argument('--dataset_sr', type=int, help='数据集音频文件的采样率', default=16000)parser.add_argument('--learning_rate', type=float, help='学习率', default=0.0001)parser.add_argument('--batch_size', type=int, help='小批量训练数据的长度', default=64)parser.add_argument('--sample_frames', type=int, help='每次训练获取多少帧数据', default=10)parser.add_argument('--iterations', type=int, help='训练迭代次数', default=30000)parser.add_argument('--dropout_rate', type=float, help='dropout率', default=0.95)return parser.parse_args(argv)if __name__ == '__main__':main(parse_arguments(sys.argv[1:]))
上面一些参数都有注释,比较简单就不再解释了,来看main函数做了什么
#训练模型,需要做以下事情
#1. 导入需要训练的数据集文件路径,存到列表中即可
#2. 导入训练集数据,每一个训练集文件都是一个双声道的音频文件,
# 其中,第一个声道存的是背景音乐,第二个声道存的是纯人声,
# 我们需要三组数据,第一组是将双声道转成单声道的数据,即让背景音乐和人声混合在一起
# 第二组数据是纯背景音乐,第三组数据是纯人声数据
#3. 通过上一步获取的数据都是时域的,我们要通过短时傅里叶变换将声音数据转到频域
#4. 初始化网络模型
#5. 获取mini-batch数据,开始进行迭代训练def main(args):#先看数据集数据是否存在if not os.path.exists(args.dataset_train_dir) or not os.path.exists(args.dataset_validate_dir):raise NameError('数据集路径"./dataset/MIR-1K/Wavfile"或"./dataset/MIR-1K/UndividedWavfile"不存在!')# 1. 导入需要训练的数据集文件路径,存到列表中即可train_file_list = load_file(args.dataset_train_dir)valid_file_list = load_file(args.dataset_validate_dir)
上面做的很简单,将我们要训练和验证的文件路径导入到相应的列表里就可以了。然后设置一些参数
# 数据集的采样率
mir1k_sr = args.dataset_sr
# 用于短时傅里叶变换,窗口大小
n_fft = 1024
# 步幅;帧移对应卷积中的stride;
hop_length = n_fft // 4# Model parameters
# 学习率
learning_rate = args.learning_rate# 用于创建rnn节点数
num_hidden_units = [1024, 1024, 1024, 1024, 1024]
# batch 长度
batch_size = args.batch_size
# 获取多少帧数据
sample_frames = args.sample_frames
# 训练迭代次数
iterations = args.iterations
# dropout
dropout_rate = args.dropout_rate# 模型保存路径
model_dir = args.model_dir
model_filename = args.model_filename
接着,我们就需要读取音频文件了,音频文件里保存的是时域的数据,读取文件后,使用快速傅里叶变换将它们转到频域中
#导入训练数据集的wav数据,
#wavs_mono_train存的是单声道,wavs_music_train 存的是背景音乐,wavs_voice_train 存的是纯人声
wavs_mono_train, wavs_music_train, wavs_voice_train = load_wavs(filenames = train_file_list, sr = mir1k_sr)
# 通过短时傅里叶变换将声音转到频域
stfts_mono_train, stfts_music_train, stfts_voice_train = wavs_to_specs(wavs_mono=wavs_mono_train, wavs_music=wavs_music_train, wavs_voice=wavs_voice_train, n_fft=n_fft,hop_length=hop_length)# 跟上面一样,只不过这里是测试集的数据
wavs_mono_valid, wavs_music_valid, wavs_voice_valid = load_wavs(filenames=valid_file_list, sr=mir1k_sr)
stfts_mono_valid, stfts_music_valid, stfts_voice_valid = wavs_to_specs(wavs_mono=wavs_mono_valid, wavs_music=wavs_music_valid, wavs_voice=wavs_voice_valid, n_fft=n_fft,hop_length=hop_length)
上面load_wavs函数的作用是分别获取左右声道的音频数据,以及获取双声道合并成单声道后的音频数据,实现代码如下,存在utils.py文件里
# 导入训练集数据,每一个训练集文件都是一个双声道的音频文件,
# 其中,第一个声道存的是背景音乐,第二个声道存的是纯人声,
# 我们需要三组数据,第一组是将双声道转成单声道的数据,即让背景音乐和人声混合在一起
# 第二组数据是纯背景音乐,第三组数据是纯人声数据
def load_wavs(filenames, sr):wavs_mono = list()wavs_music = list()wavs_voice = list()#读取wav文件,首先要求源文件是有双声道的音频文件,一个声道存的是背景音乐,另一个声道存的是纯人声#然后,将音频转成单声道,存入 wavs_mono#将背景音乐存入 wavs_music,#将纯人声存入 wavs_voicefor filename in filenames:wav, _ = librosa.load(filename, sr = sr, mono = False)assert (wav.ndim == 2) and (wav.shape[0] == 2), '要求WAV文件有两个声道!'wav_mono = librosa.to_mono(wav) * 2wav_music = wav[0, :]wav_voice = wav[1, :]wavs_mono.append(wav_mono)wavs_music.append(wav_music)wavs_voice.append(wav_voice)return wavs_mono, wavs_music, wavs_voice
wavs_to_specs函数则通过快速傅里叶变换将音频数据转到频域,使用librosa库的stft函数就可以了,实现代码如下
#通过短时傅里叶变换将声音转到频域
def wavs_to_specs(wavs_mono, wavs_music, wavs_voice, n_fft = 1024, hop_length = None):stfts_mono = list()stfts_music = list()stfts_voice = list()for wav_mono, wav_music, wav_voice in zip(wavs_mono, wavs_music, wavs_voice):stft_mono = librosa.stft(wav_mono, n_fft = n_fft, hop_length = hop_length)stft_music = librosa.stft(wav_music, n_fft = n_fft, hop_length = hop_length)stft_voice = librosa.stft(wav_voice, n_fft = n_fft, hop_length = hop_length)stfts_mono.append(stft_mono)stfts_music.append(stft_music)stfts_voice.append(stft_voice)return stfts_mono, stfts_music, stfts_voice
接着,就要初始化神经网络模型了,具体的模型我们后面再说,先看怎么初始化,代码如下
#初始化模型
model = SVMRNN(num_features = n_fft // 2 + 1, num_hidden_units = num_hidden_units)
就这么简单,然后,我们可能会在训练途中中断训练(比如断电啊),如果不边训练边保存模型,那么对于用CPU来训练的同学来说就非常痛苦了。所以,先加载模型,如果还没有模型,我们就初始化变量就OK了
# 加载模型,如果没有模型,则初始化所有变量
startepo = model.load(file_dir = model_dir)print('startepo:' + str(startepo))
然后,开始进入迭代训练的for循环中了,
#开始训练
for i in (range(iterations)):#从模型中断处开始训练if i < startepo:continue
在for循环中,获取下一个batch数据,这个数据是要喂给神经网络进行训练的,看过以前教程的对这一步应该比较熟悉了
# 获取下一batch数据
data_mono_batch, data_music_batch, data_voice_batch = get_next_batch(stfts_mono = stfts_mono_train, stfts_music = stfts_music_train, stfts_voice = stfts_voice_train,batch_size = batch_size, sample_frames = sample_frames)
那么这个batch怎么获取呢?我们是不是需要随机的获取一小段单声道的数据,和同个时间段对应的背景音乐的数据和人声数据就可以了?代码如下
#stfts_mono:单声道stft频域数据
#stfts_music:背景音乐stft频域数据
#stfts_music:人声stft频域数据
#batch_size:batch大小
#sample_frames:获取多少帧数据
def get_next_batch(stfts_mono, stfts_music, stfts_voice, batch_size = 64, sample_frames = 8):stft_mono_batch = list()stft_music_batch = list()stft_voice_batch = list()#随即选择batch_size个数据collection_size = len(stfts_mono)collection_idx = np.random.choice(collection_size, batch_size, replace = True)for idx in collection_idx:stft_mono = stfts_mono[idx]stft_music = stfts_music[idx]stft_voice = stfts_voice[idx]#有多少帧num_frames = stft_mono.shape[1]assert num_frames >= sample_frames#随机获取sample_frames帧数据start = np.random.randint(num_frames - sample_frames + 1)end = start + sample_framesstft_mono_batch.append(stft_mono[:,start:end])stft_music_batch.append(stft_music[:,start:end])stft_voice_batch.append(stft_voice[:,start:end])#将数据转成np.array,再对形状做一些变换# Shape: [batch_size, n_frequencies, n_frames]stft_mono_batch = np.array(stft_mono_batch)stft_music_batch = np.array(stft_music_batch)stft_voice_batch = np.array(stft_voice_batch)# Shape for RNN: [batch_size, n_frames, n_frequencies]data_mono_batch = stft_mono_batch.transpose((0, 2, 1))data_music_batch = stft_music_batch.transpose((0, 2, 1))data_voice_batch = stft_voice_batch.transpose((0, 2, 1))return data_mono_batch, data_music_batch, data_voice_batch
这个separate_magnitude_phase函数的实现也很简单,代码如下
#通过短时傅里叶变换后的结果是复数的,而我们训练时,
#只需要考虑频率部分就可以了,所以将频率和相位分离出来
def separate_magnitude_phase(data):return np.abs(data), np.angle(data)
接着,就将数据喂给神经网络来进行训练了
#送入神经网络,开始训练
train_loss = model.train(x_mixed_src = x_mixed_src, y_music_src = y_music_src, y_voice_src = y_voice_src,learning_rate = learning_rate, dropout_rate = dropout_rate)
然后就是一些打印损失和测试模型的代码了
if i % 10 == 0:print('Step: %d Train Loss: %f' %(i, train_loss))if i % 200 == 0:#这里是测试模型准确率的print('==============================================')data_mono_batch, data_music_batch, data_voice_batch = get_next_batch(stfts_mono = stfts_mono_valid, stfts_music = stfts_music_valid,stfts_voice = stfts_voice_valid, batch_size = batch_size, sample_frames = sample_frames)x_mixed_src, _ = separate_magnitude_phase(data = data_mono_batch)y_music_src, _ = separate_magnitude_phase(data = data_music_batch)y_voice_src, _ = separate_magnitude_phase(data = data_voice_batch)y_music_src_pred, y_voice_src_pred, validate_loss = model.validate(x_mixed_src = x_mixed_src,y_music_src = y_music_src, y_voice_src = y_voice_src, dropout_rate = dropout_rate)print('Step: %d Validation Loss: %f' %(i, validate_loss))print('==============================================')if i % 200 == 0:model.save(directory = model_dir, filename = model_filename, global_step=i)
train.py的大致框架就如上所述了,我们接着来看神经网络模型怎么建的
神经网络SVMRNN类
我们将神经网络SVMRNN类写到model.py文件中,先来看初始化函数__init__
# num_features:音频特征数
# num_hidden_units:rnn 神经元数
# tensorboard_dir: tensorboard保存的路径
def __init__(self, num_features, num_hidden_units = [256, 256, 256]):# 保存传入的参数self.num_features = num_featuresself.num_rnn_layer = len(num_hidden_units)self.num_hidden_units = num_hidden_units# 设置变量# 训练了多少步self.g_step = tf.Variable(0, dtype=tf.int32, name='g_step')# 设置占位符# 学习率self.learning_rate = tf.placeholder(tf.float32, shape=[], name='learning_rate')#混合了背景音乐和人声的数据self.x_mixed_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='x_mixed_src')#背景音乐数据self.y_music_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='y_music_src')#人声数据self.y_voice_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='y_voice_src')#keep dropout,用于RNN网络的droupoutself.dropout_rate = tf.placeholder(tf.float32)#初始化神经网络self.y_pred_music_src, self.y_pred_voice_src = self.network_init()# 设置损失函数self.loss = self.loss_init()# 设置优化器self.optimizer = self.optimizer_init()#创建会话self.sess = tf.Session()#需要保存模型,所以获取saverself.saver = tf.train.Saver(max_to_keep=1)
设置变量、占位符,初始化神经网络,设置损失函数,设置优化器,创建会话,保存模型,是不是都是很熟悉的那一套啊?
顺着上面的代码,我们来看看初始化神经网络的函数network_init的实现
#构建神经网络
def network_init(self):rnn_layer = []#根据num_hidden_units的长度来决定创建几层RNN,每个RNN长度为sizefor size in self.num_hidden_units:#使用GRU,同时,加上dropoutlayer_cell = tf.nn.rnn_cell.GRUCell(size)layer_cell = tf.contrib.rnn.DropoutWrapper(layer_cell, input_keep_prob=self.dropout_rate)rnn_layer.append(layer_cell)#创建多层RNNmulti_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layer)outputs, state = tf.nn.dynamic_rnn(cell = multi_rnn_cell, inputs = self.x_mixed_src, dtype = tf.float32)#全连接层y_dense_music_src = tf.layers.dense(inputs = outputs,units = self.num_features,activation = tf.nn.relu,name = 'y_dense_music_src')y_dense_voice_src = tf.layers.dense(inputs = outputs,units = self.num_features,activation = tf.nn.relu,name = 'y_dense_voice_src')y_music_src = y_dense_music_src / (y_dense_music_src + y_dense_voice_src + np.finfo(float).eps) * self.x_mixed_srcy_voice_src = y_dense_voice_src / (y_dense_music_src + y_dense_voice_src + np.finfo(float).eps) * self.x_mixed_srcreturn y_music_src, y_voice_src
上面就是我们神经网络结构的核心了,N层RNN网络,再加一个全连接层,接着来看损失函数
#损失函数
def loss_init(self):with tf.variable_scope('loss') as scope:#求方差loss = tf.reduce_mean(tf.square(self.y_music_src - self.y_pred_music_src)+ tf.square(self.y_voice_src - self.y_pred_voice_src), name='loss')return loss
然后就是优化器
#优化器
def optimizer_init(self):ottimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(self.loss)return ottimizer
保存和加载模型的函数
#保存模型
def save(self, directory, filename, global_step):#如果目录不存在,则创建if not os.path.exists(directory):os.makedirs(directory)self.saver.save(self.sess, os.path.join(directory, filename), global_step=global_step)return os.path.join(directory, filename)# 加载模型,如果没有模型,则初始化所有变量
def load(self, file_dir):# 初始化变量self.sess.run(tf.global_variables_initializer())# 没有模型的话,就重新初始化kpt = tf.train.latest_checkpoint(file_dir)print("kpt:", kpt)startepo = 0if kpt != None:self.saver.restore(self.sess, kpt)ind = kpt.find("-")startepo = int(kpt[ind + 1:])return startepo
然后就是训练、验证、测试模型的函数
#保存模型
def save(self, directory, filename, global_step):#如果目录不存在,则创建if not os.path.exists(directory):os.makedirs(directory)self.saver.save(self.sess, os.path.join(directory, filename), global_step=global_step)return os.path.join(directory, filename)# 加载模型,如果没有模型,则初始化所有变量
def load(self, file_dir):# 初始化变量self.sess.run(tf.global_variables_initializer())# 没有模型的话,就重新初始化kpt = tf.train.latest_checkpoint(file_dir)print("kpt:", kpt)startepo = 0if kpt != None:self.saver.restore(self.sess, kpt)ind = kpt.find("-")startepo = int(kpt[ind + 1:])return startepo
代码写完了,在终端执行python train.py就开始训练了
可以看到,损失在慢慢下降,说明我们的模型起到作用了,运行到最后损失大概再0.5-0.6之间
测试代码
上面只是在训练啊,我们要把它用起来啊,所以还要再写个测试代码test.py,跟train的代码有点类似,首先也是一些参数的设置
def parse_arguments(argv):parser = argparse.ArgumentParser()parser.add_argument('--input_dir', type=str, help='待测试的音频文件的文件夹,存放MP3文件', default='./songs/input')parser.add_argument('--output_dir', type=str, help='声乐分离后的视频文件目录,为WAV格式', default='./songs/output')parser.add_argument('--model_dir', type=str, help='模型保存的文件夹', default='./model')parser.add_argument('--model_filename', type=str, help='模型保存的文件名', default='svmrnn.ckpt')parser.add_argument('--dataset_sr', type=int, help='数据集音频文件的采样率', default=16000)parser.add_argument('--dropout_rate', type=float, help='dropout率', default=0.95)return parser.parse_args(argv)if __name__ == '__main__':main(parse_arguments(sys.argv[1:]))
如果想调整参数,在运行命令行的时候传入参数就可以了,接着来看main函数
def main(args):input_dir = args.input_diroutput_dir = args.output_dirdataset_sr = args.dataset_srmodel_dir = args.model_dirdropout_rate = args.dropout_rate#如果输入目录不存在,返回错误if not os.path.exists(input_dir):raise NameError('音频输入文件夹"./songs/input"不存在!')#输出文件夹不存在,则创建if not os.path.exists(output_dir):os.mkdir(output_dir)#找到要分离背景音乐和人声的音频文件song_filenames = list()for file in os.listdir(input_dir):if file.endswith('.mp3'):song_filenames.append(os.path.join(input_dir, file))
我们将待测试的音频文件放到songs/input文件夹下,最后得到的结果保存到songs/output文件夹下,所以先检查输入文件夹是否存在先,如果存在,将里面的MP3文件放到列表中,接着就是读取这些音频文件了
#加载输入音频文件
wavs_mono = list()
for filename in song_filenames:wav_mono, _ = librosa.load(filename, sr=dataset_sr, mono=True)wavs_mono.append(wav_mono)
然后,设置一些参数,接着把音频文件的数据从时域转到频域,
# 用于短时傅里叶变换,窗口大小
n_fft = 1024
# 步幅;帧移对应卷积中的stride;
hop_length = n_fft // 4
# 用于创建rnn节点数
num_hidden_units = [1024, 1024, 1024, 1024, 1024]#将其转到频域
stfts_mono = list()
for wav_mono in wavs_mono:stft_mono = librosa.stft(wav_mono, n_fft = n_fft, hop_length = hop_length)stfts_mono.append(stft_mono.transpose())
然后,初始化神经网络,并导入我们上面训练好的模型
#初始化神经网络
model = SVMRNN(num_features = n_fft // 2 + 1, num_hidden_units = num_hidden_units)
#导入模型
model.load(file_dir = model_dir)
接着就对文件一一处理了
for wav_filename, wav_mono, stft_mono in zip(song_filenames, wavs_mono, stfts_mono):wav_filename_base = os.path.basename(wav_filename)#单声道音频文件wav_mono_filename = wav_filename_base.split('.')[0] + '_mono.wav'#分离后的背景音乐音频文件wav_music_filename = wav_filename_base.split('.')[0] + '_music.wav'#分离后的人声音频文件wav_voice_filename = wav_filename_base.split('.')[0] + '_voice.wav'#要保存的文件的相对路径wav_mono_filepath = os.path.join(output_dir, wav_mono_filename)wav_music_hat_filepath = os.path.join(output_dir, wav_music_filename)wav_voice_hat_filepath = os.path.join(output_dir, wav_voice_filename)print('Processing %s ...' % wav_filename_base)stft_mono_magnitude, stft_mono_phase = separate_magnitude_phase(data = stft_mono)stft_mono_magnitude = np.array([stft_mono_magnitude])y_music_pred, y_voice_pred = model.test(x_mixed_src = stft_mono_magnitude, dropout_rate = dropout_rate)# 根据振幅和相位,转为复数,用于下面的逆短时傅里叶变换y_music_stft_hat = combine_magnitude_phase(magnitudes = y_music_pred[0], phases = stft_mono_phase)y_voice_stft_hat = combine_magnitude_phase(magnitudes = y_voice_pred[0], phases = stft_mono_phase)y_music_stft_hat = y_music_stft_hat.transpose()y_voice_stft_hat = y_voice_stft_hat.transpose()#逆短时傅里叶变换,将数据从频域转到时域y_music_hat = librosa.istft(y_music_stft_hat, hop_length = hop_length)y_voice_hat = librosa.istft(y_voice_stft_hat, hop_length = hop_length)#保存数据librosa.output.write_wav(wav_mono_filepath, wav_mono, dataset_sr)librosa.output.write_wav(wav_music_hat_filepath, y_music_hat, dataset_sr)librosa.output.write_wav(wav_voice_hat_filepath, y_voice_hat, dataset_sr)
代码写好后,将任意一个MP3文件放到input文件夹下,运行python test.py就开始工作了,得到结果如下
可以听到,提取出来的背景音乐还是有点人声的,不过相对原文件人声已经小很多了,而纯人声中,仔细听也还能听到很小很小的乐器的声音,毕竟我们的损失没有降到接近0,所以并不是太完美,但是效果还可以了
因为时间限制,只训练到了19200步,不过损失已经没有明显的下降了,源码链接如下:
点击下载
0zvm