机器学习模型可以对图像、音乐和故事的统计潜在空间(latent space)进行学习,而后从这个空间中采样(sample),创造出与模型在训练数据中所见到的艺术做品具备类似特征的新做品html
生成序列数据python
用深度学习生成序列数据的通用方法,就是使用前面的标记做为输入,训练一个网络(一般是循环神经网络或卷积神经网络)来预测序列中接下来的一个或多个标记。例如,给定输入the cat is on the ma,训练网络来预测目标 t,即下一个字符。与前面处理文本数据时同样,标记(token)一般是单词或字符,给定前面的标记,可以对下一个标记的几率进行建模的任何网络都叫做语言模型(language model)。语言模型可以捕捉到语言的潜在空间(latent space),即语言的统计结构git
一旦训练好了这样一个语言模型,就能够从中采样(sample,即生成新序列)。向模型中输入一个初始文本字符串[即条件数据(conditioning data)],要求模型生成下一个字符或下一个单词(甚至能够同时生成多个标记),而后将生成的输出添加到输入数据中,并屡次重复这一过程。这个循环能够生成任意长度的序列,这些序列反映了模型训练数据的结构,它们与人类书写的句子几乎相同算法
使用语言模型逐个字符生成文本的过程
数组
采样策略网络
生成文本时,如何选择下一个字符相当重要。一种简单的方法是贪婪采样(greedy sampling),就是始终选择可能性最大的下一个字符。但这种方法会获得重复的、可预测的字符串,看起来不像是连贯的语言。一种更有趣的方法是作出稍显意外的选择:在采样过程当中引入随机性,即从下一个字符的几率分布中进行采样。这叫做随机采样(stochastic sampling,stochasticity 在这个领域中就是“随机”的意思)。在这种状况下,根据模型结果,若是下一个字符是 e 的几率为0.3,那么你会有 30% 的几率选择它架构
从模型的 softmax 输出中进行几率采样是一种很巧妙的方法,它甚至能够在某些时候采样到不常见的字符,从而生成看起来更加有趣的句子,并且有时会获得训练数据中没有的、听起来像是真实存在的新单词,从而表现出创造性。但这种方法有一个问题,就是它在采样过程当中没法控制随机性的大小app
为了在采样过程当中控制随机性的大小,咱们引入一个叫做 softmax 温度(softmax temperature)的参数,用于表示采样几率分布的熵,即表示所选择的下一个字符会有多么出人意料或多么可预测。给定一个 temperature 值,将按照下列方法对原始几率分布(即模型的 softmax 输出)进行从新加权,计算获得一个新的几率分布dom
import numpy as np def reweight_distribution(original_distribution, temperature=0.5): #original_distribution 是几率值组成的一维 Numpy 数组,这些几率值之和必须等于 1。temperature 是一个因子,用于定量描述输出分布的熵 distribution = np.log(original_distribution) / temperature distribution = np.exp(distribution) return distribution / np.sum(distribution)
更高的温度获得的是熵更大的采样分布,会生成更加出人意料、更加无结构的生成数据,而更低的温度对应更小的随机性,以及更加可预测的生成数据机器学习
对同一个几率分布进行不一样的从新加权。更低的温度 = 更肯定,更高的温度 = 更随机
实现字符级的 LSTM 文本生成
首先下载语料,并将其转换为小写。接下来,咱们要提取长度为 maxlen 的序列(这些序列之间存在部分重叠),对它们进行one-hot 编码,而后将其打包成形状为 (sequences, maxlen, unique_characters) 的三维Numpy 数组。与此同时,还须要准备一个数组 y,其中包含对应的目标,即在每个所提取的序列以后出现的字符 ,下一步,构建网络。最后训练语言模型并从中采样
给定一个训练好的模型和一个种子文本片断,咱们能够经过重复如下操做来生成新的文本
demo
import keras import numpy as np from keras import layers import random import sys path = keras.utils.get_file('nietzsche.txt', origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt') # 将语料转为小写 text = open(path).read().lower() print('Corpus length:', len(text)) maxlen = 60 step = 3 sentences = [] next_chars = [] for i in range(0, len(text) - maxlen, step): sentences.append(text[i: i + maxlen]) next_chars.append(text[i + maxlen]) print('Number of sequences:', len(sentences)) # 语料中惟一字符组成的列表 chars = sorted(list(set(text))) print('Unique characters:', len(chars)) # 将惟一字符映射为它在列表 chars 中的索引 char_indices = dict((char, chars.index(char)) for char in chars) print('Vectorization...') # 将字符 one-hot 编码为二进制数组 x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool) y = np.zeros((len(sentences), len(chars)), dtype=np.bool) for i, sentence in enumerate(sentences): for t, char in enumerate(sentence): x[i, t, char_indices[char]] = 1 y[i, char_indices[next_chars[i]]] = 1 # 用于预测下一个字符的单层 LSTM 模型 model = keras.models.Sequential() model.add(layers.LSTM(128, input_shape=(maxlen, len(chars)))) model.add(layers.Dense(len(chars), activation='softmax')) optimizer = keras.optimizers.RMSprop(lr=0.01) model.compile(loss='categorical_crossentropy', optimizer=optimizer) # 模型预测,采样下一个字符的函数 def sample(preds, temperature=1.0): preds = np.asarray(preds).astype('float64') preds = np.log(preds) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas) for epoch in range(1, 41): print('epoch', epoch) model.fit(x, y, batch_size=128, epochs=1) start_index = random.randint(0, len(text) - maxlen - 1) generated_text = text[start_index: start_index + maxlen] print('--- Generating with seed: "' + generated_text + '"') for temperature in [0.2, 0.5, 1.0, 1.2]: print('------ temperature:', temperature) sys.stdout.write(generated_text) for i in range(400): sampled = np.zeros((1, maxlen, len(chars))) for t, char in enumerate(generated_text): sampled[0, t, char_indices[char]] = 1. preds = model.predict(sampled, verbose=0)[0] next_index = sample(preds, temperature) next_char = chars[next_index] generated_text += next_char generated_text = generated_text[1:] sys.stdout.write(next_char)
结果
由训练结果能够看出,,较小的温度值会获得极端重复和可预测的文本,但局部结构是很是真实的,特别是全部单词都是真正的英文单词(单词就是字符的局部模式)。随着温度值愈来愈大,生成的文本也变得更有趣、更出人意料,甚至更有创造性,它有时会创造出全新的单词,听起来有几分可信。对于较大的温度值,局部模式开始分解,大部分单词看起来像是半随机的字符串。毫无疑问,在这个特定的设置下,0.5 的温度值生成的文本最为有趣。必定要尝试多种采样策略!在学到的结构与随机性之间,巧妙的平衡可以让生成的序列很是有趣
利用更多的数据训练一个更大的模型,而且训练时间更长,生成的样本会更连贯、更真实。可是,不要期待可以生成任何有意义的文本,除非是很偶然的状况。你所作的只是从一个统计模型中对数据进行采样,这个模型是关于字符前后顺序的模型
DeepDream 是一种艺术性的图像修改技术,它用到了卷积神经网络学到的表示。DeepDream 算法与的卷积神经网络过滤器可视化技术几乎相同,都是反向运行一个卷积神经网络:对卷积神经网络的输入作梯度上升,以便将卷积神经网络靠顶部的某一层的某个过滤器激活最大化。DeepDream 使用了相同的想法,但有如下这几个简单的区别
DeepDream 过程:空间处理尺度的连续放大(八度)与放大时从新注入细节
对于每一个连续的尺度,从最小到最大,咱们都须要在当前尺度运行梯度上升,以便将以前定义的损失最大化。每次运行完梯度上升以后,将获得的图像放大 40%。在每次连续的放大以后(图像会变得模糊或像素化),为避免丢失大量图像细节,咱们可使用一个简单的技巧:每次放大以后,将丢失的细节从新注入到图像中。这种方法是可行的,由于咱们知道原始图像放大到这个尺寸应该是什么样子。给定一个较小的图像尺寸 S 和一个较大的图像尺寸 L,你能够计算将原始图像大小调整为 L 与将原始图像大小调整为 S 之间的区别,这个区别能够定量描述从 S 到 L 的细节损失
咱们能够选择任意卷积神经网络来实现 DeepDream, 不过卷积神经网络会影响可视化的效果,由于不一样的卷积神经网络架构会学到不一样的特征。接下来将使用 Keras 内置的 Inception V3模型来够生成漂亮的 DeepDream 图像
步骤以下
计算损失(loss),即在梯度上升过程当中须要最大化的量
将多个层的全部过滤器的激活同时最大化。具体来讲,就是对一组靠近顶部的层激活的 L2 范数进行加权求和,而后将其最大化。选择哪些层(以及它们对最终损失的贡献)对生成的可视化结果具备很大影响,因此咱们但愿让这些参数变得易于配置。更靠近底部的层生成的是几何图案,而更靠近顶部的层生成的则是从中可以看出某些 ImageNet 类别(好比鸟或狗)的图案
在多个连续尺度上运行梯度上升
demo
from keras.applications import inception_v3 from keras import backend as K import numpy as np import scipy from keras.preprocessing import image def resize_img(img, size): img = np.copy(img) factors = (1, float(size[0]) / img.shape[1], float(size[1]) / img.shape[2], 1) return scipy.ndimage.zoom(img, factors, order=1) def save_img(img, fname): pil_img = deprocess_image(np.copy(img)) scipy.misc.imsave(fname, pil_img) def preprocess_image(image_path): img = image.load_img(image_path) img = image.img_to_array(img) img = np.expand_dims(img, axis=0) img = inception_v3.preprocess_input(img) return img def deprocess_image(x): if K.image_data_format() == 'channels_first': x = x.reshape((3, x.shape[2], x.shape[3])) x = x.transpose((1, 2, 0)) else: x = x.reshape((x.shape[1], x.shape[2], 3)) x /= 2. x += 0.5 x *= 255. x = np.clip(x, 0, 255).astype('uint8') return x # 这个命令会禁用全部与训练有关的操做 K.set_learning_phase(0) # 构建不包括全链接层的 Inception V3网络。使用预训练的 ImageNet 权重来加载模型 model = inception_v3.InceptionV3(weights='imagenet', include_top=False) # 将层的名称映射为一个系数,这个系数定量表示该层激活对你要最大化的损失的贡献大小 layer_contributions = { 'mixed2': 0.2, 'mixed3': 3., 'mixed4': 2., 'mixed5': 1.5, } # 定义最大化的损失 layer_dict = dict([(layer.name, layer) for layer in model.layers]) loss = K.variable(0.) for layer_name in layer_contributions: coeff = layer_contributions[layer_name] activation = layer_dict[layer_name].output scaling = K.prod(K.cast(K.shape(activation), 'float32')) # 将该层特征的L2范数添加到loss中。为了不出现边界伪影,损失中仅包含非边界的像素 loss += coeff * K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling # 梯度上升过程 # 这个张量用于保存生成的图像,即梦境图像 dream = model.input # 计算损失相对于梦境图像的梯度 grads = K.gradients(loss, dream)[0] # 将梯度标准化 grads /= K.maximum(K.mean(K.abs(grads)), 1e-7) outputs = [loss, grads] # 给定一张输出图像,设置一个 Keras 函数来获取损失值和梯度值 fetch_loss_and_grads = K.function([dream], outputs) def eval_loss_and_grads(x): outs = fetch_loss_and_grads([x]) loss_value = outs[0] grad_values = outs[1] return loss_value, grad_values # 运行 iterations次梯度上升 def gradient_ascent(x, iterations, step, max_loss=None): for i in range(iterations): loss_value, grad_values = eval_loss_and_grads(x) if max_loss is not None and loss_value > max_loss: break print('...Loss value at', i, ':', loss_value) x += step * grad_values return x # 在多个连续尺度上运行梯度上升 # 梯度上升的步长 step = 0.01 # 运行梯度上升的尺度个数 num_octave = 3 # 两个尺度之间的大小比例 octave_scale = 1.4 iterations = 20 # 若是损失增大到大于 10,咱们要中断梯度上升过程,以免获得丑陋的伪影 max_loss = 10. base_image_path = 'img_url' img = preprocess_image(base_image_path) original_shape = img.shape[1:3] successive_shapes = [original_shape] for i in range(1, num_octave): # 一个由形状元组组成的列表,它定义了运行梯度上升的不一样尺度 shape = tuple([int(dim / (octave_scale ** i)) for dim in original_shape]) successive_shapes.append(shape) # 将形状列表反转,变为升序 successive_shapes = successive_shapes[::-1] original_img = np.copy(img) # 将图像 Numpy 数组的大小缩放到最小尺寸 shrunk_original_img = resize_img(img, successive_shapes[0]) for shape in successive_shapes: print('Processing image shape', shape) # 将梦境图像放大 img = resize_img(img, shape) # 运行梯度上升,改变梦境图像 img = gradient_ascent(img, iterations=iterations, step=step, max_loss=max_loss) # 将原始图像的较小版本放大,它会变得像素化 upscaled_shrunk_original_img = resize_img(shrunk_original_img, shape) # 在这个尺寸上计算原始图像的高质量版本 same_size_original = resize_img(original_img, shape) lost_detail = same_size_original - upscaled_shrunk_original_img # 将丢失的细节从新注入到梦境图像中 img += lost_detail shrunk_original_img = resize_img(original_img, shape) save_img(img, fname='dream_at_scale_' + str(shape) + '.png') save_img(img, fname='final_dream.png')
原图
dreamImage
神经风格迁移是指将参考图像的风格应用于目标图像,同时保留目标图像的内容
风格(style)是指图像中不一样空间尺度的纹理、颜色和视觉图案,内容(content)是指图像的高级宏观结构
实现风格迁移背后的关键概念与全部深度学习算法的核心思想是同样的:定义一个损失函数来指定想要实现的目标,而后将这个损失最小化。你知道想要实现的目标是什么,就是保存原始图像的内容,同时采用参考图像的风格。若是咱们可以在数学上给出内容和风格的定义,那么就有一个适当的损失函数,咱们将对其进行最小化
loss = distance(style(reference_image) - style(generated_image)) + distance(content(original_image) - content(generated_image))
这里的 distance 是一个范数函数,好比 L2 范数;content 是一个函数,输入一张图像,并计算出其内容的表示;style 是一个函数,输入一张图像,并计算出其风格的表示。将这个损失最小化,会使得 style(generated_image) 接近于 style(reference_image)、content(generated_image) 接近于 content(generated_image),从而实现咱们定义的风格迁移
深度卷积神经网络可以从数学上定义 style 和 content 两个函数
内容损失
网络更靠底部的层激活包含关于图像的局部信息,而更靠近顶部的层则包含更加全局、更加抽象的信息。卷积神经网络不一样层的激活用另外一种方式提供了图像内容在不一样空间尺度上的分解。所以,图像的内容是更加全局和抽象的,咱们认为它可以被卷积神经网络更靠顶部的层的表示所捕捉到
所以,内容损失的一个很好的候选者就是两个激活之间的 L2 范数,一个激活是预训练的卷积神经网络更靠顶部的某层在目标图像上计算获得的激活,另外一个激活是同一层在生成图像上计算获得的激活。这能够保证,在更靠顶部的层看来,生成图像与原始目标图像看起来很类似
风格损失
内容损失只使用了一个更靠顶部的层,但 Gatys 等人定义的风格损失则使用了卷积神经网络的多个层。咱们想要捉到卷积神经网络在风格参考图像的全部空间尺度上提取的外观,而不只仅是在单一尺度上。对于风格损失,Gatys 等人使用了层激活的格拉姆矩阵(Gram matrix),即某一层特征图的内积。这个内积能够被理解成表示该层特征之间相互关系的映射。这些特征相互关系抓住了在特定空间尺度下模式的统计规律,从经验上来看,它对应于这个尺度上找到的纹理的外观
所以,风格损失的目的是在风格参考图像与生成图像之间,在不一样的层激活内保存类似的内部相互关系。反过来,这保证了在风格参考图像与生成图像之间,不一样空间尺度找到的纹理看起来都很类似
最终,你可使用预训练的卷积神经网络来定义一个具备如下特色的损失
用 Keras 实现神经风格迁移
神经风格迁移能够用任何预训练卷积神经网络来实现。神经风格迁移的通常过程以下
demo
from keras.preprocessing.image import load_img, img_to_array import numpy as np from keras.applications import vgg19 from keras import backend as K from scipy.optimize import fmin_l_bfgs_b from scipy.misc import imsave import time def preprocess_image(image_path): img = load_img(image_path, target_size=(img_height, img_width)) img = img_to_array(img) img = np.expand_dims(img, axis=0) img = vgg19.preprocess_input(img) return img def deprocess_image(x): # vgg19.preprocess_input 的做用是减去 ImageNet 的平均像素值, # 使其中心为 0。这里至关于 vgg19.preprocess_input 的逆操做 x[:, :, 0] += 103.939 x[:, :, 1] += 116.779 x[:, :, 2] += 123.68 # 将图像由 BGR 格式转换为 RGB 格式。这也是 # vgg19.preprocess_input 逆操做的一部分 x = x[:, :, ::-1] x = np.clip(x, 0, 255).astype('uint8') return x target_image_path = 'cat.jpg' style_reference_image_path = 'style.png' # 设置生成图像的尺寸 width, height = load_img(target_image_path).size img_height = 400 img_width = int(width * img_height / height) # 加载预训练的 VGG19 网络,并将其应用于三张图像 target_image = K.constant(preprocess_image(target_image_path)) style_reference_image = K.constant(preprocess_image(style_reference_image_path)) # 占位符用于保存生成图像 combination_image = K.placeholder((1, img_height, img_width, 3)) # 将三张图像合并为一个批量 input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0) model = vgg19.VGG19(input_tensor=input_tensor, weights='imagenet', include_top=False) print('Model loaded.') def content_loss(base, combination): """ 内容损失 :param base: :param combination: :return: """ return K.sum(K.square(combination - base)) def gram_matrix(x): features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1))) gram = K.dot(features, K.transpose(features)) return gram def style_loss(style, combination): """ 风格损失 :param style: :param combination: :return: """ S = gram_matrix(style) C = gram_matrix(combination) channels = 3 size = img_height * img_width return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2)) def total_variation_loss(x): """ 总变差损失 对生成的组合图像的像素进行操做, 促使生成图像具备空间连续性,从而避免结果过分像素化 也能够简单理解为正则化损失 :param x: :return: """ a = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :]) b = K.square(x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :]) return K.sum(K.pow(a + b, 1.25)) # 定义最小化的最终损失 # 将层的名称映射为激活张量的字典 outputs_dict = dict([(layer.name, layer.output) for layer in model.layers]) content_layer = 'block5_conv2' style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] total_variation_weight = 1e-4 style_weight = 1. content_weight = 0.025 loss = K.variable(0.) layer_features = outputs_dict[content_layer] target_image_features = layer_features[0, :, :, :] combination_features = layer_features[2, :, :, :] loss += content_weight * content_loss(target_image_features, combination_features) for layer_name in style_layers: layer_features = outputs_dict[layer_name] style_reference_features = layer_features[1, :, :, :] combination_features = layer_features[2, :, :, :] sl = style_loss(style_reference_features, combination_features) loss += (style_weight / len(style_layers)) * sl loss += total_variation_weight * total_variation_loss(combination_image) # 使用 L-BFGS 算法进行优化,设置梯度降低过程 # 获取损失相对于生成图像的梯度 grads = K.gradients(loss, combination_image)[0] # 用于获取当前损失值和当前梯度值的函数 fetch_loss_and_grads = K.function([combination_image], [loss, grads]) class Evaluator(object): """ 这个类将 fetch_loss_and_grads 包 装起来,让你能够利用两个单独的方法 调用来获取损失和梯度,这是咱们要使 用的 SciPy 优化器所要求的 """ def __init__(self): self.loss_value = None self.grads_values = None def loss(self, x): assert self.loss_value is None x = x.reshape((1, img_height, img_width, 3)) outs = fetch_loss_and_grads([x]) loss_value = outs[0] grad_values = outs[1].flatten().astype('float64') self.loss_value = loss_value self.grad_values = grad_values return self.loss_value def grads(self, x): assert self.loss_value is not None grad_values = np.copy(self.grad_values) self.loss_value = None self.grad_values = None return grad_values evaluator = Evaluator() # 使用 SciPy 的 L-BFGS 算法来运行梯度上升过程 # 风格迁移循环 result_prefix = 'my_result' iterations = 20 x = preprocess_image(target_image_path) # 将图像展平,由于 scipy.optimize.fmin_l_bfgs_b 只能处理展平的向量 x = x.flatten() for i in range(iterations): print('Start of iteration', i) start_time = time.time() x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20) print('Current loss value:', min_val) # 保存当前的生成图像 img = x.copy().reshape((img_height, img_width, 3)) img = deprocess_image(img) fname = result_prefix + '_at_iteration_%d.png' % i imsave(fname, img) print('Image saved as', fname) end_time = time.time() print('Iteration %d completed in %ds' % (i, end_time - start_time))
以上技术所实现的仅仅是一种形式的改变图像纹理,或者叫纹理迁移。若是风格参考图像具备明显的纹理结构且高度自类似,而且内容目标不须要高层次细节就可以被识别,那么这种方法的效果最好。它一般没法实现比较抽象的迁移,好比将一幅肖像的风格迁移到另外一幅中
上面这个风格迁移算法的运行速度很慢。但这种方法实现的变换足够简单,只要有适量的训练数据,一个小型的快速前馈卷积神经网络就能够学会这种变换。所以,实现快速风格迁移的方法是,首先利用这里介绍的方法,花费大量的计算时间对一张固定的风格参考图像生成许多输入 - 输出训练样例,而后训练一个简单的卷积神经网络来学习这个特定风格的变换。一旦完成以后,对一张图像进行风格迁移是很是快的,只是这个小型卷积神经网络的一次前向传递而已
从图像的潜在空间中采样,并建立全新图像或编辑现有图像,这是目前最流行也是最成功的创造性人工智能应用。该领域的两种主要技术分别为变分自编码器(VAE,variational autoencoder)和生成式对抗网络(GAN,generative adversarial network)
从图像的潜在空间中采样
图像生成的关键思想就是找到一个低维的表示潜在空间(latent space,也是一个向量空间),其中任意点均可以被映射为一张逼真的图像。可以实现这种映射的模块,即以潜在点做为输入并输出一张图像(像素网格),叫做生成器(generator,对于 GAN 而言)或解码器(decoder,对于 VAE 而言)。一旦找到了这样的潜在空间,就能够从中有意地或随机地对点进行采样,并将其映射到图像空间,从而生成前所未见的图像
生成图像过程示例
想要学习图像表示的这种潜在空间,GAN 和 VAE 是两种不一样的策略。VAE 很是适合用于学习具备良好结构的潜在空间,其中特定方向表示数据中有意义的变化轴。GAN 生成的图像可能很是逼真,但它的潜在空间可能没有良好结构,也没有足够的连续性
VAE 生成的人脸连续空间
概念向量(concept vector):给定一个表示的潜在空间或一个嵌入空间,空间中的特定方向可能表示原始数据中有趣的变化轴
变分自编码器是一种生成式模型,特别适用于利用概念向量进行图像编辑的任务。它是一种现代化的自编码器,将深度学习的想法与贝叶斯推断结合在一块儿。自编码器是一种网络类型,其目的是将输入编码到低维潜在空间,而后再解码回来
经典的图像自编码器接收一张图像,经过一个编码器模块将其映射到潜在向量空间,而后再经过一个解码器模块将其解码为与原始图像具备相同尺寸的输出。而后,使用与输入图像相同的图像做为目标数据来训练这个自编码器,也就是说,自编码器学习对原始输入进行从新构建。经过对代码(编码器的输出)施加各类限制,咱们可让自编码器学到比较有趣的数据潜在表示。最多见的状况是将代码限制为低维的而且是稀疏的(即大部分元素为 0),在这种状况下,编码器的做用是将输入数据压缩为更少二进制位的信息
自编码器模型表示
这种自编码器不会获得特别有用或具备良好结构的潜在空间。它们也没有对数据作多少压缩。可是,VAE 向自编码器添加了一点统计魔法,迫使其学习连续的、高度结构化的潜在空间。这使得 VAE 已成为图像生成的强大工具
VAE 不是将输入图像压缩成潜在空间中的固定编码,而是将图像转换为统计分布的参数,即平均值和方差。本质上来讲,这意味着咱们假设输入图像是由统计过程生成的,在编码和解码过程当中应该考虑这一过程的随机性。而后,VAE 使用平均值和方差这两个参数来从分布中随机采样一个元素,并将这个元素解码到原始输入。这个过程的随机性提升了其稳健性,并迫使潜在空间的任何位置都对应有意义的表示,即潜在空间采样的每一个点都能解码为有效的输出
VAE模型表示
VAE 的工做原理
由于 epsilon 是随机的,因此这个过程能够确保,与 input_img 编码的潜在位置(即z-mean)靠近的每一个点都能被解码为与 input_img 相似的图像,从而迫使潜在空间可以连续地有意义。潜在空间中任意两个相邻的点都会被解码为高度类似的图像。连续性以及潜在空间的低维度,将迫使潜在空间中的每一个方向都表示数据中一个有意义的变化轴,这使得潜在空间具备很是良好的结构,所以很是适合经过概念向量来进行操做
VAE 的参数经过两个损失函数来进行训练:一个是重构损失(reconstruction loss),它迫使解码后的样本匹配初始输入;另外一个是正则化损失(regularization loss),它有助于学习具备良好结构的潜在空间,并能够下降在训练数据上的过拟合
demo
import keras from keras import layers from keras import backend as K from keras.models import Model import numpy as np from keras.datasets import mnist import matplotlib.pyplot as plt from scipy.stats import norm class CustomVariationalLayer(keras.layers.Layer): """ 用于计算 VAE 损失的自定义层 """ def vae_loss(self, x, z_decoded): x = K.flatten(x) z_decoded = K.flatten(z_decoded) xent_loss = keras.metrics.binary_crossentropy(x, z_decoded) kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1) return K.mean(xent_loss + kl_loss) def call(self, inputs): x = inputs[0] z_decoded = inputs[1] loss = self.vae_loss(x, z_decoded) self.add_loss(loss, inputs=inputs) return x def sampling(args): """ 潜在空间采样的函数 :param args: :return: """ z_mean, z_log_var = args epsilon = K.random_normal(shape=(K.shape(z_mean)[0], latent_dim), mean=0., stddev=1.) return z_mean + K.exp(z_log_var) * epsilon # 网络 img_shape = (28, 28, 1) batch_size = 16 latent_dim = 2 input_img = keras.Input(shape=img_shape) x = layers.Conv2D(32, 3, padding='same', activation='relu')(input_img) x = layers.Conv2D(64, 3, padding='same', activation='relu', strides=(2, 2))(x) x = layers.Conv2D(64, 3, padding='same', activation='relu')(x) x = layers.Conv2D(64, 3, padding='same', activation='relu')(x) shape_before_flattening = K.int_shape(x) x = layers.Flatten()(x) x = layers.Dense(32, activation='relu')(x) z_mean = layers.Dense(latent_dim)(x) z_log_var = layers.Dense(latent_dim)(x) # z_mean 和 z_log_var 是统计分布的参数,假设这个分布可以生成 input_img # 接下来的代码将使用 z_mean 和 z_log_var 来生成一个潜在空间点 z z = layers.Lambda(sampling)([z_mean, z_log_var]) # VAE 解码器网络,将潜在空间点映射为图像 decoder_input = layers.Input(K.int_shape(z)[1:]) # 对输入进行上采样 x = layers.Dense(np.prod(shape_before_flattening[1:]), activation='relu')(decoder_input) # 将 z 转换为特征图,使其形状与编码器模型最后一个 Flatten 层以前的特征图的形状相同 x = layers.Reshape(shape_before_flattening[1:])(x) # 使用一个 Conv2DTranspose 层和一个Conv2D 层,将 z 解码为与原始输入图像具备相同尺寸的特征图 x = layers.Conv2DTranspose(32, 3, padding='same', activation='relu', strides=(2, 2))(x) x = layers.Conv2D(1, 3, padding='same', activation='sigmoid')(x) # 将解码器模型实例化,它将 decoder_input转换为解码后的图像 decoder = Model(decoder_input, x) # 将实例应用于 z,以获得解码后的 z z_decoded = decoder(z) y = CustomVariationalLayer()([input_img, z_decoded]) # 训练 VAE vae = Model(input_img, y) vae.compile(optimizer='rmsprop', loss=None) vae.summary() (x_train, _), (x_test, y_test) = mnist.load_data() x_train = x_train.astype('float32') / 255. x_train = x_train.reshape(x_train.shape + (1,)) x_test = x_test.astype('float32') / 255. x_test = x_test.reshape(x_test.shape + (1,)) vae.fit(x=x_train, y=None, shuffle=True, epochs=10, batch_size=batch_size, validation_data=(x_test, None)) # 从二维潜在空间中采样一组点的网格,并将其解码为图像 n = 15 digit_size = 28 figure = np.zeros((digit_size * n, digit_size * n)) grid_x = norm.ppf(np.linspace(0.05, 0.95, n)) grid_y = norm.ppf(np.linspace(0.05, 0.95, n)) for i, yi in enumerate(grid_x): for j, xi in enumerate(grid_y): z_sample = np.array([[xi, yi]]) z_sample = np.tile(z_sample, batch_size).reshape(batch_size, 2) x_decoded = decoder.predict(z_sample, batch_size=batch_size) digit = x_decoded[0].reshape(digit_size, digit_size) figure[i * digit_size: (i + 1) * digit_size, j * digit_size: (j + 1) * digit_size] = digit plt.figure(figsize=(10, 10)) plt.imshow(figure, cmap='Greys_r') plt.show()
结果
VAE 获得的是高度结构化的、连续的潜在表示。所以,它在潜在空间中进行各类图像编辑的效果很好,好比换脸、将皱眉脸换成微笑脸等。它制做基于潜在空间的动画效果也很好,好比沿着潜在空间的一个横截面移动,从而以连续的方式显示从一张起始图像缓慢变化为不一样图像的效果
GAN 能够生成逼真的单幅图像,但获得的潜在空间可能没有良好的结构,也没有很好的连续性
生成式对抗网络(GAN,generative adversarial network)可以迫使生成图像与真实图像在统计上几乎没法区分,从而生成至关逼真的合成图像
GAN 的工做原理:一个伪造者网络和一个专家网络,两者训练的目的都是为了战胜彼此。所以,GAN 由如下两部分组成
生成器网络(generator network):它以一个随机向量(潜在空间中的一个随机点)做为输入,并将其解码为一张合成图像
判别器网络(discriminator network)或对手(adversary):以一张图像(真实的或合成的都可)做为输入,并预测该图像是来自训练集仍是由生成器网络建立
训练生成器网络的目的是使其可以欺骗判别器网络,所以随着训练的进行,它可以逐渐生成愈来愈逼真的图像,即看起来与真实图像没法区分的人造图像,以致于判别器网络没法区分两者。与此同时,判别器也在不断适应生成器逐渐提升的能力,为生成图像的真实性设置了很高的标准。一旦训练结束,生成器就可以将其输入空间中的任何点转换为一张可信图像。与 VAE 不一样,这个潜在空间没法保证具备有意义的结构,并且它仍是不连续的
GAN示意
GAN系统的优化最小值是不固定的。一般来讲,梯度降低是沿着静态的损失地形滚下山坡。但对于 GAN 而言,每下山一步,都会对整个地形形成一点改变。它是一个动态的系统,其最优化过程寻找的不是一个最小值,而是两股力量之间的平衡。所以,GAN 的训练极其困难,想要让 GAN 正常运行,须要对模型架构和训练参数进行大量的仔细调整
GAN 的简要实现流程
实现GAN的一些技巧
GAN训练循环的大体流程
demo
import keras from keras import layers import numpy as np import os from keras.preprocessing import image # 生成器 latent_dim = 32 height = 32 width = 32 channels = 3 generator_input = keras.Input(shape=(latent_dim,)) x = layers.Dense(128 * 16 * 16)(generator_input) x = layers.LeakyReLU()(x) x = layers.Reshape((16, 16, 128))(x) x = layers.Conv2D(256, 5, padding='same')(x) x = layers.LeakyReLU()(x) x = layers.Conv2DTranspose(256, 4, strides=2, padding='same')(x) x = layers.LeakyReLU()(x) x = layers.Conv2D(256, 5, padding='same')(x) x = layers.LeakyReLU()(x) x = layers.Conv2D(256, 5, padding='same')(x) x = layers.LeakyReLU()(x) x = layers.Conv2D(channels, 7, activation='tanh', padding='same')(x) generator = keras.models.Model(generator_input, x) generator.summary() # 判别器 discriminator_input = layers.Input(shape=(height, width, channels)) x = layers.Conv2D(128, 3)(discriminator_input) x = layers.LeakyReLU()(x) x = layers.Conv2D(128, 4, strides=2)(x) x = layers.LeakyReLU()(x) x = layers.Conv2D(128, 4, strides=2)(x) x = layers.LeakyReLU()(x) x = layers.Conv2D(128, 4, strides=2)(x) x = layers.LeakyReLU()(x) x = layers.Flatten()(x) x = layers.Dropout(0.4)(x) x = layers.Dense(1, activation='sigmoid')(x) discriminator = keras.models.Model(discriminator_input, x) discriminator.summary() discriminator_optimizer = keras.optimizers.RMSprop(lr=0.0008, clipvalue=1.0, decay=1e-8) discriminator.compile(optimizer=discriminator_optimizer, loss='binary_crossentropy') # 对抗网络,将生成器和判别器链接在一块儿 discriminator.trainable = False gan_input = keras.Input(shape=(latent_dim,)) gan_output = discriminator(generator(gan_input)) gan = keras.models.Model(gan_input, gan_output) gan_optimizer = keras.optimizers.RMSprop(lr=0.0004, clipvalue=1.0, decay=1e-8) gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy') # 实现 GAN 的训练 (x_train, y_train), (_, _) = keras.datasets.cifar10.load_data() x_train = x_train[y_train.flatten() == 6] x_train = x_train.reshape((x_train.shape[0],) + (height, width, channels)).astype('float32') / 255 iterations = 10000 batch_size = 20 save_dir = 'your_dir' start = 0 for step in range(iterations): random_latent_vectors = np.random.normal(size=(batch_size, latent_dim)) generated_images = generator.predict(random_latent_vectors) stop = start + batch_size real_images = x_train[start: stop] combined_images = np.concatenate([generated_images, real_images]) labels = np.concatenate([np.ones((batch_size, 1)), np.zeros((batch_size, 1))]) labels += 0.05 * np.random.random(labels.shape) d_loss = discriminator.train_on_batch(combined_images, labels) random_latent_vectors = np.random.normal(size=(batch_size, latent_dim)) misleading_targets = np.zeros((batch_size, 1)) a_loss = gan.train_on_batch(random_latent_vectors, misleading_targets) start += batch_size if start > len(x_train) - batch_size: start = 0 if step % 100 == 0: gan.save_weights('gan.h5') print('discriminator loss:', d_loss) print('adversarial loss:', a_loss) img = image.array_to_img(generated_images[0] * 255., scale=False) img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png')) img = image.array_to_img(real_images[0] * 255., scale=False) img.save(os.path.join(save_dir, 'real_frog' + str(step) + '.png'))
GAN 由一个生成器网络和一个判别器网络组成。判别器的训练目的是可以区分生成器的输出与来自训练集的真实图像,生成器的训练目的是欺骗判别器。值得注意的是,生成器从未直接见过训练集中的图像,它所知道的关于数据的信息都来自于判别器
注:
在 Keras 中,任何对象都应该是一个层,因此若是代码不是内置层的一部分,咱们应该将其包装到一个 Lambda 层(或自定义层)中
Deep learning with Python 学习笔记(11)
Deep learning with Python 学习笔记(9)