Python 迁移学习实用指南:6~11(2)https://developer.aliyun.com/article/1426854
让我们在此数据集上尝试 SVM,看看我们获得的最佳精度是多少:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC tv = TfidfVectorizer(use_idf=True, min_df=0.00005, max_df=1.0, ngram_range=(1, 1), stop_words = 'english', sublinear_tf=True) tv_train_features = tv.fit_transform(corpus) tv_test_features = tv.transform(test_corpus) clf = SVC(C=1,kernel='linear', random_state=1, gamma=0.01) svm=clf.fit(tv_train_features, labels) preds_test = svm.predict(tv_test_features) from sklearn.metrics import classification_report,accuracy_score,confusion_matrix print(classification_report(test_labels, preds_test)) print(confusion_matrix(test_labels, preds_test)) print(accuracy_score(test_labels, preds_test))
以下是 SVM 模型的结果。 我们已经对参数C
进行了调整,以便获得最佳的交叉验证精度:
precision recall f1-score support 0 0.86 0.89 0.87 1912 1 0.83 0.89 0.86 1534 2 0.75 0.78 0.76 1523 3 0.87 0.73 0.80 382 4 0.82 0.75 0.79 1027 5 0.85 0.76 0.80 940 avg / total 0.82 0.82 0.82 7318 0.82344902978956
因此,我们看到,在多类分类结果的情况下,本文的 CNN 模型也可以给出可比较的结果。 再次,和以前一样,现在也可以使用经过训练的模型来执行文本摘要。
可视化文档嵌入
在我们的文档 CNN 模型中,我们具有文档嵌入层。 让我们尝试可视化模型在这一层中学到的特征。 我们将首先获取测试集,并按如下方式计算文档嵌入:
doc_embeddings = newsgrp_model.get_document_model().predict(x_test) print(doc_embeddings.shape) (7318, 80)
我们为所有测试文档获得了 80 维嵌入向量。 为了可视化这些向量,我们将使用流行的 t-SNE 二维还原技术将向量投影到二维空间中,并绘制散点图,如下所示:
from utils import scatter_plot doc_proj = TSNE(n_components=2, random_state=42, ).fit_transform(doc_embeddings) f, ax, sc, txts = scatter_plot(doc_proj, np.array(test_labels))
前面代码的输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdhSmxeO-1681567330245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/84f83a45-f09c-4a0e-8da2-e760138193ea.png)]
散点图上的标签(0-5)代表六个类别。 如您所见,该模型学习了不错的嵌入,并且能够在 80 维空间中很好地分离出六个类。 我们可以将这些嵌入用于其他文本分析任务,例如信息检索或文本搜索。 给定一个查询文档,我们可以计算其密集嵌入,然后将其与整个语料库中的相似嵌入进行比较。 这可以帮助我们提高基于关键字的查询结果并提高检索表现。
总结
我们已经从自然语言处理,文本分类,文本摘要以及深度学习 CNN 模型在文本域中的应用中学习了一些概念。 我们已经看到,在大多数用例中,尤其是如果我们的训练数据较少时,默认的第一步就是以词嵌入为基础的迁移学习。 我们已经看到了如何将迁移学习应用于在巨大的 Amazon 产品评论数据集上学习的文本 CNN 模型,以对小型电影评论数据集(相关但不相同的领域)进行预测。
此外,我们在这里还学习了如何将学习到的 CNN 模型用于其他文本处理任务,例如将文档汇总和表示为密集向量,这些信息可以在信息检索系统中使用,以提高检索表现。
八、音频事件识别与分类
在前面的章节中,我们已经研究了一些非常有趣的案例研究,这些案例将迁移学习应用于实际问题。 图像和文本数据是我们先前已解决的两种非结构化数据形式。 我们已经展示了各种方法来应用迁移学习来获得更强大和更出色的模型,以及解决诸如缺少训练数据之类的约束。 在本章中,我们将解决识别和分类音频事件的新现实问题。
为音频数据创建预训练的深度学习模型是一个巨大的挑战,因为我们没有高效的预训练的视觉模型(例如 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 多个音频样本。 现在,基于metadata
和readme
文件的信息,我们可以创建一个类 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]))
这将创建以下内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSlMnJoL-1681567330245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/dd8a372e-8efa-4671-bb94-4831617ad446.png)]
现在让我们通过绘制它们的波形来形象化这些不同的音频源的外观。 通常,这将是每个音频样本的波形幅度图:
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()
创建的图将如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5HYDtKs-1681567330245)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/a40c9f46-fc4a-4011-81c8-82581345bdbc.png)]
您可以在上图中清楚地看到不同的音频数据样本及其源标签和相应的音频波形图。 这描绘了一些有趣的见解。 engine_idling
,jackhammer
和air_conditioner
等音源通常具有恒定的声音,不会随时间变化。 因此,您可以注意到波形中的振幅恒定。 siren
和car_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()
频谱图显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-veZzDF6i-1681567330246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/c309828b-41bc-450c-a72a-af65ae9e4d49.png)]
我们可以看到如何用频谱图将音频数据表示为很好的图像表示形式,这对于像卷积神经网络(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()
梅尔频谱图显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vZLdSTnS-1681567330246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/3d3057b8-e22d-424c-8c47-ce16823d1d67.png)]
我们可以看到,借助梅尔音阶,可以更容易地根据音频源来区分频谱图。 现在,让我们集中讨论下一节中将用作特征工程基础资源的一些特定视觉技术。 首先,让我们看一下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')
频谱图显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMTuYfex-1681567330246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/ece5b21e-0d53-4e50-9b34-5249ab3debe7.png)]
频谱图与该音频源的音频波形图一致。 音频的另一个有趣方面是,通常任何音频时间序列数据都可以分解为谐波和打击乐成分。 这些可以呈现任何音频样本的全新有趣的表示形式。 让我们获取这些组件并将它们绘制成频谱图:
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()
频谱图将显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Em9wxuZM-1681567330246)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/ed31ddc9-a4de-4ebe-8456-b06f50d3e897.png)]
您可以看到音频样本的两个不同成分显示为两个独特的声谱图,分别描述了谐波成分和打击乐成分。
音频数据的另一个非常有趣的描述是使用一个色谱图,该图显示了基于十二种不同音高类别(即{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()
色谱图将显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOH6Bi2Y-1681567330247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/9cd9e2ed-8641-450d-90ec-204ea51f95bb.png)]
随着时间的推移,我们可以清楚地看到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)
我们将遵循的特征工程策略有些复杂,但是我们将在此处尝试以简洁的方式对其进行说明。 我们已经看到我们的音频数据样本的长度不同。 但是,如果我们要构建一个强大的分类器,则每个样本的特征必须保持一致。 因此,我们将从每个音频文件中提取(固定长度的)音频子样本,并从每个这些子样本中提取特征。
我们将总共使用三种特征工程技术来构建三个特征表示图,这最终将为我们的每个音频子样本提供一个三维图像特征图。 下图描述了我们将采用的工作流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UzVKNVkB-1681567330247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/70041e60-68d3-4ebf-900d-20bf3d0972b4.png)]
这个想法来自 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')
特征图将显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-08ifWls1-1681567330247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/14342019-e837-4817-89d5-351de82d81f8.png)]
上图向我们展示了每个音频类别的一些示例特征图看起来是什么样的,并且显而易见的是,每个特征图都是三维图像。 现在,我们将这些基本特征保存到磁盘:
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")
这将创建以下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHhEc9GN-1681567330247)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/a164d6a2-685a-4ad5-b1fe-3af618612e17.png)]
我们可以看到模型在训练和验证之间的损失和准确率是相当一致的。 也许略有过拟合,但考虑到它们之间的差异很小,可以忽略不计。
评估分类器表现
从字面上看,现在该对我们的模型进行测试了。 我们将使用测试数据集对模型进行预测,然后根据基本事实标签对它们进行评估。 为此,我们首先需要使用以下代码片段获取测试数据对模型的预测,并从数字标签到实际文本标签进行反向映射:
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_shot
,jackhammer
和engine_idling
等。 似乎street_music
和children_playing
最麻烦。
混淆矩阵可以帮助我们了解最可能发生错误分类的地方,并帮助我们更好地理解这一点:
meu.display_confusion_matrix_pretty(true_labels=test_labels_categories, predicted_labels=prediction_labels_categories, classes=category_names)
矩阵将显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtNpZKdc-1681567330248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/ff28a958-6bcb-4ae9-84f8-8300bffd4aa7.png)]
从矩阵的对角线看,我们可以看到大多数模型预测都是正确的,这非常好。 关于错误分类,我们可以看到,属于street_music
,dog_bark
和children_playing
的许多样本彼此之间都被错误分类了,考虑到所有这些事件都是在公开场合和外部发生的,一种预期的他们有可能一起发生。 对于drilling
和jackhammer
也是一样。 幸运的是,gun_shot
和children_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)
可视化效果如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x7dFvfNg-1681567330248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/76c3a773-50a7-4a3a-a94e-82ffc1fbc73f.png)]
基于可视化,基于音频源,它们似乎是一致的,到目前为止,我们的流水线运行良好。 现在,我们为这些音频文件提取基本特征图:
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)
特征图将显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G1AU92QO-1681567330248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/2b20f0eb-7c5b-42d0-ba6d-a6be88b58247.png)]
根据我们在训练阶段观察到的图像,图像特征图看起来非常一致。 现在,我们可以利用我们的预测流水线来预测每种声音的音频源类别:
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
我们得出以下预测:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LpIHgYKC-1681567330248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/c409633e-d70e-481a-b49c-da5e05090126.png)]
看起来我们的模型能够正确识别所有这些音频样本。 我们鼓励您检查笔记本中的AudioIdentifier
类,以了解我们如何在后台实现预测流水线。 我们利用了在本章中学到的所有概念来构建此流水线。
总结
在本章中,我们研究了一个全新的问题和案例研究,涉及音频识别和分类。 涵盖了围绕音频数据和信号的概念,包括可视化和理解此数据类型的有效技术。
我们还研究了有效的特征工程技术,以及如何使用迁移学习从音频数据的图像表示中提取有效特征。 这向我们展示了迁移学习的希望,以及如何利用知识从一个领域(图像)迁移到另一个领域(音频),并建立一个非常强大且有效的分类器。 最后,我们建立了一个完整的端到端流水线,用于识别和分类音频数据的新样本。 请在网络上进一步检查带标注的音频的数据集,看看是否可以利用从此处学习的迁移学习中获得的概念来构建更大,更好的音频标识符和分类器。 请继续关注有关迁移学习的更多有趣示例和案例研究。
九、DeepDream
本章重点介绍了生成型深度学习的领域,这已成为真正的人工智能(AI)最前沿的核心思想之一。 我们将关注卷积神经网络(CNN)如何利用迁移学习来思考或可视化图像中的图案。 它们可以生成描述这些卷积网络思维甚至梦境方式之前从未见过的图像模式! DeepDream 于 2015 年由 Google 首次发布,由于深层网络开始从图像生成有趣的图案,因此引起了轰动。 本章将涵盖以下主要主题:
- 动机 — 心理幻觉
- 计算机视觉中的算法异同
- 通过可视化 CNN 的内部层来了解 CNN 所学的知识
- DeepDream 算法以及如何创建自己的梦境
就像前面的章节一样,我们将结合使用概念知识和直观的实际操作示例。 您可以在 GitHub 存储库中的Chapter 9
文件夹中快速阅读本章的代码。 可以根据需要参考本章。
介绍
在详细介绍神经 DeepDream 之前,让我们看一下人类所经历的类似行为。 您是否曾经尝试过寻找云中的形状,电视机中的抖动和嘈杂信号,甚至看过一张被烤面包烤成的面孔?
Pareidolia 是一种心理现象,使我们看到随机刺激中的模式。 人类倾向于感知实际上不存在的面孔或风格的趋势。 这通常导致将人的特征分配给对象。 请注意,看到不存在的模式(假阳性)相对于看不到存在的模式(假阴性)对进化结果的重要性。 例如,看到没有狮子的狮子很少会致命。 但是,没有看到有一只的掠食性狮子,那当然是致命的。
pareidolia 的神经学基础主要位于大脑深处的大脑颞叶区域,称为梭状回,在此区域,人类和其他动物的神经元专用于识别面部和其他物体。
计算机视觉中的算法异同
计算机视觉的主要任务之一是特别是对象检测和面部检测。 有许多具有面部检测功能的电子设备在后台运行此类算法并检测面部。 那么,当我们在这些软件的前面放置诱发 Pareidolia 的物体时会发生什么呢? 有时,这些软件解释面孔的方式与我们完全相同。 有时它可能与我们一致,有时它会引起我们全新的面貌。
在使用人工神经网络构建的对象识别系统的情况下,更高级别的特征/层对应于更易识别的特征,例如面部或物体。 增强这些特征可以带出计算机的视觉效果。 这些反映了网络以前看到的训练图像集。 让我们以 Inception 网络为例,让它预测一些诱发 Pareidolia 的图像中看到的物体。 让我们在下面的照片中拍摄这些三色堇花。 对我而言,这些花有时看起来像蝴蝶,有时又像愤怒的人,留着浓密的胡须的脸:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-no9G2RAt-1681567330248)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/cb28331b-c874-4ffc-bb59-a54e9a855452.jpg)]
让我们看看 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
在上一张照片中,模型找到蜜蜂。 好吧,这不是一个不好的猜测。 如您所见,在黄色的花朵中,中间的黑色/棕色阴影的下半部分确实像蜜蜂。 此外,它还会看到一些黄色和白色的蝴蝶,如硫和卷心菜蝴蝶,就像我们人类一眼就能看到的。 下图显示了这些已识别对象/类的实际图像。 显然,此输入激活了该网络中的某些特征检测器隐藏层。 也许检测昆虫/鸟类翅膀的过滤器与一些与颜色相关的过滤器一起被激活,以得出上述结论:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVAtRn8x-1681567330249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/8aafabe1-dc15-4785-a4ee-2d0abb44b08c.png)]
ImageNet 架构及其中的特征图数量很多。 让我们假设一下,我们知道可以检测这些机翼的特征映射层。 现在,给定输入图像,我们可以从这一层提取特征。 我们可以更改输入图像,以使来自该层的激活增加吗? 这意味着我们必须修改输入图像,以便在输入图像中看到更多类似机翼的物体,即使它们不在那里。 最终的图像将像梦一样,到处都是蝴蝶。 这正是 DeepDream 中完成的工作。
现在,让我们看一下 Inception 网络中的一些特征图。 要了解卷积模型学到的知识,我们可以尝试可视化卷积过滤器。
可视化特征图
可视化 CNN 模型涉及在给定一定输入的情况下,查看网络中各种卷积和池化层输出的中间层特征图。 这样就可以了解网络如何处理输入以及如何分层提取各种图像特征。 所有特征图都具有三个维度:宽度,高度和深度(通道)。 我们将尝试将它们可视化为 InceptionV3 模型。
让我们为拉布拉多犬拍摄以下照片,并尝试形象化各种特征图。 由于 InceptionV3 模型具有很深的深度,因此我们将仅可视化一些层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsySyd0K-1681567330249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/5a131554-b82b-40b6-8406-a0f8e1f1da7b.jpg)]
首先,让我们创建一个模型以获取输入图像并输出所有内部激活层。 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')
以下是各层的输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tlRJfUmC-1681567330249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/06ac1e5d-045f-416d-aea9-39cafdbf1e7a.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZysX5ZR5-1681567330249)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/66db5895-f2ec-4bb1-94b7-b5dad17f5604.png)]
前面的前两个激活层充当各种边缘检测器的集合。 这些激活保留了初始图片中几乎所有的信息。
让我们看下面的屏幕快照,它显示了网络中间的一层。 在这里,它开始识别更高级别的特征,例如鼻子,眼睛,舌头,嘴巴等:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpVuL4vL-1681567330250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/f11ab37a-cac5-4045-adb4-19e8ddaf6b2f.png)]
随着我们的上移,地物图在视觉上的解释也越来越少。 较高层的激活会携带有关所看到的特定输入的最少信息,以及有关图像目标类别(在此情况下为狗)的更多信息。
可视化 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
以下屏幕截图是某些过滤器层的可视化。 第一层具有各种类型的点图案:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UO8maSa4-1681567330250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/9b87a770-3ad0-498a-848e-7ab85919b52b.png)]
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 上运行梯度重音。这是我们之前看到的同一层,带有狗图像输入。 在下面的照片中,您可以看到一些动物从云层和蓝天中冒出来:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-el5o5fdW-1681567330250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/d521aea6-1fec-43fc-aca4-3b2550d09ca2.png)]
- 在激活层 45 上运行梯度重音。在下图中,您可以看到山上出现了一些类似狗的动物面孔:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFQ3Mk8q-1681567330250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/eff1a0b4-d5ea-4ee0-bc7d-9df87102d461.png)]
- 在激活层 50 上运行梯度。在下面的照片中,您可以看到在蓝天白云下某些特殊的类似叶的图案梦:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpUdZCaw-1681567330250)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/7e468aa3-1eb1-491a-8955-ee652dc45b57.png)]
生成这些梦境的原始图像在代码存储库中共享。
总结
在本章中,我们学习了计算机视觉中的算法稀疏。 我们已经解释了如何通过各种可视化技术来解释 CNN 模型,例如基于前向通过的激活可视化,基于梯度上升的过滤器可视化。 最后,我们介绍了 DeepDream 算法,该算法再次是对基于梯度上升的可视化技术的略微修改。 DeepDream 算法是将迁移学习应用于计算机视觉或图像处理任务的示例。
在下一章中,我们将看到更多类似的应用,它们将重点放在风格转换上。
风格迁移
绘画需要特殊技能,只有少数人已经掌握。 绘画呈现出内容和风格的复杂相互作用。 另一方面,照片是视角和光线的结合。 当两者结合时,结果是惊人的和令人惊讶的。 该过程称为艺术风格迁移。 以下是一个示例,其中输入图像是德国图宾根的 Neckarfront,风格图像是梵高着名的画作《星空》。 有趣,不是吗? 看一下以下图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SKx0Kns7-1681567330251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/794949a4-3e20-4ad0-8d38-060bc707b82a.png)]
左图:描绘德国蒂宾根 Neckarfront 的原始照片。 梵高的《星空》)。 来源:《一种艺术风格的神经算法》(Gatys 等人,arXiv:1508.06576v2)
如果您仔细查看前面的图像,则右侧的绘画风格图像似乎已经从左侧的照片中拾取了内容。 绘画的风格,颜色和笔触风格产生了最终结果。 令人着迷的结果是 Gatys 等人在论文《一种用于艺术风格的神经算法》中提出的一种迁移学习算法的结果。 我们将从实现的角度讨论本文的复杂性,并了解如何自己执行此技术。
在本章中,我们将专注于利用深度学习和传递学习来构建神经风格传递系统。 本章重点关注的领域包括:
- 了解神经风格转换
- 图像预处理方法
- 架构损失函数
- 构造自定义优化器
- 风格迁移实战
我们将涵盖有关神经风格迁移,损失函数和优化的理论概念。 除此之外,我们将使用动手方法来实现我们自己的神经风格转换模型。 本章的代码可在 GitHub 存储库的第 10 章文件夹中快速参考。 请根据需要参考本章。
了解神经风格转换
神经风格迁移是将参考图像的风格应用于特定目标图像的过程,以使目标图像的原始内容保持不变。 在这里,风格定义为参考图像中存在的颜色,图案和纹理,而内容定义为图像的整体结构和更高层次的组件。
在此,主要目的是保留原始目标图像的内容,同时在目标图像上叠加或采用参考图像的风格。 为了从数学上定义这个概念,请考虑三个图像:原始内容(表示为c
),参考风格(表示为s
)和生成的图像(表示为g
)。 我们需要一种方法来衡量在内容方面, c
和g
不同的图像的程度。 同样,就输出的风格特征而言,与风格图像相比,输出图像应具有较小的差异。 形式上,神经风格转换的目标函数可以表述为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WB3gQRG5-1681567330251)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-tl-py/img/f6f9d3f8-d00f-414c-bd48-83905e444b82.png)]
Python 迁移学习实用指南:6~11(4)https://developer.aliyun.com/article/1426856