[歌词生成] 基于LSTM语言模型和seq2seq序列模型:数据爬取、模型思想、网络搭建、歌词生成

881 阅读15分钟
原文链接: blog.csdn.net

写在前面

非常好奇强大的生成技术,写了这个小项目。模型优化无止境,这只是初步的模型,有时间我也会不断优化。 本文主要对中文歌词进行自动生成。主要使用了基于概率语言模型的方法和基于seq2seq的方法进行生成。

本文所有代码地址:Github

一、数据爬取

1、数据内容分析

本次爬取的是网易云音乐网页版的数据,这里以罗大佑的主页举例,其他歌手类似。

最开始想要通过歌手主页的歌曲列表爬取,但是发现此页面(music.163.com/#/artist?id… )只包含50首热门歌曲,这个歌曲数量有点少,所以这里通过另一种途径进行了爬取:

(1)先爬取歌手的专辑信息(music.163.com/#/artist/al… ),并获取专辑id
(2)通过专辑id获取专辑详情页面,再爬取每张专辑中的歌曲信息。

2、代码地址

Github代码地址:爬取代码

3、爬取代码分析

(1)首先,我们需要爬取歌手页面的专辑信息,前端展示的网址为:music.163.com/#/artist/al… ,但是这里要说明,爬取的网址并非此网址,而是通过浏览器Network分析出的网址:music.163.com/artist/albu… ,下面的图片可以理解的更清晰:

(2)通过上一步得到的专辑的id,构建专辑详情地址(这里url需要注意的问题和上面的问题一样)进行爬取。 (3)通过上一步得到的歌曲的id进行歌词的爬取。

代码详见github

二、基于概率语言模型的模型

1、概率语言模型思想

我们是基于语言模型的思想,即通过前n n n 个词预测下一个词,公式表示为:
^ et =ar gmax et P( et ∣et −1 t−n−1 ) e t ˆ =a rg max e t P (e t ∣e t −1 t −n−1 ) e t ​ ^ ​ = a rgm ax e t ​ ​ P( e t ​ ∣e t− n−1 t− 1 ​ )
其中^ e t e t ​ ^ ​ 表示基于前n n 个词预测出来的t t 时刻词的概率,在本次练习中,n =10 n = 10 (参数可调)。
LSTM可以考虑序列之间的联系,所以我们选择LSTM作为本次练习的网络。

2、网络搭建

(1)概览

这里首先讲下网络构建的流程,下面再就每一个流程进行详细讲解。训练网络的流程如下:

Step1: 读取训练文件,并分词。
Step2: 将分词结果和下标对应,得到char-to-index、index-to-char(或者phrase-to-index、word-to-index)用于后续索引
Step3: 生成训练数据和验证数据用于后续训练和验证。
Step4: 模型构建、训练和保存
Step5: 调参
Step6: 数据accuracy分析
Step7: 歌词生成,加载已训练模型生成数据。

下面从这六个步骤说明歌词生成的过程。说明:这里将歌词生成的部分和歌词训练的部分分开了,所以需要对训练数据得到的模型进行存储。
(这里只对主要代码进行copy,其他代码详见github,都有注释)

(2)分词和索引

①分词主要就是将句子分开。在中文里,可以将其分成“字”和“短语”,比如“我爱中国”,按照“字”进行分割的结果是:[“我”,“爱”,“中”, “国”];按照“短语”进行分割的结果是:[“我”,“爱”,“中国”]。可以使用jieba分词。

②jieba分词的举例如下:

texts = [“你爱中国”, “你是小宝宝”]
cut_res = [[“你”, “爱”, “中国”],[“你”, “是”, “小宝宝”]]
freq_words = {“你”:2, “爱”: 1, “中国”: 1, “是”: 1, “小宝宝”: 1}
word_to_index is = {‘你’: 1, ‘爱’: 2, ‘中国’: 3, ‘是’: 4, ‘小宝宝’: 5}(频率大的在前面)
index_to_word = {1: ‘你’, 2: ‘爱’, 3: ‘中国’, 4: ‘是’, 5: ‘小宝宝’}(频率大的在前面)
sequences is = [[1, 2, 3], [1, 4, 5]]

索引的目的便是在网络模型中方便输入序列,以便进行数据表示。

③本文主要基于“字”的分割,实验结果证明基于字的分割比基于“词语”的分割更有效。

(3)训练集和测试集生成

由上述模型的思想可知,我们需要生成训练数据,其输入数据X X 是前10个词,输出标签数据是第11个词。为了方便处理,我们将所有的句子连接成一个整体,通过上述思想生成数据。例如:

连接后的句子:“柔情万种本色难改胭脂内的你难解的胸怀”
生成的数据如下:
①x1: “柔情万种本色难改胭脂”, y1: “内”
②x2: “情万种本色难改胭脂内”, y2: “的”
③x3: “万种本色难改胭脂内的”, y3: “你”
④x4: “种本色难改胭脂内的你”, y4: “难”
⑤x5: “本色难改胭脂内的你难”, y5: “解”
⑥x6: “色难改胭脂内的你难解”, y6: “的”
⑦x7: “难改胭脂内的你难解的”, y7: “胸”
⑧x8: “改胭脂内的你难解的胸”, y8: “怀”

构建过程如下:

def generateTrainData(cut_word_list, word_to_index):
    """
    构造训练集,并处理成keras可以接受的输入
    :param cut_word_list: 分词之后的list
    :param seq_length: 指定的序列长度,即输入X的长度
    :return:
    """
    X_data = []
    y_data = []
    data_index = []
    n_all_words = len(cut_word_list)
    for i in range(0, n_all_words - SEQ_LENGTH - 1):
        seq_x_y = cut_word_list[i: i+SEQ_LENGTH + 1]   # 最后一个词是y
        index_x_y = [word_to_index[elem] for elem in seq_x_y]    # word to index
        data_index.append(index_x_y)
    np.random.shuffle(data_index)
    for i in range(0, len(data_index)):
        X_data.append(data_index[i][:SEQ_LENGTH])
        y_data.append(data_index[i][SEQ_LENGTH])

    # 将X_data变换成需要输入的tensor模式,将y_data变成one-hot模式
    X = np.reshape(X_data, (len(X_data), SEQ_LENGTH))
    y = np_utils.to_categorical(y_data)

    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=33)

    return X_train, X_val, y_train, y_val

(4)模型构建和训练

基于语言的概率模型本质就是分类模型,基于前面10个字对下一个字进行预测,找到概率最大的字便是预测结果。其基本模型如下:

从图中可以看出,本模型使用了2层LSTM,一层全连接层。

代码如下:

input_shape = (SEQ_LENGTH,)
x_train_in = Input(input_shape, dtype='int32', name="x_train")

# word_index存储的是所有vocabulary的映射关系
nb_words = min(MAX_NB_WORDS, len(word_to_index))
print("nb_words is::", nb_words)
embedding_layer = Embedding(nb_words, EMBEDDING_DIM, input_length=SEQ_LENGTH)(x_train_in)
print("embedding layer is::", embedding_layer)
print("build model.....")

# return_sequences=True表示返回的是序列,否则下面的LSTM无法使用,但是如果下一层不是LSTM,则可以不写
lstm_1 = LSTM(EMBEDDING_DIM, name="LSTM_1", return_sequences=True, kernel_regularizer=regularizers.l2(0.01))(embedding_layer)
lstm_2 = LSTM(EMBEDDING_DIM_2, name="LSTM_2", kernel_regularizer=regularizers.l2(0.01))(lstm_1)
dense = Dense(nb_words, activation="softmax", name="Dense_1", kernel_regularizer=regularizers.l2(0.01))(lstm_2)

model = Model(inputs=x_train_in, outputs=dense)

print(model.summary())

adam = Adam(lr=0.001, beta_1=0.9, beta_2=0.99, epsilon=1e-08)
model.compile(loss='categorical_crossentropy',
			  optimizer=adam,
			  metrics=['accuracy'])
print("Train....")

history_record = model.fit(X_train,
						  y_train,
						  batch_size=BATCH_SIZE,
						  epochs=EPOCHS,
						  validation_data=(X_val, y_val))
model.save('./model_epoch50_2lstm_1dense_seq50_phrase_based_best.h5')

(5)调参

针对以上模型主要进行了以下调参:

①lstm层数调整
A. 2层lstm换成3层lstm,结果:2层的好于3层的lstm
B. 2层lstm换成1层lstm,结果:2层lstm情况下best的结果val-loss≈3.31,val-acc≈0.55;1层lstm情况下best的结果val-loss≈4.34,val-acc≈0.43。
②优化算法调整
Adam优化算法换成RMSprop算法,结果:Adam算法效果更好。
best的情况下:Adam算法val-loss≈3.31,RMSprop算法val-loss≈4.56;Adam算法val-acc≈0.55,RMSprop算法val-acc≈0.42。
Adam和Rmsprop的配置如下:

Adam(lr=0.001, beta_1=0.9, beta_2=0.99, epsilon=1e-08)
RMSprop(lr=0.001, rho=0.9)

③添加正则项
具体:将LSTM和Dense都加上kernel_regularizer=regularizers.l2(0.01)或者都加上kernel_regularizer=regularizers.l1(0.01),结果:算法效果都不好,两个的结果都是val-loss≈6.19,val-acc≈0.13(从第一个epoch开始val-loss就一直不变)。
④添加dropout
代码如下,给中间层dropout:

    lstm_1 = LSTM(EMBEDDING_DIM, name="LSTM_1", return_sequences=True)(embedding_layer)
    dropout_1 = Dropout(0.2)(lstm_1)
    lstm_2 = LSTM(EMBEDDING_DIM_2, name="LSTM_2")(dropout_1)
    dropout_2 = Dropout(0.2)(lstm_2)
    dense = Dense(nb_words, activation="softmax", name="Dense_1")(dropout_2)

结果:在这种情况下,和没加dropout的性能基本类似,相比较于2lstm不加dropout的情况,val-loss减少0.1,但是相应的val-acc减少0.2,而且dropout会使得训练速度降低,所以这里选择不使用。
⑤调节隐层大小
将隐藏层大小调整为:EMBEDDING_DIM = 256, EMBEDDING_DIM_2 = 512
结果:EMBEDDING_DIM = 512, EMBEDDING_DIM_2 = 1024效果比较好,在此情况下,val-loss≈3.31,val-acc≈0.55。而在256-512的情况下val-loss≈3.44,val-acc≈0.53。
将隐藏层大小调整为:EMBEDDING_DIM = 800, EMBEDDING_DIM_2 = 1000
结果:EMBEDDING_DIM = 800, EMBEDDING_DIM_2 = 1000效果比较好,val-loss≈3.11,val-acc≈0.56。
⑥歌词分割标准
使用基于词语的分割(jieba分词)代替基于字的分割。结果:基于字的分割效果更好。

综上,2层lstm较好,优化算法使用Adam,基于字分割,适当调大隐藏层维度可能会使效果变得更好。

其余可调节点:
①学习率learning_rate
②loss function中加入正则项
③lstm换成双向lstm
④seq_length的调节
⑤调节MAX_NB_WORDS的值
⑥sample方法,使用Beam Search等
⑦加入attention机制
⑧将onehot表示方法替换成embedding

(6)结果分析

下面以2层lstm,1层dense,Adam优化算法和EMBEDDING_DIM = 800, EMBEDDING_DIM_2 = 1000为例分析算法结果。
训练集上的accuracy和loss以及验证集上的val-accuracy和val-loss如下所示:

从上图可以看出,当loss增加的时候,accuracy降低,在训练的时候,大概10个epoch左右就已经达到最好的状态。

(7)生成歌词

生成歌词的时候需要上一步训练得到的模型,除此之外,还需要word2index和index2word的索引,所以需要将其也存储下来(为了方便重用,代码中直接将其存储到了文件中)。 生成歌词的思路步骤如下:

①加载模型、word2index和index2word
②将给定的开头编码成输入模式(和model中的输入相同),若字数不够10,则使用“<PAD>”填充(下标为0)。
③使用①加载到的模型预测下一个输出,不断迭代,直到达到我们设定的字数(本实验使用的是200)。

代码如下:

x_pred = np.zeros((1, SEQ_LENGTH))    # 使用PAD填充

min_index = max(0, len(sentence) - SEQ_LENGTH)
for idx in range(min_index, len(sentence)):
	x_pred[0, SEQ_LENGTH - len(sentence) + idx] = word2index.get(sentence[idx], 1)   # '<UNK>' is 1

preds = model.predict(x_pred, verbose=0)[0]
next_index = sample(preds, diversity)
next_word = index2word[next_index]
sentence = sentence + next_word   # 每次都往后取一个

(8)效果展示

下面是使用上述模型训练的参数生成歌词的例子,生成的歌词长度是200。 ①以“喧嚣的人群”开头:

喧嚣的人群恩法
到底我多事全仅诉
我真的不想吗
我不想就想你走不放
孤独觉得心乱
空虚暖通
不要却把回忆抹开
其实还有什么是真理
茫风叫相恭潮自我
如果说分手
等和爱和谁幸福的脸
寒流来深月
开对在多年轻欢
我用还在一起
不分的问题
没有看起来
可惜总是很难受知道
我在你的时候
你要离开这醉不铃
这是我写的歌
曾经如何真
如同的波延
念了大师们在失着梦里里
让每个爱你
难以用爱
有分开是年轻
将你

②以“痴情”开头

痴情
天台月光希
他们它真的一点人
人世漂代傻
他背纪那一刀的晚表
你留着本绪路的车和能发现
有话就该更习惯
真的够小关还
不代一样的激悟
你家起温暖的大岛
每一众想要在一颗
想点一非往前走的都走
月下窗祖滴咸嘣该跟你无老
哦
他还是注想
我会拉失下白后
想总是为了你
要不是忍到何不民
你明了的温摸
炫散
有就进
都爱了
不代表骤什么
生命
名瑟的是谁
美女诉那对情都好
英雄流浪的小啦
隐约远的孤

③以“自由”开头

自由走
我为你方开
做了几个甜微的火尘世那几年
苦笑都不自客
人生已远离这
我看不到
听说在界缘里捉反坠
西弱过的白坤
我们肢阔都
坚持多情之前
一张居怕非醉
恋情
我牵着你的手
我像狗草月下
世幕轻嚓现路唱了被左都市
拉影后魂要用意义塌伪
靠告诉我们睡过的也没有发色这
有的航点
不对这失情的雨气
坚强自在也事我
被放手的公园而片
落水跟肉家
烟雾里往模知
莹天上电影
海漠就该灯地
淡淡事起民视残

三、基于序列模型的模型

1、序列模型seq2seq思想

seq2seq模型是编码-解码模型的一种,主要思想是通过一个Encoder将输入编码成一个语义向量c c ,接着将c c 作为Decoder的输入,从来产生输出结果。
其具体原理请参见博客:seq2seq model和Attention-based seq2seq Model(动图展示)

2、网络搭建

(1)概览

基于序列模型的歌词生成和基于概率语言模型的歌词生成步骤类似,只是模型改变,相应的训练集数据应该改变。分词和索引和上面基于概率语言模型的思想一样,训练集的生成与之前有些不同,下面就从训练集合测试集的生成部分开始说明。

(2)训练集和测试集生成

在seq2seq模型中,输入是一句话,输出也是一句话,所以其样本构造如下:

歌词句子:
-柔情万种
-本色难改
-胭脂内的你难解的胸怀
-洋场十里
生成的数据如下:
①x1: “柔情万种”, y1: “本色难改”
②x2: “本色难改”, y2: “胭脂内的你难解的胸怀”
③x3: “胭脂内的你难解的胸怀”, y3: “洋场十里”

(3)模型构建和训练

基于序列的模型基于前一句预测下一句。其模型结构如下:

decoder和encoder部分都是基于lstm,在decoder后面有一层dense层,之后又接了一个softmax层,用于预测。

代码实现如下:

encoder_inputs = Input(shape=(None, len(word_to_index_input)), dtype='float32', name="encoder_inputs")
encoder_inputs_masking = Masking(mask_value=0)(encoder_inputs)
print("build model.....")

# return_sequences=True表示返回的是序列,否则下面的LSTM无法使用,但是如果下一层不是LSTM,则可以不写
encoder = LSTM(LATENT_DIM, name="encoder_outputs", return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs_masking)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]

decoder_inputs = Input(shape=(None, len(word_to_index_target)), dtype='float32', name="decoder_inputs")
decoder_inputs_masking = Masking(mask_value=0)(decoder_inputs)
decoder_LSTM = LSTM(LATENT_DIM, name="decoder_LSTM", return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_LSTM(decoder_inputs_masking, initial_state=encoder_states)
decoder_dense = Dense(len(word_to_index_target), activation='softmax', name="Dense_1")
decoder_outputs = decoder_dense(decoder_outputs)

model = Model(inputs=[encoder_inputs, decoder_inputs], outputs=decoder_outputs)
print(model.summary())

adam = Adam(lr=0.001, beta_1=0.9, beta_2=0.99, epsilon=1e-08)
model.compile(loss='categorical_crossentropy',
			  optimizer=adam,
			  metrics=['accuracy'])

print("Train....")

history_record = model.fit([encoder_input_data_train, decoder_input_data_train],
						   decoder_target_data_train,
						   batch_size=BATCH_SIZE,
						   epochs=EPOCHS,
						   validation_data=(
						   [encoder_input_data_val, decoder_input_data_val], decoder_target_data_val))
model.save('./model_seq2seq_300epoch_final.h5')_record

(4)调参

seq2seq的调参过程和方法和上面基于语言模型的方法类似。除此之外,也可以尝试如下小变动:
①加入attention机制
②将前一个词的输入结果+encoder的输入结果当做输入进行下一个词的预测等。
③增大数据集(因为测试中发现大的数据集生成效果更好)
④将其他相似任务模型的参数迁移到此任务,或者将数据分批,后一批加载前一批的参数数据进行参数迁移。

测试发现,在基于语言的模型中,数据量不要求非常多,但是seq2seq模型相对来说,需要更大的数据量,训练过程所需要的时间也更多,这里就不再进行调参结果的分析。

(5)结果分析

下面以seq2seq模型,隐层向量维度为1000,限制最大句子长度为20,优化函数为Adam为例分析算法结果。 训练集上的accuracy和loss以及验证集上的val-accuracy和val-loss如下所示:

从上图可以看出,训练集的acc还在持续增加,loss持续降低;但是验证集上val-loss在先减小再减小(尽管val-acc一直在增加),这是典型的过拟合(分析详见:[交叉熵损失和accuracy关系] 验证集上val-loss先降低再增加,而val-accuracy一直在增加),但是我们存储的val-loss最小时的模型,在epoch=15,val-loss≈3.1,val-acc≈0.45。

(6)生成歌词

生成歌词部分使用上述训练的模型和参数进行模型生成,但是需要注意,我们需要将输入数据通过encoder模型进行编码,通过decoder对其进行预测,所以这里将encoder和decoder分开,然后基于这2个分开的子模型进行歌词的生词。 代码如下:

# Encode the input as state vectors.
states_value = encoder_model.predict(input_seq_rep)

# Generate empty target sequence of length 1.
target_seq = np.zeros((1, 1, len(word2index_target)))
# Populate the first character of target sequence with the start character.
target_seq[0, 0, word2index_target['\t']] = 1.

# Sampling loop for a batch of sequences
# (to simplify, here we assume a batch of size 1).

decoded_sentence_all = ''
max_decoder_seq_length = 20   # 生成的每个句子的最大长度
count = 0
while count < 10:   # 生成10个句子
	stop_condition = False    # 到达行结束符
	decoded_sentence = ""
	while not stop_condition:
		output_tokens, h, c = decoder_model.predict(
			[target_seq] + states_value)

		# Sample a token1, :])
		sampled_token_index = sample(output_tokens[0, -1, :], 1)
		sampled_char = index2word_target[sampled_token_index]    # 生成的新的字
		decoded_sentence += sampled_char

		if(sampled_char == '\n' or len(decoded_sentence) > max_decoder_seq_length):
			if sampled_char != '\n':
				print("长度为20,直接结束,不用等待生成换行符!")
				decoded_sentence += '\n'   # 若不自动换行,则手动添加一个结束符
			stop_condition = True

		if decoded_sentence != '\n':   # 防止出现没有内容的行
			# Update the target sequence (of length 1).
			target_seq = np.zeros((1, 1, len(word2index_target)))
			target_seq[0, 0, sampled_token_index] = 1.
			# Update states
			states_value = [h, c]
	if decoded_sentence != '\n':   # 防止出现没有内容的行
		count += 1
		decoded_sentence_all += decoded_sentence
return decoded_sentence_all

代码主要实现10句歌词的生成,对于每句歌词结束的条件是遇到换行符或者长度等于20。随着歌词的不断生成,下一个预测字的输入长度也在不断增加,代码中使用target_seq控制。

(7)效果展示

下面是使用上述模型训练的参数生成歌词的例子,生成10个句子。
①以“喧嚣的人群”开头:

喧嚣的人群
别让我受伤悲哀
可以
折拿
效
发谈
根皮
满
下根
道
根

②以“痴情”开头:

痴情
晚到
更的与片
千彼生活成
段
子有大海
泥遍
花
甘台仙仙
度
拼下泥

③以“自由”开头

自由
我决定
生鱼的浪旁
烂
泥术羿回光
射下讨
光亮亮
角角有角空
予回起角
度
光角度限

四、总结

从上面可以看出,基于概率语言模型的生成效果好于基于序列模型,可能有以下原因: ①基于序列的模型主要对有前后关联信息的上下文有利,而歌词和诗歌还有些不同,诗歌的前后关联更强一些,而歌词很多上下句是关联不大的,如下面的歌词(罗大佑《倒影》中的歌词):

会吗会吗
只因为软弱的自己
总不能说我不理
要嘛要嘛

②对于序列模型这里因为资源问题没有进行调参,在模型不断训练的过程中,很快出现了过拟合,如果采取一些方式,可能效果更好。 ③在seq2seq模型中,最开始使用一个歌手的数据,结果非常差,后来扩大了数据集,效果好很多,想让结果更好,可以考虑继续增大训练集。