投稿/爆料
厂商入驻

【原创】写给艺术家的循环神经网络教程

技术干货|论智|2017-11-22 16:30

本文并不打算写成一篇循环神经网络的全面概览,而是面向艺术家、设计师以及其他不具备机器学习背景的读者。通过一个基于p5.js、使用简单的Javascript代码编写的例子,向艺术家和设计师展示如何使用循环神经网络创作生成性艺术,以及基于循环神经网络建模的基本思路。

neural

介绍

这些年来,机器学习逐渐成为创意社区流行的工具。诸如风格迁移t-SNE自编码器生成对抗网络其他很多方法都进入了数字艺术家的工具箱。许多技术利用卷积神经网络进行特征提取和特征处理。

另一方面,基于循环神经网络和其他自回归模型,可以创造强大的工具,这些可以生成逼真的序列数据。艺术家们运用这些技术来生成文本音乐声音。我觉得目前大家较少关注的一个领域是矢量图形的生成,这也许是因为数据不足。

手写是素描艺术的一种形式。最近,我和Shan Carter、Ian JohnsonChris Olah合作,在distill.pub上发表了一篇关于手写生成的文章,可视化了一个生成手写的循环神经网络的内部机制。事实上,这个项目也是我自己的一个元实验。我并没有直接进行可视化实验和写作,我先给一个预先训练好的手写模型创建了一个易于使用的Javascript界面​​,并让我的合作者(非常有天赋的数据可视化艺术家),试验这个模型,并创作一些东西。后来他们创造了distill.pub文章中的那些美丽的交互式可视化实验。

因此我决定写这篇博客,详细介绍一个与distill.pub项目中使用的相同的手写模型,希望其他艺术家和设计师也可以利用这些技术来创作,甚至更深入这个领域。

手写之脑建模

其实,我们写信的时候,大脑里有很多事情在发生。根据我们写信的目的,我们制定一个要写什么的计划,选择合适的词汇,决定笔迹的工整程度,然后才拿起笔,开始在一叠纸上书写。书写时我们需要决定落笔的位置,往哪个方向移动笔,以及何时提笔。

要创建一个Javascript模型来模拟写信的整个人脑是很难的,但是我们可以集中精力到书写过程的最后部分,也就是落笔的位置、移动笔的方向和提笔的时机,来近似地建模手写之脑。所以我们的手写过程模型将只关注笔的位置,以及笔是否接触纸张。

我们的模型还有两个假设。第一个假设是模型下一步要写的内容只取决于已写的内容。但是,当我们书写的时候,尽管我们精确地记忆了前一笔的细节,我们实际上并不精确地记得我们之前写了多少笔,我们只对我们已经书写的内容有一个模糊的概念。事实上,这个模糊的概念可以通过循环神经网络建模。

使用RNN(循环神经网络),我们可以直接将这类的模糊知识存储到RNN的神经元中,我们把这个对象称为RNN的隐藏状态。这个隐藏状态只是一个浮点数向量,用来记录每个神经元的活动。我们的模型接下来将要书写的内容,取决于它的隐藏状态。这个隐藏状态在书写的过程中会不断更新,因而将不断改变。我们将在下一节讲解这一过程是如何进行的。

模型的第二个假设是,模型不能完全确定下一步应该书写的内容。实际上,模型接下来要写什么的决定是随机的。例如,模型在书写字符y的时候,它可能会决定继续写这个字符,使y底部的钩子大一点,也可能决定突然结束这个字符并将笔移动到另一个位置。因此,我们的模型的输出结果不是精确的下一步书写的内容,而是下一步书写内容的概率分布。我们需要从这个概率分布中取样来决定下一步到底要写什么。

下面的示意图概括了以上两个假设。示意图描述了使用具有隐藏状态的循环神经网络模型来生成随机序列的过程。

RNN生成随机序列

如果你不完全理解上面的示意图,别担心。下一节中,我们会使用Javascript逐步演示上图中的操作。

用于手写的循环神经网络

我们已经预先训练了一个循环神经网络模型来完成上一节所述的手写任务。我们将在本节介绍如何基于Javascript和p5.js使用这个模型。以下是用于手写生成的整个p5.js sketch。

var x, y;
var dx, dy;
var pen;
var prev_pen;
var rnn_state;
var pdf;
var temperature = 0.65;
var screen_width = window.innerWidth;
var screen_height = window.innerHeight;
var line_color;

function restart() {
  x = 50;
  y = screen_height/2;
  dx = 0;
  dy = 0;
  prev_pen = 0;
  rnn_state = Model.random_state();
  line_color = color(random(255), random(255), random(255))
}

function setup() {
  restart();
  createCanvas(screen_width, screen_height);
  frameRate(60);
  background(255);
  fill(255);
}

function draw() {
  rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
  pdf = Model.get_pdf(rnn_state);
  [dx, dy, pen] = Model.sample(pdf, temperature);
  if (prev_pen == 0) {
    stroke(line_color);
    strokeWeight(2.0);
    line(x, y, x+dx, y+dy);
  }
  x += dx;
  y += dy;
  prev_pen = pen;
  if (x > screen_width - 50) {
    restart();
    background(255);
    fill(255);
  }
}

我们将解释每行代码是如何工作的。首先,我们需要定义一些变量来记录笔到底在哪里(x,y)。我们的模型将使用较小的坐标偏移量(dx,dy)来确定笔下一步应该到哪儿。而(x,y)将是(dx,dy)的累积。

var x, y; // 笔的位置的绝对坐标
var dx, dy; // 笔画的偏移量,单位为像素

另外,我们的笔并不总是接触纸张。因此我们需要一个pen变量来建模这个信息。如果pen的值为零,那么笔在当前时刻接触纸张。我们还需要记录前一时刻的pen变量,并将其存储到prev_pen中。

var pen; // 记录笔是否接触纸张,值为0或1
var prev_pen; // 前一时刻笔是否接触纸张

我们的模型在每个时刻生成的(dx, dy, pen)变量组成一个列表,这个列表足够让我们绘制模型在屏幕上生成的内容。所有变量(dx, dy, x, y, pen, prev_pen)的初始值为零。

我们还定义了一些RNN模型使用的变量:

var rnn_state; // 储存rnn的隐藏状态
var pdf; // 储存混合密度分布的所有参数

// 控制模型的不确定性
// temperatuer越高,越不确定
var temperature = 0.65; // 非负

如前所述,rnn_state变量表示RNN的隐藏状态。这个变量会储存RNN以为它过去书写的内容的模糊概念。我们将使用代码后面的update函数来更新rnn_state

rnn_state = Model.update([dx, dy, prev_pen], rnn_state);

rnn_state对象会用来生成模型下一步将干什么的概率分布。这个概率分布用pdf对象表示。代码后面的get_pdf函数可以用来从rnn_state取得pdf对象,就像这样:

pdf = Model.get_pdf(rnn_state);

一个额外的temperature(温度)变量让我们可以控制我们希望模型的自信或不确定程度。结合pdf对象,我们可以使用sample函数从概率分布中取样下一组(dx, dy, pen)的值:

[dx, dy, pen] = Model.sample(pdf, temperature);

我们还需要一些变量来控制书写的颜色,以及记录浏览器的分辨率大小:

// 储存浏览器的分辨率大小
var screen_width = window.innerWidth; // 宽
var screen_height = window.innerHeight; // 高

// 颜色
var line_color;

变量声明完毕,现在我们可以初始化这些变量了。我们将创建一个restart函数来初始化这些变量,因为我们以后将多次重新初始化它们。

function restart() {
  // 从画布的左侧开始的50个像素
  x = 50;
  // 画布的当中
  y = screen_height/2;

  // 初始化笔的状态
  dx = 0;
  dy = 0;
  prev_pen = 0;
  // 注意:我们的书写基于前一笔的状态

  // 用随机值初始化rnn的隐藏状态
  rnn_state = Model.random_state();

  // 使用随机RGB值初始化颜色
  line_color = color(random(255), random(255), random(255))
}

定义restart函数之后,我们可以定义p5.js通常的setup函数来初始化sketch:

function setup() {
  restart(); // 为此demo初始化变量
  // 根据屏幕尺寸创建画布
  createCanvas(screen_width, screen_height); 
  frameRate(60); // 每秒60帧
  // 清除背景(空白)
  background(255);
  fill(255);
}

我们的手写生成将通过p5.js框架的draw函数完成。这个函数每秒会被调用60次。每次调用函数的时候,RNN将在屏幕上书写一些内容。

function draw() {
  // 基于笔先前的状态和隐藏状态得出下一个隐藏状态
  rnn_state = Model.update([dx, dy, prev_pen], rnn_state);

  // 从隐藏状态中获取概率分布的参数
  pdf = Model.get_pdf(rnn_state);

  // 基于概率分布和temperature取样下一笔的状态
  [dx, dy, pen] = Model.sample(pdf, temperature);

  // 仅当笔接触纸张的时候在纸上书写
  if (prev_pen == 0) {
    // 设置当前线条颜色
    stroke(line_color);
    // 设定当前线条的粗度为2个像素
    strokeWeight(2.0);
    // 画一条线,将先前的点和当前的点连接起来
    line(x, y, x+dx, y+dy);
  }

  // 基于偏移量更新绝对坐标  
  x += dx;
  y += dy;

  // 将前一笔的状态更新为我们刚取样的当前的状态
  prev_pen = pen;

  // 如果rnn开始在接近屏幕右边缘的地方书写了,
  // 那就重启。
  if (x > screen_width - 50) {
    restart();
    // 清除屏幕
    background(255);
    fill(255);
  }
}

在每一帧中,draw函数会根据它之前在屏幕上书写的内容来更新模型的隐藏状态。基于这个隐藏的状态,模型将生成下一步生成内容的概率分布。基于这个概率分布和temperature参数,我们将随机采样一组新的(dx,dy,pen)变量,这一组变量用来表示下一步的行动。基于这组新的变量,如果笔先前接触了纸张,模型将在屏幕上画一条线,并更新笔的全局位置。一旦笔的全局位置靠近屏幕的右边缘,模型将重置sketch并重新开始。

以下为sketch的最终效果:

sketch

到此为止,基于p5.js,和一段简短的Javascript代码,我们成功在浏览器中生成手写。

变温概率分布取样

变量pdf用于储存每个时刻下一笔的概率分布。pdf内部实际上包含了一个复杂概率分布的参数(一组正态分布的均值和标准差)。我们选择将dxdy的概率分布建模为混合密度分布

但究竟什么是混合密度分布?好吧,统计学家(数据科学家)喜欢用数学上易于处理的知名分布(例如正态分布)为概率分布建模,并试图确定分布的参数(例如正态分布的均值和标准差),以便达成对数据的最优拟合。然而,当处理一些复杂的事物时,比如书写数据的笔触,我们发现简单的正态分布在建模数据方面表现得不够好。直觉上,书写的笔触要么靠近前一个位置,要么在词或字符完成时跳到另一个位置。

应付这个问题有一个直截了当的方法,建模概率分布为许多正态分布的和。对于我们的例子而言,我们将书写笔触建模为20个正态分布的和。使用20个正态分布的混合,我们的模型在实际手写数据的建模方面表现得还不错。具体技术细节请参考这篇文章

当我们采用这个概率分布,并从这个分布中取样以得到下一组(dx, dy, pen)的值,进而确定下一步书写的内容时,我们使用temperature参数来控制模型的不确定性水平。如果温度参数非常高,那么我们更有可能从概率分布的不太可能区域中取样。如果温度参数非常低,或者接近于零,那么我们只会从分布的最可能部分取样。

下图可视化了上述内容:

temperature

不同温度下的取样区域

出于简明的考虑,上图模拟的是20个带温度参数的一维正态分布的混合。在手写模型中,概率分布是20个二维正态分布的混合。下图展示了不同温度下手写效果的变化。

temperatures-handwriting

总的来说,温度较低时,手写模型更具确定性,书写的效果一般来说相对要整洁和逼真一些。而温度较高时,书写的效果一般来说要“奔放”一些。

结语

本文的相关代码发布在github上,对机器学习感兴趣的艺术家、设计师和其他同学可自行fork。

本文仅仅是对循环神经网络的一个初步介绍。如果你希望更深入地了解机器学习开发的整个过程,并训练你自己的模型,可以参考下面这些优秀的免费学习资源。如果你使用keras来创建和训练你的模型,可以看看kera.js这个工具。kera.js可以导出预先训练好的模型,用在浏览器中。我个人还没有用过keras.js,因为我觉得使用Javascript从头开始编写手写模型很有意思。

最后,这个模型已经被人移植到bl.ocks上,一些人扩展了它,用来做一些非常有趣事情

原文 Recurrent Neural Network Tutorial for Artists
感谢原作者hardmaru授权论智编译,未经授权禁止转载。详情见转载须知

本文来自机器人之家,如若转载,请注明出处:https://www.jqr.com/news/008900
爆料投稿,欢迎投递至邮箱:service@jqr.com
机器学习人工智能深度学习 Javascript
推荐阅读

0 条回复

评论

游客
robot
发布需求
联系客服联系客服
robot
联系客服