Stanford CS230深度学习(六)目标检测、人脸识别和神经风格迁移

在CS230的lecture 6中主要吴恩达老师讲述了一些关于机器学习和深度学习的tips,用一个触发词台灯的例子教我们如何快速的解决实际中遇到的问题,这节课主要是偏思维上的了解,还是要实际问题实际分析。

在课后的coursera上的C4M3和C4M4主要是卷积神经网络的一些应用,包括用YOLO算法进行目标检测(Object detection)、基于Inception v2进行人脸识别(Face recognition)、基于VGG19完成神经风格迁移(Neural style transfer)等。

所有的作业都是在TensorFlow2.1 CPU、Python3.7下实现的。

回顾知识点

1. 目标检测(Object detection)

将目标检测化成回归问题

目标检测包含两个方面,一是图片中是否存在物体以及物体是什么,二是若存在物体它的位置在哪。根据这两个问题,可以将目标检测的问题转化为回归问题。

对于是否存在,可以直接使用逻辑回归得到概率值 p c p_c pc,物体具体是什么可以由softmax多分类得到各自的概率,假设是3分类,概率分别记为 c 1 , c 2 , c 3 c_1,c_2,c_3 c1,c2,c3。而对于目标位置,可以用将图片的左上角记为 ( 0 , 0 ) (0,0) (0,0),右下角记为 ( 1 , 1 ) (1,1) (1,1),目标存在的中心位置的坐标为 ( b x , b y ) (b_x,b_y) (bx,by),它的长宽分别占整张图的比例记为 ( b h , b w ) (b_h,b_w) (bh,bw),那这样就得到一个标签向量 y = ( p c , b x , b y , b h , b w , c 1 , c 2 , c 3 ) y=(p_c,b_x,b_y,b_h,b_w,c_1,c_2,c_3) y=(pc,bx,by,bh,bw,c1,c2,c3),训练集也是这样标记好的标签图片的话,就可以直接用类似欧氏距离的度量标准来衡量模型检测的好坏,从而就可以定义模型的损失函数。

卷积网络实现滑动窗口检测

由于标签训练集都是一些主要只包含目标物体的图像,相当于在正常图片中裁剪出目标物体的那个方框,所以如果想要预测的更准确,需要用滑动窗口遍历正常图片,得到一个个小的方框,再进行目标检测。

但是一个图片可能包含很多个这样的滑动窗口,重复操作太浪费时间,可以借助卷积网络来一次性遍历所有窗口,达到只用前向传播一次的目的。

为了构建滑动窗口的卷积应用,首先要把神经网络的全连接层转化成卷积层,通过设置过滤器的大小即可转换,参数量不变,但是连接方式变了,这样所有的前向传播都是卷积操作。
在这里插入图片描述

整个网络都是全卷积的网络,通过卷积层中过滤器滑动每个特定的窗口,能很好的实现原本需要重复操作的一个个小窗口的检测,避免了大量的重复计算。
在这里插入图片描述

Bounding Box预测

但是像上述那样滑动窗口得到的边框很大概率不是一个很合适的边框,可能只包含了物体的一小部分,为了得到精确的边框,YOLO算法将输入图像分成19*19个格子,每个格子都包含一个标签y(类似 y = ( p c , b x , b y , b h , b w , c 1 , c 2 , c 3 ) y=(p_c,b_x,b_y,b_h,b_w,c_1,c_2,c_3) y=(pc,bx,by,bh,bw,c1,c2,c3))。这样,若某个物体的中心位置的坐标落入到某个格子,那么这个格子就负责检测出这个物体。整个图片的输出就应该是 19 × 19 × ( 5 + 3 ) 19\times 19\times(5+3) 19×19×(5+3),其中5表示存在物体的概率以及物体的位置信息,3个是物体的分类数量,作业中分了80类,所以是 19 × 19 × 85 19\times 19\times85 19×19×85

此外,为了能让一个格子能检测出多个对象,还多个采用了不同形状的描框(Anchor Boxes),这样可以让一个格子中多个对象匹配到合适形状的边框。因为每个格子预测的标签中包含了这个物体检测出来的边框,这个物体只需选择和自己边框最匹配的一个描框即可,这个匹配程度是用交并比(Intersection over union, IoU)来衡量的,其实就是两个方框的交集区域的面积与并集区域面积之间的比例,越接近1说明越匹配。本次作业中使用了5个描框,因此整个图片的输出大小为 19 × 19 × 5 × 85 19\times 19\times5\times85 19×19×5×85

非极大值抑制(Non-max suppression)

对于相邻多个格子都认为自己检测到了物体,但是这个物体是一样的,这就造成了最终有很多重复的边框输出,为了只输出一个最好的预测边框,非极大值抑制(NMS)会先丢弃掉预测概率低于阈值的锚框,然后选择最大概率的那个描框作为输出边框,再过滤掉所有与预测边框的IoU值比较大的那些描框(因为他们和最大概率那个描框重复较多),这样就完成了一个描框的输出。根据预先设定的最大描框数量,再重复几次这个非极大值抑制操作即可得到最后目标检测的可视化结果了。

2. 人脸识别(Face recognition)

人脸验证(face verification)与人脸识别(face recognition)有所不同,前者是一个二分类的问题,也就是在给出一个id后验证人脸和id是否匹配,匹配上说明是同一个人即输出1,否则输出0;而人脸识别比较复杂,它没有给出验证的id,而是在事先设置的数据库中寻找匹配的人脸,如果数据库中有那就输出这个人,否则就输出不存在。人脸识别可以使用人脸验证的模型作为基础模型,然后遍历所有数据库中的样本分别进行验证,但是这样需要验证的模型精度相当高才能得到一个比较好的识别模型。

一次学习(One-shot)和Siamese网络

在大多数人脸识别应用中,你需要通过单单一张图片或者单单一个人脸样例就能去识别这个人,像这样只有一个训练样本的情况就是一次学习(One-shot learning)。

一次学习主要是依托一个好的神经网络,这个网络可以在一个大的样本集上学到一些general knowledge,然后将这个网络运用到一个样本的学习上来,它就可以专注地学习这个人独有的特征,等下次再见到这个样本的人脸,就可以识别出来了。因此,一次学习也算是迁移学习的范畴。

当输入人脸图片时,我们最后可以得到这张图片是否是某个人,也就是进行最后有一个sigmoid或者softmax的输出,但如果每次新加入一个需要识别的人脸,也就意味着需要重新设计模型,至少需要在最后一层多输出一个类别,这样子代价很大,不方便。所以每次只需要通过模型得到样本的特征编码,通过比较两个样本的编码之间的欧氏距离,根据距离的大小来判断两个样本是否是同一个人,即 ∥ f ( A ) − f ( B ) ∥ 2 \|f(A)-f(B)\|^2 f(A)f(B)2是否超过了阈值。这种输出两个样本之间编码距离的网络就是孪生网络(Siamese network)。

Triplet损失

为了更好地训练这个网络,告诉它什么样的编码更好,需要定义一个合适的损失函数。Triplet损失衡量了输入图片与正例(同一个人)和反例图片(不同且差别很大的人)之间的关系。

具体来说,我们希望输入的图片和正例之间距离越小越好,这样说明两个图片的特征越相似,即最小化 d ( A , P ) = ∥ f ( A ) − f ( P ) ∥ 2 d(A,P)=\|f(A)-f(P)\|^2 d(A,P)=f(A)f(P)2,反之,我们希望不是同一个人之间的特征编码差别尽量大一些,所以输入的图片和反例之间距离越大越好,至少输入图片和反例之间距离要超过它与正例之间距离,即 d ( A , N ) = ∥ f ( A ) − f ( N ) ∥ 2 > d ( A , P ) = ∥ f ( A ) − f ( P ) ∥ 2 d(A,N)=\|f(A)-f(N)\|^2>d(A,P)=\|f(A)-f(P)\|^2 d(A,N)=f(A)f(N)2>d(A,P)=f(A)f(P)2。类似支持向量机(SVM)的做法,我们希望这两个距离之间有一定的间隔,也就是说我们希望输入图片和反例之间距离要比它与正例之间距离至少大一个正数 α \alpha α,这个 α \alpha α就是两个距离之间的间隔(margin),即 d ( A , P ) + α = ∥ f ( A ) − f ( P ) ∥ 2 + α ⩽ ∥ f ( A ) − f ( N ) ∥ 2 = d ( A , N ) d(A,P)+\alpha=\|f(A)-f(P)\|^2+\alpha\leqslant\|f(A)-f(N)\|^2=d(A,N) d(A,P)+α=f(A)f(P)2+αf(A)f(N)2=d(A,N)由此,定义triplet loss为 ℓ ( A , P , N ) = max ⁡ ( ∥ f ( A ) − f ( P ) ∥ 2 + α − ∥ f ( A ) − f ( N ) ∥ 2 , 0 ) ) \ell(A,P,N)=\max(\|f(A)-f(P)\|^2+\alpha-\|f(A)-f(N)\|^2,0)) (A,P,N)=max(f(A)f(P)2+αf(A)f(N)2,0))它表示当满足上述不等式时, ∥ f ( A ) − f ( P ) ∥ 2 + α − ∥ f ( A ) − f ( N ) ∥ 2 ⩽ 0 \|f(A)-f(P)\|^2+\alpha-\|f(A)-f(N)\|^2\leqslant0 f(A)f(P)2+αf(A)f(N)20,此时损失记为0,当不满足时,损失就是一个正数。这样就可以让孪生网络达到我们想要的功能。

3. 神经风格迁移(Neural style transfer)

神经风格迁移就是把一张图片的内容再加上另一张图片的风格得到一个新的图片。具体来说,在给定一个初始化的图片后,在一种适合的损失函数下,把这张图片的所有像素点当做是参数,用神经网络来训练这张图片,最后得到一个内容不变但风格类似于令一张图片的新图片。我认为这其中最关键的在于损失函数模型,只要理解了这两点基本上神经风格迁移就能完成。

损失函数

由于我们希望得到的图片是一张内容还是原内容,但风格接近目标风格图片的这样一张图片,这样得到的损失函数评价的是生成图片G与内容图片C和风格图片S之间的关系,所以主要就从内容和风格来评价这个生成的图片,即内容损失和风格损失。

  • 内容损失 J c o n t e n t ( C , G ) J_{content}(C,G) Jcontent(C,G)
    由于神经网络的隐层实际上是在学习图片中的边界或者某个部分的图像,因此让生成图片G的某个隐层的激活输出与内容图片C的输出之间的差别逐渐变小,那说明生成图片G在图片的构成上逐渐向内容图片C靠拢,所以他们各自的某个隐层的激活输出之间的差的L2范数就可以作为评价的标准,即 J c o n t e n t ( C , G ) = 1 4 ∗ n H ∗ n W ∗ n C ∥ a C − a G ∥ 2 J_{content}(C,G)={1\over 4 * n_H * n_W * n_C}\|a^C-a^G\|^2 Jcontent(C,G)=4nHnWnC1aCaG2其中这个隐层的选择也是可以调整的,一般说来,选择一个中间一点的隐层比较合适,太深的可能只是保证生成图片是这个类的但内容差别很大,而太浅可能会导致生成图片只是像素上相似,但是可能都没有一个完整的图案。

  • 风格损失 J s t y l e ( S , G ) J_{style}(S,G) Jstyle(S,G)
    首先一个问题就是风格是什么呢?它不是一个很好描述的量,人可能会很擅长区分,一眼就看出来,但是也说不太明白。在神经风格迁移的论文中,图片的风格定义为神经网络层中各个通道之间激活项的相关系数,这个相关系数由Gram矩阵来表示。
    假如风格图片的某一层的激活输出为 a S a^S aS a S a^S aS的shape为 n H × n W × n C n_H \times n_W \times n_C nH×nW×nC,那 a S a^S aS的Gram矩阵 G S G^S GS就是一个 n C × n C n_C \times n_C nC×nC的矩阵,其中 G S G^S GS的第 k k k k ′ k' k列元素为 G k k ′ S = ∑ i = 1 n H ∑ j = 1 n W a i j k a i j k ′ G_{kk'}^S=\sum_{i=1}^{n_H}\sum_{j=1}^{n_W}a_{ijk}a_{ijk'} GkkS=i=1nHj=1nWaijkaijk G k k ′ S G_{kk'}^S GkkS衡量了这一层激活中通道 k k k k ′ k' k之间的相关性,也就是计算了不同过滤器的输出之间的相关性,例如某个检测条纹的过滤器就和某个颜色关联性比较大,这样如果生成的图片中的过滤器也有类似的关联性,那说明他们之间风格比较像。
    因此,类似内容损失,我们希望生成图片和风格图片之间的Gram矩阵之间差别越来越小,这样生成图片G的风格就会越来越像S,所以某一隐层的风格损失就定义为 J s t y l e ( S , G ) = 1 ( 2 ∗ n H ∗ n W ∗ n C ) 2 ∥ G S − G G ∥ 2 J_{style}(S,G)={1\over (2 * n_H * n_W * n_C)^2}\|G^S-G^G\|^2 Jstyle(S,G)=(2nHnWnC)21GSGG2关于隐层的选择,由于每个隐层由于所处的位置,过滤器检测到的特征不太一样,因此在计算风格损失时最好多选用深浅不一的隐层进行计算,然后将他们各自的损失求和得到总的风格损失 J s t y l e ( S , G ) J_{style}(S,G) Jstyle(S,G)

利用模型来学习

神经风格迁移一般说来都是用的预训练模型,那些已经在很多图片上学习的比较好的模型会更适合抓住图片的内容和风格,用在风格迁移上更好。但是风格迁移又和一般的迁移学习不太一样,一般迁移学习是微调模型的参数来得到一个适合于新任务的模型,而在神经风格迁移中,我们要训练或者说微调的变成了一张图片的所有像素点,把这些像素点的值作为参数来进行训练,每次利用模型计算损失函数,然后得到损失函数对于这张生成图片像素点的导数,然后进行梯度更新,如此迭代来得到我们要的图片。

在本次作业中,建议使用的是VGG19,原本代码中是给出来VGG19的参数,但是由于TensorFlow版本的原因,不能直接将tf1的代码在tf2环境下跑,所以后面的代码中先是用Keras的接口直接下载VGG19的模型,然后再进行训练。

作业代码

1. Car Detection with YOLO

本次作业主要分为两部分,第一部分就是在已有YOLO检测算法的基础上如何过滤选择描框,首先丢弃预测值低于阈值的描框,然后用NMS过滤出IoU最大的描框,这些跟着作业都可以一步步实现。然后第二部分就是如何得到一个已经训练好的YOLO模型,作业中给出来的yolo.h5文件是在tf1下的,在tf2下运行就会报错,因此如何获得模型对新手来说也是个挺大的挑战,这是我整理的方法:tf2.1下生成yolo.h5文件,可以按照这样的步骤获得一个训练好的模型。

import os
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import h5py
# tf2.1版本,需要改动一些接口,例如keras改成tf.keras等
from yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxes
from yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body


# 过滤掉预测的概率值低于阈值的box,并输出过滤之后的box的信息(概率值、位置信息,分类类别)
def yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold=0.6):
    """Filters YOLO boxes by thresholding on object and class confidence.
    
    Arguments:
    box_confidence -- tensor of shape (19, 19, 5, 1)
    boxes -- tensor of shape (19, 19, 5, 4)
    box_class_probs -- tensor of shape (19, 19, 5, 80)
    threshold -- real value, if [ highest class probability score < threshold], then get rid of the corresponding box
    
    Returns:
    scores -- tensor of shape (None,), containing the class probability score for selected boxes
    boxes -- tensor of shape (None, 4), containing (b_x, b_y, b_h, b_w) coordinates of selected boxes
    classes -- tensor of shape (None,), containing the index of the class detected by the selected boxes
    
    Note: "None" is here because you don't know the exact number of selected boxes, as it depends on the threshold. 
    For example, the actual output size of scores would be (10,) if there are 10 boxes.
    """
    # 每个类的概率 shape=(19, 19, 5, 80)
    box_class_scores = box_confidence * box_class_probs
    
    # 找出概率最大的那个类的索引以及概率 shape=(19, 19, 5)
    index_class = tf.keras.backend.argmax(box_class_scores, axis=-1)
    max_scores = tf.keras.backend.max(box_class_scores, axis=-1)
    
    # 筛选出最大概率大于阈值的那些而得到掩码
    filtering_mask = max_scores > threshold
    
    # 输出筛选后的概率值、位置信息以及分类类别
    scores = tf.boolean_mask(max_scores, filtering_mask)
    boxes = tf.boolean_mask(boxes, filtering_mask)
    classes = tf.boolean_mask(index_class, filtering_mask)
    
    return scores, boxes, classes

# 测试
tf.random.set_seed(1)
box_confidence = tf.random.normal([19, 19, 5, 1], mean=1, stddev=4)
boxes = tf.random.normal([19, 19, 5, 4], mean=1, stddev=4)
box_class_probs = tf.random.normal([19, 19, 5, 80], mean=1, stddev=4)
scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = 0.5)

print("scores[2] = " + str(scores[2].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.shape))
print("boxes.shape = " + str(boxes.shape))
print("classes.shape = " + str(classes.shape))

# scores[2] = 26.991379
# boxes[2] = [-4.0077353  1.0848972 -1.2055032 -5.972679 ]
# classes[2] = 43
# scores.shape = (1784,)
# boxes.shape = (1784, 4)
# classes.shape = (1784,)


# non-maximum suppression(NMS)非最大抑制
# 首先定义IoU函数,这个函数下面也用不到,TensorFlow有自带的NMS函数
def iou(box1, box2):
    """Implement the intersection over union (IoU) between box1 and box2
    
    Arguments:
    box1 -- first box, list object with coordinates (x1, y1, x2, y2) 左上角的坐标
    box2 -- second box, list object with coordinates (x1, y1, x2, y2) 右下角的坐标
    """
    # 求交区域的坐标
    i_x1 = max(box1[0], box2[0])
    i_y1 = max(box1[1], box2[1])
    i_x2 = min(box1[2], box2[2])
    i_y2 = min(box1[3], box2[3])
    
    # 面积
    area_box1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area_box2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    area_i = max(0, i_x2 - i_x1) * max(0, i_y2 - i_y1)
    
    #IoU
    IoU = area_i / (area_box1 + area_box2 - area_i)
    
    return IoU

# 测试
box1 = (2, 1, 4, 3)
box2 = (1, 2, 3, 4) 
print("iou = " + str(iou(box1, box2)))
# iou = 0.14285714285714285

# NMS filtering 保留概率最大的描框,去除掉与这个描框的IoU大于一个阙值的其余描框
def yolo_non_max_suppression(scores, boxes, classes, max_boxes=10, iou_threshold=0.5):
    """
    Applies Non-max suppression (NMS) to set of boxes
    
    Arguments:
    scores -- tensor of shape (None,), output of yolo_filter_boxes()
    boxes -- tensor of shape (None, 4), output of yolo_filter_boxes() that have been scaled to the image size (see later)
    classes -- tensor of shape (None,), output of yolo_filter_boxes()
    max_boxes -- integer, maximum number of predicted boxes you'd like
    iou_threshold -- real value, "intersection over union" threshold used for NMS filtering
    
    Returns:
    scores -- tensor of shape (, None), predicted score for each box
    boxes -- tensor of shape (4, None), predicted box coordinates
    classes -- tensor of shape (, None), predicted class for each box
    
    Note: The "None" dimension of the output tensors has obviously to be less than max_boxes. Note also that this
    function will transpose the shapes of scores, boxes, classes. This is made for convenience.
    """
    # 用tf.image.non_max_suppression来执行NMS获得描框的索引
    indices = tf.image.non_max_suppression(boxes, scores, max_output_size=max_boxes, iou_threshold=iou_threshold)
    
    # 用tf.gather来获取切片
    scores = tf.gather(scores, indices)
    boxes = tf.gather(boxes, indices)
    classes = tf.gather(classes, indices)
    
    return scores, boxes, classes

# 测试
tf.random.set_seed(1)
scores = tf.random.normal([54,], mean=1, stddev=4)
boxes = tf.random.normal([54, 4], mean=1, stddev=4)
classes = tf.random.normal([54,], mean=1, stddev=4)
scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes)

print("scores[2] = " + str(scores[2].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.shape))
print("boxes.shape = " + str(boxes.shape))
print("classes.shape = " + str(classes.shape))

# scores[2] = 7.183007
# boxes[2] = [ 3.8470404 -0.9571458 -2.0568852 -3.1489944]
# classes[2] = -0.62746906
# scores.shape = (10,)
# boxes.shape = (10, 4)
# classes.shape = (10,)


# 整合上面的两个函数yolo_filter_boxes和yolo_non_max_suppression,对所有的YOLO输出的框进行过滤
def yolo_eval(yolo_outputs, image_shape=(720., 1280.), max_boxes=10, score_threshold=0.6, iou_threshold=0.5):
    """
    Converts the output of YOLO encoding (a lot of boxes) to your predicted boxes along with their scores, box coordinates and classes.
    
    Arguments:
    yolo_outputs -- output of the encoding model (for image_shape of (608, 608, 3)), contains 4 tensors:
                    box_confidence: tensor of shape (None, 19, 19, 5, 1)
                    box_xy: tensor of shape (None, 19, 19, 5, 2)
                    box_wh: tensor of shape (None, 19, 19, 5, 2)
                    box_class_probs: tensor of shape (None, 19, 19, 5, 80)
    image_shape -- tensor of shape (2,) containing the input shape, in this notebook we use (608., 608.) (has to be float32 dtype)
    max_boxes -- integer, maximum number of predicted boxes you'd like
    score_threshold -- real value, if [ highest class probability score < threshold], then get rid of the corresponding box
    iou_threshold -- real value, "intersection over union" threshold used for NMS filtering
    
    Returns:
    scores -- tensor of shape (None, ), predicted score for each box
    boxes -- tensor of shape (None, 4), predicted box coordinates
    classes -- tensor of shape (None,), predicted class for each box
    """
    box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs
    
    # 将yolo锚框坐标(x,y,w,h)转换为角的坐标(x1,y1,x2,y2)
    boxes = yolo_boxes_to_corners(box_xy, box_wh)
    # 过滤掉概率值小于阈值的描框
    scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold=score_threshold)
    # 缩放描框,将原本608x608大小的描框缩放成image_shape大小的
    boxes = scale_boxes(boxes, image_shape)
    # 使用非最大抑制用IoU阈值过滤掉多余描框
    scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes=max_boxes, iou_threshold=iou_threshold)
     
    return scores, boxes, classes

# 测试
tf.random.set_seed(1)
yolo_outputs = (tf.random.normal([19, 19, 5, 1], mean=1, stddev=4),
                tf.random.normal([19, 19, 5, 2], mean=1, stddev=4),
                tf.random.normal([19, 19, 5, 2], mean=1, stddev=4),
                tf.random.normal([19, 19, 5, 80], mean=1, stddev=4))
scores, boxes, classes = yolo_eval(yolo_outputs)

print("scores[2] = " + str(scores[2].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.shape))
print("boxes.shape = " + str(boxes.shape))
print("classes.shape = " + str(classes.shape))  
    
# scores[2] = 138.91467
# boxes[2] = [ 1626.0632 -4109.4087  5543.386  -3679.8076]
# classes[2] = 48
# scores.shape = (10,)
# boxes.shape = (10, 4)
# classes.shape = (10,)


# 测试YOLO预训练模型
# 加载类别名称、描框大小、图片大小
class_names = read_classes("YOLO/model_data/coco_classes.txt")
anchors = read_anchors("YOLO/model_data/yolo_anchors.txt")
image_shape = (720., 1280.)

# 加载预训练的YOLO模型
yolo_model = tf.keras.models.load_model("YOLO/model_data/yolov2.h5")

# 查看模型
yolo_model.summary()


# 用YOLO模型预测图片,得到检测后的图片并保存和展示,tf2中不需要会话
def predict(image_file):
    """
    Ues this YOLOv2 model to predict boxes for "image_file". Prints and plots the preditions.
    
    Arguments:
    image_file -- name of an image stored in the "images" folder.
    
    Returns:
    scores -- tensor of shape (None, ), scores of the predicted boxes
    boxes -- tensor of shape (None, 4), coordinates of the predicted boxes
    classes -- tensor of shape (None, ), class index of the predicted boxes
    """
    # 预处理图片,自行修改图片存放的路径
    image, image_data = preprocess_image("image/images/" + image_file, model_image_size = (608, 608))
    # 得到模型输出,(1, 19, 19, 425),numpy格式,下面需要转换成tensor
    outputs = yolo_model.predict(image_data)
    # 把模型的输出转化为需要过滤的张量(box_confidence, box_xy, box_wh, box_class_probs)
    yolo_outputs = yolo_head(tf.convert_to_tensor(outputs), anchors, len(class_names))
    # 用上面的yolo_eval函数过滤描框
    scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)
    # 打印预测的信息,检测到多少个框
    print('Found {} boxes for {}'.format(len(boxes), image_file))
    # 给不同类别生成不同颜色
    colors = generate_colors(class_names)
    # 在图片中画出框
    draw_boxes(image, scores, boxes, classes, class_names, colors)
    # 保存该图片,注意这里需要在当前目录下存在一个out文件夹存放这些图片
    image.save(os.path.join("out", image_file), quality=90)
    # 展示预测后的图片
    output_image = plt.imread(os.path.join("out", image_file))
    plt.imshow(output_image)
    
    return scores, boxes, classes

# 测试
plt.imshow(plt.imread('image/images/test.jpg'))
out_scores, out_boxes, out_classes = predict('test.jpg')


# 循环处理120张图(从'0001.jpg'到'0120.jpg')
for i in range(1, 121):
    file_name = '0' * (4-len(str(i))) + str(i) + '.jpg'
    print('file name: %s' % file_name)
    out_scores, out_boxes, out_classes = predict(file_name)

# 将处理结果制作动图
import imageio

frames = []
for i in range(1, 121):
    file_name = '0' * (4-len(str(i))) + str(i) + '.jpg'
    frames.append(imageio.imread("out/" + file_name))

imageio.mimsave('car_detection.gif', frames, 'GIF', duration=0.2)

最后制作的动图的截取:
在这里插入图片描述

2. Face Recognition for the Happy House

这次作业中并没有根据triplet损失来自己训练模型,这里用的是InceptionV2,按道理来说下载好了预训练模型后,需要用triplet来自己微调得到更适合自己任务的模型的,作业中直接提供了参数,后面可以再自己尝试下重新训练。

注意TensorFlow2.1 CPU版本只支持channels_last格式,所以原本设计好的channels_first模型就不能调用,需要将fr_utils和inception_blocks_v2两个文件中有关通道设置的都改成channels_last的格式,主要需要改动BN层的axis参数、卷积层的data_format、concatenate的axis等,搭建好模型后再使用set_weights来给每一层赋权重,权重不会因为通道的格式改变,所以只需修改模型即可。如果是GPU版本的可以直接跳过,使用作业给出的模型就好。

import tensorflow as tf
import numpy as np
from fr_utils import *
from inception_blocks_v2 import *

# tf CPU版本只支持channels_last,所以后面改了下模型
tf.keras.backend.set_image_data_format('channels_last')


# 导入Inception model作为face recognition模型
# 注意修改输入的shape
# 在另外两个支持文档中需要改变BN层的axis、channels_last、concatenate的axis
FRmodel = faceRecoModel(input_shape=(96, 96, 3))

# 模型的总参数数量 Total Params: 3743280
print("Total Params:", FRmodel.count_params())
FRmodel.summary()

# 定义triplet损失
def triplet_loss(y_true, y_pred, alpha=0.2):
    """
    Implementation of the triplet loss as defined by formula (3)
    
    Arguments:
    y_true -- true labels, required when you define a loss in Keras, you don't need it in this function.
    y_pred -- python list containing three objects:
            anchor -- the encodings for the anchor images, of shape (None, 128)
            positive -- the encodings for the positive images, of shape (None, 128)
            negative -- the encodings for the negative images, of shape (None, 128)
    
    Returns:
    loss -- real number, value of the loss
    """
    anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2]
    d_ap = tf.reduce_sum(tf.square(tf.subtract(anchor, positive)), axis=-1)
    d_an = tf.reduce_sum(tf.square(tf.subtract(anchor, negative)), axis=-1)
    l = tf.add(tf.subtract(d_ap, d_an), alpha)
    loss = tf.reduce_sum(tf.maximum(l, 0))
    return loss

# 测试
tf.random.set_seed(1)
y_true = (None, None, None)
y_pred = (tf.random.normal([3, 128], mean=6, stddev=0.1),
          tf.random.normal([3, 128], mean=1, stddev=1),
          tf.random.normal([3, 128], mean=3, stddev=4))
loss = triplet_loss(y_true, y_pred)
print("loss = " + str(loss.numpy()))
# loss = 1094.3419

# 编译模型
FRmodel.compile(optimizer='adam', loss=triplet_loss, metrics=['accuracy'])

# 不自己训练,而是使用训练好的权重。下面加载权重,需要一点时间
load_weights_from_FaceNet(FRmodel)

# face verification 1v1
# 建立一个字典database储存可进入happy house的人脸编码
database = {}
database["danielle"] = img_to_encoding("image/face_images/danielle.png", FRmodel)
database["younes"] = img_to_encoding("image/face_images/younes.jpg", FRmodel)
database["tian"] = img_to_encoding("image/face_images/tian.jpg", FRmodel)
database["andrew"] = img_to_encoding("image/face_images/andrew.jpg", FRmodel)
database["kian"] = img_to_encoding("image/face_images/kian.jpg", FRmodel)
database["dan"] = img_to_encoding("image/face_images/dan.jpg", FRmodel)
database["sebastiano"] = img_to_encoding("image/face_images/sebastiano.jpg", FRmodel)
database["bertrand"] = img_to_encoding("image/face_images/bertrand.jpg", FRmodel)
database["kevin"] = img_to_encoding("image/face_images/kevin.jpg", FRmodel)
database["felix"] = img_to_encoding("image/face_images/felix.jpg", FRmodel)
database["benoit"] = img_to_encoding("image/face_images/benoit.jpg", FRmodel)
database["arnaud"] = img_to_encoding("image/face_images/arnaud.jpg", FRmodel)

# 身份验证,L2距离小于0.7则开门,否则不开
def verify(image_path, identity, database, model):
    """
    Function that verifies if the person on the "image_path" image is "identity".
    
    Arguments:
    image_path -- path to an image
    identity -- string, name of the person you'd like to verify the identity. Has to be a resident of the Happy house.
    database -- python dictionary mapping names of allowed people's names (strings) to their encodings (vectors).
    model -- your Inception model instance in Keras
    
    Returns:
    dist -- distance between the image_path and the image of "identity" in the database.
    door_open -- True, if the door should open. False otherwise.
    """
    # 图像的编码
    i_encoding = img_to_encoding(image_path, model)
    # 计算距离
    dist = np.linalg.norm(i_encoding - database[identity])
    # 判断并打印信息
    door_open = True if dist < 0.7 else False
    msg = "It's " + identity + ", welcome home!" if door_open==True else "It's not " + identity + ", please go away."
    print(msg)
    return dist, door_open
    
# 测试
plt.imshow(plt.imread("image/face_images/camera_0.jpg"))
plt.imshow(plt.imread("image/face_images/younes.jpg"))
verify("image/face_images/camera_0.jpg", "younes", database, FRmodel)
# It's younes, welcome home!
# (0.6710072, True)

plt.imshow(plt.imread("image/face_images/camera_2.jpg"))
plt.imshow(plt.imread("image/face_images/kian.jpg"))
verify("image/face_images/camera_2.jpg", "kian", database, FRmodel)
# It's not kian, please go away.
# (0.8580012, False)

# 建立人脸识别模型,把前面的人脸检测改一改
def who_is_it(image_path, database, model):
    """
    Implements face recognition for the happy house by finding who is the person on the image_path image.
    
    Arguments:
    image_path -- path to an image
    database -- database containing image encodings along with the name of the person on the image
    model -- your Inception model instance in Keras
    
    Returns:
    min_dist -- the minimum distance between image_path encoding and the encodings from the database
    identity -- string, the name prediction for the person on image_path
    """
    # 图像的编码
    i_encoding = img_to_encoding(image_path, model)
    # 遍历数据库中的所有身份,找到最小距离的那个身份
    min_dist = 100
    for (name, db_encoding) in database.items():
        dist = np.linalg.norm(i_encoding - db_encoding)
        if dist < min_dist:
            min_dist = dist
            identity = name
    # 判断并打印信息
    if min_dist < 0.7:
        print("It's " + identity + ", the distance is " + str(min_dist))
    else:
        print("Not in the database.")
    return min_dist, identity

# 测试
who_is_it("image/face_images/camera_0.jpg", database, FRmodel)
# It's younes, the distance is 0.6710072
# (0.6710072, 'younes')

3. Art Generation with Neural Style Transfer

这部分的代码是在原本作业的基础上自己改的,而且改动比较大,可能会存在问题,可以留言联系我交流交流。

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from skimage.transform import resize
from nst_utils import *


''' ============获得模型==========='''
# 迁移学习,用VGG-19,作业中的VGG19是在tf1下的,有很多代码不一样,所以我自行改成tf2.1的
# 使用Keras的接口下载模型,注意很慢,我的渣渣网下了半个小时
base_model = tf.keras.applications.vgg19.VGG19(include_top=False, # 不包含最后的全连接层
                                               weights='imagenet', # 加载预训练的权重
                                               input_shape=(300, 400, 3), # 输入的大小
                                               pooling='avg') # 最后一层用avg池化
# 固定参数,都设置为不可训练的,
base_model.trainable = False
base_model.summary()

# 得到中间层的输出,前5个计算风格代价,最后一个用来计算内容代价,为后面做准备
Outputs = (base_model.get_layer('block1_conv1').output, 
           base_model.get_layer('block2_conv1').output, 
           base_model.get_layer('block3_conv1').output, 
           base_model.get_layer('block4_conv1').output, 
           base_model.get_layer('block5_conv1').output, 
           base_model.get_layer('block4_conv2').output)
# 得到模型
VGG19_model = tf.keras.Model(inputs=base_model.input, outputs=Outputs)


''' ============定义损失函数==========='''
# 先计算内容代价
# 选取一个适合的隐层,不要太浅也别太深的,将这一层的激活作为代价的输入,代价即为两个tensor的L2模的平方(以及乘上一个常数)
def compute_content_cost(a_C, a_G):
    """
    Computes the content cost
    
    Arguments:
    a_C -- tensor of dimension (1, n_H, n_W, n_C), hidden layer activations representing content of the image C 
    a_G -- tensor of dimension (1, n_H, n_W, n_C), hidden layer activations representing content of the image G
    
    Returns: 
    J_content -- scalar that you compute using equation 1 above.
    """
    n_H, n_W, n_C = a_C.shape[1:]
    J_content = tf.reduce_sum(tf.square(tf.subtract(a_C, a_G))) / (4 * n_H * n_W * n_C)
    return J_content
    
# 测试 
tf.random.set_seed(1)
a_C = tf.random.normal([1, 4, 4, 3], mean=1, stddev=4)
a_G = tf.random.normal([1, 4, 4, 3], mean=1, stddev=4)
J_content = compute_content_cost(a_C, a_G)
print("J_content = " + str(J_content.numpy()))
# J_content = 7.056877


# 先计算某一层的风格代价
def compute_layer_style_cost(a_S, a_G):
    """
    Arguments:
    a_S -- tensor of dimension (1, n_H, n_W, n_C), hidden layer activations representing style of the image S 
    a_G -- tensor of dimension (1, n_H, n_W, n_C), hidden layer activations representing style of the image G
    
    Returns: 
    J_style_layer -- tensor representing a scalar value, style cost defined above by equation (2)
    """
    n_H, n_W, n_C = a_S.shape[1:]
    # 先计算风格矩阵(gram matrix)得到通道之间的相关性
    a_S = tf.reshape(a_S, [n_H * n_W, n_C])
    a_G = tf.reshape(a_G, [n_H * n_W, n_C])    
    gram_matrix_S = tf.matmul(tf.transpose(a_S), a_S)
    gram_matrix_G = tf.matmul(tf.transpose(a_G), a_G)
    # 计算代价
    J_style_layer = tf.reduce_sum(tf.square(tf.subtract(gram_matrix_S, gram_matrix_G))) / ((2 * n_H * n_W * n_C)**2)
    return J_style_layer
    
# 测试
tf.random.set_seed(1)
a_S = tf.random.normal([1, 4, 4, 3], mean=1, stddev=4)
a_G = tf.random.normal([1, 4, 4, 3], mean=1, stddev=4)
J_style_layer = compute_layer_style_cost(a_S, a_G)    
print("J_style_layer = " + str(J_style_layer.numpy()))
# J_style_layer = 14.017808


# 计算多层的风格代价
def compute_style_cost(a_Ss, a_Gs):
    """
    Computes the overall style cost from several chosen layers
    
    Arguments:
    a_Ss -- list of 5 tensor representing style of the image S 
    a_Gs -- list of 5 tensor representing style of the image G
    
    Returns: 
    J_style -- tensor representing a scalar value, style cost defined above by equation (2)
    """
    # 初始化
    J_style = 0
    # 累加每一层的代价
    for i in range(5):
        J_style_layer = compute_layer_style_cost(a_Ss[i], a_Gs[i])
        # 每层的权重都设为0.2
        J_style += 0.2 * J_style_layer
    return J_style


# 合起来,得到总的代价函数
# 内容和风格的比例改一改会得到不一样的效果,风格比重太小出来的效果不好
def total_cost(r_G, r_C=r_C, r_S=r_S, alpha=10, beta=40):
    """
    Computes the total cost function
    
    Arguments:
    r_C -- outputs of content image from VGG19_model
    r_S -- outputs of style image from VGG19_model
    r_G -- outputs of generated image from VGG19_model
    alpha -- hyperparameter weighting the importance of the content cost
    beta -- hyperparameter weighting the importance of the style cost
    
    Returns:
    J -- total cost as defined by the formula above.
    """
    a_C = r_C[-1]
    a_G = r_G[-1]
    a_Ss = r_S[0:5]
    a_Gs = r_G[0:5]
    # 内容代价
    J_content = compute_content_cost(a_C, a_G)
    # 风格代价
    J_style = compute_style_cost(a_Ss, a_Gs)
    # 总的
    J = alpha * J_content + beta * J_style
    return J


''' ============优化迭代得到生成图片==========='''
# 导入内容图片C
C = plt.imread("image/louvre_small.jpg")
plt.imshow(C)
C = reshape_and_normalize_image(C)

# 导入风格图片S
S = plt.imread("image/sandstone.jpg")
plt.imshow(S)
S = reshape_and_normalize_image(S)

# 根据内容图片加噪音生成初始化的生成图片
G = generate_noise_image(C)
plt.imshow(G[0])

# 将G设置为可训练的参数,在每次迭代中更新每个像素
G = tf.Variable(G, dtype = tf.float32)
# 得到目标内容以及目标风格的数值,用来传入total_cost计算总代价
r_C = VGG19_model(C) # 长度为6的元组,每个元素是一个tensor
r_S = VGG19_model(S)

# 采用Adam优化器
optimizer = tf.keras.optimizers.Adam(lr=2)

# 用tf.GradientTape自动求微分(需要运行一段时间)
for i in range(20):
    with tf.GradientTape() as tape:
        r_G = VGG19_model(G)
        J = total_cost(r_G)
    # 得到梯度dJ/dG
    dG = tape.gradient(J, G)
    # 将梯度和变量传入优化器
    optimizer.apply_gradients(grads_and_vars=[(dG, G)])
    # 打印信息并保存图片
    if i % 2 == 0:
        print("Total cost of iteration " + str(i) + " :" + str(J.numpy()))
        save_image("output_nst/" + str(i) + ".png", G)

# 保存最后得到的图片G
save_image("output_nst/generated_image.png", G)
plt.imshow(plt.imread("output_nst/generated_image.png"))


''' ============按照自己理解来改一下==========='''
# 上面是按照作业中给出的接口来处理图片的,感觉代码不是很正确,所以按照自己的想法改了下
# 导入内容图片C
C = plt.imread("image/louvre_small.jpg")
C = C/255.0
C = C.reshape(1, 300, 400, 3)
plt.imshow(C[0])

# 导入风格图片S
S = plt.imread("image/Starry.jpg")
S = resize(S, (300,400,3))
S = S.reshape(1, 300, 400, 3)
plt.imshow(S[0])

# 直接用内容图片来初始化G,并将G设置为可训练的参数,在每次迭代中更新每个像素
G = tf.Variable(C, dtype = tf.float32)
# 得到目标内容以及目标风格的数值,用来传入total_cost计算总代价
r_C = VGG19_model(C) # 长度为6的元组,每个元素是一个tensor
r_S = VGG19_model(S)

# 采用Adam优化器
optimizer = tf.keras.optimizers.Adam(lr=0.05)

# 用tf.GradientTape自动求微分(需要运行一段时间)
for i in range(20):
    with tf.GradientTape() as tape:
        r_G = VGG19_model(G)
        J = total_cost(r_G)
    # 得到梯度dJ/dG
    dG = tape.gradient(J, G)
    # 将梯度和变量传入优化器
    optimizer.apply_gradients(grads_and_vars=[(dG, G)])
    # 打印信息并保存图片
    print("Total cost of iteration " + str(i) + " :" + str(J.numpy()))
    # 把每次更新后的图片都规定在0-1之间
    G.assign(tf.clip_by_value(G, 0, 1))  

plt.imshow(G[0])
# 保存最后得到的图片G
plt.imsave("output_nst/generated_image2.png", G[0].numpy())

以下分别是内容图片C、风格图片S、生成图片G:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

效果不是特别好,尤其是用作业原本的函数来处理图片时效果更差。我想可能使用更厉害的模型而不是VGG可能效果会更好,而且由于电脑配置太差,只能用CPU训练,如果用GPU跑可以增加训练次数应该效果会更好。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值