通过SketchRNN、PCA和t-SNE从Google QuickDraw数据集中显示矢量图的潜在空间|附源码

  1. 云栖社区>
  2. 博客>
  3. 正文

通过SketchRNN、PCA和t-SNE从Google QuickDraw数据集中显示矢量图的潜在空间|附源码

【方向】 2017-06-10 22:13:06 浏览9425
展开阅读全文

更多深度文章,请关注:https://yq.aliyun.com/cloud

79b373d51eb109fd75ac6dd612fe1d96b9107ad9

本文是作者最近发布的Google QuickDraw数据集一系列笔记的第三部分,使用的最近发布的SketchRNN模型。下面介绍QuickDraw数据集及SketchRNN模型。
        QuickDraw数据集是由世界各地1500多万人参与的“快速绘画” AI实验后收集数百万幅图画建成,要求参与者在20秒内绘制属于某个类(例如“猫”)的图像如下图所示,选择一个类别,可以看见数据库中该类所有的形状。

2fe2089ec0ed5b84a527913712fa475ca9591aea

当然,如果数据集中没有和你想象一样的形状,你也可以帮助向该数据库中增添一些涂鸦,进行个人绘画表演。

25e02e433cc92c54480a29cf266faf369ffa9b74 49417fcbdccba38dd7d1d93ee3ca314c04d4d095
        SketchRNN是基于上述数据集训练的生成模型,被训练成能够生成矢量它巧妙地集合了机器学习中最近开发的许多最新的工具和技术,例如Variational AutoencodersHyperLSTMs(一个用于LSTM的HyperNetwork)自回归模型 ,Layer NormalizationRecurrent DropoutAdam optimizer

        SketchRNN系统是由谷歌探究AI能否创作艺术的新项目的一部分,类似于教AI去绘画,另外不仅仅是让人工智能学习如何画画,还要能够“用类似于人类的方式概括归纳抽象的概念”,比如去画“猪”的广义概念,而不是画特定的动物,这显然不是一件简单的事情(下图 SketchRNN系统画的“猪”)。

70a2d2c1d429e95023c9ba0b0674aa0373548164

该模型能够将你的灵魂画作对应成实际的物品,当然,如果画作太过于抽象,暂时是无法识别或者是识别错误,毕竟它还没有学会读心术(自己画的自行车,系统无法识别)。

e6f82e43b014419c1744cd5f513148c9528ca3762bbd775563fe7e0fa81f490ff7055730bef5f2aa
       本文代码舍弃了那些旨在解释或演示代码块,只保留运行实验所需的代码。“潜在空间中的主成分分析 ”部分的所有内容直接从以前的博客摘取。随意跳过该部分,因为接下来是真正有趣的分析。这里是第一第二博客的链接之前所讲述的一切都是一些实用功能,以便于现在的可视化分析

        本文是笔记代码的结合,作者已经做出了风格以及其它一些细微的改变,以确保Python3向前兼容

  • 1. 本文有点令人误解这是因为本文主要是探索Aaron Koblin羊市场 aaron-sheep数据集,这是一个较小的轻量级数据集,以及一个手册,演示了在这个数据集已经预先训练的各种模型。由于数据集模式与QuickDraw数据集相同,因此在此数据集上执行的实验也不失一般性。
  • 2. Magenta目前只支持Python 2版本

260a4122bad0cd267dd1ecc681f4616f03269718

接下来都是实验的所需的python代码,其中[num]是表示该github工程文件的代码块:
在代码块[2]中:


%matplotlib inline
%config InlineBackend.figure_format = 'svg'
%load_ext autoreload
%autoreload 2

在代码块[3]中


import matplotlib.pyplot as plt
import matplotlib.patches as patches

import numpy as np
import tensorflow as tf

from matplotlib.animation import FuncAnimation
from matplotlib.path import Path
from matplotlib import rc

from sklearn.decomposition import PCA
from sklearn.manifold import TSNE

from itertools import product
from six.moves import map, zip

在代码块[5]中:


from magenta.models.sketch_rnn.sketch_rnn_train import \
    (load_env,
     load_checkpoint,
     reset_graph,
     download_pretrained_models,
     PRETRAINED_MODELS_URL)
from magenta.models.sketch_rnn.model import Model, sample
from magenta.models.sketch_rnn.utils import (lerp,
                                             slerp,
                                             get_bounds, 
                                             to_big_strokes,
                                             to_normal_strokes)

在代码块[6]中:


# For inine display of animation
# equivalent to rcParams['animation.html'] = 'html5'
rc('animation', html='html5')

在代码块[5]中:


# set numpy output to something sensible
np.set_printoptions(precision=8, 
                    edgeitems=6, 
                    linewidth=200, 
                    suppress=True)

在代码块[6]中:


tf.logging.info("TensorFlow Version: {}".format(tf.__version__))

INFO:tensorflow:TensorFlow版本:1.1.0

获得预训练的模型和数据

在代码块[7]中:


DATA_DIR = ('http://github.com/hardmaru/sketch-rnn-datasets/'
            'raw/master/aaron_sheep/')
MODELS_ROOT_DIR = '/tmp/sketch_rnn/models'

在代码块[8]中:


DATA_DIR

输出[8]:

'http://github.com/hardmaru/sketch-rnn-datasets/raw/master/aaron_sheep/'

在代码块[9]中:

PRETRAINED_MODELS_URL

输出[9]:

'http://download.magenta.tensorflow.org/models/sketch_rnn.zip'

在代码块[10]中:

download_pretrained_models(
    models_root_dir=MODELS_ROOT_DIR,
    pretrained_models_url=PRETRAINED_MODELS_URL)


接下来让我们看看aaron_sheep现在数据集训练的规范化层模型。

在代码块[11]中:


MODEL_DIR = MODELS_ROOT_DIR + '/aaron_sheep/layer_norm'

在代码块[12]中:


(train_set, 
 valid_set, 
 test_set, 
 hps_model, 
 eval_hps_model, 
 sample_hps_model) = load_env(DATA_DIR, MODEL_DIR)

INFO:tensorflow:Downloading http://github.com/hardmaru/sketch-rnn-datasets/raw/master/aaron_sheep/aaron_sheep.npz
INFO:tensorflow:Loaded 7400/300/300 from aaron_sheep.npz
INFO:tensorflow:Dataset combined: 8000 (7400/300/300), avg len 125
INFO:tensorflow:model_params.max_seq_len 250.
total images <= max_seq_len is 7400
total images <= max_seq_len is 300
total images <= max_seq_len is 300
INFO:tensorflow:normalizing_scale_factor 18.5198.

在代码块[13]中:


class SketchPath(Path):
    
    def __init__(self, data, factor=.2, *args, **kwargs):
        
        vertices = np.cumsum(data[::, :-1], axis=0) / factor
        codes = np.roll(self.to_code(data[::,-1].astype(int)), 
                        shift=1)
        codes[0] = Path.MOVETO

        super(SketchPath, self).__init__(vertices, 
                                         codes, 
                                         *args, 
                                         **kwargs)
        
    @staticmethod
    def to_code(cmd):
        # if cmd == 0, the code is LINETO
        # if cmd == 1, the code is MOVETO (which is LINETO - 1)
        return Path.LINETO - cmd

在代码块[14]中:


def draw(sketch_data, factor=.2, pad=(10, 10), ax=None):

    if ax is None:
        ax = plt.gca()

    x_pad, y_pad = pad
    
    x_pad //= 2
    y_pad //= 2
        
    x_min, x_max, y_min, y_max = get_bounds(data=sketch_data,
                                            factor=factor)

    ax.set_xlim(x_min-x_pad, x_max+x_pad)
    ax.set_ylim(y_max+y_pad, y_min-y_pad)

    sketch = SketchPath(sketch_data)

    patch = patches.PathPatch(sketch, facecolor='none')
    ax.add_patch(patch)

加载预先训练好的模型

在代码块[15]中:


# construct the sketch-rnn model here:
reset_graph()
model = Model(hps_model)
eval_model = Model(eval_hps_model, reuse=True)
sample_model = Model(sample_hps_model, reuse=True)

INFO:tensorflow:Model using gpu.
INFO:tensorflow:Input dropout mode = False.
INFO:tensorflow:Output dropout mode = False.
INFO:tensorflow:Recurrent dropout mode = True.
INFO:tensorflow:Model using gpu.
INFO:tensorflow:Input dropout mode = False.
INFO:tensorflow:Output dropout mode = False.
INFO:tensorflow:Recurrent dropout mode = False.
INFO:tensorflow:Model using gpu.
INFO:tensorflow:Input dropout mode = False.
INFO:tensorflow:Output dropout mode = False.
INFO:tensorflow:Recurrent dropout mode = False.

在代码块[16]中:


sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())

在代码块[17]中:


# loads the weights from checkpoint into our model
load_checkpoint(sess=sess, checkpoint_path=MODEL_DIR)

INFO:tensorflow:Loading model /tmp/sketch_rnn/models/aaron_sheep/layer_norm/vector.
INFO:tensorflow:Restoring parameters from /tmp/sketch_rnn/models/aaron_sheep/layer_norm/vector

在代码块[18]中:


def encode(input_strokes):
    strokes = to_big_strokes(input_strokes).tolist()
    strokes.insert(0, [0, 0, 1, 0, 0])
    seq_len = [len(input_strokes)]
    z = sess.run(eval_model.batch_z,
                 feed_dict={
                    eval_model.input_data: [strokes], 
                    eval_model.sequence_lengths: seq_len})[0]
    return z

在代码块[19]中:


def decode(z_input=None, temperature=.1, factor=.2):
    z = None
    if z_input is not None:
        z = [z_input]
    sample_strokes, m = sample(
        sess, 
        sample_model, 
        seq_len=eval_model.hps.max_seq_len, 
        temperature=temperature, z=z)
    return to_normal_strokes(sample_strokes)

用主成分分析探索潜在空间

PCA是一种降维方法,是通过线性变换将原始数据变换为一组各维度线性无关的表示,可用于提取数据的主要特征分量,常用于高维数据的降维。

下面,我们将测试集中的所有草图编码为学习的128维潜在空间中的表示。

在代码块[20]中:


Z = np.vstack(map(encode, test_set.strokes))
Z.shape

输出[20]:

300,128 注:128代表128

然后,找到潜在空间中编码数据中代表最大方差方向的两个主轴。 

在代码块[22]中:

pca = PCA(n_components=2)

在代码块[23]中:

pca.fit(Z)

输出[23]:

PCAcopy = Trueiterated_power ='auto'n_components = 2random_state = Nonesvd_solver ='auto'tol = 0.0whiten = False 

这两个组成部分分别方差的2% 

在代码块[24]中:




pca.explained_variance_ratio_

输出[24]:

[0.021402470.02067117] 

接下来将数据从128维潜在空间映射为由前两个主要分量构建的二维空间 

在代码块[25]中:



Z_pca = pca.transform(Z)
Z_pca.shape

出[25]:

300,2 注:2代表2

在代码块[26]中:


fig, ax = plt.subplots()

ax.scatter(*Z_pca.T)

ax.set_xlabel('$pc_1$')
ax.set_ylabel('$pc_2$')

plt.show()

10074f8ae3f998247196cc8b710294b002a94514

我们想在上图中通过图中的对应点可视化原始草图,其中每个点对应于草图降维到2维的隐藏代码。然而,由于图像太密集,在没有重叠的情况下无法适应足够大的草图。因此,我们将注意力限制在包含80%数据点的较小区域,蓝色阴影矩形突出显示感兴趣的区域。 

在代码块[106]中:


((pc1_min, pc2_min), 
 (pc1_max, pc2_max)) = np.percentile(Z_pca, q=[5, 95], axis=0)

在代码块[107]中:


roi_rect = patches.Rectangle(xy=(pc1_min, pc2_min),
                             width=pc1_max-pc1_min,
                             height=pc2_max-pc2_min, alpha=.4)

在代码块[108]中:


fig, ax = plt.subplots()

ax.scatter(*Z_pca.T)
ax.add_patch(roi_rect)

ax.set_xlabel('$pc_1$')
ax.set_ylabel('$pc_2$')

plt.show()

501b3b92fec57aa1697920cf662a9eedb199614a

在代码块[109]中:


fig, ax = plt.subplots(figsize=(10, 10))

ax.set_xlim(pc1_min, pc1_max)
ax.set_ylim(pc2_min, pc2_max)

for i, sketch in enumerate(test_set.strokes):
    sketch_path = SketchPath(sketch, factor=7e+1)
    sketch_path.vertices[::,1] *= -1
    sketch_path.vertices += Z_pca[i]
    patch = patches.PathPatch(sketch_path, facecolor='none')
    ax.add_patch(patch)

ax.set_xlabel('$pc_1$')
ax.set_ylabel('$pc_2$')
    
plt.savefig("../../files/sketchrnn/aaron_sheep_pca.svg", 
            format="svg", dpi=1200)

0bb5ce7f30aadc771cb1f9cb68fcb559424ff2cd

可以在这里找到SVG图像。

备注:一个更加聪明的方法涉及使用matplotlib变换Collections API,具体是通过实例化带有关键参数oofets的PathCollection

PCA投影中的线性插值
在先前定义的矩形区域生成100个均匀间隔的网格,下图网格中的点以橙色表示,覆盖在测试数据点的顶部。 

在代码块[35]中:


pc1 = lerp(pc1_min, pc1_max, np.linspace(0, 1, 10))

在代码块[36]中:


pc2 = lerp(pc2_min, pc2_max, np.linspace(0, 1, 10))

在代码块[37]中:


pc1_mesh, pc2_mesh = np.meshgrid(pc1, pc2)

在代码块[38]中:


fig, ax = plt.subplots()

ax.set_xlim(pc1_min-.5, pc1_max+.5)
ax.set_ylim(pc2_min-.5, pc2_max+.5)

ax.scatter(*Z_pca.T)
ax.scatter(pc1_mesh, pc2_mesh)

ax.set_xlabel('$pc_1$')
ax.set_ylabel('$pc_2$')

plt.show()

ca59cf88c62e628fdbae5b1724e3a83aee2322d8

接下来,通过应用PCA的逆变换来将网格上的100个点投影到原始的128维潜在空间。 

在代码块[39]中:


np.dstack((pc1_mesh, pc2_mesh)).shape

输出[39]:

10102 

在代码块[40]中:


z_grid = np.apply_along_axis(pca.inverse_transform,  
                             arr=np.dstack((pc1_mesh, pc2_mesh)),
                             axis=2)
z_grid.shape

输出[40]:

1010128 

然后,使用这些隐藏代码及译码器重建相应的草图,并观察草图如何在所关注的矩形区域范围之间转换特别是草图从左到右从上到下转换,因为这些是潜在表示中最大差异的方向。首先,以较低的温度设置τ=0.1运行译码器以最小化样本的随机性。 

在代码块[94]中:


fig, ax_arr = plt.subplots(nrows=10, 
                           ncols=10, 
                           figsize=(8, 8),
                           subplot_kw=dict(xticks=[],
                                           yticks=[],
                                           frame_on=False))
fig.tight_layout()

for i, ax_row in enumerate(ax_arr):    
    for j, ax in enumerate(ax_row):
        draw(decode(z_grid[-i-1,j], temperature=.1), ax=ax)
        ax.axis('off')
    
plt.show()

fd1df68981c9dbc48f39d82545322415f64ef174

可以从上图中观察一些有趣的变换和模式。右下角可以看到一群类似的修剪过或者无修饰的当向左上方移动时,开始看到一些羊毛粗糙的圆形涂鸦的羊;沿着中间部分可以看到最逼真的羊在左上角,可以看到一些抽象的草图

在代码块[95]中:


fig, ax_arr = plt.subplots(nrows=10, 
                           ncols=10, 
                           figsize=(8, 8),
                           subplot_kw=dict(xticks=[],
                                           yticks=[],
                                           frame_on=False))
fig.tight_layout()

for i, ax_row in enumerate(ax_arr):    
    for j, ax in enumerate(ax_row):
        draw(decode(z_grid[-i-1,j], temperature=.6), ax=ax)
        ax.axis('off')
    
plt.show()

3460dfd54dfbfb4243327af44b3460796f9dc624

特征羊分解
本文将草图学习潜在代表主要组成部分称为特征羊

使用译码器,可以可视化前2个特征羊的草图表示这些特征羊是将潜在表示转换为二维子空间的正交权重矢量。通过将它们视为隐藏代码,并将其重构建草图,我们能够提炼出一些有意义的解释。 

在代码块[48]中:


z_pc1, z_pc2 = pca.components_

在代码块[49]中:


pca.explained_variance_ratio_

输出[49]:

[0.021402470.02067117] 

在代码块[50]中:

pca.explained_variance_

输出[50]:

[2.513943172.42804484]

我们从τ=.01...1.0增加温度绘制特征羊的重建草图每个温度设置采集5个样品。 

在代码块[58]中:

fig, ax_arr = plt.subplots(nrows=5, 
                           ncols=10, 
                           figsize=(8, 4),
                           subplot_kw=dict(xticks=[],
                                           yticks=[],
                                           frame_on=False))
fig.tight_layout()

for row_num, ax_row in enumerate(ax_arr):    
    for col_num, ax in enumerate(ax_row):
        t = (col_num + 1) / 10.
        draw(decode(z_pc1, temperature=t), ax=ax)

        if row_num + 1 == len(ax_arr):
            ax.set_xlabel(r'$\tau={}$'.format(t))
        
plt.show()

a74dcd0ce663c22315214a172fc0d4ce2fa8a595

在代码块[63]中:

fig, ax_arr = plt.subplots(nrows=5, 
                           ncols=10, 
                           figsize=(8, 4),
                           subplot_kw=dict(xticks=[],
                                           yticks=[],
                                           frame_on=False))
fig.tight_layout()

for row_num, ax_row in enumerate(ax_arr):    
    for col_num, ax in enumerate(ax_row):
        t = (col_num + 1) / 10.
        draw(decode(z_pc2, temperature=t), ax=ax)

        if row_num + 1 == len(ax_arr):
            ax.set_xlabel(r'$\tau={}$'.format(t))
        
plt.show()

2da9d11183046ef318eb401fc5507fe5095484f2

对于第一特征羊,在τ=0.1主要看到一个圆形的黑色头发有两条长的圆形腿一些尾巴。沿着轴线方向可以看到,羊头部和腿部结构会发生变化,这占据了草图的大部分差异。

现在看看第二特征羊,集中τ值较低时生成的样品,可以看到一些粗略潦草的圆圈代表羊的身体3-4个松散连接的腿和一个小圆头。从上到下的观察,可以发现,与第一特征不同的是这似乎是沿着羊身体结构变化的方向。

t-SNE可视化
     t分布随机相邻嵌入(t-SNE)是一种常用的非线性降维技术,通常用于可视化高维数据。它是几种嵌入方法之一,其目的是将数据点嵌入到较低维空间中,以保持原始高维空间中的点对点的距离,一般适用于二维空间的可视化

     特别地,t-SNE通常用于深度学习,以检查和可视化深层神经网络学到的内容。例如,在图像分类问题中,可以将卷积神经网络视为一系列变换,逐渐将图像转换为表示,使得类可以更容易地被线性分类器区分。因此,可以分类器前的最后一层输出作为“代码”然后使用t-SNE在二维中嵌入和可视化图像代码表示。

在这里,将每个矢量绘图的128维隐藏代码嵌入到二维中,并将其在该二维子空间中进行可视化。 

在代码块[97]中:

tsne = TSNE(n_components=2, n_iter=5000)

在代码块[98]中:

Z_tsne = tsne.fit_transform(Z)

在代码块[99]中:

fig, ax = plt.subplots()

ax.scatter(*Z_tsne.T)

ax.set_xlabel('$c_1$')
ax.set_ylabel('$c_2$')

plt.show()

36da666fa84c604b892bab2a677a61dc24e1cb21

在代码块[100]中:

tsne.kl_divergence_

输出[100]:

2.2818214893341064 

像以前一样,定义一个矩形区域 

在代码块[101]中:

((c1_min, c2_min), 
 (c1_max, c2_max)) = np.percentile(Z_tsne, q=[5, 95], axis=0)

在代码块[102]中:

roi_rect = patches.Rectangle(xy=(c1_min, c2_min),
                             width=c1_max-c1_min,
                             height=c2_max-c2_min, alpha=.4)

在代码块[103]中:

fig, ax = plt.subplots()

ax.scatter(*Z_tsne.T)
ax.add_patch(roi_rect)

ax.set_xlabel('$c_1$')
ax.set_ylabel('$c_2$')

plt.show()

526d1649c235f1118a570d29a9d2143cdca0592a

在代码块[104]中:

fig, ax = plt.subplots(figsize=(10, 10))

ax.set_xlim(c1_min, c1_max)
ax.set_ylim(c2_min, c2_max)

for i, sketch in enumerate(test_set.strokes):
    sketch_path = SketchPath(sketch, factor=2.)
    sketch_path.vertices[::,1] *= -1
    sketch_path.vertices += Z_tsne[i]
    patch = patches.PathPatch(sketch_path, facecolor='none')
    ax.add_patch(patch)

ax.axis('off')

plt.savefig("../../files/sketchrnn/aaron_sheep_tsne.svg", 
            format="svg")

f3e240d59e9d21aa95b8ce2ee27d63d13b22c369

完整的SVG图片可以在此获得。虽然这产生了很好的结果,但作者认为如果可视化在一个具有多类的更大数据集上会更有说服力。通过这种方式,可以看到t-SNE有效地形成了对不同类的聚类图,并突出显示了每个集群中的变化。

如果是RGB光栅图,使用像t-SNE这样的技术会获得很奇特的结果(比如多个头的小狗等)。然而,相似之处往往是表面的,主要是基于色度、亮度等方面。当嵌入代码表示时,相似性变得更加抽象,并且往往是基于类和语义。此外,这也允许我们能够检测出神经网络形成了什么样的相似性概念。

88d0d453f9aa1d6741a1f480689abf3b1cf832c3

在另一方面,矢量图是由连续的笔画组成,能被表示成笛卡尔平面上的偏移量序列。一般来说,任意长度序列之间的相似性度量并没有得到很好地定义。因此,不能直接在矢量图上应用降维技术。

使用变形的递归神经网络,比如SketchRNN,可以将原始图转换为潜在空间中丰富的表示,这样做可以使得我们运用强大的技术如t-SNE去组织并可视化这些表示。另外利用相似性能够提炼更多的语义和抽象的概念。

本文的工作是受到其它一些优秀的文章和工作的启发,如下所示:

作者信息

Louis Tiao计算机科学和数学的研究者,研究领域集中于算法设计与分析、计算机科学理论、人工智能和机器学习

Linkedin:http://www.linkedin.com/in/ltiao/?ppe=1

Githubhttps://github.com/scikit-learn/scikit-learn

本文由北邮@爱可可-爱生活老师推荐,阿里云云栖社区组织翻译。

文章原标题《Visualizing the Latent Space of Vector Drawings from the Google QuickDraw Dataset with SketchRNN, PCA and t-SNE》,作者:Louis Tiao,译者:海棠,审阅:阿福 

附件为原文的pdf

文章为简译,更为详细的内容,请查看原文

附件下载: Visualiz...[【方向】].1497104040.pdf

网友评论

登录后评论
0/500
评论
【方向】
+ 关注