Keras 神经网络秘籍(三)

原文:annas-archive.org/md5/b038bef44808c012b36120474e9e0841

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:自动驾驶汽车中的图像分析应用

在前几章中,我们学习了物体分类以及物体定位。在本章中,我们将通过多个与自动驾驶汽车相关的案例研究。

你将学习以下内容:

  • 交通标志识别

  • 预测汽车需要转动的角度范围

  • 使用 U-net 架构识别道路上的汽车

  • 路面上物体的语义分割

交通标志识别

在本案例研究中,我们将了解如何将信号分类为 43 种可能的类别之一。

准备就绪

对于本练习,我们将采用以下策略:

  1. 下载包含所有可能交通标志的数据集

  2. 对输入图像执行直方图归一化处理:

    • 某些图像是在明亮的白天拍摄的,而其他一些可能是在黄昏时拍摄的

    • 不同的光照条件会导致像素值的变化,具体取决于拍摄照片时的光照条件

    • 直方图归一化对像素值进行归一化处理,使它们具有相似的分布

  3. 缩放输入图像

  4. 构建、编译并拟合模型以减少类别交叉熵损失值

如何实现…

  1. 下载数据集,如下所示(代码文件可在 GitHub 中的Traffic_signal_detection.ipynb找到)。数据集可通过论文获得:J. Stallkamp, M. Schlipsing, J. Salmen, C. Igel, 《人与计算机:基准机器学习算法在交通标志识别中的表现》:
$ wget http://benchmark.ini.rub.de/Dataset/GTSRB_Final_Training_Images.zip
$ unzip GTSRB_Final_Training_Images.zip
  1. 将图像路径读取到列表中,如下所示:
from skimage import io
import os
import glob

root_dir = '/content/GTSRB/Final_Training/Images/'
all_img_paths = glob.glob(os.path.join(root_dir, '*/*.ppm'))

图像的样例如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/12bf31d5-f7a6-4255-afeb-78f1abe0d861.png

请注意,某些图像的形状较小,而某些图像的光照较强。因此,我们需要对图像进行预处理,使所有图像在光照和形状方面都进行标准化。

  1. 对输入数据集执行直方图归一化处理,如下所示:
import numpy as np
from skimage import color, exposure, transform

NUM_CLASSES = 43
IMG_SIZE = 48

def preprocess_img(img):
     hsv = color.rgb2hsv(img)
     hsv[:, :, 2] = exposure.equalize_hist(hsv[:, :, 2])
     img = color.hsv2rgb(hsv)
     img = transform.resize(img, (IMG_SIZE, IMG_SIZE))
     return img

在前面的代码中,我们首先将 RGB 格式的图像转换为**色调饱和度值(HSV)**格式。通过将图像从 RGB 格式转换为 HSV 格式,我们实质上是将 RGB 组合值转换为一个数组,然后再将其转换为单维数组。

然后,我们将使用equalize_hist方法对以 HSV 格式获得的值进行归一化,使它们归于相同的尺度。

一旦图像在 HSV 格式的最后一个通道中被归一化,我们将它们转换回 RGB 格式。

最后,我们将图像调整为标准尺寸。

  1. 检查图像在通过直方图归一化之前的状态,并将其与归一化后的状态进行对比(即通过preprocess_img函数处理后的图像),如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/c4e7a40e-ffcd-451d-9a24-7547c009ff94.pnghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/548e4913-54ac-4117-814e-c8b5207d0392.png

从前面的图片中可以看出,经过直方图归一化后(右侧图像),图像的可见度发生了显著变化(左侧图像)。

  1. 按如下方式准备输入和输出数组:
count = 0
imgs = []
labels = []
for img_path in all_img_paths:
     img = preprocess_img(io.imread(img_path))
     label = img_path.split('/')[-2]
     imgs.append(img)
     labels.append(label)

X = np.array(imgs)
Y = to_categorical(labels, num_classes = NUM_CLASSES)
  1. 按如下方式构建训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size = 0.2, random_state= 42)
  1. 如下所示,构建并编译模型:
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',input_shape=(IMG_SIZE, IMG_SIZE, 3), activation='relu'))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(64, (3, 3), padding='same',activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Conv2D(128, (3, 3), padding='same',activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(NUM_CLASSES, activation='softmax'))
model.summary()

model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy']

模型的总结如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/23d3c55c-1505-4cf1-a1b8-10214e8d9207.png

  1. 如下所示,拟合模型:
model.fit(X_train, y_train,batch_size=32,epochs=5,validation_data = (X_test, y_test))

前面的代码生成了一个模型,其准确率约为 99%:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/fe909540-e0d5-466a-bd5c-bfc9597710c9.png

此外,如果您执行与我们相同的分析,但没有进行直方图归一化(曝光校正),模型的准确率约为 97%。

预测汽车需要转动的角度

在本案例中,我们将基于提供的图像来理解需要转动汽车的角度。

准备就绪

我们采用的构建转向角度预测策略如下:

  1. 收集一个数据集,其中包含道路的图像和需要转动方向盘的相应角度

  2. 预处理图像

  3. 将图像传入 VGG16 模型以提取特征

  4. 构建一个神经网络,执行回归以预测转向角度,这是一个需要预测的连续值

如何进行……

  1. 下载以下数据集。该数据集可以通过以下链接获得:github.com/SullyChen/driving-datasets:(代码文件可以在 GitHub 中的Car_steering_angle_detection.ipynb找到):
$ pip install PyDrive 

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

file_id = '0B-KJCaaF7elleG1RbzVPZWV4Tlk' # URL id. 
downloaded = drive.CreateFile({'id': file_id})
downloaded.GetContentFile('steering_angle.zip')

$ unzip steering_angle.zip
  1. 导入相关的包,如下所示:
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import pi
import cv2
import scipy.misc
import tensorflow as tf
  1. 将图像及其对应的弧度角度分别读取到单独的列表中,如下所示:
DATA_FOLDER = "/content/driving_dataset/"
DATA_FILE = os.path.join(DATA_FOLDER, "data.txt")
x = []
y = []

train_batch_pointer = 0
test_batch_pointer = 0

with open(DATA_FILE) as f:
     for line in f:
         image_name, angle = line.split() 
         image_path = os.path.join(DATA_FOLDER, image_name)
         x.append(image_path) 
         angle_radians = float(angle) * (pi / 180) #converting angle into radians
         y.append(angle_radians)
y = np.array(y)
  1. 如下所示,创建训练集和测试集:
split_ratio = int(len(x) * 0.8)
train_x = x[:split_ratio]
train_y = y[:split_ratio]
test_x = x[split_ratio:]
test_y = y[split_ratio:]
  1. 检查训练和测试数据集中的输出标签值,如下所示:
fig = plt.figure(figsize = (10, 7))
plt.hist(train_y, bins = 50, histtype = "step",color='r')
plt.hist(test_y, bins = 50, histtype = "step",color='b')
plt.title("Steering Wheel angle in train and test")
plt.xlabel("Angle")
plt.ylabel("Bin count")
plt.grid('off')
plt.show()

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/40a861cd-fd88-473f-a389-2d38d2e18b25.png

  1. 删除前 100 行的像素,因为这些像素与道路图像无关,然后将处理后的图像传入 VGG16 模型。此外,在此练习中,我们仅使用数据集中的前 10,000 张图像,以便更快地构建模型。删除前 100 行的像素,如下所示:
x = []
y = []
for i in range(10000):
     im = cv2.imread(train_x[i])
     im = im[100:,:,:]/255
     vgg_im = vgg16_model.predict(im.reshape(1,im.shape[0],im.shape[1],3))
     x.append(vgg_im)
     y.append(train_y[i])
x1 = np.array(x)
x1 = x1.reshape(x1.shape[0],4,14,512)
y1 = np.array(y)
  1. 如下所示,构建并编译模型:
model = Sequential()
model.add(Flatten(input_shape=(4,14,512)))
model.add(Dense(512, activation='relu'))
model.add(Dropout(.5))
model.add(Dense(100, activation='linear'))
model.add(Dropout(.2))
model.add(Dense(50, activation='linear'))
model.add(Dropout(.1))
model.add(Dense(10, activation='linear'))
model.add(Dense(1, activation='linear'))
model.summary()

注意,输出层采用线性激活,因为输出是一个连续值,范围从 -9 到 +9。模型的总结如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/edb25ad9-1735-49e8-8eac-039a1b89c67e.png

现在,我们将按如下方式编译已定义的模型:

model.compile(loss='mean_squared_error',optimizer='adam')
  1. 如下所示,拟合模型:
model.fit(x1/11, y1,batch_size=32,epochs=10, validation_split = 0.1, verbose = 1)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/971589a3-0d22-4c0f-8fa9-30f7742401d4.png

测试损失是前面图表中损失较低的那条线。

注意,我们已经将输入数据集除以 11,以便将其缩放到 0 到 1 之间。现在,我们应该能够根据预测的角度模拟汽车的运动。

模型对样本图像的转向角度预测结果如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/6d08b8a4-ce50-464a-9b5d-46afa3198668.png

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/342b5540-0861-4d72-b58c-908420c84c6f.png

请注意,使用上述模型时需要非常小心。它应首先在多种日光条件下进行测试,然后再进入生产环境。

使用 U-net 架构进行实例分割

到目前为止,在前两章中,我们已经学习了如何检测物体,以及如何识别图像中物体所在的边界框。在本节中,我们将学习如何进行实例分割,在实例分割中,属于某个特定物体的所有像素都会被突出显示,而其他像素则不会(这类似于用零掩膜掉所有不属于物体的像素,并用像素值 1 掩膜属于物体的像素)。

准备开始

为了执行实例分割,我们将执行以下操作:

  1. 在一个数据集上工作,该数据集具有输入图像及其对应的掩膜图像,掩膜图像显示对象在图像中的像素位置:

    • 图像及其掩膜图像
  2. 我们将通过预训练的 VGG16 模型将图像传递,以提取每个卷积层的特征

  3. 我们将逐渐上采样卷积层,以便我们获得形状为 224 x 224 x 3 的输出图像

  4. 我们将冻结使用 VGG16 权重的层

  5. 将上采样的卷积层与下采样的卷积层连接起来:

    • 这形成了 U 形连接

    • U 形连接帮助模型获得类似于 ResNet 的上下文(之前下采样的层提供上下文,除了上采样的层外)

    • 如果我们取第一层的输出,重建图像会更容易,因为大部分图像在第一层中是完好的(早期层学习图像的轮廓)。如果我们尝试通过上采样最后几层来重建图像,那么很有可能大部分图像信息会丢失

  6. 拟合一个将输入图像映射到掩膜图像的模型:

    • 注意,掩膜图像本质上是二进制的——黑色值对应于像素值 0,白色像素的值为 1
  7. 在所有 224 x 224 x 1 像素中最小化二元交叉熵损失函数

之所以称该模型为U-net 架构,是因为模型的可视化如下所示——一个旋转的 U 形结构:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/b4e97c5a-c390-4983-92f7-c9da19ef8318.png

模型的 U 形结构是由于早期层连接到下采样层的上采样版本。

如何实现…

在以下代码中,我们将执行实例分割,以检测图像中的汽车:

  1. github.com/divamgupta/image-segmentation-keras下载并导入文件,如下所示:
$ wget https://www.dropbox.com/s/0pigmmmynbf9xwq/dataset1.zip
$ unzip dataset1.zip
dir_data = "/content/dataset1"
dir_seg = dir_data + "/annotations_prepped_train/"
dir_img = dir_data + "/images_prepped_train/"
import glob, os
all_img_paths = glob.glob(os.path.join(dir_img, '*.png'))
all_mask_paths = glob.glob(os.path.join(dir_seg, '*.png'))
  1. 将图像及其对应的掩膜读取为数组,如下所示:
import cv2
from scipy import ndimage
x = []
y = []
for i in range(len(all_img_paths)):
  img = cv2.imread(all_img_paths[i])
  img = cv2.resize(img,(224,224))
  mask_path = dir_seg+all_img_paths[i].split('/')[4]
  img_mask = ndimage.imread(mask_path)
  img_mask = cv2.resize(img_mask,(224,224))
  x.append(img)
  y.append(img_mask)

x = np.array(x)/255
y = np.array(y)/255
y2 = np.where(y==8,1,0)

在前面的步骤中,我们创建了输入和输出数组,并且还对输入数组进行了归一化。最后,我们从所有其他内容中分离出了汽车的掩模,因为该数据集有 12 个唯一类别,其中汽车的像素值被标记为 8。

输入和掩模图像的示例如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1c1e655a-3c45-45ce-bfda-8863dbc5dbf8.png

此外,我们创建了输入和输出数组,其中我们对输入数组进行缩放,并重新塑形输出数组(以便可以传递给网络),如下所示:

x = np.array(x)
x = x/255
y2 = np.array(y2)
y2 = y2.reshape(y2.shape[0],y2.shape[1],y2.shape[2],1)
  1. 构建模型,其中图像首先通过 VGG16 模型层,提取卷积特征,如下所示:

在以下代码中,我们导入了预训练的 VGG16 模型:

from keras.applications.vgg16 import VGG16 as PTModel
from keras.layers import Input, Conv2D, concatenate, UpSampling2D, BatchNormalization, Activation, Cropping2D, ZeroPadding2D
from keras.layers import Input, merge, Conv2D, MaxPooling2D,UpSampling2D, Dropout, Cropping2D, merge, concatenate
from keras.optimizers import Adam
from keras.callbacks import ModelCheckpoint, LearningRateScheduler
from keras import backend as K
from keras.models import Model
base_pretrained_model = PTModel(input_shape = (224,224,3), include_top = False, weights = 'imagenet')
base_pretrained_model.trainable = False

在以下代码中,当不同的卷积层通过 VGG16 模型时,我们提取了特征:

conv1 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block1_conv2').output).output
conv2 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block2_conv2').output).output
conv3 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block3_conv3').output).output
conv4 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block4_conv3').output).output
drop4 = Dropout(0.5)(conv4)
conv5 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block5_conv3').output).output
drop5 = Dropout(0.5)(conv5)

在以下代码中,我们使用UpSampling方法对特征进行上采样,并在每一层将其与下采样后的 VGG16 卷积特征进行拼接:

up6 = Conv2D(512, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(drop5))
merge6 = concatenate([drop4,up6], axis = 3) 

conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge6)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv6)
conv6 = BatchNormalization()(conv6)
up7 = Conv2D(256, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv6))
merge7 = concatenate([conv3,up7], axis = 3)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge7)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv7)
conv7 = BatchNormalization()(conv7)
up8 = Conv2D(128, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv7))
merge8 = concatenate([conv2,up8],axis = 3)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge8)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv8)
conv8 = BatchNormalization()(conv8)
up9 = Conv2D(64, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv8))
merge9 = concatenate([conv1,up9], axis = 3)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge9)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = BatchNormalization()(conv9)
conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9)

在以下代码中,我们定义了模型的输入和输出,其中输入首先传递给base_pretrained_model,输出是conv10(其形状为 224 x 224 x 1—我们输出的预期形状):

model = Model(input = base_pretrained_model.input, output = conv10)
  1. 冻结通过训练得到的 VGG16 模型的卷积层,如下所示:
for layer in model.layers[:18]:
     layer.trainable = False
  1. 编译并拟合模型,以处理数据集中前 1,000 张图像,如下所示:
from keras import optimizers
adam = optimizers.Adam(1e-3, decay = 1e-6)
model.compile(loss='binary_crossentropy',optimizer=adam,metrics=['accuracy'])
history = model.fit(x,y,validation_split = 0.1,batch_size=1,epochs=5,verbose=1)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3027213c-790c-4e63-8d37-c02db987f53a.png

  1. 在数据集的最后两张测试图像上测试之前的模型(这些是具有validation_split = 0.1的测试图像),如下所示:
y_pred = model.predict(x[-2:].reshape(2,224,224,3))

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/35f3e4be-1c52-4657-a157-aaf44df7d756.png

我们可以看到,对于给定的道路输入,生成的掩模非常真实,并且比之前的方法更好,因为预测的掩模图像中没有噪点。

图像中对象的语义分割

在上一节中,我们学习了如何对包含单一对象的图像进行分割。在本节分割中,我们将学习如何进行分割,以便能够区分图像中存在的多个对象,尤其是在道路图像中。

准备开始

我们将采用的策略是,在道路图像上执行语义分割,如下所示:

  1. 收集一个数据集,其中包含标注了图像中多个对象位置的信息:

    • 语义图像的示例如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/9bee9bc6-479d-41cb-a8f7-25a68aa90b73.png

  1. 将输出掩模转换为多维数组,其中列数等于所有可能的唯一对象的数量。

  2. 如果有 12 个可能的唯一值(12 个唯一对象),将输出图像转换为形状为 224 x 224 x 12 的图像:

    • 一个通道的值表示该通道对应的对象在图像中的该位置存在。
  3. 利用我们在前面部分看到的模型架构,训练一个具有 12 个可能输出值的模型

  4. 通过将所有三个通道分配相同的输出,将预测结果重塑为三个通道:

    • 输出是 12 个可能类别的概率预测的最大值(argmax)

如何实现…

语义分割的代码实现如下(代码文件可以在 GitHub 上找到,名为Semantic_segmentation.ipynb):

  1. 下载数据集,如下所示:
!wget https://www.dropbox.com/s/0pigmmmynbf9xwq/dataset1.zip
!unzip dataset1.zip
dir_data = "/content/dataset1"
dir_seg = dir_data + "/annotations_prepped_train/"
dir_img = dir_data + "/images_prepped_train/"
import glob, os
all_img_paths = glob.glob(os.path.join(dir_img, '*.png'))
all_mask_paths = glob.glob(os.path.join(dir_seg, '*.png'))
  1. 将图像及其对应标签分别读取到不同的列表中,如下所示:
import cv2
from scipy import ndimage
for i in range(len(all_img_paths)):
     img = cv2.imread(all_img_paths[i])
     img = cv2.resize(img,(224,224))
     mask_path = dir_seg+all_img_paths[i].split('/')[4]
     img_mask = ndimage.imread(mask_path)
     img_mask = cv2.resize(img_mask,(224,224))
     x.append(img)
     y.append(img_mask)
  1. 定义一个函数,将三个通道的输出图像转换为 12 个通道,其中有 12 个唯一的输出值:

    1. 提取输出中存在的唯一值(对象)的数量,如下所示:
n_classes = len(set(np.array(y).flatten()))
    1. 将掩模图像转换为一热编码版本,通道数量与数据集中对象的总数相同,如下所示:
def getSegmentationArr(img):
      seg_labels = np.zeros(( 224, 224, n_classes ))
      for c in range(n_classes):
            seg_labels[: , : , c ] = (img == c ).astype(int)
      return seg_labels

y2 = []
for i in range(len(y)):
     y2.append(getSegmentationArr(y[i]))

y2 = np.array(y2)
x = x/255
  1. 构建模型:

    1. 将图像传递给预训练的 VGG16 模型,如下所示:
from keras.applications.vgg16 import VGG16 as PTModel
base_pretrained_model = PTModel(input_shape = (224,224,3), include_top = False, weights = 'imagenet')
base_pretrained_model.trainable = False
    1. 提取图像的 VGG16 特征,如下所示:
conv1 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block1_conv2').output).output
conv2 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block2_conv2').output).output
conv3 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block3_conv3').output).output
conv4 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block4_conv3').output).output
drop4 = Dropout(0.5)(conv4)
conv5 = Model(inputs=base_pretrained_model.input,outputs=base_pretrained_model.get_layer('block5_conv3').output).output
drop5 = Dropout(0.5)(conv5)
    1. 将卷积特征通过上采样层传递,并将它们连接形成一个简单的 U-net 架构,如下所示:
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge6)
conv6 = Conv2D(512, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv6)
conv6 = BatchNormalization()(conv6)
up7 = Conv2D(256, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv6))
merge7 = concatenate([conv3,up7], axis = 3)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge7)
conv7 = Conv2D(256, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv7)
conv7 = BatchNormalization()(conv7)
up8 = Conv2D(128, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv7))
merge8 = concatenate([conv2,up8],axis = 3)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge8)
conv8 = Conv2D(128, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv8)
conv8 = BatchNormalization()(conv8)
up9 = Conv2D(64, 2, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(UpSampling2D(size =(2,2))(conv8))
merge9 = concatenate([conv1,up9], axis = 3)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(merge9)
conv9 = Conv2D(64, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = Conv2D(2, 3, activation = 'relu', padding = 'same',kernel_initializer = 'he_normal')(conv9)
conv9 = BatchNormalization()(conv9)
conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9)

model = Model(input = base_pretrained_model.input, output = conv10)
  1. 冻结 VGG16 层,如下所示:
for layer in model.layers[:18]:
     layer.trainable = False
  1. 编译并拟合模型,如下所示:
model.compile(optimizer=Adam(1e-3, decay = 1e-6), 
 loss='categorical_crossentropy', metrics = ['accuracy'])

history = model.fit(x,y2,epochs=15,batch_size=1,validation_split=0.1)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cae3a4fb-af64-4933-b45b-f0ace18dc9c3.png

  1. 对测试图像进行预测,如下所示:
y_pred = model.predict(x[-2:].reshape(2,224,224,3))
y_predi = np.argmax(y_pred, axis=3)
y_testi = np.argmax(y2[-2:].reshape(2,224,224,12), axis=3)

import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(231)
plt.imshow(x[-1])
plt.axis('off')
plt.title('Original image')
plt.grid('off')
plt.subplot(232)
plt.imshow(y[-1])
plt.axis('off')
plt.title('Masked image')
plt.grid('off')
plt.subplot(233)
plt.imshow(y_predi[-1])
plt.axis('off')
plt.title('Predicted masked image')
plt.grid('off')
plt.subplot(234)
plt.imshow(x[-2])
plt.axis('off')
plt.grid('off')
plt.subplot(235)
plt.imshow(y[-2])
plt.axis('off')
plt.grid('off')
plt.subplot(236)
plt.imshow(y_predi[-2])
plt.axis('off')
plt.grid('off')
plt.show()

上面的代码将生成一张图像,其中预测的语义图像与实际的语义图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/9a872688-dd90-47ec-8d9c-675650960d84.png

从前面的图像中可以看出,我们能够准确地识别图像中的语义结构,且准确度非常高(我们训练的模型约为 90%)。

第八章:图像生成

在前面的章节中,我们学习了如何预测图像的类别并检测物体在图像中的位置。如果我们反向操作,给定一个类别后,我们应该能够生成一张图像。在这种情况下,生成网络非常有用,我们尝试创建看起来与原始图像非常相似的新图像。

本章将涵盖以下几种方法:

  • 通过对抗性攻击生成能够欺骗神经网络的图像

  • 使用 DeepDream 算法生成图像

  • 图像之间的神经风格迁移

  • 使用生成对抗网络生成数字图像

  • 使用深度卷积生成对抗网络生成数字图像

  • 使用深度卷积生成对抗网络(Deep Convolutional GAN)生成面部

  • 面部从一个到另一个的过渡

  • 对生成的图像进行向量算术运算

介绍

在前几章中,我们确定了分类图像到正确类别的最优权重。通过改变以下内容,可以改变图像的输出类别:

  • 连接输入层和输出层的权重,输入像素保持恒定

  • 输入像素值保持不变时,权重保持恒定

本章将采用这两种技术来生成图像。

在对抗性攻击的案例研究中,神经风格迁移和 DeepDream 将利用改变输入像素值的技巧。而涉及生成对抗网络GAN)的技术,则会利用改变连接输入像素值和输出的某些权重的技巧。

本章的前三个案例研究将利用改变输入像素值的技巧,而其余的则利用改变连接输入和输出的权重。

通过对抗性攻击生成能够欺骗神经网络的图像

为了了解如何对图像执行对抗性攻击,我们先了解如何使用迁移学习进行常规预测,然后我们将弄清楚如何调整输入图像,以便图像的类别完全不同,尽管我们几乎没有改变输入图像。

准备工作

让我们通过一个例子,尝试识别图像中的物体类别:

  1. 读取一张猫的图像

  2. 预处理图像,以便将其传递给 Inception 网络

  3. 导入预训练的 Inception v3 模型

  4. 预测图像中物体的类别

  5. 由于 Inception v3 在预测属于 ImageNet 类之一的物体时表现良好,图像将被预测为波斯猫

当前任务是以这样的方式改变图像,使其满足以下两个标准:

  • 使用相同的网络对新图像进行预测时,应该以非常高的概率预测为非洲象

  • 新生成的图像应在人眼看来与原始图像无法区分

为了实现这一目标,我们将遵循以下策略:

  1. 定义损失函数:

    • 损失是图像(波斯猫)属于非洲象类别的概率

    • 损失越高,我们离目标就越近

    • 因此,在此情况下,我们将最大化我们的损失函数

  2. 计算损失变化相对于输入变化的梯度:

    • 这一步有助于理解哪些输入像素将输出向我们的目标推进
  3. 基于计算出的梯度更新输入图像:

    • 确保原始图像中的像素值在最终图像中不会偏移超过 3 个像素

    • 这确保了生成的图像在人眼看来与原始图像无法区分

  4. 重复步骤 2 和步骤 3,直到更新后的图像被预测为非洲象,且置信度至少为 0.8

如何操作…

现在,让我们在代码中实现这一策略(代码文件可在 GitHub 上的Adversarial_attack.ipynb找到):

  1. 读取猫的图像:
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('/content/cat.JPG')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (299,299))
plt.imshow(img)
plt.axis('off')

图像的绘制如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/33c46965-bc81-4b5e-aec1-f52dacb165dc.png

  1. 预处理图像,以便将其传递到 Inception 网络:
original_image = cv2.resize(img,(299,299)).astype(float)
original_image /= 255.
original_image -= 0.5
original_image *= 2.
original_image = np.expand_dims(original_image, axis=0)
  1. 导入预训练模型:
import numpy as np
from keras.preprocessing import image
from keras.applications import inception_v3
model = inception_v3.InceptionV3()
  1. 预测图像中对象的类别:
predictions = model.predict(original_image)
predicted_classes = inception_v3.decode_predictions(predictions, top=1)
imagenet_id, name, confidence = predicted_classes[0][0]
print("This is a {} with {:.4}% confidence".format(name, confidence * 100))

前面的代码结果如下:

" This is a Persian_cat with 95.45% confidence"
  1. 定义输入和输出:
model = inception_v3.InceptionV3()
model_input_layer = model.layers[0].input
model_output_layer = model.layers[-1].output

model_input_layer是模型的输入,model_output_layer是输入图像的各种类别的概率(最后一层使用 softmax 激活)。

  1. 设置原始图像变化的限制:
max_change_above = np.copy(original_image) + 0.01
max_change_below = np.copy(original_image) - 0.01
hacked_image = np.copy(original_image)

在前面的代码中,我们指定了原始图像可以改变的限制。

  1. 初始化代价函数,使得要伪装的对象类型是非洲象(预测向量中第 386 个索引值):
learning_rate = 0.1
object_type_to_fake = 386
cost_function = model_output_layer[0, object_type_to_fake]

model_output_layer的输出是感兴趣图像的各种类别的概率。在此实例中,我们指定代价函数将由我们试图将对象伪装成的目标对象的索引位置来决定。

  1. 初始化代价函数相对于输入的梯度:
gradient_function = K.gradients(cost_function, model_input_layer)[0]

这段代码计算了cost_function相对于model_input_layer(即输入图像)变化的梯度。

  1. 映射与输入相关的代价和梯度函数:
grab_cost_and_gradients_from_model = K.function([model_input_layer], [cost_function, gradient_function])
cost = 0.0

在前面的代码中,我们正在计算cost_function(图像属于非洲象类别的概率)和相对于输入图像的梯度。

  1. 一直更新输入图像,直到生成图像的非洲象概率至少达到 80%:
while cost < 0.80:
    cost, gradients = grab_cost_and_gradients_from_model([hacked_image, 0])
    hacked_image += gradients * learning_rate
    hacked_image = np.clip(hacked_image, max_change_below, max_change_above)
    print("Model's predicted likelihood that the image is an African elephant: 
{:.8}%".format(cost * 100))

在前面的代码中,我们获取与输入图像(hacked_image)对应的代价和梯度。此外,我们通过梯度(与学习率相乘)更新输入图像。最后,如果被修改的图像超过了输入图像的最大变化阈值,我们将对其进行裁剪。

不断循环这些步骤,直到你得到输入图像的概率至少为 0.8。

随着训练轮次的增加,波斯猫图像被识别为非洲象图像的概率变化如下:

epochs = range(1, len(prob_elephant) + 1)
plt.plot(epochs, prob_elephant, 'b')
plt.title('Probability of African elephant class')
plt.xlabel('Epochs')
plt.ylabel('Probability')
plt.grid('off')

修改后的图像属于非洲象类别的概率变化如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/a8c81887-f7bb-4364-8ea0-27f4321ada37.png

  1. 预测更新图像的类别:
model.predict(hacked_image)[0][386]

predict方法的输出是修改后图像属于非洲象类别的概率,值为 0.804。

  1. 对更新后的输入图像进行去处理(因为它在预处理时已经被缩放)以便可视化:
hacked_image = hacked_image/2
hacked_image = hacked_image + 0.5
hacked_image = hacked_image*255
hacked_image = np.clip(hacked_image, 0, 255).astype('uint8')

plt.subplot(131)
plt.imshow(img)
plt.title('Original image')
plt.axis('off')
plt.subplot(132)
plt.imshow(hacked_image[0,:,:,:])
plt.title('Hacked image')
plt.axis('off')
plt.subplot(133)
plt.imshow(img - hacked_image[0,:,:,:])
plt.title('Difference')
plt.axis('off')

原始图像、修改后的(被篡改的)图像以及两者之间的差异将如下打印出来:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1db1140a-75c9-4b31-9012-aeba89a7bb24.png

注意,输出现在在视觉上与原始图像无法区分。

有趣的是,尽管像素值几乎没有变化,但我们成功地欺骗了神经网络(inception v3 模型),使它预测了一个不同的类别。这是一个很好的例子,展示了如果用于预测的算法暴露给可以制作欺骗系统图像的用户,可能会遇到的安全漏洞。

使用 DeepDream 算法生成图像

在上一节中,我们稍微调整了输入图像的像素。在这一节中,我们将进一步调整输入图像,以便生成一张仍然是相同物体的图像,但比原图更具艺术感。该算法是使用神经网络进行风格迁移技术的核心。

让我们了解一下 DeepDream 如何工作的直觉。

我们将通过一个预训练模型(例如 VGG19)来处理我们的图像。我们已经了解到,根据输入图像,预训练模型中的某些滤波器激活得最多,而某些滤波器则激活得最少。

我们将提供我们希望激活的神经网络层。

神经网络会调整输入像素值,直到我们获得所选层的最大值。

然而,我们也会确保最大可能的激活值不超过某个值,因为如果激活值过高,结果图像可能会与原始图像有很大不同。

准备就绪

理解了这些直觉后,让我们来看看如何实现 DeepDream 算法的步骤:

  1. 选择你想要最强激活的神经网络层,并为这些层对整体损失计算的贡献分配权重。

  2. 提取给定层的输出,当图像通过该层时,并计算每一层的损失值:

    • 当图像在某一层的输出平方和最大时,图像会最强地激活该层。
  3. 提取输入像素值变化相对于损失的梯度。

  4. 根据上一阶段提取的梯度更新输入像素值。

  5. 提取更新后的输入像素值在所有选定层中的损失值(激活的平方和)。

  6. 如果损失值(激活值的加权平方和)大于预定义的阈值,则停止更新图像。

如何做到这一点…

让我们在代码中实现这些步骤(代码文件可在 GitHub 的Deepdream.ipynb中找到):

  1. 导入相关的包并导入图像:
import keras.backend as K
import multiprocessing
import tensorflow as tf
import warnings
from keras.applications.vgg19 import VGG19
from keras.applications.imagenet_utils import preprocess_input
from scipy.optimize import minimize
from skimage import img_as_float, img_as_ubyte
from skimage.io import imread, imsave
from skimage.transform import pyramid_gaussian, rescale
import scipy
from keras.preprocessing import image
from keras.applications.vgg19 import preprocess_input
import matplotlib.pyplot as plt
%matplotlib inline

对图像进行预处理,使其能够传递到 VGG19 模型:

def preprocess_image(image_path):
     img = image.load_img(image_path, target_size=(img_nrows, img_ncols))
     img = image.img_to_array(img)
     img = np.expand_dims(img, axis=0)
     img[:, :, :, 0] -= 103.939
     img[:, :, :, 1] -= 116.779
     img[:, :, :, 2] -= 123.68
     img = img[:, :, :, ::-1]/255
     return img

构建一个去处理已处理图像的函数:

def deprocess_image(x):
     x = x[:,:,:,::-1]*255
     x[:, :, :, 0] += 103.939
     x[:, :, :, 1] += 116.779
     x[:, :, :, 2] += 123.68
     x = np.clip(x, 0, 255).astype('uint8')
     return x

预处理图像:

img = preprocess_image('/content/cat.png')
  1. 定义对整体损失值计算有贡献的层:
layer_contributions = {
    'block2_pool':0.3,
    'block5_pool': 1.5}

在前面的代码中,我们展示了将使用第二层和第五层池化层,并且分配这两层对整体损失值的贡献权重。

  1. 初始化损失函数:
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'))
     loss += coeff * K.sum(K.square(activation)) / scaling
     print(loss)

在前面的代码中,我们遍历了感兴趣的层(layer_contributions),并记录了为每层分配的权重(coeff)。此外,我们还计算了感兴趣层的输出(activation),并通过对激活值进行缩放后求平方和来更新损失值。

  1. 初始化梯度值:
dream = model.input
grads = K.gradients(loss, dream)[0]

K.gradients方法给出了损失相对于输入变化(dream)的梯度。

  1. 对梯度值进行归一化,以便梯度的变化速度较慢:
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
  1. 创建一个函数,将输入图像映射到损失值以及损失值相对于输入像素值变化的梯度(其中输入图像是dream):
outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)
  1. 定义一个函数,提供给定输入图像的损失值和梯度值:
def eval_loss_and_grads(img):
      outs = fetch_loss_and_grads([img])
      loss_value = outs[0]
      grad_values = outs[1]
      return loss_value, grad_values
  1. 基于获得的损失和梯度值,通过多次迭代更新原始图像。

在下面的代码中,我们遍历图像 100 次。我们定义了图像变化的学习率和图像可能发生的最大损失(变化):

for i in range(100):
      learning_rate=0.01
      max_loss=20

在下面的代码中,我们提取了图像的损失值和梯度值,然后在损失值超过定义的阈值时停止图像的变化:

     loss_value, grad_values = eval_loss_and_grads(img)
     if max_loss is not None and loss_value > max_loss:
         print(loss_value)
         break
     print('...Loss value at', i, ':', loss_value)

在下面的代码中,我们根据梯度值更新图像,并进行去处理图像并打印图像:

    img += learning_rate * grad_values
    img2 = deprocess_image(img.copy())
    plt.imshow(img2[0,:,:,:])
    plt.axis('off')
    plt.show()

前面的代码生成的图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d34a9836-cc7c-49dd-8afb-fe8cc5ac2545.png

请注意,前面图像中的波浪图案可能是因为这些是最大化各个网络层激活的模式。

在这里,我们看到了扰动输入像素的另一种应用, 在这种情况下,结果是图像略显艺术感。

图像之间的神经风格迁移

在之前的步骤中,修改的像素值试图最大化滤波器的激活值。然而,这并没有给我们提供指定图像风格的灵活性;此时,神经风格迁移派上了用场。

在神经风格迁移中,我们有一个内容图像和一个风格图像,我们尝试以一种方式将这两张图像结合起来,既能保持内容图像中的内容,又能保持风格图像的风格。

准备中

神经风格迁移的直觉如下。

我们尝试以类似于 DeepDream 算法的方式修改原始图像。然而,额外的步骤是将损失值分为内容损失和风格损失。

内容损失指的是生成图像与内容图像之间的差异。风格损失指的是风格图像与生成图像之间的相关性。

虽然我们提到损失是基于图像之间的差异来计算的,但在实践中,我们通过确保使用来自图像的激活值而不是原始图像来稍微修改它。例如,第二层的内容损失将是内容图像和生成图像在通过第二层时激活值之间的平方差。

尽管计算内容损失看起来很直接,但让我们尝试理解如何计算生成图像与风格图像之间的相似性。

一种叫做 gram 矩阵的技术出现了。gram 矩阵计算生成图像和风格图像之间的相似度,计算公式如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/5754caec-464d-4b8e-a375-65df6ffdc898.png

其中,GM(l) 是风格图像 S 和生成图像 G 在层 l 处的 gram 矩阵值。

gram 矩阵是通过将一个矩阵与其自身的转置相乘得到的。

现在我们可以计算风格损失和内容损失了,最终的修改输入图像是最小化整体损失的图像,也就是风格损失和内容损失的加权平均值。

神经风格迁移的实现步骤如下:

  1. 将图像通过一个预训练模型。

  2. 提取预定义层的层值。

  3. 将生成的图像初始化为与内容图像相同。

  4. 将生成的图像通过模型并提取其在相同层的值。

  5. 计算内容损失。

  6. 将风格图像通过模型的多个层,并计算风格图像的 gram 矩阵值。

  7. 将生成的图像通过与风格图像相同的层,并计算其对应的 gram 矩阵值。

  8. 提取两张图像的 gram 矩阵值之间的平方差。这将是风格损失。

  9. 整体损失将是风格损失和内容损失的加权平均值。

  10. 最小化整体损失的输入图像将是最终的目标图像。

如何做到…

  1. 导入相关的包和内容、样式图像,它们需要结合在一起形成艺术图像,如下所示(代码文件可在 GitHub 上的Neural_style_transfer.ipynb找到):
from keras.preprocessing.image import load_img, save_img, img_to_array
import numpy as np
import time
from keras.applications import vgg19
from keras.applications.imagenet_utils import preprocess_input
from keras import backend as K
import tensorflow as tf
import keras

style_img = cv2.imread('/content/style image.png')
style_img = cv2.cvtColor(style_img, cv2.COLOR_BGR2RGB)
style_img = cv2.resize(style_img,(224,224))

base_img = cv2.imread('/content/cat.png')
base_img = cv2.cvtColor(base_img, cv2.COLOR_BGR2RGB)
base_img = cv2.resize(base_img,(224,224))

样式图像和基础图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/6e087838-928d-476e-b7e8-a1153df3d213.pnghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/470f0a29-7b89-40ca-a2ba-33d574883c08.png

  1. 初始化vgg19模型,以便图像可以通过其网络传递:
from keras.applications import vgg19
model = vgg19.VGG19(include_top=False, weights='imagenet')
  1. 重新调整基础图像并提取 VGG19 模型中block3_conv4层的特征值:
base_img = base_img.reshape(1,224,224,3)/255
from keras import backend as K
get_3rd_layer_output = K.function([model.layers[0].input],
[model.get_layer('block3_conv4').output])
layer_output_base = get_3rd_layer_output([base_img])[0]

在前面的代码中,我们定义了一个函数,该函数获取输入图像并在预定义层中提取输出。

  1. 定义需要提取内容和样式损失的层,以及需要分配给每个层的相应权重:
layer_contributions_content = {'block3_conv4': 0.1}

layer_contributions_style =    { 'block1_pool':1,
                                 'block2_pool':1,
                                 'block3_conv4':1}

在前面的代码中,我们定义了计算内容和样式损失的层,并为这些层产生的损失分配了相应的权重。

  1. 定义 Gram 矩阵和样式损失函数:

在以下代码中,我们定义了一个函数,该函数计算作为通过扁平化图像获得的特征的点积的 Gram 矩阵输出:

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):
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_nrows * img_ncols
    return K.sum(K.square(S - C)) / (4\. * (pow(channels,2)) * (pow(size,2)))
  1. 初始化损失值函数:

计算内容损失:

layer_dict = dict([(layer.name, layer) for layer in model.layers])
loss = K.variable(0.)
for layer_name in layer_contributions_content:
      coeff = layer_contributions_content[layer_name]
      activation = layer_dict[layer_name].output
      scaling = K.prod(K.cast(K.shape(activation), 'float32'))
      loss += coeff * K.sum(K.square(activation - layer_output_base)) / scaling

在前面的代码中,我们根据计算内容损失的层中的损失更新损失值。请注意,layer_output_base是通过内容层传递原始基础图像时的输出(如第 3 步所定义)。

激活(基于修改后的图像)和layer_output_base(基于原始图像)之间的差异越大,图像的内容损失就越大。

计算样式损失:

for layer_name in layer_contributions_style:
    coeff = layer_contributions_style[layer_name]
    activation = layer_dict[layer_name].output
    scaling = K.prod(K.cast(K.shape(activation), 'float32'))
    style_layer_output = K.function([model.layers[0].input],
model.get_layer(layer_name).output])
    layer_output_style = style_layer_output([style_img.reshape(1,224,224,3)/255])[0][0]
    loss += style_loss(layer_output_style, activation[0])

在前面的代码中,我们以与计算内容损失相同的方式计算样式损失,但在不同的层上,并使用我们构建的不同自定义函数:style_loss

  1. 构建一个函数,将输入图像映射到损失值和相应的梯度值:
dream = model.input
grads = K.gradients(loss, dream)[0]
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)
outputs = [loss, grads]
fetch_loss_and_grads = K.function([dream], outputs)

def eval_loss_and_grads(img):
      outs = fetch_loss_and_grads([img])
      loss_value = outs[0]
      grad_values = outs[1]
      return loss_value, grad_values

前面的代码以与DeepDream 算法生成图像食谱非常相似的方式获取损失和梯度值。

  1. 运行模型多个周期:
for i in range(2000):
      step=0.001
      loss_value, grad_values = eval_loss_and_grads(img)
      print('...Loss value at', i, ':', loss_value)
      img -= step * grad_values
      if(i%100 ==0):
            img2 = img.copy().reshape(224,224,3)
            img2 = np.clip(img2*255, 0, 255).astype('uint8')
            plt.imshow(img2)
            plt.axis('off')
            plt.show()

前面的代码生成了一张将内容图像和样式图像相结合的图像:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/03042fd3-6db8-40c1-bfe4-071359915494.png

通过选择不同的层来计算内容和样式损失,并为这些层在各自样式或内容贡献中分配不同的系数权重,最终生成的图像可能会有所不同。

在前面的三个案例研究中,我们看到如何通过改变输入像素值来生成新图像。在本章的其余部分,我们将采用一种不同的生成新图像的方法:使用生成对抗网络(GANs)。

使用生成对抗网络生成数字图像

一个生成对抗网络(GAN)使用一堆神经网络生成一张与原始图像集非常相似的新图像。它在图像生成中有着广泛的应用,并且 GAN 研究领域正在快速进展,旨在生成那些非常难以与真实图像区分的图像。在本节中,我们将理解 GAN 的基础知识——它是如何工作的,以及 GAN 变种之间的差异。

一个 GAN 由两个网络组成:生成器和判别器。生成器尝试生成一张图像,判别器则尝试确定它收到的输入图像是真实的图像还是生成的(假的)图像。

为了进一步理解,假设判别器模型试图将一张图片分类为人脸图像或非人脸图像,数据集中包含了成千上万的人脸图像和非人脸图像。

一旦我们训练模型以区分人脸和非人脸,当我们向模型展示一张新的人脸时,模型仍然会将其分类为人脸,而它会学习将非人脸分类为非人脸。

生成器网络的任务是生成与原始图像集非常相似的图像,以至于判别器会被“欺骗”,认为生成的图像实际上来自原始数据集。

准备工作

我们将采用的生成图像的策略如下:

  1. 使用生成器网络生成合成图像,初始步骤是生成一张噪声图像,该图像是通过将一组噪声值重新塑形为我们图像的形状来生成的。

  2. 将生成的图像与原始图像集合连接,并让判别器预测每个图像是生成的图像还是原始图像——这确保了判别器被训练:

    • 请注意,判别器网络的权重在这一迭代过程中得到了训练。

    • 判别器网络的损失是图像的预测值和实际值之间的二元交叉熵。

    • 生成图像的输出值将是假的(0),而原始图像的值将是真实的(1)。

  3. 现在,判别器已经经过了一次迭代训练,接下来训练生成器网络,修改输入噪声,使其看起来更像真实图像而非合成图像——一个有可能欺骗判别器的图像。这个过程包括以下步骤:

    1. 输入噪声通过生成器网络传递,生成器将输入转化为图像。

    2. 从生成器网络生成的图像会传递到判别器网络——但请注意,在这一迭代中判别器网络的权重是被冻结的,因此它们不会在这一迭代中被训练(因为它们已经在步骤 2 中进行了训练)。

    3. 从判别器得到的生成图像输出值将是真实的(1),因为它的任务是欺骗判别器。

    4. 生成器网络的损失是输入图像的预测与实际值之间的二进制交叉熵(对于所有生成的图像,实际值为 1)——这确保了生成器网络的权重被微调:

      • 请注意,在这一步中,判别器网络的权重已被冻结

      • 冻结判别器可以确保生成器网络从判别器提供的反馈中学习

    5. 重复这些步骤多次,直到生成真实的图像。

如何操作…

对抗性攻击欺骗神经网络部分,我们讨论了如何生成一个与原始图像非常相似的图像的策略。在这一部分,我们将实现从 MNIST 数据集生成数字图像的过程(代码文件可在 GitHub 上的Vanilla_and_DC_GAN.ipynb中找到):

  1. 导入相关的包:
import numpy as np
from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout
from keras.layers import BatchNormalization
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Sequential
from keras.optimizers import Adam
import matplotlib.pyplot as plt
%matplotlib inline
plt.switch_backend('agg')
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Reshape
from keras.layers.core import Activation
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import UpSampling2D
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers.core import Flatten
from keras.optimizers import SGD
from keras.datasets import mnist
import numpy as np
from PIL import Image
import argparse
import math
  1. 定义参数:
shape = (28, 28, 1)
epochs = 400
batch = 32
save_interval = 100
  1. 定义生成器和判别器网络:
def generator():
    model = Sequential()
    model.add(Dense(256, input_shape=(100,)))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(512))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(1024))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(28 * 28 * 1, activation='tanh'))
    model.add(Reshape(shape))
    return model

对于生成器,我们构建了一个模型,它接收一个形状为 100 维的噪声向量,并将其转换为一个形状为 28 x 28 x 1 的图像。注意,我们在模型中使用了LeakyReLU激活函数。生成器网络的摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/bee45c41-5ea7-4026-b15b-ef627ff7902a.png

在以下代码中,我们构建了一个判别器模型,其中我们输入一个形状为 28 x 28 x 1 的图像,并输出一个值为 1 或 0 的结果,表示输入图像是原始图像还是伪造图像:

def discriminator():
     model = Sequential()
     model.add(Flatten(input_shape=shape))
     model.add(Dense((28 * 28 * 1), input_shape=shape))
     model.add(LeakyReLU(alpha=0.2))
     model.add(Dense(int((28 * 28 * 1) / 2)))
     model.add(LeakyReLU(alpha=0.2))
     model.add(Dense(1, activation='sigmoid'))
     return model

判别器网络的摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cf8f01e2-d615-49af-9cae-12915b038e85.png

编译生成器和判别器模型:

Generator = generator()
Generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
Discriminator = discriminator()
Discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8),metrics=['accuracy'])
  1. 定义堆叠的生成器判别器模型,帮助优化生成器的权重,同时冻结判别器网络的权重。堆叠的生成器判别器接受我们传入模型的随机噪声作为输入,并使用生成器网络将噪声转换为一个 28 x 28 的图像。此外,它还判断这个 28 x 28 的图像是真实的还是伪造的:
def stacked_generator_discriminator(D, G):
    D.trainable = False
    model = Sequential()
    model.add(G)
    model.add(D)
    return model

stacked_generator_discriminator = stacked_generator_discriminator(Discriminator, Generator)
stacked_generator_discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
  1. 定义一个函数来绘制生成的图像:
def plot_images(samples=16, step=0):
    noise = np.random.normal(0, 1, (samples, 100))
    images = Generator.predict(noise)
    plt.figure(figsize=(10, 10))
    for i in range(images.shape[0]):
        plt.subplot(4, 4, i + 1)
        image = images[i, :, :, :]
        image = np.reshape(image, [28, 28])
        plt.imshow(image, cmap='gray')
        plt.axis('off')
    plt.tight_layout()
    plt.show()
  1. 提供输入图像:
(X_train, _), (_, _) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)

我们丢弃了y_train数据集,因为我们不需要输出标签,模型是基于给定的图像集合(即X_train)生成新图像的。

  1. 通过多次训练周期优化图像:

在以下代码中,我们正在获取真实图像(legit_images)并生成假图像(synthetic_images)数据,我们将尝试通过修改噪声数据(gen_noise)作为输入,将其转换为逼真的图像,如下所示:

for cnt in range(4000):
      random_index = np.random.randint(0, len(X_train) - batch / 2)
      legit_images = X_train[random_index: random_index + batch // 2].reshape(batch // 2, 28, 28, 1)
      gen_noise = np.random.normal(-1, 1, (batch // 2, 100))/2
      synthetic_images = Generator.predict(gen_noise)

在以下代码中,我们正在训练判别器(使用train_on_batch方法),其中真实图像应输出 1,而假图像应输出 0:

x_combined_batch = np.concatenate((legit_images, synthetic_images))
y_combined_batch = np.concatenate((np.ones((batch // 2, 1)), np.zeros((batch // 2, 1))))
d_loss = Discriminator.train_on_batch(x_combined_batch, y_combined_batch)

在以下代码中,我们正在准备一组新的数据,其中noise是输入,y_mislabeled是输出,用于训练生成器(请注意,输出与我们训练判别器时的输出正好相反):

noise = np.random.normal(-1, 1, (batch, 100))/2
y_mislabled = np.ones((batch, 1))

在以下代码中,我们正在训练生成器和判别器的堆叠组合,其中判别器的权重被冻结,而生成器的权重会更新,以最小化损失值。生成器的任务是生成能够欺骗判别器输出 1 的图像:

g_loss = stacked_generator_discriminator.train_on_batch(noise, y_mislabled)

在以下代码中,我们观察生成器损失和判别器损失在不同周期的输出:

logger.info('epoch: {}, [Discriminator: {}], [Generator: {}]'.format(cnt, d_loss[0], g_loss))
    if cnt % 100 == 0:
          plot_images(step=cnt)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/135e0b94-6745-46c3-8bd1-e9adfdcd07b4.jpg

判别器和生成器损失随着周期增加的变化如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/37f6d676-e678-494b-a8e3-9856cbc10712.png

请注意,前面的输出在生成图像的真实感方面还有很大的改进空间。

还有更多…

我们看到的输出也是模型架构的函数。例如,可以将模型各层的激活函数更改为 tanh,看看生成的输出如何变化,从而大致了解生成图像的样子。

使用深度卷积 GAN 生成图像

在上一部分中,我们研究了使用 Vanilla 生成器和判别器网络生成数字。然而,我们也可以遇到一种情况,即通过使用卷积架构,网络能更好地学习图像中的特征,因为 CNN 中的滤波器会学习图像中的特定细节。深度卷积生成对抗网络DCGANs)利用这一现象生成新的图像。

如何实现…

虽然 DCGAN 的工作原理与 GAN(我们在上一个示例中使用的模型)非常相似,但主要的区别在于 DCGAN 的生成器和判别器架构,其结构如下(代码文件可以在 GitHub 上找到,文件名为Vanilla_and_DC_GAN.ipynb):

def generator():
    model = Sequential()
    model.add(Dense(input_dim=100, output_dim=1024))
    model.add(Activation('tanh'))
    model.add(Dense(128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('tanh'))
    model.add(Reshape((7, 7, 128), input_shape=(128*7*7,)))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(64, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(1, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    return model

def discriminator():
    model = Sequential()
    model.add(Conv2D(64, (5, 5),padding='same',input_shape=(28, 28, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(128, (5, 5)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(1024))
    model.add(Activation('tanh'))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model

请注意,在 DCGAN 中,我们对输入数据执行了多次卷积和池化操作。

如果我们重新执行在 Vanilla GAN(生成对抗网络用于生成图像)示例中执行的完全相同的步骤,但这次使用定义了卷积和池化架构的模型(即 DCGAN),我们将得到以下生成的图像:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2875a289-9afe-430e-8502-c63a9c999541.png

随着迭代轮次增加,生成器和判别器的损失值变化如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/9a924e72-de38-489a-93be-b1fac122e4ce.png

我们可以看到,尽管其他一切保持不变,仅模型架构发生了变化,但通过 DCGAN 生成的图像比 Vanilla GAN 的结果真实得多。

使用深度卷积 GAN 生成面部

到目前为止,我们已经了解了如何生成新图像。在本节中,我们将学习如何从现有的面部数据集中生成一组新的面部图像。

准备工作

我们将在本次练习中采用的方案与我们在 使用深度卷积 GAN 生成图像 处方中的方法非常相似:

  1. 收集一个包含多个面部图像的数据集。

  2. 在开始时生成随机图像。

  3. 通过展示包含面部和随机图像的组合来训练判别器,判别器需要区分实际面部图像和生成的面部图像。

  4. 一旦判别器模型训练完成,将其冻结,并调整随机图像,使得判别器现在会给经过调整的随机图像分配更高的属于原始面部图像的概率。

  5. 重复前面两步,进行多次迭代,直到生成器不再继续训练。

如何实现…

面部生成的代码实现如下(代码文件在 GitHub 上可用,名为 Face_generation.ipynb):

  1. 下载数据集。建议下载的数据集和相关代码已在 GitHub 上提供。以下是图像示例:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cba9b1ce-cca6-459e-b691-2a0b84628ee0.png

  1. 定义模型架构:
def generator():
    model = Sequential()
    model.add(Dense(input_dim=100, output_dim=1024))
    model.add(Activation('tanh'))
    model.add(Dense(128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('tanh'))
    model.add(Reshape((7, 7, 128), input_shape=(128*7*7,)))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(64, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Conv2D(1, (5, 5), padding='same'))
    model.add(Activation('tanh'))
    return model

请注意,上述代码与我们在 深度卷积生成对抗网络 处方中构建的生成器相同:

def discriminator():
    model = Sequential()
    model.add(Conv2D(64, (5, 5),padding='same',input_shape=(28, 28, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(128, (5, 5)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(1024))
    model.add(Activation('tanh'))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model

请注意,上述架构与我们在 使用深度卷积 GAN 生成图像 部分中构建的架构相同:

def stacked_generator_discriminator(D, G):
    D.trainable = False
    model = Sequential()
    model.add(G)
    model.add(D)
    return model
  1. 定义用于加载、预处理和反处理图像的实用函数,并绘制图像:
def plot_images(samples=16, step=0):
    noise = np.random.normal(0, 1, (samples, 100))
    images = deprocess(Generator.predict(noise))
    plt.figure(figsize=(5, 5))
    for i in range(images.shape[0]):
        plt.subplot(4, 4, i + 1)
        image = images[i, :, :, :]
        image = np.reshape(image, [56, 56,3])
        plt.imshow(image, cmap='gray')
        plt.axis('off')
    plt.tight_layout()
    plt.show()

请注意,我们正在将图像调整为较小的形状,以便通过模型调整的参数数量最小化:

def preprocess(x):
    return (x/255)*2-1

def deprocess(x):
    return np.uint8((x+1)/2*255)
  1. 导入数据集并进行预处理:
from skimage import io
import os
import glob
root_dir = '/content/lfwcrop_color/'
all_img_paths = glob.glob(os.path.join(root_dir, '*/*.ppm'))

在以下代码中,我们正在创建输入数据集并将其转换为数组:

import numpy as np
X_train = []
for i in range(len(all_img_paths)):
  img = cv2.imread(all_img_paths[i])
  X_train.append(preprocess(img))
len(X_train)
X_train = np.array(X_train)
  1. 编译生成器、判别器和堆叠的生成器-判别器模型:
Generator = generator()
Generator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))

Discriminator = discriminator()
Discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8),metrics=['accuracy'])

stacked_generator_discriminator = stacked_generator_discriminator(Discriminator, Generator)
stacked_generator_discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5, decay=8e-8))
  1. 以类似于我们在 深度卷积生成对抗网络 处方中使用的方式,运行模型多轮迭代:
%matplotlib inline
$pip install logger
from logger import logger
for cnt in range(10000):
      random_index = np.random.randint(0, len(X_train) - batch / 2)
      legit_images = X_train[random_index: random_index + batch // 2].reshape(batch // 2, 56, 56, 3)
      gen_noise = np.random.normal(0, 1, (batch // 2, 100))
      syntetic_images = Generator.predict(gen_noise)
      x_combined_batch = np.concatenate((legit_images, syntetic_images))
      y_combined_batch = np.concatenate((np.ones((batch // 2, 1)), np.zeros((batch // 2, 1))))
      d_loss = Discriminator.train_on_batch(x_combined_batch, y_combined_batch)
      noise = np.random.normal(0, 1, (batch*2, 100))
      y_mislabled = np.ones((batch*2, 1))
      g_loss = stacked_generator_discriminator.train_on_batch(noise, y_mislabled)
      logger.info('epoch: {}, [Discriminator: {}], [Generator: {}]'.format(cnt, d_loss[0], g_loss))
      if cnt % 100 == 0:
          plot_images(step=cnt)

上述代码生成的图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/c8296899-5777-40c5-ac1d-5f259a7dae53.png

请注意,尽管这些图像看起来非常模糊,但这张图片是原始的,不存在于原始数据集中。通过改变模型架构并增加更深的层次,这个输出还有很大的提升空间。

随着训练轮数增加,判别器和生成器损失值的变化如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/807a0f3c-24e4-42f7-87fa-48fffa9fff7e.png

请注意,从前面的图表中,我们可能希望训练模型的轮数少一些,以使生成器的损失值不那么高。

从一张人脸过渡到另一张人脸

现在我们已经能够生成面部图像了,接下来让我们在生成的图像上进行一些向量运算。

在这个练习中,我们将执行从一个人脸到另一个人脸的生成过渡。

准备开始

我们将继续从《使用深度卷积 GAN 进行人脸生成》部分构建的图像生成模型开始。

假设我们希望看到一张生成的人脸图像逐渐过渡到另一张生成的人脸图像。这个过程是通过慢慢改变从第一个向量(第一张生成图像的向量)到第二个向量(第二张生成图像的向量)来实现的。你可以将每个潜在的(向量)维度看作是图像的某个特定方面。

我们将采用的策略如下:

  1. 生成两张图像

  2. 在 10 步中将第一张生成图像转换为第二张生成图像

  3. 在第一步中,将第一张生成图像的权重设为 1,第二张生成图像的权重设为 0。

  4. 在第二步中,将第一张生成图像的权重设为 0.9,第二张生成图像的权重设为 0.1。

  5. 重复前面的步骤,直到我们将第一张生成图像的权重设为 0,第二张生成图像的权重设为 1。

如何执行…

我们将编写在《准备开始》部分中概述的策略,代码如下(代码文件可以在 GitHub 上的Face_generation.ipynb中找到):

  1. 从随机噪声生成第一张图像(请注意,我们将从《使用深度卷积 GAN 进行人脸生成》部分的第 6 步继续):
gen_noise = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')

生成的图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/0f786cff-d1c3-4593-acf5-c2471595c8c2.png

  1. 从随机噪声生成第二张图像:
gen_noise2 = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise2)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show() 

以下是前面代码片段的输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f6450874-5966-4739-9766-ea1b6f6c4997.png

  1. 生成从第一张图像到第二张图像的可视化:
plt.figure(figsize=(10, 8))
for i in range(10):
  gen_noise3 = gen_noise + (gen_noise2 - gen_noise)*(i+1)/10
  syntetic_images = Generator.predict(gen_noise3)
  plt.subplot(1, 10, i+1)
  plt.imshow(deprocess(syntetic_images)[0])
  plt.axis('off')

我们将获得以下输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/dfcd4d0b-296b-45d5-b28a-8ad685a06a23.jpg

请注意,在前面的输出中,我们已经慢慢将第一张图像转换成了第二张图像。

在生成图像上执行向量运算

现在我们理解了潜在向量表示在改变生成图像结果中的关键作用,接下来让我们用具有特定人脸对齐的图像进一步构建我们的直觉。

准备开始

我们将采用的向量运算策略如下:

  1. 生成三张基于 100 个向量值随机噪声的图像

  2. 确保三张图像中有两张生成的面朝左,并且有一张面朝右。

  3. 计算一个新的向量,它是对齐同一方向的图像之和,再从对齐在相反方向的图像中减去该向量。

  4. 从上一步骤中获得的结果向量生成图像

如何操作…

我们将按照以下策略进行编程(代码文件在 GitHub 上可作为 Face_generation.ipynb 获取)。注意,我们将从 使用深度卷积 GAN 生成面孔 部分的第 6 步继续:

  1. 生成三个向量(确保两幅图像对齐在一个方向上,另一幅图像则通过改变生成的噪声与之对立方向对齐):
gen_noise = np.random.normal(0, 1, (1, 100))
gen_noise2 = np.random.normal(0, 1, (1, 100))
gen_noise3 = np.random.normal(0, 1, (1, 100))
syntetic_images = Generator.predict(gen_noise4)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show()
  1. 绘制生成的图像:
plt.subplot(131)
syntetic_images = Generator.predict(gen_noise)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 1')
plt.subplot(132)
syntetic_images = Generator.predict(gen_noise2)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 2')
plt.subplot(133)
syntetic_images = Generator.predict(gen_noise3)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.title('Image 3')

三个生成的图像如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/424f17c6-4b45-4a97-a375-f043c7af965f.png

我们可以看到图像 2 和 3 的人脸朝向右侧,而图像 1 的人脸正面朝前。

  1. 对这些图像的每一个向量表示进行向量运算,以查看结果:
gen_noise4 = gen_noise + gen_noise2 - gen_noise3
syntetic_images = Generator.predict(gen_noise4)
plt.imshow(deprocess(syntetic_images)[0])
plt.axis('off')
plt.show()  

上述代码生成了如下的面孔:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/78804f8b-9305-493a-a5fb-fb140c532770.png

上述运算显示,向量运算(图像 1 + 图像 2 - 图像 3 的向量)生成的图像使得面孔朝前,从而增强了我们对潜在向量表示工作原理的直觉。

还有更多…

我们仅仅触及了 GAN 的基础;目前有多种基于 GAN 的技术正在变得流行。我们将讨论其中一些技术的应用:

  • pix2pix:想象一个场景,你涂鸦(草图)一个物体的结构,然后这个物体以多种形式呈现。pix2pix 是一种帮助实现这一点的算法。

  • Cycle GAN:想象一个场景,你希望一个物体看起来像完全不同的物体(例如,你希望一个马的物体看起来像一只斑马,反之亦然)。你还希望确保图像的其他所有部分保持不变,只有物体发生变化。在这种情况下,Cycle GAN 很有用。

  • BigGAN 是最近的一项发展,它生成的图像看起来极为真实。

第九章:编码输入

在本章中,我们将涵盖以下内容:

  • 编码的需求

  • 编码图像

  • 用于推荐系统的编码

引言

一幅典型的图像由数千个像素组成;文本也由数千个独特单词组成,而公司的独特客户数量可能达到百万级。考虑到这一点,用户、文本和图像三者都必须表示为数千个维度平面中的向量。在这样一个高维空间中表示向量的缺点在于,我们将无法有效计算向量之间的相似性。

表示图像、文本或用户在较低维度中有助于我们将非常相似的实体分组。编码是执行无监督学习的一种方法,以最小信息损失的方式将输入表示为较低维度,同时保留与相似图像有关的信息。

在本章中,我们将学习以下内容:

  • 将图像编码到更低维度

    • 香草自编码器

    • 多层自编码器

    • 卷积自编码器

  • 可视化编码

  • 在推荐系统中编码用户和项目

  • 计算编码实体之间的相似性

编码的需求

编码通常用于向量维度巨大的情况。编码有助于将大向量转换为具有较少维度的向量,同时不会从原始向量中丢失太多信息。在接下来的几节中,让我们探讨编码图像、文本和推荐系统的需求。

文本分析中的编码需求

要了解文本分析中编码的必要性,让我们考虑以下情景。让我们看看以下两个句子:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/295eeeca-8cb4-4c90-b9a8-c303c76c4fdf.png

在传统的文本分析中,前两个句子被独热编码如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/26bdf572-bf24-4ad1-9c8d-30c1cc2c3814.png

请注意,这两个句子中有五个唯一单词。

单词的独热编码版本导致句子的编码版本如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/87b77716-a1e5-4266-b56f-37c4292347a9.png

在上述情景中,我们可以看到两个句子之间的欧几里德距离大于零,因为likeenjoy的编码是不同的。然而,直观上,我们知道 like 和 enjoy 这两个词非常相似。此外,IChess之间的距离与likeenjoy之间的距离相同。

请注意,鉴于这两个句子中有五个唯一单词,我们将每个单词表示为五维空间中的一个单词。在编码版本中,我们以较低维度(比如三维)表示一个单词,以使相似的单词之间的距离较小,而不是相似的单词之间的距离较大。

图像分析中编码的需求

为了理解图像分析中对编码的需求,我们来考虑一个场景:我们对图像进行分组,但图像的标签并不存在。为了进一步澄清,我们来看一下 MNIST 数据集中相同标签的以下图像:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/a63ab262-0216-4f6e-82c4-5a3d9254d6f0.png

直观地,我们知道前面这两张图片对应的是相同的标签。然而,当我们计算这两张图片之间的欧几里得距离时,距离大于零,因为这两张图片中突出显示的像素不同。

你应该注意到在存储图像信息时存在以下问题:

尽管图像由总共 28 x 28 = 784 个像素组成,但大部分列是黑色的,因此这些列没有包含信息,导致它们在存储信息时占用了比实际需要更多的空间。

使用自动编码器,我们将前面的两张图片表示为较低维度,这样编码版本之间的距离会变得更小,同时确保编码版本不会丢失太多原始图像的信息。

推荐系统中对编码的需求

为了理解推荐系统中对编码的需求,我们来考虑顾客电影推荐的场景。类似于文本分析,如果我们对每部电影/顾客进行独热编码,我们将为每部电影(因为电影数量成千上万)得到多个千维的向量。基于顾客的观看习惯,将用户编码到更低的维度,并根据电影的相似性对电影进行分组,这样可以帮助我们推荐顾客更可能观看的电影。

类似的概念也可以应用于电子商务推荐引擎,以及在超市中向顾客推荐商品。

编码一张图片

图像编码可以通过多种方式进行。在接下来的章节中,我们将对比普通自动编码器、多层自动编码器和卷积自动编码器的性能。自动编码一词指的是以一种方式进行编码,使得原始输入可以在图像中用更少的维度重建。

自动编码器将图像作为输入,并将输入图像编码为较低的维度,这样我们就可以仅使用输入图像的编码版本来重建原始图像。本质上,你可以认为相似图像的编码版本具有相似的编码值。

准备工作

在我们定义策略之前,让我们先了解一下自动编码器是如何工作的:

  1. 我们将定义一个包含 11 个值的玩具数据集

  2. 我们将把这 11 个值表示为较低维度(二维):

    • 在降低维度的同时,尽可能保留输入数据中存在的信息

    • 低维空间中的向量称为嵌入/编码 向量瓶颈 特征/向量,或者是压缩表示

    • 通过将输入值与一个维度为 11 x 2 的随机权重矩阵进行矩阵乘法,11 个值被转换为两个值。

    • 较低维度的向量表示瓶颈特征。瓶颈特征是重建原始图像所需的特征。

  3. 我们将重建较低维度的瓶颈特征向量,以获得输出向量:

    • 二维特征向量与一个形状为 2 x 11 的矩阵相乘,得到一个形状为 1 x 11 的输出。1 x 2 与 2 x 11 向量的矩阵乘法将得到一个 1 x 11 形状的输出。
  4. 我们将计算输入向量和输出向量之间的平方差之和:

  5. 我们通过调整随机初始化的权重向量来最小化输入和输出向量之间的平方差之和。

  6. 结果编码向量将是一个低维度的向量,表示二维空间中的 11 维向量。

在利用神经网络时,您可以将编码向量视为连接输入层和输出层的隐藏层。

此外,对于神经网络,输入层和输出层的值是完全相同的,隐藏层的维度低于输入层。

在本教程中,我们将了解多种自编码器:

  • Vanilla 自编码器

  • 多层自编码器

  • 卷积自编码器

如何做到…

在接下来的部分,我们将实现多种自编码器的变种(代码文件可在 GitHub 上的Auto_encoder.ipynb中找到)。

Vanilla 自编码器

一个 Vanilla 自编码器长得如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d67047e4-6d9e-4cd9-bccf-fdd5f74f2ea5.png

如前图所示,Vanilla 自编码器使用最少的隐藏层和隐藏单元来重建输入数据。

为了理解 Vanilla 自编码器如何工作,让我们按照以下步骤操作,其中我们使用原始图像的低维编码版本来重建 MNIST 图像(代码文件可在 GitHub 上的Auto_encoder.ipynb中找到):

  1. 导入相关包:
import tensorflow as tf
import keras
import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
  1. 导入数据集:
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
  1. 重塑并缩放数据集:
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1]*X_train.shape[2])
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1]*X_test.shape[2])
X_train = X_train/255
X_test = X_test/255
  1. 构建网络架构:
model = Sequential()
model.add(Dense(32, input_dim=784, activation='relu'))
model.add(Dense(784, activation='relu'))
model.summary()

模型的摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/20ebaedb-619c-4ea0-a329-6281c3dc9837.png

在前面的代码中,我们将一个 784 维的输入表示为一个 32 维的编码版本。

  1. 编译并拟合模型:
model.compile(loss='mean_squared_error', optimizer='adam',metrics=['accuracy'])
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=10, batch_size=1024, verbose=1)

请注意,我们使用均方误差损失函数,因为像素值是连续的。此外,输入和输出数组是相同的——X_train

  1. 打印前四个输入图像的重建结果:
import matplotlib.pyplot as plt
%matplotlib inline
plt.subplot(221)
plt.imshow(model.predict(X_test[0,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(222)
plt.imshow(model.predict(X_test[1,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(223)
plt.imshow(model.predict(X_test[2,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.subplot(224)
plt.imshow(model.predict(X_test[3,:].reshape(1,784)).reshape(28,28), cmap=plt.get_cmap('gray'))
plt.axis('off')
plt.show()

重建后的 MNIST 数字如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/5f536303-198e-479f-a4d6-b3d57a3b67a4.png

为了了解自编码器的效果如何,我们来比较一下之前的预测和原始输入图像:

原始的 MNIST 数字如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/17100fd3-9b8d-42bd-8eab-da2d28d86814.png

从前面的图像中,我们可以看到,重建的图像与原始输入图像相比有些模糊。

为了避免模糊问题,我们来构建更深的多层自编码器(从而产生更多的参数),这样可以更好地表示原始图像。

多层自编码器

多层自编码器如下所示,其中有更多的隐藏层将输入层与输出层连接起来:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/1f3ee687-42fa-453a-b0a0-93c0bf13c546.png

本质上,多层自编码器通过更多的隐藏层来重建输入。

为了构建多层自编码器,我们将重复前一节中的相同步骤,直到步骤 3。然而,步骤 4,即定义网络架构的部分,将被修改为包含多层,如下所示:

model = Sequential()
model.add(Dense(100, input_dim=784, activation='relu'))
model.add(Dense(32,activation='relu'))
model.add(Dense(100,activation='relu'))
model.add(Dense(784, activation='relu'))
model.summary()

模型的摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2314656c-b2fc-4c71-a031-206d5f870de8.png

在上述网络中,我们的第一个隐藏层有 100 个单元,第二个隐藏层(即图像的嵌入版本)是 32 维的,第三个隐藏层是 100 维的。

一旦网络架构定义完成,我们就可以编译并运行它,步骤如下:

model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=25, batch_size=1024, verbose=1)

上述模型的预测结果如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/ff4c5081-bf2f-4346-9439-33d59b9a2bd4.png

请注意,与原始图像相比,之前的预测结果仍然有些模糊。

卷积自编码器

到目前为止,我们已经探讨了传统和多层自编码器。在本节中,我们将看到卷积自编码器如何从低维向量中重建原始图像。

卷积自编码器如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/3cd826e0-e037-416a-992e-7ab29296eb34.png

本质上,卷积自编码器通过更多的隐藏层来重建输入,其中隐藏层包括卷积、池化以及对下采样图像的上采样。

类似于多层自编码器,卷积自编码器与其他类型的自编码器在模型架构上有所不同。在以下代码中,我们将定义卷积自编码器的模型架构,而其他步骤与传统自编码器保持一致,直到步骤 3

X_trainX_test 形状之间唯一的区别如下所示:

(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()

X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],X_train.shape[2],1)
X_test = X_test.reshape(X_test.shape[0],X_test.shape[1],X_test.shape[2],1)
X_train = X_train/255
X_test = X_test/255

请注意,在前面的步骤中,我们正在重塑图像,以便将其传递给 conv2D 方法:

  1. 定义模型架构:
model = Sequential()
model.add(Conv2D(32, (3,3), input_shape=(28, 28,1), activation='relu',padding='same',name='conv1'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool1'))
model.add(Conv2D(16, (3,3), activation='relu',padding='same',name='conv2'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool2'))
model.add(Conv2D(8, (3,3), activation='relu',padding='same',name='conv3'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool3'))
model.add(Conv2D(32, (3,3), activation='relu',padding='same',name='conv4'))
model.add(MaxPooling2D(pool_size=(2, 2),name='pool4'))
model.add(Flatten(name='flatten'))
model.add(Reshape((1,1,32)))
model.add(Conv2DTranspose(8, kernel_size = (3,3), activation='relu'))
model.add(Conv2DTranspose(16, kernel_size = (5,5), activation='relu'))
model.add(Conv2DTranspose(32, kernel_size = (8,8), activation='relu'))
model.add(Conv2DTranspose(32, kernel_size = (15,15), activation='relu'))
model.add(Conv2D(1, (3, 3), activation='relu',padding='same'))
model.summary()

在前面的代码中,我们定义了一个卷积架构,将输入图像重塑为具有 32 维嵌入版本的图像,该嵌入版本位于架构的中间,最后进行上采样,从而使我们能够重建该图像。

模型的摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/008c615f-9a99-4eb9-9a39-141aa90aeda8.jpg

  1. 编译并拟合模型
from keras.optimizers import Adam
adam = Adam(lr=0.001)
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(X_train, X_train, validation_data=(X_test, X_test),epochs=10, batch_size=1024, verbose=1)

一旦我们对前四个测试数据点进行预测,重建后的图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/ede7d605-daf3-4166-a7ae-c42a38c8a464.png

请注意,当前的重构效果略优于之前使用 Vanilla 和多层自编码器对测试图像进行的两个重构。

将相似的图像分组

在前面的章节中,我们将每个图像表示为较低维度的向量,直觉是相似的图像会有相似的嵌入,而不相似的图像则会有不同的嵌入。然而,我们还没有考察相似度度量,也没有详细检查嵌入。

在本节中,我们将尝试在二维空间中绘制嵌入。我们可以使用一种叫做t-SNE的技术,将 32 维向量降维到二维空间。(更多关于 t-SNE 的内容可以参考这里:www.jmlr.org/papers/v9/vandermaaten08a.html。)

通过这种方式,我们的直觉——相似的图像会有相似的嵌入——可以得到验证,因为相似的图像应该聚类在二维平面上。

在以下代码中,我们将所有测试图像的嵌入表示为二维平面:

  1. 提取测试集中每个 10,000 张图像的 32 维向量:
from keras.models import Model
layer_name = 'flatten'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
intermediate_output = intermediate_layer_model.predict(X_test)
  1. 执行 t-SNE 生成二维向量:
from sklearn.manifold import TSNE
tsne_model = TSNE(n_components=2, verbose=1, random_state=0)
tsne_img_label = tsne_model.fit_transform(intermediate_output)
tsne_df = pd.DataFrame(tsne_img_label, columns=['x', 'y'])
tsne_df['image_label'] = y_test
  1. 绘制测试图像嵌入的 t-SNE 维度可视化:
from ggplot import *
chart = ggplot(tsne_df, aes(x='x', y='y', color='factor(image_label)'))+ geom_point(size=70,alpha=0.5)
chart

二维空间中嵌入的可视化如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/9f76bf7a-b13e-4f60-85bd-a035f6b14dd5.png

请注意,在上面的图表中,我们可以看到,相同标签的图像通常会形成簇。

推荐系统的编码

到目前为止,在前面的章节中,我们对图像进行了编码。在本节中,我们将对电影相关数据集中的用户和电影进行编码。原因在于,可能会有数百万个用户和成千上万部电影在目录中。因此,我们不能直接对这些数据进行独热编码。在这种情况下,编码就显得尤为重要。矩阵分解是推荐系统中常用的编码技术之一。在下一节中,我们将理解它的工作原理,并为用户和电影生成嵌入。

准备工作

编码用户和电影的思路如下:

如果两个用户在喜欢某些电影方面相似,那么表示这两个用户的向量应该相似。类似地,如果两部电影相似(可能属于同一类型或有相同的演员阵容),它们的向量应该相似。

我们将采用的电影编码策略,目的是根据用户观看过的历史电影推荐一组新的电影,具体如下:

  1. 导入包含用户信息及他们给不同电影打分的数据集

  2. 为用户和电影分配 ID

  3. 将用户和电影转换为 32 维向量

  4. 使用 Keras 的功能 API 执行电影和用户的 32 维向量的点积:

    • 如果有 100,000 个用户和 1,000 部电影,则电影矩阵将是 1,000 x 32 维,而用户矩阵将是 100,000 x 32 维。

    • 两者的点积将是 100,000 x 1,000 的维度。

  5. 将输出展平并通过一个密集层,然后连接到输出层,输出层具有线性激活,输出值的范围从 1 到 5。

  6. 训练模型

  7. 提取电影的嵌入权重

  8. 提取用户的嵌入权重

  9. 可以通过计算目标电影与数据集中其他所有电影的成对相似度,找到与给定电影相似的电影。

如何实现…

在下面的代码中,我们将为一个用户和一部电影设计一个向量,这在典型的推荐系统中使用(代码文件可以在 GitHub 的Recommender_systems.ipynb中找到):

  1. 导入数据集。推荐的数据集可以在 GitHub 上的代码中找到。
import numpy as np
import pandas as pd
from keras.layers import Input, Embedding, Dense, Dropout, merge, Flatten, dot
from keras.models import Model
from keras.optimizers import Adam
ratings = pd.read_csv('...') # Path to the user-movie-ratings file

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/922112db-3673-419c-ae17-295a587c0051.jpg

  1. 将用户和电影转换为分类变量。在下面的代码中,我们创建了两个新的变量——User2Movies2——它们是分类变量:
ratings['User2']=ratings['User'].astype('category')
ratings['Movies2']=ratings['Movies'].astype('category')
  1. 为每个用户和电影分配一个唯一的 ID:
users = ratings.User.unique()
movies = ratings.Movies.unique()
userid2idx = {o:i for i,o in enumerate(users)}
moviesid2idx = {o:i for i,o in enumerate(movies)}
idx2userid = {i:o for i,o in enumerate(users)}
idx2moviesid = {i:o for i,o in enumerate(movies)}
  1. 将唯一的 ID 作为新列添加到原始表中:
ratings['Movies2'] = ratings.Movies.apply(lambda x: moviesid2idx[x])
ratings['User2'] = ratings.User.apply(lambda x: userid2idx[x])
  1. 为每个用户 ID 和唯一 ID 定义嵌入:
n_users = ratings.User.nunique()
n_movies = ratings.Movies.nunique()

在前面的代码中,我们正在提取数据集中唯一用户和唯一电影的总数:

def embedding_input(name,n_in,n_out):
  inp = Input(shape=(1,),dtype='int64',name=name)
  return inp, Embedding(n_in,n_out,input_length=1)(inp)

在前面的代码中,我们定义了一个函数,输入一个 ID,将其转换为一个嵌入向量,该向量的维度为n_out,总共有n_in个值:

n_factors = 100
user_in, u = embedding_input('user_in', n_users, n_factors)
article_in, a = embedding_input('article_in', n_movies, n_factors)

在前面的代码中,我们正在为每个唯一用户以及每个唯一电影提取 100 个维度。

  1. 定义模型:
x = dot([u,a], axes=1)
x=Flatten()(x)
x = Dense(500, activation='relu')(x)
x = Dense(1)(x)
model = Model([user_in,article_in],x)
adam = Adam(lr=0.01)
model.compile(adam,loss='mse')
model.summary()

模型摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f135011c-a121-4c89-868a-a4d1f89a3baa.jpg

  1. 训练模型:
model.fit([ratings.User2,ratings.Movies2], ratings.rating, epochs=50,batch_size=128)
  1. 提取每个用户或电影的向量:
# Extracting user vectors
model.get_weights()[0]

# Extracting movie vectors
model.get_weights()[1]

正如我们之前所想,类似的电影应该有相似的向量。

通常,在识别嵌入之间的相似性时,我们使用一种称为余弦相似度的度量(如何计算余弦相似度的更多信息将在下一章中介绍)。

对于一个随机选中的电影,其位于第 574^(个)位置,余弦相似度计算如下:

from sklearn.metrics.pairwise import cosine_similarity
np.argmax(cosine_similarity(model.get_weights()[1][574].reshape(1,-1),model.get_weights()[1][:574].reshape(574,100)))

从前面的代码中,我们可以计算出与位于分类电影列中第 574^(个)位置的电影最相似的 ID。

一旦我们查看电影 ID 列表,我们应该会看到与给定电影最相似的电影,直观上它们确实是相似的。

第十章:使用单词向量进行文本分析

在上一章中,我们学习了如何对图像、用户或电影进行编码以用于推荐系统,在这些系统中,相似的物品有相似的向量。在本章中,我们将讨论如何对文本数据进行编码。

你将学习以下主题:

  • 从零开始在 Python 中构建单词向量

  • 使用 skip-gram 和 CBOW 模型构建单词向量

  • 使用预训练的单词向量进行向量运算

  • 创建文档向量

  • 使用 fastText 构建单词向量

  • 使用 GloVe 构建单词向量

  • 使用单词向量构建情感分类

介绍

在传统的解决与文本相关的问题的方法中,我们会进行一-hot 编码。然而,如果数据集包含成千上万个唯一的单词,那么得到的一-hot 编码向量将有成千上万的维度,这可能会导致计算问题。此外,在这种情况下,相似的单词并不会拥有相似的向量。Word2Vec 是一种方法,帮助我们为相似的单词获得相似的向量。

为了理解 Word2Vec 如何有用,我们来探讨以下问题。

假设我们有两个输入句子:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2ffc33e5-9913-45da-96d2-64b6c54b31b6.png

从直观上看,我们知道enjoy(享受)和like(喜欢)是相似的单词。然而,在传统的文本挖掘中,当我们对这些单词进行一-hot 编码时,输出如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/06066bdf-95ff-41d5-85cb-e1789acc850e.png

注意到一-hot 编码会将每个单词分配到一个列中。这样的一-hot 编码的主要问题是,Ienjoy之间的欧几里得距离与enjoylike之间的欧几里得距离是相同的。

然而,从直观上看,我们知道enjoylike之间的距离应该比Ienjoy之间的距离要小,因为enjoylike是彼此相似的。

从零开始在 Python 中构建单词向量

我们将构建单词向量的原理是相关的单词周围会有相似的词

例如:queen(皇后)和princess(公主)这两个单词会更频繁地拥有与kingdom(王国)相关的词汇在它们周围。从某种程度上说,这些单词的上下文(周围的词汇)是相似的。

准备工作

当我们将周围的单词作为输入,中间的单词作为输出时,我们的数据集(包含两个句子)如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d5a3f1d0-5066-49a1-80a8-70c53b0979e8.png

注意我们是使用中间词作为输出,其余的词作为输入。这个输入和输出的向量化形式如下所示(回想一下我们在第九章的文本分析中编码的必要性部分中,输入编码部分,如何将一个句子转换成向量):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/17715c08-bfa2-4f12-a2dd-c8b30b67a2cc.png

请注意,第一行中输入的向量化形式是*{0, 1, 1, 1, 0},因为输入词的索引是{1, 2, 3},输出是{1, 0, 0, 0, 0},因为输出词的索引是{1}*。

在这种情况下,我们的隐藏层有三个神经元与之相关联。我们的神经网络将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/536560c0-abd1-4c24-8e19-5e09c079e68f.png

每一层的维度如下:

权重形状备注
输入层1 x 5每一行与五个权重相乘。
隐藏层5 x 3每五个输入权重分别连接到隐藏层中的三个神经元。
隐藏层输出1 x 3这是输入和隐藏层的矩阵乘法。
从隐藏层到输出层的权重3 x 5三个输出隐藏单元被映射到五个输出列(因为有五个独特的词)。
输出层1 x 5这是隐藏层输出与从隐藏层到输出层权重的矩阵乘法。

请注意,在构建词向量时,我们不会对隐藏层进行激活处理。

输出层的值没有限制在特定范围内。因此,我们通过 softmax 函数将其转换为词的概率。此外,我们最小化交叉熵损失,以便在整个网络中得到最优的权重值。现在,给定词的词向量就是当输入是该词的一热编码版本时,隐藏层单元的值(而不是输入句子)。

如何实现…

现在我们知道了如何生成词向量,接下来我们在 GitHub 中编写生成词向量的过程(代码文件为Word_vector_generation.ipynb):

  1. 定义感兴趣的句子:
docs = ["I enjoy playing TT", "I like playing TT"]

从前面的内容可以预期,enjoylike的词向量应该相似,因为它们周围的词是完全相同的。

  1. 现在让我们创建每个句子的独热编码版本:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=0, token_pattern=r"\b\w+\b")
vectorizer.fit(docs)

请注意,vectorizer 定义了将文档转换为向量格式的参数。此外,我们传入更多的参数,以确保像I这样的词不会在CountVectorizer中过滤掉。

此外,我们将把文档适配到定义的 vectorizer 中。

  1. 将文档转换为向量格式:
vector = vectorizer.transform(docs)
  1. 验证所做的转换:
print(vectorizer.vocabulary_)
print(vector.shape)
print(vector.toarray())

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/5a5b6dc6-d127-48fa-b65e-75253bde3f66.png

请注意,vocabulary_返回各种词的索引,并且转换后的toarray向量返回句子的独热编码版本。

  1. 创建输入和输出数据集:
x = []
y = []
for i in range(len(docs)):
     for j in range(len(docs[i].split())):
         t_x = []
         t_y = []
         for k in range(4):
             if(j==k):
                 t_y.append(docs[i].split()[k])
                 continue
             else:
                 t_x.append(docs[i].split()[k])
         x.append(t_x)
         y.append(t_y)

x2 = []
y2 = []
for i in range(len(x)):
     x2.append(' '.join(x[i]))
     y2.append(' '.join(y[i]))

从前面的代码,我们已经创建了输入和输出数据集。这里是输入数据集:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/0e9852c5-b6c2-45ca-a41f-8ad7a8f4a1d2.jpg

这里是输出数据集:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/7269b7b9-b223-4ba1-b9cf-0103abd330da.png

  1. 将前述的输入和输出词转换为向量:
vector_x = vectorizer.transform(x2)
vector_x.toarray()
vector_y = vectorizer.transform(y2)
vector_y.toarray()

这里是输入数组:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/4ac45077-8dbc-41aa-b6b8-5ad81fbd5a88.png

这里是输出数组:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/98d56a01-487d-4a36-8ae1-f040f3cf8a1e.png

  1. 定义一个神经网络模型,该模型通过一个包含三个单元的隐藏层来映射输入和输出向量:
model = Sequential()
model.add(Dense(3, activation='linear', input_shape=(5,)))
model.add(Dense(5,activation='sigmoid'))
  1. 编译并拟合模型:
model.compile(loss='binary_crossentropy',optimizer='adam')

model.fit(vector_x, vector_y, epochs=1000, batch_size=4,verbose=1)
  1. 通过提取中间层的值来提取词向量,输入为每个单独词的向量(而不是一个句子):
from keras.models import Model
layer_name = 'dense_5'
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)

在之前的代码中,我们从我们感兴趣的层提取输出:在我们初始化的模型中,名为dense_5的层。

在下面的代码中,我们在传递词的一次性编码版本作为输入时提取中间层的输出:

for i in range(len(vectorizer.vocabulary_)):
     word = list(vectorizer.vocabulary_.keys())[i]
     word_vec = vectorizer.transform([list(vectorizer.vocabulary_.keys())[i]]).toarray()
     print(word, intermediate_layer_model.predict(word_vec))

单个词的词向量如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d0f17447-2ac1-4512-b2be-0c821dd11b4e.png

注意,享受喜欢之间的相关性比其他词更强,因此它们更好地表示词向量。

由于我们在构建模型时没有指定层名称,因此您运行的模型的名称可能不同。此外,在我们没有明确指定模型名称的情况下,每次初始化模型时,层名称都会发生变化。

测量词向量之间的相似度

词向量之间的相似度可以通过多种度量方法来测量——以下是两种常见的度量方法:

  • 余弦相似度

  • 欧几里得距离

两个不同向量,AB之间的余弦相似度计算如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cf0c3808-dfe4-473c-be3a-b0fd82932a7e.png

在上一节的例子中,享受喜欢之间的余弦相似度计算如下:

享受 = (-1.43, -0.94, -2.49)

喜欢 = (-1.43, -0.94, -2.66)

这里是享受喜欢向量之间的相似度:

(-1.43-1.43 + -0.94*-0.94 ±2.49*-2.66)/ sqrt((-1.43)² + (-0.94)² + (-2.49)²)* sqrt((-1.43)² + (-0.94)² + (-2.66)²) = 0.99*

两个不同向量,AB之间的欧几里得距离计算如下:

distance = sqrt(A-B)²

= sqrt((-1.43 - (-1.43))² + (-0.94 - (-0.94))² + (-2.49 - (-2.66))²)

= 0.03

使用 skip-gram 和 CBOW 模型构建词向量

在之前的食谱中,我们构建了一个词向量。在本食谱中,我们将使用gensim库构建 skip-gram 和 CBOW 模型。

准备工作

我们在本例中采用的方法来构建词向量称为连续词袋模型CBOW)。之所以称为 CBOW,解释如下:

我们以这句话为例:我享受玩 TT

下面是 CBOW 模型处理此句子的方式:

  1. 固定一个大小为 1 的窗口。

    • 通过指定窗口大小,我们确定了给定词的左右两边将被考虑的词的数量。
  2. 给定窗口大小,输入和输出向量将如下所示:

输入词输出词
{我, 玩}{享受}
{享受,TT}{玩}

另一种构建词向量的方法是 skip-gram 模型,其中之前的步骤被反转,如下所示:

输入词输出词
{enjoy}{I, playing}
{playing}{enjoy, TT}

无论是 skip-gram 模型还是 CBOW 模型,得到单词隐藏层值的方法都是相同的,正如我们在前面的部分讨论过的那样。

如何实现:

现在我们理解了单词向量的构建后台工作,让我们使用 skip-gram 和 CBOW 模型来构建单词向量。为了构建模型,我们将使用航空公司情感数据集,其中给出了推文文本以及对应的情感。为了生成词向量,我们将使用gensim包,如下所示(代码文件在 GitHub 上可用,文件名为word2vec.ipynb):

  1. 安装gensim包:
$pip install gensim
  1. 导入相关的包:
import gensim
import pandas as pd
  1. 读取航空公司推文情感数据集,其中包含与航空公司相关的评论(文本)及其相应的情感。数据集可以从d1p17r2m4rzlbo.cloudfront.net/wp-content/uploads/2016/03/Airline-Sentiment-2-w-AA.csv获取:
data=pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv')
data.head()                   

数据集样本如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/7cdad718-62b5-41b1-8158-947dd9313b85.png

  1. 对前面的文本进行预处理,执行以下操作:

    • 将每个单词标准化为小写。

    • 移除标点符号,仅保留数字和字母。

    • 移除停用词:

import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
  1. 将句子拆分为一个词汇表(tokens)的列表,这样它们就可以传递给gensim。第一句的输出应如下所示:
data['text'][0].split()

上面的代码通过空格分割句子,结果如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/20e6f8c6-be19-41b0-a33e-6c2574611a1d.png

我们将遍历所有文本并将其附加到一个列表中,如下所示:

list_words=[]
for i in range(len(data)):
     list_words.append(data['text'][i].split())

让我们查看列表中的前三个子列表:

list_words[:3]

前三句话的词汇表如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/b06668c0-b3d5-4475-afce-4f836f7cf292.png

  1. 构建Word2Vec模型:
from gensim.models import Word2Vec

定义词向量的维度、上下文窗口的大小以及词汇的最小计数要求,作为它有资格拥有词向量的标准,如下所示:

model = Word2Vec(size=100,window=5,min_count=30, sg=0, alpha = 0.025)

在前面的代码中,size表示词向量的大小(维度),window表示考虑的上下文词汇的大小,min_count指定词汇出现的最小频率,sg表示是否使用 skip-gram(当sg=1时)或使用 CBOW(当sg=0时),alpha表示模型的学习率。

一旦模型定义完成,我们将传递我们的列表来构建词汇表,如下所示:

model.build_vocab(list_words)

一旦构建好词汇表,过滤掉在整个语料库中出现次数少于 30 次的词语,剩余的最终词汇如下所示:

model.wv.vocab.keys()
  1. 通过指定需要考虑的示例(列表)总数以及运行的迭代次数(epochs)来训练模型,如下所示:
model.train(list_words, total_examples=model.corpus_count, epochs=100)

在上述代码中,list_words(单词列表)是输入,total_examples表示要考虑的列表总数,epochs是运行的训练轮数。

或者,你也可以通过在Word2Vec方法中指定iter参数来训练模型,具体如下:

model = Word2Vec(list_words,size=100,window=5,min_count=30, iter = 100)
  1. 提取给定词(month)的词向量,具体如下:
model['month']

对应"month"这个词的词向量如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/074b3142-43d7-44e8-9b3d-0875022063eb.png

两个词之间的相似度可以按以下方式计算:

model.similarity('month','year')
0.48

给定词与最相似的词是通过以下方式计算的:

model.most_similar('month')

month这个词最相似的词如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d45f1891-0a22-4ffe-b943-338221155ccd.png

请注意,尽管这些相似度看起来较低,有些最相似的词也不直观,但一旦我们在比我们现有的 11,000 条推文数据集更大的数据集上进行训练,结果会更加真实。

在上述场景中,运行模型若干轮后,看看与"month"这个词最相似的词是什么:

model = Word2Vec(size=100,window=5,min_count=30, sg=0)
model.build_vocab(list_words)
model.train(list_words, total_examples=model.corpus_count, epochs=5)
model.most_similar('month')

与"month"最相似的词如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/ccf6b20e-4013-4fc5-aea4-643a97f97cb9.png

我们可以看到,如果训练轮数较少,与month最相似的词不太直观,而当训练轮数较多时,结果更具直观性,特别是因为在训练轮数少的情况下,权重没有完全优化。

通过将sg参数的值设置为1,可以将相同的操作应用于 skip-gram。

使用预训练的词向量进行向量运算

在前一节中,我们看到的一个限制是,句子的数量太少,无法构建一个强健的模型(我们在前一节中看到,month 和 year 之间的相关性大约为 0.4,比较低,因为它们属于相同类型的词)。

为了克服这种情况,我们将使用由 Google 训练的词向量。Google 提供的预训练词向量包括一个包含 3,000,000 个单词和短语的词汇表,这些词向量是在 Google 新闻数据集上的单词上训练的。

如何操作……

  1. 从 Google News 下载预训练的词向量(代码文件可以在 GitHub 上作为word2vec.ipynb获取):
$wget https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz

解压下载的文件:

$gunzip '/content/GoogleNews-vectors-negative300.bin.gz'

此命令解压bin文件,该文件是模型的保存版本。

  1. 加载模型:
from gensim.models import KeyedVectors
filename = '/content/GoogleNews-vectors-negative300.bin'
model = KeyedVectors.load_word2vec_format(filename, binary=True)
  1. 加载与给定词month最相似的词:
model.most_similar('month')

month最相似的词如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/df73a48b-f995-4ad3-98a2-97506f9bad8d.png

  1. 我们将执行向量运算;也就是说,我们将尝试回答以下类比问题:woman 与 man 的关系,什么与 king 的关系最相似?请查看以下代码:
result = model.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
print(result)

上述算式的输出结果如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/568bc06c-e395-4066-895d-74c8d070fd04.png

在这种情况下,将woman的词向量从man的词向量中减去,并将其加到king的词向量中,从而得到一个最接近queen的词向量。

创建文档向量

为了理解文档向量的存在原因,我们来梳理一下以下的直觉。

单词bank在金融和河流两个语境中都有使用。我们如何识别给定句子或文档中的bank是与河流主题相关,还是与金融主题相关呢?

这个问题可以通过添加文档向量来解决,这与单词向量的生成方式类似,但在此基础上加入了段落 ID 的一热编码版本,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d1f47a77-220b-459a-9c00-2e9d68fb81d9.png

在前述场景中,段落 ID 包含了那些仅通过单词无法捕捉到的差异。例如,在句子on the bank of river中,on the bank of是输入,river是输出,单词on, theof由于是高频词,因此不会对预测产生贡献,而单词bank则使得输出预测变得模糊,可能是河流或是美国。这个特定文档/句子的文档 ID 将帮助识别该文档是与河流相关,还是与金融相关。这个模型称为段落向量的分布式记忆模型PV-DM)。

例如,如果文档数量为 100,则段落 ID 的一热编码版本将是 100 维的。同样,如果符合最小频率的唯一单词数量为 1,000,则这些单词的一热编码版本将是 1,000 维的。当隐藏层的大小(即单词向量大小)为 300 时,参数的总数将是 100 * 300 + 1,000 * 300 = 330,000。

文档向量将是当所有输入单词的一热编码版本为 0 时的隐藏层值(即,单词的影响被中和,仅考虑文档/段落 ID 的影响)。

类似于输入和输出在 skip-gram 和 CBOW 模型中相互转换的方式,即使是文档向量,输出和输入也可以按照如下方式交换:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/4cc8cbac-3d82-4fe8-ad28-0b5ac55e1631.png

这种模型的表示称为段落向量与分布式词袋模型PVDBOW)。

准备中

我们构建文档向量的策略如下:

  • 对输入句子进行预处理,去除标点符号,并将所有单词小写,同时移除停用词(如andthe等出现频率很高且不对句子提供上下文意义的词)。

  • 给每个句子标记上其句子 ID。

    • 我们为每个句子分配一个 ID。
  • 使用 Doc2Vec 方法提取文档 ID 以及单词的向量。

    • 在较多的训练周期中训练 Doc2Vec 方法,以便对模型进行训练。

如何实现……

现在我们已经理解了文档向量是如何生成的,并且制定了构建文档向量的策略,接下来让我们生成航空公司推文数据集的文档向量(代码文件在 GitHub 上可用,名为word2vec.ipynb):

  1. 导入相关包:
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from nltk.tokenize import word_tokenize
  1. 预处理推文文本:
import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
  1. 创建一个包含标记文档的字典,其中文档 ID 会与文本(推文)一起生成:
import nltk
nltk.download('punkt')
tagged_data = [TaggedDocument(words=word_tokenize(_d.lower()), tags=[str(i)]) for i, _d in enumerate(data['text'])]

标记文档数据如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/cfc4df1f-d5d2-4bbd-94a5-b9e08757d5c3.png

在上面的代码中,我们正在提取句子(文档)中所有组成词汇的列表。

  1. 按照如下方式初始化一个带有参数的模型:
max_epochs = 100
vec_size = 300
alpha = 0.025
model = Doc2Vec(size=vec_size,
                alpha=alpha,
                min_alpha=0.00025,
                min_count=30,
                dm =1)

在上面的代码片段中,size 表示文档的向量大小,alpha 表示学习率,min_count 表示单词的最小频率,dm = 1 表示 PV-DM

构建词汇表:

model.build_vocab(tagged_data)
  1. 在标记数据上训练模型,进行多次迭代:
model.train(tagged_data,epochs=100,total_examples=model.corpus_count)
  1. 训练过程会生成单词和文档/段落 ID 的向量。

词向量可以与上一节中提取的方式相似地获取,如下所示:

model['wife']

可以按如下方式获取文档向量:

model.docvecs[0]

上述代码片段生成了第一个文档的文档向量片段。

  1. 提取与给定文档 ID 最相似的文档:
similar_doc = model.docvecs.most_similar('457')
print(similar_doc)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/45742cdd-3b4c-44a5-8459-8e4833b21286.png

在上面的代码片段中,我们正在提取与文档 ID 457 最相似的文档 ID,该 ID 为 827。

让我们来看一下文档 457 和 827 的文本:

data['text'][457]

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/87db2662-06c1-414d-a69d-25c9b5e0bacd.png

data['text'][827]

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2c6c1158-93c8-4324-8de6-169ac90b85d4.png

如果我们检查模型的词汇表,我们会看到除了单词 just,所有其他单词都出现在两个句子之间——因此很明显,文档 ID 457 与文档 ID 827 最相似。

使用 fastText 构建词向量

fastText 是由 Facebook 研究团队创建的一个库,用于高效学习词表示和句子分类。

fastText 与 word2vec 的不同之处在于,word2vec 将每个单词视为最小的单位,其向量表示是要查找的,而 fastText 假设一个单词是由字符的 n-gram 组成的;例如,sunny 由 [sun, sunn, sunny][sunny, unny, nny] 等组成,其中我们看到了原始单词的一个子集,大小为 n,其中 n 可以从 1 到原始单词的长度。

使用 fastText 的另一个原因是,有些单词在 skip-gram 或 CBOW 模型中没有达到最小频率阈值。例如,单词 appendedappend 可能没有太大区别。然而,如果 append 出现频率较高,并且在新的句子中我们遇到的是 appended 而不是 append,那么我们就无法为 appended 提供向量。在这种情况下,fastText 的 n-gram 考虑非常有用。

实际上,fastText 使用的是 skip-gram/CBOW 模型,但它增强了输入数据集,以便考虑到未见过的单词。

准备就绪

我们将采用的策略是使用 fastText 提取词向量,具体如下:

  1. 使用 gensim 库中的 fastText 方法

  2. 预处理输入数据

  3. 将每个输入句子拆分成一个列表的列表

  4. 在输入的列表列表上构建词汇表

  5. 使用之前的输入数据训练模型,进行多次迭代

  6. 计算单词之间的相似度

如何操作…

在以下代码中,让我们看看如何使用 fastText 生成词向量(代码文件在 GitHub 上的 word2vec.ipynb 中可用):

  1. 导入相关的包:
from gensim.models.fasttext import FastText
  1. 预处理并将数据集准备为列表的列表,就像我们为 word2vec 模型所做的那样:
import re
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop = set(stopwords.words('english'))
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)

在之前的代码中,我们正在对输入文本进行预处理。接下来,让我们将输入文本转换成一个列表的列表:

list_words=[]
for i in range(len(data)):
     list_words.append(data['text'][i].split())
  1. 定义模型(指定每个词的向量数)并构建词汇表:
ft_model = FastText(size=100)
ft_model.build_vocab(list_words)
  1. 训练模型:
ft_model.train(list_words, total_examples=ft_model.corpus_count,epochs=100)
  1. 检查模型词汇表中不存在的单词的词向量。例如,单词 first 存在于词汇表中;然而,单词 firstli 不在词汇表中。在这种情况下,检查 firstfirstli 的词向量之间的相似度:
ft_model.similarity('first','firstli')

之前代码片段的输出是 0.97,表示这两个单词之间有很高的相关性。

因此,我们可以看到 fastText 词向量帮助我们生成词汇表中不存在的单词的词向量。

前述方法也可以用于修正数据集中可能存在的拼写错误,因为拼写错误的单词通常出现的频率较低,而与之最相似且频率最高的单词更可能是拼写正确的版本。

拼写修正可以通过向量运算来执行,如下所示:

result = ft_model.most_similar(positive=['exprience', 'prmise'], negative=['experience'], topn=1)
print(result)

请注意,在之前的代码中,正向词有拼写错误,而负向词没有。代码的输出是 promise,这意味着它可能会修正我们的拼写错误。

此外,还可以通过以下方式执行:

ft_model.most_similar('exprience', topn=1)

[(‘experience’, 0.9027844071388245)]

然而,请注意,当存在多个拼写错误时,这种方法不起作用。

使用 GloVe 构建词向量

类似于 word2vec 生成词向量的方式,GloVe(即全局词向量表示的缩写)也生成词向量,但采用的是不同的方法。在本节中,我们将探讨 GloVe 的工作原理,然后详细介绍 GloVe 的实现细节。

准备就绪

GloVe 旨在实现两个目标:

  • 创建捕获意义的词向量

  • 利用全局计数统计而非仅依赖局部信息

GloVe 通过查看单词的共现矩阵并优化损失函数来学习词向量。GloVe 算法的工作细节可以从以下示例中理解:

让我们考虑一个场景,其中有两个句子,如下所示:

Sentences
这是测试
这也是一个

让我们尝试构建一个词共现矩阵。在我们的玩具数据集中有五个唯一单词,从而得到的词共现矩阵如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/adf142cf-c7c0-4105-83bb-56260c16ab48.png

在上表中,单词thisis在数据集的两行中共同出现,因此共现值为 2。类似地,单词thistest仅在数据集中出现一次,因此共现值为 1。

但是,在前述矩阵中,我们没有考虑两个单词之间的距离。考虑两个单词之间距离的直觉是,共同出现的单词之间的距离越远,它们的相关性可能越低。

我们将引入一个新的度量——偏移量,它惩罚给定单词与共现单词之间的高距离。例如,在第一句中,testthis的距离为 2,因此我们将共现数除以 2。

转换后的共现矩阵现在如下所示:

thisistestalsoa
this020.50.50.33
is00110.5
test00000
also00001
a00000

现在我们已经构建了矩阵,让我们引入一个额外的参数:单词的上下文。例如,如果窗口大小为 2,则单词thisa之间的共现值将为 0,因为两个单词之间的距离大于 2。当上下文窗口大小为 2 时,转换后的共现矩阵如下所示:

thisistestalsoa
this020.50.50
is00110.5
test00000
also00001
a00000

现在我们得到了一个修改后的共现矩阵,我们随机初始化了每个单词的词向量,此示例中每个单词的维度为 2。每个单词的随机初始化权重和偏置值如下所示:

WordWeights 1Weights 2Weights 3Bias
this-0.640.82-0.080.16
is-0.89-0.310.79-0.34
test-0.010.140.82-0.35
also-0.1-0.670.890.26
a-0.1-0.840.350.36

由于前面的权重和偏置是随机初始化的,我们修改权重以优化损失函数。为了做到这一点,我们定义感兴趣的损失函数如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/7d0d0021-4a99-46aa-9ec9-6a8121574215.png

在前面的公式中,w[i]表示第i个单词的词向量,w[j]表示第j个单词的词向量;b[i]b[j]分别是与第i个和第j个单词对应的偏置;*X[ij]*表示我们之前定义的最终共现值。

例如,当i是单词thisj是单词also时,*X[ij]*的值为 0.5。

当*X[ij]*的值为 0 时,f(x[ij])的值为 0;否则,计算方式如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/e3cdf189-d305-4b20-88c4-6c4ba834f4b1.png

在前面的公式中,alpha 通过经验发现为 0.75,*x[max]为 100,xx[ij]*的值。

现在公式已经定义,让我们将其应用到我们的矩阵中,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/d9c693ff-588a-47e5-b951-5503216d9586.png

第一张表表示单词共现矩阵和随机初始化的权重和偏置。

第二张表表示损失值计算,我们在其中计算整体加权损失值。

我们优化权重和偏置,直到整体加权损失值最小。

如何操作…

既然我们已经知道了如何使用 GloVe 生成词向量,那么我们可以在 Python 中实现相同的功能(代码文件可以在 GitHub 上的word2vec.ipynb中找到):

  1. 安装 GloVe:
$pip install glove_python
  1. 导入相关包:
from glove import Corpus, Glove
  1. 按照我们在 word2vec、skip-gram 和 CBOW 算法中预处理数据集的方式进行预处理,如下所示:
import re
import nltk
from nltk.corpus import stopwords
import pandas as pd
nltk.download('stopwords')
stop = set(stopwords.words('english'))
data = pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv?dl=1')
def preprocess(text):
    text=text.lower()
    text=re.sub('[⁰-9a-zA-Z]+',' ',text)
    words = text.split()
    words2 = [i for i in words if i not in stop]
    words3=' '.join(words2)
    return(words3)
data['text'] = data['text'].apply(preprocess)
list_words=[]
for i in range(len(data)):
      list_words.append(data['text'][i].split())
  1. 创建一个语料库并为其配备词汇表:
corpus.fit(list_words, window=5)

语料库的字典可以通过以下方式找到:

corpus.dictionary

得到唯一单词及其对应的单词 ID 如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/a15b2447-73bd-42bd-97e8-5bb1d630f4e9.png

前面的截图表示单词的关键值及其对应的索引。

以下代码片段给出了共现矩阵:

corpus.matrix.todense()

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/0248814c-3205-47f8-b94d-79788deb21df.png

  1. 让我们定义模型参数,即向量的维度数量、学习率和要运行的 epoch 数,如下所示:
glove = Glove(no_components=100, learning_rate=0.025)
glove.fit(corpus.matrix, epochs=30, no_threads=4, verbose=True)
  1. 模型拟合完成后,可以通过以下方式找到词向量的权重和偏置:
glove.word_biases.tolist()
glove.word_vectors.tolist()
  1. 给定单词的词向量可以通过以下方式确定:
glove.word_vectors[glove.dictionary['united']]
  1. 给定单词的最相似单词可以通过以下方式确定:
glove.most_similar('united')

最相似的单词与“united”比较,输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f88b5cb8-7fc0-4a9f-97cc-13fdc15c6c0e.png

请注意,与united最相似的单词是属于其他航空公司的单词。

使用词向量构建情感分类

在前面的章节中,我们学习了如何使用多个模型生成词向量。在本节中,我们将学习如何构建一个情感分类器来处理给定句子。我们将继续使用航空公司情感推文数据集进行此练习。

如何操作…

生成词向量,方法与我们在前面的例子中提取的方式相同(代码文件可以在 GitHub 上的word2vec.ipynb找到):

  1. 导入包并下载数据集:
import re
import nltk
from nltk.corpus import stopwords
import pandas as pd
nltk.download('stopwords')
stop = set(stopwords.words('english'))
data=pd.read_csv('https://www.dropbox.com/s/8yq0edd4q908xqw/airline_sentiment.csv?dl=1')
  1. 对输入文本进行预处理:
def preprocess(text):
 text=text.lower()
 text=re.sub('[⁰-9a-zA-Z]+',' ',text)
 words = text.split()
 words2 = [i for i in words if i not in stop]
 words3=' '.join(words2)
 return(words3)
data['text'] = data['text'].apply(preprocess)
  1. 提取数据集中所有句子的列表列表:
t=[]
for i in range(len(data)):
 t.append(data['text'][i].split())
  1. 构建一个 CBOW 模型,其中上下文窗口size5,向量长度为 100:
from gensim.models import Word2Vec
model = Word2Vec(size=100,window=5,min_count=30, sg=0)
  1. 指定词汇表以构建模型,然后进行训练:
model.build_vocab(t)
model.train(t, total_examples=model.corpus_count, epochs=100)
  1. 提取给定推文的平均向量:
import numpy as np
features= []
for i in range(len(t)):
      t2 = t[i]
      z = np.zeros((1,100))
      k=0
      for j in range(len(t2)):
            try:
              z = z+model[t2[j]]
              k= k+1
            except KeyError:
              continue
      features.append(z/k)

我们正在对输入句子中所有单词的词向量取平均值。此外,会有一些不在词汇表中的单词(出现频率较低的单词),如果我们尝试提取它们的词向量,将会导致错误。我们为这个特定场景部署了trycatch错误处理机制。

  1. 对特征进行预处理,将其转换为数组,分割数据集为训练集和测试集,并重塑数据集,以便可以传递给模型:
features = np.array(features)

from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features, data['airline_sentiment'], test_size=0.30,random_state=10)
X_train = X_train.reshape(X_train.shape[0],100)
X_test = X_test.reshape(X_test.shape[0],100)
  1. 编译并构建神经网络,以预测推文的情感:
from keras.layers import Dense, Activation
from keras.models import Sequential
from keras.utils import to_categorical
from keras.layers.embeddings import Embedding
model = Sequential()
model.add(Dense(1000,input_dim = 100,activation='relu'))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()

上面定义的模型摘要如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/f192b1ad-1bfb-48b4-aec9-fe09290cf0ad.png

在上述模型中,我们有一个 1000 维的隐藏层,将 100 个输入的平均词向量值连接到输出层,输出值为 1(1 或 0 表示正面或负面情感):

model.fit(X_train, y_train, batch_size=128, nb_epoch=5, validation_data=(X_test, y_test),verbose = 1)

我们可以看到,在预测推文情感时,我们的模型准确率约为 90%。

  1. 绘制预测结果的混淆矩阵:
pred = model.predict(X_test)
pred2 = np.where(pred>0.5,1,0)
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, pred2)

混淆矩阵的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/nn-keras-cb/img/2c7b07d2-adcd-4ee5-bcaa-b8bfd5201169.png

从上面可以看到,在 2644 个句子中,我们预测它们为正面情感,且它们确实是正面情感。125 个句子被预测为负面情感,但实际为正面。209 个句子被预测为正面情感,但实际上是负面情感,最后,485 个句子被预测为负面情感,且实际为负面情感。

还有更多内容…

虽然我们使用 CBOW 模型和推文中所有词向量的平均值实现了情感分类,但我们本可以采用以下其他方法:

  • 使用 skip-gram 模型。

  • 使用 doc2vec 模型通过文档向量构建模型。

  • 使用基于 fastText 模型的词向量。

  • 使用基于 GloVe 的词向量。

  • 使用预训练模型的词向量值。

虽然这些方法工作原理相似,但上述模型的一个限制是它没有考虑单词的顺序。有更复杂的算法可以解决单词顺序的问题,这将在下一章讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值