查看原文
其他

周董下次发新歌,可以请 AI 来作词吗?

以下文章来源于深度学习每日摘要 ,作者张泽旺

By 超神经


场景描述:周董终于发了新歌,立刻在各个平台刷屏。这首新歌《说好不哭》,上线两小时,销售额就破了两千万。方文山回归、阿信助唱等因素都让这首歌火爆的一塌糊涂。方文山的作词水平一般人可能难以企及,但如果让 AI 学习训练之后,会不会作出同样水平的歌词呢?


关键词: AI 作词  序列建模  文本生成



周杰伦的一首新歌《说好不哭》,引发了全民围观的热潮,从新歌发布当晚 11 点到昨天中午,周杰伦都一直牢牢占据着微博热搜。毕竟距离周董上一次发布歌曲,已经过去了 489  天。

听到这首新歌,很多人都哭了,但也引来了不少口水。虽有争议,但不妨碍它在朋友圈刷屏,社交平台霸榜,甚至导致 QQ 音乐一度崩溃,因为这首歌勾起了无数人的青春记忆。

「周五」组合出镜新歌 MV

此外,这首新歌也因为方文山的回归,令无数粉丝倍加期待。

方文山的作词与周董的作曲配可谓天衣无缝,在早期的合作中,创作出许多经典作品,《爱在西元前》、《东风破》等等,都因绝美的歌词让人印象深刻。

方文山在微博自评《说好不哭》歌词

少了方文山写的词,周董的歌好像总少了那么一些意境。周杰伦自己也曾说:「我的曲如果没有方文山的词,不会中(受欢迎)。」

那么除了这位周董的御用作词人之外,还有谁能写出方文山笔下风格多变、颇具诗意的歌词呢?无处不在人工智能已经做出尝试。


在 GitHub 上,有几个有趣的项目,用机器学习生成了周董风格(准确说是方文山风格)的歌词,它们到底是怎么实现的呢?

本文跟随其中一个项目,详细了解 AI 作词的过程。

 

原理回顾


机器作词是序列建模(以下用 seq2seq )的典型应用,其基本思想就是给定序列 A ,机器负责产生序列 B ,并且再将序列 B 作为输入,机器负责生成序列 C...如此循环下去即可生成无限长度的序列。

seq2seq模型图,左边是编码器,右边是解码器
假设问题是从序列 A 到序列 B 之间的映射,那么 seq2seq 模型的工作流程如下:
  • 序列 A 中的每一个单词通过 word_embedding 操作以后,作为 input 进入编码器,编码器可以是一个多层 RNN 结构,编码器输出一个向量;

  • 训练时,解码器的输入跟编码器的输入是一样的,然后解码器的输出与序列 B 之间的交叉熵作为模型的目标函数;

  • 生成的时候,首先给定一个种子序列作为编码器的输入,并且解码器的上一时刻的输出作为下一时刻的输入,如此循环往复,直到生成给定数量的序列。


本文建立的模型就是基于以上原理。


 

模型代码设计


要完成自动生成歌词需要一个生成模型,而生成模型一般都是无监督问题,但在此需要将它转化成有监督问题,以发现数据内在的关联性,比如上下文的衔接,然后用预测学习来代替无监督学习。


就有监督学习而言,通常需要准备好具有映射关系的数据集:X 和 Y。虽然歌词是一个整体,但是这个整体是序列组成的,序列与序列之间会有一定的时序关系。比如
天青色等烟雨  而我在等你
就可以把「天青色等烟雨」看作 X ,把「而我在等你」看作 Y ,如果我们将 X 作为输入,而网络输出的是 Y ,那就能说明模型具备了写词的能力。
以此为依据,建立了数据集。
 
数据预处理后,接下来是建立模型,主要是围绕 seq2seq 模型构建,而在编码器和解码器部分,可以自由构造,如选择不同的 rnn_cell ,或者选择不同的层数、神经元个数。
另一个重要的部分是目标函数,要确保目标函数对于所有要训练的参数是可微的,这样就可以构建端对端的基于后向误差更新的深度学习系统。


当有监督学习训练的模型的误差满足要求后,就可以把参数保存下来,用此模型去生成歌词。
模型构建其实就是一个抽样的过程,给定种子序列,选好特定的抽样方法,即可生成无限多个汉字组成的序列。


为了了解训练过程中的误差更新趋势,还需要建立日志记录以及日志可视化的部分,这样以便于我们做后期的模型性能分析,本文中会粗略提及。


本项目的文件结构如下图所示:
.
├── analysis
│   └── plot.py
├── data
│   ├── context.npy
│   ├── lyrics.txt
│   ├── origin.txt
│   ├── parse.py
│   └── vocab.pkl
├── log
│   ├── 2016-12-1022:11:22.txt
│   └── 2016-12-1022:11:22.txt.png
├── preprocess.py
├── README.md
├── result
│   └── sequence
├── sample.py
├── save
│   ├── checkpoint
│   ├── config.pkl
│   ├── model.ckpt-223
│   ├── model.ckpt-223.meta
│   └── words_vocab.pkl
├── seq2seq_rnn.py
├── train.py
└── utils.py
它们的功能分别如下:
  • 主目录下面的 utils.py 是公共函数库,preprocess.py 是数据预处理代码, seq2seq_rnn.py 是模型代码,sample.py 是抽样生成过程,train.py 是训练过程;

  • log 目录中存储的是训练过程中的日志文件;

  • save 目录中存储的是训练过程中的模型存储文件;

  • data 目录中存放的是原始歌词数据库以及处理过的数据库;

  • result 目录中存放的是生成的序列;

  • analysis 目录中存放的是用于可视化的代码文件;


 

数据预处理


原始歌词文件是从百度文库中下载的,其中包括了一些不必要的文本,在此过滤了全部非中文字符,并使用空格分隔相邻句子。
下面这段代码,就是剔除源歌词文件中的多余空格以及非中文字符:
reg = re.compile(ur"[\s+]")
c = reg.sub(' ',unicode(c))
reg = re.compile(ur"[^\u4e00-\u9fa5\s]")
c = reg.sub('',unicode(c))
c = c.strip()
将源歌词文件处理成连续的句子文件以后,还需要将句子划分成很多对训练样本。
首先需要统计歌词中所有不同汉字的总数(包括一个空格),并且对这些汉字进行索引,可将原文由汉字变成整型数组,这样训练的时候读取数组即可;
另外,索引可用来进行 word_embedding ,即将每个单词映射成一个特征向量。下面一段代码就是建立词典以及上下文的过程:
def build_dataset(self):
   ''' parse all sentences to build a vocabulary
       dictionary and vocabulary list
   '''
   with codecs.open(self.input_file, "r",encoding='utf-8') as f:
       data = f.read()
   wordCounts = collections.Counter(data)
   self.vocab_list = [x[0] for x in wordCounts.most_common()]
   self.vocab_size = len(self.vocab_list)
   self.vocab_dict = {x: i for i, x in enumerate(self.vocab_list)}    
   with codecs.open(self.vocab_file, 'wb',encoding='utf-8') as f:
       cPickle.dump(self.vocab_list, f)
   self.context = np.array(list(map(self.vocab_dict.get, data)))
   np.save(self.context_file, self.context)
然后确定出要建立的每对样本长度,以及训练时候的 batch_size 大小,进而把数据集分成很多个 mini-batch,可以在训练时依次读取。
为了预处理方便,我们选择了固定的样本序列长度,并且让 X 和 Y 的长度一致,从数据集中选取 X 和 Y 的时候每次滑动步长为 1 ,间隔也为 1 ,如下代码所示:
def init_batches(self):
   '''
       Split the dataset into mini-batches,
       xdata and ydata should be the same length here
       we add a space before the context to make sense.
   '''
   self.num_batches = int(self.context.size / (self.batch_size * self.seq_length))
   self.context = self.context[:self.num_batches * self.batch_size * self.seq_length]
   xdata = self.context
   ydata = np.copy(self.context)
   ydata[:-1] = xdata[1:]
   ydata[-1] = xdata[0]
   self.x_batches = np.split(xdata.reshape(self.batch_size, -1), self.num_batches, 1)
   self.y_batches = np.split(ydata.reshape(self.batch_size, -1), self.num_batches, 1)
   self.pointer = 0
可以看到 Y 的最后一个数是设置为 X 的第一个数,因此我们在数据集的开头插入了一个空格使得整体连贯。
pointer 作为标记使用,标记当前训练的是哪一个 mini-batch,如果所有 mini-batch 都训练过了,即完成了一个 Epoch, pointer 将置零,如下面代码所示:
def next_batch(self):
   ''' pointer for outputing mini-batches when training
   '''
   x, y = self.x_batches[self.pointer], self.y_batches[self.pointer]
   self.pointer += 1
   if self.pointer == self.num_batches:
       self.pointer = 0
   return x, y
 

编写基于 LSTM 的 seq2seq 模型


数据预处理完成以后,接下来是建立 seq2seq 模型。主要分为三步:
  • 确定编码器和解码器中 cell 的结构,即采用什么循环单元,多少个神经元以及多少个循环层;

  • 将输入数据转化成 TensorFlow 的 seq2seq.rnn_decoder 需要的格式,并得到最终的输出以及最后一个隐含状态;

  • 将输出数据经过 softmax 层得到概率分布,并且得到误差函数,确定梯度下降优化器;


TensorFlow 提供的 rnncell 共有三种,分别是 RNN、GRU、LSTM,因此这里也提供三种选择,并且每一种都可以使用多层结构,即 MultiRNNCell ,如下代码所示:
if args.rnncell == 'rnn':
   cell_fn = rnn_cell.BasicRNNCell
elif args.rnncell == 'gru':
   cell_fn = rnn_cell.GRUCell
elif args.rnncell == 'lstm':
   cell_fn = rnn_cell.BasicLSTMCell
else:    
   raise Exception("rnncell type error: {}".format(args.rnncell))
cell = cell_fn(args.rnn_size)
self.cell = rnn_cell.MultiRNNCell([cell] * args.num_layers)
选择好了 cell 的结构以后,接下来就是将输入数据传递的 seq2seq 模型中去。
TensorFlow 的 seq2seq.py 文件中提供了多个用于建立 seq2seq 的函数,这里选择出了两个,分别是 rnn_decoder 以及 attention_decoder,下面以 rnn_decoder 为例。
从 tensorflow 源码中可以看到, rnn_decoder 函数主要有四个参数,它们的注释如下:
decoder_inputs: A list of 2D Tensors [batch_size x input_size].
initial_state: 2D Tensor with shape [batch_size x cell.state_size].

cell: rnn_cell.RNNCell defining the cell function and size.

loop_function: If not None, this function will be applied to the i-th output
in order to generate the i+1-st input, and decoder_inputs will be ignored,
except for the first element ("GO" symbol). This can be used for decoding,
but also for training to emulate http://arxiv.org/abs/1506.03099.
Signature -- loop_function(prev, i) = next
* prev is a 2D Tensor of shape [batch_size x output_size],
* i is an integer, the step number (when advanced control is needed),
* next is a 2D Tensor of shape [batch_size x input_size].


可以看到,decoder_inputs 其实就是输入的数据,要求的格式为一个 list,并且 list 中的 tensor 大小应该为 [batch_size,input_size],换句话说这个 list 的长度就是 seq_length ;
但我们原始的输入数据的维度为 [args.batch_size, args.seq_length],看起来缺少了一个 input_size 维度,其实就是 word_embedding 的维度,或者说 word2vec 的大小。

这里需要手动进行手动 word_embedding ,并且这个 embedding 矩阵是一个可以学习的参数:
self.input_data = tf.placeholder(tf.int32, [args.batch_size, args.seq_length])with tf.variable_scope('rnnlm'):
softmax_w = build_weight([args.rnn_size, args.vocab_size],name='soft_w')
softmax_b = build_weight([args.vocab_size],name='soft_b')
word_embedding = build_weight([args.vocab_size, args.embedding_size],name='word_embedding')
inputs_list = tf.split(1, args.seq_length, tf.nn.embedding_lookup(word_embedding, self.input_data))
inputs_list = [tf.squeeze(input_, [1]) for input_ in inputs_list]
initial_state 是 cell 的初始状态,其维度是 [batch_size,cell.state_size],由于 rnn_cell 模块提供了对状态的初始化函数,因此我们可以直接调用:
self.initial_state = self.cell.zero_state(args.batch_size, tf.float32)
cell 就是我们要构建的解码器和编码器的 cell ,上面已经提过了。
最后一个参数是 loop_function ,其作用是在生成的时候,把解码器上一时刻的输出作为下一时刻的输入,并且这个 loop_function 需要自己建立,如下所示:
def loop(prev, _):
   prev = tf.matmul(prev, softmax_w) + softmax_b
   prev_symbol = tf.stop_gradient(tf.argmax(prev, 1))
   return tf.nn.embedding_lookup(embedding, prev_symbol)
最后,在构建好 seq2seq 的模型后,将上面参数传入 rnn_decoder 函数即可:
outputs, last_state = seq2seq.rnn_decoder(inputs_list, self.initial_state, self.cell, loop_function=loop if infer else None, scope='rnnlm')
其中 outputs 是与 decoder_inputs 同样维度的量,即每一时刻的输出;last_state 的维度是 [batch_size,cell.state_size],即最后时刻的所有 cell 的状态。
接下来需要 outputs 来确定目标函数,而 last-state 的作用是作为抽样生成函数下一时刻的状态。


TensorFlow 中提供了 sequence_loss_by_example 函数用于按照权重来计算整个序列中每个单词的交叉熵,返回的是每个序列的 log-perplexity 。

为了使用 sequence_loss_by_example 函数,我们首先需要将 outputs 通过一个前向层,同时需要一个 softmax 概率分布,这个在生成中会用到:
output = tf.reshape(tf.concat(1, outputs), [-1, args.rnn_size])
self.logits = tf.matmul(output, softmax_w) + softmax_b
self.probs = tf.nn.softmax(self.logits)
loss = seq2seq.sequence_loss_by_example([self.logits],
      [tf.reshape(self.targets, [-1])],
      [tf.ones([args.batch_size * args.seq_length])],
      args.vocab_size)    
# average loss for each word of each timestepself.cost = tf.reduce_sum(loss) / args.batch_size / args.seq_length
最后就是建立一个 op,以便训练,例如 var_op、var_trainable_op、train_op、initial_op、saver:
self.lr = tf.Variable(0.0, trainable=False)
self.var_trainable_op = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, self.var_trainable_op),
           args.grad_clip)
optimizer = tf.train.AdamOptimizer(self.lr)
self.train_op = optimizer.apply_gradients(zip(grads, self.var_trainable_op))
self.initial_op = tf.initialize_all_variables()
self.saver = tf.train.Saver(tf.all_variables(),max_to_keep=5,keep_checkpoint_every_n_hours=1)
self.logfile = args.log_dir+str(datetime.datetime.strftime(datetime.datetime.now(),'%Y-%m-%d %H:%M:%S')+'.txt').replace(' ','').replace('/','')
self.var_op = tf.all_variables()
train_op 即为训练时需要运行的。


 

编写抽样生成函数


如上所述,在抽样生成的时候,我们首先需要一个种子序列,同时在第一步的时候,我们需要向网络传入一个 0 的初始状态,并通过种子序列的第一个字得到下一个隐含状态,然后再结合种子的第二个字传入下一个隐含状态,直到种子序列传入完毕:
state = sess.run(self.cell.zero_state(1, tf.float32))        
for word in start:
   x = np.zeros((1, 1))
   x[0, 0] = words[word]
   feed = {self.input_data: x, self.initial_state:state}
    [probs, state] = sess.run([self.probs, self.final_state], feed)
种子序列运行完毕以后,接下来就进入真正的抽样过程了,即拿上一时刻的 state 以及上一时刻输出 probs 中的最佳单词作为下一时刻的输入,那么给定了一个所有单词的概率分布 probs ,该时刻的最佳单词如何定义呢?这里我列举了三种情况:
  • argmax 型:即找出 probs 中最大值所对应的索引,然后去单词表中找到该索引对应的单词即为最佳单词;

  • weighted 型:即随机取样,其工作流程如下:首先,计算此 probs 的累加总和 S ;其次,随机生成一个 0~1 之间的随机数,并将其与 probs 的总和相乘得到 R;最后,将 R 依次减去 probs 中每个数,直到 R 变成负数的那个 probs 的索引,即为我们要挑选的最佳单词;

  • combined 型:这里我把 argmax 和 weighted 结合起来了,即每次遇到一个空格(相当于一句歌词的结尾),就使用 weighted 型,而其他时候都使用 argmax 型;

这三种的实现方式如下所示:
def random_pick(p,word,sampling_type):
   def weighted_pick(weights):
       t = np.cumsum(weights)
       s = np.sum(weights)
       return(int(np.searchsorted(t, np.random.rand(1)*s)))    
   if sampling_type == 'argmax':
       sample = np.argmax(p)    
   elif sampling_type == 'weighted':
       sample = weighted_pick(p)
   elif sampling_type == 'combined':        
       if word == ' ':
           sample = weighted_pick(p)
       else:
           sample = np.argmax(p)
   return sample
最后,抽样生成过程的具体代码如下所示,其中 start 是种子序列, attention 是判断是否加入了注意力机制。
word = start[-1]
for n in range(num):
   x = np.zeros((1, 1))
   x[0, 0] = words[word]
   if not self.args.attention:
       feed = {self.input_data: [x], self.initial_state:state}
       [probs, state] = sess.run([self.probs, self.final_state], feed)
   else:
       feed = {self.input_data: x, self.initial_state:state,self.attention_states:attention_states}
       [probs, state] = sess.run([self.probs, self.final_state], feed)
   p = probs[0]
   sample = random_pick(p,word,sampling_type)
   pred = vocab[sample]
   ret += pred
   word = pred

  ret 数组即为最终的生成序列。


 

编写训练函数


训练函数需要完成的功能主要有:提供用户可设置的超参数、读取配置文件、按照 mini-batch 进行批训练、使用 saver 保存模型参数、记录训练误差等等,下面将列举部分代码进行说明。


首先,我们使用 argparse.ArgumentParser 对象进行解析命令行参数,或者设置默认参数,例如:
parser.add_argument('--rnn_size', type=int, default=128,
            help='set size of RNN hidden state')
设置了 rnn_size 默认大小为 128 ,而用户也可以在命令行使用类似于以下这种方式来指定参数大小:
python train.py --rnn_size 256
其次,我们需要提供是否继续训练的判断,也就说是从头开始训练还是导入一个已经训练过的模型继续训练,即下面的语句:
if args.keep is True:
   print('Restoring')
   model.saver.restore(sess, ckpt.model_checkpoint_path)        
else:
   print('Initializing')
   sess.run(model.initial_op)
然后就是将 X 和 Y 数据 ,feed 到模型中去运行 op 并得到误差值:
x, y = text_parser.next_batch()
feed = {model.input_data: x, model.targets: y, model.initial_state: state}
train_loss, state, _ = sess.run([model.cost, model.final_state, model.train_op], feed)
训练过程比较简单,基本上就是设置参数-导入数据-导入模型-运行 op  -得到误差-下一个 Epoch 继续训练直到满足要求为止。


 

编写日志


这里说的日志可以理解为保存参数、保存训练过程中的误差以及训练时间等等,仅作抛砖引玉的说明。
为了使得每一次训练都不会白白浪费,我们需要设置好参数保存,如可以设置训练了多少个样本就保存一次参数、训练了多少个 Epoch 就保存一次:
if (e*text_parser.num_batches+b)%args.save_every==0 、
       or (e==args.num_epochs-1 and b==text_parser.num_batches-1):
   checkpoint_path = os.path.join(args.save_dir, 'model.ckpt')
   model.saver.save(sess, checkpoint_path, global_step = e)
   print("model has been saved in:"+str(checkpoint_path))
记录训练误差也是很重要的一步,很多时候我们需要分析 cost 曲线随时间或者是迭代次数的变化趋势。

因此这里我们建立了一个 logging 函数(在utils.py文件中),并且在每一个 Epoch 训练结束的时候就记录一次该 Epoch 的平均误差、运行时间等等:
delta_time = end - start
ave_loss = np.array(total_loss).mean()
logging(model,ave_loss,e,delta_time,mode='train')
 

编写可视化函数


由于时间的关系,这里仅对日志文件做了初步的可视化,即提取日志文件中的 Epoch 以及对应的误差,从而得到一条 Cost-Epoch 曲线,可视化的函数的部分代码如下:
if line.startswith('Epoch'):        
   if 'validate' in line:
       index2 = index2 + 1
       cost = line.split(':')[2]
       indexValidateList.append(index2)
       validateCostList.append(float(cost))
   elif 'train error rate' in line:
       index1 = index1+1
       cost = line.split(':')[2]
       indexCostList.append(index1)
       costList.append(float(cost))
然后使用 matplotlib 库进行作图:
def plot(self):
   title,indexCostList,costList = self.parse()
   p1 = plt.plot(indexCostList,costList,marker='o',color='b',label='train cost')
   plt.xlabel('Epoch')
   plt.ylabel('Cost')
   plt.legend()
   plt.title(title)    
   if self.saveFig:
       plt.savefig(self.logFile+'.png',dpi=100)
       #plt.savefig(self.logFile+'.eps',dpi=100)
   if self.showFig:
       plt.show()
 

设置训练超参


超参的选择一直是训练深度学习的一个难点,无论是循环神经元的个数、层数还是训练样本批处理的大小,都没有一个固定的判断准则,超参设置因问题而已,而且很多时候论文中使用的经验规则,而本文也只能根经验设置参数。
最终选择了两层 LSTM,每层包含 128 个神经元作为 seq2seq 模型的 cell,词向量 word_embedding 的大小为 100,批处理大小设置为 32 ,序列长度为16,并且使用了 Adam 随机梯度下降算法,学习率设置为 0.001 ,一共训练了 230 个周期。

 

结果展示


在训练完所有 Epoch 以后,运用可视化函数作出了误差随训练周期的变化图像:

并且使用 weighted 的抽样生成方法,选择了「从前」作为种子序列,然后让机器生成了长度为 200 的歌词。
如下所示,其中有几句看起来似乎有押韵的意思,整体上看起来不知道要表达什么。


《机器作词》                                                                                                                                                                               从前笑 爱说走 晚到
为什么我都会开错记忆不多 
幸福搁抽勾 
我只为大参城真 说越没有
简单一个音 最后的秘密
大人情了解境 
再继续永远都不会痛 
不知道很感 但它怕开地后想念你
怕你身边 把那一天 
你最好了 鸣债不手切最后走的生活
好拥有 你想的兵水停 
有什么回吻你都懂不到 
谁说没有结果 我是不是多提醒你
好多永远隔着对 会心碎你走罪你
谁说用手语 想要随时想能打开
想要当然魅力太强被曾人迷人蛋
哈哈哈


为了使生成的歌词更加通顺,作者又加入了十多部近代中文小说、散文以及林夕的所有歌词作为语言模型的预训练语料。

通过训练好基于 LSTM-seqseq 的语言模型之后,使用周杰伦的歌词来在此模型上接着训练了40个 Epoch,同样以「从前」作为种子序列,生成了长度为 200 的周杰伦风格的歌词。

《机器作词》                                                                                                                                                                      
从前进开封 出水芙蓉加了星
在狂风叫我的爱情 让你更暖
心上人在脑海别让人的感觉
想哭 是加了糖果
船舱小屋上的锈却现有没有在这感觉
你的美 没有在
我却很专心分化
还记得
原来我怕你不在 我不是别人
要怎么证明我没力气 我不用 三个马蹄
生命潦草我在蓝脸喜欢笑
中古世界的青春的我
而她被芒草割伤
我等月光落在黑蝴蝶
外婆她却很想多笑对你不懂 我走也绝不会比较多
反方向左右都逢源不恐 只能永远读着对白



 总结 & 后续 

机器生成歌词跟机器作曲一样,都是序列建模的运用,本文使用了周杰伦早期的所有歌词。

虽然训练样本较少,数据预处理较为粗糙,但是基本上把整个建模过程都认真介绍了一遍,模型超参设置仅供参考,完整代码可以访问 github ,可以去 fork 一下。后续将考虑使用外部动态网络或强化学习进行持续优化。

Github 地址:
https://github.com/hundred06/jaylyrics_generation_tensorflow


除了本项目,在 GitHub 上,还有几个类似的工作。


比如其中一个是使用字符级别的 RNN 进行文本生成,利用了 MXNet-Gluon 框架,并基于 PyTorch 实现了如下创作:


我们的爱
持纵的微银 
我路茫未望
象对躲睡被仁消 整虹伪沙
乡月裂续武深面到
身壁许达道 
用移照叫生


GitHub 地址:
https://github.com/L1aoXingyu/Char-RNN-Gluon

说回周董,在去年他担任评委的《中国好声音》上,其麾下的战将,就有一位使用 AI 作词的清华学霸,凭借机器改编的 Rap,技惊四座。

也许周董下次的歌曲,也可以考虑下由 AI 来作词。当然,就目前效果来看,AI 还需继续努力。


注:本文介绍的工作完成于 2016 年,训练的环境是 Ubuntu 16.04 操作系统,使用的 tensorflow 版本是 r0.11 ,所使用的 python 版本为 2.7,所用的 GPU 是 Nvidia GeForce GTX 960M。

本文转载自公众号:深度学习每日摘要,内容有部分改动;原作者:张泽旺。二次转载请联系原文作者。

—— 完 ——

扫描二维码,加入讨论群

获得优质数据集

回复「读者」自动入群

更多精彩内容(点击图片阅读)

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存