Python-智能项目-二-

21 阅读1小时+

Python 智能项目(二)

原文:Intelligent Projects Using Python

协议:CC BY-NC-SA 4.0

六、基于 GANs 的时尚产业风格迁移

风格转移的概念是指将一个产品的风格渲染成另一个产品的过程。想象一下,你的时尚狂朋友买了一个蓝色印花的包,想要一双相似印花的鞋子来搭配它。在 2016 年之前,这可能是不可能的,除非他们是一位时装设计师的朋友,这位设计师必须先设计鞋子,然后才能获准生产。然而,随着生成对抗网络的最新进展,这种设计过程很容易实现。

生成性对抗网络是通过在生成器网络和鉴别器网络之间玩零和游戏来学习的网络。假设一个时装设计师想要设计一个特定结构的手提包,并且正在探索不同的印花。设计师可能会画出包的结构草图,然后将草图图像输入到一个生成性对抗网络中,为手提包设计出几种可能的最终图案。这种风格转移的过程可以让顾客自己绘制产品设计和图案,而不需要设计师的大量投入,从而对时尚行业产生巨大的影响。时装公司也可以通过推荐类似设计和风格的产品来补充顾客已经拥有的产品而受益。

在这个项目中,我们将构建一个智能人工智能系统,该系统将生成与给定手提包风格相似的鞋子,反之亦然。我们之前讨论的普通 GAN 不足以实现这个项目;我们需要的是 GAN 的定制版本,比如 DiscoGAN 和 CycleGAN。

在本章中,我们将涵盖以下主题:

  • 我们将讨论 DiscoGAN 背后的工作原理和数学基础
  • 我们将比较 DiscoGAN 和 CycleGAN,它们在架构和工作原理上非常相似
  • 我们将训练一个 DiscoGAN,学会从一些给定的包的草图中生成包的图像
  • 最后,我们将讨论与培训 DiscoGAN 相关的复杂性

技术要求

读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04上找到

查看以下视频,查看正在运行的代码: bit.ly/2CUZLQb

不安

A DiscoGAN 是一个生成性对抗网络,在给定域 a 中的图像的情况下,生成域 B 中产品的图像。下图所示为 DiscoGAN 网络的体系结构图:

Figure 4.1: Architectural diagram of a DiscoGAN

在域 B 中生成的图像在样式和图案上都类似于域 A 中的图像。无需在训练期间显式配对来自两个域的图像,就可以学习这种关系。鉴于物品配对是一项耗时的任务,这是一项相当强大的功能。在高层次上,它试图以神经网络的形式学习两个生成器函数GABT3】和GBAT7】,使得图像xAT11】在通过生成器GABT15】馈入时,产生在域 b 中看起来逼真的图像xABT19】此外, 当这个图像 x AB 通过另一个发电机网络 G BA 馈入时,它应该产生一个图像xABAT31】,理想情况下应该与原始图像xAT35】相同。 关于生成器函数,以下关系应该成立:******

然而,在实践中,发生器函数 G ABG BA 不可能是彼此的逆函数,因此我们通过选择 L1 或 L2 范数损失来尽可能地最小化重建图像和原始图像之间的损失。L1 赋范损失基本上是每个数据点的绝对误差之和,而 L2 赋范损失代表每个数据点的平方损失之和。我们可以将单个图像的 L2 范数损失表示如下:

仅仅最小化前面的损失是不够的。我们必须确保创建的图像 x B 在域 B 中看起来是真实的。例如,如果我们将域 A 中的衣服映射到域 B 中的鞋子,我们必须确保xBT7】类似于鞋子。域 B 侧的鉴别器 D B 将检测到 x B 为假的,如果图像作为鞋不够逼真,因此也必须考虑与此相关的损失。一般来说,在训练过程中,鉴别器既被馈送生成的图像xAB= GAB(xA)又被馈送域 B 中的原始图像,我们在这里选择用 y B 来表示,这样它就学会了将真实图像与虚假图像进行分类。你可能还记得,在一个 GAN 中,生成器和鉴别器互相玩一个零和极小极大游戏*,以不断变好,直到达到平衡。如果假图像看起来不够逼真,鉴别器会对其进行处罚,这意味着生成器必须学习在给定输入图像xAT37】的情况下生成更好的图像xABT33】。考虑到所有这些,我们可以将我们想要最小化的发生器的损耗公式化为重构损耗和关于鉴别器识别 x AB 为假的损耗。第二个损失将试图使生成器在域 B 中产生逼真的图像。将域 A 中的图像 x A 映射到域 B 中的图像的生成器损失可以表示如下:***

L2 准则下的重建损失可表示如下:

由于我们处理的是图像,我们可以假设 x A 是所有像素的展平向量,以符合 L2 范数术语。如果我们假设 x A 是一个矩阵,那么最好将称为弗罗贝纽斯范数。然而,这些只是数学术语,本质上我们只是取原始图像和重建图像之间像素值差异的平方和。

让我们考虑一下发生器在追求使变换后的图像 x AB 在鉴别器看来是真实的过程中试图最小化的成本。鉴别器会试图将图像标记为假图像,因此生成器 G AB 应该产生 x AB ,使得其作为假图像的对数损失尽可能小。如果域 B 中的鉴别器DBT18】将真实图像标记为1,将虚假图像标记为0,则图像真实的概率由 D B (。),那么发电机就要让 x AB 在鉴频器网络下极有可能,让DB(xB)= DB(GAB(xA)尽量靠近1。日志丢失方面,生成器要尽量减少前面概率的负日志,基本上给我们CD(AB)T42】,如下图:**

结合*(3)**(4)*,我们可以得到将一幅图像从域 A 映射到域 B 的总生成器开销 C_G AB ,如下图:

最大的问题是,我们就此打住吗?由于我们有来自两个域的图像,为了获得更好的映射,我们也可以从域 B 获取图像,并通过生成器 G BA 将其映射到域 A。如果我们在域 B 中取一个图像 x B ,通过生成器GBAT15】将其转换为图像xBA,域 A 的鉴别器由DAT19】给出,那么与这样的转换相关联的代价函数由下式给出:**

如果我们对这两个域中的全部图像进行求和,发电机损耗将由 (5) 和*(6)*的总和给出,如下所示:

现在,让我们建立鉴别器试图最小化的成本函数,以建立零和最小/最大游戏。每个域中的鉴别器将尝试区分真实图像和虚假图像,因此鉴别器GBT3】将尝试最小化成本C _ DBT7】,如下所示:**

类似地,鉴别器DAT3】会尝试最小化成本C _ DAT7】,如下所示:**

结合*(8)**(9)*整体鉴频器成本由 C D 给出,如下:

如果我们将GABT3】、GBAT7】、DAT11】和DBT15】的参数表示为,那么网络的优化参数可以表示如下:**

对代价函数进行随机梯度下降,如 Adam,以达到最优解。请注意,如前所述,生成性对抗网络的解决方案是关于被优化的成本函数的鞍点。

-你好

A CycleGAN 基本上类似于 DiscoGAN,只是做了一个小小的修改。在 CycleGAN 中,我们可以灵活地确定重建损失相对于 GAN 损失或鉴别器损失的权重。此参数有助于根据手头的问题以正确的比例平衡损失,以帮助网络在训练时更快地收敛。CycleGAN 的其余实现与 DiscoGAN 相同。

学习从草图轮廓生成自然手袋

在这一章中,我们将从草图轮廓生成手提包,而不使用使用 DiscoGAN 的显式配对。我们将草图图像表示为属于域 A,而将自然手提包图像表示为属于域 B。将有两个生成器:一个获取域 A 的图像并将它们映射到在域 B 下看起来真实的图像,另一个则相反:一个将手提包图像从域 B 映射到在域 A 下看起来真实的图像。鉴别器将尝试从每个域中的真实图像中识别生成器生成的假图像。生成器和鉴别器会互相进行一个极大极小零和游戏。

为了训练这个网络,我们将需要两套图像,草图,或手袋的轮廓和手袋的自然图像。图片可从以下链接下载:https://people . eecs . Berkeley . edu/~ ting huiz/projects/pix2pix/dataset/edge S2 手袋. tar.gz

在接下来的几节中,我们将介绍在 TensorFlow 中定义 DiscoGAN 网络的过程,然后训练它使用手提包草图生成逼真的手提包图像,这些草图充当图像的边缘。我们将从定义发电机网络的架构开始。

预处理图像

edges2handbags数据集文件夹中的每个图像都包含同一图像中bag的图片和bag edges的图片。为了训练网络,我们需要将它们分离为属于两个域 A 和 B 的图像,我们已经在 DiscoGAN 的体系结构中讨论过这两个域。通过使用以下代码(image_split.py),可以将映像分为域 A 和域 B 映像:

# -*- coding: utf-8 -*-
"""
Created on Fri Apr 13 00:10:12 2018

@author: santanu
"""

import numpy as np
import os
from scipy.misc import imread
from scipy.misc import imsave
import fire
from elapsedtimer import ElapsedTimer
from pathlib import Path
import shutil 
'''
Process the images in Domain A and Domain and resize appropriately
Inputs contain the Domain A and Domain B image in the same image
This program will break them up and store them in their respecective folder

'''

def process_data(path,_dir_):
    os.chdir(path)
    try: 
        os.makedirs('trainA')
    except:
        print(f'Folder trainA already present, cleaning up and recreating empty folder trainA')
        try:
            os.rmdir('trainA')
        except:
            shutil.rmtree('trainA')

        os.makedirs('trainA')

    try: 
        os.makedirs('trainB')
    except:
        print(f'Folder trainA already present, cleaning up and recreating empty folder trainB')
        try:
            os.rmdir('trainB')
        except:
            shutil.rmtree('trainB')
        os.makedirs('trainB')
    path = Path(path) 
    files = os.listdir(path /_dir_)
    print('Images to process:', len(files))
    i = 0
    for f in files:
        i+=1 
        img = imread(path / _dir_ / str(f))
        w,h,d = img.shape
        h_ = int(h/2)
        img_A = img[:,:h_]
        img_B = img[:,h_:]
        imsave(f'{path}/trainA/{str(f)}_A.jpg',img_A)
        imsave(f'{path}/trainB/{str(f)}_B.jpg',img_A)
        if ((i % 10000) == 0 & (i >= 10000)):
            print(f'the number of input images processed : {i}')
    files_A = os.listdir(path / 'trainA')
    files_B = os.listdir(path / 'trainB')
    print(f'No of images written to {path}/trainA is {len(files_A)}')
    print(f'No of images written to {path}/trainA is {len(files_B)}')

with ElapsedTimer('process Domain A and Domain B Images'):
    fire.Fire(process_data)

image_split.py代码可以如下调用:

python image_split.py --path /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ --_dir_ train

输出日志如下:

Folder trainA already present, cleaning up and recreating empty folder trainA
Folder trainA already present, cleaning up and recreating empty folder trainB
Images to process: 138569 the number of input images processed : 10000
the number of input images processed : 20000
the number of input images processed : 30000

.....

迪厅的发电机

DiscoGAN 的生成器是前馈卷积神经网络,其中输入和输出是图像。在网络的第一部分,图像在空间维度上按比例缩小,而输出要素图的数量随着图层的进展而增加。在网络的第二部分,图像沿空间维度按比例放大,而输出要素图的数量逐层减少。在最终输出图层中,将生成与输入具有相同空间维度的图像。如果将图像xAT3】转换为xABT7】从域 A 转换为域 B 的生成器用GABT15】表示,那么我们就有了**

这里展示的是build_generator函数,我们可以用它来构建 DiscoGAN 网络的生成器:

def build_generator(self,image,reuse=False,name='generator'):
    with tf.variable_scope(name):
        if reuse:
            tf.get_variable_scope().reuse_variables()
        else:
            assert tf.get_variable_scope().reuse is False

            """U-Net generator"""
        def lrelu(x, alpha,name='lrelu'):
            with tf.variable_scope(name):
                return tf.nn.relu(x) - alpha * tf.nn.relu(-x)

    """Layers used during downsampling"""
        def common_conv2d(layer_input,filters,f_size=4,
                          stride=2,padding='SAME',norm=True,
                          name='common_conv2d'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()

                else:
                    assert tf.get_variable_scope().reuse is False

                d = 
               tf.contrib.layers.conv2d(layer_input,filters,
                                        kernel_size=f_size,
                                        stride=stride,padding=padding)
                if norm:
                    d = tf.contrib.layers.batch_norm(d)

                d = lrelu(d,alpha=0.2)
                return d

         """Layers used during upsampling"""

       def common_deconv2d(layer_input,filters,f_size=4,
                           stride=2,padding='SAME',dropout_rate=0,
                           name='common_deconv2d'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()

                else:
                    assert tf.get_variable_scope().reuse is False

                u = 
                tf.contrib.layers.conv2d_transpose(layer_input,
                                                   filters,f_size,
                                                   stride=stride,
                                                   padding=padding)
                if dropout_rate:
                    u = tf.contrib.layers.dropout(u,keep_prob=dropout_rate)

                u = tf.contrib.layers.batch_norm(u)
                u = tf.nn.relu(u)
                return u 

        # Downsampling
        #  64x64 -> 32x32
        dwn1 = common_conv2d(image,self.gf,stride=2,norm=False,name='dwn1') 
        #  32x32 -> 16x16
       dwn2 = common_conv2d(dwn1,self.gf*2,stride=2,name='dwn2')           
        #  16x16   -> 8x8
       dwn3 = common_conv2d(dwn2,self.gf*4,stride=2,name='dwn3')           
        #  8x8   -> 4x4 
       dwn4 = common_conv2d(dwn3,self.gf*8,stride=2,name='dwn4')            
        #  4x4   -> 1x1 
       dwn5 = common_conv2d(dwn4,100,stride=1,padding='valid',name='dwn5') 

        # Upsampling
        #  4x4    -> 4x4
        up1 = 
       common_deconv2d(dwn5,self.gf*8,stride=1,
                       padding='valid',name='up1')      
        #  4x4    -> 8x8
        up2 = common_deconv2d(up1,self.gf*4,name='up2')                  
        #  8x8    -> 16x16
        up3 = common_deconv2d(up2,self.gf*2,name='up3')                  
        #  16x16    -> 32x32 
        up4 = common_deconv2d(up3,self.gf,name='up4')                    
       out_img = tf.contrib.layers.conv2d_transpose(up4,self.channels,
                                                    kernel_size=4,stride=2,                                                                                                                                      
                                                    padding='SAME',
                                                    activation_fn=tf.nn.tanh) 
       # 32x32 -> 64x64
        return out_img

在生成器函数中,我们定义了一个泄漏的 ReLU 激活函数,并使用了一个泄漏因子0.2。我们还定义了卷积层生成函数common_conv2d,用于对图像进行下采样,以及common_deconv2d,用于将下采样图像上采样到其原始空间维度。

我们使用tf.get_variable_scope().reuse_variables()reuse选项定义生成器函数。当同一生成器函数被多次调用时,重用选项确保我们重用特定生成器使用的相同变量。当我们删除重用选项时,我们为生成器创建了一组新的变量。

例如,我们可以使用生成器函数来创建两个生成器网络,因此在第一次创建这些网络时,我们不会使用reuse选项。如果再次引用该生成器函数,我们使用reuse选项。卷积(下采样)和反卷积(上采样)过程中的激活函数是泄漏 ReLU,在此之前进行批处理归一化,以实现稳定和快速的收敛。

网络中不同图层的输出要素图数量为self.gf 或其倍数。对于我们的 DiscoGAN 网络,我们选择self.gf作为64

发生器中需要注意的一点是输出层的tanh激活功能。这确保了由发生器产生的图像的像素值将在[-1, +1]的范围内。这使得输入图像具有在[-1, +1]范围内的像素强度变得重要,这可以通过对像素强度进行简单的逐元素变换来完成,如下所示:

同样,要将图像转换为可显示的 0-255 像素强度格式,我们需要应用逆变换,如下所示:

不适者的歧视性

DiscoGAN 的辨别者将学会在特定领域区分真实图像和虚假图像。我们将有两个鉴别器:一个用于域 A,一个用于域 b。鉴别器也是可以执行二进制分类的卷积网络。与传统的基于分类的卷积网络不同,鉴别器没有任何完全连接的层。使用步长为 2 的卷积对输入图像进行下采样,直到最后一层,输出为 1 x 1。同样,我们使用泄漏 ReLU 作为激活函数,并使用批处理标准化来实现稳定快速的收敛。以下代码显示了 TensorFlow 中的鉴别器构建函数实现:

def build_discriminator(self,image,reuse=False,name='discriminator'):
    with tf.variable_scope(name):
        if reuse:
            tf.get_variable_scope().reuse_variables()
        else:
            assert tf.get_variable_scope().reuse is False

        def lrelu(x, alpha,name='lrelu'):

            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()
                else:
                    assert tf.get_variable_scope().reuse is False

            return tf.nn.relu(x) - alpha * tf.nn.relu(-x)

                """Discriminator layer"""

        def d_layer(layer_input,filters,f_size=4,stride=2,norm=True,
                    name='d_layer'):
            with tf.variable_scope(name):
                if reuse:
                    tf.get_variable_scope().reuse_variables()
                else:
                    assert tf.get_variable_scope().reuse is False

                d = 
                tf.contrib.layers.conv2d(layer_input,
                                         filters,kernel_size=f_size,
                                         stride=2, padding='SAME')
                if norm:
                    d = tf.contrib.layers.batch_norm(d)

                d = lrelu(d,alpha=0.2)
                return d

        #64x64 -> 32x32        
        down1 = d_layer(image,self.df, norm=False,name='down1')  
        #32x32 -> 16x16
        down2 = d_layer(down1,self.df*2,name='down2')         
        #16x16 -> 8x8
        down3 = d_layer(down2,self.df*4,name='down3')         
        #8x8 -> 4x4
        down4 = d_layer(down3,self.df*8,name='down4')        
        #4x4 -> 1x1
        down5  = 
       tf.contrib.layers.conv2d(down4,1,kernel_size=4,stride=1,
                                padding='valid')

        return down5

鉴别器网络的不同层中的输出特征图的数量或者是self.df或者是它的倍数。为了我们的网络,我们把self.df当成了64

构建网络并定义成本函数

在本节中,我们将使用生成器和鉴别器函数构建整个网络,并定义在训练过程中要优化的成本函数。张量流代码如下:

def build_network(self):
    def squared_loss(y_pred,labels):
        return tf.reduce_mean((y_pred - labels)**2)
   def abs_loss(y_pred,labels):
        return tf.reduce_mean(tf.abs(y_pred - labels))  
   def binary_cross_entropy_loss(logits,labels):
        return tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
                                        labels=labels,logits=logits))
     self.images_real = tf.placeholder(tf.float32,[None,self.image_size,self.image_size,self.input_dim + self.output_dim])

    self.image_real_A = self.images_real[:,:,:,:self.input_dim]
    self.image_real_B = 
    self.images_real[:,:,:,self.input_dim:self.input_dim + self.output_dim]
    self.images_fake_B = 
    self.build_generator(self.image_real_A,
                         reuse=False,name='generator_AB')
    self.images_fake_A = 
    self.build_generator(self.images_fake_B,
                         reuse=False,name='generator_BA')
    self.images_fake_A_ = 
    self.build_generator(self.image_real_B,
                         reuse=True,name='generator_BA')
    self.images_fake_B_ = 
    self.build_generator(self.images_fake_A_,
                         reuse=True,name='generator_AB')

    self.D_B_fake = 
    self.build_discriminator(self.images_fake_B ,
                             reuse=False, name="discriminatorB")
    self.D_A_fake = 
    self.build_discriminator(self.images_fake_A_,
                             reuse=False, name="discriminatorA") 

    self.D_B_real = 
    self.build_discriminator(self.image_real_B,
                             reuse=True, name="discriminatorB")
    self.D_A_real = 
    self.build_discriminator(self.image_real_A,
                             reuse=True, name="discriminatorA")
    self.loss_GABA = 
    self.lambda_l2*squared_loss(self.images_fake_A,self.image_real_A) +
    binary_cross_entropy_loss(labels=tf.ones_like(self.D_B_fake),
    logits=self.D_B_fake)
    self.loss_GBAB = 
    self.lambda_l2*squared_loss(self.images_fake_B_,
    self.image_real_B) + 
    binary_cross_entropy_loss(labels=tf.ones_like(self.D_A_fake),
    logits=self.D_A_fake)
    self.generator_loss = self.loss_GABA + self.loss_GBAB

    self.D_B_loss_real =     
    binary_cross_entropy_loss(tf.ones_like(self.D_B_real),self.D_B_real)
    self.D_B_loss_fake = 
    binary_cross_entropy_loss(tf.zeros_like(self.D_B_fake),self.D_B_fake)
    self.D_B_loss = (self.D_B_loss_real + self.D_B_loss_fake) / 2.0

    self.D_A_loss_real = 
    binary_cross_entropy_loss(tf.ones_like(self.D_A_real),self.D_A_real)
    self.D_A_loss_fake = 
    binary_cross_entropy_loss(tf.zeros_like(self.D_A_fake),self.D_A_fake)
    self.D_A_loss = (self.D_A_loss_real + self.D_A_loss_fake) / 2.0

    self.discriminator_loss = self.D_B_loss + self.D_A_loss
    self.loss_GABA_sum = tf.summary.scalar("g_loss_a2b", self.loss_GABA)
    self.loss_GBAB_sum = tf.summary.scalar("g_loss_b2a", self.loss_GBAB)
    self.g_total_loss_sum = tf.summary.scalar("g_loss", self.generator_loss)
    self.g_sum = tf.summary.merge([self.loss_GABA_sum,
                                   self.loss_GBAB_sum,self.g_total_loss_sum])
    self.loss_db_sum = tf.summary.scalar("db_loss", self.D_B_loss)
    self.loss_da_sum = tf.summary.scalar("da_loss", self.D_A_loss)
    self.loss_d_sum = tf.summary.scalar("d_loss",self.discriminator_loss)

    self.db_loss_real_sum = tf.summary.scalar("db_loss_real", self.D_B_loss_real)
    self.db_loss_fake_sum = tf.summary.scalar("db_loss_fake", self.D_B_loss_fake)
    self.da_loss_real_sum = tf.summary.scalar("da_loss_real", self.D_A_loss_real)
    self.da_loss_fake_sum = tf.summary.scalar("da_loss_fake", self.D_A_loss_fake)
    self.d_sum = tf.summary.merge(
            [self.loss_da_sum, self.da_loss_real_sum, self.da_loss_fake_sum,
             self.loss_db_sum, self.db_loss_real_sum, self.db_loss_fake_sum,
             self.loss_d_sum]
        )

    trainable_variables = tf.trainable_variables()
    self.d_variables = 
    [var for var in trainable_variables if 'discriminator' in var.name]
    self.g_variables =
    [var for var in trainable_variables if 'generator' in var.name]

    print ('Variable printing start :'  )
    for var in self.d_variables: 
        print(var.name)

    self.test_image_A = 
    tf.placeholder(tf.float32,[None, self.image_size,
                   self.image_size,self.input_dim], name='test_A')
    self.test_image_B =
    tf.placeholder(tf.float32,[None, self.image_size,
                   self.image_size,self.output_c_dim], name='test_B')
    self.saver = tf.train.Saver()

在构建网络中,我们首先定义了 L2 赋范误差和二元交叉熵误差的代价函数。L2 赋范误差将用作重构损失,而二进制交叉熵将用作鉴别器损失。然后,我们使用生成器函数为两个域中的图像定义占位符,并为每个域中的假图像定义张量流运算。我们还通过传递特定于该域的假图像和真实图像来定义鉴别器输出的 ops。除此之外,我们还为这两个域中的重建图像定义了张量流运算。

一旦定义了 ops,我们使用它们来计算损失函数,考虑图像的重建损失和归因于鉴别器的损失。请注意,我们使用了相同的生成器函数来定义从域 A 到域 B 的生成器,以及从域 B 到域 A 的生成器。我们唯一不同的做法是为两个网络提供不同的名称:generator_ABgenerator_BA。由于变量范围被定义为name,这两个生成器将有不同的以提供的名称为前缀的权重集。

下表显示了我们需要跟踪的不同损失变量。相对于发生器或鉴别器的参数,所有这些损失都需要最小化:

| 不同损失的变量 | 描述 | | self.D_B_loss_real | 鉴别器DBT3】对 B 域真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DB.) | | self.D_B_loss_fake | 鉴别器DBT3】二值交叉熵损失在 B 域伪图像分类中的应用(This loss is to be minimized with respect to the parameters of the discriminator DB.) | | self.D_A_loss_real | 鉴别器DAT3】在对域 A 中的真实图像进行分类时的二值交叉熵损失(This loss is to be minimized with respect to the parameters of the discriminator DA.) | | self.D_A_loss_fake | 鉴别器DAT3】二值交叉熵损失在分类域 A 中的伪图像(This loss is to be minimized with respect to the parameters of the discriminator DA.) | | self.loss_GABA | 通过两个生成器 G ABG BA 将域 A 中的图像映射到域 B 再映射回域 A 的重建损失)加上域 B 中鉴别器标记为真实图像的伪图像*GAB(xA)的二值交叉熵(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) | | self.loss_GBAB | 通过两个生成器 G BAG AB 加上伪图像GBA(xB)*的二值交叉熵将域 B 中的图像映射到 A 再映射回 B 的重构损失被域 A 中的鉴别器标记为真实图像(This loss is to be minimized with respect to the parameters of the generators GAB and GBA.) |

前四个损耗组成鉴别器损耗,相对于鉴别器DAT3】和DBT7】的参数需要最小化。最后两个损耗组成发电机损耗,相对于发电机 G ABG BA 的参数需要最小化。**

损失变量通过tf.summary.scaler 与 TensorBoard 关联,以便在训练过程中监控这些损失,确保损失以期望的方式减少。稍后,我们将在训练过程中看到这些损失痕迹在 TensorBoard 中的样子。

构建培训流程

train_network函数中,我们首先为发生器和鉴别器损失函数定义优化器。我们将 Adam 优化器用于生成器和鉴别器,因为这是随机梯度下降优化器的高级版本,在训练 GANs 时非常有效。Adam 使用梯度的衰减平均值,很像稳定梯度的动量,以及平方梯度的衰减平均值,提供了关于成本函数曲率的信息。与tf.summary定义的不同损耗相关的变量被写入日志文件,因此可以通过 TensorBoard 进行监控。以下是train功能的详细代码:

def train_network(self):

        self.learning_rate = tf.placeholder(tf.float32)
        self.d_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.discriminator_loss,var_list=self.d_variables)
        self.g_optimizer = tf.train.AdamOptimizer(self.learning_rate,beta1=self.beta1,beta2=self.beta2).minimize(self.generator_loss,var_list=self.g_variables) 

        self.init_op = tf.global_variables_initializer()
        self.sess = tf.Session()
        self.sess.run(self.init_op)
        #self.dataset_dir = '/home/santanu/Downloads/DiscoGAN/edges2handbags/train/'
        self.writer = tf.summary.FileWriter("./logs", self.sess.graph)
        count = 1
        start_time = time.time()

        for epoch in range(self.epoch):
            data_A = os.listdir(self.dataset_dir + 'trainA/')
            data_B = os.listdir(self.dataset_dir + 'trainB/')
            data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for file_name in data_A ] 

            data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for file_name in data_B ] 
            np.random.shuffle(data_A)
            np.random.shuffle(data_B)
            batch_ids = min(min(len(data_A), len(data_B)), self.train_size) // self.batch_size
            lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step)

            for id_ in range(0, batch_ids):
                batch_files = list(zip(data_A[id_ * self.batch_size:(id_ + 1) * self.batch_size],
                                      data_B[id_ * self.batch_size:(id_ + 1) * self.batch_size]))
                batch_images = [load_train_data(batch_file, self.load_size, self.fine_size) for batch_file in batch_files]
                batch_images = np.array(batch_images).astype(np.float32)

                    # Update G network and record fake outputs
                fake_A, fake_B, _, summary_str = self.sess.run(
                        [self.images_fake_A_,self.images_fake_B,self.g_optimizer,self.g_sum],
                        feed_dict={self.images_real: batch_images, self.learning_rate:lr})
                self.writer.add_summary(summary_str, count)
                [fake_A,fake_B] = self.pool([fake_A, fake_B])

                    # Update D network
                _, summary_str = self.sess.run(
                        [self.d_optimizer,self.d_sum],
                        feed_dict={self.images_real: batch_images,
                               # self.fake_A_sample: fake_A,
                               # self.fake_B_sample: fake_B,
                                   self.learning_rate: lr})
                self.writer.add_summary(summary_str, count)

                count += 1
                print(("Epoch: [%2d] [%4d/%4d] time: %4.4f" % (
                        epoch, id_, batch_ids, time.time() - start_time)))

                if count % self.print_freq == 1:
                    self.sample_model(self.sample_dir, epoch, id_)

                if count % self.save_freq == 2:
                    self.save_model(self.checkpoint_dir, count)

正如我们在代码末尾看到的那样,sample_model函数在训练过程中不时被调用,以检查基于来自另一个域的输入图像在一个域中生成的图像的质量。基于save_freq,模型也会定期保存。

我们在前面的代码中提到的sample_model功能和save_model功能在这里举例说明,以供参考:

def sample_model(self, sample_dir, epoch, id_):
    if not os.path.exists(sample_dir):
        os.makedirs(sample_dir)
    data_A = os.listdir(self.dataset_dir + 'trainA/')
    data_B = os.listdir(self.dataset_dir + 'trainB/') 
    data_A = [ (self.dataset_dir + 'trainA/' + str(file_name)) for 
              file_name in data_A ]
    data_B = [ (self.dataset_dir + 'trainB/' + str(file_name)) for 
              file_name in data_B ]
   np.random.shuffle(data_A)
    np.random.shuffle(data_B)
    batch_files = 
    list(zip(data_A[:self.batch_size], data_B[:self.batch_size]))
    sample_images = 
    [load_train_data(batch_file, is_testing=True) for 
     batch_file in batch_files]
    sample_images = np.array(sample_images).astype(np.float32)

    fake_A, fake_B = self.sess.run(
            [self.images_fake_A_,self.images_fake_B],
            feed_dict={self.images_real: sample_images}
        )
    save_images(fake_A, [self.batch_size, 1],
                    './{}/A_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))
    save_images(fake_B, [self.batch_size, 1],
                    './{}/B_{:02d}_{:04d}.jpg'.format(sample_dir, epoch, id_))

在这个sample_model功能中,从域 A 中随机选择的图像被拍摄并馈送到生成器 G AB 以产生域 B 中的图像。类似地,从域 B 中随机选择的图像被馈送到生成器GBAT8】以产生域 A 中的图像。这些输出图像由两个生成器在不同的时期生成,并且批次被保存在样本文件夹中,以查看生成器在训练过程中是否随着时间的推移而改进以产生更好的图像质量。

使用 TensorFlow 保存功能保存模型的save_model功能如下所示:

def save_model(self,checkpoint_dir,step):
    model_name = "discogan.model"
    model_dir = "%s_%s" % (self.dataset_dir, self.image_size)
    checkpoint_dir = os.path.join(checkpoint_dir, model_dir)

    if not os.path.exists(checkpoint_dir):
        os.makedirs(checkpoint_dir)

    self.t(self.sess,
                    os.path.join(checkpoint_dir, model_name),
                    global_step=step)

GAN 训练的重要参数值

在本节中,我们将讨论用于训练 DiscoGAN 的不同参数值。下表列出了这些内容:

| 参数名称 | 变量名和值集 | 原理 | | 亚当优化器的学习速率 | self.l_r = 2e-4 | 我们应该始终训练一个学习率低的 GAN 网络,以获得更好的稳定性,DiscoGAN 也不例外。 | | 亚当优化器的衰减率 | self.beta1 = 0.5 self.beta2 = 0.99 | 参数beta1定义梯度的衰减平均值,而参数beta2定义梯度平方的衰减平均值。 | | 世 | self.epoch = 200 | 200在这个实现中,对于 DiscoGAN 网络的收敛来说,时代已经足够好了。 | | 批量 | self.batch_size = 64 | 64的批量对于这个实现很有效。然而,由于资源限制,我们可能不得不选择较小的批量。 | | 学习率线性下降的时期 | epoch_step = 10 | 在epoch_step指定的时代数后,学习率线性下降,由以下方案确定:lr = self.l_r if epoch < self.epoch_step else self.l_r*(self.epoch-epoch)/(self.epoch-self.epoch_step) |

调用培训

我们之前展示的所有函数都是在一个DiscoGAN()类中创建的,该类具有在__init__ 函数中声明的重要参数值,如下面的代码块所示。训练网络时唯一需要通过的两个参数是dataset_dir和需要进行训练的epochs的数量

   def __init__(self,dataset_dir,epochs=200):
        # Input shape
        self.dataset_dir = dataset_dir
        self.lambda_l2 = 1.0
        self.image_size = 64
        self.input_dim = 3
        self.output_dim = 3
        self.batch_size = 64 
        self.df = 64
        self.gf = 64
        self.channels = 3
        self.output_c_dim = 3
        self.l_r = 2e-4
        self.beta1 = 0.5
        self.beta2 = 0.99
        self.weight_decay = 0.00001
        self.epoch = epochs
        self.train_size = 10000
        self.epoch_step = 10
        self.load_size = 64
        self.fine_size = 64 
        self.checkpoint_dir = 'checkpoint'
        self.sample_dir = 'sample'
        self.print_freq = 5
        self.save_freq = 10 
        self.pool = ImagePool()

        return None

现在我们已经定义了训练模型所需的所有内容,我们可以通过process_main 函数调用训练,如下所示:

   def process_main(self):
        self.build_network()
        self.train_network()

我们之前为培训演示的端到端代码在脚本cycledGAN_edges_to_bags.py中。我们可以通过运行 python 脚本cycledGAN_edges_to_bags.py来训练模型,如下所示:

python cycledGAN_edges_to_bags.py process_main  --dataset_dir /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/edges2handbags/ epochs 100

脚本cycledGAN_edges_to_bags.py执行的输出日志如下:

Epoch: [ 0] [ 0/ 156] time: 3.0835
Epoch: [ 0] [ 1/ 156] time: 3.9093
Epoch: [ 0] [ 2/ 156] time: 4.3661
Epoch: [ 0] [ 3/ 156] time: 4.8208
Epoch: [ 0] [ 4/ 156] time: 5.2821
Epoch: [ 0] [ 5/ 156] time: 6.2380
Epoch: [ 0] [ 6/ 156] time: 6.6960
Epoch: [ 0] [ 7/ 156] time: 7.1528
Epoch: [ 0] [ 8/ 156] time: 7.6138
Epoch: [ 0] [ 9/ 156] time: 8.0732
Epoch: [ 0] [ 10/ 156] time: 8.8163
Epoch: [ 0] [ 11/ 156] time: 9.6669
Epoch: [ 0] [ 12/ 156] time: 10.1256
Epoch: [ 0] [ 13/ 156] time: 10.5846
Epoch: [ 0] [ 14/ 156] time: 11.0427
Epoch: [ 0] [ 15/ 156] time: 11.9135
Epoch: [ 0] [ 16/ 156] time: 12.3712
Epoch: [ 0] [ 17/ 156] time: 12.8290
Epoch: [ 0] [ 18/ 156] time: 13.2899
Epoch: [ 0] [ 19/ 156] time: 13.7525
.......

监控发生器和鉴别器损耗

损失可以在 TensorBoard 仪表板中监控。可以按如下方式调用 TensorBoard 仪表板:

  1. 从终端运行以下命令:
 tensorboard --logdir=./logs

./logs是存储程序专用的张量板日志的目的地,应在程序中定义如下:

      self.writer = tf.summary.FileWriter("./logs", self.sess.graph)

  1. 一旦执行了步骤 1 中的命令,导航到 TensorBoard 的localhost:6006站点:

下面的截图显示了在项目中实施的 DiscoGAN 培训期间,TensorBoard 中看到的发生器和鉴别器损耗的一些痕迹:

Figure 4.2: Tensorboard Scalars section containing the traces for different losses

以下屏幕截图显示了随着培训的进行,域 A 中鉴别器的损耗成分:

Figure 4.3: Losses of discriminator in domain A

从前面的截图中,我们可以看到不同批次的域 A 中鉴别器的损失。da_lossda_loss_realda_loss_fake 损失的总和*。*da_loss_real稳定下降,因为鉴别器很容易学会在域 A 中识别真实图像,而伪图像的损失稳定在 0.69 左右,这是当二进制分类器输出 1/2 概率的类时可以预期的logloss。发生这种情况是因为生成器也在同时学习使假图像看起来真实,因此使得鉴别器很难容易地将生成器图像分类为假图像。在域 B 的鉴别器的损失概况看起来类似于在域 a 的上一个截图中说明的那些

现在让我们看看发电机的损耗曲线,如下所示:

Figure 4.4: Loss profiles for the generators of the DiscoGAN

g_loss_a2b 是从域 A 到域 B 再到域 B 重建图像的组合生成器损失,以及与使变换后的图像在域 B 中看起来逼真相关联的二进制交叉熵损失。类似地, g_loss_b2a是从域 B 到域 A 再到域 B 重建图像的组合发生器损失,也是与使转换后的图像在域 A 中看起来真实相关的二进制交叉熵损失。正如我们从上一个截图中的张量板视觉效果中看到的,这两个损失分布以及它们的总和g_loss随着批次的进行而稳步减少。

因为训练生成性对抗网络通常是相当棘手的,所以监控它们的损失情况的进展以了解训练是否按预期进行是有意义的。

DiscoGAN 生成的样本图像

当我们到达这一章的结尾时,让我们来看看由 DiscoGAN 在这两个域中生成的一些图像:

Figure 4.5: Handbag images generated given the sketches 

以下截图包含生成的手提袋草图图像(域 A) :

Figure 4.6: Sketches generated given the handbag images 

我们可以看到,DiscoGAN 在将任何一个领域的图像转换为另一个领域的高质量逼真图像方面做得很好。

摘要

我们现在已经到了这一章的结尾。现在,您应该非常熟悉 DiscoGAN 的技术细节和实现复杂性。我们在这一章中探索的概念可以用来实现各种各样的生成性对抗网络,这些网络具有适合当前问题的微妙变化。这个 DiscoGAN 网络的端到端实现位于 GitHub 存储库中,位于https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 04

第五章视频字幕应用中,我们将会看到视频到文本的翻译应用,它们属于人工智能中专家系统的范畴。

五、视频字幕应用

随着视频制作的速度以指数级的速度增长,视频已经成为一种重要的传播媒介。然而,由于缺乏适当的字幕,更多的观众仍然无法观看视频。

视频字幕是翻译视频以生成有意义的内容摘要的艺术,在计算机视觉和机器学习领域是一项具有挑战性的任务。传统的视频字幕方法并没有产生多少成功的故事。然而,随着最近人工智能在深度学习帮助下的发展,视频字幕最近获得了大量的关注。卷积神经网络和递归神经网络的能力使得构建端到端的企业级视频字幕系统成为可能。卷积神经网络对视频中的图像帧进行处理,提取重要特征,再由递归神经网络依次处理,生成有意义的视频摘要。视频字幕系统的一些重要应用如下:

  • 工业工厂安全措施的自动监控
  • 基于通过视频字幕获得的内容对视频进行聚类
  • 银行、医院和其他公共场所更好的安全系统
  • 在网站中搜索视频以获得更好的用户体验

通过深度学习构建智能视频字幕系统主要需要两种类型的数据:视频和文本字幕,它们是训练端到端系统的标签。

作为本章的一部分,我们将讨论以下内容:

  • 讨论美国有线电视新闻网和 LSTM 在视频字幕中的作用
  • 探索序列到序列视频字幕系统的体系结构
  • 利用 s 序列到序列—视频到文本架构构建视频字幕系统

在下一节中,我们将介绍卷积神经网络和 LSTM 版本的递归神经网络如何用于构建端到端视频字幕系统。

技术要求

你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 05上找到

查看以下视频,查看正在运行的代码: bit.ly/2BeXK1c

视频字幕中的 CNNs 和 LSTMs

视频减去音频可以被认为是以顺序方式排列的图像的集合。可以使用在特定图像分类问题上训练的卷积神经网络,例如 ImageNet ,从这些图像中提取重要特征。预训练网络的最后一个完全连接层的激活可以用于从视频的顺序采样图像中导出特征。从视频中顺序采样图像的频率取决于视频中内容的类型,并且可以通过训练进行优化。

下图所示(图 5.1 )是用于从视频中提取特征的预训练神经网络:

Figure 5.1: Video image feature extraction using pre-trained neural networks

从上图中我们可以看到,视频中顺序采样的图像通过预先训练的卷积神经网络,最后一个完全连接层中 4,096 单元的激活作为输出。如果将时间 t 的视频图像表示为xtT8】,最后一个全连接层的输出表示为ft∈R4096,则ft =fw(xt)这里, W 表示卷积神经网络直到最后一个完全连接层的权重。

这些系列的输出特征为 f 1、 f 2、。。。。。 f t。。 f N 可以作为输入馈送到递归神经网络,该神经网络学习基于输入特征生成文本标题,如下图所示(图 5.2 ):

Figure 5.2: LSTM in the processing of the sequential input features from CNN

从上图中我们可以看到,生成的特征 f 1、 f 2、。。。。。 f t。。来自预训练卷积神经网络的 f N 由 LSTM 依次处理,以产生文本输出 o 1、 o 2、。。。。。 o t。。 o N、 是给定视频的文字说明。例如,上图中视频的标题可能是一名戴着黄色头盔的男子正在工作:

o1,o2,。。。。。ot。。。oN = {“A”,“人”“在”“A”“黄色”“头盔”“正在”“工作”}

现在我们已经很好地了解了视频字幕在深度学习框架中的工作原理,接下来让我们在下一节讨论一个更高级的视频字幕网络,称为序列到序列视频字幕。在本章中,我们将使用相同的网络架构来构建视频字幕系统。

一种序列到序列视频字幕系统

序列到序列的体系结构基于一篇名为序列到序列—视频到文本的论文,作者是 Subhashini Venugopalan、Marcus Rohrbach、Jeff Donahue、Raymond Mooney、Trevor Darrell 和 Kate Saenko。论文可以在arxiv.org/pdf/1505.00…找到。

在下图(图 5.3 )中,说明了一个基于前一篇论文的序列到序列视频字幕神经网络架构:

Figure 5.3: Sequence-to-sequence video-captioning network architecture

序列到序列模型像以前一样通过预先训练的卷积神经网络处理视频图像帧,最后一个完全连接的层的输出激活被作为特征馈送到随后的 LSTMs。如果我们在时间步长 t 将预训练卷积神经网络的最后一个完全连接层的输出激活表示为 f t ∈ R 4096 ,那么我们将具有来自视频的 N 图像帧的 N 这样的特征向量。这些 N 特征向量 f1,f2,.。。。。f t 。。。f N 依次馈入 LSTMs,生成文字字幕。

有两个背靠背的 LSTMs,LSTMs 中的序列数是来自视频的图像帧数和字幕词汇表中文本字幕的最大长度之和。如果网络在视频的 N 个图像帧上训练,并且词汇表中的最大文本字幕长度为 M ,则 LSTMs 在 (N+M) 个时间步长上训练。在 N 时间步骤中,第一 LSTM 处理特征向量 f1、f2、.。。。。f t 。。。f N 依次,其产生的隐藏状态馈入第二 LSTM。在这些 N 时间步长中,第二 LSTM 不需要文本输出目标。如果我们将第一个 LSTM 在时间步长 t 处的隐藏状态表示为 h t ,则第一个 N 时间步长对第二个 LSTM 的输入为 h t 。请注意,从 N+1 时间步长到第一个 LSTM 的输入是零填充的,因此输入对隐藏状态htT31】t>N没有影响。请注意,这并不能保证 t > N 的隐藏状态htT37】总是会相同。其实我们可以选择将hTT41】作为hTT45】给第二个 LSTM 进行任意时间步长 t > N.**

(N+1) 时间步开始,第二个 LSTM 需要一个文本输出目标。在任何时间步 t > N 输入的是ht,wt-1,其中htT13】是第一个 LSTM 在时间步twt-1T19】是在时间步 (t-1)的文字说明词

(N+1) 时间步,输入到第二个 LSTM 的单词wNT7】是由<bos>表示的句子的开始。一旦产生句尾符号<eos>,网络被训练停止产生字幕词。总而言之,这两个 LSTMs 的设置方式是,一旦处理完所有视频图像帧特征 ,它们就开始产生文本字幕词。

处理时间步长 t > N 的第二个 LSTM 输入的另一种方法是只馈送*【wT-1而不是【hT,wT-1,并在时间步长 T【hT,c传递第一个 LSTM 的隐藏和单元状态这样一个视频字幕网络的架构可以说明如下(见图 5.4* ):

Figure 5.4: An alternate architecture for sequence to sequence model

预先训练的卷积神经网络通常具有共同的结构,例如VGG16VGG19ResNet,并且在 ImageNet 上预先训练。然而,我们可以基于从我们正在为其构建视频字幕系统的领域中的视频提取的图像来重新训练这些架构。我们还可以选择一个全新的 CNN 架构,并在特定领域的视频图像上对其进行训练。

到目前为止,我们已经涵盖了使用本节中说明的序列到序列架构开发视频字幕系统的所有技术先决条件。请注意,本节中建议的替代架构设计是为了鼓励读者尝试几种设计,看看哪种设计最适合给定的问题和数据集。

从下一部分开始,我们将致力于构建智能视频字幕系统。

视频字幕系统的数据

我们通过在MSVD dataset上训练模型来构建视频字幕系统,T0 是一个来自微软的字幕视频库。所需数据可从以下链接下载:http://www . cs . utexas . edu/users/ml/clamp/video description/YouTube eclipse . tar 视频的文字说明可通过以下链接获得:https://github . com/jazzsaxsifa/video _ to _ sequence/files/387979/video _ corps . CSV . zip

MSVD dataset里有大概1,938的视频。我们将使用这些来训练序列到序列视频字幕系统。还要注意的是,我们将在图 5.3 所示的序列对序列模型上构建模型。然而,建议读者尝试在图 5.4 中展示的架构上训练一个模型,并看看它的运行情况。

处理视频图像以创建 CNN 功能

一旦我们从指定位置下载了数据,下一个任务就是处理视频图像帧,从预先训练的卷积神经网络的最后一个完全连接的层中提取特征。我们使用在 ImageNet 上预先训练的VGG16卷积神经网络。我们将激活从VGG16的最后一个完全连接的层中取出。由于VGG16的最后一个完全连通层有4096个单位,我们的特征向量ftT8】对于每个时间步长 t 是一个4096,维度向量即ft∈R4096

在通过VGG16处理视频中的图像之前,需要从视频中进行采样。我们从视频中取样图像,使得每个视频都有80帧。在处理来自VGG1680图像帧之后,每个视频将具有80特征向量 f1、f2、。。。。。f t 。。。f 80 。这些特征将被馈送到 LSTMs 以生成文本序列。我们在喀拉斯使用预先训练的VGG16模型。我们创建一个VideoCaptioningPreProcessing类,首先通过函数video_to_frames从每个视频中提取80视频帧作为图像,然后这些视频帧由函数extract_feats_pretrained_cnn中预先训练的VGG16卷积神经网络处理。

extract_feats_pretrained_cnn的输出是每个视频帧的维度4096的 CNN 特征。因为我们处理的是每个视频的80帧,所以每个视频都会有80这样的4096维度向量。

video_to_frames功能可以编码如下:

    def video_to_frames(self,video):

        with open(os.devnull, "w") as ffmpeg_log:
            if os.path.exists(self.temp_dest):
                print(" cleanup: " + self.temp_dest + "/")
                shutil.rmtree(self.temp_dest)
            os.makedirs(self.temp_dest)
            video_to_frames_cmd = ["ffmpeg",'-y','-i', video, 
                                       '-vf', "scale=400:300", 
                                       '-qscale:v', "2", 
                                       '{0}/%06d.jpg'.format(self.temp_dest)]
            subprocess.call(video_to_frames_cmd,
                            stdout=ffmpeg_log, stderr=ffmpeg_log)

从前面的代码中,我们可以看到在video_to_frames功能中,ffmpeg工具用于将视频转换为 JPEG 格式的图像帧。为图像帧指定给ffmpeg的尺寸是300 x 400。关于ffmpeg工具的更多信息,请参考以下链接:www.ffmpeg.org/

extract_feats_pretrained_cnnfunction中已经建立了从最后一个完全连接的层中提取特征的预先训练的 CNN 模型。该函数的代码如下:

# Extract the features from the pre-trained CNN 
    def extract_feats_pretrained_cnn(self):

        model = self.model_cnn_load()
        print('Model loaded')

        if not os.path.isdir(self.feat_dir):
            os.mkdir(self.feat_dir)
        #print("save video feats to %s" % (self.dir_feat))
        video_list = glob.glob(os.path.join(self.video_dest, '*.avi'))
        #print video_list 

        for video in tqdm(video_list):

            video_id = video.split("/")[-1].split(".")[0]
            print(f'Processing video {video}')

            #self.dest = 'cnn_feat' + '_' + video_id
            self.video_to_frames(video)

            image_list = 
            sorted(glob.glob(os.path.join(self.temp_dest, '*.jpg')))
            samples = np.round(np.linspace(
                0, len(image_list) - 1,self.frames_step))
            image_list = [image_list[int(sample)] for sample in samples]
            images = 
            np.zeros((len(image_list),self.img_dim,self.img_dim,
                     self.channels))
            for i in range(len(image_list)):
                img = self.load_image(image_list[i])
                images[i] = img
            images = np.array(images)
            fc_feats = model.predict(images,batch_size=self.batch_cnn)
            img_feats = np.array(fc_feats)
            outfile = os.path.join(self.feat_dir, video_id + '.npy')
            np.save(outfile, img_feats)
            # cleanup
            shutil.rmtree(self.temp_dest)

我们首先使用model_cnn_load函数加载预先训练好的 CNN 模型,然后根据指定给ffmpeg.的采样频率,使用video_to_frames函数为每个视频提取几个视频帧作为图像。我们没有处理通过ffmpeg创建的视频中的所有图像帧,而是使用np.linspace函数拍摄了80等间距的图像帧。使用load_image功能将ffmpeg生成的图像调整到224 x 224的空间维度。最后,这些调整大小的图像通过预先训练的 VGG16 卷积神经网络(CNN),并且在输出层之前的最后一个完全连接层的输出被提取作为特征。这些提取的特征向量存储在numpy阵列中,并在下一阶段由 LSTM 网络处理以产生视频字幕。本节中定义的功能model_cnn_load定义如下:

   def model_cnn_load(self):
         model = VGG16(weights = "imagenet", include_top=True,input_shape = 
                                  (self.img_dim,self.img_dim,self.channels))
         out = model.layers[-2].output
         model_final = Model(input=model.input,output=out)
         return model_final

从前面的代码可以看出,我们正在加载一个在 ImageNet 上预先训练的VGG16卷积神经网络,并且我们正在提取第二个最后一层(索引为-2)的输出作为我们的维度特征向量4096

在输入美国有线电视新闻网之前,处理原始ffmpeg图像的图像读取和调整大小功能load_image定义如下:

    def load_image(self,path):
        img = cv2.imread(path)
        img = cv2.resize(img,(self.img_dim,self.img_dim))
        return img 

预处理脚本可以通过调用以下命令来运行:

 python VideoCaptioningPreProcessing.py process_main --video_dest '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/' --feat_dir '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/features/' --temp_dest '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/temp/' --img_dim 224 --channels 3 --batch_size=128 --frames_step 80

该预处理步骤的输出是作为扩展的 numpy 数组对象npy写入的维度409680特征向量。每个视频都有自己的存储在feat_dir中的numpy数组对象。预处理步骤持续约 28 分钟,从日志中我们可以看到如下内容:

Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/jmoT2we_rqo_0_5.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋| 1967/1970 [27:57<00:02, 1.09it/s]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/NKtfKR4GNjU_0_20.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊| 1968/1970 [27:58<00:02, 1.11s/it]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/4cgzdXlJksU_83_90.avi
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉| 1969/1970 [27:59<00:01, 1.08s/it]Processing video /media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/data/0IDJG0q9j_k_1_24.avi
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1970/1970 [28:00<00:00, 1.06s/it]
28.045 min: VideoCaptioningPreProcessing

在下一节中,我们将讨论视频标签字幕的预处理。

处理视频的标记字幕

corpus.csv文件包含文字说明形式的视频描述(见图 5.5 )。下面的截图显示了数据片段。我们可以删除一些[VideoID,Start,End]组合记录,并将其作为测试文件,供以后评估:

Figure 5.5: A snapshot of the format of the captions file

VideoIDStartEnd列组合形成视频名称,格式如下:VideoID_Start_End.avi。基于视频名称,卷积神经网络VGG16的特征被存储为VideoID_Start_End.npy。在下面的代码块中说明了处理视频的文本标题和创建从VGG16到视频图像特征的路径交叉引用的功能:

def get_clean_caption_data(self,text_path,feat_path):
        text_data = pd.read_csv(text_path, sep=',')
        text_data = text_data[text_data['Language'] == 'English']
        text_data['video_path'] =
        text_data.apply(lambda row: 
         row['VideoID']+'_'+str(int(row['Start']))+'_'+str(int(row['End']))+'.npy',    
         axis=1)
        text_data['video_path'] = 
        text_data['video_path'].map(lambda x: os.path.join(feat_path, x))
        text_data = 
        text_data[text_data['video_path'].map(lambda x: os.path.exists(x))]
        text_data = 
        text_data[text_data['Description'].map(lambda x: isinstance(x, str))]

        unique_filenames = sorted(text_data['video_path'].unique())
        data =
        text_data[text_data['video_path'].map(lambda x: x in unique_filenames)]
        return data

在定义的get_data函数中,我们从video_corpus.csv文件中移除所有非英文的标题。完成后,我们通过首先构建视频名称(作为VideoIDStartEnd要素的串联)并为其添加要素目录名称前缀来形成到视频要素的链接。然后,我们删除所有不指向特征目录中任何实际视频特征向量或具有无效非文本描述的视频语料库文件记录。

数据如下图所示(图 5.6 ):

Figure 5.6: Caption data after preprocessing

构建训练和测试数据集

一旦我们训练好模型,我们就想评估模型的运行情况。我们可以根据测试集中的视频内容来验证为测试数据集生成的标题。可以使用以下功能创建列车测试集数据集。我们可以在训练期间创建测试数据集,并在模型训练完成后将其用于评估:

   def train_test_split(self,data,test_frac=0.2):
        indices = np.arange(len(data))
        np.random.shuffle(indices)
        train_indices_rec = int((1 - test_frac)*len(data))
        indices_train = indices[:train_indices_rec]
        indices_test = indices[train_indices_rec:]
        data_train, data_test = 
        data.iloc[indices_train],data.iloc[indices_test]
        data_train.reset_index(inplace=True)
        data_test.reset_index(inplace=True)
        return data_train,data_test

一般保留 20%的数据用于评估是一种公平的做法。

构建模型

在本节中,将说明核心的模型构建练习。我们首先在文本标题的词汇表中为单词定义一个嵌入层,然后是两个 LSTMs。权重self.encode_Wself.encode_b用于从卷积神经网络中降低特征ftT6】的维数。对于第二个 LSTM (LSTM 2),任何时候的另一个输入步骤 t > N 是前一个单词 w t-1 ,以及来自第一个 LSTM (LSTM 1)的输出 h t 。用于w*t-1的单词嵌入被馈送到 LSTM 2,而不是原始的一个热编码向量。对于第一个 N (self.video_lstm_step),LSTM 1 处理来自 CNN 的输入特征 f t ,输出隐藏状态 h t (输出 1) 馈给 LSTM 2。在这个编码阶段,LSTM 2 不接收任何字*wt-1T36】作为输入。**

(N+1) 时间步长,我们进入解码阶段,其中,与来自 LSTM 1 的*htT7】(output 1)*一起,前一个时间步长字嵌入向量 w t-1 被馈送到 LSTM 2。在该阶段,没有输入到 LSTM 1,因为所有的特征 f t 在时间步骤 N 用尽。解码阶段的时间步数由self.caption_lstm_step决定。

现在,如果我们用一个函数 *f2、*来表示 LSTM 2 的活动,那么 f 2 (h t、wt-1)= h2t,其中 h 2t 是 LSTM 2 在时间步长 t 的隐藏状态。通过 softmax 函数将时间 t 处的隐藏状态 h 2t 转换为输出单词上的概率分布,并且具有最高概率的一个被选择作为下一个单词:

=

这些权重 W ho 和 *b、*在以下代码块中定义为self.word_emb_Wself.word_emb_b。更多详细信息,请参考build_model功能。为了便于解释,构建功能被分解为 3 个部分。构建模型有 3 个主要单元

  • 定义阶段:定义变量、字幕词的嵌入层和序列到序列模型的两个 LSTMs。
  • 编码阶段:在这个阶段我们通过 LSTM1 的时间步长传递视频帧图像特征,并将每个时间步长的隐藏状态传递到 LSTM 2 上。该活动一直进行到时间步长 N ,其中 N 是每个视频的采样视频帧图像的数量。
  • 解码阶段:在解码阶段,LSTM 2 开始生成文本字幕。关于时间步长,解码阶段从步长 N +1 开始。从 LSTM 2 的每个时间步长生成的字连同 LSTM 1 的隐藏状态一起作为输入被馈送到下一个状态。

模型变量的定义

视频字幕模型的变量和其他相关定义可定义如下:

 Defining the weights associated with the Network
        with tf.device('/cpu:0'):
            self.word_emb = 
            tf.Variable(tf.random_uniform([self.n_words, self.dim_hidden],
                        -0.1, 0.1), name='word_emb')

        self.lstm1 = 
        tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
        self.lstm2 = 
        tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
        self.encode_W = 
        tf.Variable( tf.random_uniform([self.dim_image,self.dim_hidden],
                    -0.1, 0.1), name='encode_W')
        self.encode_b = 
        tf.Variable( tf.zeros([self.dim_hidden]), name='encode_b')

        self.word_emb_W =
        tf.Variable(tf.random_uniform([self.dim_hidden,self.n_words], 
        -0.1,0.1), name='word_emb_W')
        self.word_emb_b = 
        tf.Variable(tf.zeros([self.n_words]), name='word_emb_b')

        # Placeholders 
        video = 
       tf.placeholder(tf.float32, [self.batch_size, 
       self.video_lstm_step, self.dim_image])
        video_mask = 
        tf.placeholder(tf.float32, [self.batch_size, self.video_lstm_step])

        caption = 
        tf.placeholder(tf.int32, [self.batch_size, self.caption_lstm_step+1])
        caption_mask = 
        tf.placeholder(tf.float32, [self.batch_size, self.caption_lstm_step+1])

        video_flat = tf.reshape(video, [-1, self.dim_image])
        image_emb = tf.nn.xw_plus_b( video_flat, self.encode_W,self.encode_b )
        image_emb = 
        tf.reshape(image_emb, [self.batch_size, self.lstm_steps, self.dim_hidden])

        state1 = tf.zeros([self.batch_size, self.lstm1.state_size])
        state2 = tf.zeros([self.batch_size, self.lstm2.state_size])
        padding = tf.zeros([self.batch_size, self.dim_hidden])

所有相关变量以及占位符都是由前面的代码定义的。

编码阶段

在编码阶段,我们通过 LSTM 1 的时间步长顺序处理每个视频图像帧特征(来自 CNN 最后一层)。视频图像帧的尺寸为4096.在将那些高维视频帧特征向量馈送到 LSTM 1 之前,它们被缩小到较小的尺寸512.

LSTM 1 处理视频帧图像,并且在每个时间步骤将隐藏状态传递给 LSTM 2,并且该过程持续到时间步骤 N ( self.video_lstm_step)。编码器的代码如下:

probs = []
        loss = 0.0

        # Encoding Stage 
        for i in range(0, self.video_lstm_step):
            if i > 0:
                tf.get_variable_scope().reuse_variables()

            with tf.variable_scope("LSTM1"):
                output1, state1 = self.lstm1(image_emb[:,i,:], state1)

            with tf.variable_scope("LSTM2"):
                output2, state2 = self.lstm2(tf.concat([padding, output1],1), state2)

解码阶段

在解码阶段,产生视频字幕的字。LSTM 1 号没有其他输入。然而,LSTM 1 向前滚动,并且产生的隐藏状态被馈送到 LSTM 2 时间步长,如前所述。每一步 LSTM 2 的另一个输入是字幕中前一个单词的嵌入向量。因此,在每一步,LSTM 2 产生一个新的字幕词,其条件是在先前时间步中预测的词以及在那个时间步来自 LSTM 1 的隐藏状态。解码器的代码如下:

# Decoding Stage to generate Captions 
        for i in range(0, self.caption_lstm_step):

            with tf.device("/cpu:0"):
                current_embed = tf.nn.embedding_lookup(self.word_emb, caption[:, i])

            tf.get_variable_scope().reuse_variables()

            with tf.variable_scope("LSTM1"):
                output1, state1 = self.lstm1(padding, state1)

            with tf.variable_scope("LSTM2"):
                output2, state2 = 
                 self.lstm2(tf.concat([current_embed, output1],1), state2)

为每个小批量建立损失

优化的损失是关于在 LSTM 2 的每个时间步长从整个字幕词语料库中预测正确词的分类交叉熵损失。对于批次中的所有数据点,在解码阶段的每个步骤中累积相同的数据。解码阶段与损失累积相关的代码如下:

            labels = tf.expand_dims(caption[:, i+1], 1)
            indices = tf.expand_dims(tf.range(0, self.batch_size, 1), 1)
            concated = tf.concat([indices, labels],1)
            onehot_labels = 
            tf.sparse_to_dense(concated, tf.stack
                              ([self.batch_size,self.n_words]), 1.0, 0.0)

            logit_words = 
            tf.nn.xw_plus_b(output2, self.word_emb_W, self.word_emb_b)
        # Computing the loss 
            cross_entropy =   
            tf.nn.softmax_cross_entropy_with_logits(logits=logit_words,
            labels=onehot_labels)
            cross_entropy = 
            cross_entropy * caption_mask[:,i]
            probs.append(logit_words)

            current_loss = tf.reduce_sum(cross_entropy)/self.batch_size
            loss = loss + current_loss

损耗可以通过任何合理的梯度下降优化器进行优化,例如 Adam、RMSprop 等。我们将选择Adam进行实验,因为它在大多数深度学习优化中表现良好。我们可以使用 Adam 优化器定义训练操作,如下所示:

with tf.variable_scope(tf.get_variable_scope(),reuse=tf.AUTO_REUSE):
    train_op = tf.train.AdamOptimizer(self.learning_rate).minimize(loss) 

为标题创建词汇

在本节中,我们为视频标题创建词汇。我们创建了一些额外的单词,如下所示:

eos => End of Sentence
bos => Beginning of Sentence 
pad => When there is no word to feed,required by the LSTM 2 in the initial N time steps
unk => A substitute for a word that is not included in the vocabulary

LSTM 2,其中一个字是一个输入,将需要这四个额外的符号。对于 (N+1) 时间步长,当我们开始生成字幕时,我们馈入上一个时间步长 w t-1 的单词。对于要生成的第一个单词,没有有效的前一时间步单词,因此我们输入虚拟单词<bos>,表示句子的开始。同样,当我们到达最后一个时间步时, w t-1 是字幕的最后一个字。我们训练模型输出最后一个单词<eos>,表示句子的结尾。当遇到句尾时,LSTM 2 停止发出任何进一步的单词。

用一个例子来说明,就拿天气好这句话来说吧。以下是从时间步长 (N+1) 开始的 LSTM 2 的输入和输出标签:

| 时间步长 | 输入 | 输出 | | N+1 | <bos>hN+1T4】 | 这 | | N+2 | 第、hN+2T3】 | 天气 | | N+3 | 天气,hN+3T3】 | 是 | | N+4 | 是,hN+4T3】 | 美丽的 | | N+5 | 漂亮,hN+5T3】 | <eos> |

创建词汇的create_word_dict功能详细说明如下:

    def create_word_dict(self,sentence_iterator, word_count_threshold=5):

        word_counts = {}
        sent_cnt = 0

        for sent in sentence_iterator:
            sent_cnt += 1
            for w in sent.lower().split(' '):
               word_counts[w] = word_counts.get(w, 0) + 1
        vocab = [w for w in word_counts if word_counts[w] >= word_count_threshold]

        idx2word = {}
        idx2word[0] = '<pad>'
        idx2word[1] = '<bos>'
        idx2word[2] = '<eos>'
        idx2word[3] = '<unk>'

        word2idx = {}
        word2idx['<pad>'] = 0
        word2idx['<bos>'] = 1
        word2idx['<eos>'] = 2
        word2idx['<unk>'] = 3

        for idx, w in enumerate(vocab):
            word2idx[w] = idx+4
            idx2word[idx+4] = w

        word_counts['<pad>'] = sent_cnt
        word_counts['<bos>'] = sent_cnt
        word_counts['<eos>'] = sent_cnt
        word_counts['<unk>'] = sent_cnt

        return word2idx,idx2word

训练模型

在这一节中,我们将所有部分放在一起,构建训练视频字幕模型的功能。

首先,我们创建单词词汇词典,结合来自训练和测试数据集的视频字幕。完成后,我们调用build_model功能来创建视频字幕网络,将两个 LSTMs 结合起来。对于每个有特定开始结束的视频,有多个输出视频字幕。在每一批中,从可用的多个视频字幕中随机选择具有特定开始和结束的视频的输出视频字幕。LSTM 2 的输入文本标题被调整为在时间步长 (N+1) 具有作为<bos>的起始单词,而输出文本标题的结束单词被调整为具有作为<eos>的最终文本标签。每个时间步长上分类交叉熵损失的总和被作为特定视频的总交叉熵损失。在每个时间步骤中,我们计算整个单词词汇表的分类交叉熵损失,可以表示如下:

这里,是在时间步长 t 时实际目标词的一个热编码向量,是来自模型的预测概率向量。

在训练期间捕获每个时期的损失,以了解损失减少的本质。这里需要注意的另一件重要的事情是,我们正在使用张量流的tf.train.saver函数保存训练好的模型,以便我们可以恢复模型来进行推理。

train功能的详细代码如下图,供参考:

     def train(self):
        data = self.get_data(self.train_text_path,self.train_feat_path)
        self.train_data,self.test_data = self.train_test_split(data,test_frac=0.2)
        self.train_data.to_csv(f'{self.path_prj}/train.csv',index=False)
        self.test_data.to_csv(f'{self.path_prj}/test.csv',index=False)

        print(f'Processed train file written to {self.path_prj}/train_corpus.csv')
        print(f'Processed test file written to {self.path_prj}/test_corpus.csv')

        train_captions = self.train_data['Description'].values
        test_captions = self.test_data['Description'].values

        captions_list = list(train_captions) 
        captions = np.asarray(captions_list, dtype=np.object)

        captions = list(map(lambda x: x.replace('.', ''), captions))
        captions = list(map(lambda x: x.replace(',', ''), captions))
        captions = list(map(lambda x: x.replace('"', ''), captions))
        captions = list(map(lambda x: x.replace('\n', ''), captions))
        captions = list(map(lambda x: x.replace('?', ''), captions))
        captions = list(map(lambda x: x.replace('!', ''), captions))
        captions = list(map(lambda x: x.replace('\\', ''), captions))
        captions = list(map(lambda x: x.replace('/', ''), captions))

        self.word2idx,self.idx2word = self.create_word_dict(captions, 
                                      word_count_threshold=0)

        np.save(self.path_prj/ "word2idx",self.word2idx)
        np.save(self.path_prj/ "idx2word" ,self.idx2word)
        self.n_words = len(self.word2idx)

        tf_loss, tf_video,tf_video_mask,tf_caption,tf_caption_mask, tf_probs,train_op= 
        self.build_model()
        sess = tf.InteractiveSession()

        saver = tf.train.Saver(max_to_keep=100, write_version=1)
        tf.global_variables_initializer().run()

        loss_out = open('loss.txt', 'w')
        val_loss = []

        for epoch in range(0,self.epochs):
            val_loss_epoch = []

            index = np.arange(len(self.train_data))

            self.train_data.reset_index()
            np.random.shuffle(index)
            self.train_data = self.train_data.loc[index]

            current_train_data = 
            self.train_data.groupby(['video_path']).first().reset_index()

            for start, end in zip(
                    range(0, len(current_train_data),self.batch_size),
                    range(self.batch_size,len(current_train_data),self.batch_size)):

                start_time = time.time()

                current_batch = current_train_data[start:end]
                current_videos = current_batch['video_path'].values

                current_feats = np.zeros((self.batch_size, 
                                self.video_lstm_step,self.dim_image))
                current_feats_vals = list(map(lambda vid: np.load(vid),current_videos))
                current_feats_vals = np.array(current_feats_vals) 

                current_video_masks = np.zeros((self.batch_size,self.video_lstm_step))

                for ind,feat in enumerate(current_feats_vals):
                    current_feats[ind][:len(current_feats_vals[ind])] = feat
                    current_video_masks[ind][:len(current_feats_vals[ind])] = 1

                current_captions = current_batch['Description'].values
                current_captions = list(map(lambda x: '<bos> ' + x, current_captions))
                current_captions = list(map(lambda x: x.replace('.', ''), 
                                    current_captions))
                current_captions = list(map(lambda x: x.replace(',', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('"', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('\n', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('?', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('!', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('\\', ''), 
                                   current_captions))
                current_captions = list(map(lambda x: x.replace('/', ''), 
                                   current_captions))

                for idx, each_cap in enumerate(current_captions):
                    word = each_cap.lower().split(' ')
                    if len(word) < self.caption_lstm_step:
                        current_captions[idx] = current_captions[idx] + ' <eos>'
                    else:
                        new_word = ''
                        for i in range(self.caption_lstm_step-1):
                            new_word = new_word + word[i] + ' '
                        current_captions[idx] = new_word + '<eos>'

                current_caption_ind = []
                for cap in current_captions:
                    current_word_ind = []
                    for word in cap.lower().split(' '):
                        if word in self.word2idx:
                            current_word_ind.append(self.word2idx[word])
                        else:
                            current_word_ind.append(self.word2idx['<unk>'])
                    current_caption_ind.append(current_word_ind)

                current_caption_matrix = 
                sequence.pad_sequences(current_caption_ind, padding='post', 
                                       maxlen=self.caption_lstm_step)
                current_caption_matrix = 
                np.hstack( [current_caption_matrix, 
                          np.zeros([len(current_caption_matrix), 1] ) ] ).astype(int)
                current_caption_masks =
                np.zeros( (current_caption_matrix.shape[0], 
                           current_caption_matrix.shape[1]) )
                nonzeros = 
                np.array( list(map(lambda x: (x != 0).sum() + 1, 
                         current_caption_matrix ) ))

                for ind, row in enumerate(current_caption_masks):
                    row[:nonzeros[ind]] = 1

                probs_val = sess.run(tf_probs, feed_dict={
                    tf_video:current_feats,
                    tf_caption: current_caption_matrix
                    })

                _, loss_val = sess.run(
                        [train_op, tf_loss],
                        feed_dict={
                            tf_video: current_feats,
                            tf_video_mask : current_video_masks,
                            tf_caption: current_caption_matrix,
                            tf_caption_mask: current_caption_masks
                            })
                val_loss_epoch.append(loss_val)

                print('Batch starting index: ', start, " Epoch: ", epoch, " loss: ", 
                loss_val, ' Elapsed time: ', str((time.time() - start_time)))
                loss_out.write('epoch ' + str(epoch) + ' loss ' + str(loss_val) + '\n')

            # draw loss curve every epoch
            val_loss.append(np.mean(val_loss_epoch))
            plt_save_dir = self.path_prj / "loss_imgs"
            plt_save_img_name = str(epoch) + '.png'
            plt.plot(range(len(val_loss)),val_loss, color='g')
            plt.grid(True)
            plt.savefig(os.path.join(plt_save_dir, plt_save_img_name))

            if np.mod(epoch,9) == 0:
                print ("Epoch ", epoch, " is done. Saving the model ...")
                saver.save(sess, os.path.join(self.path_prj, 'model'), global_step=epoch)

        loss_out.close()

从前面的代码中我们可以看到,我们通过基于batch_size.随机选择一组视频来创建每个批次

对于每个视频,标签都是随机选择的,因为同一个视频已经由多个标记者进行了标记。对于每个选定的标题,我们清理标题文本,并将其中的单词转换为它们的单词索引。字幕的目标移动了 1 个时间步长,因为在每一步中,我们根据字幕中的前一个单词来预测单词。针对指定数量的时期训练模型,并且以指定的时期间隔(此处为9)检查模型。

培训结果

可以使用以下命令训练模型:

python Video_seq2seq.py process_main --path_prj '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/' --caption_file video_corpus.csv --feat_dir features --cnn_feat_dim 4096 --h_dim 512 --batch_size 32 --lstm_steps 80 --video_steps=80 --out_steps 20 --learning_rate 1e-4--epochs=100 

| 参数 | | | Optimizer | Adam | | learning rate | 1e-4 | | Batch size | 32 | | Epochs | 100 | | cnn_feat_dim | 4096 | | lstm_steps | 80 | | out_steps | 20 | | h_dim | 512 |

培训的输出日志如下:

Batch starting index: 1728 Epoch: 99 loss: 17.723186 Elapsed time: 0.21822428703308105
Batch starting index: 1760 Epoch: 99 loss: 19.556421 Elapsed time: 0.2106935977935791
Batch starting index: 1792 Epoch: 99 loss: 21.919321 Elapsed time: 0.2206578254699707
Batch starting index: 1824 Epoch: 99 loss: 15.057275 Elapsed time: 0.21275663375854492
Batch starting index: 1856 Epoch: 99 loss: 19.633915 Elapsed time: 0.21492290496826172
Batch starting index: 1888 Epoch: 99 loss: 13.986136 Elapsed time: 0.21542596817016602
Batch starting index: 1920 Epoch: 99 loss: 14.300303 Elapsed time: 0.21855640411376953
Epoch 99 is done. Saving the model ...
24.343 min: Video Captioning

正如我们所看到的,使用 GeForce Zotac 1070 GPU 在 100 个时代训练该模型需要大约 24 分钟。

每个时期的训练损失减少如下所示(图 5.7 ):

Figure 5.7 Loss profile during training

从上图(图 5.7 )可以看出,在最初的几个历元中,损耗降低幅度较高,然后在历元 80 前后逐渐降低。在下一节中,我们将说明该模型如何为看不见的视频生成字幕。

用看不见的测试视频推断

出于推理的目的,我们构建了一个生成器函数build_generator,复制build_model的逻辑来定义所有模型变量和加载模型并在其上运行推理所需的 TensorFlow 操作:

    def build_generator(self):
        with tf.device('/cpu:0'):
            self.word_emb = 
            tf.Variable(tf.random_uniform([self.n_words, self.dim_hidden],
                        -0.1, 0.1), name='word_emb')

        self.lstm1 =
        tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)
        self.lstm2 = 
        tf.nn.rnn_cell.BasicLSTMCell(self.dim_hidden, state_is_tuple=False)

        self.encode_W = 
        tf.Variable(tf.random_uniform([self.dim_image,self.dim_hidden], 
                    -0.1, 0.1), name='encode_W')
        self.encode_b = 
        tf.Variable(tf.zeros([self.dim_hidden]), name='encode_b')

        self.word_emb_W = 
        tf.Variable(tf.random_uniform([self.dim_hidden,self.n_words],
                     -0.1,0.1), name='word_emb_W')
        self.word_emb_b = 
         tf.Variable(tf.zeros([self.n_words]), name='word_emb_b')
        video = 
        tf.placeholder(tf.float32, [1, self.video_lstm_step, self.dim_image])
        video_mask = 
         tf.placeholder(tf.float32, [1, self.video_lstm_step])

        video_flat = tf.reshape(video, [-1, self.dim_image])
        image_emb = tf.nn.xw_plus_b(video_flat, self.encode_W, self.encode_b)
        image_emb = tf.reshape(image_emb, [1, self.video_lstm_step, self.dim_hidden])

        state1 = tf.zeros([1, self.lstm1.state_size])
        state2 = tf.zeros([1, self.lstm2.state_size])
        padding = tf.zeros([1, self.dim_hidden])

        generated_words = []

        probs = []
        embeds = []

        for i in range(0, self.video_lstm_step):
            if i > 0:
                tf.get_variable_scope().reuse_variables()

            with tf.variable_scope("LSTM1"):
                output1, state1 = self.lstm1(image_emb[:, i, :], state1)

            with tf.variable_scope("LSTM2"):
                output2, state2 = 
                self.lstm2(tf.concat([padding, output1],1), state2)

        for i in range(0, self.caption_lstm_step):
            tf.get_variable_scope().reuse_variables()

            if i == 0:
                with tf.device('/cpu:0'):
                    current_embed = 
                    tf.nn.embedding_lookup(self.word_emb, tf.ones([1], dtype=tf.int64))

            with tf.variable_scope("LSTM1"):
                output1, state1 = self.lstm1(padding, state1)

            with tf.variable_scope("LSTM2"):
                output2, state2 = 
                self.lstm2(tf.concat([current_embed, output1],1), state2)

            logit_words = 
            tf.nn.xw_plus_b( output2, self.word_emb_W, self.word_emb_b)
            max_prob_index = tf.argmax(logit_words, 1)[0]
            generated_words.append(max_prob_index)
            probs.append(logit_words)

            with tf.device("/cpu:0"):
                current_embed =
                tf.nn.embedding_lookup(self.word_emb, max_prob_index)
                current_embed = tf.expand_dims(current_embed, 0)

            embeds.append(current_embed)

        return video, video_mask, generated_words, probs, embeds

推理功能

在推理过程中,我们调用build_generator来定义模型和推理所需的其他张量流操作,然后我们使用tf.train.Saver.restoreutility从训练好的模型中加载保存的权重。一旦模型被加载并准备好为每个测试视频进行推断,我们提取其相应的视频帧图像预处理特征(来自 CNN),并将其传递给模型进行推断:

   def inference(self):
        self.test_data = self.get_test_data(self.test_text_path,self.test_feat_path)
        test_videos = self.test_data['video_path'].unique()

        self.idx2word = 
        pd.Series(np.load(self.path_prj / "idx2word.npy").tolist())

        self.n_words = len(self.idx2word)
        video_tf, video_mask_tf, caption_tf, probs_tf, last_embed_tf =           
        self.build_generator()

        sess = tf.InteractiveSession()

        saver = tf.train.Saver()
        saver.restore(sess,self.model_path)

        f = open(f'{self.path_prj}/video_captioning_results.txt', 'w')
        for idx, video_feat_path in enumerate(test_videos):
            video_feat = np.load(video_feat_path)[None,...]
            if video_feat.shape[1] == self.frame_step:
                video_mask = np.ones((video_feat.shape[0], video_feat.shape[1]))
            else:
                continue

            gen_word_idx = 
            sess.run(caption_tf, feed_dict={video_tf:video_feat, 
                     video_mask_tf:video_mask})
            gen_words = self.idx2word[gen_word_idx]

            punct = np.argmax(np.array(gen_words) == '<eos>') + 1
            gen_words = gen_words[:punct]

            gen_sent = ' '.join(gen_words)
            gen_sent = gen_sent.replace('<bos> ', '')
            gen_sent = gen_sent.replace(' <eos>', '')
            print(f'Video path {video_feat_path} : Generated Caption {gen_sent}')
            print(gen_sent,'\n')
            f.write(video_feat_path + '\n')
            f.write(gen_sent + '\n\n')

可以通过调用以下命令来运行推理:

python Video_seq2seq.py process_main --path_prj '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/' --caption_file '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/test.csv' --feat_dir features --mode inference --model_path '/media/santanu/9eb9b6dc-b380-486e-b4fd-c424a325b976/Video Captioning/model-99'

评估结果

评估结果很有希望。测试集0lh_UWF9ZP4_82_87.avi8MVo7fje_oE_139_144.avi的两个视频的推理结果如下:

在下面的截图中,我们举例说明了对视频video0lh_ UWF9ZP4_82_87.avi的推断结果:

Inference on video 0lh_UWF9ZP4_82_87.avi using the trained model

在下面的截图中,我们说明了对另一个video8MVo7fje_oE_139_144.avi的推断结果:

Inference on a video/8MVo7fje_oE_139_144.avi using the trained model

从前面的截图*、*可以看出,训练好的模型很好的为提供的测试视频想出了一个好的标题。

这个项目的代码可以在 GitHub 位置找到。VideoCaptioningPreProcessing.py模块可用于预处理视频并创建卷积神经网络特征,而Video_seq2seq.py模块可用于训练端到端视频字幕系统并对其进行推理。

摘要

现在,我们激动人心的视频字幕项目已经结束。您应该能够使用 TensorFlow 和 Keras 构建自己的视频字幕系统。您还应该能够使用本章中解释的技术诀窍来开发其他高级模型,包括卷积神经网络和递归神经网络。下一章将使用受限的玻尔兹曼机器构建一个智能推荐系统。期待您的参与!

六、智能推荐系统

随着互联网上大量数字信息的出现,用户高效地获取物品成为一个挑战。推荐系统是信息过滤系统,它处理数字数据过载的问题,根据用户的偏好、兴趣和行为,从以前的活动中推断出项目或信息。

在本章中,我们将涵盖以下主题:

  • 介绍推荐系统
  • 基于潜在因子分解的协同过滤
  • 利用深度学习进行潜在因素协同过滤
  • 使用受限玻尔兹曼机器 ( RBM )构建推荐系统
  • 训练限制性商业惯例的对比分歧
  • 使用 RBMs 的协同过滤
  • 使用 RBMs 实现协同过滤应用程序

技术要求

读者应该具备 Python 3 和人工智能的基本知识,才能完成本章中的项目。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 06上找到

查看以下视频,查看正在运行的代码: bit.ly/2Sgc0R3

什么是推荐系统?

推荐系统在当今世界无处不在。无论是网飞的电影推荐还是亚马逊的产品推荐,推荐系统都产生了巨大的影响。推荐系统可以大致分为基于内容的过滤系统、协同过滤系统和基于潜在因素的过滤推荐系统。基于内容的过滤依赖于基于项目内容的手工编码特征。基于用户如何对现有项目进行评级,创建用户简档,并将用户提供的排名给予这些项目:

Figure 6.1: Content-based filtering illustration

从上图中我们可以看到(图 6.1 ),用户 A 购买了名为深度学习神经网络的书籍。由于人工智能这本书的内容与这两本书相似,基于内容的推荐系统已经向用户 A 推荐了这本书人工智能。正如我们所看到的,在基于内容的过滤中,根据用户的偏好向用户推荐项目。这不涉及其他用户如何评价这本书。

协同过滤尝试识别属于给定用户的相似用户,然后推荐相似用户喜欢、购买或评价较高的用户项目。这一般称为用户-用户协同过滤。相反,找到与给定项目相似的项目,并向同样喜欢、购买或高度评价其他相似项目的用户推荐项目。这个名字叫做物品-物品协同过滤:

Figure 6.2: item-item collaborative filtering illustration

在上图(图 6.2 )中,用户 A用户 B 的购书品味非常相似。用户 A 最近购买了书籍深度学习神经网络。由于用户 B用户 A 非常相似,因此用户-用户协同推荐系统也将这些书籍推荐给用户 B

基于潜在因子分解的推荐系统

基于潜在因子分解的过滤推荐方法试图通过分解评级来发现代表用户和项目简档的潜在特征。与基于内容的过滤特征不同,这些潜在特征是不可解释的,并且可以表示复杂的特征。例如,在电影推荐系统中,潜在特征之一可能代表幽默、悬疑和浪漫的特定比例的线性组合。通常,对于已经评级的项目,用户 i 对项目 j 给出的评级rijT3】可以表示为。其中 u i 是基于潜在因素的用户简档向量, v i 是基于相同潜在因素的项目向量:

Figure 6.3: Latent factor-based filtering illustration

上图所示(图 6.3 )是一种基于潜在因素的推荐方法,其中评分矩阵Rm x nT5】已经分解为用户简档矩阵Um x kT9】和项目简档矩阵Pn x kT13】的乘积,其中 k 是模型的潜在因素数。基于这些简档,我们可以通过计算用户简档和项目简档的内部产品来推荐到目前为止还没有被用户购买的项目。内部产品给出了用户在购买产品时可能给出的暂定评级。**

创建这些用户和项目简档的方法之一是,在根据用户和项目的某种形式的平均值填写缺失值后,对评分矩阵执行奇异值分解 ( 奇异值分解)。根据奇异值分解,评级矩阵 R 可以分解如下:

我们可以将用户档案矩阵取为 US 1/2 ,然后将物品档案矩阵转置为 S 1/2 V T 形成潜在因素模型。您可能会有一个问题,当分级矩阵中缺少与用户未分级的电影相对应的条目时,如何执行 SVD。常见的方法是在执行 SVD 之前,通过用户的平均评分或全球评分平均值来估算缺失的评分。

潜在因素协同过滤的深度学习

您可以利用深度学习方法来导出给定维度的用户和项目简档向量,而不是使用奇异值分解。

对于每个用户 i、可以通过一个嵌入层定义一个用户向量 u i ∈ R k 。同样,对于每个项目 j、可以通过另一个嵌入层定义一个项目向量vj∈Rk。然后,用户 i 对某个项目 j 的评分rijT19】可以表示为uIT27】和vjT31】的点积,如图所示:**

您可以修改神经网络,为用户和项目添加偏见。假设我们想要 k 潜在组件,那么 m 用户的嵌入矩阵 U 的尺寸将是 m x k 。类似地, n 项的嵌入矩阵 V 的尺寸为 n x k

基于深度学习的潜在因素模型部分,我们将使用这种嵌入方法来创建基于100K Movie Lens数据集的推荐系统。数据集可以从https://grouplens.org/datasets/movielens/下载。

我们将使用u1.base作为训练数据集,u1.test作为保持测试数据集。

基于深度学习的潜在因素模型

潜在因素协同过滤的深度学习部分讨论的基于深度学习的潜在因素模型可以按照图 6.4 所示进行设计:

Figure 6.4: Deep learning-based latent factor model on Movie Lens 100 K dataset

user_IDmovie_ID从它们对应的嵌入矩阵中提取用户和电影嵌入向量。在图中, embedding_1 代表用户标识的嵌入层, embedding_2 代表电影标识的嵌入层。用户嵌入向量和电影嵌入向量的点积在点 _1 层执行,以输出评分分数(一到五)。定义模型的代码如下所示:

def model(max_users,max_movies,latent_factors):
    user_ID = Input(shape=(1,))
    movie_ID = Input(shape=(1,))
    x = Embedding(max_users,latent_factors, input_length=1)(user_ID)
    y = Embedding(max_movies,latent_factors, input_length=1)(movie_ID)
    out = dot([x,y],axes=2,normalize=False)
    out= Reshape((1,))(out)
    model = Model(inputs=[user_ID,movie_ID],outputs=out)
    print(model.summary())
    return model

在前面的model功能中,max_usersmax_movies分别确定用户和电影嵌入矩阵的大小。模型的参数只不过是用户和电影嵌入矩阵的组成部分。所以如果我们有 m 用户和 n 电影,并且我们选择了 k 的潜在维度,那么我们就有 m x k + n x k = (m + n)k 参数需要学习。

数据处理功能可以编码如下:

data_dir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
outdir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')

#Function to read data 
def create_data(rating,header_cols):
    data = pd.read_csv(rating,header=None,sep='\t')
    #print(data)
    data.columns = header_cols
    return data

#Movie ID to movie name dict 
def create_movie_dict(movie_file):
    print(movie_file)
    df = pd.read_csv(movie_file,sep='|', encoding='latin-1',header=None)
    movie_dict = {}
    movie_ids = list(df[0].values)
    movie_name = list(df[1].values)
    for k,v in zip(movie_ids,movie_name):
        movie_dict[k] = v 
    return movie_dict

# Function to create training validation and test data
def train_val(df,val_frac=None):
    X,y = df[['userID','movieID']].values,df['rating'].values
    #Offset the ids by 1 for the ids to start from zero
    X = X - 1 
    if val_frac != None:
        X_train, X_test, y_train, y_val = train_test_split(X, y, test_size=val_frac,random_state=0)
        return X_train, X_val, y_train, y_val
    else:
        return X,y

需要注意的一点是user_IDmovie_ID都减去了1,以确保标识从0开始,而不是从1开始,以便嵌入层可以正确引用它们。

调用数据处理和训练的代码如下:

#Data processing and model training 

train_ratings_df = create_data(f'{data_dir}/u1.base',['userID','movieID','rating','timestamp']) 
test_ratings_df = create_data(f'{data_dir}/u1.test',['userID','movieID','rating','timestamp']) 
X_train, X_val,y_train, y_val = train_val(train_ratings_df,val_frac=0.2)
movie_dict = create_movie_dict(f'{data_dir}/u.item')
num_users = len(train_ratings_df['userID'].unique())
num_movies = len(train_ratings_df['movieID'].unique())

print(f'Number of users {num_users}')
print(f'Number of movies {num_movies}')
model = model(num_users,num_movies,40)
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
model.compile(loss='mse',optimizer='adam')
callbacks = [EarlyStopping('val_loss', patience=2), 
             ModelCheckpoint(f'{outdir}/nn_factor_model.h5', save_best_only=True)]
model.fit([X_train[:,0],X_train[:,1]], y_train, nb_epoch=30, validation_data=([X_val[:,0],X_val[:,1]], y_val), verbose=2, callbacks=callbacks)

该模型的建立是为了存储关于验证错误的最佳模型。从训练日志中我们可以看到,该模型收敛于大约0.8872的验证 RMSE,如下所示:

Train on 64000 samples, validate on 16000 samples
Epoch 1/30
 - 4s - loss: 8.8970 - val_loss: 2.0422
Epoch 2/30
 - 3s - loss: 1.3345 - val_loss: 1.0734
Epoch 3/30
 - 3s - loss: 0.9656 - val_loss: 0.9704
Epoch 4/30
 - 3s - loss: 0.8921 - val_loss: 0.9317
Epoch 5/30
 - 3s - loss: 0.8452 - val_loss: 0.9097
Epoch 6/30
 - 3s - loss: 0.8076 - val_loss: 0.8987
Epoch 7/30
 - 3s - loss: 0.7686 - val_loss: 0.8872
Epoch 8/30
 - 3s - loss: 0.7260 - val_loss: 0.8920
Epoch 9/30
 - 3s - loss: 0.6842 - val_loss: 0.8959

我们现在在看不见的测试数据集上评估模型的性能。可以调用以下代码对测试数据集运行推理:

#Evaluate on the test dataset 
model = load_model(f'{outdir}/nn_factor_model.h5')
X_test,y_test = train_val(test_ratings_df,val_frac=None)
pred = model.predict([X_test[:,0],X_test[:,1]])[:,0]
print('Hold out test set RMSE:',(np.mean((pred - y_test)**2)**0.5))
pred = np.round(pred)
test_ratings_df['predictions'] = pred
test_ratings_df['movie_name'] = test_ratings_df['movieID'].apply(lambda x:movie_dict[x])

从日志中我们可以看到,RMSE 的保持测试大约在0.95左右,如下所示:

Hold out test set RMSE: 0.9543926404313371

现在,我们通过调用下面一行代码为测试数据集中 ID 为1的用户评估模型的性能:

#Check evaluation results for the UserID = 1 
test_ratings_df[test_ratings_df['userID'] == 1].sort_values(['rating','predictions'],ascending=False)

从以下结果中我们可以看出(图 6.5 )该模型在预测训练中未看到的电影的收视率方面做得很好:

Figure 6.5: Results of evaluation for UserID 1

与深度学习法潜在因素法相关的代码可以在https://github . com/packt publishing/Intelligent-Projects-use-Python/tree/master/chapter 06找到。

SVD++

一般来说,奇异值分解不会捕捉数据中可能存在的用户和项目偏差。一种被称为 SVD++的方法考虑了潜在因子分解方法中的用户和项目偏差,并且在诸如网飞挑战赛等比赛中非常流行。

进行基于潜在因素的推荐最常见的方式是将用户简档和偏好定义为 u i ∈ R kb i ∈ R ,将项目简档和偏好定义为*vI∈Rkbj*∈11 用户 i 对项目 j 提供的等级定义如下:

是所有收视率的全局均值。

用户简档和项目简档然后通过最小化在预测由用户评级的所有项目的评级时的误差平方和来确定。要优化的平方误差损失可以表示如下:

I ij 是一个指标功能,如果用户 i 有一个评级项目 *j,则该指标功能为 1;*否则为零。

相对于用户和项目简档的参数,成本被最小化。通常,这种优化会导致过度拟合,因此用户的规范和项目配置文件在成本函数中被用作规范,如下所示:

这里λ1T3λ2T7】是正则化常数。通常,使用一种流行的梯度下降技术交替最小二乘 ( ALS )进行优化,该技术通过保持项目参数固定来交替更新用户简档参数,反之亦然。**

surprise包很好的实现了 SVD++。在下一节中,我们将在100K movie lens数据集上用 SVD++训练一个模型,并查看性能指标。

电影镜头 100k 数据集的 SVD++训练模型

使用以下命令可通过conda下载surprise包:

conda install -c conda-forge scikit-surprise 

SVD++对应的算法在surprise中命名为SVDpp。我们可以按如下方式加载所有必需的包:

import numpy as np
from surprise import SVDpp # SVD++ algorithm
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import cross_validate
from surprise.model_selection import train_test_split

可以使用surprise中的Dataset.load_builtin实用程序下载100K Movie lens数据集并使其可供代码使用。我们按照8020的比例将数据分为训练集和保持测试集。数据处理代码行如下:

# Load the movie lens 10k data and split the data into train test files(80:20)
data = Dataset.load_builtin('ml-100k')
trainset, testset = train_test_split(data, test_size=.2)

接下来,我们将对数据进行5折叠交叉验证,并查看交叉验证结果。我们为随机梯度下降选择了0.008的学习速率。同样为了防止过度拟合,我们为 L1 和 L2 正则化选择了0.1正则化常数。这些代码行的详细信息如下:

#Perform 5 fold cross validation with all data 
algo = SVDpp(n_factors=40, n_epochs=40, lr_all=0.008, reg_all=0.1)
# Run 5-fold cross-validation and show results summary
cross_validate(algo,data, measures=['RMSE', 'MAE'], cv=5, verbose=True)

交叉验证的结果如下:

Evaluating RMSE, MAE of algorithm SVDpp on 5 split(s). Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std RMSE (testset) 0.9196 0.9051 0.9037 0.9066 0.9151 0.9100 0.0062 MAE (testset) 0.7273 0.7169 0.7115 0.7143 0.7228 0.7186 0.0058 Fit time 374.57 374.58 369.74 385.44 382.36 377.34 5.72 Test time 2.53 2.63 2.74 2.79 2.84 2.71 0.11   

从前面的结果可以看出,模型的5 fold cv RMSE0.91Movie Lens 100K数据集的结果令人印象深刻。

现在我们只在训练数据集trainset上训练模型,然后在测试集上评估模型。相关代码行如下:

model = SVDpp(n_factors=40, n_epochs=10, lr_all=0.008, reg_all=0.1)
model.fit(trainset)

一旦模型已经被训练,我们就在保持测试数据集测试集上评估模型。相关代码行如下:

#validate the model on the testset
pred = model.test(testset)
print("SVD++ results on the Test Set")
accuracy.rmse(pred, verbose=True)   

验证的输出如下:

SVD++ results on the test set
RMSE: 0.9320

从前面的结果中我们可以看到,SVD++模型在 RMSE 为0.93的测试数据集上做得非常好。结果与我们在此之前训练的基于深度学习的模型潜在因素模型(RMSE 的0.95)相当。

用于推荐的受限玻尔兹曼机器部分*、*中,我们将研究用于构建推荐系统的受限玻尔兹曼机器。该方法可以扩展到大规模数据集,因此在协同过滤中得到了广泛的应用。协同过滤领域的大部分数据集是稀疏的,导致非凸优化问题很难解决。与其他因子分解方法(如奇异值分解)相比,径向基函数在数据集上不太容易受到这种稀疏性问题的影响。

推荐使用受限玻尔兹曼机器

受限玻尔兹曼机器是一类属于无监督学习技术的神经网络。受限玻尔兹曼机器 ( RBMs ),因为它们广为人知,试图通过将输入数据投影到隐藏层来学习数据的隐藏结构。

隐藏层激活被期望编码输入信号并重新创建它。受限玻尔兹曼机器通常处理二进制数据:

Figure 6.6: Restricted Boltzmann machines for binary data

为了刷新我们的记忆,前面的图(图 6.6 )是一个有 m 输入或可见单位的 RBM。这是投射到一个隐藏层与 n 单位。给定可见层输入,隐藏单元相互独立,因此可以如下采样,其中表示 sigmoid 函数:

类似地,给定隐藏层激活,可见层单元是独立的,可以如下采样:

RBM 的参数是可见层单元 i 和隐藏层单元 j 之间的广义权重连接 w ij ∈ W m x n ,可见单元 i 处的偏置 c i ∈ b ,以及偏置 c j ∈ c

RBM 的这些参数是通过最大化可见输入数据的可能性来学习的。如果我们用表示组合参数集,并且我们有一组 T 训练输入数据点,那么在 RBM,我们尝试最大化似然函数:

我们通常不使用乘积形式,而是最大化可能性的对数,或者最小化对数可能性的负值,以使函数在数学上更加方便。如果我们将对数似然的负值表示为成本函数 C ,那么:

成本函数通常通过梯度下降最小化。成本函数相对于参数的梯度由预期项组成,表示如下:

术语表示隐藏单元和可见单元的联合概率分布上任何给定量的期望。此外,表示给定可见单位 v 的采样隐藏层输出。在梯度下降的每次迭代中计算联合概率分布的期望值在计算上是困难的。我们求助于一种叫做对比发散的智能方法来计算期望值,这将在下一节讨论。

对比分歧

计算联合概率分布期望值的方法之一是通过吉布斯抽样从联合概率分布中产生大量样本,然后将样本的平均值作为期望值。在吉布斯抽样中,联合概率分布中的每个变量都可以抽样,条件是其余变量。由于可见单元是独立的,给定隐藏单元,反之亦然,您可以将隐藏单元采样为,然后给定隐藏单元的可见单元激活为。然后,我们可以将样本作为从联合概率分布中采样的样本。这样,我们可以生成大量的样本,比如说 M ,取它们的平均值来计算期望的期望值。然而,在梯度下降的每一步中进行如此广泛的采样将使训练过程慢得不可接受,因此,我们不是在梯度下降的每一步中计算许多样本的平均值,而是从联合概率分布中仅生成一个样本,该样本应该代表整个联合概率分布上的期望:

Figure 6.7: Contrastive divergence illustration

从上图(图 6.7 )的图示可以看出,我们从看到的可见输入 v (t) 开始,基于条件概率分布 P(h/v = v (t) ) 对隐藏层激活进行采样。再次,使用条件概率分布P(v/h = h),我们对*v进行采样。基于条件概率分布P(h/v = v)*的隐藏单元下一次采样给我们,然后使用采样可见单元激活给我们。样本vh 整个联合概率分布的代表样本,即。同样用于计算任何包含 vh 的表达式的期望值。这种取样过程被称为对比发散。

从可见输入开始,然后从条件分布 P(v/h)P(v/h) 中依次采样,构成 Gibbs 采样的一个步骤,并从联合分布中给出一个样本 (v/h) 。我们可以选择从条件概率分布中选取连续几次采样迭代后的样本,而不是在吉布斯采样的每一步都选取样本 (v/h) 。如果在 Gibbs 抽样的 k 步之后,选择了代表元素,则对比散度称为 CD-k图 6.7 所示的对比差异可称为 CD-2、,因为我们是在两步吉布斯取样后选择样品的。

使用 RBMs 的协同过滤

限制性玻尔兹曼机器可用于在进行推荐时进行协同过滤。我们将使用这些资源管理系统向用户推荐电影。他们使用不同用户为不同电影提供的分级进行训练。用户不会观看或评价所有的电影,所以这个训练好的模型可以用来向用户推荐看不见的电影。

我们应该有的第一个问题是如何在 RBM 中处理等级,因为等级本质上是有序的,而 RBM 处理的是二进制数据。等级可以被视为二进制数据,代表等级的单位数量等于每个等级的唯一值的数量。例如:在评级系统中,级别从 1 到 5 不等,有五个二进制单位,其中一个对应的级别设置为 1,其余的为零。对 RBM 可见的单位是为用户提供的不同电影的等级。如所讨论的,每个等级将以二进制表示,并且对于每个可见单元,将存在来自所有二进制可见单元的权重连接,对应于电影分级。由于每个用户会对不同的电影集进行分级,因此每个用户的输入将会不同。然而,从电影分级单位到隐藏单位的权重连接对于所有用户来说都是通用的。

下图(图 6.8a**图 6.8b )所示为用户 A用户 B 的 RBM 视图。用户T10】A 和用户 B 对不同的一组电影进行了评分。然而,正如我们所看到的,对于每个用户来说,每个电影中隐藏单元的权重连接是相同的。关于用户 A 的 RBM 评级如下:

Figure 6.8a: RBM for collaborative filtering User A view

关于用户 B 的 RBM 评级如下:

Figure 6.8b: RBM for collaborative filtering User B view

还有一点需要注意的是,如果有 M 部电影,并且如果每部电影都可能有 k 个等级,那么 RBM 的可见单位数量就是 M * k 。同样,如果二进制隐藏单元的数量为 n ,那么*【W】*中的权重连接数等于 M * k * n 。给定可见层输入,每个隐藏单元 h j 可以独立于其他隐藏单元进行采样,如下所示:

这里, m = M * k.

与传统的 RBM 不同,在给定隐藏层激活的情况下,该网络中可见层的二进制单位不能被独立采样。关于电影等级的每一个 k 二进制单位通过 k 路软最大值激活功能联系在一起。如果给定隐藏单元的特定电影的可见单元的输入是,则电影的等级lI 的一般输入计算如下:

这里, (i - 1)k + l 是电影 i 的可视单元的索引,用于排名 l 。类似地,任何特定电影的可视单元都可以根据软最大值函数给出的概率进行采样,如下所示:

在定义隐藏和可见单元的输出时,还有一点很重要,那就是需要概率抽样,而不是默认输出是最大概率的输出。如果给定可见单位的隐藏单位激活概率为 P,,则统一生成范围【0,1】内的随机数 r ,如果 (P > r) ,则隐藏单位激活设置为真。该方案将确保在很长一段时间内激活设置为真的概率 P 。类似地,电影的可见单元是根据给定隐藏单元的概率从跨国发行中采样的。因此,如果对于一部特定的电影,给定隐藏单元激活的情况下,不同评级的概率范围从 1 到 5,分别是 (p 1 、p 2 、p 3 、p 4 、p 5 ) ,那么可以从多项式分布中对评级值进行采样,其概率质量函数如下:

这里:

我们现在具备了创建用于协同过滤的受限玻尔兹曼机器所需的所有技术知识。

使用 RBM 实现协同过滤

在接下来的几个部分中,我们将使用受限的玻尔兹曼机器来实现一个协作过滤系统,其技术原理在前面的部分中有所阐述。我们将使用的数据集是 MovieLens 100K 数据集,其中包含用户为不同电影提供的一到五个等级。数据集可以从grouplens.org/datasets/mo…下载。

这个协同过滤系统的 TensorFlow 实现将在接下来的几节中介绍。

处理输入

每行输入的评分文件记录包含字段userIdmovieIdratingtimestamp。我们处理每个记录以创建一个训练文件,其形式为一个具有与userIdmovieIdrating相关的三个维度的numpy数组。从 1 到 5 的等级是一个热编码的,因此沿着等级维度的长度是 5。我们用 80%的输入记录创建训练数据,而剩下的 20%保留用于测试目的。用户评分的电影数量为1682。训练文件包含943用户,因此训练数据的维度为(943,1682,5)。训练文件中的每个用户都是 RBM 的训练记录,将包含一些用户已评分的电影和一些用户未评分的电影。一些电影分级也被删除,以包括在测试文件中。RBM 将根据可用的分级进行训练,在隐藏单元中捕获输入数据的隐藏结构,然后尝试根据捕获的隐藏结构为每个用户重建所有电影的输入分级。我们还创建了几个字典来存储实际电影标识和它们在训练/测试数据集中的索引的交叉引用。以下是创建培训和测试文件的详细代码:

"""
@author: santanu
"""

import numpy as np
import pandas as pd
import argparse

'''
Ratings file preprocessing script to create training and hold out test datasets
'''

def process_file(infile_path):
    infile = pd.read_csv(infile_path,sep='\t',header=None)
    infile.columns = ['userId','movieId','rating','timestamp']
    users = list(np.unique(infile.userId.values))
    movies = list(np.unique(infile.movieId.values))

    test_data = []
    ratings_matrix = np.zeros([len(users),len(movies),5])
    count = 0 
    total_count = len(infile)
    for i in range(len(infile)):
        rec = infile[i:i+1]
        user_index = int(rec['userId']-1)
        movie_index = int(rec['movieId']-1)
        rating_index = int(rec['rating']-1)
        if np.random.uniform(0,1) < 0.2 :
            test_data.append([user_index,movie_index,int(rec['rating'])])

        else:
            ratings_matrix[user_index,movie_index,rating_index] = 1 

        count +=1 
        if (count % 100000 == 0) & (count>= 100000):
            print('Processed ' + str(count) + ' records out of ' + str(total_count))

    np.save(path + 'train_data',ratings_matrix)
    np.save(path + 'test_data',np.array(test_data))

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--path',help='input data path')
    parser.add_argument('--infile',help='input file name')
    args = parser.parse_args()
    path = args.path
    infile = args.infile
    process_file(path + infile)

训练文件是维度为 m x n x knumpy数组对象,其中 m 为用户总数, n 为电影总数, k 为离散评分值个数(一到五)。为了构建测试集,我们从训练数据集中随机选择 20%的 m x n 评分条目。因此,测试集评级样本的所有 k 评级值在训练数据集中标记为零。在测试集中,我们没有将数据扩展为三维 numpy 数组格式,因此它可以用于训练。相反,我们只是将useridmovieid和指定的评级保存在三列中。请注意,存储在列车和测试文件中的useridmovieid不是原始评级数据文件u.data中的实际标识。它们被1抵消,以适应从0而不是从1开始的 Python 和numpy索引

以下命令可用于调用数据预处理脚本:

python preprocess_ratings.py --path '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/' --infile 'u.data'

为协同过滤建立 RBM 网络

以下函数_network为协同过滤创建所需的 RBM 结构。首先,我们定义输入的权重、偏差和占位符。然后定义sample_hiddensample_visible函数,根据概率分别对隐藏激活和可见激活进行采样。隐藏单元从伯努利分布中采样,概率由 sigmoid 函数提供,而与每部电影相关的可见单元从多项式分布中采样,概率由 softmax 函数提供。不需要创建软最大概率,因为tf.multinomial函数可以直接从逻辑中采样,而不是实际概率。

我们通过定义基于吉布斯抽样的对比发散逻辑来跟进此事。gibbs_step函数实现 Gibbs 抽样的一个步骤,然后利用这个步骤实现顺序的对比发散 k

现在我们已经有了所有必要的函数,我们创建 TensorFlow ops 来对给定可见输入的隐藏状态self.h进行采样,并对给定采样隐藏状态的可见单位self.x进行采样。我们还使用对比散度从 v 和 h 的联合概率分布中抽取(self.x_s,self.h_s)作为代表性样本,即P(v,h/model),用于计算梯度中的不同期望项。

_network函数的最后一步是根据梯度更新 RBM 模型的权重和偏差。正如我们前面看到的,梯度是基于给定可见层输入的隐藏层激活self.h,以及通过对比发散得到的联合概率分布P(v,h/model)的代表性样本,即(self.x_s,self.h_s)

TensorFlow 操作self.x_,指的是给定隐藏层激活self.h的可见层激活,在推断过程中,该操作将有助于得出尚未被每个用户评级的电影的评级:

    def __network(self):

        self.x = tf.placeholder(tf.float32, [None,self.num_movies,self.num_ranks], name="x") 
        self.xr = tf.reshape(self.x, [-1,self.num_movies*self.num_ranks], name="xr") 
        self.W = tf.Variable(tf.random_normal([self.num_movies*self.num_ranks,self.num_hidden], 0.01), name="W") 
        self.b_h = tf.Variable(tf.zeros([1,self.num_hidden], tf.float32, name="b_h")) 
        self.b_v = tf.Variable(tf.zeros([1,self.num_movies*self.num_ranks],tf.float32, name="b_v")) 
        self.k = 2 

## Converts the probability into discrete binary states i.e. 0 and 1 
        def sample_hidden(probs):
            return tf.floor(probs + tf.random_uniform(tf.shape(probs), 0, 1)) 

        def sample_visible(logits):

            logits = tf.reshape(logits,[-1,self.num_ranks])
            sampled_logits = tf.multinomial(logits,1) 
            sampled_logits = tf.one_hot(sampled_logits,depth = 5)
            logits = tf.reshape(logits,[-1,self.num_movies*self.num_ranks])
            print(logits)
            return logits 

## Gibbs sampling step
        def gibbs_step(x_k):
          # x_k = tf.reshape(x_k,[-1,self.num_movies*self.num_ranks]) 
            h_k = sample_hidden(tf.sigmoid(tf.matmul(x_k,self.W) + self.b_h))
            x_k = sample_visible(tf.add(tf.matmul(h_k,tf.transpose(self.W)),self.b_v))
            return x_k
## Run multiple gives Sampling step starting from an initital point 
        def gibbs_sample(k,x_k):

            for i in range(k):
                x_k = gibbs_step(x_k)
# Returns the gibbs sample after k iterations
            return x_k

# Constrastive Divergence algorithm
# 1\. Through Gibbs sampling locate a new visible state x_sample based on the current visible state x 
# 2\. Based on the new x sample a new h as h_sample 
        self.x_s = gibbs_sample(self.k,self.xr)
        self.h_s = sample_hidden(tf.sigmoid(tf.matmul(self.x_s,self.W) + self.b_h))

# Sample hidden states based given visible states
        self.h = sample_hidden(tf.sigmoid(tf.matmul(self.xr,self.W) + self.b_h))
# Sample visible states based given hidden states
        self.x_ = sample_visible(tf.matmul(self.h,tf.transpose(self.W)) + self.b_v)

# The weight updated based on gradient descent 
        #self.size_batch = tf.cast(tf.shape(x)[0], tf.float32)
        self.W_add = tf.multiply(self.learning_rate/self.batch_size,tf.subtract(tf.matmul(tf.transpose(self.xr),self.h),tf.matmul(tf.transpose(self.x_s),self.h_s)))
        self.bv_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.xr,self.x_s), 0, True))
        self.bh_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.h,self.h_s), 0, True))
        self.updt = [self.W.assign_add(self.W_add), self.b_v.assign_add(self.bv_add), self.b_h.assign_add(self.bh_add)]

预处理步骤的数据可以在训练和推理过程中使用如下所示的read_data函数读取:

    def read_data(self):

        if self.mode == 'train':
           self.train_data = np.load(self.train_file)
           self.num_ranks = self.train_data.shape[2]
           self.num_movies = self.train_data.shape[1]
           self.users = self.train_data.shape[0]

        else:
           self.train_df = pd.read_csv(self.train_file)
           self.test_data = np.load(self.test_file)
           self.test_df = pd.DataFrame(self.test_data,columns=['userid','movieid','rating'])

           if self.user_info_file != None:
               self.user_info_df = pd.read_csv(self.user_info_file,sep='|',header=None)
               self.user_info_df.columns=['userid','age','gender','occupation','zipcode']

           if self.movie_info_file != None:
               self.movie_info_df = pd.read_csv(self.movie_info_file,sep='|',encoding='latin-1',header=None)
               self.movie_info_df = self.movie_info_df[[0,1]] 
               self.movie_info_df.columns = ['movieid','movie Title']

此外,在推断过程中,除了测试文件之外,我们还读取了所有电影和分级的预测文件 CSV(在前面代码的推断部分中为self.train_file),而不管它们是否已经被评级。一旦训练好模型,就进行预测。由于我们已经有了训练后预测的评分,我们在推断期间需要做的就是将评分预测信息与测试文件的实际评分信息相结合(更多细节请参见后面的traininference部分)。此外,我们从用户和电影元数据文件中读取信息供以后使用。

训练 RBM

这里说明的_train功能可以用来训练 RBM。在这个函数中,我们首先调用_network函数来构建 RBM 网络结构,然后在激活的 TensorFlow 会话中为指定数量的时期训练模型。使用张量流的saver功能以指定的时间间隔保存模型:

      def _train(self):

        self.__network()
       # TensorFlow graph execution

        with tf.Session() as sess:
            self.saver = tf.train.Saver()
            #saver = tf.train.Saver(write_version=tf.train.SaverDef.V2) 
            # Initialize the variables of the Model
            init = tf.global_variables_initializer()
            sess.run(init)

            total_batches = self.train_data.shape[0]//self.batch_size
            batch_gen = self.next_batch()
            # Start the training 
            for epoch in range(self.epochs):
                if epoch < 150:
                    self.k = 2

                if (epoch > 150) & (epoch < 250):
                    self.k = 3

                if (epoch > 250) & (epoch < 350):
                    self.k = 5

                if (epoch > 350) & (epoch < 500):
                    self.k = 9

                    # Loop over all batches
                for i in range(total_batches):
                    self.X_train = next(batch_gen)
                    # Run the weight update 
                    #batch_xs = (batch_xs > 0)*1
                    _ = sess.run([self.updt],feed_dict={self.x:self.X_train})

                # Display the running step 
                if epoch % self.display_step == 0:
                    print("Epoch:", '%04d' % (epoch+1))
                    print(self.outdir)
                    self.saver.save(sess,os.path.join(self.outdir,'model'), 
                                    global_step=epoch)
           # Do the prediction for all users all items irrespective of whether they 
             have been rated
            self.logits_pred = tf.reshape(self.x_,
           [self.users,self.num_movies,self.num_ranks])
            self.probs = tf.nn.softmax(self.logits_pred,axis=2)
            out = sess.run(self.probs,feed_dict={self.x:self.train_data})
            recs = []
            for i in range(self.users):
                for j in range(self.num_movies):
                    rec = [i,j,np.argmax(out[i,j,:]) +1]
                    recs.append(rec)
            recs = np.array(recs)
            df_pred = pd.DataFrame(recs,columns=
            ['userid','movieid','predicted_rating'])
            df_pred.to_csv(self.outdir + 'pred_all_recs.csv',index=False)

            print("RBM training Completed !")

在前面的函数中需要强调的一件重要事情是使用自定义的next_batch函数创建随机批次。该函数在下面的代码片段中定义,用于定义迭代器batch_gen,该迭代器可以由next方法调用来检索下一个小批量:

def next_batch(self):
    while True:
        ix = np.random.choice(np.arange(self.data.shape[0]),self.batch_size)
        train_X = self.data[ix,:,:] 
        yield train_X

需要注意的一点是,在培训结束时,我们会预测来自所有用户的所有电影的评分,无论它们是否被评分。具有最大概率的评级,将从五个可能的评级中给出(即从一到五)作为最终评级。因为在 Python 中索引是从零开始的,所以我们在使用argmax获得具有最高概率的评级位置后,添加一个来获得实际评级。因此,在培训结束时,我们有一个pred_all_recs.csv文件,其中包含所有培训和测试记录的预测评分。请注意,测试记录嵌入在培训记录中,1 到 5 的所有评分指标都设置为零。

然而,一旦我们从用户观看过的电影的隐藏表示中充分训练了模型,它就学会从用户没有看过的电影中生成评级。

可以通过调用以下命令来训练模型:

python rbm.py main_process --mode train --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/train_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --num_hidden 5 --epochs 1000

仅用5隐藏图层训练1000时代的模型需要大约52秒,从日志中我们可以看到:

RBM training Completed !
52.012 s: process RBM 

Note that the Restricted Boltzmann Machine Network has been trained on an Ubuntu machine with a GeForce Zotac 1070 GPU and 64 GB of RAM. Training time may vary based on the system used to train the network.

使用经过训练的 RBM 进行推理

鉴于我们已经在训练中生成了包含所有预测的文件pred_all_recs.csv,RBM 的推断非常简单。我们需要做的就是根据提供的测试文件从pred_all_recs.csv中提取测试记录。此外,我们通过将1添加到它们当前的值中来求助于原始的useridmovieid。返回原 ID 的目的是能够从u.useru.item文件中添加用户和电影信息。

推理块如下:

    def inference(self):

        self.df_result = self.test_df.merge(self.train_df,on=['userid','movieid'])
        # in order to get the original ids we just need to add 1 
        self.df_result['userid'] = self.df_result['userid'] + 1
        self.df_result['movieid'] = self.df_result['movieid'] + 1
        if self.user_info_file != None:
            self.df_result.merge(self.user_info_df,on=['userid'])
        if self.movie_info_file != None:
            self.df_result.merge(self.movie_info_df,on=['movieid'])
        self.df_result.to_csv(self.outdir + 'test_results.csv',index=False)

        print(f'output written to {self.outdir}test_results.csv')
        test_rmse = (np.mean((self.df_result['rating'].values - 
        self.df_result['predicted_rating'].values)**2))**0.5
        print(f'test RMSE : {test_rmse}')

推论可以如下调用:

 python rbm.py main_process --mode test --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/pred_all_recs.csv' --test_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/test_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --user_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.user' --movie_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.item'

通过仅使用 RBM 的5隐藏单元,我们实现了约1.19的 RMSE 测试,这是值得称赞的,因为我们选择了这样一个简单的网络。推理的输出日志在以下代码块中提供,以供参考:

output written to /home/santanu/ML_DS_Catalog-/Collaborating Filtering/test_results.csv
test RMSE : 1.1999306704742303
458.058 ms: process RBM

我们从test_results.csvuserid 1的推断结果如下(见图 6.9 ):

Figure 6.9: Holdout data validation results on userid 1

从前面截图中的预测(图 6.9 )我们可以看出,RBM 在预测userid 1的电影坚守集方面做得很好。

建议您将最终评分预测作为对每个电影评分预测的多项式概率分布的评分预期,并查看与我们将最终评分作为多项式分布的最高概率的方法相比的情况。协同过滤的 RBM 纸可以在www.cs.toronto.edu/~rsalakhu/p…T2 找到。与受限玻尔兹曼机相关的代码可在https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06/RBM . py找到。

摘要

看完这一章,你现在应该能够使用受限的玻尔兹曼机器构建一个智能推荐系统,并根据你的领域和需求以有趣的方式扩展它。关于本章所述项目的详细实施,请参考https://github . com/PacktPublishing/Intelligent-Projects-use-Python/blob/master/chapter 06上该项目的 GiHub 链接。

在下一章中,我们将讨论如何创建一个移动应用程序来对电影评论进行情感分析。我期待你的参与。

七、电影评论情感分析手机应用

在这个现代时代,将数据发送到云中基于人工智能的应用程序进行推理是司空见惯的事情。例如,用户可以将在手机上拍摄的图像发送到亚马逊 Rekognition API,该服务可以标记图像中存在的各种对象、人物、文本、场景等。使用托管在云中的基于人工智能的应用程序的服务的优势在于它的易用性。移动应用只需要向基于人工智能的服务发出一个 HTTPS 请求,并附上图像,几秒钟内,服务就会提供推理结果。其中一些机器学习即服务提供商如下:

  • 亚马逊索赔案
  • 亚马逊波利
  • 亚马逊 Lex
  • 微软 Azure 认知服务
  • IBM 沃森
  • 谷歌云视觉

下图,*图 7.1,*说明了这种应用程序在云上托管时的体系结构,以及它如何与移动设备交互:

Figure 7.1: Mobile app communicating with an AI model hosted on the cloud

正如您在上图中看到的那样,移动应用程序向托管在云上的模型发送图像定位和分类请求以及图像,模型在对提供的图像运行推理后发回结果。在云上使用这种服务的优势如下:

  • 没有必要收集数据来训练这种模型
  • 将人工智能模型作为一项服务来托管没有任何痛苦
  • 没有必要担心模型的重新训练

所有这些都将由服务提供商负责。然而,在云上使用这种人工智能应用程序也有几个缺点,包括以下几点:

  • 用户不能在本地移动设备上运行推理。所有的推断都需要通过向托管人工智能应用程序的服务器发送网络请求来完成。在没有网络连接的情况下,移动应用程序将无法工作。此外,通过网络从模型中获取预测可能会有一些延迟。
  • 如果它不是一个免费托管的云应用程序,用户通常会为他们运行的推理数量付费。
  • 托管在云上的模型非常通用,用户无法控制用自己的数据训练这些模型。如果数据是唯一的,这种基于通用数据训练的应用程序可能不会提供很好的结果。

部署在云上的人工智能应用程序的上述缺点可以通过在移动设备本身上运行推理来克服,而不是通过互联网将数据发送给人工智能应用程序。

该模型可以在任何具有适当的中央处理器和图形处理器的系统上进行训练,使用针对移动应用程序设计的问题的训练数据。然后可以将训练好的模型转换成优化的文件格式,只需要运行推理所需的权重和操作。然后,优化后的模型可以与移动应用程序集成,整个项目可以作为应用程序加载到移动设备上。训练好的模型的优化文件应该尽可能的小,因为模型会和手机应用程序代码一起存储在手机上。在本章中,我们将使用 TensorFlow mobile 开发一个安卓手机应用程序。

技术要求

您将需要具备 Python 3、TensorFlow 和 Java 的基本知识

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 07上找到

查看以下视频,查看正在运行的代码: bit.ly/2S1sddw

使用 TensorFlow 手机构建安卓手机应用

在这个项目中,我们将使用 TensorFlow 的移动功能来优化作为协议缓冲对象的训练模型。然后,我们将该模型与一个安卓应用程序集成,其逻辑将用 Java 编写。我们需要执行以下步骤:

  1. 在 TensorFlow 中建立一个模型,并用相关数据进行训练。

  2. 一旦模型在验证数据集上表现令人满意,就将 TensorFlow 模型转换为优化的 protobuf 对象(例如,optimized_model.pb)。

  3. 下载安卓工作室及其先决条件。用 Java 开发核心应用逻辑,用 XML 开发接口页面。

  4. 将 TensorFlow 训练的模型 protobuf 对象及其关联的依赖项集成到项目的资产文件夹中。

  5. 构建项目并运行它。

这个安卓应用的实现如下图所示(图 7.2 ):

Figure 7.2: Mobile app deployment architectural diagram

安卓应用中的电影评论评级

我们将构建一个安卓应用程序,该应用程序将电影评论作为输入,并根据对电影评论的情感分析,提供从05的评级作为输出。LSTM 版本的递归神经网络将首先被训练来对电影的情感进行二元分类。训练数据将包括基于文本的电影评论,以及二进制标签011的标签代表有积极情绪的评论,而0表示电影有消极情绪。从模型中,我们将预测情绪为正的概率,然后将概率放大五倍,将其转换为合理的评级。该模型将使用 TensorFlow 构建,然后将训练好的模型转换为优化的冻结 protobuf 对象,以与安卓应用程序逻辑集成。冻结对象的大小将比原始训练模型小得多,并且仅用于推断目的。

我们将在ai.stanford.edu/~amaas/data…使用可用的数据集,在下面的文章中使用,标题为学习用于情感分析的词向量:

@InProceedings{maas-EtAl:2011:ACL-HLT2011,
  author    = {Maas, Andrew L.  and  Daly, Raymond E. and  Pham, Peter T.  and  Huang, Dan  and  Ng, Andrew Y.  and  Potts, Christopher},
  title     = {Learning Word Vectors for Sentiment Analysis},
  booktitle = {Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies},
  month     = {June},
  year      = {2011},
  address   = {Portland, Oregon, USA},
  publisher = {Association for Computational Linguistics},
  pages     = {142--150},
  url       = {http://www.aclweb.org/anthology/P11-1015}
}

预处理电影评论文本

电影评论文本需要被预处理并转换成数字标记,对应于语料库中的不同单词。通过获取第一个50000频繁单词,Keras 标记器将用于将单词转换为数字索引或标记。我们已经限制了电影评论的字数上限。如果电影评论的单词标记少于1000单词标记,评论的开头用零填充。预处理之后,数据被分成训练集、验证集和测试集。Keras Tokenizer对象被保存以供推理时使用。

对电影评论进行预处理的详细代码如下:

# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 22:36:00 2018
@author: santanu
"""
import numpy as np
import pandas as pd
import os
import re
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import pickle
import fire 
from elapsedtimer import ElapsedTimer

# Function to clean the text and convert it into lower case 
def text_clean(text):
    letters = re.sub("[^a-zA-z0-9\s]", " ",text)
    words = letters.lower().split()
    text = " ".join(words)

    return text

def process_train(path):
    review_dest = []
    reviews = []
    train_review_files_pos = os.listdir(path + 'train/pos/')
    review_dest.append(path + 'train/pos/')
    train_review_files_neg = os.listdir(path + 'train/neg/')
    review_dest.append(path + 'train/neg/')
    test_review_files_pos = os.listdir(path + 'test/pos/') 
    review_dest.append(path + 'test/pos/')
    test_review_files_neg = os.listdir(path + 'test/neg/')
    review_dest.append(path + 'test/neg/')

    sentiment_label = [1]*len(train_review_files_pos) + \
                      [0]*len(train_review_files_neg) + \
                      [1]*len(test_review_files_pos) + \
                      [0]*len(test_review_files_neg)

    review_train_test = ['train']*len(train_review_files_pos) + \
                        ['train']*len(train_review_files_neg) + \
                        ['test']*len(test_review_files_pos) + \
                        ['test']*len(test_review_files_neg)

    reviews_count = 0 

    for dest in review_dest:
        files = os.listdir(dest)
        for f in files:
            fl = open(dest + f,'r')
            review = fl.readlines()
            review_clean = text_clean(review[0])
            reviews.append(review_clean)
            reviews_count +=1

    df = pd.DataFrame()
    df['Train_test_ind'] = review_train_test
    df['review'] = reviews
    df['sentiment_label'] = sentiment_label
    df.to_csv(path + 'processed_file.csv',index=False)
    print ('records_processed',reviews_count)
    return df

def process_main(path):
    df = process_train(path)
    # We will tokenize the text for the most common 50000 words.
    max_fatures = 50000
    tokenizer = Tokenizer(num_words=max_fatures, split=' ')
    tokenizer.fit_on_texts(df['review'].values)
    X = tokenizer.texts_to_sequences(df['review'].values) 
    X_ = []
    for x in X:
        x = x[:1000] 
        X_.append(x) 
    X_ = pad_sequences(X_)
    y = df['sentiment_label'].values
    index = list(range(X_.shape[0]))
    np.random.shuffle(index)
    train_record_count = int(len(index)*0.7)
    validation_record_count = int(len(index)*0.15)

    train_indices = index[:train_record_count]
    validation_indices = index[train_record_count:train_record_count + 
                              validation_record_count]
    test_indices = index[train_record_count + validation_record_count:]
    X_train,y_train = X_[train_indices],y[train_indices]
    X_val,y_val = X_[validation_indices],y[validation_indices]
    X_test,y_test = X_[test_indices],y[test_indices]

    np.save(path + 'X_train',X_train)
    np.save(path + 'y_train',y_train)
    np.save(path + 'X_val',X_val)
    np.save(path + 'y_val',y_val)
    np.save(path + 'X_test',X_test)
    np.save(path + 'y_test',y_test)

    # saving the tokenizer oject for inference
    with open(path + 'tokenizer.pickle', 'wb') as handle:
        pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)

if __name__ == '__main__':
    with ElapsedTimer('Process'):
        fire.Fire(process_main)

代码preprocess.py可以如下调用:

python preprocess.py --path /home/santanu/Downloads/Mobile_App/aclImdb/ 

其输出日志如下所示:

Using TensorFlow backend.
records_processed 50000
24.949 s: Process

构建模型

我们将建立一个简单的 LSTM 版本的递归神经网络,在输入层后面有一个嵌入层。嵌入层词向量用维数为 100 的预训练手套向量初始化,层定义为trainable,这样词向量嵌入可以基于训练数据进行自我更新。隐藏状态和单元状态的维度也保持为100。该模型使用二元交叉熵损失进行训练。为了避免过拟合,在损失函数中加入了脊线正则化。亚当优化器 用于训练模型。

下面的代码片段显示了用于在 TensorFlow 中构建模型的函数:

def _build_model(self):

        with tf.variable_scope('inputs'):
            self.X = tf.placeholder(shape=[None, self.sentence_length],dtype=tf.int32,name="X")
            print (self.X)
            self.y = tf.placeholder(shape=[None,1], dtype=tf.float32,name="y")
            self.emd_placeholder = tf.placeholder(tf.float32,shape=[self.n_words,self.embedding_dim]) 

        with tf.variable_scope('embedding'):
            # create embedding variable
            self.emb_W =tf.get_variable('word_embeddings',[self.n_words, self.embedding_dim],initializer=tf.random_uniform_initializer(-1, 1, 0),trainable=True,dtype=tf.float32)
            self.assign_ops = tf.assign(self.emb_W,self.emd_placeholder)

            # do embedding lookup
            self.embedding_input = tf.nn.embedding_lookup(self.emb_W,self.X,"embedding_input") 
            print( self.embedding_input )
            self.embedding_input = tf.unstack(self.embedding_input,self.sentence_length,1) 
            #rint( self.embedding_input)

        # define the LSTM cell
        with tf.variable_scope('LSTM_cell'):
            self.cell = tf.nn.rnn_cell.BasicLSTMCell(self.hidden_states)

        # define the LSTM operation
        with tf.variable_scope('ops'):
            self.output, self.state = tf.nn.static_rnn(self.cell,self.embedding_input,dtype=tf.float32)

        with tf.variable_scope('classifier'):
            self.w = tf.get_variable(name="W", shape=[self.hidden_states,1],dtype=tf.float32)
            self.b = tf.get_variable(name="b", shape=[1], dtype=tf.float32)
        self.l2_loss = tf.nn.l2_loss(self.w,name="l2_loss")
        self.scores = tf.nn.xw_plus_b(self.output[-1],self.w,self.b,name="logits")
        self.prediction_probability = tf.nn.sigmoid(self.scores,name='positive_sentiment_probability')
        print (self.prediction_probability)
        self.predictions = tf.round(self.prediction_probability,name='final_prediction')

        self.losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.scores,labels=self.y)
        self.loss = tf.reduce_mean(self.losses) + self.lambda1*self.l2_loss
        tf.summary.scalar('loss', self.loss)

        self.optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.losses)

        self.correct_predictions = tf.equal(self.predictions,tf.round(self.y))
        print (self.correct_predictions)

        self.accuracy = tf.reduce_mean(tf.cast(self.correct_predictions, "float"),         name="accuracy")
        tf.summary.scalar('accuracy', self.accuracy)

训练模型

在本节中,我们将说明训练模型的 TensorFlow 代码。模型被训练为适度的10 epochs,以避免过度拟合。优化器使用的学习速率是0.001,而训练批次大小和验证批次大小分别设置在25050。需要注意的是,我们正在使用tf.train.write_graph功能将模型图定义保存在model.pbtxt文件中。此外,一旦模型被训练,我们将使用tf.train.Saver 功能将模型权重保存在检查点文件model_ckpt中。将使用model.pbtxtmodel_ckpt文件以 protobuf 格式创建 TensorFlow 模型的优化版本,该版本可与安卓应用程序集成:

   def _train(self):

        self.num_batches = int(self.X_train.shape[0]//self.batch_size)
        self._build_model()
        self.saver = tf.train.Saver()

        with tf.Session() as sess:
            init = tf.global_variables_initializer()
            sess.run(init) 
            sess.run(self.assign_ops,feed_dict={self.emd_placeholder:self.embedding_matrix}) 
            tf.train.write_graph(sess.graph_def, self.path, 'model.pbtxt') 
            print (self.batch_size,self.batch_size_val)
            for epoch in range(self.epochs):
                gen_batch = self.batch_gen(self.X_train,self.y_train,self.batch_size)
                gen_batch_val = self.batch_gen(self.X_val,self.y_val,self.batch_size_val)

                for batch in range(self.num_batches):
                    X_batch,y_batch = next(gen_batch) 
                    X_batch_val,y_batch_val = next(gen_batch_val)
                    sess.run(self.optimizer,feed_dict={self.X:X_batch,self.y:y_batch})
                    c,a = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch,self.y:y_batch})
                    print(" Epoch=",epoch," Batch=",batch," Training Loss: ","{:.9f}".format(c), " Training Accuracy=", "{:.9f}".format(a))
                    c1,a1 = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch_val,self.y:y_batch_val})
                    print(" Epoch=",epoch," Validation Loss: ","{:.9f}".format(c1), " Validation Accuracy=", "{:.9f}".format(a1))
                results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
                print(results)

                if epoch % self.checkpoint_step == 0:
                    self.saver.save(sess, os.path.join(self.path,'model'), global_step=epoch) 

            self.saver.save(sess,self.path + 'model_ckpt')
            results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
            print(results)

批量生成器

train功能中,我们将使用批次生成器,根据通过的批次大小,生成随机批次。生成器函数可以定义如下。请注意,这些功能使用yield代替return。通过调用带有所需参数的函数,将创建一个批处理迭代器对象。通过对迭代器对象应用next方法可以检索批次。我们将在每个纪元开始时调用生成器函数,这样批处理在每个纪元中将是随机的。

下面的代码片段说明了用于生成批处理迭代器对象的函数:

def batch_gen(self,X,y,batch_size):

        index = list(range(X.shape[0]))
        np.random.shuffle(index)
        batches = int(X.shape[0]//batch_size)

        for b in range(batches):
            X_train,y_train = X[index[b*batch_size: (b+1)*batch_size],:],
                                      y[index[b*batch_size: (b+1)*batch_size]]
            yield X_train,y_train

模型培训活动的详细代码在脚本**movie_review_model_train.py**中。同样的训练可以如下调用:

python movie_review_model_train.py process_main --path /home/santanu/Downloads/Mobile_App/ --epochs 10

培训的输出如下:

Using TensorFlow backend.
(35000, 1000) (35000, 1)
(7500, 1000) (7500, 1)
(7500, 1000) (7500, 1)
no of positive class in train: 17497
no of positive class in test: 3735
Tensor("inputs/X:0", shape=(?, 1000), dtype=int32)
Tensor("embedding/embedding_lookup:0", shape=(?, 1000, 100), dtype=float32)
Tensor("positive_sentiment_probability:0", shape=(?, 1), dtype=float32)
.....
25.043 min: Model train 

将模型冻结为 protobuf 格式

保存的训练好的模型,以model.pbtxtmodel_ckpt文件的形式,不能被安卓应用直接使用。我们需要将其转换为优化的 protobuf 格式(一个.pb扩展名文件),可以与安卓应用程序集成。优化后的 protobuf 格式的文件大小将远小于model.pbtxtmodel_ckpt文件的组合大小。

下面的代码(freeze_code.py)将从model.pbtxtmodel_ckpt文件创建优化的原型 buf 模型:

# -*- coding: utf-8 -*-

import sys
import tensorflow as tf
from tensorflow.python.tools import freeze_graph
from tensorflow.python.tools import optimize_for_inference_lib
import fire
from elapsedtimer import ElapsedTimer

#path = '/home/santanu/Downloads/Mobile_App/'
#MODEL_NAME = 'model'

def model_freeze(path,MODEL_NAME='model'):

    # Freeze the graph

    input_graph_path = path + MODEL_NAME+'.pbtxt'
    checkpoint_path = path + 'model_ckpt'
    input_saver_def_path = ""
    input_binary = False
    output_node_names = 'positive_sentiment_probability'
    restore_op_name = "save/restore_all"
    filename_tensor_name = "save/Const:0"
    output_frozen_graph_name = path + 'frozen_'+MODEL_NAME+'.pb'
    output_optimized_graph_name = path + 'optimized_'+MODEL_NAME+'.pb'
    clear_devices = True

    freeze_graph.freeze_graph(input_graph_path, input_saver_def_path,
                            input_binary, checkpoint_path, output_node_names,
                            restore_op_name, filename_tensor_name,
    output_frozen_graph_name, clear_devices, "")

    input_graph_def = tf.GraphDef()

    with tf.gfile.Open(output_frozen_graph_name, "rb") as f:
        data = f.read()
        input_graph_def.ParseFromString(data)

    output_graph_def = optimize_for_inference_lib.optimize_for_inference(
            input_graph_def,
            ["inputs/X" ],#an array of the input node(s)
            ["positive_sentiment_probability"],
            tf.int32.as_datatype_enum # an array of output nodes
            )

    # Save the optimized graph

    f = tf.gfile.FastGFile(output_optimized_graph_name, "w")
    f.write(output_graph_def.SerializeToString())

if __name__ == '__main__':
    with ElapsedTimer('Model Freeze'):
        fire.Fire(model_freeze)

正如您在前面的代码中看到的,我们首先声明输入张量和输出张量,方法是在声明模型时引用它们的定义名称。使用输入和输出张量,以及model.pbtxtmodel_ckpt 文件,利用tensorflow.python.toolsfreeze_graph 功能冻结模型。下一步,冷冻模型进一步优化,使用tensorflow.python.tools中的optimize_for_inference_lib功能创建原型 buf 模型,命名为optimized_model.pb。这个优化后的 protobuf 模型optimized_model.pb将与安卓应用程序集成,用于推理。

可以调用freeze_code.py模型创建 protobuf 格式文件,如下所示:

python freeze_code.py --path /home/santanu/Downloads/Mobile_App/ --MODEL_NAME model

执行上述命令的输出如下:

39.623 s: Model Freeze

创建用于推理的单词到标记词典

在预处理过程中,我们训练了一个 Keras 标记器,用数字单词索引替换单词,这样处理后的电影评论就可以输入到 LSTM 模型中进行训练。我们还保留了词频最高的第一个50000单词,并将复习顺序设置为最大长度1000。虽然训练好的 Keras tokenizer 是为了推断而保存的,但它不能被安卓应用直接使用。我们可以恢复 Keras 标记器,并将第一个50000单词及其对应的单词索引保存在文本文件中。这个文本文件可以在安卓应用中使用,以便建立一个单词到索引词典将评论文本的单词转换成它们的单词索引。需要注意的是,通过参考tokenizer.word_index.可以从加载的 Keras 标记器对象中检索到单词到索引的映射。执行此活动tokenizer_2_txt.py的详细代码如下:

import keras 
import pickle 
import fire
from elapsedtimer import ElapsedTimer

#path = '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle'
#path_out = '/home/santanu/Downloads/Mobile_App/word_ind.txt'
def tokenize(path,path_out):
    with open(path, 'rb') as handle:
        tokenizer = pickle.load(handle)

    dict_ = tokenizer.word_index

    keys = list(dict_.keys())[:50000] 
    values = list(dict_.values())[:50000]
    total_words = len(keys)
    f = open(path_out,'w')
    for i in range(total_words):
        line = str(keys[i]) + ',' + str(values[i]) + '\n'
        f.write(line)

    f.close()

if __name__ == '__main__':
    with ElapsedTimer('Tokeize'):
        fire.Fire(tokenize)

tokenizer_2_txt.py可以如下运行:

python tokenizer_2_txt.py --path '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle' --path_out '/home/santanu/Downloads/Mobile_App/word_ind.txt'

前面命令的输出日志如下:

Using TensorFlow backend.
165.235 ms: Tokenize

App 界面页面设计

使用安卓工作室可以设计一个简单的手机应用界面,相关代码将生成一个 XML 文件。正如你在下面的截图中看到的那样(图 7.3 ),这个应用程序由一个简单的电影评论文本框组成,用户可以在这里输入他们的电影评论,一旦完成,就按下 SUBMIT 按钮。一旦按下 SUBMIT 按钮,评论将被传递给核心 app 逻辑,核心 app 逻辑将处理电影评论文本,并将其传递给 TensorFlow 优化模型进行推理。

作为推断的一部分,将计算情感评分,该评分将显示在移动应用程序上,并显示为星级:

Figure 7.3: Mobile app user interface page format

生成前面提到的移动应用视图所需的 XML 文件如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:layout_editor_absoluteY="81dp">

    <TextView
        android:id="@+id/desc"
        android:layout_width="100dp"
        android:layout_height="26dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="44dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="44dp"
        android:layout_marginTop="36dp"
        android:text="Movie Review"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.254"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="HardcodedText" />

    <EditText
        android:id="@+id/Review"
        android:layout_width="319dp"
        android:layout_height="191dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/desc" />

    <RatingBar
        android:id="@+id/ratingBar"
        android:layout_width="240dp"
        android:layout_height="49dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="52dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="52dp"
        android:layout_marginTop="28dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.238"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/score"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/score"
        android:layout_width="125dp"
        android:layout_height="39dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="96dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="96dp"
        android:layout_marginTop="32dp"
        android:ems="10"
        android:inputType="numberDecimal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.135"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/submit" />

    <Button
        android:id="@+id/submit"
        android:layout_width="wrap_content"
        android:layout_height="35dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="136dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="136dp"
        android:layout_marginTop="24dp"
        android:text="SUBMIT"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/Review" />

</android.support.constraint.ConstraintLayout>

需要注意的一点是,用户和移动应用核心逻辑交互的变量是在 XML 文件中声明的,在android:id选项中。例如,用户提供的电影评论将由Review变量处理,如这里所示的 XML 文件中所定义的:

android:id="@+id/Review"

安卓应用的核心逻辑

安卓应用的核心逻辑是处理用户请求,以及传递的数据,然后将结果发送回用户。作为这个移动应用程序的一部分,核心逻辑将接受用户提供的电影评论,处理原始数据,并将其转换为训练有素的 LSTM 模型可以运行推理的格式。Java 中的OnClickListener功能用于监控用户是否提交了处理请求。所提供的电影评论中的每个词都需要被改变为它们的索引,然后输入可以被直接馈送到优化的训练 LSTM 模型用于推断。除了优化的 protobuf 模型之外,还为此目的存储了单词及其对应索引的字典。使用TensorFlowInferenceInterface 方法对训练好的模型进行推理。优化后的 protobuf 模型和单词字典及其对应的索引存储在assets文件夹中。综上所述,应用核心逻辑执行的任务如下:

  1. 将索引词典中的单词加载到WordToInd HashMap中。在训练模型之前,单词到索引字典是在文本预处理期间从标记器中得到的。

  2. 使用OnClickListener方法监控用户是否提交了电影评论进行推断。

  3. 如果已经提交了电影评论,则该评论将从绑定到 XML 的Review变量中读取。通过删除标点符号等方式清理评论,然后将其拆分为单词。使用HashMap功能WordToInd,每个单词都被转换成相应的索引。这些指数构成了我们的张量流模型的InputVec输入,用于推断。输入向量长度为1000;所以,如果评论少于1000个字,向量在开头用零填充。

  4. 在下一步中,使用TensorFlowInferenceInterface功能创建一个mInferenceInterface对象,将优化后的 protobuf 模型(扩展名为.pb)从assets文件夹加载到内存中。需要定义张量流模型的输入节点和输出节点,这些节点将被引用来进行推理,就像原始模型一样。对于我们的模型,它们被定义为INPUT_NODEOUTPUT_NODE,它们分别包含 TensorFlow 输入占位符的名称和输出情感概率 ops。mInferenceInterface对象的feed方法用于将InputVec值分配给模型的INPUT_NODE,而mInferenceInterfacerun方法执行OUTPUT_NODE。最后,使用mInferenceInterfacefetch方法将推理结果填充到浮动变量value_中。 ***** 情绪得分(情绪为正的概率)通过乘以 5 转换为评级。然后通过ratingBar 变量将此反馈到安卓 app 用户界面。****

****Java 中移动应用的核心逻辑如下:

package com.example.santanu.abc;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.Button;
import android.widget.EditText;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;

public class MainActivity extends AppCompatActivity {

    private TensorFlowInferenceInterface mInferenceInterface;
    private static final String MODEL_FILE = "file:///android_asset/optimized_model.pb";
    private static final String INPUT_NODE = "inputs/X";
    private static final String OUTPUT_NODE = "positive_sentiment_probability";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Create references to the widget variables

        final TextView desc = (TextView) findViewById(R.id.desc);
        final Button submit = (Button) findViewById(R.id.submit);
        final EditText Review = (EditText) findViewById(R.id.Review);
        final TextView score = (TextView) findViewById(R.id.score);
        final RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar);

        //String filePath = "/home/santanu/Downloads/Mobile_App/word2ind.txt";
        final Map<String,Integer> WordToInd = new HashMap<String,Integer>();
        //String line;

        //reader = new BufferedReader(new InputStreamReader(getAssets().open("word2ind.txt")));

        BufferedReader reader = null;
        try {
            reader = new BufferedReader(
                    new InputStreamReader(getAssets().open("word_ind.txt")));

            // do reading, usually loop until end of file reading
            String line;
            while ((line = reader.readLine()) != null)
            {
                String[] parts = line.split("\n")[0].split(",",2);
                if (parts.length >= 2)
                {

                    String key = parts[0];
                    //System.out.println(key);
                    int value = Integer.parseInt(parts[1]);
                    //System.out.println(value);
                    WordToInd.put(key,value);
                } else

                {
                    //System.out.println("ignoring line: " + line);
                }
            }
        } catch (IOException e) {
            //log the exception
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    //log the exception
                }
            }
        }

        //line = reader.readLine();

        // Create Button Submit Listener

        submit.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // Read Values
                String reviewInput = Review.getText().toString().trim();
                System.out.println(reviewInput);

                String[] WordVec = reviewInput.replaceAll("[^a-zA-z0-9 ]", "").toLowerCase().split("\\s+");
                System.out.println(WordVec.length);

                int[] InputVec = new int[1000];
                // Initialize the input
                for (int i = 0; i < 1000; i++) {
                    InputVec[i] = 0;
                }
                // Convert the words by their indices

                int i = 1000 - 1 ;
                for (int k = WordVec.length -1 ; k > -1 ; k--) {
                    try {
                        InputVec[i] = WordToInd.get(WordVec[k]);
                        System.out.println(WordVec[k]);
                        System.out.println(InputVec[i]);

                    }
                    catch (Exception e) {
                        InputVec[i] = 0;

                    }
                    i = i-1;
                }

                if (mInferenceInterface == null) {
                    AssetManager assetManager = getAssets();
                    mInferenceInterface = new TensorFlowInferenceInterface(assetManager,MODEL_FILE);
                }

                float[] value_ = new float[1];

                mInferenceInterface.feed(INPUT_NODE,InputVec,1,1000);
                mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
                System.out.println(Float.toString(value_[0]));
                mInferenceInterface.fetch(OUTPUT_NODE, value_);

                double scoreIn;
                scoreIn = value_[0]*5;
                double ratingIn = scoreIn;
                String stringDouble = Double.toString(scoreIn);
                score.setText(stringDouble);
                ratingBar.setRating((float) ratingIn);

            }

        });

    }
}

需要注意的一点是,我们可能需要编辑应用的build.gradle文件,以便将包添加到依赖项中:

org.tensorflow:tensorflow-android:1.7.0

测试移动应用程序

我们将用两部电影的评论来测试手机应用:阿凡达星际阿凡达影评取自www.rogerebert.com/reviews/ava…,内容如下:

“看《阿凡达》的时候,我感觉和 1977 年看《星球大战》的时候差不多。那是我带着不确定的期望走进的另一部电影。詹姆斯·卡梅隆的电影就像他的《泰坦尼克号》一样,一直是令人怀疑的热门话题。他又一次通过简单地发表一部非凡的电影让质疑者哑口无言。在好莱坞,至少还有一个人知道如何明智地花费 2.5 亿美元,或者是 3 亿美元。

《阿凡达》不仅仅是一部轰动一时的娱乐片,尽管它确实如此。这是技术上的突破。它有一个明确的绿色和反战信息。发动邪教是命中注定的。它包含这样的视觉细节,它将奖励重复观看。它发明了一种新的语言,纳威语,就像《指环王》一样,尽管幸运的是,我怀疑这种语言是否能被人类使用,即使是十几岁的人。它创造了新的电影明星。这是一个活动,是那些你觉得必须看才能跟上对话的电影之一。”

评论者给电影的评分是 4/5,而手机应用给的评分是 4.8/5 左右,如下图截图所示(图 7.4 ):

Figure 7.4. Mobile app review rating of the movie Avatar

同样,我们将评估该应用为电影*《星际穿越》*提供的评级,评论来自www.rottentomatoes.com/m/interstel…。审核如下:

《星际穿越》代表了更多的惊心动魄、发人深省、视觉上光彩照人的电影制作,观众已经开始期待编剧兼导演克里斯托弗·诺兰的作品,尽管它的知识面有些超出了观众的理解范围

电影在烂番茄上的平均评分是 7/10,当缩放到 5 时,评分是 3.5/5,而手机应用预测的评分是 3.37,如下图截图所示(图 7.5 ):

Figure 7.5. Mobile app review rating of the movie Interstellar

正如您在前面两张插图中看到的,移动电影评论评级应用程序在为电影评论提供合理评级方面做得很好。

摘要

读完这一章,读者应该对如何利用 TensorFlow 的移动功能在安卓应用程序中部署深度学习模型有了一个大致的了解。本章中涉及的技术细节和实现细节应该对读者有益,帮助他们构建智能安卓移动应用程序,并以有趣的方式扩展它们。本项目的详细代码位于https://github . com/PacktPublishing/Python-人工智能-项目/第 07 章

在下一章中,我们将为客户服务构建一个对话式 AI 聊天机器人。我们期待您的参与。****