10. 卷积神经网络

10.1 卷积的定义

每次通过移动卷积核,并与图片对应位置处的感受野像素相乘累加,得到此位置的输出值。卷积核即是行、列为𝑙大小的权值矩阵𝑿,对应到特征图上大小为𝑙的窗口即为感受野,感受野与权值矩阵𝑿相乘累加,得到此位置的输出值.

注意:
输入的每个通道处的感受野均与卷积核的对应通道相乘累加,得到与通道数量相等的中间变量,这些中间变量全部相加即得到当前位置的输出值。输入通道的通道数量决定了卷积核的通道数。一个卷积核只能得到一个输出矩阵,无论输入𝒀的通道数量

10.2 padding
10.2.1 padding=“valid”

输出长宽比原来图片长宽略小

layers.Conv2D(filters=6, kernel_size=3, padding="valid", strides=2)
10.2.2 padding=“same”

输出长宽等于原来图片长宽

layers.Conv2D(filters=6, kernel_size=3, padding="same", strides=2)
10.3 自定义权值设置
x = tf.random.normal([2, 5, 5, 3])
w = tf.random.normal([3, 3, 3, 4])
out = tf.nn.conv2d(x, w, strides=1, padding=[[0, 0], [0, 0], [0, 0], [0, 0]])
print(out.shape)
# (2, 3, 3, 4)
x = tf.random.normal([2, 5, 5, 3])
w = tf.random.normal([3, 3, 3, 4])
out = tf.nn.conv2d(x, w, strides=1, padding="SAME")
# (2, 5, 5, 4)

当w,h不能整除stride时候

x = tf.random.normal([2, 5, 5, 3])
w = tf.random.normal([3, 3, 3, 4])
out = tf.nn.conv2d(x, w, strides=3, padding="SAME")
# # 高宽先 padding 成可以整除 3 的最小整数 6,然后 6 按 3 倍减少,得到 2x2
print(out.shape)
# (2, 2, 2, 4)
10.4 卷积层类
x = tf.random.normal([2, 5, 5, 3])
layer = layers.Conv2D(filters=4, kernel_size=3, strides=1, padding="SAME")
out = layer(x)
print(out.shape)
# (2, 5, 5, 4)
10.4 LeNet-5实战
network = tf.keras.Sequential([
    layers.Conv2D(filters=6, kernel_size=3, strides=1),
    layers.MaxPooling2D(pool_size=2, strides=2),
    layers.ReLU(),
    layers.Conv2D(16, kernel_size=3, strides=1),
    layers.MaxPooling2D(pool_size=2, strides=2),
    layers.ReLU(),
    layers.Flatten(),
    layers.Dense(120, activation=tf.nn.relu),
    layers.Dense(84, activation=tf.nn.relu),
    layers.Dense(10)
])

network.build(input_shape=(4, 28, 28, 1))
network.summary()

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_train = db_train.shuffle(buffer_size=100).batch(4)

criteon = tf.keras.losses.CategoricalCrossentropy(from_logits=True)

for epoch in range(2):
    for step, (x, y) in enumerate(db_train):
        with tf.GradientTape() as tape:
            x = tf.expand_dims(x, axis=3)
            out = network(x)
            y_onehot = tf.one_hot(y, depth=10)
            loss = criteon(y_onehot, out)
        grads = tape.gradient(loss, network.trainable_variables)
        optimizer = tf.keras.optimizers.Adam()
        optimizer.apply_gradients(zip(grads, network.trainable_variables))
        if step % 1000 == 0:
            y_pred = tf.argmax(out, axis=-1)
            y = tf.cast(y, tf.int64)
            correct = tf.reduce_sum(tf.cast(tf.equal(y_pred, y), tf.float32))
            print("epoch {}, step {}, accuracy {}".format(epoch+1, step+1, correct.numpy()/y_pred.shape[0]))


correct, total = 0, 0
for x, y in db_test:
    x = tf.expand_dims(x, axis=3)
    out = network(x)
    pred = tf.argmax(out, axis=-1)
    y = tf.cast(y, tf.int64)
    correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
    total += x.shape[0]
print("test_acc{}".format(correct/total))
10.5 Fine-tuning的定义

应用表示学习的思想,训练好的卷积神经网络往往能够学习到较好的特征,这种特征的提取方法一般是通用的。比如在猫、狗任务上学习到头、脚、身躯等特征的表示,在其它动物上也能够一定程度上使用。基于这种思想,可以将在任务 A 上训练好的深层神经网络的前面数个特征提取层迁移到任务 B 上只需要训练任务 B 的分类逻辑(表现为网络的最末数层),即可取得非常好的效果,这种方式是迁移学习的一种,从神经网络角度也称为网络微调(Fine-tuning)。

10.6 池化层的定义

在卷积层中,可以通过调节步长参数𝑠实现特征图的高宽成倍缩小,从而降低了网络的参数量。实际上,除了通过设置步长,还有一种专门的网络层可以实现尺寸缩减功能,它就是这里要介绍的池化层(Pooling Layer)。

池化层同样基于局部相关性的思想,通过从局部相关的一组元素中进行采样或信息聚合,从而得到新的元素值。特别地,最大池化层(Max Pooling)从局部相关元素集中选取最大的一个元素值,平均池化层(Average Pooling)从局部相关元素集中计算平均值并返回。

代码实现:

layers.MaxPooling2D(pool_size=2, strides=2),
10.7 BatchNorm 层

考虑 Sigmoid 激活函数和它的梯度分布,如下图 10.39 所示,Sigmoid 函数在𝑦 ∈[-2, 2] 区间的导数值在 [0.1, 0 .25] 区间分布;当𝑦 > 2 或𝑦 < −2时,Sigmoid 函数的导数变得很小,逼近于 0,从而容易出现梯度弥散现象。为了避免因为输入较大或者较小而导致Sigmoid 函数出现梯度弥散现象,将函数输入𝑦标准化映射到 0 附近的一段较小区间将变得非常重要,可以从图 10.39 看到,通过标准化重映射后,值被映射在 0 附近,此处的导数值不至于过小,从而不容易出现梯度弥散现象。这是使用标准化手段受益的一个例子。

计算公式:

tensorflow 深度学习 龙良曲pdf_卷积


代码实现:

x_train = tf.random.normal((100, 28, 28, 4))

x_test = tf.random.normal((100, 28, 28, 4))
y_test = tf.random.uniform(shape=(100, ), minval=0, maxval=10, dtype=tf.int64)
y_test = tf.one_hot(y_test, depth=10)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))

network = tf.keras.Sequential([
    layers.Conv2D(filters=6, kernel_size=3, strides=1),
    layers.BatchNormalization()
])
network.build(input_shape=[100, 28, 28, 4])
network.summary()
# 添加BatchNorm层就需要指定是否属于训练模式
with tf.GradientTape() as tape:
    out = network(x, training=True)
# 测试代码
for x, y in db_test:
    out = network(x, training=False)
10.8 经典神经网络
10.8.1 AlexNet

AlexNet 的创新之处在于:
❑ 层数达到了较深的 8 层。
❑ 采用了 ReLU 激活函数,过去的神经网络大多采用 Sigmoid 激活函数,计算相对复
杂,容易出现梯度弥散现象。
❑ 引入 Dropout 层。Dropout 提高了模型的泛化能力,防止过拟合。

10.8.2 VGG

VGG 系列网络的创新之处在于:
❑ 层数提升至 19 层。
❑ 全部采用更小的3 × 3卷积核,相对于 AlexNet 中 × 的卷积核,参数量更少,计算代
价更低。
❑ 采用更小的池化层2 × 2窗口和步长𝑠 = 2,而 AlexNet 中是步长𝑠 = 2、3 × 3的池化窗

10.8.3 GoogLeNet

GoogLeNet 网络采用模块化设计的思想,通过大量堆叠 Inception 模块,形成了复杂的
网络结构。如下图 10.47 所示,Inception 模块的输入为𝒀,通过 4 个子网络得到 4 个网络
输出,在通道轴上面进行拼接合并,形成 Inception 模块的输出。这 4 个子网络是:
❑ 1× 1卷积层。
❑ 1×1 卷积层,再通过一个3 × 3卷积层。
❑ 1×1 卷积层,再通过一个 5×5 卷积层。
❑ 3 × 3最大池化层,再通过1 × 1卷积层。

10.9 VGG13实战

下面代码完成了VGG13的构建已经一次前项运算

x = tf.random.normal([128, 32, 32, 3])

conv_layers = [
    layers.Conv2D(64, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.Conv2D(64, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.MaxPooling2D(pool_size=[2, 2], strides=2, padding="SAME"),

    layers.Conv2D(128, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.Conv2D(128, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.MaxPooling2D(pool_size=[2, 2], strides=2, padding="SAME"),

    layers.Conv2D(256, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.Conv2D(256, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.MaxPooling2D(pool_size=[2, 2], strides=2, padding="SAME"),

    layers.Conv2D(512, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.MaxPooling2D(pool_size=[2, 2], strides=2, padding="SAME"),

    layers.Conv2D(512, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="SAME", activation=tf.nn.relu),
    layers.MaxPooling2D(pool_size=[2, 2], strides=2, padding="SAME")

]

conv_net = tf.keras.Sequential(conv_layers)
conv_net.build(input_shape=(128, 32, 32, 3))
conv_net.summary()

fc_net = tf.keras.Sequential([
    layers.Dense(256, activation=tf.nn.relu),
    layers.Dense(128, activation=tf.nn.relu),
    layers.Dense(10, activation=None)
])

fc_net.build(input_shape=[128, 512])
fc_net.summary()

out = conv_net(x)
out = tf.squeeze(out)
out = fc_net(out)
print(out.shape)
10.10 空洞卷积

普通的卷积层为了减少网络的参数量,卷积核的设计通常选择较小的1 × 1和3 × 3感受野大小。小卷积核使得网络提取特征时的感受野区域有限,但是增大感受野的区域会增加网络的参数量和计算代价,因此需要权衡设计。

tensorflow 深度学习 龙良曲pdf_tensorflow_02


空洞卷积本质就是把卷积盒子放大,如dilation_rate=2就是隔两个点计算一次

通过设置dilation_rate参数可以调整卷积核空洞的大小

x = tf.random.normal([1, 7, 7, 1])
layer = layers.Conv2D(filters=1, kernel_size=3, strides=1, dilation_rate=2)
out = layer(x)
print(out.shape)
# (1, 3, 3, 1)
10.11 转置卷积

恢复原来Tensor的shape,但是里面的数值不一样

x = tf.range(25) + 1
x = tf.reshape(x, [1, 5, 5, 1])
x = tf.cast(x, tf.float32)

w = tf.constant([[-1, -2, -3.], [4, -5, 6], [-7, 8, -9]])
w = tf.expand_dims(w, axis=2)
w = tf.expand_dims(w, axis=3)

out = tf.nn.conv2d(x, w, strides=2, padding="VALID")
print(out.shape)
# (1, 2, 2, 1)
xx = tf.nn.conv2d_transpose(out, w, strides=2, padding="VALID", output_shape=[1, 5, 5, 1])
print(xx.shape)
# (1, 5, 5, 1)

转置卷积类实现

x = tf.range(16) + 1
x = tf.reshape(x, [1, 4, 4, 1])
x = tf.cast(x, tf.float32)

w = tf.constant([[-1, 2, -3.], [4, -5, 6], [-7, 8, -9]])
w = tf.expand_dims(w, axis=2)
w = tf.expand_dims(w, axis=3)

out = tf.nn.conv2d(x, w, strides=1, padding="VALID")
print(out.shape)
# (1, 2, 2, 1)

layer = layers.Conv2DTranspose(1, kernel_size=3, strides=1, padding="VALID")
xx = layer(out)
print(xx.shape)
# (1, 4, 4, 1)
10.12 分离卷积

这里以深度可分离卷积(Depth-wise Separable Convolution)为例。普通卷积在对多通道输入进行运算时,卷积核的每个通道与输入的每个通道分别进行卷积运算,得到多通道的特征图,再对应元素相加产生单个卷积核的最终输出,如图 10.60 所示。

tensorflow 深度学习 龙良曲pdf_卷积_03


分离卷积的计算流程则不同,卷积核的每个通道与输入的每个通道进行卷积运算,得到多个通道的中间特征,如图 10.61 所示。这个多通道的中间特征张量接下来进行多个1×1 卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。可以看到,分离卷积层包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。

tensorflow 深度学习 龙良曲pdf_卷积_04

10.13 深度残差网络

2015 年,微软亚洲研究院何凯明等人发表了基于 Skip Connection 的深度残差网络(Residual Neural Network,简称 ResNet)算法 [10],并提出了 18 层、34 层、50 层、101层、152 层的 ResNet-18、ResNet-34、ResNet-50、ResNet-101 和 ResNet-152 等模型,甚至成功训练出层数达到 1202 层的极深层神经网络。ResNet 在 ILSVRC 2015 挑战赛 ImageNet数据集上的分类、检测等任务上面均获得了最好性能,ResNet 论文至今已经获得超 25000的引用量,可见 ResNet 在人工智能行业的影响力。

ResNet 通过在卷积层的输入和输出之间添加 Skip Connection 实现层数回退机制,如下
图 10.63 所示,输入𝒚通过两个卷积层,得到特征变换后的输出ℱ(𝒚),与输入𝒚进行对应元
素的相加运算,得到最终输出ℋ(𝒚):
ℋ(𝒚) = 𝒚 + ℱ(𝒚)
ℋ(𝒚)叫作残差模块(Residual Block,简称 ResBlock)。由于

深度残差网络代码的实现

class BasicBlock(layers.Layer):
    def __init__(self, filter_num, stride=1):
        super().__init__()
        self.conv1 = layers.Conv2D(filter_num, (3, 3), stride=stride,padding="SAME")
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation("relu")

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding="SAME")
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = keras.Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x
    
    def call(self, inputs, training=None):
        out = self.conv1(inputs)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        identity = self.downsample(inputs)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output
10.14 DenseNet

Skip Connection 的思想在 ResNet 上面获得了巨大的成功,研究人员开始尝试不同的Skip Connection 方案,其中比较流行的就是 DenseNet [11]。DenseNet 将前面所有层的特征图信息通过 Skip Connection 与当前层输出进行聚合,与 ResNet 的对应位置相加方式不同,DenseNet 采用在通道轴𝑐维度进行拼接操作,聚合特征信息

tensorflow 深度学习 龙良曲pdf_ide_05

10.15 ResNet + cifar10实战
class BasicBlock(layers.Layer):
    def __init__(self, filter_num, stride=1):
        super().__init__()
        self.conv1 = layers.Conv2D(filters=filter_num, kernel_size=(3, 3),
                                   strides=stride, padding="SAME")
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation("relu")

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding="SAME")
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = keras.Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x

    def call(self, inputs, training=None):
        out = self.conv1(inputs)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        identity = self.downsample(inputs)

        output = layers.add([out, identity])
        output = tf.nn.relu(output)

        return output


class ResNet(keras.Model):
    def __init__(self, layer_dims, num_classes=10):
        super().__init__()
        self.stem = keras.Sequential([
            layers.Conv2D(64, (3, 3), strides=(1, 1)),
            layers.BatchNormalization(),
            layers.Activation("relu"),
            layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding="SAME")
        ])
        self.layer1 = self.build_resblock(64, layer_dims[0])
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)

        self.avgpool = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(num_classes)

    def build_resblock(self, filter_num, blocks, stride=1):
        res_blocks = keras.Sequential()
        res_blocks.add(BasicBlock(filter_num, stride))

        for _ in range(1, blocks):
            res_blocks.add(BasicBlock(filter_num, stride=1))
        return res_blocks

    def call(self, inputs, training=None):
        x = self.stem(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = self.fc(x)

        return x


resnet18 = ResNet(layer_dims=[2, 2, 2, 2])
resnet18.build(input_shape=(100, 32, 32, 3))
resnet18.summary()


def preprocess(x, y):
    x = 2 * tf.cast(x, dtype=tf.float32) / 225. - 1.
    y = tf.cast(y, dtype=tf.float32)
    return x, y


(x, y), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
y = tf.squeeze(y, axis=1)
y_test = tf.squeeze(y_test, axis=1)
print(x.shape, y.shape, x_test.shape, y_test.shape)

train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(512)

test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.shuffle(1000).map(preprocess).batch(512)


sample = next(iter(train_db))
print(sample[0].shape, sample[1].shape, tf.reduce_mean(sample[0]),
      tf.reduce_max(sample[0]))

for epoch in range(50):
    for step, (x, y) in enumerate(train_db):
        with tf.GradientTape() as tape:
            logits = resnet18(x)
            y = tf.cast(y, tf.int32)
            y_onehot = tf.one_hot(y, depth=10)
            loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
            loss = tf.reduce_mean(loss)
        grads = tape.gradient(loss, resnet18.trainable_variables)
        optimizer = tf.keras.optimizers.Adam()
        optimizer.apply_gradients(zip(grads, resnet18.trainable_variables))
        print("step {}, epoch: {}, loss {}".format(step, epoch, loss))

11. 循环神经网络

11.1 序列的表示方法
11.1.2 Word Embedding:把文字编码为数值的过程.

在神经网络中,单词的表示向量可以直接通过训练的方式得到,我们把单词的表示层.叫作 Embedding 层。Embedding 层负责把单词编码为某个词向量𝒗,它接受的是采用数字编码的单词编号𝑖,如 2 表示“I”,3 表示“me”等,系统总单词数量记为𝑁 vocab ,输出长度为𝑛的向量𝒗:
𝒗 = 𝑔 𝜃 (𝑖|𝑁 vocab ,𝑛)
Embedding 层实现起来非常简单,构建一个 shape 为[𝑁 vocab ,𝑛]的查询表对象 table,对于任意的单词编号𝑖,只需要查询到对应位置上的向量并返回即可:
𝒗 = 𝑢𝑏𝑐𝑙𝑓[𝑖]
Embedding 层是可训练的,它可放置在神经网络之前,完成单词到向量的转换,得到的表示向量可以继续通过神经网络完成后续任务,并计算误差ℒ,采用梯度下降算法来实现端到端(end-to-end)的训练

上述代码创建了 10 个单词的 Embedding 层,每个单词用长度为 4 的向量表示,可以传入数字编码为 0~9 的输入,得到这 4 个单词的词向量,这些词向量随机初始化的,尚未经过网络训练,
embedding TensorFlow的代码

x = tf.range(5)
x = tf.random.shuffle(x)
# 一共10个单词,每个单词的长度为4个数值
net = layers.Embedding(input_dim=10, output_dim=4)
out = net(x)
# (5, 4)
11.1.3 预训练词向量

Embedding 层的查询表是随机初始化的,需要从零开始训练。实际上,我们可以使用预训练的 Word Embedding 模型来得到单词的表示方法,基于预训练模型的词向量相当于迁移了整个语义空间的知识,往往能得到更好的性能。

目前应用的比较广泛的预训练模型有 Word2Vec 和 GloVe 等。它们已经在海量语料库训练得到了较好的词向量表示方法,并可以直接导出学习到的词向量表,方便迁移到其它任务。比如 GloVe 模型 GloVe.6B.50d,词汇量为 40 万,每个单词使用长度为 50 的向量表示,用户只需要下载对应的模型文件即可,“glove6b50dtxt.zip”模型文件约 69MB。

11.2 循环神经网络

tensorflow 深度学习 龙良曲pdf_tensorflow_06

11.2.1 循环神经网络TensorFlow 使用方法

层。在 TensorFlow 中,可以通过 layers.SimpleRNNCell 来完成𝜎(𝑾 𝒙 𝑢 + 𝑾 𝑢−1 +𝒄)计算。需要注意的是,在 TensorFlow 中,RNN 表示通用意义上的循环神经网络,对于我们目前介绍的基础循环神经网络,它一般叫做 SimpleRNN。SimpleRNN 与 SimpleRNNCell 的区别在于,带 Cell 的层仅仅是完成了一个时间戳的前向运算,不带 Cell 的层一般是基于Cell 层实现的,它在内部已经完成了多个时间戳的循环运算,因此使用起来更为方便快捷

SimpleRNNCell较为底层api,需要记录状态张量h

cell = layers.SimpleRNNCell(3)
cell.build(input_shape=(None, 4))
# 打印wxh, whh, b张量
print(cell.trainable_variables)

单层循环神经网络的示例

h0 = [tf.zeros([4, 64])]
x = tf.random.normal([4, 80, 100])
xt = x[:, 0, :]
# (4, 100)

cell = layers.SimpleRNNCell(64)
out, h1 = cell(xt, h0)
print(out.shape)
# (4, 64)
# 输出与记忆张量h是一样的
print(out == h1[0])

两层CNN

xt = x[:, 0, :]

cell0 = layers.SimpleRNNCell(64)
cell1 = layers.SimpleRNNCell(64)

h0 = [tf.zeros([4, 64])]
h1 = [tf.zeros([4, 64])]

for xt in tf.unstack(x, axis=1):
    out0, h0 = cell0(xt, h0)
    out1, h1 = cell1(out0, h1)

需要注意的是,循环神经网络的每一层、每一个时间戳上面均有状态输出,那么对于后续任务来说,我们应该收集哪些状态输出最有效呢?一般来说,最末层 Cell 的状态有可能保存了高层的全局语义特征,因此一般使用最末层的输出作为后续任务网络的输入。更特别地,每层最后一个时间戳上的状态输出包含了整个序列的全局信息,如果只希望选用一个状态变量来完成后续任务,比如情感分类问题,一般选用最末层、最末时间戳的状态输出最为合适

高级api: SimpleRNN

# 默认返回最后一个时间戳上的输出
layer = layers.SimpleRNN(64)
x = tf.random.normal([4, 80, 100])
out = layer(x)
# (4, 64)

# 返回所有时间戳上面的输出
layer2 = layers.SimpleRNN(64, return_sequences=True)
out = layer2(x)
print(out.shape)
# (4, 80, 64)

Sequential形式

net = keras.Sequential([
    layers.SimpleRNN(64, return_sequences=True),
    layers.SimpleRNN(64)
])
net.build(input_shape=(4, 80, 100))
net.summary()
11.2.2 RNN情感分析示例
batchsz = 128
total_words = 10000
max_review_len = 80
embedding_len = 100
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)

print(x_train.shape, len(x_train[1]), y_train.shape)
print(x_test.shape, len(x_test[0]), y_test.shape)

# 打印编码表
word_index = keras.datasets.imdb.get_word_index()
for k, v in word_index.items():
    print(k, v)
    break

word_index = {k: (v+3) for k, v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2
word_index["<UNUSED>"] = 3

reverse_word_index = {v: k for k, v in word_index.items()}


# 尝试反编码并打印原来的句子
def decode_review(text):
    return " ".join([reverse_word_index.get(word, "?") for word in text])
print(decode_review(x_train[0]))

x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len)
print(decode_review(x_train[0]))

db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# drop_remainder丢弃最后一个长度不满足设定batchsize的batch
db_train = db_train.shuffle(1000).batch(batchsz, drop_remainder=True)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)

print(x_train.shape, tf.reduce_max(y_train), tf.reduce_min(y_train))


class MyRNN(keras.Model):
    def __init__(self, units):
        super(MyRNN, self).__init__()
        self.state0 = [tf.zeros([batchsz, units])]
        self.state1 = [tf.zeros([batchsz, units])]
        self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
        self.rnn_cell0 = layers.SimpleRNNCell(units=units, dropout=0.5)
        self.rnn_cell1 = layers.SimpleRNNCell(units=units, dropout=0.5)
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None, mask=None):
        x = inputs
        x = self.embedding(x)
        state0 = self.state0
        state1 = self.state1
        # 这里每次喂进去的是这个batch的所有句子的第1个, 第2个,  .... 第n个单词
        for word in tf.unstack(x, axis=1):
            out0, state0 = self.rnn_cell0(word, state0, training)
            out1, state1 = self.rnn_cell1(out0, state1, training)
        x = self.outlayer(out1)
        prob = tf.sigmoid(x)
        return prob


def main():
    units = 64
    epochs = 20
    model = MyRNN(units)
    model.compile(optimizer=keras.optimizers.Adam(0.001), loss=keras.losses.binary_crossentropy,
                  metrics=["accuracy"])
    model.fit(db_train, epochs=epochs, validation_data=db_test)
    model.evaluate(db_test)


if __name__ == '__main__':
    main()
11.2.3 梯度爆炸

以下代码模拟了梯度爆炸的过程

w = tf.ones([2, 2]) * 1.4
eigenvalues = tf.linalg.eigh(w)[0]
print(eigenvalues)

val = [w]
for i in range(10):
    val.append([val[-1]@w])

print(val[-1])
norm = list(map(lambda x: tf.norm(x).numpy(), val))
plt.plot(range(1, 12),norm)
plt.show()
11.2.4 梯度裁剪
  1. 在张量上面进行裁剪
a = tf.random.uniform([2, 2])
print(a)
# tf.Tensor(
# [[0.43999135 0.41552186]
#  [0.5206189  0.27707696]], shape=(2, 2), dtype=float32)
print(tf.clip_by_value(a, 0.4, 0.6))
# tf.Tensor(
# [[0.43999135 0.41552186]
#  [0.5206189  0.4       ]], shape=(2, 2), dtype=float32)
  1. 直接裁剪L2范数
a = tf.random.uniform([2, 2]) * 50
b = tf.clip_by_norm(a, 5)
print(a)
# tf.Tensor(
# [[28.309864 28.277618]
#  [29.487293 46.968872]], shape=(2, 2), dtype=float32)
print(b)
# tf.Tensor(
# [[2.0698576 2.0675   ]
#  [2.1559446 3.4340992]], shape=(2, 2), dtype=float32)
print(tf.norm(a), tf.norm(b))
# tf.Tensor(68.38602, shape=(), dtype=float32) tf.Tensor(5.0, shape=(), dtype=float32)
  1. 全局梯度裁剪
w1 = tf.random.normal([3, 3])
w2 = tf.random.normal([3, 3])

global_norm = tf.math.sqrt(tf.norm(w1)**2, tf.norm(w2)**2)

(ww1, ww2), global_norm2 = tf.clip_by_global_norm([w1, w2], 2)
print(global_norm, global_norm2)

global_norm2 = tf.math.sqrt(tf.norm(ww1)**2, tf.norm(ww2)**2)
print(global_norm2)
# tf.Tensor(1.7466415, shape=(), dtype=float32)

可以看到,通过裁剪后,网络参数的梯度组的总范数缩减到max_norm = 2。需要注意的是,tf.clip_by_global_norm 返回裁剪后的张量 List 和 global_norm 这两个对象,其中global_norm 表示裁剪前的梯度总范数和

11.2.5 梯度弥散

对于梯度弥散现象,可以通过增大学习率、减少网络深度、添加 Skip Connection 等一系列的措施抑制。增大学习率𝜃可以在一定程度防止梯度弥散现象,当出现梯度弥散时,网络的梯度∇ 𝜃 ℒ接近于 0,此时若学习率𝜃也较小,如η = 1e − 5,则梯度更新步长更加微小。通过增大学习率,如令𝜃 = 1e − 2,有可能使得网络的状态得到快速更新,从而逃离梯度弥散区域。
对于深层次的神经网络,梯度由最末层逐渐向首层传播,梯度弥散一般更有可能出现在网络的开始数层。在深度残差网络出现之前,几十上百层的深层网络训练起来非常困难,前面数层的网络梯度极容易出现梯度离散现象,从而使得网络参数长时间得不到更新。深度残差网络较好地克服了梯度弥散现象,从而让神经网络层数达到成百上千。一般来说,减少网络深度可以减轻梯度弥散现象,但是网络层数减少后,网络表达能力也会偏弱,需要用户自行平衡。

11.3 LSTM网络

tensorflow 深度学习 龙良曲pdf_ide_07


在 LSTM 中,有两个状态向量c和 ,其中c作为 LSTM 的内部状态向量,可以理解为LSTM 的内存状态向量 Memory,而 表示 LSTM 的输出向量。相对于基础的 RNN 来说,LSTM 把内部 Memory 和输出分开为两个变量,同时利用三个门控:输入门(Input Gate)、**遗忘门(Forget Gate)输出门(Output Gate)**来控制内部信息的流动。门控机制可以理解为控制数据流通量的一种手段,类比于水阀门:当水阀门全部打开时,水流畅通无阻地通过;当水阀门全部关闭时,水流完全被隔断。在 LSTM 中,阀门开和程度利用门控值向量g表示,如图 11.15 所示,通过σ(g)激活函数将门控制压缩到[0,1]之间区间,当σ(g) = 0时,门控全部关闭,输出o = 0;当σ(g) = 1时,门控全部打开,输出o = x。通过门控机制可以较好地控制数据的流量程度。

tensorflow 深度学习 龙良曲pdf_ide_08

11.4 遗忘门

遗忘门作用于 LSTM 状态向量c上面,用于控制上一个时间戳的记忆c t−1 对当前时间戳

的影响。遗忘门的控制变量g f 由

g f = σ(W f [t−1 , x t ] +b f )

产生,如图 11.16 所示,其中W f 和b f 为遗忘门的参数张量,可由反向传播算法自动优化,σ为激活函数,一般使用 Sigmoid 函数。当门控g f = 1时,遗忘门全部打开,LSTM 接受上一个状态c t−1 的所有信息;当门控g f = 0时,遗忘门关闭,LSTM 直接忽略c t−1 ,输出为 0的向量。这也是遗忘门的名字由来。经过遗忘门后,LSTM 的状态向量变为g f c t−1 。

tensorflow 深度学习 龙良曲pdf_tensorflow_09

11.5 输入门

输入门用于控制 LSTM 对输入的接收程度。首先通过对当前时间戳的输入x t 和上一个时间戳的输出 t−1 做非线性变换得到新的输入向量c t :

c t = tanh(W c [t−1 , x t ]+ b c )

其中W c 和b c 为输入门的参数,需要通过反向传播算法自动优化,tanh 为激活函数,用于将输入标准化到[−1,1]区间。c t 并不会全部刷新进入 LSTM 的 Memory,而是通过输入门控制接受输入的量。输入门的控制变量同样来自于输入x t 和输出 t−1 :

g i = σ(W i [t−1 , x t ] +b i )

其中W i 和b i 为输入门的参数,需要通过反向传播算法自动优化,σ为激活函数,一般使用Sigmoid 函数。输入门控制变量g i 决定了 LSTM 对当前时间戳的新输入c t 的接受程度:当g i = 0时,LSTM 不接受任何的新输入c t ;当g i = 1时,LSTM 全部接受新输入c t ,如图11.17 所示。

经过输入门后,待写入 Memory 的向量为g i c t 。

tensorflow 深度学习 龙良曲pdf_卷积核_10

11.6 输出门

LSTM 的内部状态向量c t 并不会直接用于输出,这一点和基础的 RNN 不一样。基础的RNN 网络的状态向量 既用于记忆,又用于输出,所以基础的 RNN 可以理解为状态向量c和输出向量 是同一个对象。在 LSTM 内部,状态向量并不会全部输出,而是在输出门的作用下有选择地输出。输出门的门控变量g o 为:

g o = σ(W o [t−1 , x t ] +b o )

其中W o 和b o 为输出门的参数,同样需要通过反向传播算法自动优化,σ为激活函数,一般使用 Sigmoid 函数。当输出门g o = 0时,输出关闭,LSTM 的内部记忆完全被隔断,无法用作输出,此时输出为 0 的向量;当输出门g o = 1时,输出完全打开,LSTM 的状态向量c t 全部用于输出。LSTM 的输出由:

t= g o ∙ tanh(c t )

产生,即内存向量c t 经过tanh激活函数后与输入门作用,得到 LSTM 的输出。由于g o ∈[0,1],tanh(c t ) ∈ [−1,1],因此 LSTM 的输出 t ∈ [−1,1]。

tensorflow 深度学习 龙良曲pdf_卷积核_11

11.7 LSTM的使用方法
11.7.1 LSTMCell

LSTMCell 的用法和 SimpleRNNCell 基本一致,区别在于 LSTM 的状态变量 List 有两个,即[ t , c t ],需要分别初始化,其中 List 第一个元素为 ht ,第二个元素为c t 。调用 cell完成前向运算时,返回两个元素,第一个元素为 cell 的输出,也就是 ht ,第二个元素为cell 的更新后的状态 List:[ht , ct ]。首先新建一个状态向量长度h = 64的 LSTM Cell,其中状态向量ct 和输出向量 t 的长度都为h,代码如下:

x = tf.random.normal([2, 80, 100])
xt = x[:, 0, :]
cell = layers.LSTMCell(64)

state = [tf.random.normal([2, 64]), tf.random.normal([2, 64])]
# 初始化状态和输出 List,[h,c]  out = h
out, state = cell(xt, state)
print(id(out), id(state[0]), id(state[1]))


for xt in tf.unstack(x, axis=1):
    out, state = cell(xt, state)
11.7.2 LSTM层

经过 LSTM 层前向传播后,默认只会返回最后一个时间戳的输出,如果需要返回每个时间戳上面的输出,需要设置 return_sequences=True 标志。例如:

x = tf.random.normal([2, 80, 100])
# 返回每次输出的时间序列
layer = layers.LSTM(64, return_sequences=True)
out = layer(x)
print(out.shape)
# (2, 80, 64)

# 只返回最后一个时间戳上的输出
layer2 = layers.LSTM(64)
out2 = layer2(x)
print(out2.shape)
# (2, 64)
11.7.3 Sequential写法

对于多层神经网络,可以通过 Sequential 容器包裹多层 LSTM 层,并设置所有非末层网络 return_sequences=True,这是因为非末层的 LSTM 层需要上一层在所有时间戳的输出作为输入。

x = tf.random.normal([2, 80, 100])
net = keras.Sequential([
    layers.LSTM(units=64, return_sequences=True),
    layers.LSTM(units=64, return_sequences=True),
    layers.LSTM(units=80)
])

net.build(input_shape=(2, 50, 100))
net.summary()
11.4 GRU网络

LSTM 具有更长的记忆能力,在大部分序列任务上面都取得了比基础的 RNN 模型更好的性能表现,更重要的是,LSTM 不容易出现梯度弥散现象。但是 LSTM 结构相对较复杂,计算代价较高,模型参数量较大。因此,科学家们尝试简化 LSTM 内部的计算流程,特别是减少门控数量。研究发现,遗忘门是 LSTM 中最重要的门控 [2],甚至发现只有遗忘门的简化版网络在多个基准数据集上面优于标准 LSTM 网络。在众多的简化版 LSTM中,门控循环网络(Gated Recurrent Unit,简称 GRU)是应用最广泛的 RNN 变种之一。GRU把内部状态向量和输出向量合并,统一为状态向量 ,门控数量也减少到 2 个:复位门(Reset Gate)和更新门(Update Gate),如图 11.19 所示。

11.5 复位门

复位门用于控制上一个时间戳的状态 t−1 进入 GRU 的量。门控向量g r 由当前时间戳输入x t 和上一时间戳状态 t−1 变换得到,关系如下:

g r = σ(W r [t−1 , x t ] +b r )

其中W r 和b r 为复位门的参数,由反向传播算法自动优化,σ为激活函数,一般使用

Sigmoid 函数。门控向量g r 只控制状态 t−1 ,而不会控制输入x t :

̃ t = tanh(W [g rt−1 , x t ] +b )

当g r = 0时,新输入 ̃ t 全部来自于输入x t ,不接受 t−1 ,此时相当于复位时, t−1 和输入x t 共同产生新输入 ̃ t ,如图 11.20 所示。

tensorflow 深度学习 龙良曲pdf_tensorflow_12

11.6 更新门

更新门用控制上一时间戳状态向量g z 由t−1 和新输入 t 对新状态向量 t 的影响程度。更新门控

g z = σ(W z [t−1 , x t ] +b z )

得到,其中W z 和b z 为更新门的参数,由反向传播算法自动优化,σ为激活函数,一般使用Sigmoid 函数。g z 用与控制新输入 ̃ t 信号, − g z 用于控制状态 t−1 信号:

t= ( − g z )t−1+ g z ̃ t

可以看到, ̃ t 和 t−1 对 t 的更新量处于相互竞争、此消彼长的状态。当更新门g z = 0时,̃t 全部来自上一时间戳状态 t−1 ;当更新门g z = 1时, t 全部来自新输入 t 。

tensorflow 深度学习 龙良曲pdf_卷积_13

11.7 GRU的使用方法
# GRU cell
x = tf.random.normal([2, 80, 100])
xt = x[:, 0, :]
h = [tf.zeros([2, 64])]
cell = layers.GRUCell(64)
out, h = cell(xt, h)
print(out.shape)
# (2, 64)

# GRU
x = tf.random.normal([2, 80, 100])
net = keras.Sequential([
    layers.GRU(64, return_sequences=True),
    layers.GRU(64)
])
out = net(x)
print(out.shape)
# (2, 64)
11.8 LSTM/GRU情况分类问题再战,并使用GloVe预训练词向量
import numpy as np


print("Indexing word vectors")
embeddings_index = {}

GLOVE_DIR = "glove.6B.100d.txt"
with open(GLOVE_DIR, encoding="utf-8") as f:
    for index, line in enumerate(f):
        values = line.split()
        word = values[0]
        coefs = np.asarray(values[1:], dtype="float32")
        embeddings_index[word] = coefs
print("found %s word vectors" % len(embeddings_index))


batchsz = 128
total_words = 10000
max_review_len = 80
embedding_len = 100
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)

print(x_train.shape, len(x_train[1]), y_train.shape)
print(x_test.shape, len(x_test[0]), y_test.shape)

# 打印编码表
word_index = keras.datasets.imdb.get_word_index()
for k, v in word_index.items():
    print(k, v)
    break

word_index = {k: (v+3) for k, v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2
word_index["<UNUSED>"] = 3

reverse_word_index = {v: k for k, v in word_index.items()}


# 尝试反编码并打印原来的句子
def decode_review(text):
    return " ".join([reverse_word_index.get(word, "?") for word in text])
print(decode_review(x_train[0]))


num_words = min(total_words, len(word_index))
embedding_matrix = np.zeros((num_words, embedding_len))
for word, i in word_index.items():
    if i >= total_words:
        continue
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector
print(embedding_matrix.shape)


x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len)
print(decode_review(x_train[0]))

db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# drop_remainder丢弃最后一个长度不满足设定batchsize的batch
db_train = db_train.shuffle(1000).batch(batchsz, drop_remainder=True)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)

print(x_train.shape, tf.reduce_max(y_train), tf.reduce_min(y_train))


class MyRNN(keras.Model):
    def __init__(self, units):
        super(MyRNN, self).__init__()
        self.state0 = [tf.zeros([batchsz, units])]
        self.state1 = [tf.zeros([batchsz, units])]
        # 直接使用GloVe来初始化embedding层不参与训练
        self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
        self.embedding.build(input_shape=(None, max_review_len))
        self.embedding.set_weights([embedding_matrix])

        self.gru_cell0 = layers.GRUCell(units=units, dropout=0.5)
        self.gru_cell1 = layers.GRUCell(units=units, dropout=0.5)
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None, mask=None):
        x = inputs
        x = self.embedding(x)
        state0 = self.state0
        state1 = self.state1
        # 这里每次喂进去的是这个batch的所有句子的第1个, 第2个,  .... 第n个单词
        for word in tf.unstack(x, axis=1):
            out0, state0 = self.gru_cell0(word, state0, training)
            out1, state1 = self.gru_cell1(out0, state1, training)
        x = self.outlayer(out1)
        prob = tf.sigmoid(x)
        return prob


def main():
    units = 64
    epochs = 20
    model = MyRNN(units)
    model.compile(optimizer=keras.optimizers.Adam(0.001), loss=keras.losses.binary_crossentropy,
                  metrics=["accuracy"])
    model.fit(db_train, epochs=epochs, validation_data=db_test)
    model.evaluate(db_test)


if __name__ == '__main__':
    main()

12. 自编码器

面对海量的无标注数据,有没有办法能够从中学习到数据的分布P(x)的算法?这就是我们这章要介绍的无监督学习(Unsupervised Learning)算法。特别地,如果算法把x作为监督信号来学习,这类算法称为自监督学习(Self-supervised Learning),本章要介绍的自编码器算法就是属于自监督学习范畴。

12.1 自编码器的原理

PCA
让我们来考虑有监督学习中神经网络的功能:
o = f θ (x), x ∈ R d in , o ∈ R d out
d in 是输入的特征向量长度,d out 是网络输出的向量长度。对于分类问题,网络模型通过把
长度为d in 输入特征向量x变换到长度为d out 的输出向量o,这个过程可以看成是特征降的过程,把原始的高维输入向量x变换到低维的变量o。特征降维(Dimensionality Reduction)在机器学习中有广泛的应用,比如文件压缩(Compression)、数据预处(Preprocessing)等。最常见的降维算法有主成分分析法(Principal components analysis,简称 PCA),通过对协方差矩阵进行特征分解而得到数据的主要成分,但是 PCA 本质上是一种线性变换,提取特征的
能力极为有限。

自编码器

我们尝试着利用数据x本身作为监督信号来指导网络的训练,即希望神经网络能够学习到映射f θ : x → x。我们把网络f θ 切分为两个部分,前面的子网络尝试学习映射关系:g θ 1 : x → z,后面的子网络尝试学习映射关系h θ 2 : z → x,如图 12.1 所示。我们把g θ 1 看成一个数据编码(Encode)的过程,把高维度的输入x编码成低维度的隐变量z(Latent Variable,或隐藏变量),称为 Encoder 网络(编码器);h θ 2 看成数据解码(Decode)的过程,把编码过后的输入z解码为高维度的x,称为 Decoder 网络(解码器)。

tensorflow 深度学习 龙良曲pdf_卷积_14


编码器和解码器共同完成了输入数据x的编码和解码过程,我们把整个网络模型f θ 叫做自动编码器(Auto-Encoder),简称自编码器。如果使用深层神经网络来参数化g θ 1 和h θ 2 函数,则称为**深度自编码器(Deep Auto-encoder),**如图 12.2 所示

tensorflow 深度学习 龙良曲pdf_卷积_15


自编码器能够将输入变换到隐藏向量z,并通过解码器重建(Reconstruct,或恢复)出x 。我们希望解码器的输出能够完美地或者近似恢复出原来的输入,即x ≈ x,那么,自编码器的优化目标可以写成:

tensorflow 深度学习 龙良曲pdf_ide_16


其中dist(x, x )表示 x和x 的距离度量,称为重建误差函数。最常见的度量方法有欧氏距离(Euclidean distance)的平方,计算方法如下:

tensorflow 深度学习 龙良曲pdf_卷积_17

12.2 自编码器的实战,重建MNIST
import numpy as np


batchsz = 100
h_dim = 20

(x_train, y_train), (x_test, y_test) = keras.datasets.fashion_mnist.load_data()
x_train, x_test = x_train.astype(np.float32) / 255., x_test.astype(np.float32) / 255.
train_db = tf.data.Dataset.from_tensor_slices(x_train)
train_db = train_db.shuffle(batchsz * 5).batch(batchsz, drop_remainder=True)

test_db = tf.data.Dataset.from_tensor_slices(x_test)
test_db = test_db.batch(batchsz)

print("x_train shape {}".format(x_train.shape), "x_test shape {}".format(x_test.shape))


class AE(keras.Model):
    def __init__(self):
        super(AE, self).__init__()
        self.encoder = keras.Sequential([
            layers.Dense(256, activation=tf.nn.relu),
            layers.Dense(128, activation=tf.nn.relu),
            layers.Dense(h_dim)
        ])
        self.decoder = keras.Sequential([
            layers.Dense(128, activation=tf.nn.relu),
            layers.Dense(256, activation=tf.nn.relu),
            layers.Dense(784)
        ])

    def call(self, inputs, training=None, mask=None):
        h = self.encoder(inputs)
        x_hat = self.decoder(h)
        return x_hat

model = AE()
model.build(input_shape=(4, 784))
model.summary()

from PIL import Image


def save_images(imgs, name):
    new_im = Image.new("L", (280, 280))
    index = 0
    for i in range(0, 280, 28):
        for j in range(0, 280, 28):
            im = imgs[index]
            im = Image.fromarray(im, mode="L")
            new_im.paste(im, (j, i))
            index += 1
    new_im.save(name)


for epoch in range(10):
    for step, x in enumerate(train_db):
        x = tf.reshape(x, [-1, 784])
        with tf.GradientTape() as tape:
            x_rec_logits = model(x)
            rec_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=x, logits=x_rec_logits)
            rec_loss = tf.reduce_mean(rec_loss)
        grads = tape.gradient(rec_loss, model.trainable_variables)
        optimizer = keras.optimizers.Adam()
        optimizer.apply_gradients(zip(grads, model.trainable_variables))

        if step % 100 == 0:
            print(epoch, step, float(rec_loss))

    x = next(iter(test_db))
    logits = model(tf.reshape(x, [-1, 784]))
    x_hat = tf.sigmoid(logits)
    x_hat = tf.reshape(x_hat, [-1, 28, 28])

    x_concat = tf.concat([x[:50], x_hat[:50]], axis=0)
    x_concat = x_concat.numpy() * 255.
    x_concat = x_concat.astype(np.uint8)
    save_images(x_concat, "ae_images/rec_epoch_%d.png" % epoch)
12.3 自编码器的变种
  • Denoising Auto-Encoder

    -Dropout Auto-Encoder
    自编码器网络同样面临过拟合的风险,Dropout Auto-Encoder 通过随机断开网络的连接来减少网络的表达能力,防止过拟合。Dropout Auto-Encoder 实现非常简单,通过在网络层中插入 Dropout 层即可实现网络连接的随机断开。
  • Adversarial Auto-Encoder
    为了能够方便地从某个已知的先验分布中p(z)采样隐藏变量z,方便利用p(z)来重建输入,对抗自编码器(Adversarial Auto-Encoder)利用额外的判别器网络(Discriminator,简称 D网络)来判定降维的隐藏变量z是否采样自先验分布p(z),如图 12.10 所示。判别器网络的输出为一个属于[0,1]区间的变量,表征隐藏向量是否采样自先验分布p(z):所有采样自先验分布p(z)的z标注为真,采样自编码器的条件概率q(z|x)的z标注为假。通过这种方式训练,除了可以重建样本,还可以约束条件概率分布q(z|x)逼近先验分布p(z)。

    -VAE
  • Reparameterization Trick
    Reparameterization 图片生成实例
import numpy as np
from PIL import Image

def save_images(imgs, name):
    new_im = Image.new("L", (280, 280))
    index = 0
    for i in range(0, 280, 28):
        for j in range(0, 280, 28):
            im = imgs[index]
            im = Image.fromarray(im, mode="L")
            new_im.paste(im, (j, i))
            index += 1
    new_im.save(name)

batchsz = 100
z_dim = 100

(x_train, y_train), (x_test, y_test) = keras.datasets.fashion_mnist.load_data()
x_train, x_test = x_train.astype(np.float32) / 255., x_test.astype(np.float32) / 255.
train_db = tf.data.Dataset.from_tensor_slices(x_train)
train_db = train_db.shuffle(batchsz * 5).batch(batchsz, drop_remainder=True)
test_db = tf.data.Dataset.from_tensor_slices(x_test)
test_db = test_db.batch(batchsz)


class VAE(keras.Model):
    def __init__(self):
        super(VAE, self).__init__()
        self.fc1 = layers.Dense(128)
        self.fc2 = layers.Dense(z_dim)
        self.fc3 = layers.Dense(z_dim)

        self.fc4 = layers.Dense(128)
        self.fc5 = layers.Dense(784)

    def encoder(self, x):
        h = tf.nn.relu(self.fc1(x))
        mu = self.fc2(h)
        log_var = self.fc3(h)
        return mu, log_var

    def decoder(self, z):
        out = tf.nn.relu(self.fc4(z))
        out = self.fc5(out)
        return out

    def reparameterize(self, mu, log_var):
        eps = tf.random.normal(log_var.shape)
        std = tf.exp(log_var) ** 0.5
        z = mu + std * eps
        return z

    def call(self, inputs, training=None, mask=None):
        mu, log_var = self.encoder(inputs)
        z = self.reparameterize(mu, log_var)
        x_hat = self.decoder(z)
        return x_hat, mu, log_var

model = VAE()
model.build(input_shape=(4, 784))
model.summary()

optimizer = keras.optimizers.Adam()

for epoch in range(5):
    for step, x in enumerate(train_db):
        x = tf.reshape(x, [-1, 784])
        with tf.GradientTape() as tape:
            x_rec_logits, mu, log_var = model(x)
            rec_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=x, logits=x_rec_logits)
            rec_loss = tf.reduce_sum(rec_loss) / x.shape[0]
            kl_div = -0.5 * (log_var + 1 - mu ** 2 - tf.exp(log_var))
            kl_div = tf.reduce_sum(kl_div) / x.shape[0]
            loss = rec_loss + 1. * kl_div
        grads = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(grads, model.trainable_variables))

        if step % 100 == 0:
            # 打印训练误差
            print(epoch, step, 'kl div:', float(kl_div), 'rec loss:', float(rec_loss))

    z = tf.random.normal((batchsz, z_dim))
    logits = model.decoder(z)

    x_hat = tf.sigmoid(logits)
    x_hat = tf.reshape(x_hat, [-1, 28, 28]).numpy()*225.
    x_hat = x_hat.astype(np.uint8)
    save_images(x_hat, 'vae_images/epoch_%d_sampled.png' % epoch)

    x = next(iter(test_db))
    logits, _,  _ = model(tf.reshape(x, [-1, 784]))
    x_hat = tf.sigmoid(logits)
    x_hat = tf.reshape(x_hat, [-1, 28, 28])
    x_concat = tf.concat([x[:50], x_hat[:50]], axis=0)
    x_concat = x_concat.numpy()*255.
    x_concat = x_concat.astype(np.uint8)
    save_images(x_concat, 'vae_images/epoch_%d_rec.png' % epoch)

13. 生产对抗网络GAN

生成网络G

生成网络 G 和自编码器的 Decoder 功能类似,从先验分布p z (∙)中采样隐藏变量z~p z (∙),通过生成网络 G 参数化的p g (x|z)分布,获得生成样本x~p g (x|z),如图13.3 所示。其中隐藏变量z的先验分布p z (∙)可以假设为某中已知的分布,比如多元均匀分布z~Uniform(−1,1)。

tensorflow 深度学习 龙良曲pdf_ide_18


p g (x|z)可以用深度神经网络来参数化,如下图 13.4 所示,从均匀分布p z (∙)中采样出隐藏变量z,经过多层转置卷积层网络参数化的p g (x|z)分布中采样出样本x f 。从输入输出层面来看,生成器 G 的功能是将隐向量z通过神经网络转换为样本向量x f ,下标f代表假样本(Fake samples)

tensorflow 深度学习 龙良曲pdf_ide_19


判别网络D

判别网络和普通的二分类网络功能类似,它接受输入样本x的数据集,包含了采样自真实数据分布p r (∙)的样本x r ~p r (∙),也包含了采样自生成网络的假样本x f ~p g (x|z),x r 和x f 共同组成了判别网络的训练数据集。判别网络输出为x属于真实样本的概率P(x为真|x),我们把所有真实样本x r 的标签标注为真(1),所有生成网络产生的样本x f 标注为假(0),通过最小化判别网络 D 的预测值与标签之间的误差来优化判别网络参数,

tensorflow 深度学习 龙良曲pdf_卷积_20

13.1 GAN的实战

以下是以cifar数据集做实战,利用GAN生成新的图像,discriminator的输入为(4, 32, 32, 3), generator的输入为(4, 100),其中为了满足输入输出的shape, generator的第二层,discriminator的第一层注释

batch_size = 4
z_dim = 100

class Generator(keras.Model):
    def __init__(self):
        super(Generator, self).__init__()
        filter = 64
        self.conv1 = layers.Conv2DTranspose(filters=filter*8, kernel_size=4, strides=1, padding="valid", use_bias=False)
        self.bn1 = layers.BatchNormalization()

        # self.conv2 = layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False)
        # self.bn2 = layers.BatchNormalization()

        self.conv3 = layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False)
        self.bn3 = layers.BatchNormalization()

        self.conv4 = layers.Conv2DTranspose(filters=filter*1, kernel_size=4, strides=2, padding="same", use_bias=False)
        self.bn4 = layers.BatchNormalization()

        self.conv5 = layers.Conv2DTranspose(filters=3, kernel_size=4, strides=2, padding="same", use_bias=False)

    def call(self, inputs, training=None, mask=None):
        x = inputs
        x = tf.reshape(x, (x.shape[0], 1, 1, x.shape[1]))
        x = tf.nn.relu(x)
        x = tf.nn.relu(self.bn1(self.conv1(x), training=training))
        # x = tf.nn.relu(self.bn2(self.conv2(x), training=training))
        x = tf.nn.relu(self.bn3(self.conv3(x), training=training))
        x = tf.nn.relu(self.bn4(self.conv4(x), training=training))
        x = self.conv5(x)
        x = tf.tanh(x)
        return x

# generator = Generator()
# generator.build(input_shape=(32, 1024))
# generator.summary()
# x = tf.random.normal([32, 1024])
# y = generator(x)
# print(y.shape)


class Discriminator(keras.Model):
    def __init__(self):
        super(Discriminator, self).__init__()
        filter = 64

        # self.conv1 = layers.Conv2D(filter, 4, 2, "valid", use_bias=False)
        # self.bn1 = layers.BatchNormalization()

        self.conv2 = layers.Conv2D(filter*2, 4, 2, "valid", use_bias=False)
        self.bn2 = layers.BatchNormalization()

        self.conv3 = layers.Conv2D(filter*4, 4, 2, "valid", use_bias=False)
        self.bn3 = layers.BatchNormalization()

        self.conv4 = layers.Conv2D(filter*8, 3, 1, "valid", use_bias=False)
        self.bn4 = layers.BatchNormalization()

        self.conv5 = layers.Conv2D(filter*16, 3, 1, "valid", use_bias=False)
        self.bn5 = layers.BatchNormalization()

        self.pool = layers.GlobalAveragePooling2D()

        self.flatten = layers.Flatten()
        self.fc = layers.Dense(1)

    def call(self, inputs, training=None, mask=None):
        # x = tf.nn.leaky_relu(self.bn1(self.conv1(inputs), training=training))

        x = tf.nn.leaky_relu(self.bn2(self.conv2(inputs), training=training))
        x = tf.nn.leaky_relu(self.bn3(self.conv3(x), training=training))
        x = tf.nn.leaky_relu(self.bn4(self.conv4(x), training=training))
        x = tf.nn.leaky_relu(self.bn5(self.conv5(x), training=training))
        x = self.pool(x)
        x = self.flatten(x)
        logits = self.fc(x)
        return logits

# discriminator = Discriminator()
# discriminator.build(input_shape=(32, 64, 64, 3))
# discriminator.summary()
# x = tf.random.normal([32, 64, 64, 3])
# y = discriminator(x)
# print(y.shape)

# filter = 64
# generator = keras.Sequential([
#
#         layers.Conv2DTranspose(filters=filter*8, kernel_size=4, strides=1, padding="valid", use_bias=False),
#         layers.BatchNormalization(),
#
#         # layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False),
#         # layers.BatchNormalization(),
#
#         layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.Conv2DTranspose(filters=filter*1, kernel_size=4, strides=2, padding="same", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.Conv2DTranspose(filters=3, kernel_size=4, strides=2, padding="same", use_bias=False),
# ])
#
# discriminator = keras.Sequential([
#         # layers.Conv2D(filter, 4, 2, "valid", use_bias=False),
#         # layers.BatchNormalization(),
#
#         layers.Conv2D(filter*2, 4, 2, "valid", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.Conv2D(filter*4, 4, 2, "valid", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.Conv2D(filter*8, 3, 1, "valid", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.Conv2D(filter*16, 3, 1, "valid", use_bias=False),
#         layers.BatchNormalization(),
#
#         layers.GlobalAveragePooling2D(),
#
#         layers.Flatten(),
#         layers.Dense(1),
# ])


def celoss_one(logits):
    y = tf.ones_like(logits)
    loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
    return tf.reduce_mean(loss)


def celoss_zeros(logits):
    y = tf.zeros_like(logits)
    loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
    return tf.reduce_mean(loss)


def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
    fake_image = generator(batch_z, is_training)
    d_fake_logits = discriminator(fake_image, is_training)

    d_real_logits = discriminator(batch_x, is_training)

    d_loss_real = celoss_one(d_real_logits)
    d_loss_fake = celoss_zeros(d_fake_logits)

    loss = d_loss_fake + d_loss_real
    return loss


def g_loss_fn(generator, discriminator, batch_z, is_training):
    fake_image = generator(batch_z, is_training)
    d_fake_logits = discriminator(fake_image, is_training)
    loss = celoss_one(d_fake_logits)
    return loss

generator = Generator()
# 如果用load_weights就不要做build否则会报错
generator.load_weights("./gan_model/generator/gan_model_generator_weights.ckpt.index")
# generator.build(input_shape=(4, z_dim))
# generator.summary()

discriminator = Discriminator()
# 如果用load_weights就不要做build否则会报错
discriminator.load_weights("./gan_model/discriminator/gan_model_discriminator_weights.ckpt.index")
# discriminator.build(input_shape=(4, 32, 32, 3))
# discriminator.summary()

g_optimizer = keras.optimizers.Adam()
d_optimizer = keras.optimizers.Adam()


(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
print(x_test.shape, y_train.shape)
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(4)
db_iter = iter(db_train)

from PIL import Image
import numpy as np


def save_images(imgs, name):
    new_im = Image.new("L", (64, 64))
    index = 0
    for i in range(0, 64, 32):
        for j in range(0, 64, 32):
            im = imgs[index]
            im = Image.fromarray(np.uint8(im), mode="RGB")
            new_im.paste(im, (j, i))
            index += 1
    new_im.save(name)


for epoch in range(10000):
    for _ in range(5):
        batch_z = tf.random.normal([batch_size, z_dim])
        batch_x, _ = next(db_iter)
        batch_x = tf.cast(batch_x, tf.float32)
        with tf.GradientTape() as tape:
            d_loss = d_loss_fn(generator, discriminator, batch_z, batch_x, is_training=True)
            grads = tape.gradient(d_loss, discriminator.trainable_variables)
            d_optimizer.apply_gradients(zip(grads, discriminator.trainable_variables))

    batch_z = tf.random.normal([batch_size, z_dim])
    batch_x, _ = next(db_iter)
    batch_x = tf.cast(batch_x, tf.float32)
    with tf.GradientTape() as tape:
        g_loss = g_loss_fn(generator, discriminator, batch_z, is_training=True)
        grads = tape.gradient(g_loss, generator.trainable_variables)
        g_optimizer.apply_gradients(zip(grads, generator.trainable_variables))

    print("epoch {}, g_loss {}, d_loss {}".format(epoch, g_loss, d_loss))
    if epoch % 10 == 0:
        generator.save_weights("./gan_model/generator/gan_model_generator_weights.ckpt")
        discriminator.save_weights("./gan_model/discriminator/gan_model_discriminator_weights.ckpt")

        imgs = generator(batch_z)
        imgs = (imgs + 1.) / 2. * 255.
        imgs = imgs.numpy()
        save_images(batch_x, "./gan_images/epoch{}_save_origin_image.png".format(epoch))
        save_images(imgs, "./gan_images/epoch{}_save_image.png".format(epoch))
13.2 GAN的变种
13.2.1 DCGAN

最初始的 GAN 网络主要基于全连接层实现生成器 G 和判别器 D 网络,由于图片的维度较高,网络参数量巨大,训练的效果并不优秀。DCGAN [2]提出了使用转置卷积层实现的生成网络,普通卷积层来实现的判别网络,大大地降低了网络参数量,同时图片的生成效果也大幅提升,展现了 GAN 模型在图片生成效果上超越 VAE 模型的潜质。此外,DCGAN 作者还提出了一系列经验性的 GAN 网络训练技巧,这些技巧在 WGAN 提出之前被证实有益于网络的稳定训练。前面我们已经使用 DCGAN 模型完成了二次元动漫头像的图片生成实战.

13.2.2 infoGAN

(Interpretable Representation),即希望隐向量𝒛能够对应到数据的语义特征。比如对于MNIST 手写数字图片,我们可以认为数字的类别、字体大小和书写风格等是图片的隐藏变量,希望模型能够学习到这些分离的(Disentangled)可解释特征表示方法,从而可以通人为控制隐变量来生成指定内容的样本。对于 CelebA 名人照片数据集,希望模型可以把发型、眼镜佩戴情况、面部表情等特征分隔开,从而生成指定形态的人脸图片。分离的可解释特征有什么好处呢?它可以让神经网络的可解释性更强,比如𝒛包含了一些分离的可解释特征,那么我们可以通过仅仅改变这一个位置上面的特征来获得不同语义的生成数据,如图 13.10 所示,通过将“戴眼镜男士”与“不戴眼镜男士”的隐向量相减,并与“不戴眼镜女士”的隐向量相加,可以生成“戴眼镜女士”的生成图片。

13.2.3 CycleGAN
13.2.4 EqualGAN

GAN 的训练问题一直被诟病,很容易出现训练不收敛和模式崩塌的现象。WGAN [5]从理论层面分析了原始的 GAN 使用 JS 散度存在的缺陷,并提出了可以使用 Wasserstein 距离来解决这个问题。在 WGAN-GP [6]中,作者提出了通过添加梯度惩罚项,从工程层很好的实现了 WGAN 算法,并且实验性证实了 WGAN 训练稳定的优点。

13.2.5 BigGAN

在 SAGAN 的基础上,BigGAN [9]尝试将 GAN 的训练扩展到大规模上去,利用正交正则化等技巧保证训练过程的稳定性。BigGAN 的意义在于启发人们,GAN 网络的训练同样可以从大数据、大算力等方面受益。BigGAN 图片生成效果达到了前所未有的高度:Inception score 记录提升到 166.5(提高了 52.52);Frechet Inception Distance 下降到7.4,降低了 18.65,如图 13.13 所示,图片的分辨率可达512 × 512,图片细节极其逼真。

13.3 WGAN实战,决策器加入惩罚项
batch_size = 4
z_dim = 100


class Generator(keras.Model):
    def __init__(self):
        super(Generator, self).__init__()
        filter = 64
        self.conv1 = layers.Conv2DTranspose(filters=filter*8, kernel_size=4, strides=1, padding="valid", use_bias=False)
        self.bn1 = layers.BatchNormalization()

        # self.conv2 = layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False)
        # self.bn2 = layers.BatchNormalization()

        self.conv3 = layers.Conv2DTranspose(filters=filter*2, kernel_size=4, strides=2, padding="same", use_bias=False)
        self.bn3 = layers.BatchNormalization()

        self.conv4 = layers.Conv2DTranspose(filters=filter*1, kernel_size=4, strides=2, padding="same", use_bias=False)
        self.bn4 = layers.BatchNormalization()

        self.conv5 = layers.Conv2DTranspose(filters=3, kernel_size=4, strides=2, padding="same", use_bias=False)

    def call(self, inputs, training=None, mask=None):
        x = inputs
        x = tf.reshape(x, (x.shape[0], 1, 1, x.shape[1]))
        x = tf.nn.relu(x)
        x = tf.nn.relu(self.bn1(self.conv1(x), training=training))
        # x = tf.nn.relu(self.bn2(self.conv2(x), training=training))
        x = tf.nn.relu(self.bn3(self.conv3(x), training=training))
        x = tf.nn.relu(self.bn4(self.conv4(x), training=training))
        x = self.conv5(x)
        x = tf.tanh(x)
        return x

# generator = Generator()
# generator.build(input_shape=(32, 1024))
# generator.summary()
# x = tf.random.normal([32, 1024])
# y = generator(x)
# print(y.shape)


class Discriminator(keras.Model):
    def __init__(self):
        super(Discriminator, self).__init__()
        filter = 64

        # self.conv1 = layers.Conv2D(filter, 4, 2, "valid", use_bias=False)
        # self.bn1 = layers.BatchNormalization()

        self.conv2 = layers.Conv2D(filter*2, 4, 2, "valid", use_bias=False)
        self.bn2 = layers.BatchNormalization()

        self.conv3 = layers.Conv2D(filter*4, 4, 2, "valid", use_bias=False)
        self.bn3 = layers.BatchNormalization()

        self.conv4 = layers.Conv2D(filter*8, 3, 1, "valid", use_bias=False)
        self.bn4 = layers.BatchNormalization()

        self.conv5 = layers.Conv2D(filter*16, 3, 1, "valid", use_bias=False)
        self.bn5 = layers.BatchNormalization()

        self.pool = layers.GlobalAveragePooling2D()

        self.flatten = layers.Flatten()
        self.fc = layers.Dense(1)

    def call(self, inputs, training=None, mask=None):
        # x = tf.nn.leaky_relu(self.bn1(self.conv1(inputs), training=training))

        x = tf.nn.leaky_relu(self.bn2(self.conv2(inputs), training=training))
        x = tf.nn.leaky_relu(self.bn3(self.conv3(x), training=training))
        x = tf.nn.leaky_relu(self.bn4(self.conv4(x), training=training))
        x = tf.nn.leaky_relu(self.bn5(self.conv5(x), training=training))
        x = self.pool(x)
        x = self.flatten(x)
        logits = self.fc(x)
        return logits

def celoss_one(logits):
    y = tf.ones_like(logits)
    loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
    return tf.reduce_mean(loss)


def celoss_zeros(logits):
    y = tf.zeros_like(logits)
    loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
    return tf.reduce_mean(loss)


def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
    fake_image = generator(batch_z, is_training)
    d_fake_logits = discriminator(fake_image, is_training)
    d_real_logits = discriminator(batch_x, is_training)
    # 计算梯度惩罚项
    gp = gradient_penalty(discriminator, batch_x, fake_image)
    loss = tf.reduce_mean(d_fake_logits) - tf.reduce_mean(d_real_logits) + 10. * gp
    return loss, gp


def g_loss_fn(generator, discriminator, batch_z, is_training):
    fake_image = generator(batch_z, is_training)
    d_fake_logits = discriminator(fake_image, is_training)
    loss = - tf.reduce_mean(d_fake_logits)
    return loss


def gradient_penalty(discriminator, batch_x, fake_image):
    batchsz = batch_x.shape[0]
    t = tf.random.uniform([batchsz, 1, 1, 1])
    t = tf.broadcast_to(t, batch_x.shape)
    interplate = t * batch_x + (1 - t) * fake_image

    with tf.GradientTape() as tape:
        tape.watch([interplate])
        d_interplote_logits = discriminator(interplate)
    grads = tape.gradient(d_interplote_logits, interplate)
    grads = tf.reshape(grads, [grads.shape[0], -1])
    gp = tf.norm(grads, axis=1)
    gp = tf.reduce_mean((gp - 1.) ** 2)
    return gp


generator = Generator()
# 如果用load_weights就不要做build否则会报错
# generator.load_weights("./gan_model/generator/wgan_model_generator_weights.ckpt.index")
generator.build(input_shape=(4, z_dim))
generator.summary()

discriminator = Discriminator()
# 如果用load_weights就不要做build否则会报错
# discriminator.load_weights("./gan_model/discriminator/wgan_model_discriminator_weights.ckpt.index")
discriminator.build(input_shape=(4, 32, 32, 3))
discriminator.summary()

g_optimizer = keras.optimizers.Adam()
d_optimizer = keras.optimizers.Adam()


(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
print(x_test.shape, y_train.shape)
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(4)
db_iter = iter(db_train)

from PIL import Image
import numpy as np


def save_images(imgs, name):
    new_im = Image.new("L", (64, 64))
    index = 0
    for i in range(0, 64, 32):
        for j in range(0, 64, 32):
            im = imgs[index]
            im = Image.fromarray(np.uint8(im), mode="RGB")
            new_im.paste(im, (j, i))
            index += 1
    new_im.save(name)


for epoch in range(10000):
    for _ in range(5):
        batch_z = tf.random.normal([batch_size, z_dim])
        batch_x, _ = next(db_iter)
        batch_x = tf.cast(batch_x, tf.float32)
        with tf.GradientTape() as tape:
            d_loss, _ = d_loss_fn(generator, discriminator, batch_z, batch_x, is_training=True)
            grads = tape.gradient(d_loss, discriminator.trainable_variables)
            d_optimizer.apply_gradients(zip(grads, discriminator.trainable_variables))

    batch_z = tf.random.normal([batch_size, z_dim])
    batch_x, _ = next(db_iter)
    batch_x = tf.cast(batch_x, tf.float32)
    with tf.GradientTape() as tape:
        g_loss = g_loss_fn(generator, discriminator, batch_z, is_training=True)
        grads = tape.gradient(g_loss, generator.trainable_variables)
        g_optimizer.apply_gradients(zip(grads, generator.trainable_variables))

    print("epoch {}, g_loss {}, d_loss {}".format(epoch, g_loss.numpy(), d_loss.numpy()))
    if epoch % 10 == 0:
        generator.save_weights("./wgan_model/generator/gan_model_generator_weights.ckpt")
        discriminator.save_weights("./wgan_model/discriminator/gan_model_discriminator_weights.ckpt")

        imgs = generator(batch_z)
        imgs = (imgs + 1.) / 2. * 255.
        imgs = imgs.numpy()
        save_images(batch_x, "./wgan_images/epoch{}_save_origin_image.png".format(epoch))
        save_images(imgs, "./wgan_images/epoch{}_save_image.png".format(epoch))

14. 迁移学习

迁移学习(Transfer Learning)是机器学习的一个研究方向,主要研究如何将任务 A 上面学习到的知识迁移到任务 B 上,以提高在任务 B 上的泛化性能。例如任务 A 为猫狗分类问题,需要训练一个分类器能够较好的分辨猫和狗的样本图片,任务 B 为牛羊分类问题。可以发现,任务 A 和任务 B 存在大量的共享知识,比如这些动物都可以从毛发、体型、形态、发色等方面进行辨别。因此在任务 A 训练获得的分类器已经掌握了这部份知识,在训练任务 B 的分类器时,可以不从零开始训练,而是在任务 A 上获得的知识的基础上面进行训练微调(Fine-tuning),这和“站在巨人的肩膀上”思想非常类似。通过迁移任务 A 上学习的知识,在任务 B 上训练分类器可以使用更少的样本和更少的训练代价,并且获得不错的泛化能力

from tensorflow import keras
from tensorflow.keras import layers


net = keras.applications.DenseNet121(weights="imagenet", include_top=False, pooling="max")
net.trainable = False

newnet = keras.Sequential([
    net,
    layers.Dense(1024, activation="relu"),
    layers.BatchNormalization(),
    layers.Dropout(rate=0.5),
    layers.Dense(5)
])
newnet.build(input_shape=(4, 224, 224, 3))
newnet.summary()