375
京东网上商城
通过SketchRNN、PCA和t-SNE从Google QuickDraw数据集中显示矢量图的潜在空间|附源码
本文是作者最近发布的Google QuickDraw数据集一系列笔记中的第三部分,使用的最近发布的SketchRNN模型。下面介绍QuickDraw数据集及SketchRNN模型。
QuickDraw数据集是由世界各地1500多万人参与的“快速绘画” AI实验后收集数百万幅图画建成,要求参与者在20秒内绘制出属于某个类(例如“猫”)的图像。如下图所示,选择一个类别,可以看见数据库中该类所有的形状。
当然,如果数据集中没有和你想象一样的形状,你也可以帮助向该数据库中增添一些涂鸦,进行个人绘画表演。


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


本文是笔记与代码的结合,作者已经做出了风格以及其它一些细微的改变,以确保Python3能向前兼容。
- 1. 本文有点令人误解,这是因为本文主要是探索Aaron Koblin羊市场 (aaron-sheep)数据集,这是一个较小的轻量级数据集,以及一个手册,演示了在这个数据集上已经预先训练好的各种模型。由于该数据集模式与QuickDraw数据集相同,因此在此数据集上执行的实验也不失一般性。
- 2. Magenta目前只支持Python 2版本。
接下来都是实验的所需的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 = ('https://github.com/hardmaru/sketch-rnn-datasets/'
'raw/master/aaron_sheep/')
MODELS_ROOT_DIR = '/tmp/sketch_rnn/models'
在代码块[8]中:
DATA_DIR
'https://github.com/hardmaru/sketch-rnn-datasets/raw/master/aaron_sheep/'
在代码块[9]中:
PRETRAINED_MODELS_URL
输出[9]:
'https://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 https://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)
用主成分分析探索潜在空间
下面,我们将测试集中的所有草图编码为学习到的128维潜在空间中的表示。
在代码块[20]中:
Z = np.vstack(map(encode, test_set.strokes))
Z.shape
输出[20]:
300,128 128
然后,找到潜在空间中编码数据中代表最大方差方向的两个主轴。
在代码块[22]中:
pca = PCA(n_components=2)
在代码块[23]中:
pca.fit(Z)
输出[23]:
PCAsvd_solver ='auto'
这两个组成部分分别约占方差的2%
在代码块[24]中:
pca.explained_variance_ratio_
输出[24]:
[0.02140247
接下来将数据从128维的潜在空间映射为由前两个主要分量构建的二维空间
在代码块[25]中:
Z_pca = pca.transform(Z)
Z_pca.shape
输出[25]:
300,2 2
在代码块[26]中:
fig, ax = plt.subplots()
ax.scatter(*Z_pca.T)
ax.set_xlabel('$pc_1$')
ax.set_ylabel('$pc_2$')
plt.show()
我们想在上图中通过图中的对应点可视化出原始草图,其中每个点对应于草图降维到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()
在代码块[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)
可以在这里找到SVG图像。
备注:一个更加聪明的方法涉及到使用matplotlib变换和Collections API,具体是通过实例化带有关键参数oofets的PathCollection。
在先前定义的矩形区域生成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()
接下来,通过应用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()
可以从上图中观察到一些有趣的变换和模式。在右下角,可以看到一群类似的修剪过或者无修饰的羊;当向左上方移动时,会开始看到一些羊毛、粗糙的圆形涂鸦的羊;沿着中间部分,可以看到最逼真的羊;在左上角,可以看到一些抽象的草图。
在代码块[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()
特征羊分解
本文将草图学习的潜在代表主要组成部分称为特征羊。
使用译码器,可以可视化前2个特征羊的草图表示,这些特征羊是将潜在表示转换为二维子空间中的正交权重矢量。通过将它们视为隐藏代码,并将其重构建为草图,我们能够提炼出一些有意义的解释。
在代码块[48]中:
z_pc1, z_pc2 = pca.components_
在代码块[49]中:
pca.explained_variance_ratio_
输出[49]:
[0.02140247
在代码块[50]中:
pca.explained_variance_
输出[50]:
[2.51394317
我们从τ=.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()
在代码块[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()
对于第一特征羊,在τ=0.1,主要看到一个圆形的黑色头发、有两条长的圆形腿和一些尾巴。沿着轴线方向可以看到,羊的头部和腿部结构会发生变化,这占据了草图的大部分差异。
现在看看第二特征羊,集中τ值较低时生成的样品,可以看到一些粗略潦草的圆圈代表羊的身体、3-4个松散连接的腿和一个小圆头。从上到下的观察,可以发现,与第一特征不同的是这似乎是沿着羊身体结构变化的方向。
t分布随机相邻嵌入(t-SNE)是一种常用的非线性降维技术,通常用于可视化高维数据。它是几种嵌入方法之一,其目的是将数据点嵌入到较低维空间中,以保持原始高维空间中的点对点的距离,一般适用于二维空间的可视化。
特别地,t-SNE通常用于深度学习,以检查和可视化深层神经网络学到的内容。例如,在图像分类问题中,可以将卷积神经网络视为一系列变换,逐渐将图像转换为表示,使得类可以更容易地被线性分类器区分。因此,可以将分类器前的最后一层输出作为“代码”,然后使用t-SNE在二维中嵌入和可视化图像的代码表示。
- 1.关于PCA与t-SNE的详细比较可以查看笔者先前的博客。
- 2.Wattenberg, et al.“How to Use t-SNE Effectively”, Distill, 2016.
- 3.斯坦福大学CS231n:卷积神经网络视觉识别课程中关于网络的可视化课程笔记。
在这里,将每个矢量绘图的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()
在代码块[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()
在代码块[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")
完整的SVG图片可以在此获得。虽然这产生了很好的结果,但作者认为如果可视化在一个具有多类的更大数据集上会更有说服力。通过这种方式,可以看到t-SNE有效地形成了对不同类的聚类图,并突出显示了每个集群中的变化。
- 1. Visualizing MNIST
- 2. Visualizing Representations
- 3. Latent space visualization
- 4. Analyzing 50k fonts using deep neural networks
- 5. Organizing the World of Fonts with AI
作者信息
Louis Tiao:计算机科学和数学的研究者,研究领域集中于算法设计与分析、计算机科学理论、人工智能和机器学习。
Linkedin:https://www.linkedin.com/in/ltiao/?ppe=1
Github:https://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,译者:海棠,审阅:阿福
最后更新:2017-06-10 23:32:03