520,用AI为她写首歌吧!

5月20号要到了,即“我爱你”节。随着时代的发展,诸如此类的谐音节日在商家和自媒体的宣传下越来越成为一个固定的节日,为恋爱中的男男女女过节的理由,也就是多了一个花钱的理由。作为新一代属于无产阶级的知识青年,节要过,但是没有钱,如何在贫穷的状态下保持仪式感呢?想想还是得花心思,并且要对胃口。

最近在学文本分析相关的内容,那么,是不是可以用深度学习的方法,以“我爱你”为开头,以她喜欢的歌手的风格写歌,生成歌词呢?

我感觉也许可以试试,结果感觉还可以,例如以520为开头,第一句就是“情起”,后面还有“怕爱到夜里”,“此情惹梦”这些容祖儿风格较为明显的句子,还是可以看出这首歌词想表达的意思的,改改还是能用的(逃)。

那么整个过程是什么样的呢?歌词生成的需要获得训练数据,架构模型,模型训练,最后是生成歌词。

步骤: 

1、爬取她喜爱的歌手的所有歌词。 

2、构建并训练LSTM模型。

3、特定歌手风格的歌词预测。

编程语言:Python

深度学习框架:Keras

一、知识基础

1、Selenium 

我们的歌词来源于网易云音乐,由于网易云音乐的歌词爬取较为困难,爬虫的方式以浏览器自动化进行。在开始编程之前,我们需要先了解一个能够自动控制浏览器的工具——Selenium。Selenium是一个用于Web应用程序测试的工具,直接运行在浏览器中,就像真正的用户在操作一样[1]。关于Selenium的安装和使用,可以参考我之前的文章Python+Selenium,让浏览器自动帮你下文献。

2、LSTM 

我们的歌词数据,各个字之间按照特定的顺序排列,字与字之间的是否临近符合一定的概率分布,比如“我”与“们”这两个字在一起的概率大于“我”与“门”在一起的概率,也就是说,字与字之间的排列存在着相关性。这种数据与数据之间存在相关性的、前一个数据能够影响后一个数据的出现的数量类型,称为序列数据,常见的包括视频、音频、文本等。

对于这样的数据,循环神经网络(RNN,Recurrent Neural Network)是一种比较好的解决方式。循环神经网络有重复的结构模块(例如tanh层),前一个数据点一个数据点在经过神经网络得到输出结果的同时,对后一个数据点有一个影响权重,影响后一个数据点结果的生成。

长短时记忆网络(LSTM,Long Short-Term Memory)是RNN的一种形式。不同于标准循环神经网络的结构,循环神经由三个门构成:遗忘门(Forget gate)、输入门(Input gate)和输出门(Output gate),三个门构成4个重复的结构,交互方式比较复杂。除了权重在整个过程中流动外,还有另一个状态变量:细胞,细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互,信息在上面流传保持不变会很容易[2]。LSTM的结构使得它擅长长时间记忆某个特性,可以解决长序列训练过程中的梯度消失和梯度爆炸问题,使其在语音识别、机器翻译等方面都具有广泛的应用。(图片来源于吴恩达deeplearning公开课的PPT)

3、n-gram语言模型 

n-gram是一种统计语言模型,用来根据前(t-1)个字来预测第t个字[3],第t个字出现的概率计算公式如下。

这个概率模型除了应用在文本生成外,还应用在拼音输入法上。

二、歌词数据爬取

网易云音乐的官网经过了改版,网上的许多关于网易云音乐的爬虫demo都宣告失效了,现学JavaScript解析、Ajax响应时间也不太够,想起了之前曾经用Selenium做过文献爬虫(Python+Selenium,让浏览器自动帮你下文献),似乎也能够派上用场。用Selenium搞爬虫(严格意义上应该是浏览器自动化),核心代码如下:

import lxml.etree as etree
from selenium import webdriver

driver= webdriver.Chrome() #浏览器初始化(打开浏览器)
driver.get('https://www.baidu.com/') #打开你的url,这里以百度为例
driver.switch_to.frame(driver.find_element_by_name("contentFrame")) #frame转化
html = driver.page_source #得到网页信息
info_html = etree.HTML(html) #使用lxml.etree解析网页

# 在得到info_html后,我们就可以像一般的爬虫一样,使用xpath,分析HTML结构,得到我们想要的信息了。

我们的爬虫步骤如下:

1、获取歌手所有专辑信息(专辑id);

2、遍历所有专辑,获得歌手所有歌曲的链接;

3、获取所有歌曲,获得歌词数据并存储。

比如我要爬周杰伦所有歌曲的歌词数据,在网易云音乐搜索“周杰伦”,得到歌手主页。分析URL,发现每个歌手都有一个id,例如周杰伦的id是6452。这样说的话,写好一个爬虫,之后只要得到这个歌手的id,就可以爬取他在网易云音乐上的所有歌词。

为了尽可能的得到所有的歌词数据,经过网页分析(点击F12进入浏览器的开发者模式),发现同样可以通过专辑的id(例如专辑《哎哟,不错哦》的id为3084335),获取专辑内的所有歌曲链接,链接位于class='txt'的下面的里。

分析清楚之后,我们进入歌词页,发现歌词存储在id='lyric-content的里面。

那么知道如何获取歌词后,我们可以写代码,先获取所有的专辑id,然后进入每个专辑,获取每一首歌的链接,之后写一个循环,循环访问每一首歌的URL,获取每一首歌的歌词数据。最终,获取网易云音乐上周杰伦歌曲的歌词数据为398条,并按照专辑归纳每一首歌的歌词数据。

当然,我女票不喜欢周杰伦,而是喜欢Jony J等5位歌手,那么写个循环,爬取这几位歌手的所有歌词。

三、模型构建与训练

1、数据预处理 

在构建模型之前,先进行数据预处理,这是花费时间很多的一个步骤,流程如下:

1、去除所有非中文字符;

2、去除所有非歌词字符;

3、将数据处理成能够输入到模型中的形式。

为了保证结果质量,去掉歌词内容的所有非中文字符,并将所有数字变成中文数字,函数如下。

defis_Chinese(word):
    for ch in word:
        if '\u4e00' <= ch <= '\u9fff':
            return True
    return False

defis_number(s):
    try:
        float(s)
        return True
    except ValueError:
        pass
    return False

defhandle_sentence(sentence):
    word_num = ['零','一','二','三','四','五','六','七','八','九']
    output = ''
    for word in sentence:
        if is_number(word):
            output += word_num[int(word)]
        elif is_Chinese(word):
            output += word
    return output

非歌词字符内容不是本身的歌词信息,需要去除。典型的非歌词的句子以下面这些词语开头,需要写函数把这些句子去掉。

del_words = ["原唱", "改编", "编曲", "编辑", "歌名", "歌手", "专辑", "木吉他", "贝斯", "发行日", "曲", "监制", "制作人", "中乐演奏",
                 "其余所有乐器演奏", "演奏", "和音", "联合制作", "制作", "录音", "混音", "录音室", "混音室", "录音师",
                 "混音师", "统筹", "制作统筹", "执行制作", "母带后期处理", "企划", "鼓","合声", "二胡", "乌克丽丽",
                 "过带", "Bass", "Scratch", "OP", "Guitar", "SP", "Bass", "SCRATCH", "Programmer", "弦乐", "小提琴",
                 "女声", "Cello solo", "Piano", "吉他", "钢琴", "os", "弦乐", "和声", "DJ", "Tibet", "Violin", "Viola",
                 "Cello", "和声", "母带","音乐", "打击乐", "Vocal", "次中音", "长号", "小号", "Music", "监制", "作词",
                 "词/曲", "箫", "筝", "作词", "作曲", "Program", "键盘", "制作",':',":",'暂时没有歌词 ','纯音乐,无歌词']

flag_contain = 0    # 此文件是否包含歌词信息,若为空,则不需要最后添加空行
with open(file_path + file + '/' + file_name, 'r', encoding='utf-8') as f:
    for line in f.readlines():
        flag_del = 0    # 此行是否需要去除
        for word in line:
            if word in del_words:
                flag_del = 1    # 若有“作词”等开头的句子,说明不是歌词,则直接将此行去掉
                break

        if flag_del == 1:
            continue

在完成上述步骤后,我们需要将数据处理成能够输入到模型中的形式。将数据输入到模型,需要将一句一句的歌词,变成一个又一个的数据点的序列,数据点可以是一个个字,也可以是一个个词。有实验结果证明,基于字的分割比基于词语的分割更有效[4]。

我们构造使用cut_words函数,来得到所有歌词的字列表,然后构造这些字的vocabulary,为每一个字编码。在本文中,使用句号“。”作为每句话的结尾符,构造 "PAD"和"UNK"作为空缺填补和终止字符的标识符。

def cut_words(file_name): #将歌词分割为字
    with open(file_name, 'r', encoding='utf8') as f:
        content = f.read().replace('\n', '。')   # 使用句号作为句子的结束符
    return list(content)


def map_words(cut_word_list): #建立字与index的关系字典
    vocabulary = sorted(list(set(cut_word_list)))
    word_to_index = dict((w, i+2) for i, w in enumerate(vocabulary))
    word_to_index["PAD"] = 0   # 填补
    word_to_index["UNK"] = 1   # unknown

    index_to_word = dict((index, word) for word, index in word_to_index.items())

    # 将2个文件存储,在生成歌词时使用
    word_to_index_json = json.dumps(word_to_index)
    index_to_word_json = json.dumps(index_to_word)
    with open('data/周杰伦/word_to_index_word.txt', 'w', encoding='utf8') as w:
        w.write(word_to_index_json)
    with open('data/周杰伦/index_to_word_word.txt', 'w', encoding='utf8') as w:
        w.write(index_to_word_json)
    return word_to_index, index_to_word

我们使用的是n-gram语言模型进行歌词生成,也就是基于前n个字生成第n+1个字。我们要将一句话,变成输入x和输出y的形式。例如,对于歌词:“小朋友你是否有很多问号”,假设n=5,这句话会生成的数据如下:

x1 = ['小','朋','友','你','是'], y1 = '否'

x2 = ['朋','友','你','是','否'], y2 = '有'

x3 = ['友','你','是','否','有'], y3 = '很'

x4 = ['你','是','否','有','很'], y4 = '多'

x5 = ['是','否','有','很','多'], y5 = '问'

x6 = ['否','有','很','多','问'], y6 = '号'

最后将这一句话划分成了6条数据,将x1-x6的字转化为这个字的编码,把y1-y6转化为one-hot形式。

One-Hot编码,又称为一位有效编码,主要是采用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候只有一位有效[5]。

我们将当前歌手的每一个专辑中的每一首歌分割成一句句话,然后通过这样的方式,生成可以进入模型训练的数据集合train_x和train_y。

2、LSTM模型构建

基于n-gram语言模型LSTM模型本质上是分类模型,将train_x中每个x划分到类别y,所以使用的是“many-to-one”的LSTM模型,模型构建如下图,包括一个Embeding层、两个LSTM层和一个Dense层。

在本文使用的深度学习框架为Keras,使用的方式为:

import keras

目前Keras已经集成到TensorFlow2.0中,使用的方式也可以是:

import tensorflow.python.keras as keras

在构建模型之前,设置全局参数如下。SEQ_LENGTH表征使用的是前几个字预测后一个字。MAX_NB_WORDS是保留多少个高频字,我们的歌词生成结果从前MAX_NB_WORDS中取字。EMBEDDING_DIM是EMBEDDING层的维度,以及LSTM层内隐层的维度。EMBEDDING_DIM_2是第二个LSTM层内隐层的维度。BATCH_SIZE是一次处理多少组数据,EPOCHS是迭代次数(也即是训练次数)。

SEQ_LENGTH = 5 # 通过前n个词,生成后一个词,SEQ_LENGTH=n
MAX_NB_WORDS = 10000  # vocabulary中最多保留多少最高频的字
EMBEDDING_DIM = 800 # embedding层的维度以及第一层lstm输入维度
EMBEDDING_DIM_2 = 1600 # 第二层lstm的输出维度
BATCH_SIZE = 250    # batch的大小
EPOCHS = 30    # 迭代次数

使用上述的参数构建模型如下。SEQ_LENGTH = 5,也就是模型示意图中的n=5,模型整体长度为5,Embedding将每个字变成词嵌入的形式[6],LSTM分别生成隐层是800和1600的LSTM层,也就是其中的细胞和状态函数的维度为800和1600。在这一部分,参数是否庞大,在建模之前可以计算一下LSTM模型中的参数个数[7],以防报错。Dense层可以选择激活函数,因为是多分类问题,所以我们选择的激活函数是softmax。

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))
embedding_layer = Embedding(nb_words, EMBEDDING_DIM, input_length=SEQ_LENGTH)(x_train_in) #变成词嵌入矩阵

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

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

在完成模型构建后,选择的优化器为Adam优化器,损失函数选择categorical_crossentropy loss(交叉熵损失函数),参数设置如下。

1、Adam 是一种可以替代传统随机梯度下降(SGD)过程的一阶优化算法,它能基于训练数据迭代地更新神经网络权重[8]。

2、交叉熵是用来评估当前训练得到的概率分布与真实分布的差异情况。它刻画的是实际输出(概率)与期望输出(概率)的距离,也就是交叉熵的值越小,两个概率分布就越接近[9]。

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

模型构建好之后,在CMD中运行模型,即可开始训练。

python model.py

四、训练结果与歌词预测

以周笔畅的歌词为例,经过了25次迭代(8个小时左右),val_loss降低到2.63,val_acc为0.62,结果还是比较让人满意的。

由于不同歌手的歌词量不一样,生成的训练数据也不一样。少的如窦靖童,由于是新人,仅有十几首歌,训练数据仅有1113条,训练过程只需要几分钟完成,但是结果也最差;多的如容祖儿,作为一代天后产量颇丰,从网易云上爬取了上千首歌曲,训练数据为33.68万条,模型总参数达到3660万个(如下图),设置训练次数10次,达到的准确率超过其他歌手训练25次的效果,但时间花费也较多,需要花费14个小时。不过与此相对应,生成的歌词质量也较好。

经过了几十个小时的训练与优化,得到的训练结果如下,个人感觉还可以接受,一些句子改改可以用来写情书吧(捂脸)。用不同歌手的风格,说“我爱你”,希望她会喜欢~


参考资料

[1] Selenium百度百科,https://baike.baidu.com/item/selenium/18266

[2] LSTM原理及实现,https://blog.csdn.net/gzj_1101/article/details/79376798

[3] n-gram语言模型,https://www.jianshu.com/p/e91f061d6d91,https://blog.csdn.net/ahmanz/article/details/51273500

[4] [歌词生成] 基于LSTM语言模型和seq2seq序列模型:数据爬取、模型思想、网络搭建、歌词生成,https://blog.csdn.net/quiet_girl/article/details/84768821 Lyrics-generation,https://github.com/Nana0606/lyrics_generation

[5] 详解one-hot编码,https://www.cnblogs.com/shuaishuaidefeizhu/p/11269257.html

[6] NLP 中的embedding layer,https://www.cnblogs.com/DjangoBlog/p/9224780.html

[7] LSTM 隐藏层维度和输入维度的问题,https://blog.csdn.net/weixin_42856002/article/details/99185022

[8] Adam 优化算法的基本机制,https://www.cnblogs.com/yifdu25/p/8183587.html

[9] 解析损失函数之categorical_crossentropy loss与 Hinge loss,https://www.jianshu.com/p/ae3932eda8f2

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值