这篇文章不是对循环神经网络的综合概述。它适用于没有任何机器学习背景的读者。其目的是向艺术家和设计师展示如何使用预先训练的神经网络——使用简单的Javascript和p5.js库来制作交互式数字作品。
机器学习近年来已成为创意界的热门工具。诸如风格转换,t-sne,自编码器,生成对抗网络以及无数其他方法等技术已经进入了数字艺术家的工具箱。许多技术利用卷积神经网络进行特征提取和特征处理以实现他们想要获得的效果。
在频谱的另一端,递归神经网络和其他自回归模型使强大的工具可以产生逼真的时序数据。艺术家们运用这样的技术来生成文本,音乐和声音。由于缺乏可用的数据,我觉得目前缺乏重点的领域之一是矢量图形的生成。
手写是素描艺术品的一种形式。最近,我与珊卡特,伊恩·约翰逊和克里斯·奥拉他们合作在distill.pub上面发布一个在手写系列。具体来说,后期的实验帮助可视化经常性神经网络并进行训练以达到手绘效果。事实上,这个项目也是我自己的一个元实验。我并没有直接从事可视化实验和写作工作,而是着手创建一个易于使用的Javascript界面可以让其预先设定好手写模型,并让我的合作者——非常有才华的数据可视化艺术家们,试验从模型中创造出一些东西。他们结束了在distill.pub文章中创建了美丽的交互式可视化实验。
我决定写这篇文章,并提供distill.pub项目中使用的手写模型和解释,希望其他艺术家和设计师也可以利用这些技术,更深入到这个领域。
当我们写信的时候,脑海里会设想很多东西和场景。根据我们写作完成的目标,我们制定一个计划,写下我们要写的内容,选择一个合适的词汇选择,如何整理我们的笔迹,然后拿起笔,然后开始在纸垫上面决定放置笔的位置,移动笔的位置以及何时拾取笔。
要创建一个Javascript模型来模拟整个人脑来编写字母是很困难的,但是我们可以尝试通过关注手写过程的最后部分(即放置笔的位置)来模拟大脑,即如何移动它,何时拾取它。所以我们的手写只关心笔的位置,以及笔是否接触纸垫。
我们也对模型做两个假设。第一个假设是模型下一步将要写的决定只取决于过去写的内容。但是,当我们写东西的时候,在我们精确地记住了最后一个笔画的细节的时候,我们实际上并不记得我们之前写了多少笔画,而只是对写出的东西有一个模糊的概念。关于之前写的内容的这种模糊的概念事实上可以在循环神经网络的背景下建模。
使用RNN,我们可以将这种类型的模糊知识直接存储到RNN的神经元中,并将这个对象称为RNN 的隐藏状态。这个隐藏状态只是一个浮点数的向量,用来跟踪每个神经元的活动。我们的模型接下来要写什么,将取决于它的隐藏状态。这个隐藏的状态对象在写完东西之后会不断更新,所以会不断变化。我们将在下一部分演示如何工作。
关于模型的第二个假设是模型不能完全确定下一步应该写什么。事实上,模型接下来要写什么的决定是随机的。例如,模型正在写人物yyyy,它可能会决定继续写字符yyyy使得字符下端变大,或者可以决定突然结束字符并将笔移动到另一个位置。因此,我们的模型的输出结果不会是下一个要写的内容,而是下一个要写的内容的概率分布。我们需要从这个概率分布中抽样来决定接下来要写什么。
这两个假设可以总结在下面的图中,它描述了使用具有隐藏状态的递归神经网络模型来生成随机序列的过程。
生成序列模型框架
如果你不完全理解这个图,别担心。在下一节中,我们将演示使用Javascript逐行进行的操作。
我们已经预先训练了一个循环的神经网络模型来完成上一节所描述的手写任务。在本节中,我们将介绍如何在p5.js的 Javascript中使用这个模型。以下是用于手写生成的整个p5.js草图。
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; // absolute coordinates of where the pen is
var dx, dy; // offsets of the pen strokes, in pixels
另外,我们的笔并不总是触摸纸张。我们需要一个变量来称呼pen
这个变量。如果pen
为零,则我们的笔在当前时间步骤触摸纸张。我们还需要跟踪上pen
一个时间步骤的变量,并将其存储到prev_pen
。
// keep track of whether pen is touching paper. 0 or 1.
var pen;
var prev_pen; // pen at the previous timestep
如果我们有一个(dx, dy, pen)
由我们的模型在每个时间步生成的变量列表,我们就可以使用这些数据来绘制模型在屏幕上生成的内容。在开始时,所有这些变量(dx, dy, x, y, pen, prev_pen
)将被初始化为零。
我们还将定义一些将由我们的RNN模型使用的变量对象:
var rnn_state; // store the hidden states the rnn
// store all the parameters of a mixture-density distribution
var pdf;
// controls the amount of uncertainty of the model
// the higher the temperature, the more uncertainty.
var temperature = 0.65; // a non-negative number.
如前一节所述,rnn_state
变量将表示RNN 的隐藏状态。这个变量将保留所有关于什么的RNN,模糊的思想还以为它已经写在了过去。为了更新rnn_state
,我们将update
在后面的代码中使用模型中的函数。
rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
该对象rnn_state
将用于生成模型将接下来将写入的概率分布。那个概率分布将被表示为被调用的对象pdf
。为了获得pdf
对象rnn_state
,我们将在get_pdf
稍后使用这个函数,如下所示:
pdf = Model.get_pdf(rnn_state);
一个额外的变量叫temperature
我们可以控制我们不那么确定想要建立的模型。结合pdf
对象,我们可以使用sample
模型中的函数(dx, dy, pen)
从我们的概率分布中抽样下一组值。稍后我们将使用以下函数:
[dx, dy, pen] = Model.sample(pdf, temperature);
现在我们唯一需要的其他变量是控制手写的颜色,并跟踪浏览器的屏幕尺寸。
// stores the browser's dimensions
var screen_width = window.innerWidth;
var screen_height = window.innerHeight;
// colour for the handwriting
var line_color;
现在我们准备初始化所有这些我们刚才宣布的实际手写代数变量。我们将创建一个函数restart
来初始化这些变量,因为我们将在稍后重新初始化它们。
function restart() {
// set x to be 50 pixels from the left of the canvas
x = 50;
// set y somewhere in middle of the canvas
y = screen_height/2;
// initialize pen's states to zero.
dx = 0;
dy = 0;
prev_pen = 0;
// note: we draw lines based off previous pen's state
// randomise the rnn's initial hidden states
rnn_state = Model.random_state();
// randomise colour of line by choosing RGB values
line_color = color(random(255), random(255), random(255))
}
创建restart
函数后,我们可以定义通常的p5.js setup
函数来初始化草图。
function setup() {
restart(); // initialize variables for this demo
createCanvas(screen_width, screen_height);
frameRate(60); // 60 frames per second
// clear the background to be blank white colour
background(255);
fill(255);
}
我们的手写生成将发生在p5.js框架的draw
功能中。这个功能被称为每秒60次。每次调用这个函数时,RNN都会在屏幕上画一些东西。
function draw() {
// using the previous pen states, and hidden state
// to get next hidden state
rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
// get the parameters of the probability distribution
// from the hidden state
pdf = Model.get_pdf(rnn_state);
// sample the next pen's states
// using our probability distribution and temperature
[dx, dy, pen] = Model.sample(pdf, temperature);
// only draw on the paper if pen is touching the paper
if (prev_pen == 0) {
// set colour of the line
stroke(line_color);
// set width of the line to 2 pixels
strokeWeight(2.0);
// draw line connecting prev point to current point.
line(x, y, x+dx, y+dy);
}
// update the absolute coordinates from the offsets
x += dx;
y += dy;
// update the previous pen's state
// to the current one we just sampled
prev_pen = pen;
// if the rnn starts drawing close to the right side
// of the screen, restart our demo
if (x > screen_width - 50) {
restart();
// reset screen
background(255);
fill(255);
}
}
在每一帧,该draw
功能将根据之前在屏幕上绘制的内容更新模型的隐藏状态。从这个隐藏的状态,模型将产生下一个将会产生的概率分布。基于这个概率分布,随着temperature
参数,我们将以一组新(dx, dy, pen)
变量的形式随机抽样采取什么行动。基于这组新的变量,如果笔先前触摸了纸垫,它将在屏幕上画一条线,并更新笔的全局位置。一旦笔的全局位置靠近屏幕的右侧,它将重置草图并重新开始。
把所有这些放在一起,我们得到下面的手写生成草图。
所以你有在使用Javascript的几行中的Web浏览器吧,笔迹代p5.js。
该变量pdf
应该存储每个时间步下一笔笔划的概率分布。pdf
实际上,对象实际上只包含一个复杂概率分布的参数(即一组正态分布的均值和标准差)。我们选择的概率分布模型dx
,并dy
为混合密度分布。
但究竟什么是一个混合密度分布?统计学家(数据科学家)喜欢用众所周知的数学上易于处理的分布(例如正态分布)对概率分布进行建模,并试图确定分布的参数(例如正态分布的均值和标准偏差)适合数据。但是,在处理复杂的事情时,如笔迹数据的笔触,我们发现简单的正态分布不足以模拟数据。直观上,手写笔画要么保持靠近前一个位置,要么在字或字符完成时跳到另一个位置。
处理这个问题的直接方法是将概率分布建模为许多正态分布的总和。在我们的例子中,我们将手写笔画建模为20个正态分布的和。使用20个正态分布的混合,我们的模型可以为实际手写数据建模做一个好的工作。更多的技术细节可以在这篇文章中获得。
当我们采用这个概率分布,并从这个分布中抽样得到一组(dx, dy, pen)
值来确定下一步要画什么时,我们使用这个temperature
参数来控制模型的不确定性水平。如果温度参数非常高,那么我们更有可能在概率分布的不太可能的区域内获得样本。如果温度参数非常低或接近于零,那么我们只会从分布的最可能的部分获得样本。
在下面的草图中,你可以通过改变温度参数来可视化概率分布如何增加。你也可以通过拖动顶部的橙色条来控制温度参数。
通过调整温度来实现可视化混合密度分布。
为了简单起见,上述演示模拟了具有温度参数的二十个一维正态分布的混合。在手写模型中,概率分布是二十个二维正态分布的混合。在下一个草图中,你可以在写字时修改手写模型的温度,以查看手写如何随温度变化而变化。
当温度保持低时,手写模型变得非常确定,所以手写通常更整齐,更逼真。增加温度将增加选择概率分布的可能性较小的可能性,所以手写样本将趋于更加时髦和不确定。
机器学习与设计相结合的一个更有趣的方面是探索人机交互。典型的机器学习框架+ python堆栈使得部署真正的交互式Web应用程序变得困难,因为他们通常需要在服务器端写入专用的Web服务来处理用户端的用户交互。关于Javascript框架(如p5.js)的好处是交互式编程可以轻松完成,并且可以在Web浏览器中轻松部署。
我们可以从基本手写演示中创建一个可能的交互式扩展,即让用户交互式地将一些手写输入到屏幕上,当用户空闲时,让模型持续预测其余的笔迹样本。另一个我们可以建立的扩展,类似于distill.pub文章,是让模型采样符合用户创建的手写路径的多个可能的路径。
其实这里有无数其他的可能性可以用这个模型来实验。将此模型与更高级的框架(如paper.js或d3.js)结合起来以产生更好看的笔触。
如果您是一位对机器学习感兴趣的艺术家或设计师,您可以将包含用于此文章的代码的github存储库分叉,并根据自己的喜好使用它。
这篇文章只是抓了复发神经网络的表面。如果你想更多地参与整个机器学习的开发过程并且训练自己的模型,那么有很多优秀的资源来学习如何用TensorFlow或者keras来建立模型。如果您使用keras来构建和训练模型,甚至可以使用一个名为keras.js的工具来导出预先训练好的模型,以便使用Web浏览器,因此您可以创建模型接口,如本文中使用的Javascript手写模型。我还没有亲自使用过keras.js,而且我发现在Javascript中从头开始编写手写模型非常有趣。
更新: