handson-tl-py-merge-2

61 阅读23分钟

Python 迁移学习实用指南(三)

原文:Hands-On Transfer Learning with Python

协议:CC BY-NC-SA 4.0

八、音频事件识别与分类

在前面的章节中,我们已经研究了一些非常有趣的案例研究,这些案例将迁移学习应用于实际问题。 图像和文本数据是我们先前已解决的两种非结构化数据形式。 我们已经展示了各种方法来应用迁移学习来获得更强大和更出色的模型,以及解决诸如缺少训练数据之类的约束。 在本章中,我们将解决识别和分类音频事件的新现实问题。

为音频数据创建预训练的深度学习模型是一个巨大的挑战,因为我们没有高效的预训练的视觉模型(例如 VGG 或 Inception(适用于图像数据)或基于词嵌入的模型(如 Word2vec 或 GloVe)的优势) 文本数据)。 然后可能会出现一个问题,那就是我们对音频数据的策略是什么。 我们将在本章中探索一些创新方法,敬请期待! 本章将涵盖以下主要方面:

  • 了解音频事件分类
  • 制定我们的现实问题
  • 探索性音频事件分析
  • 特征工程和音频事件的表示
  • 使用迁移学习的音频事件分类
  • 构建深度学习音频事件识别器

在本章中,我们将研究识别和分类音频事件的实际案例研究。 诸如音频特征工程,转换学习,深度学习和面向对象编程等概念将用于构建健壮的,自动化的音频事件标识符以进行分类。 您可以在 GitHub 存储库中的Chapter 8文件夹中快速阅读本章的代码。 可以根据需要参考本章。

了解音频事件分类

到现在为止,您应该了解分类或分类的基本任务,在这里我们已经有了结构化或非结构化的数据,这些数据通常用特定的组或类别进行标记或标注。 自动分类的主要任务是建立一个模型,以便使用未来的数据点,我们可以根据各种数据属性或特征将每个数据点分类或记录为一种特定的类别。

在前面的章节中,我们已经研究了文本和图像的分类。 在本章中,我们将研究对音频事件进行分类。 音频事件基本上是通常由音频信号捕获的事件或活动的发生。 通常,短的音频片段用于表示音频事件,因为即使它们反复出现,声音通常也很相似。 但是,有时,可能会使用更长的音频剪辑来表示更复杂的音频事件。 音频事件的示例可能是儿童在操场上玩耍,警笛警报,狗吠等。 实际上,谷歌已经建立了一个名为 AudioSet 的海量数据集,它是带标注的音频事件的一个手动的大规模数据集,他们还发表了几篇有关音频事件识别和分类的论文。 我们将使用较小的数据集来解决问题,但有兴趣的读者一定应该查看这个庞大的数据集,其中包含 632 个音频事件类,其中包括从 YouTube 视频中提取的 208420 个人工标记的 10 秒声音剪辑的集合。

制定我们的现实问题

我们这里的实际案例研究的主要目标是音频事件的识别和分类。 这是一个监督学习问题,我们将在音频事件数据集上使用属于特定类别(它们是声音的来源)的音频数据样本进行处理。

我们将利用迁移学习和深度学习中的概念来构建可靠的分类器,从而在任何给定音频样本属于我们预定类别之一的情况下,我们都应该能够正确预测该声音的来源。 我们将使用的数据集通常被称为 UrbanSound8K 数据集,并且具有 8,732 个带标签的音频声音文件(其持续时间通常等于或大于 4 秒),其中包含城市常见声音的摘录。 该数据集中的声音的十个类别如下:

  • air_conditioner

  • car_horn

  • children_playing

  • dog_bark

  • drilling

  • engine_idling

  • gun_shot

  • jackhammer

  • siren

  • streen_music

有关此数据集以及其他可能的数据集和计划的详细说明,我们建议读者访问 UrbanSound 网站,并查看创建者 J. Salamon,C。Jacoby 和 JP Bello 的这篇令人惊异的论文,《数据集和城市声音研究分类法》(22 届 ACM 国际多媒体国际会议,2014 年 11 月,美国奥兰多)。我们感谢他们,以及纽约大学城市科学与进步中心CUSP), 现实。

要获取数据,您需要在其网站上填写表格,然后您将通过电子邮件获得下载链接。 解压缩文件后,您应该能够看到十个文件夹(十折)中的所有音频文件,以及一个包含有关数据集更多详细信息的readme文件。

探索性音频事件分析

我们将遵循标准的工作流程,对音频数据进行模型的分析,可视化,建模和评估。 下载完所有数据后,您会注意到总共有十个文件夹包含WAV格式的音频数据样本。 我们还有一个元数据文件夹,其中包含UrbanSound8K.csv文件中每个音频文件的元数据信息。 您可以使用此文件为每个文件分配类标签,也可以了解文件命名术语以进行相同的操作。

每个音频文件都以特定格式命名。 该名称采用[fsID]-[classID]-[occurrenceID]-[sliceID].wav格式,其格式如下:

  • [fsID]:从中摘录该片段(片段)的录音的自由声音 ID
  • [classID]:声音类别的数字标识符
  • [occurrenceID]:数字标识符,用于区分原始录音中声音的不同出现
  • [sliceID]:数字标识符,用于区分同一事件中获取的不同片段

每个类标识符都是一个数字,可以映射到特定的类标签。 我们将在不久的将来对此进行更多的扩展。 让我们从对音频数据的一些基本探索性分析开始。 如果您想自己运行示例,可以从我们的 GitHub 存储库中引用Exploratory Analysis Sound Data.ipynb Jupyter 笔记本。

首先,我们加载以下依赖项,包括librosa模块,如果没有该模块,则可能需要安装:

import glob 
import os 
import librosa 
import numpy as np 
import matplotlib.pyplot as plt 
from matplotlib.pyplot import specgram 
import pandas as pd 
import librosa.display 
import IPython.display 
import soundfile as sf 

%matplotlib inline 

librosa模块是用于音频和音乐分析的出色的开源 Python 框架。 我们建议读者更详细地检查该框架。 在接下来的部分中,我们将使用它来分析音频数据并从中提取特征。 现在让我们加载一个数据文件夹进行分析:

files = glob.glob('UrbanSound8K/audio/fold1/*') 
len(files) 

873 

我们可以看到每个文件夹大致包含 870 多个音频样本。 现在,基于metadatareadme文件的信息,我们可以创建一个类 ID,以名称映射音频样本类别:

class_map = {'0' : 'air_conditioner', '1' : 'car_horn',  
             '2' : 'children_playing', '3' : 'dog_bark',  
             '4' : 'drilling', '5' : 'engine_idling',  
             '6' : 'gun_shot', '7' : 'jackhammer',  
             '8' : 'siren', '9' : 'street_music'} 
pd.DataFrame(sorted(list(class_map.items()))) 

现在让我们从属于这些类别的每个类别中抽取十个不同的音频样本,以进行进一步分析:

samples = [(class_map[label], 
           [f for f in files if f.split('-')[1] == label][0]) 
                                       for label in class_map.keys()] 
samples 

[('street_music', 'UrbanSound8K/audio/fold1\108041-9-0-11.wav'), 
 ('engine_idling', 'UrbanSound8K/audio/fold1\103258-5-0-0.wav'), 
 ('jackhammer', 'UrbanSound8K/audio/fold1\103074-7-0-0.wav'), 
 ('air_conditioner', 'UrbanSound8K/audio/fold1\127873-0-0-0.wav'), 
 ('drilling', 'UrbanSound8K/audio/fold1\14113-4-0-0.wav'), 
 ('children_playing', 'UrbanSound8K/audio/fold1\105415-2-0-1.wav'), 
 ('gun_shot', 'UrbanSound8K/audio/fold1\102305-6-0-0.wav'), 
 ('siren', 'UrbanSound8K/audio/fold1\106905-8-0-0.wav'), 
 ('car_horn', 'UrbanSound8K/audio/fold1\156194-1-0-0.wav'), 
 ('dog_bark', 'UrbanSound8K/audio/fold1\101415-3-0-2.wav')] 

现在我们有了示例数据文件,在执行任何分析之前,我们仍然需要将音频数据读入内存。 我们注意到librosa对某些音频文件抛出了错误(因为它们的长度或采样率很短)。 因此,我们利用soundfile Python 框架读取音频文件,以获取其原始数据和原始采样率。 您可以在此处获取有关soundfile框架的更多信息

音频采样率定义为每秒传输的音频采样数,通常以 Hz 或 kHz(1 kHz 为 1,000 Hz)为单位。 librosa的默认采样率为 22,050 Hz,这是我们将重新采样所有音频数据以保持一致性的方式。 以下代码可帮助我们读取数据,并显示原始音频数据的总长度:

def get_sound_data(path, sr=22050): 
data, fsr = sf.read(path) 
data_22k = librosa.resample(data.T, fsr, sr) 
if len(data_22k.shape) > 1: 
    data_22k = np.average(data_22k, axis=0) 
    return data_22k, sr 

sample_data = [(sample[0], get_sound_data(sample[1])) for sample in 
                samples] 
[(sample[0], sample[1][0].shape) for sample in sample_data] 

[('street_music', (88200,)), ('engine_idling', (88200,)), 
 ('jackhammer', (88200,)), ('air_conditioner', (44982,)), 
 ('drilling', (88200,)), ('children_playing', (88200,)), 
 ('gun_shot', (57551,)), ('siren', (88200,)), 
 ('car_horn', (5513,)), ('dog_bark', (88200,))] 

很明显,大多数音频采样的持续时间约为四秒钟,但有些采样的持续时间却很短。 Jupyter 笔记本的魅力在于,您甚至可以将音频嵌入笔记本本身,并使用以下片段播放它。

对于sample_data中的数据:

print(data[0], ':') 
IPython.display.display(IPython.display.Audio(data=data[1[0],rate=data[                                              1][1]))

这将创建以下内容:

现在让我们通过绘制它们的波形来形象化这些不同的音频源的外观。 通常,这将是每个音频样本的波形幅度图:

i = 1 
fig = plt.figure(figsize=(15, 6)) 
for item in sample_data: 
    plt.subplot(2, 5, i) 
    librosa.display.waveplot(item[1][0], sr=item[1][1], color='r',  
                             alpha=0.7) 
    plt.title(item[0]) 
    i += 1 
plt.tight_layout() 

创建的图将如下所示:

您可以在上图中清楚地看到不同的音频数据样本及其源标签和相应的音频波形图。 这描绘了一些有趣的见解。 engine_idlingjackhammerair_conditioner等音源通常具有恒定的声音,不会随时间变化。 因此,您可以注意到波形中的振幅恒定。 sirencar_horn通常也具有恒定的音频波形,并具有间歇性的幅度增加。gun_shot通常在开始时会发出很大的声音,然后保持沉默。 dog_bark间歇地进入。 因此,除了静音以外,声音还具有短的高振幅间隔。 您还能找到更多有趣的模式吗?

音频数据的另一种有趣的可视化技术是声谱图。 通常,声谱图是一种视觉表示技术,用于表示音频数据中的频谱。 它们也被普遍称为超声检查仪语音图。 让我们将音频样本可视化为频谱图:

i = 1 
fig = plt.figure(figsize=(15, 6)) 

for item in sample_data: 
    plt.subplot(2, 5, i) 
    specgram(item[1][0], Fs=item[1][1]) 
    plt.title(item[0]) 
    i += 1 
plt.tight_layout() 

频谱图显示如下:

我们可以看到如何用频谱图将音频数据表示为很好的图像表示形式,这对于像卷积神经网络CNN)这样的模型很有用,因为可以肯定地看到不同音频源在声谱图中存在明显差异。 但是,我们将使用梅尔谱图,它通常比基本谱图更好,因为它代表了梅尔刻度的谱图。 名称 mel 来自单词 melody。 这表明比例尺基于音高比较。 因此,梅尔音阶是对音高的感知尺度,听众已将其判断为彼此之间的距离相等。 如果我们使用 CNN 从这些频谱图中提取特征,这将非常有用。 以下代码段描绘了梅尔频谱图:

i = 1 
fig = plt.figure(figsize=(15, 6)) 
for item in sample_data: 
    plt.subplot(2, 5, i) 
    S = librosa.feature.melspectrogram(item[1][0], sr=item[1]   
    [1],n_mels=128) 
    log_S = librosa.logamplitude(S) 
    librosa.display.specshow(log_S, sr=item[1][1],   
    x_axis='time',y_axis='mel') 
    plt.title(item[0]) 
    plt.colorbar(format='%+02.0f dB') 
    i += 1 
plt.tight_layout()

梅尔频谱图显示如下:

我们可以看到,借助梅尔音阶,可以更容易地根据音频源来区分频谱图。 现在,让我们集中讨论下一节中将用作特征工程基础资源的一些特定视觉技术。 首先,让我们看一下gun_shot音频样本作为梅尔频谱图的样子:

y = sample_data[6][1][0] 
S = librosa.feature.melspectrogram(y, sr=22050, n_mels=128) 
log_S = librosa.logamplitude(S) 
plt.figure(figsize=(12,4)) 
librosa.display.specshow(log_S, sr=22050, x_axis='time', y_axis='mel') 
plt.colorbar(format='%+02.0f dB') 

频谱图显示如下:

频谱图与该音频源的音频波形图一致。 音频的另一个有趣方面是,通常任何音频时间序列数据都可以分解为谐波和打击乐成分。 这些可以呈现任何音频样本的全新有趣的表示形式。 让我们获取这些组件并将它们绘制成频谱图:

y_harmonic, y_percussive = librosa.effects.hpss(y) 
S_harmonic   = librosa.feature.melspectrogram(y_harmonic,sr=22050, 
                                              n_mels=128) 
S_percussive = librosa.feature.melspectrogram(y_percussive,sr=22050) 
log_Sh = librosa.power_to_db(S_harmonic) 
log_Sp = librosa.power_to_db(S_percussive) 

# Make a new figure 
plt.figure(figsize=(12,6)) 
plt.subplot(2,1,1) 
librosa.display.specshow(log_Sh, sr=sr, y_axis='mel') 
plt.title('mel power spectrogram (Harmonic)') 
plt.colorbar(format='%+02.0f dB') 
plt.subplot(2,1,2) 
librosa.display.specshow(log_Sp, sr=sr, x_axis='time', y_axis='mel') 
plt.title('mel power spectrogram (Percussive)') 
plt.colorbar(format='%+02.0f dB') 
plt.tight_layout() 

频谱图将显示如下:

您可以看到音频样本的两个不同成分显示为两个独特的声谱图,分别描述了谐波成分和打击乐成分。

音频数据的另一个非常有趣的描述是使用一个色谱图,该图显示了基于十二种不同音高类别(即{C, C#, D, D#, E, F, F#, G, G#, A, A#, B}。 这是用于描述音频信号随时间变化的各种音调强度的出色视觉工具。 通常,在构建色谱图之前,会对原始音频信号执行傅立叶变换或 Q 变换:

C = librosa.feature.chroma_cqt(y=y_harmonic, sr=sr) 
# Make a new figure 
plt.figure(figsize=(12, 4)) 
# Display the chromagram: the energy in each chromatic pitch class 
# as a function of time 
librosa.display.specshow(C, sr=sr, x_axis='time', y_axis='chroma', 
                         vmin=0, vmax=1) 
plt.title('Chromagram') 
plt.colorbar() 
plt.tight_layout() 

色谱图将显示如下:

随着时间的推移,我们可以清楚地看到gun_shot音频样本的各种音调强度,这对于作为特征提取的基础图像肯定是有效的。 在下一节中,我们将使用其中一些技术进行特征提取。

特征工程和音频事件的表示

要构建可靠的分类模型,我们需要从原始音频数据中获得可靠且良好的特征表示。 我们将利用上一节中学到的一些技术进行特征工程。 如果您想自己运行示例,可以在Feature Engineering.ipynb Jupyter 笔记本中使用本节中使用的代码段。 我们将重用先前导入的所有库,并在此处利用joblib将特征保存到磁盘:

from sklearn.externals import joblib 

接下来,我们将加载所有文件名,并定义一些工具函数以读取音频数据,还使我们能够获取音频子样本的窗口索引,我们将在短期内利用它们:

# get all file names 
ROOT_DIR = 'UrbanSound8K/audio/' 
files = glob.glob(ROOT_DIR+'/**/*') 

# load raw audio data 
def get_sound_data(path, sr=22050): 
    data, fsr = sf.read(path) 
    data_resample = librosa.resample(data.T, fsr, sr) 
    if len(data_resample.shape) > 1: 
        data_resample = np.average(data_resample, axis=0) 
    return data_resample, sr 

# function to get start and end indices for audio sub-sample 
def windows(data, window_size): 
    start = 0 
    while start < len(data): 
        yield int(start), int(start + window_size) 
        start += (window_size / 2) 

我们将遵循的特征工程策略有些复杂,但是我们将在此处尝试以简洁的方式对其进行说明。 我们已经看到我们的音频数据样本的长度不同。 但是,如果我们要构建一个强大的分类器,则每个样本的特征必须保持一致。 因此,我们将从每个音频文件中提取(固定长度的)音频子样本,并从每个这些子样本中提取特征。

我们将总共使用三种特征工程技术来构建三个特征表示图,这最终将为我们的每个音频子样本提供一个三维图像特征图。 下图描述了我们将采用的工作流程:

这个想法来自 Karol J. Piczak 的出色论文,《具有卷积神经网络的环境声音分类》(IEEE2015)。他将梅尔频谱图用于一般必要的特征,CNN 可以使用这些特征来进行特征提取。 但是,我们已经考虑了对最终特征图的一些其他转换。

第一步是将帧(列)的总数定义为 64 ,将波段(行)的总数定义为 64,这形成了每个特征图的尺寸(64 x 64)。 然后,基于此,我们提取音频数据的窗口,从每个音频数据样本中形成子样本。

考虑每个音频子样本,我们首先创建一个梅尔声谱图。 由此,我们创建了一个对数缩放的梅尔频谱图,作为特征图之一,音频子样本的谐波分量和敲击分量的平均特征图(再次对数缩放),以及对数缩放的 mel 频谱图的增量或导数作为第三特征图。 这些特征图的每一个都可以表示为64 x 64图像,并且通过组合它们,我们可以为每个音频子样本获得尺寸为(64, 64, 3)的 3-D 特征图。 现在,为该工作流程定义函数:

def extract_features(file_names, bands=64, frames=64): 
    window_size = 512 * (frames - 1)   
    log_specgrams_full = [] 
    log_specgrams_hp = [] 
    class_labels = [] 

    # for each audio sample 
    for fn in file_names: 
        file_name = fn.split('\')[-1] 
        class_label = file_name.split('-')[1] 
        sound_data, sr = get_sound_data(fn, sr=22050) 

        # for each audio signal sub-sample window of data 
        for (start,end) in windows(sound_data, window_size): 
            if(len(sound_data[start:end]) == window_size): 
                signal = sound_data[start:end] 

                # get the log-scaled mel-spectrogram 
                melspec_full = librosa.feature.melspectrogram(signal, 
                                                              n_mels = 
                                                                 bands) 
                logspec_full = librosa.logamplitude(melspec_full) 
                logspec_full = logspec_full.T.flatten()[:,np.newaxis].T 

                # get the log-scaled, averaged values for the  
                # harmonic and percussive components 
                y_harmonic, y_percussive =librosa.effects.hpss(signal) 
                melspec_harmonic =  
                         librosa.feature.melspectrogram(y_harmonic,   
                                                        n_mels=bands) 
                melspec_percussive =   
                        librosa.feature.melspectrogram(y_percussive,   
                                                       n_mels=bands) 
                logspec_harmonic = 
                       librosa.logamplitude(melspec_harmonic) 
                logspec_percussive = 
                       librosa.logamplitude(melspec_percussive) 
                logspec_harmonic = logspec_harmonic.T.flatten()[:, 
                                                          np.newaxis].T 
                logspec_percussive = logspec_percussive.T.flatten()[:,  
                                                          np.newaxis].T 
                logspec_hp = np.average([logspec_harmonic,  
                                        logspec_percussive],  
                                        axis=0) 
                log_specgrams_full.append(logspec_full) 
                log_specgrams_hp.append(logspec_hp) 
                class_labels.append(class_label) 

    # create the first two feature maps             
    log_specgrams_full = np.asarray(log_specgrams_full).reshape( 
                                        len(log_specgrams_full), bands,  
                                        frames, 1) 
    log_specgrams_hp = np.asarray(log_specgrams_hp).reshape( 
                                        len(log_specgrams_hp), bands,   
                                        frames, 1) 
    features = np.concatenate((log_specgrams_full,  
                               log_specgrams_hp,                                  
                               np.zeros(np.shape( 
                                      log_specgrams_full))),  
                               axis=3) 

    # create the third feature map which is the delta (derivative)    
    # of the log-scaled mel-spectrogram 
    for i in range(len(features)): 
        features[i, :, :, 2] = librosa.feature.delta(features[i,   
                                                              :, :, 0]) 
    return np.array(features), np.array(class_labels, dtype = np.int) 

现在我们准备使用此函数。 我们将在前面的工作流程中讨论的策略基础上,将其用于所有 8,732 音频样本,以从该数据的许多子样本中创建特征图。

features, labels = extract_features(files) 
features.shape, labels.shape 
((30500, 64, 64, 3), (30500,)) 

我们从 8,732 个音频数据文件中总共获得了 30,500 个特征图。 这非常好,并且正如我们前面所讨论的,每个特征图都是尺寸(64, 64, 3)。 现在,基于以下 30,500 个数据点,查看音频源的整体类表示形式:

from collections import Counter 
Counter(labels) 
Counter({0: 3993, 1: 913, 2: 3947, 3: 2912, 4: 3405, 
         5: 3910, 6: 336, 7: 3473, 8: 3611, 9: 4000}) 

我们可以看到,不同类别中数据点的总体分布是相当均匀和适当的。 对于诸如 1(car_horn)和 6(gun_shot)的某些类别,表示与其他类别相比非常低; 这是可以预期的,因为这些类别的音频数据持续时间通常比其他类别要短得多。 现在让我们继续可视化这些特征图:

class_map = {'0' : 'air_conditioner', '1' : 'car_horn', '2' :  
            'children_playing','3' : 'dog_bark', '4' : 'drilling','5' : 
            'engine_idling','6' : 'gun_shot', '7' : 'jackhammer', '8' :  
            'siren', '9' : 'street_music'} 
categories = list(set(labels)) 
sample_idxs = [np.where(labels == label_id)[0][0] for label_id in 
               categories] 
feature_samples = features[sample_idxs] 

plt.figure(figsize=(16, 4)) 
for index, (feature_map, category) in enumerate(zip(feature_samples,  
                                                    categories)): 
    plt.subplot(2, 5, index+1) 
    plt.imshow(np.concatenate((feature_map[:,:,0],  
                               feature_map[:,:,1],    
                               feature_map[:,:,2]),  
                               axis=1),
                               cmap='viridis') 
    plt.title(class_map[str(category)]) 
plt.tight_layout() 
t = plt.suptitle('Visualizing Feature Maps for Audio Clips') 

特征图将显示如下:

上图向我们展示了每个音频类别的一些示例特征图看起来是什么样的,并且显而易见的是,每个特征图都是三维图像。 现在,我们将这些基本特征保存到磁盘:

joblib.dump(features, 'base_features.pkl') 
joblib.dump(labels, 'dataset_labels.pkl') 

这些基本特征将作为下一部分进一步特征设计的起点,在此我们将释放迁移学习的真正力量。

使用迁移学习的音频事件分类

现在,我们准备开始构建音频事件分类器。 我们有基本的特征图,但仍然需要做更多的特征工程。 您始终可以从头开始构建 CNN 以摄取这些图像,然后将其连接到完全连接的深多层感知器MLP)来构建分类器。 但是,在这里,我们将通过使用一种预训练的模型进行特征提取来利用迁移学习的力量。 更具体地说,我们将使用 VGG-16 模型作为特征提取器,然后在这些特征上训练完全连接的深度网络。

从基本特征构建数据集

第一步是加载基本特征,并创建训练,验证和测试数据集。 为此,我们需要从磁盘加载基本特征和标签:

features = joblib.load('base_features.pkl') 
labels = joblib.load('dataset_labels.pkl') 
data = np.array(list(zip(features, labels))) 
features.shape, labels.shape 

((30500, 64, 64, 3), (30500,)) 

现在,我们将随机整理数据并创建训练,验证和测试数据集:

np.random.shuffle(data) 
train, validate, test = np.split(data, [int(.6*len(data)),int(.8*len(data))]) 
train.shape, validate.shape, test.shape 

((18300, 2), (6100, 2), (6100, 2)) 

最后,我们还可以使用以下代码段检查每个数据集中的每类分布:

print('Train:', Counter(item[1] for item in train),'nValidate:', Counter(item[1] for item in validate),'nTest:',Counter(item[1] for item 
        in test)) 

Train: Counter({9: 2448, 2: 2423, 0: 2378, 5: 2366, 8: 2140,  
                7: 2033, 4: 2020, 3: 1753, 1: 542, 6: 197})  
Validate: Counter({0: 802, 5: 799, 2: 774, 9: 744, 8: 721,  
                   7: 705, 4: 688, 3: 616, 1: 183, 6: 68})  
Test: Counter({0: 813, 9: 808, 2: 750, 8: 750, 5: 745, 7: 735,  
               4: 697, 3: 543, 1: 188, 6: 71}) 

因此,我们可以看到整个数据集中每个类的数据点一致且均匀地分布。

迁移学习以进行特征提取

现在来了有趣的部分。 我们准备利用迁移学习从基本特征映射图中为每个数据点提取有用的特征。 为此,我们将使用出色的预训练深度学习模型,该模型已被证明是图像上非常有效的特征提取器。 我们将在这里使用 VGG-16 模型。 但是,我们将在这里使用它作为简单的特征提取器,而无需进行任何微调(这是我们在前几章中探讨的内容)。

随意利用微调,这甚至可以带来更好的分类器。 我们首先定义一些基本的工具和函数来处理基本图像:

from keras.preprocessing import image 
from keras.applications.imagenet_utils import preprocess_input 
from PIL import Image 
def process_sound_data(data): 
    data = np.expand_dims(data, axis=0) 
    data = preprocess_input(data) 
    return data 

现在,我们将加载 VGG-16 模型,但仅作为特征提取器。 因此,我们最终将不会使用其密集层:

from keras.applications import vgg16 
from keras.models import Model 
import keras 
vgg = vgg16.VGG16(include_top=False, weights='imagenet',input_shape=
                  (64, 64, 3)) 
output = vgg.layers[-1].output 
output = keras.layers.Flatten()(output) 
model = Model(vgg.input, output) 
model.trainable = False 
model.summary() 

_________________________________________________________________ 
Layer (type)                 Output Shape              Param #    
================================================================= 
input_2 (InputLayer)         (None, 64, 64, 3)         0          
_________________________________________________________________ 
block1_conv1 (Conv2D)        (None, 64, 64, 64)        1792       
_________________________________________________________________ 
block1_conv2 (Conv2D)        (None, 64, 64, 64)        36928      
_________________________________________________________________ 
... 
... 
_________________________________________________________________ 
block5_conv3 (Conv2D)        (None, 4, 4, 512)         2359808    
_________________________________________________________________ 
block5_pool (MaxPooling2D)   (None, 2, 2, 512)         0          
_________________________________________________________________ 
flatten_2 (Flatten)          (None, 2048)              0          
================================================================= 
Total params: 14,714,688 
Trainable params: 0 
Non-trainable params: 14,714,688 
_________________________________________________________________ 

从前面的模型摘要中可以明显看出,我们输入的基本特征图图像的尺寸为(64, 64, 3),从中我们最终将得到大小为 2,048 的一维特征向量。 让我们构建一个通用函数,以帮助我们利用迁移学习并获得这些特征,这些特征通常被称为瓶颈特征

def extract_tl_features(model, base_feature_data): 
    dataset_tl_features = [] 
    for index, feature_data in enumerate(base_feature_data): 
        if (index+1) % 1000 == 0: 
            print('Finished processing', index+1, 'sound feature maps') 
        pr_data = process_sound_data(feature_data) 
        tl_features = model.predict(pr_data) 
        tl_features = np.reshape(tl_features,  
                                 tl_features.shape[1]) 
        dataset_tl_features.append(tl_features) 
    return np.array(dataset_tl_features) 

现在可以将此函数与我们的 VGG-16 模型一起使用,以从我们的每个音频子样本基本特征图图像中提取有用的特征。 我们将对所有数据集执行此操作:

# extract train dataset features 
train_base_features = [item[0] for item in train] 
train_labels = np.array([item[1] for item in train]) 
train_tl_features = extract_tl_features(model=model,  
                            base_feature_data=train_base_features) 

# extract validation dataset features 
validate_base_features = [item[0] for item in validate] 
validate_labels = np.array([item[1] for item in validate]) 
validate_tl_features = extract_tl_features(model=model,  
                         base_feature_data=validate_base_features) 

# extract test dataset features 
test_base_features = [item[0] for item in test] 
test_labels = np.array([item[1] for item in test]) 
test_tl_features = extract_tl_features(model=model,  
                        base_feature_data=test_base_features) 

train_tl_features.shape, validate_tl_features.shape, test_tl_features.shape 

((18300, 2048), (6100, 2048), (6100, 2048)) 

现在,我们可以将这些特征和标签保存到磁盘上,以便以后可以随时用于构建分类器,而不必依赖于始终保持笔记本计算机处于打开状态:

joblib.dump(train_tl_features, 'train_tl_features.pkl') 
joblib.dump(train_labels, 'train_labels.pkl') 
joblib.dump(validate_tl_features, 'validate_tl_features.pkl') 
joblib.dump(validate_labels, 'validate_labels.pkl') 
joblib.dump(test_tl_features, 'test_tl_features.pkl') 
joblib.dump(test_labels, 'test_labels.pkl') 

建立分类模型

现在,我们准备在上一节中提取的特征上构建分类模型。 如果您想自己运行示例,可以在Modeling.ipynb Jupyter 笔记本中使用此部分的代码。 首先,让我们加载一些基本的依赖项:

from sklearn.externals import joblib 
import keras 
from keras import models 
from keras import layers 
import model_evaluation_utils as meu 
import matplotlib.pyplot as plt 

%matplotlib inline 

我们将使用名为model_evaluation_utils的漂亮模型评估工具模块来评估我们的分类器并稍后测试其表现。 现在让我们加载特征集和数据点类标签:

train_features = joblib.load('train_tl_features.pkl') 
train_labels = joblib.load('train_labels.pkl') 
validation_features = joblib.load('validate_tl_features.pkl') 
validation_labels = joblib.load('validate_labels.pkl') 
test_features = joblib.load('test_tl_features.pkl') 
test_labels = joblib.load('test_labels.pkl') 
train_features.shape, validation_features.shape, test_features.shape 

((18300, 2048), (6100, 2048), (6100, 2048)) 

train_labels.shape, validation_labels.shape, test_labels.shape 

((18300,), (6100,), (6100,)) 

因此,我们可以看到我们所有的特征集和相应的标签均已加载。 输入特征集是从上一节中使用的 VGG-16 模型获得的大小为 2,048 的一维向量。 现在,我们需要对分类类标签进行一次热编码,然后才能将其输入到深度学习模型中。 以下代码段可帮助我们实现这一目标:

from keras.utils import to_categorical 
train_labels_ohe = to_categorical(train_labels) 
validation_labels_ohe = to_categorical(validation_labels) 
test_labels_ohe = to_categorical(test_labels) 
train_labels_ohe.shape, validation_labels_ohe.shape, test_labels_ohe.shape 

((18300, 10), (6100, 10), (6100, 10)) 

现在,我们将使用具有四个隐藏层的完全连接的网络来构建深度学习分类器。 我们将使用常见的组件(如丢弃法)来防止过拟合,并使用模型的 Adam 优化器。 以下代码描述了模型架构的详细信息:

model = models.Sequential() 
model.add(layers.Dense(1024, activation='relu', 
          input_shape=(train_features.shape[1],))) 
model.add(layers.Dropout(0.4)) 
model.add(layers.Dense(1024, activation='relu')) 
model.add(layers.Dropout(0.4)) 
model.add(layers.Dense(512, activation='relu')) 
model.add(layers.Dropout(0.5)) 
model.add(layers.Dense(512, activation='relu')) 
model.add(layers.Dropout(0.5)) 
model.add(layers.Dense(train_labels_ohe.shape[1],activation='softmax')) 
model.compile(loss='categorical_crossentropy', 
              optimizer='adam',metrics=['accuracy']) 
model.summary() 

_________________________________________________________________ 
Layer (type)                 Output Shape              Param #    
================================================================= 
dense_1 (Dense)              (None, 1024)              2098176    
_________________________________________________________________ 
dropout_1 (Dropout)          (None, 1024)              0          
_________________________________________________________________ 
dense_2 (Dense)              (None, 1024)              1049600    
_________________________________________________________________ 
dropout_2 (Dropout)          (None, 1024)              0          
_________________________________________________________________ 
dense_3 (Dense)              (None, 512)               524800     
_________________________________________________________________ 
dropout_3 (Dropout)          (None, 512)               0          
_________________________________________________________________ 
dense_4 (Dense)              (None, 512)               262656     
_________________________________________________________________ 
dropout_4 (Dropout)          (None, 512)               0          
_________________________________________________________________ 
dense_5 (Dense)              (None, 10)                5130       
================================================================= 

Total params: 3,940,362 
Trainable params: 3,940,362 
Non-trainable params: 0 

然后,在 AWS p2.x 实例上对该模型进行了约 50 个周期的训练,批量大小为 128。 您可以尝试使用时间和批量大小来获得可靠的模型,如下所示:

history = model.fit(train_features, train_labels_ohe,epochs=50, 
                    batch_size=128, 
                    validation_data=(validation_features,  
                    validation_labels_ohe),shuffle=True, verbose=1) 
Train on 18300 samples, validate on 6100 samples 
Epoch 1/50 
18300/18300 - 2s - loss: 2.7953 - acc: 0.3959 - val_loss: 1.0665 - val_acc: 0.6675 
Epoch 2/50 
18300/18300 - 1s - loss: 1.1606 - acc: 0.6211 - val_loss: 0.8179 - val_acc: 0.7444 
... 
... 
Epoch 48/50 
18300/18300 - 1s - loss: 0.2753 - acc: 0.9157 - val_loss: 0.4218 - val_acc: 0.8797 
Epoch 49/50 
18300/18300 - 1s - loss: 0.2813 - acc: 0.9142 - val_loss: 0.4220 - val_acc: 0.8810 
Epoch 50/50 
18300/18300 - 1s - loss: 0.2631 - acc: 0.9197 - val_loss: 0.3887 - val_acc: 0.8890 

我们获得的验证准确率接近 89%,这非常好,看起来很有希望。 我们还可以绘制模型的整体精度图和损耗图,以更好地了解事物的外观,如下所示:

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) 
t = f.suptitle('Deep Neural Net Performance', fontsize=12) 
f.subplots_adjust(top=0.85, wspace=0.2) 
epochs = list(range(1,51)) 
ax1.plot(epochs, history.history['acc'], label='Train Accuracy') 
ax1.plot(epochs, history.history['val_acc'], label='Validation Accuracy') 
ax1.set_ylabel('Accuracy Value') 
ax1.set_xlabel('Epoch') 
ax1.set_title('Accuracy') 
l1 = ax1.legend(loc="best") 
ax2.plot(epochs, history.history['loss'], label='Train Loss') 
ax2.plot(epochs, history.history['val_loss'], label='Validation Loss') 
ax2.set_ylabel('Loss Value') 
ax2.set_xlabel('Epoch') 
ax2.set_title('Loss') 
l2 = ax2.legend(loc="best") 

这将创建以下图:

我们可以看到模型在训练和验证之间的损失和准确率是相当一致的。 也许略有过拟合,但考虑到它们之间的差异很小,可以忽略不计。

评估分类器表现

从字面上看,现在该对我们的模型进行测试了。 我们将使用测试数据集对模型进行预测,然后根据基本事实标签对它们进行评估。 为此,我们首先需要使用以下代码片段获取测试数据对模型的预测,并从数字标签到实际文本标签进行反向映射:

predictions = model.predict_classes(test_features) 
class_map = {'0' : 'air_conditioner', '1' : 'car_horn',  
             '2' : 'children_playing', '3' : 'dog_bark',  
             '4' : 'drilling', '5' : 'engine_idling',  
             '6' : 'gun_shot', '7' : 'jackhammer',  
             '8' : 'siren', '9' : 'street_music'} 
test_labels_categories = [class_map[str(label)]for label in 
                          test_labels] 
prediction_labels_categories = [class_map[str(label)]for label in 
                 predictions] category_names = list(class_map.values()) 

现在让我们使用model_evaluation_utils模块来根据测试数据评估模型的表现。 我们首先获得总体表现指标:

meu.get_metrics(true_labels=test_labels_categories,  
                predicted_labels=prediction_labels_categories) 

Accuracy: 0.8869 
Precision: 0.8864 
Recall: 0.8869 
F1 Score: 0.8861 

我们获得了总体模型准确率,并且f1-score接近 89%,这非常好,并且与我们从验证数据集中获得的一致。 接下来让我们看一下每类模型的表现:

meu.display_classification_report(true_labels=test_labels_categories,     
                       predicted_labels=prediction_labels_categories,
                       classes=category_names) 

                  precision    recall  f1-score   support 

        car_horn       0.87      0.73      0.79       188 
           siren       0.95      0.94      0.94       750 
        drilling       0.88      0.93      0.90       697 
        gun_shot       0.94      0.94      0.94        71 
children_playing       0.83      0.79      0.81       750 
 air_conditioner       0.89      0.94      0.92       813 
      jackhammer       0.92      0.93      0.92       735 
   engine_idling       0.94      0.95      0.95       745 
        dog_bark       0.87      0.83      0.85       543 
    street_music       0.81      0.81      0.81       808 

     avg / total       0.89      0.89      0.89      6100 

这使我们可以更清楚地了解模型确实运行良好以及可能遇到问题的确切类。 大多数类似乎运行良好,尤其是设备声音,例如gun_shotjackhammerengine_idling等。 似乎street_musicchildren_playing最麻烦。

混淆矩阵可以帮助我们了解最可能发生错误分类的地方,并帮助我们更好地理解这一点:

meu.display_confusion_matrix_pretty(true_labels=test_labels_categories,  
                         predicted_labels=prediction_labels_categories,
                         classes=category_names) 

矩阵将显示如下:

从矩阵的对角线看,我们可以看到大多数模型预测都是正确的,这非常好。 关于错误分类,我们可以看到,属于street_musicdog_barkchildren_playing的许多样本彼此之间都被错误分类了,考虑到所有这些事件都是在公开场合和外部发生的,一种预期的他们有可能一起发生。 对于drillingjackhammer也是一样。 幸运的是,gun_shotchildren_playing之间的错误分类几乎没有重叠。

因此,在这个复杂的案例研究中,我们可以看到有效的迁移学习是如何工作的,在该案例中,我们利用图像分类器帮助我们构建了强大而有效的音频事件分类器。 现在,我们可以使用以下代码保存此模型以供将来使用:

model.save('sound_classification_model.h5') 

您现在可能会认为这很好。 但是,我们在静态数据集上进行了所有操作。 我们将如何在现实世界中使用此模型进行音频事件识别和分类? 我们将在下一节中讨论策略。

构建深度学习音频事件识别器

现在,我们将研究一种策略,利用该策略,我们可以利用上一节中构建的分类模型来构建实际的音频事件标识符。 这将使我们能够利用本章中定义的整个工作流程来获取任何新的音频文件,并预测该文件可能属于的类别,从构建基本特征图开始,使用 VGG-16 模型提取特征,然后利用我们的分类模型做出预测。 如果您想自己运行示例,可以在Prediction Pipeline.ipynb Jupyter 笔记本中使用本节中使用的代码段。 笔记本包含AudioIdentifier类,该类是通过重用本章前面各节中构建的所有组件而创建的。 请参阅笔记本以访问该类的完整代码,因为我们将更加关注实际的预测流水线,以使内容更加简洁。 我们将通过为类的实例提供分类模型的路径来初始化它:

ai = 
 AudioIdentifier(prediction_model_path='sound_classification_model.h5')

现在,我们已经下载了十个音频类别中的三个的三个全新的音频数据文件。 让我们加载它们,以便我们可以在它们上测试模型的表现:

siren_path = 'UrbanSound8K/test/sirenpolice.wav' 
gunshot_path = 'UrbanSound8K/test/gunfight.wav' 
dogbark_path = 'UrbanSound8K/test/dog_bark.wav' 
siren_audio, siren_sr = ai.get_sound_data(siren_path) 
gunshot_audio, gunshot_sr = ai.get_sound_data(gunshot_path) 
dogbark_audio, dogbark_sr = ai.get_sound_data(dogbark_path) 
actual_sounds = ['siren', 'gun_shot', 'dog_bark'] 
sound_data = [siren_audio, gunshot_audio, dogbark_audio] 
sound_rate = [siren_sr, gunshot_sr, dogbark_sr] 
sound_paths = [siren_path, gunshot_path, dogbark_path] 

让我们可视化这三个音频文件的波形,并了解它们的结构:

i = 1 
fig = plt.figure(figsize=(12, 3.5)) 
t = plt.suptitle('Visualizing Amplitude Waveforms for Audio Clips', 
                  fontsize=14) 
fig.subplots_adjust(top=0.8, wspace=0.2) 

for sound_class, data, sr in zip(actual_sounds, sound_data,sound_rate): 
    plt.subplot(1, 3, i) 
    librosa.display.waveplot(data, sr=sr, color='r', alpha=0.7) 
    plt.title(sound_class) 
    i += 1 
plt.tight_layout(pad=2.5) 

可视化效果如下所示:

基于可视化,基于音频源,它们似乎是一致的,到目前为止,我们的流水线运行良好。 现在,我们为这些音频文件提取基本特征图:

siren_feature_map = ai.extract_base_features(siren_audio)[0] 
gunshot_feature_map = ai.extract_base_features(gunshot_audio)[0] 
dogbark_feature_map = ai.extract_base_features(dogbark_audio)[0] 
feature_maps = [siren_feature_map, gunshot_feature_map,dogbark_feature_map] 
plt.figure(figsize=(14, 3)) 
t = plt.suptitle('Visualizing Feature Maps for Audio 
                  Clips',fontsize=14) 
fig.subplots_adjust(top=0.8, wspace=0.1) 

for index, (feature_map, category) in 
  enumerate(zip(feature_maps,actual_sounds)): 
    plt.subplot(1, 3, index+1) 
    plt.imshow(np.concatenate((feature_map[:,:,0],  
                               feature_map[:,:,1],   
                               feature_map[:,:,2]), axis=1),
                               cmap='viridis')         
plt.title(category) 
plt.tight_layout(pad=1.5) 

特征图将显示如下:

根据我们在训练阶段观察到的图像,图像特征图看起来非常一致。 现在,我们可以利用我们的预测流水线来预测每种声音的音频源类别:

predictions = 
   [ai.prediction_pipeline(audiofile_path,return_class_label=True)  
                   for audiofile_path in sound_paths] 
result_df = pd.DataFrame({'Actual Sound': actual_sounds, 
                          'Predicted Sound': predictions, 
                          'Location': sound_paths}) 
result_df 

我们得出以下预测:

看起来我们的模型能够正确识别所有这些音频样本。 我们鼓励您检查笔记本中的AudioIdentifier类,以了解我们如何在后台实现预测流水线。 我们利用了在本章中学到的所有概念来构建此流水线。

总结

在本章中,我们研究了一个全新的问题和案例研究,涉及音频识别和分类。 涵盖了围绕音频数据和信号的概念,包括可视化和理解此数据类型的有效技术。

我们还研究了有效的特征工程技术,以及如何使用迁移学习从音频数据的图像表示中提取有效特征。 这向我们展示了迁移学习的希望,以及如何利用知识从一个领域(图像)迁移到另一个领域(音频),并建立一个非常强大且有效的分类器。 最后,我们建立了一个完整的端到端流水线,用于识别和分类音频数据的新样本。 请在网络上进一步检查带标注的音频的数据集,看看是否可以利用从此处学习的迁移学习中获得的概念来构建更大,更好的音频标识符和分类器。 请继续关注有关迁移学习的更多有趣示例和案例研究。

九、DeepDream

本章重点介绍了生成型深度学习的领域,这已成为真正的人工智能AI)最前沿的核心思想之一。 我们将关注卷积神经网络CNN)如何利用迁移学习来思考或可视化图像中的图案。 它们可以生成描述这些卷积网络思维甚至梦境方式之前从未见过的图像模式! DeepDream 于 2015 年由 Google 首次发布,由于深层网络开始从图像生成有趣的图案,因此引起了轰动。 本章将涵盖以下主要主题:

  • 动机 — 心理幻觉
  • 计算机视觉中的算法异同
  • 通过可视化 CNN 的内部层来了解 CNN 所学的知识
  • DeepDream 算法以及如何创建自己的梦境

就像前面的章节一样,我们将结合使用概念知识和直观的实际操作示例。 您可以在 GitHub 存储库中的Chapter 9文件夹中快速阅读本章的代码。 可以根据需要参考本章。

介绍

在详细介绍神经 DeepDream 之前,让我们看一下人类所经历的类似行为。 您是否曾经尝试过寻找云中的形状,电视机中的抖动和嘈杂信号,甚至看过一张被烤面包烤成的面孔?

Pareidolia 是一种心理现象,使我们看到随机刺激中的模式。 人类倾向于感知实际上不存在的面孔或风格的趋势。 这通常导致将人的特征分配给对象。 请注意,看到不存在的模式(假阳性)相对于看不到存在的模式(假阴性)对进化结果的重要性。 例如,看到没有狮子的狮子很少会致命。 但是,没有看到有一只的掠食性狮子,那当然是致命的。

pareidolia 的神经学基础主要位于大脑深处的大脑颞叶区域,称为梭状回,在此区域,人类和其他动物的神经元专用于识别面部和其他物体。

计算机视觉中的算法异同

计算机视觉的主要任务之一是特别是对象检测和面部检测。 有许多具有面部检测功能的电子设备在后台运行此类算法并检测面部。 那么,当我们在这些软件的前面放置诱发 Pareidolia 的物体时会发生什么呢? 有时,这些软件解释面孔的方式与我们完全相同。 有时它可能与我们一致,有时它会引起我们全新的面貌。

在使用人工神经网络构建的对象识别系统的情况下,更高级别的特征/层对应于更易识别的特征,例如面部或物体。 增强这些特征可以带出计算机的视觉效果。 这些反映了网络以前看到的训练图像集。 让我们以 Inception 网络为例,让它预测一些诱发 Pareidolia 的图像中看到的物体。 让我们在下面的照片中拍摄这些三色堇花。 对我而言,这些花有时看起来像蝴蝶,有时又像愤怒的人,留着浓密的胡须的脸:

让我们看看 Inception 模型在其中的表现。 我们将使用在 ImageNet 数据上训练的预训练的 Inception 网络模型。 要加载模型,请使用以下代码:

from keras.applications import inception_v3
from keras import backend as K
from keras.applications.imagenet_utils import decode_predictions
from keras.preprocessing import image
K.set_learning_phase(0)

model = inception_v3.InceptionV3(weights='imagenet',include_top=True)

要读取图像文件并将其转换为一个图像的数据批,这是 Inception 网络模型的predict函数的预期输入,我们使用以下函数:

def preprocess_image(image_path):
    img = image.load_img(image_path)
    img = image.img_to_array(img)
    #convert single image to a batch with 1 image
    img = np.expand_dims(img, axis=0) 
    img = inception_v3.preprocess_input(img)
    return img

现在,让我们使用前面的方法预处理输入图像并预测模型看到的对象。 我们将使用modeld.predict方法来获取 ImageNet 中所有 1,000 个类的预测类概率。 要将此概率数组转换为按概率得分的降序排列的实类标签,我们使用keras中的decode_predictions方法。 可在此处找到所有 1,000 个 ImageNet 类或同义词集的列表。 请注意,三色堇花不在训练模型的已知类集中:

img = preprocess_image(base_image_path)
preds = model.predict(img)
for n, label, prob in decode_predictions(preds)[0]:
    print (label, prob)

的预测。 最高预测的类别都不具有很大的概率,这是可以预期的,因为模型之前没有看到过这种特殊的花朵:

bee 0.022255851
earthstar 0.018780833
sulphur_butterfly 0.015787734
daisy 0.013633176
cabbage_butterfly 0.012270376

在上一张照片中,模型找到蜜蜂。 好吧,这不是一个不好的猜测。 如您所见,在黄色的花朵中,中间的黑色/棕色阴影的下半部分确实像蜜蜂。 此外,它还会看到一些黄色和白色的蝴蝶,如卷心菜蝴蝶,就像我们人类一眼就能看到的。 下图显示了这些已识别对象/类的实际图像。 显然,此输入激活了该网络中的某些特征检测器隐藏层。 也许检测昆虫/鸟类翅膀的过滤器与一些与颜色相关的过滤器一起被激活,以得出上述结论:

ImageNet 架构及其中的特征图数量很多。 让我们假设一下,我们知道可以检测这些机翼的特征映射层。 现在,给定输入图像,我们可以从这一层提取特征。 我们可以更改输入图像,以使来自该层的激活增加吗? 这意味着我们必须修改输入图像,以便在输入图像中看到更多类似机翼的物体,即使它们不在那里。 最终的图像将像梦一样,到处都是蝴蝶。 这正是 DeepDream 中完成的工作。

现在,让我们看一下 Inception 网络中的​​一些特征图。 要了解卷积模型学到的知识,我们可以尝试可视化卷积过滤器。

可视化特征图

可视化 CNN 模型涉及在给定一定输入的情况下,查看网络中各种卷积和池化层输出的中间层特征图。 这样就可以了解网络如何处理输入以及如何分层提取各种图像特征。 所有特征图都具有三个维度:宽度,高度和深度(通道)。 我们将尝试将它们可视化为 InceptionV3 模型。

让我们为拉布拉多犬拍摄以下照片,并尝试形象化各种特征图。 由于 InceptionV3 模型具有很深的深度,因此我们将仅可视化一些层:

首先,让我们创建一个模型以获取输入图像并输出所有内部激活层。 InceptionV3 中的激活层称为activation_i。 因此,我们可以从加载的 Inception 模型中过滤掉激活层,如以下代码所示:

activation_layers = [ layer.output for layer in model.layers if 
                      layer.name.startswith("activation_")]

layer_names = [ layer.name for layer in model.layers if 
                layer.name.startswith("activation_")]

现在,让我们创建一个模型,该模型获取输入图像并将所有上述激活层特征作为列表输出,如以下代码所示:

from keras.models import Model
activation_model = Model(inputs=model.input, outputs=activation_layers)

现在,要获得输出激活,我们可以使用predict函数。 我们必须使用与先前定义的相同的预处理函数对图像进行预处理,然后再将其提供给 Inception 网络:

img = preprocess_image(base_image_path)
activations = activation_model.predict(img)

我们可以绘制这些先前的激活。 一个激活层中的所有过滤器/特征图都可以绘制在网格中。 因此,根据层中滤镜的数量,我们将图像网格定义为 NumPy 数组,如以下代码所示(以下代码的某些部分来自这里):

import matplotlib.pyplot as plt

images_per_row = 8
idx = 1 #activation layer index

layer_activation=activations[idx]
# This is the number of features in the feature map
n_features = layer_activation.shape[-1]
# The feature map has shape (1, size1, size2, n_features)
r = layer_activation.shape[1]
c = layer_activation.shape[2]

# We will tile the activation channels in this matrix
n_cols = n_features // images_per_row
display_grid = np.zeros((r * n_cols, images_per_row * c))
print(display_grid.shape)

现在,我们将遍历激活层中的所有特征映射,并将缩放后的输出放到网格中,如以下代码所示:

# We'll tile each filter into this big horizontal grid
    for col in range(n_cols):
        for row in range(images_per_row):
            channel_image = layer_activation[0,:, :, col *   
                                             images_per_row + row]
            # Post-process the feature to make it visually palatable
            channel_image -= channel_image.mean()
            channel_image /= channel_image.std()
            channel_image *= 64
            channel_image += 128
            channel_image = np.clip(channel_image, 0, 
                                    255).astype('uint8')
            display_grid[col * r : (col + 1) * r,
            row * c : (row + 1) * c] = channel_image
    # Display the grid
    scale = 1\. / r
    plt.figure(figsize=(scale * display_grid.shape[1],
               scale * display_grid.shape[0]))
    plt.title(layer_names[idx]+" #filters="+str(n_features))
    plt.grid(False)
    plt.imshow(display_grid, aspect='auto', cmap='viridis')        

以下是各层的输出:

前面的前两个激活层充当各种边缘检测器的集合。 这些激活保留了初始图片中几乎所有的信息。

让我们看下面的屏幕快照,它显示了网络中间的一层。 在这里,它开始识别更高级别的特征,例如鼻子,眼睛,舌头,嘴巴等:

随着我们的上移,地物图在视觉上的解释也越来越少。 较高层的激活会携带有关所看到的特定输入的最少信息,以及有关图像目标类别(在此情况下为狗)的更多信息。

可视化 InceptionV3 学习的过滤器的另一种方法是显示每个过滤器输出最大激活值的可视模式。 这可以通过输入空间中的梯度上升来完成。 基本上,通过使用图像空间中的梯度上升进行优化,找到使感兴趣的活动(层中神经元的激活)最大化的输入图像。 最终的输入图像将是所选过滤器最大程度地响应的输入图像。

每个激活层都有许多特征图。 以下代码演示了如何从最后一个激活层提取单个特征图。 这个激活值实际上是我们要最大化的损失:

layer_name = 'activation_94'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

要相对于此loss函数计算输入图像的梯度,我们可以如下使用keras后端梯度函数:

grads = K.gradients(loss, model.input)[0]
# We add 1e-5 before dividing so as to avoid accidentally dividing by 
# 0.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

因此,给定一个激活层和一个可能是随机噪声的起始输入图像,我们可以使用上面的梯度计算应用梯度上升来获得特征图所表示的图案。 跟随generate_pattern函数执行相同的操作。 归一化输出模式,以便我们在图像矩阵中具有可行的 RGB 值,这是通过使用deprocess_image方法完成的。 以下代码是不言自明的,并具有内联注释来解释每一行:

def generate_pattern(layer_name, filter_index, size=150):
    # Build a loss function that maximizes the activation
    # of the nth filter of the layer considered.
    layer_output = model.get_layer(layer_name).output
    loss = K.mean(layer_output[:, :, :, filter_index])
    # Compute the gradient of the input picture wrt this loss
    grads = K.gradients(loss, model.input)[0]
    # Normalization trick: we normalize the gradient
    grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
    # This function returns the loss and grads given the input picture
    iterate = K.function([model.input], [loss, grads])
    # We start from a gray image with some noise
    input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
    # Run gradient ascent for 40 steps
    step = 1.
    for i in range(40):
        loss_value, grads_value = iterate([input_img_data])
        input_img_data += grads_value * step
        img = input_img_data[0]
    return deprocess_image(img)

def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1
    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)
    # convert to RGB array
    x *= 255
    x = np.clip(x, 0, 255).astype('uint8')
    return x

以下屏幕截图是某些过滤器层的可视化。 第一层具有各种类型的点图案:

DeepDream

DeepDream 是一种艺术性的图像修改技术,它利用了以同名电影命名的深层 CNN 代码 Inception 所学习的表示形式。 我们可以拍摄任何输入图像并对其进行处理,以生成令人毛骨悚然的图片,其中充满了算法上的拟南芥伪像,鸟羽毛,狗似的面孔,狗眼-这是 DeepDream 修道院在 ImageNet 上接受过训练的事实,狗在这里繁殖,鸟类种类过多。

DeepDream 算法与使用梯度上升的 ConvNet 过滤器可视化技术几乎相同,不同之处在于:

  • 在 DeepDream 中,最大程度地激活了整个层,而在可视化中,只最大化了一个特定的过滤器,因此将大量特征图的可视化混合在一起
  • 我们不是从随机噪声输入开始,而是从现有图像开始; 因此,最终的可视化效果将修改先前存在的视觉模式,从而以某种艺术性的方式扭曲图像的元素
  • 输入图像以不同的比例(称为八度)进行处理,从而提高了可视化效果的质量

现在,让我们修改上一部分中的可视化代码。 首先,我们必须更改loss函数和梯度计算。 以下是执行相同操作的代码:

layer_name = 'activation_41'
activation = model.get_layer(layer_name).output

# We avoid border artifacts by only involving non-border pixels in the #loss.
scaling = K.prod(K.cast(K.shape(activation), 'float32'))
loss = K.sum(K.square(activation[:, 2: -2, 2: -2, :])) / scaling

# This tensor holds our generated image
dream = model.input

# Compute the gradients of the dream with regard to the loss.
grads = K.gradients(loss, dream)[0]

# Normalize gradients.
grads /= K.maximum(K.mean(K.abs(grads)), 1e-7)

iterate_grad_ac_step = K.function([dream], [loss, grads])

第二个变化是输入图像,因此我们必须提供要在其上运行 DeepDream 算法的输入图像。 第三个变化是,我们没有在单个图像上应用梯度强调,而是创建了各种比例的输入图像并应用了梯度强调,如以下代码所示:

num_octave = 4 # Number of scales at which to run gradient ascent
octave_scale = 1.4 # Size ratio between scales
iterations = 20 # Number of ascent steps per scale

# If our loss gets larger than 10, 
# we will interrupt the gradient ascent process, to avoid ugly  
# artifacts
max_loss = 20.

base_image_path = 'Path to Image You Want to Use'
# Load the image into a Numpy array
img = preprocess_image(base_image_path)
print(img.shape)
# We prepare a list of shape tuples
# defining the different scales at which we will run gradient ascent
original_shape = img.shape[1:3]
successive_shapes = [original_shape]
for i in range(1, num_octave):
    shape = tuple([int(dim / (octave_scale ** i)) for dim in  
                   original_shape])
    successive_shapes.append(shape)

# Reverse list of shapes, so that they are in increasing order
successive_shapes = successive_shapes[::-1]

# Resize the Numpy array of the image to our smallest scale
original_img = np.copy(img)
shrunk_original_img = resize_img(img, successive_shapes[0])
print(successive_shapes)

#Example Octaves for image of shape (1318, 1977)
[(480, 720), (672, 1008), (941, 1412), (1318, 1977)]

以下代码显示了 DeepDream 算法的一些工具函数。 函数deprocess_image基本上是 InceptionV3 模型的预处理输入的逆运算符:

import scipy

def deprocess_image(x):
    # Util function to convert a tensor into a valid image.
    if K.image_data_format() == 'channels_first':
        x = x.reshape((3, x.shape[2], x.shape[3]))
        x = x.transpose((1, 2, 0))
    else:
        x = x.reshape((x.shape[1], x.shape[2], 3))
    x /= 2.
    x += 0.5
    x *= 255.
    x = np.clip(x, 0, 255).astype('uint8')
    return x

def resize_img(img, size):
    img = np.copy(img)
    factors = (1,
               float(size[0]) / img.shape[1],
               float(size[1]) / img.shape[2],
               1)
    return scipy.ndimage.zoom(img, factors, order=1)

def save_img(img, fname):
    pil_img = deprocess_image(np.copy(img))
    scipy.misc. (fname, pil_img)

在每个连续的音阶上,从最小到最大的八度音程,我们都执行梯度上升以使该音阶上的先前定义的损耗最大化。 每次梯度爬升后,生成的图像将放大 40%。 在每个升级步骤中,一些图像细节都会丢失; 但是我们可以通过添加丢失的信息来恢复它,因为我们知道该比例的原始图像:

MAX_ITRN = 20
MAX_LOSS = 20
learning_rate = 0.01

for shape in successive_shapes:
    print('Processing image shape', shape)
    img = resize_img(img, shape)
    img = gradient_ascent(img,
                          iterations=MAX_ITRN,
                          step=learning_rate,
                          max_loss=MAX_LOSS)
    upscaled_shrunk_original_img = resize_img(shrunk_original_img, 
                                              shape)
    same_size_original = resize_img(original_img, shape)
    lost_detail = same_size_original - upscaled_shrunk_original_img
    print('adding lost details', lost_detail.shape)
    img += lost_detail
    shrunk_original_img = resize_img(original_img, shape)
    save_img(img, fname='dream_at_scale_' + str(shape) + '.png')

save_img(img, fname='final_dream.png')

示例

以下是 DeepDream 输出的一些示例:

  • 在激活层 41 上运行梯度重音。这是我们之前看到的同一层,带有狗图像输入。 在下面的照片中,您可以看到一些动物从云层和蓝天中冒出来:

  • 在激活层 45 上运行梯度重音。在下图中,您可以看到山上出现了一些类似狗的动物面孔:

  • 在激活层 50 上运行梯度。在下面的照片中,您可以看到在蓝天白云下某些特殊的类似叶的图案梦:

生成这些梦境的原始图像在代码存储库中共享。

总结

在本章中,我们学习了计算机视觉中的算法稀疏。 我们已经解释了如何通过各种可视化技术来解释 CNN 模型,例如基于前向通过的激活可视化,基于梯度上升的过滤器可视化。 最后,我们介绍了 DeepDream 算法,该算法再次是对基于梯度上升的可视化技术的略微修改。 DeepDream 算法是将迁移学习应用于计算机视觉或图像处理任务的示例。

在下一章中,我们将看到更多类似的应用,它们将重点放在风格转换上。

风格迁移

绘画需要特殊技能,只有少数人已经掌握。 绘画呈现出内容和风格的复杂相互作用。 另一方面,照片是视角和光线的结合。 当两者结合时,结果是惊人的和令人惊讶的。 该过程称为艺术风格迁移。 以下是一个示例,其中输入图像是德国图宾根的 Neckarfront,风格图像是梵高着名的画作《星空》。 有趣,不是吗? 看一下以下图像:

左图:描绘德国蒂宾根 Neckarfront 的原始照片。 梵高的《星空》)。 来源:《一种艺术风格的神经算法》(Gatys 等人,arXiv:1508.06576v2)

如果您仔细查看前面的图像,则右侧的绘画风格图像似乎已经从左侧的照片中拾取了内容。 绘画的风格,颜色和笔触风格产生了最终结果。 令人着迷的结果是 Gatys 等人在论文《一种用于艺术风格的神经算法》中提出的一种迁移学习算法的结果。 我们将从实现的角度讨论本文的复杂性,并了解如何自己执行此技术。

在本章中,我们将专注于利用深度学习和传递学习来构建神经风格传递系统。 本章重点关注的领域包括:

  • 了解神经风格转换
  • 图像预处理方法
  • 架构损失函数
  • 构造自定义优化器
  • 风格迁移实战

我们将涵盖有关神经风格迁移,损失函数和优化的理论概念。 除此之外,我们将使用动手方法来实现我们自己的神经风格转换模型。 本章的代码可在 GitHub 存储库的第 10 章文件夹中快速参考。 请根据需要参考本章。

了解神经风格转换

神经风格迁移是将参考图像的风格应用于特定目标图像的过程,以使目标图像的原始内容保持不变。 在这里,风格定义为参考图像中存在的颜色,图案和纹理,而内容定义为图像的整体结构和更高层次的组件。

在此,主要目的是保留原始目标图像的内容,同时在目标图像上叠加或采用参考图像的风格。 为了从数学上定义这个概念,请考虑三个图像:原始内容(表示为c),参考风格(表示为s)和生成的图像(表示为g)。 我们需要一种方法来衡量在内容方面, cg不同的图像的程度。 同样,就输出的风格特征而言,与风格图像相比,输出图像应具有较小的差异。 形式上,神经风格转换的目标函数可以表述为:

此处,αβ是用于控制内容和风格成分对整体损失的影响的权重。 此描述可以进一步简化,并表示如下:

在这里,我们可以根据前面的公式定义以下组件:

  • dist是规范函数; 例如,L2 规范距离
  • style(...)是用于为参考风格和生成的图像计算风格表示的函数
  • content(...)是一个函数,可为原始内容和生成的图像计算内容的表示形式
  • I[c]I[s]I[g],并分别生成图像

因此,最小化此损失会导致风格(I[g])接近风格(I[s]),以及内容(I[g])接近内容(I[c])。 这有助于我们达成有效的风格转换所需的规定。 我们将尝试最小化的损失函数包括三个部分: 即将讨论的内容损失风格损失总变化损失。 关键思想或目标是保留原始目标图像的内容,同时在目标图像上叠加或采用参考图像的风格。 此外,在神经风格转换的背景下,您应该记住以下几点:

  • 风格可以定义为参考图像中存在的调色板,特定图案和纹理
  • 内容可以定义为原始目标图像的整体结构和更高级别的组件

到目前为止,我们知道深度学习对于计算机视觉的真正威力在于利用诸如深层卷积神经网络CNN)模型之类的模型,这些模型可用于在构建这些损失函数时提取正确的图像表示。 在本章中,我们将使用迁移学习的原理来构建用于神经风格迁移的系统,以提取最佳特征。 在前面的章节中,我们已经讨论了与计算机视觉相关的任务的预训练模型。 在本章中,我们将再次使用流行的 VGG-16 模型作为特征提取器。 执行神经风格转换的主要步骤如下所示:

  • 利用 VGG-16 帮助计算风格,内容和生成图像的层激活
  • 使用这些激活来定义前面提到的特定损失函数
  • 最后,使用梯度下降来最大程度地减少总损耗

如果您想更深入地研究神经风格转换背后的核心原理和理论概念,建议您阅读以下文章:

  • A Neural Algorithm of Artistic Style, by Leon A. Gatys, Alexander S. Ecker, and Matthias Bethge (https://arxiv.org/abs/1508.06576)
  • Perceptual Losses for Real-Time Style Transfer and Super-Resolution, by Justin Johnson, Alexandre Alahi, and Li Fei-Fei (https://arxiv.org/abs/1603.08155)

图像预处理方法

在这种情况下,实现此类网络的第一步也是最重要的一步是对数据或图像进行预处理。 以下代码段显示了一些用于对图像进行大小和通道调整的快速工具:

import numpy as np
from keras.applications import vgg16
from keras.preprocessing.image import load_img, img_to_array

def preprocess_image(image_path, height=None, width=None):
    height = 400 if not height else height
    width = width if width else int(width * height / height)
    img = load_img(image_path, target_size=(height, width))
    img = img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = vgg16.preprocess_input(img)
    return img

def deprocess_image(x):
    # Remove zero-center by mean pixel
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68
    # 'BGR'->'RGB'
    x = x[:, :, ::-1]
    x = np.clip(x, 0, 255).astype('uint8')
    return x

当我们要编写自定义损失函数和操作例程时,我们将需要定义某些占位符。 请记住,keras是一个利用张量操作后端(例如tensorflowtheanoCNTK)执行繁重工作的高级库。 因此,这些占位符提供了高级抽象来与基础张量对象一起使用。 以下代码段为风格,内容和生成的图像以及神经网络的输入张量准备了占位符:

from keras import backend as K

# This is the path to the image you want to transform.
TARGET_IMG = 'lotr.jpg'
# This is the path to the style image.
REFERENCE_STYLE_IMG = 'pattern1.jpg'

width, height = load_img(TARGET_IMG).size
img_height = 480
img_width = int(width * img_height / height)

target_image = K.constant(preprocess_image(TARGET_IMG, 
                          height=img_height,  
                          width=img_width))
style_image = K.constant(preprocess_image(REFERENCE_STYLE_IMG, 
                         height=img_height,  
                         width=img_width))

# Placeholder for our generated image
generated_image = K.placeholder((1, img_height, img_width, 3))

# Combine the 3 images into a single batch
input_tensor = K.concatenate([target_image,
                              style_image,
                              generated_image], axis=0)

我们将像前几章一样加载预训练的 VGG-16 模型。 也就是说,没有顶部的全连接层。 唯一的区别是我们将为模型输入提供输入张量的大小尺寸。 以下代码段有助于我们构建预训练模型:

model = vgg16.VGG16(input_tensor=input_tensor,
                    weights='imagenet',
                    include_top=False)

构建损失函数

如背景小节所述,神经风格迁移的问题围绕内容和风格的损失函数。 在本小节中,我们将讨论和定义所需的损失函数。

内容损失

在任何基于 CNN 的模型中,来自顶层的激活都包含更多的全局和抽象信息(例如,诸如人脸之类的高级结构),而底层将包含局部信息(例如,诸如眼睛,鼻子, 边缘和角落)。 我们希望利用 CNN 的顶层来捕获图像内容的正确表示。 因此,对于内容损失,考虑到我们将使用预训练的 VGG-16 模型,我们可以将损失函数定义为通过计算得出的顶层激活(给出特征表示)之间的 L2 范数(缩放和平方的欧几里得距离)。 目标图像,以及在生成的图像上计算的同一层的激活。 假设我们通常从 CNN 的顶层获得与图像内容相关的特征表示,则预期生成的图像看起来与基本目标图像相似。 以下代码段显示了计算内容损失的函数:

def content_loss(base, combination):
    return K.sum(K.square(combination - base))

风格损失

关于神经风格迁移的原始论文,《一种由神经科学风格的神经算法》,由 Gatys 等人撰写。利用 CNN 中的多个卷积层(而不是一个)来从参考风格图像中提取有意义的风格和表示,捕获与外观或风格有关的信息。 不论图像内容如何,​​在所有空间尺度上都可以工作。风格表示可计算 CNN 不同层中不同特征之间的相关性。

忠于原始论文,我们将利用 Gram 矩阵并在由卷积层生成的特征表示上进行计算。 Gram 矩阵计算在任何给定的卷积层中生成的特征图之间的内积。 内积项与相应特征集的协方差成正比,因此可以捕获趋于一起激活的层的特征之间的相关性。 这些特征相关性有助于捕获特定空间比例的图案的相关汇总统计信息,这些统计信息与风格,纹理和外观相对应,而不与图像中存在的组件和对象相对应。

因此,风格损失定义为参考风格的 Gram 矩阵与生成的图像之间的差异的按比例缩放的 Frobenius 范数(矩阵上的欧几里得范数)。 最小化此损失有助于确保参考风格图像中不同空间比例下找到的纹理在生成的图像中相似。 因此,以下代码段基于 Gram 矩阵计算定义了风格损失函数:

def style_loss(style, combination, height, width):

    def build_gram_matrix(x):
        features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
        gram_matrix = K.dot(features, K.transpose(features))
        return gram_matrix

    S = build_gram_matrix(style)
    C = build_gram_matrix(combination)
    channels = 3
    size = height * width
    return K.sum(K.square(S - C))/(4\. * (channels ** 2) * (size ** 2))

总变化损失

据观察,仅减少风格和内容损失的优化会导致高度像素化和嘈杂的输出。 为了解决这个问题,引入了总变化损失。 总变化损失正则化损失相似。 引入此方法是为了确保生成的图像中的空间连续性和平滑性,以避免产生嘈杂的像素化结果。 在函数中的定义如下:

def total_variation_loss(x):
    a = K.square(
        x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width 
          - 1, :])
    b = K.square(
        x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 
          1, 1:, :])
    return K.sum(K.pow(a + b, 1.25))

总损失函数

在定义了用于神经风格传递的整体损失函数的组成部分之后,下一步就是将这些构造块缝合在一起。 由于内容和风格信息是由 CNN 在网络中的不同深度捕获的,因此我们需要针对每种损失类型在适当的层上应用和计算损失。 我们将对卷积层进行 1 到 5 层的风格损失,并为每一层设置适当的权重。

这是构建整体损失函数的代码片段:

# weights for the weighted average loss function
content_weight = 0.05
total_variation_weight = 1e-4

content_layer = 'block4_conv2'
style_layers =  ['block1_conv2', 'block2_conv2',   
                 'block3_conv3','block4_conv3', 'block5_conv3'] 
style_weights = [0.1, 0.15, 0.2, 0.25, 0.3]

# initialize total loss
loss = K.variable(0.)

# add content loss
layer_features = layers[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss += content_weight * content_loss(target_image_features,
                                      combination_features)

# add style loss
for layer_name, sw in zip(style_layers, style_weights):
    layer_features = layers[layer_name]
    style_reference_features = layer_features[1, :, :, :]
    combination_features = layer_features[2, :, :, :]
    sl = style_loss(style_reference_features, combination_features, 
                    height=img_height, width=img_width)
    loss += (sl*sw)

# add total variation loss
loss += total_variation_weight * total_variation_loss(generated_image)

构造自定义优化器

目的是在优化算法的帮助下迭代地使总损失最小化。 Gatys 等人的论文中,使用 L-BFGS 算法进行了优化,该算法是基于准牛顿法的一种优化算法,通常用于解决非线性优化问题和参数估计。 该方法通常比标准梯度下降收敛更快。

SciPy 在scipy.optimize.fmin_l_bfgs_b()中提供了一个实现。 但是,局限性包括该函数仅适用于平面一维向量,这与我们正在处理的三维图像矩阵不同,并且损失函数和梯度的值需要作为两个单独的函数传递。 我们基于模式构建一个Evaluator类,然后由keras创建者 FrançoisChollet 创建,以一次计算损失和梯度值,而不是独立和单独的计算。 这将在首次调用时返回损耗值,并将缓存下一次调用的梯度。 因此,这将比独立计算两者更为有效。 以下代码段定义了Evaluator类:

class Evaluator(object):

    def __init__(self, height=None, width=None):
        self.loss_value = None
        self.grads_values = None
        self.height = height
        self.width = width

    def loss(self, x):
        assert self.loss_value is None
        x = x.reshape((1, self.height, self.width, 3))
        outs = fetch_loss_and_grads([x])
        loss_value = outs[0]
        grad_values = outs[1].flatten().astype('float64')
        self.loss_value = loss_value
        self.grad_values = grad_values
        return self.loss_value

    def grads(self, x):
        assert self.loss_value is not None
        grad_values = np.copy(self.grad_values)
        self.loss_value = None
        self.grad_values = None
        return grad_values

evaluator = Evaluator(height=img_height, width=img_width)

风格迁移实战

难题的最后一步是使用所有构建块并在操作中执行风格转换! 可以从数据目录中获取艺术/风格和内容图像,以供参考。 以下代码片段概述了如何评估损耗和梯度。 我们还按规律的间隔/迭代(510等)写回输出,以了解神经风格迁移的过程如何在经过一定的迭代次数后考虑的图像转换图像,如以下代码段所示:

from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
from imageio import imwrite
import time

result_prefix = 'st_res_'+TARGET_IMG.split('.')[0]
iterations = 20

# Run scipy-based optimization (L-BFGS) over the pixels of the 
# generated image
# so as to minimize the neural style loss.
# This is our initial state: the target image.
# Note that `scipy.optimize.fmin_l_bfgs_b` can only process flat 
# vectors.
x = preprocess_image(TARGET_IMG, height=img_height, width=img_width)
x = x.flatten()

for i in range(iterations):
    print('Start of iteration', (i+1))
    start_time = time.time()
    x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x,
                                     fprime=evaluator.grads, maxfun=20)
    print('Current loss value:', min_val)
    if (i+1) % 5 == 0 or i == 0:
        # Save current generated image only every 5 iterations
        img = x.copy().reshape((img_height, img_width, 3))
        img = deprocess_image(img)
        fname = result_prefix + '_iter%d.png' %(i+1)
        imwrite(fname, img)
        print('Image saved as', fname)
    end_time = time.time()
    print('Iteration %d completed in %ds' % (i+1, end_time - start_time))

到现在为止,必须非常明显的是,神经风格转换是一项计算量巨大的任务。 对于所考虑的图像集,在具有 8GB RAM 的 Intel i5 CPU 上,每次迭代花费了 500-1,000 秒(尽管在 i7 或 Xeon 处理器上要快得多!)。 以下代码段显示了我们在 AWS 的 p2.x 实例上使用 GPU 所获得的加速,每次迭代仅需 25 秒! 以下代码片段还显示了一些迭代的输出。 我们打印每次迭代的损失和时间,并在每五次迭代后保存生成的图像:

Start of iteration 1
Current loss value: 10028529000.0
Image saved as st_res_lotr_iter1.png
Iteration 1 completed in 28s
Start of iteration 2
Current loss value: 5671338500.0
Iteration 2 completed in 24s
Start of iteration 3
Current loss value: 4681865700.0
Iteration 3 completed in 25s
Start of iteration 4
Current loss value: 4249350400.0
.
.
.
Start of iteration 20
Current loss value: 3458219000.0
Image saved as st_res_lotr_iter20.png
Iteration 20 completed in 25s

现在,您将学习神经风格迁移模型如何考虑内容图像的风格迁移。 请记住,我们在某些迭代之后为每对风格和内容图像执行了检查点输出。 我们利用matplotlibskimage加载并了解我们系统执行的风格转换魔术!

我们将非常受欢迎的《指环王》电影中的以下图像用作我们的内容图像,并将基于花卉图案的精美艺术品用作我们的风格图像:

在以下代码段中,我们将在各种迭代之后加载生成的风格化图像:

from skimage import io
from glob import glob
from matplotlib import pyplot as plt

%matplotlib inline
content_image = io.imread('lotr.jpg')
style_image = io.imread('pattern1.jpg')

iter1 = io.imread('st_res_lotr_iter1.png')
iter5 = io.imread('st_res_lotr_iter5.png')
iter10 = io.imread('st_res_lotr_iter10.png')
iter15 = io.imread('st_res_lotr_iter15.png')
iter20 = io.imread('st_res_lotr_iter20.png')
fig = plt.figure(figsize = (15, 15))
ax1 = fig.add_subplot(6,3, 1)
ax1.imshow(content_image)
t1 = ax1.set_title('Original')

gen_images = [iter1,iter5, iter10, iter15, iter20]

for i, img in enumerate(gen_images):
    ax1 = fig.add_subplot(6,3,i+1)
    ax1.imshow(content_image)
    t1 = ax1.set_title('Iteration {}'.format(i+5))
plt.tight_layout()
fig.subplots_adjust(top=0.95)
t = fig.suptitle('LOTR Scene after Style Transfer')

以下是显示原始图像和每五次迭代后生成的风格图像的输出:

以下是高分辨率的最终风格图像。 您可以清楚地看到花卉图案的纹理和风格是如何在原始《指环王》电影图像中慢慢传播的,并赋予了其良好的复古外观:

让我们再举一个风格迁移示例。 下图包含我们的内容图像,即来自黑豹的著名虚构的城市瓦卡达。 风格图片是梵高非常受欢迎的画作《星空》! 我们将在风格传递系统中将它们用作输入图像:

以下是高分辨率的最终风格图像,显示在下面的图像中。 您可以清楚地看到风格绘画中的纹理,边缘,颜色和图案如何传播到城市内容图像中:

天空和架构物采用了与您在绘画中可以观察到的非常相似的形式,但是内容图像的整体结构得以保留。 令人着迷,不是吗? 现在用您自己感兴趣的图像尝试一下!

总结

本章介绍了深度学习领域中一种非常新颖的技术,它利用了深度学习的力量来创造艺术! 确实,数据科学既是一门艺术,也是正确使用数据的科学,而创新则是推动这一发展的事物。 我们介绍了神经风格迁移的核心概念,如何使用有效的损失函数来表示和表达问题,以及如何利用迁移学习的力量和像 VGG-16 这样的预训练模型来提取正确的特征表示。

计算机视觉领域不断发展,深度学习与迁移学习相结合为创新和构建新颖的应用打开了大门。 本章中的示例应帮助您了解该领域的广泛新颖性,并使您能够走出去并尝试新技术,模型和方法来构建诸如神经风格转换的系统! 随之而来的是有关图像标题和着色的更有趣,更复杂的案例研究。 敬请关注!